Merge branch 'main' into gpui2

Marshall Bowers created

Change summary

.cargo/config.toml                                                                     |    4 
.gitignore                                                                             |    1 
Cargo.lock                                                                             |  362 
Cargo.toml                                                                             |    7 
Dockerfile                                                                             |    2 
Procfile                                                                               |    7 
README.md                                                                              |   19 
assets/icons/Icons/exit.svg                                                            |    3 
assets/icons/arrow_down_12.svg                                                         |   10 
assets/icons/arrow_down_16.svg                                                         |    3 
assets/icons/arrow_down_8.svg                                                          |   10 
assets/icons/arrow_left_12.svg                                                         |   10 
assets/icons/arrow_left_16.svg                                                         |    3 
assets/icons/arrow_left_8.svg                                                          |   10 
assets/icons/arrow_right_12.svg                                                        |   10 
assets/icons/arrow_right_16.svg                                                        |    3 
assets/icons/arrow_right_8.svg                                                         |   10 
assets/icons/arrow_up_12.svg                                                           |   10 
assets/icons/arrow_up_16.svg                                                           |    3 
assets/icons/arrow_up_8.svg                                                            |   10 
assets/icons/arrow_up_right.svg                                                        |    0 
assets/icons/assist_15.svg                                                             |    0 
assets/icons/backspace _12.svg                                                         |    3 
assets/icons/backspace _16.svg                                                         |    3 
assets/icons/backspace _8.svg                                                          |    3 
assets/icons/bolt.svg                                                                  |    0 
assets/icons/bolt_12.svg                                                               |    3 
assets/icons/bolt_16.svg                                                               |    3 
assets/icons/bolt_slash_12.svg                                                         |   10 
assets/icons/bolt_slash_16.svg                                                         |    3 
assets/icons/bolt_slash_8.svg                                                          |   10 
assets/icons/caret_down.svg                                                            |    0 
assets/icons/caret_down_12.svg                                                         |    3 
assets/icons/caret_down_16.svg                                                         |    3 
assets/icons/caret_down_8.svg                                                          |   10 
assets/icons/caret_left_12.svg                                                         |    3 
assets/icons/caret_left_16.svg                                                         |    3 
assets/icons/caret_left_8.svg                                                          |   10 
assets/icons/caret_right_12.svg                                                        |    3 
assets/icons/caret_right_16.svg                                                        |    3 
assets/icons/caret_right_8.svg                                                         |   10 
assets/icons/caret_up.svg                                                              |    0 
assets/icons/caret_up_12.svg                                                           |    3 
assets/icons/caret_up_16.svg                                                           |    3 
assets/icons/caret_up_8.svg                                                            |   10 
assets/icons/case_insensitive.svg                                                      |    0 
assets/icons/channel_hash.svg                                                          |    6 
assets/icons/check_12.svg                                                              |    3 
assets/icons/check_16.svg                                                              |    3 
assets/icons/check_8.svg                                                               |    3 
assets/icons/chevron_down_12.svg                                                       |    3 
assets/icons/chevron_down_16.svg                                                       |    3 
assets/icons/chevron_down_8.svg                                                        |    3 
assets/icons/chevron_left_12.svg                                                       |    3 
assets/icons/chevron_left_16.svg                                                       |    3 
assets/icons/chevron_left_8.svg                                                        |    3 
assets/icons/chevron_right_12.svg                                                      |    3 
assets/icons/chevron_right_16.svg                                                      |    3 
assets/icons/chevron_right_8.svg                                                       |    3 
assets/icons/chevron_up_12.svg                                                         |    3 
assets/icons/chevron_up_16.svg                                                         |    3 
assets/icons/chevron_up_8.svg                                                          |    3 
assets/icons/circle_check.svg                                                          |    0 
assets/icons/circle_check_12.svg                                                       |   10 
assets/icons/circle_check_8.svg                                                        |   10 
assets/icons/circle_info_12.svg                                                        |   10 
assets/icons/circle_info_16.svg                                                        |    3 
assets/icons/circle_info_8.svg                                                         |   10 
assets/icons/circle_up_12.svg                                                          |   10 
assets/icons/circle_up_16.svg                                                          |    3 
assets/icons/circle_up_8.svg                                                           |   10 
assets/icons/circle_x_mark_12.svg                                                      |   10 
assets/icons/circle_x_mark_16.svg                                                      |    3 
assets/icons/circle_x_mark_8.svg                                                       |   10 
assets/icons/cloud_12.svg                                                              |    3 
assets/icons/cloud_8.svg                                                               |    3 
assets/icons/cloud_slash_8.svg                                                         |   10 
assets/icons/copilot_16.svg                                                            |    4 
assets/icons/copilot_disabled.svg                                                      |    0 
assets/icons/copilot_error.svg                                                         |    0 
assets/icons/copilot_init.svg                                                          |    0 
assets/icons/copy.svg                                                                  |    5 
assets/icons/delete_12.svg                                                             |    3 
assets/icons/delete_16.svg                                                             |    3 
assets/icons/delete_8.svg                                                              |    3 
assets/icons/desktop.svg                                                               |    0 
assets/icons/disable_screen_sharing_12.svg                                             |    3 
assets/icons/disconnected.svg                                                          |    0 
assets/icons/dock_bottom_12.svg                                                        |   11 
assets/icons/dock_bottom_8.svg                                                         |   11 
assets/icons/dock_modal_12.svg                                                         |   11 
assets/icons/dock_modal_8.svg                                                          |   11 
assets/icons/dock_right_12.svg                                                         |   11 
assets/icons/dock_right_8.svg                                                          |   11 
assets/icons/download.svg                                                              |    0 
assets/icons/download_8.svg                                                            |   10 
assets/icons/ellipsis_14.svg                                                           |    3 
assets/icons/enable_screen_sharing_12.svg                                              |    3 
assets/icons/exit.svg                                                                  |   10 
assets/icons/external_link.svg                                                         |    0 
assets/icons/feedback_16.svg                                                           |    3 
assets/icons/file.svg                                                                  |    0 
assets/icons/file_12.svg                                                               |    4 
assets/icons/file_16.svg                                                               |    4 
assets/icons/file_8.svg                                                                |    4 
assets/icons/filter_12.svg                                                             |    3 
assets/icons/filter_14.svg                                                             |    6 
assets/icons/folder_tree_12.svg                                                        |   10 
assets/icons/folder_tree_16.svg                                                        |    3 
assets/icons/folder_tree_8.svg                                                         |   10 
assets/icons/git_diff_12.svg                                                           |    2 
assets/icons/git_diff_8.svg                                                            |    2 
assets/icons/github-copilot-dummy.svg                                                  |    0 
assets/icons/html.svg                                                                  |    5 
assets/icons/kebab.svg                                                                 |    5 
assets/icons/leave_12.svg                                                              |    3 
assets/icons/lock.svg                                                                  |    6 
assets/icons/lock_8.svg                                                                |    3 
assets/icons/magic-wand.svg                                                            |    0 
assets/icons/magnifying_glass_12.svg                                                   |   10 
assets/icons/magnifying_glass_16.svg                                                   |    3 
assets/icons/magnifying_glass_8.svg                                                    |   10 
assets/icons/match_case.svg                                                            |    3 
assets/icons/match_word.svg                                                            |    2 
assets/icons/maximize.svg                                                              |    6 
assets/icons/maximize_8.svg                                                            |    2 
assets/icons/menu.svg                                                                  |    0 
assets/icons/mic-mute.svg                                                              |    0 
assets/icons/mic.svg                                                                   |    0 
assets/icons/microphone.svg                                                            |    5 
assets/icons/minimize.svg                                                              |    6 
assets/icons/minimize_8.svg                                                            |    2 
assets/icons/plus.svg                                                                  |    9 
assets/icons/plus_12.svg                                                               |   10 
assets/icons/plus_16.svg                                                               |   10 
assets/icons/plus_8.svg                                                                |   10 
assets/icons/quote.svg                                                                 |    0 
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-left.svg                                                      |    8 
assets/icons/radix/caret-right.svg                                                     |    8 
assets/icons/radix/caret-sort.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/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/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/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/maximize.svg                                                        |    4 
assets/icons/radix/minimize.svg                                                        |    4 
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/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-moderate.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/robot_14.svg                                                              |    4 
assets/icons/screen.svg                                                                |    4 
assets/icons/select-all.svg                                                            |    2 
assets/icons/speaker-loud.svg                                                          |    0 
assets/icons/speaker-off.svg                                                           |    0 
assets/icons/speech_bubble_12.svg                                                      |    3 
assets/icons/split_12.svg                                                              |   12 
assets/icons/split_message.svg                                                         |    0 
assets/icons/stop_sharing.svg                                                          |    2 
assets/icons/success.svg                                                               |    4 
assets/icons/terminal_12.svg                                                           |   10 
assets/icons/terminal_16.svg                                                           |    3 
assets/icons/terminal_8.svg                                                            |   10 
assets/icons/triangle_exclamation_12.svg                                               |   10 
assets/icons/triangle_exclamation_16.svg                                               |    3 
assets/icons/triangle_exclamation_8.svg                                                |   10 
assets/icons/unlock_8.svg                                                              |   10 
assets/icons/user_circle_12.svg                                                        |   10 
assets/icons/user_circle_8.svg                                                         |   10 
assets/icons/user_group_12.svg                                                         |    3 
assets/icons/user_group_8.svg                                                          |   10 
assets/icons/user_plus_12.svg                                                          |    5 
assets/icons/user_plus_16.svg                                                          |    5 
assets/icons/user_plus_8.svg                                                           |   10 
assets/icons/version_control_branch_12.svg                                             |    1 
assets/icons/word_search.svg                                                           |    0 
assets/icons/word_search_12.svg                                                        |    4 
assets/icons/x_mark_12.svg                                                             |   10 
assets/icons/x_mark_16.svg                                                             |   10 
assets/icons/x_mark_8.svg                                                              |   10 
assets/icons/zed_x_copilot.svg                                                         |    0 
assets/keymaps/default.json                                                            |   71 
assets/keymaps/sublime_text.json                                                       |    1 
assets/keymaps/vim.json                                                                |  106 
assets/settings/default.json                                                           |   39 
crates/activity_indicator/src/activity_indicator.rs                                    |    4 
crates/ai/Cargo.toml                                                                   |   36 
crates/ai/src/ai.rs                                                                    |  294 
crates/ai/src/completion.rs                                                            |  212 
crates/ai/src/embedding.rs                                                             |   78 
crates/assistant/Cargo.toml                                                            |   48 
crates/assistant/README.zmd                                                            |    0 
crates/assistant/features.zmd                                                          |    0 
crates/assistant/src/assistant.rs                                                      |  113 
crates/assistant/src/assistant_panel.rs                                                |  714 
crates/assistant/src/assistant_settings.rs                                             |    0 
crates/assistant/src/codegen.rs                                                        |  663 
crates/assistant/src/prompts.rs                                                        |  404 
crates/assistant/src/streaming_diff.rs                                                 |    0 
crates/auto_update/src/auto_update.rs                                                  |    8 
crates/auto_update/src/update_notification.rs                                          |    2 
crates/call/src/call.rs                                                                |  113 
crates/call/src/participant.rs                                                         |    2 
crates/call/src/room.rs                                                                |   58 
crates/channel/Cargo.toml                                                              |    5 
crates/channel/src/channel.rs                                                          |   12 
crates/channel/src/channel_buffer.rs                                                   |  172 
crates/channel/src/channel_chat.rs                                                     |  540 
crates/channel/src/channel_store.rs                                                    |  457 
crates/channel/src/channel_store/channel_index.rs                                      |  238 
crates/channel/src/channel_store_tests.rs                                              |  251 
crates/client/Cargo.toml                                                               |    9 
crates/client/src/client.rs                                                            |   30 
crates/client/src/telemetry.rs                                                         |   70 
crates/client/src/test.rs                                                              |    9 
crates/client/src/user.rs                                                              |   43 
crates/collab/Cargo.toml                                                               |   16 
crates/collab/admin_api.conf                                                           |    4 
crates/collab/k8s/manifest.template.yml                                                |   60 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql                         |   44 
crates/collab/migrations/20230907114200_add_channel_messages.sql                       |   19 
crates/collab/migrations/20230925210437_add_channel_changes.sql                        |   19 
crates/collab/migrations/20230926102500_add_participant_index_to_room_participants.sql |    1 
crates/collab/src/api.rs                                                               |  217 
crates/collab/src/db.rs                                                                |   30 
crates/collab/src/db/ids.rs                                                            |   56 
crates/collab/src/db/queries.rs                                                        |    2 
crates/collab/src/db/queries/buffers.rs                                                |  408 
crates/collab/src/db/queries/channels.rs                                               |  486 
crates/collab/src/db/queries/contacts.rs                                               |    4 
crates/collab/src/db/queries/messages.rs                                               |  347 
crates/collab/src/db/queries/projects.rs                                               |   42 
crates/collab/src/db/queries/rooms.rs                                                  |  145 
crates/collab/src/db/queries/signups.rs                                                |  349 
crates/collab/src/db/queries/users.rs                                                  |   38 
crates/collab/src/db/tables.rs                                                         |    4 
crates/collab/src/db/tables/channel.rs                                                 |    8 
crates/collab/src/db/tables/channel_chat_participant.rs                                |   41 
crates/collab/src/db/tables/channel_message.rs                                         |   45 
crates/collab/src/db/tables/observed_buffer_edits.rs                                   |   43 
crates/collab/src/db/tables/observed_channel_messages.rs                               |   41 
crates/collab/src/db/tables/room_participant.rs                                        |   11 
crates/collab/src/db/tests.rs                                                          |   31 
crates/collab/src/db/tests/buffer_tests.rs                                             |  352 
crates/collab/src/db/tests/channel_tests.rs                                            |  875 
crates/collab/src/db/tests/db_tests.rs                                                 |  993 
crates/collab/src/db/tests/message_tests.rs                                            |  244 
crates/collab/src/rpc.rs                                                               |  591 
crates/collab/src/tests.rs                                                             |    2 
crates/collab/src/tests/channel_buffer_tests.rs                                        |  595 
crates/collab/src/tests/channel_message_tests.rs                                       |  360 
crates/collab/src/tests/channel_tests.rs                                               |  291 
crates/collab/src/tests/following_tests.rs                                             | 1699 
crates/collab/src/tests/integration_tests.rs                                           | 1132 
crates/collab/src/tests/random_channel_buffer_tests.rs                                 |    6 
crates/collab/src/tests/random_project_collaboration_tests.rs                          |    2 
crates/collab/src/tests/test_server.rs                                                 |  116 
crates/collab_ui/Cargo.toml                                                            |    3 
crates/collab_ui/src/channel_view.rs                                                   |  199 
crates/collab_ui/src/chat_panel.rs                                                     |  885 
crates/collab_ui/src/collab_panel.rs                                                   |  723 
crates/collab_ui/src/collab_panel/contact_finder.rs                                    |    4 
crates/collab_ui/src/collab_titlebar_item.rs                                           |  368 
crates/collab_ui/src/collab_ui.rs                                                      |   46 
crates/collab_ui/src/notifications.rs                                                  |    2 
crates/collab_ui/src/panel_settings.rs                                                 |   35 
crates/collab_ui/src/project_shared_notification.rs                                    |   15 
crates/collab_ui/src/sharing_status_indicator.rs                                       |    2 
crates/command_palette/src/command_palette.rs                                          |   49 
crates/copilot/src/copilot.rs                                                          |   70 
crates/copilot_button/src/copilot_button.rs                                            |    8 
crates/diagnostics/Cargo.toml                                                          |    3 
crates/diagnostics/src/diagnostics.rs                                                  |   41 
crates/diagnostics/src/items.rs                                                        |    3 
crates/diagnostics/src/project_diagnostics_settings.rs                                 |   28 
crates/diagnostics/src/toolbar_controls.rs                                             |  115 
crates/drag_and_drop/src/drag_and_drop.rs                                              |   79 
crates/editor/Cargo.toml                                                               |    3 
crates/editor/src/display_map.rs                                                       |   96 
crates/editor/src/display_map/block_map.rs                                             |   20 
crates/editor/src/display_map/fold_map.rs                                              |   22 
crates/editor/src/display_map/inlay_map.rs                                             |  318 
crates/editor/src/display_map/tab_map.rs                                               |   40 
crates/editor/src/display_map/wrap_map.rs                                              |   24 
crates/editor/src/editor.rs                                                            |  495 
crates/editor/src/editor_tests.rs                                                      |   66 
crates/editor/src/element.rs                                                           |  105 
crates/editor/src/hover_popover.rs                                                     |  376 
crates/editor/src/inlay_hint_cache.rs                                                  |  103 
crates/editor/src/items.rs                                                             |   42 
crates/editor/src/link_go_to_definition.rs                                             |  207 
crates/editor/src/multi_buffer.rs                                                      |   43 
crates/editor/src/scroll/scroll_amount.rs                                              |   10 
crates/editor/src/test/editor_lsp_test_context.rs                                      |    6 
crates/editor/src/test/editor_test_context.rs                                          |   19 
crates/feedback/Cargo.toml                                                             |    2 
crates/feedback/src/feedback_editor.rs                                                 |   11 
crates/file_finder/Cargo.toml                                                          |    1 
crates/file_finder/src/file_finder.rs                                                  |  573 
crates/fs/Cargo.toml                                                                   |    8 
crates/fs/src/fs.rs                                                                    |   29 
crates/fuzzy/src/fuzzy.rs                                                              |    4 
crates/fuzzy/src/paths.rs                                                              |   38 
crates/git/Cargo.toml                                                                  |    2 
crates/gpui/Cargo.toml                                                                 |    5 
crates/gpui/src/app.rs                                                                 |   52 
crates/gpui/src/app/window.rs                                                          |   32 
crates/gpui/src/font_cache.rs                                                          |    7 
crates/gpui/src/geometry.rs                                                            |   74 
crates/gpui/src/keymap_matcher.rs                                                      |   71 
crates/gpui/src/keymap_matcher/keymap.rs                                               |   12 
crates/gpui/src/keymap_matcher/keystroke.rs                                            |   50 
crates/gpui/src/platform/mac/event.rs                                                  |    1 
crates/gpui/src/platform/mac/window.rs                                                 |   40 
crates/gpui/src/platform/test.rs                                                       |   35 
crates/gpui/src/views.rs                                                               |    4 
crates/gpui/src/views/select.rs                                                        |   48 
crates/gpui2/src/element.rs                                                            |   46 
crates/gpui2/src/elements/pressable.rs                                                 |   11 
crates/gpui2/src/elements/text.rs                                                      |   16 
crates/gpui2/src/style.rs                                                              |  176 
crates/gpui2_macros/src/styleable_helpers.rs                                           |  279 
crates/language/src/buffer.rs                                                          |   14 
crates/language/src/buffer_tests.rs                                                    |    2 
crates/language/src/language.rs                                                        |   83 
crates/language_tools/src/lsp_log.rs                                                   |  161 
crates/language_tools/src/lsp_log_tests.rs                                             |    9 
crates/lsp/src/lsp.rs                                                                  |    8 
crates/picker/src/picker.rs                                                            |   37 
crates/project/Cargo.toml                                                              |    4 
crates/project/src/project.rs                                                          |  265 
crates/project/src/project_tests.rs                                                    |   35 
crates/project/src/search.rs                                                           |   61 
crates/project/src/terminals.rs                                                        |    1 
crates/project_panel/src/project_panel.rs                                              |  108 
crates/project_symbols/src/project_symbols.rs                                          |    2 
crates/quick_action_bar/Cargo.toml                                                     |    2 
crates/quick_action_bar/src/quick_action_bar.rs                                        |   56 
crates/rich_text/Cargo.toml                                                            |   30 
crates/rich_text/src/rich_text.rs                                                      |  287 
crates/rpc/proto/zed.proto                                                             |  436 
crates/rpc/src/proto.rs                                                                |   45 
crates/rpc/src/rpc.rs                                                                  |    2 
crates/search/src/buffer_search.rs                                                     |  418 
crates/search/src/project_search.rs                                                    |  411 
crates/search/src/search.rs                                                            |   42 
crates/semantic_index/Cargo.toml                                                       |   16 
crates/semantic_index/README.md                                                        |   39 
crates/semantic_index/eval/gpt-engineer.json                                           |  114 
crates/semantic_index/eval/tree-sitter.json                                            |  104 
crates/semantic_index/examples/eval.rs                                                 |  531 
crates/semantic_index/src/db.rs                                                        |  172 
crates/semantic_index/src/embedding_queue.rs                                           |    3 
crates/semantic_index/src/parsing.rs                                                   |   37 
crates/semantic_index/src/semantic_index.rs                                            |  327 
crates/semantic_index/src/semantic_index_settings.rs                                   |    2 
crates/semantic_index/src/semantic_index_tests.rs                                      |   22 
crates/sqlez/Cargo.toml                                                                |    4 
crates/storybook/Cargo.toml                                                            |   12 
crates/storybook/src/collab_panel.rs                                                   |  177 
crates/storybook/src/components.rs                                                     |   97 
crates/storybook/src/stories.rs                                                        |    3 
crates/storybook/src/stories/components.rs                                             |   22 
crates/storybook/src/stories/components/assistant_panel.rs                             |   16 
crates/storybook/src/stories/components/breadcrumb.rs                                  |   45 
crates/storybook/src/stories/components/buffer.rs                                      |   36 
crates/storybook/src/stories/components/chat_panel.rs                                  |   46 
crates/storybook/src/stories/components/collab_panel.rs                                |   16 
crates/storybook/src/stories/components/context_menu.rs                                |   21 
crates/storybook/src/stories/components/facepile.rs                                    |   25 
crates/storybook/src/stories/components/keybinding.rs                                  |   64 
crates/storybook/src/stories/components/language_selector.rs                           |   16 
crates/storybook/src/stories/components/multi_buffer.rs                                |   24 
crates/storybook/src/stories/components/palette.rs                                     |   53 
crates/storybook/src/stories/components/panel.rs                                       |   25 
crates/storybook/src/stories/components/project_panel.rs                               |   20 
crates/storybook/src/stories/components/recent_projects.rs                             |   16 
crates/storybook/src/stories/components/status_bar.rs                                  |   16 
crates/storybook/src/stories/components/tab.rs                                         |   91 
crates/storybook/src/stories/components/tab_bar.rs                                     |   46 
crates/storybook/src/stories/components/terminal.rs                                    |   16 
crates/storybook/src/stories/components/theme_selector.rs                              |   16 
crates/storybook/src/stories/components/title_bar.rs                                   |   16 
crates/storybook/src/stories/components/toolbar.rs                                     |   70 
crates/storybook/src/stories/components/traffic_lights.rs                              |   18 
crates/storybook/src/stories/elements.rs                                               |    5 
crates/storybook/src/stories/elements/avatar.rs                                        |   23 
crates/storybook/src/stories/elements/button.rs                                        |  192 
crates/storybook/src/stories/elements/icon.rs                                          |   19 
crates/storybook/src/stories/elements/input.rs                                         |   16 
crates/storybook/src/stories/elements/label.rs                                         |   18 
crates/storybook/src/stories/kitchen_sink.rs                                           |   26 
crates/storybook/src/story.rs                                                          |   44 
crates/storybook/src/story_selector.rs                                                 |  178 
crates/storybook/src/storybook.rs                                                      |  145 
crates/storybook/src/workspace.rs                                                      |  444 
crates/terminal/src/mappings/keys.rs                                                   |    1 
crates/terminal/src/terminal_settings.rs                                               |    1 
crates/terminal_view/src/terminal_panel.rs                                             |   46 
crates/terminal_view/src/terminal_view.rs                                              |   24 
crates/text/src/selection.rs                                                           |    4 
crates/theme/src/components.rs                                                         |    1 
crates/theme/src/theme.rs                                                              |   46 
crates/ui/Cargo.toml                                                                   |   16 
crates/ui/docs/_project.md                                                             |   13 
crates/ui/docs/elevation.md                                                            |   57 
crates/ui/src/children.rs                                                              |    7 
crates/ui/src/components.rs                                                            |  163 
crates/ui/src/components/assistant_panel.rs                                            |   91 
crates/ui/src/components/breadcrumb.rs                                                 |   71 
crates/ui/src/components/buffer.rs                                                     |  233 
crates/ui/src/components/chat_panel.rs                                                 |  108 
crates/ui/src/components/collab_panel.rs                                               |  161 
crates/ui/src/components/command_palette.rs                                            |   29 
crates/ui/src/components/context_menu.rs                                               |   65 
crates/ui/src/components/editor_pane.rs                                                |   60 
crates/ui/src/components/facepile.rs                                                   |   28 
crates/ui/src/components/icon_button.rs                                                |   67 
crates/ui/src/components/keybinding.rs                                                 |  158 
crates/ui/src/components/language_selector.rs                                          |   36 
crates/ui/src/components/list.rs                                                       |  512 
crates/ui/src/components/multi_buffer.rs                                               |   42 
crates/ui/src/components/palette.rs                                                    |  152 
crates/ui/src/components/panel.rs                                                      |  142 
crates/ui/src/components/panes.rs                                                      |  132 
crates/ui/src/components/player_stack.rs                                               |   65 
crates/ui/src/components/project_panel.rs                                              |   58 
crates/ui/src/components/recent_projects.rs                                            |   32 
crates/ui/src/components/status_bar.rs                                                 |  144 
crates/ui/src/components/tab.rs                                                        |  131 
crates/ui/src/components/tab_bar.rs                                                    |   85 
crates/ui/src/components/terminal.rs                                                   |   84 
crates/ui/src/components/theme_selector.rs                                             |   37 
crates/ui/src/components/title_bar.rs                                                  |  117 
crates/ui/src/components/toast.rs                                                      |   66 
crates/ui/src/components/toolbar.rs                                                    |   49 
crates/ui/src/components/traffic_lights.rs                                             |   78 
crates/ui/src/components/workspace.rs                                                  |  186 
crates/ui/src/element_ext.rs                                                           |    6 
crates/ui/src/elements.rs                                                              |   19 
crates/ui/src/elements/avatar.rs                                                       |   41 
crates/ui/src/elements/button.rs                                                       |  203 
crates/ui/src/elements/details.rs                                                      |   33 
crates/ui/src/elements/icon.rs                                                         |  185 
crates/ui/src/elements/input.rs                                                        |  106 
crates/ui/src/elements/label.rs                                                        |  161 
crates/ui/src/elements/player.rs                                                       |  133 
crates/ui/src/elements/stack.rs                                                        |   31 
crates/ui/src/elements/tool_divider.rs                                                 |   17 
crates/ui/src/lib.rs                                                                   |   20 
crates/ui/src/prelude.rs                                                               |  274 
crates/ui/src/static_data.rs                                                           |  966 
crates/ui/src/theme.rs                                                                 |   18 
crates/ui/src/tokens.rs                                                                |   25 
crates/ui/tracker.md                                                                   |  133 
crates/util/Cargo.toml                                                                 |    4 
crates/util/src/util.rs                                                                |   14 
crates/vim/Cargo.toml                                                                  |    2 
crates/vim/src/command.rs                                                              |  438 
crates/vim/src/editor_events.rs                                                        |   34 
crates/vim/src/insert.rs                                                               |  120 
crates/vim/src/motion.rs                                                               |   62 
crates/vim/src/normal.rs                                                               |   47 
crates/vim/src/normal/case.rs                                                          |    2 
crates/vim/src/normal/change.rs                                                        |  176 
crates/vim/src/normal/delete.rs                                                        |  154 
crates/vim/src/normal/increment.rs                                                     |  268 
crates/vim/src/normal/repeat.rs                                                        |  327 
crates/vim/src/normal/scroll.rs                                                        |   90 
crates/vim/src/normal/search.rs                                                        |  204 
crates/vim/src/normal/substitute.rs                                                    |    4 
crates/vim/src/state.rs                                                                |   20 
crates/vim/src/test.rs                                                                 |   86 
crates/vim/src/test/neovim_backed_binding_test_context.rs                              |   17 
crates/vim/src/test/neovim_backed_test_context.rs                                      |   76 
crates/vim/src/test/neovim_connection.rs                                               |    8 
crates/vim/src/utils.rs                                                                |    3 
crates/vim/src/vim.rs                                                                  |  117 
crates/vim/src/visual.rs                                                               |   72 
crates/vim/test_data/test_clear_counts.json                                            |    7 
crates/vim/test_data/test_command_basics.json                                          |    6 
crates/vim/test_data/test_command_goto.json                                            |    5 
crates/vim/test_data/test_command_replace.json                                         |   22 
crates/vim/test_data/test_command_search.json                                          |   11 
crates/vim/test_data/test_ctrl_d_u.json                                                |   22 
crates/vim/test_data/test_delete_w.json                                                |    8 
crates/vim/test_data/test_delete_with_counts.json                                      |   16 
crates/vim/test_data/test_dot_repeat.json                                              |    2 
crates/vim/test_data/test_folds_panic.json                                             |   13 
crates/vim/test_data/test_increment.json                                               |   16 
crates/vim/test_data/test_increment_radix.json                                         |   18 
crates/vim/test_data/test_increment_steps.json                                         |   14 
crates/vim/test_data/test_insert_with_counts.json                                      |   36 
crates/vim/test_data/test_insert_with_repeat.json                                      |   23 
crates/vim/test_data/test_repeat_motion_counts.json                                    |   13 
crates/vim/test_data/test_selection_goal.json                                          |    8 
crates/vim/test_data/test_visual_yank.json                                             |    6 
crates/vim/test_data/test_zero.json                                                    |    7 
crates/workspace/Cargo.toml                                                            |    2 
crates/workspace/src/dock.rs                                                           |    6 
crates/workspace/src/item.rs                                                           |   30 
crates/workspace/src/notifications.rs                                                  |    2 
crates/workspace/src/pane.rs                                                           |  371 
crates/workspace/src/pane_group.rs                                                     |   77 
crates/workspace/src/searchable.rs                                                     |   18 
crates/workspace/src/shared_screen.rs                                                  |    2 
crates/workspace/src/workspace.rs                                                      |  642 
crates/zed/Cargo.toml                                                                  |    9 
crates/zed/src/languages.rs                                                            |   38 
crates/zed/src/languages/css/config.toml                                               |    1 
crates/zed/src/languages/elixir.rs                                                     |  298 
crates/zed/src/languages/rust.rs                                                       |   51 
crates/zed/src/languages/rust/embedding.scm                                            |    4 
crates/zed/src/main.rs                                                                 |   19 
crates/zed/src/menus.rs                                                                |    8 
crates/zed/src/zed.rs                                                                  |   73 
docs/tools.md                                                                          |    4 
rust-toolchain.toml                                                                    |    2 
script/deploy                                                                          |    3 
script/evaluate_semantic_index                                                         |    3 
script/reset_db                                                                        |    2 
script/start-local-collaboration                                                       |    1 
styles/src/style_tree/app.ts                                                           |    2 
styles/src/style_tree/assistant.ts                                                     |   14 
styles/src/style_tree/chat_panel.ts                                                    |  175 
styles/src/style_tree/collab_panel.ts                                                  |   15 
styles/src/style_tree/component_test.ts                                                |    1 
styles/src/style_tree/contact_notification.ts                                          |    8 
styles/src/style_tree/copilot.ts                                                       |   10 
styles/src/style_tree/editor.ts                                                        |    6 
styles/src/style_tree/search.ts                                                        |   87 
styles/src/style_tree/simple_message_notification.ts                                   |    8 
styles/src/style_tree/tab_bar.ts                                                       |    2 
styles/src/style_tree/titlebar.ts                                                      |    8 
styles/src/style_tree/update_notification.ts                                           |    8 
styles/src/style_tree/welcome.ts                                                       |    2 
852 files changed, 28,843 insertions(+), 12,626 deletions(-)

Detailed changes

.cargo/config.toml πŸ”—

@@ -1,2 +1,6 @@
 [alias]
 xtask = "run --package xtask --"
+
+[build]
+# v0 mangling scheme provides more detailed backtraces around closures
+rustflags = ["-C", "symbol-mangling-version=v0"]

Cargo.lock πŸ”—

@@ -2,12 +2,6 @@
 # It is not intended for manual editing.
 version = 3
 
-[[package]]
-name = "Inflector"
-version = "0.11.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
-
 [[package]]
 name = "activity_indicator"
 version = "0.1.0"
@@ -73,24 +67,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
 dependencies = [
  "cfg-if 1.0.0",
+ "getrandom 0.2.10",
  "once_cell",
  "version_check",
 ]
 
 [[package]]
 name = "aho-corasick"
-version = "0.7.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
-dependencies = [
- "memchr",
-]
-
-[[package]]
-name = "aho-corasick"
-version = "1.0.4"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a"
+checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
 dependencies = [
  "memchr",
 ]
@@ -100,33 +86,25 @@ name = "ai"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "chrono",
- "collections",
- "ctor",
- "editor",
- "env_logger 0.9.3",
- "fs",
+ "async-trait",
+ "bincode",
  "futures 0.3.28",
  "gpui",
- "indoc",
  "isahc",
- "language",
+ "lazy_static",
  "log",
- "menu",
- "ordered-float",
- "project",
+ "matrixmultiply",
+ "ordered-float 2.10.0",
+ "parking_lot 0.11.2",
+ "parse_duration",
+ "postage",
  "rand 0.8.5",
  "regex",
- "schemars",
- "search",
+ "rusqlite",
  "serde",
  "serde_json",
- "settings",
- "smol",
- "theme",
- "tiktoken-rs 0.4.5",
+ "tiktoken-rs 0.5.4",
  "util",
- "workspace",
 ]
 
 [[package]]
@@ -136,7 +114,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3
 dependencies = [
  "log",
  "serde",
- "toml 0.7.6",
+ "toml 0.7.8",
 ]
 
 [[package]]
@@ -146,7 +124,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.29",
+ "syn 2.0.37",
 ]
 
 [[package]]
@@ -165,14 +143,14 @@ dependencies = [
  "mio-anonymous-pipes",
  "mio-extras",
  "miow 0.3.7",
- "nix 0.26.2",
+ "nix 0.26.4",
  "parking_lot 0.12.1",
  "regex-automata 0.1.10",
  "serde",
  "serde_yaml",
  "signal-hook",
  "signal-hook-mio",
- "toml 0.7.6",
+ "toml 0.7.8",
  "unicode-width",
  "vte",
  "windows-sys",
@@ -235,24 +213,23 @@ dependencies = [
 
 [[package]]
 name = "anstream"
-version = "0.3.2"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
+checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
 dependencies = [
  "anstyle",
  "anstyle-parse",
  "anstyle-query",
  "anstyle-wincon",
  "colorchoice",
- "is-terminal 0.4.9",
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
+checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46"
 
 [[package]]
 name = "anstyle-parse"
@@ -274,9 +251,9 @@ dependencies = [
 
 [[package]]
 name = "anstyle-wincon"
-version = "1.0.2"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c"
+checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
 dependencies = [
  "anstyle",
  "windows-sys",
@@ -312,6 +289,44 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
 
+[[package]]
+name = "assistant"
+version = "0.1.0"
+dependencies = [
+ "ai",
+ "anyhow",
+ "chrono",
+ "client",
+ "collections",
+ "ctor",
+ "editor",
+ "env_logger 0.9.3",
+ "fs",
+ "futures 0.3.28",
+ "gpui",
+ "indoc",
+ "isahc",
+ "language",
+ "log",
+ "menu",
+ "ordered-float 2.10.0",
+ "parking_lot 0.11.2",
+ "project",
+ "rand 0.8.5",
+ "regex",
+ "schemars",
+ "search",
+ "serde",
+ "serde_json",
+ "settings",
+ "smol",
+ "theme",
+ "tiktoken-rs 0.4.5",
+ "util",
+ "uuid 1.4.1",
+ "workspace",
+]
+
 [[package]]
 name = "async-broadcast"
 version = "0.4.1"
@@ -343,7 +358,7 @@ dependencies = [
  "futures-core",
  "futures-io",
  "once_cell",
- "pin-project-lite 0.2.12",
+ "pin-project-lite 0.2.13",
  "tokio",
 ]
 
@@ -357,7 +372,7 @@ dependencies = [
  "futures-core",
  "futures-io",
  "memchr",
- "pin-project-lite 0.2.12",
+ "pin-project-lite 0.2.13",
 ]
 
 [[package]]
@@ -482,13 +497,13 @@ dependencies = [
 
 [[package]]
 name = "async-recursion"
-version = "1.0.4"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
+checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.29",
+ "syn 2.0.37",
 ]
 
 [[package]]
@@ -511,7 +526,7 @@ dependencies = [
  "log",
  "memchr",
  "once_cell",
- "pin-project-lite 0.2.12",
+ "pin-project-lite 0.2.13",
  "pin-utils",
  "slab",
  "wasm-bindgen-futures",
@@ -525,7 +540,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
 dependencies = [
  "async-stream-impl",
  "futures-core",
- "pin-project-lite 0.2.12",
+ "pin-project-lite 0.2.13",
 ]
 
 [[package]]
@@ -536,7 +551,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.29",
+ "syn 2.0.37",
 ]
 
 [[package]]
@@ -567,7 +582,7 @@ dependencies = [
  "futures-core",
  "futures-io",
  "rustls 0.19.1",
- "webpki 0.21.4",
+ "webpki",
  "webpki-roots 0.21.1",
 ]
 
@@ -579,7 +594,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.29",
+ "syn 2.0.37",
 ]
 
 [[package]]
@@ -592,15 +607,15 @@ dependencies = [
  "futures-io",
  "futures-util",
  "log",
- "pin-project-lite 0.2.12",
+ "pin-project-lite 0.2.13",
  "tungstenite 0.16.0",
 ]
 
 [[package]]
 name = "atoi"
-version = "1.0.0"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
 dependencies = [
  "num-traits",
 ]
@@ -681,7 +696,7 @@ dependencies = [
  "axum-core",
  "base64 0.13.1",
  "bitflags 1.3.2",
- "bytes 1.4.0",
+ "bytes 1.5.0",
  "futures-util",
  "headers",
  "http",
@@ -692,7 +707,7 @@ dependencies = [
  "memchr",
  "mime",
  "percent-encoding",
- "pin-project-lite 0.2.12",
+ "pin-project-lite 0.2.13",
  "serde",
  "serde_json",
  "serde_urlencoded",
@@ -713,7 +728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc"
 dependencies = [
  "async-trait",
- "bytes 1.4.0",
+ "bytes 1.5.0",
  "futures-util",
  "http",
  "http-body",
@@ -729,11 +744,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb"
 dependencies = [
  "axum",
- "bytes 1.4.0",
+ "bytes 1.5.0",
  "futures-util",
  "http",
  "mime",
- "pin-project-lite 0.2.12",
+ "pin-project-lite 0.2.13",
  "serde",
  "serde_json",
  "tokio",
@@ -754,7 +769,7 @@ dependencies = [
  "cfg-if 1.0.0",
  "libc",
  "miniz_oxide 0.7.1",
- "object 0.32.0",
+ "object 0.32.1",
  "rustc-demangle",
 ]
 
@@ -769,19 +784,6 @@ dependencies = [
  "nix 0.23.2",
 ]
 
-[[package]]
-name = "bae"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33b8de67cc41132507eeece2584804efcb15f85ba516e34c944b7667f480397a"
-dependencies = [
- "heck 0.3.3",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
 [[package]]
 name = "base64"
 version = "0.13.1"
@@ -790,9 +792,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
 
 [[package]]
 name = "base64"
-version = "0.21.2"
+version = "0.21.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
+checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
 
 [[package]]
 name = "base64ct"
@@ -800,6 +802,17 @@ version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
+[[package]]
+name = "bigdecimal"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa"
+dependencies = [
+ "num-bigint 0.4.4",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "bincode"
 version = "1.3.3"
@@ -848,7 +861,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn 2.0.29",
+ "syn 2.0.37",
  "which",
 ]
 
@@ -985,7 +998,7 @@ dependencies = [
  "collections",
  "editor",
  "gpui",
- "itertools",
+ "itertools 0.10.5",
  "language",
  "outline",
  "project",
@@ -1008,20 +1021,20 @@ dependencies = [
 
 [[package]]
 name = "bstr"
-version = "1.6.0"
+version = "1.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
+checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
 dependencies = [
  "memchr",
- "regex-automata 0.3.6",
+ "regex-automata 0.3.8",
  "serde",
 ]
 
 [[package]]
 name = "bumpalo"
-version = "3.13.0"
+version = "3.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
+checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
 
 [[package]]
 name = "bytecheck"
@@ -1069,9 +1082,9 @@ dependencies = [
 
 [[package]]
 name = "bytes"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
 
 [[package]]
 name = "call"
@@ -1234,6 +1247,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "client",
+ "clock",
  "collections",
  "db",
  "feature_flags",
@@ -1251,12 +1265,13 @@ dependencies = [
  "serde",
  "serde_derive",
  "settings",
+ "smallvec",
  "smol",
  "sum_tree",
  "tempfile",
  "text",
  "thiserror",
- "time 0.3.27",
+ "time",
  "tiny_http",
  "url",
  "util",
@@ -1265,18 +1280,17 @@ dependencies = [
 
 [[package]]
 name = "chrono"
-version = "0.4.26"
+version = "0.4.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
+checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
 dependencies = [
  "android-tzdata",
  "iana-time-zone",
  "js-sys",
  "num-traits",
  "serde",
- "time 0.1.45",
  "wasm-bindgen",
- "winapi 0.3.9",
+ "windows-targets 0.48.5",
 ]
 
 [[package]]
@@ -1324,24 +1338,23 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.3.24"
+version = "4.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487"
+checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136"
 dependencies = [
  "clap_builder",
- "clap_derive 4.3.12",
- "once_cell",
+ "clap_derive 4.4.2",
 ]
 
 [[package]]
 name = "clap_builder"
-version = "4.3.24"
+version = "4.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e"
+checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56"
 dependencies = [
  "anstream",
  "anstyle",
- "clap_lex 0.5.0",
+ "clap_lex 0.5.1",
  "strsim",
 ]
 
@@ -1360,14 +1373,14 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.3.12"
+version = "4.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
+checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
 dependencies = [
  "heck 0.4.1",
  "proc-macro2",
  "quote",
- "syn 2.0.29",
+ "syn 2.0.37",
 ]
 
 [[package]]
@@ -1381,9 +1394,9 @@ dependencies = [
 
 [[package]]
 name = "clap_lex"
-version = "0.5.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
+checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
 
 [[package]]
 name = "cli"
@@ -1426,10 +1439,11 @@ dependencies = [
  "settings",
  "smol",
  "sum_tree",
+ "sysinfo",
  "tempfile",
  "text",
  "thiserror",
- "time 0.3.27",
+ "time",
  "tiny_http",
  "url",
  "util",
@@ -1483,7 +1497,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.20.0"
+version = "0.23.2"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1528,16 +1542,16 @@ dependencies = [
  "rpc",
  "scrypt",
  "sea-orm",
- "sea-query",
  "serde",
  "serde_derive",
  "serde_json",
  "settings",
  "sha-1 0.9.8",
+ "smallvec",
  "sqlx",
  "text",
  "theme",
- "time 0.3.27",
+ "time",
  "tokio",
  "tokio-tungstenite",
  "toml 0.5.11",
@@ -1548,6 +1562,7 @@ dependencies = [
  "tracing-subscriber",
  "unindent",
  "util",
+ "uuid 1.4.1",
  "workspace",
 ]
 
@@ -1564,6 +1579,7 @@ dependencies = [
  "collections",
  "context_menu",
  "db",
+ "drag_and_drop",
  "editor",
  "feature_flags",
  "feedback",
@@ -1577,12 +1593,14 @@ dependencies = [
  "postage",
  "project",
  "recent_projects",
+ "rich_text",
  "schemars",
  "serde",
  "serde_derive",
  "settings",
  "theme",
  "theme_selector",
+ "time",
  "util",
  "vcs_menu",
  "workspace",
@@ -1614,7 +1632,7 @@ version = "4.6.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
 dependencies = [
- "bytes 1.4.0",
+ "bytes 1.5.0",
  "memchr",
 ]
 
@@ -1666,6 +1684,12 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed3d0b5ff30645a68f35ece8cea4556ca14ef8a1651455f789a099a0513532a6"
 
+[[package]]
+name = "const-oid"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
+
 [[package]]
 name = "context_menu"
 version = "0.1.0"
@@ -1783,9 +1807,9 @@ dependencies = [
 
 [[package]]
 name = "core-services"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51b344b958cae90858bf6086f49599ecc5ec8698eacad0ea155509ba11fab347"
+checksum = "92567e81db522550ebaf742c5d875624ec7820c2c7ee5f8c60e4ce7c2ae3c0fd"
 dependencies = [
  "core-foundation",
 ]
@@ -1954,7 +1978,7 @@ dependencies = [
  "cranelift-codegen",
  "cranelift-entity",
  "cranelift-frontend",
- "itertools",
+ "itertools 0.10.5",
  "log",
  "smallvec",
  "wasmparser",
@@ -2085,9 +2109,9 @@ dependencies = [
 
 [[package]]
 name = "curl-sys"
-version = "0.4.65+curl-8.2.1"
+version = "0.4.66+curl-8.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "961ba061c9ef2fe34bbd12b807152d96f0badd2bebe7b90ce6c8c8b7572a0986"
+checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9"
 dependencies = [
  "cc",
  "libc",
@@ -2095,14 +2119,14 @@ dependencies = [
  "openssl-sys",
  "pkg-config",
  "vcpkg",
- "winapi 0.3.9",
+ "windows-sys",
 ]
 
 [[package]]
 name = "dashmap"
-version = "5.5.1"
+version = "5.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28"
+checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
 dependencies = [
  "cfg-if 1.0.0",
  "hashbrown 0.14.0",
@@ -2158,6 +2182,17 @@ dependencies = [
  "byteorder",
 ]
 
+[[package]]
+name = "der"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
 [[package]]
 name = "deranged"
 version = "0.3.8"
@@ -2167,6 +2202,17 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "derive_more"
 version = "0.99.17"
@@ -2176,7 +2222,7 @@ dependencies = [
  "convert_case 0.4.0",
  "proc-macro2",
  "quote",
- "rustc_version 0.4.0",
+ "rustc_version",
  "syn 1.0.109",
 ]
 
@@ -2218,6 +2264,9 @@ dependencies = [
  "lsp",
  "postage",
  "project",
+ "schemars",
+ "serde",
+ "serde_derive",
  "serde_json",
  "settings",
  "smallvec",
@@ -2249,6 +2298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
  "block-buffer 0.10.4",
+ "const-oid",
  "crypto-common",
  "subtle",
 ]
@@ -2350,15 +2400,15 @@ dependencies = [
 
 [[package]]
 name = "dyn-clone"
-version = "1.0.13"
+version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555"
+checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd"
 
 [[package]]
 name = "editor"
 version = "0.1.0"
 dependencies = [
- "aho-corasick 0.7.20",
+ "aho-corasick",
  "anyhow",
  "client",
  "clock",
@@ -2375,17 +2425,18 @@ dependencies = [
  "git",
  "gpui",
  "indoc",
- "itertools",
+ "itertools 0.10.5",
  "language",
  "lazy_static",
  "log",
  "lsp",
- "ordered-float",
+ "ordered-float 2.10.0",
  "parking_lot 0.11.2",
  "postage",
  "project",
  "pulldown-cmark",
  "rand 0.8.5",
+ "rich_text",
  "rpc",
  "schemars",
  "serde",
@@ -2412,6 +2463,9 @@ name = "either"
 version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "encoding_rs"
@@ -2465,9 +2519,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
 [[package]]
 name = "erased-serde"
-version = "0.3.29"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a"
+checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c"
 dependencies = [
  "serde",
 ]
@@ -2485,9 +2539,9 @@ dependencies = [
 
 [[package]]
 name = "errno"
-version = "0.3.2"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f"
+checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
 dependencies = [
  "errno-dragonfly",
  "libc",
@@ -2514,6 +2568,17 @@ dependencies = [
  "svg_fmt",
 ]
 
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if 1.0.0",
+ "home",
+ "windows-sys",
+]
+
 [[package]]
 name = "euclid"
 version = "0.22.9"
@@ -2618,6 +2683,7 @@ dependencies = [
 name = "file_finder"
 version = "0.1.0"
 dependencies = [
+ "collections",
  "ctor",
  "editor",
  "env_logger 0.9.3",
@@ -2648,6 +2714,12 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "finl_unicode"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
+
 [[package]]
 name = "fixedbitset"
 version = "0.4.2"
@@ -2678,13 +2750,12 @@ checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
 
 [[package]]
 name = "flume"
-version = "0.10.14"
+version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
+checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
 dependencies = [
  "futures-core",
  "futures-sink",
- "pin-project",
  "spin 0.9.8",
 ]
 
@@ -2788,7 +2859,6 @@ dependencies = [
  "lazy_static",
  "libc",
  "log",
- "lsp",
  "parking_lot 0.11.2",
  "regex",
  "rope",
@@ -2800,7 +2870,7 @@ dependencies = [
  "sum_tree",
  "tempfile",
  "text",
- "time 0.3.27",
+ "time",
  "util",
 ]
 
@@ -2912,13 +2982,13 @@ dependencies = [
 
 [[package]]
 name = "futures-intrusive"
-version = "0.4.2"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
 dependencies = [
  "futures-core",
  "lock_api",
- "parking_lot 0.11.2",
+ "parking_lot 0.12.1",
 ]
 
 [[package]]
@@ -2938,7 +3008,7 @@ dependencies = [
  "futures-io",
  "memchr",
  "parking",
- "pin-project-lite 0.2.12",
+ "pin-project-lite 0.2.13",
  "waker-fn",
 ]
 
@@ -2950,7 +3020,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.29",
+ "syn 2.0.37",
 ]
 
 [[package]]
@@ -2979,7 +3049,7 @@ dependencies = [
  "futures-sink",
  "futures-task",
  "memchr",
- "pin-project-lite 0.2.12",
+ "pin-project-lite 0.2.13",
  "pin-utils",
  "slab",
  "tokio-io",
@@ -3106,7 +3176,7 @@ version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d"
 dependencies = [
- "aho-corasick 1.0.4",
+ "aho-corasick",
  "bstr",
  "fnv",
  "log",
@@ -3165,14 +3235,14 @@ dependencies = [
  "futures 0.3.28",
  "gpui_macros",
  "image",
- "itertools",
+ "itertools 0.10.5",
  "lazy_static",
  "log",
  "media",
  "metal",
  "num_cpus",
  "objc",
- "ordered-float",
+ "ordered-float 2.10.0",
  "parking",
  "parking_lot 0.11.2",
  "pathfinder_color",
@@ -3194,7 +3264,7 @@ dependencies = [
  "sum_tree",
  "taffy",
  "thiserror",
- "time 0.3.27",
+ "time",
  "tiny-skia",
  "usvg",
  "util",
@@ -3259,14 +3329,14 @@ dependencies = [
  "gpui3_macros",
  "gpui_macros",
  "image",
- "itertools",
+ "itertools 0.10.5",
  "lazy_static",
  "log",
  "media",
  "metal",
  "num_cpus",
  "objc",
- "ordered-float",
+ "ordered-float 2.10.0",
  "parking",
  "parking_lot 0.11.2",
  "pathfinder_geometry",

Cargo.toml πŸ”—

@@ -2,6 +2,7 @@
 members = [
     "crates/activity_indicator",
     "crates/ai",
+    "crates/assistant",
     "crates/audio",
     "crates/auto_update",
     "crates/breadcrumbs",
@@ -65,6 +66,7 @@ members = [
     "crates/sqlez",
     "crates/sqlez_macros",
     "crates/feature_flags",
+    "crates/rich_text",
     "crates/storybook",
     "crates/storybook2",
     "crates/sum_tree",
@@ -72,6 +74,7 @@ members = [
     "crates/text",
     "crates/theme",
     "crates/theme_selector",
+    "crates/ui",
     "crates/util",
     "crates/semantic_index",
     "crates/vim",
@@ -106,12 +109,14 @@ rand = { version = "0.8.5" }
 refineable = { path = "./crates/refineable" }
 regex = { version = "1.5" }
 rust-embed = { version = "8.0", features = ["include-exclude"] }
+rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
 schemars = { version = "0.8" }
 serde = { version = "1.0", features = ["derive", "rc"] }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
 smallvec = { version = "1.6", features = ["union"] }
 smol = { version = "1.2" }
+sysinfo = "0.29.10"
 tempdir = { version = "0.3.7" }
 thiserror = { version = "1.0.29" }
 time = { version = "0.3", features = ["serde", "serde-well-known"] }
@@ -119,6 +124,8 @@ toml = { version = "0.5" }
 tree-sitter = "0.20"
 unindent = { version = "0.1.7" }
 pretty_assertions = "1.3.0"
+git2 = { version = "0.15", default-features = false}
+uuid = { version = "1.1.2", features = ["v4"] }
 
 tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" }
 tree-sitter-c = "0.20.1"

Dockerfile πŸ”—

@@ -1,6 +1,6 @@
 # syntax = docker/dockerfile:1.2
 
-FROM rust:1.72-bullseye as builder
+FROM rust:1.73-bullseye as builder
 WORKDIR app
 COPY . .
 

Procfile πŸ”—

@@ -1,3 +1,4 @@
-web: cd ../zed.dev && PORT=3000 npx vercel dev
-collab: cd crates/collab && cargo run serve
-livekit: livekit-server --dev
+web: cd ../zed.dev && PORT=3000 npm run dev
+collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
+livekit: livekit-server --dev
+postgrest: postgrest crates/collab/admin_api.conf

README.md πŸ”—

@@ -13,17 +13,13 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
   sudo xcodebuild -license
   ```
 
-* Install rustup (rust, cargo, etc.)
-  ```
-  curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-  ```
-  
-* Install homebrew and node
+* Install homebrew, node and rustup-init (rustup, rust, cargo, etc.)
   ```
   /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
-  brew install node
+  brew install node rustup-init
+  rustup-init # follow the installation steps
   ```
-  
+
 * Install postgres and configure the database
   ```
   brew install postgresql@15
@@ -31,15 +27,16 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
   psql -c "CREATE ROLE postgres SUPERUSER LOGIN" postgres
   psql -U postgres -c "CREATE DATABASE zed"
   ```
-  
-* Install the `LiveKit` server and the `foreman` process supervisor:
+
+* Install the `LiveKit` server, the `PostgREST` API server, and the `foreman` process supervisor:
 
     ```
     brew install livekit
+    brew install postgrest
     brew install foreman
     ```
 
-* Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies:
+* Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies:
 
     ```
     cd ..

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

@@ -1,3 +0,0 @@
-<svg width="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="black"/>
-</svg>

assets/icons/arrow_down_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1271)">
-<path d="M10.9007 7.45347L6.60649 11.7477C6.44009 11.9168 6.22001 12 5.99993 12C5.77985 12 5.56031 11.9161 5.39283 11.7484L1.09859 7.45414C0.763098 7.11865 0.763098 6.57516 1.09859 6.23967C1.43407 5.90419 1.97756 5.90419 2.31305 6.23967L5.14108 9.06904V0.834694C5.14108 0.359912 5.52568 0 5.97577 0C6.42587 0 6.85878 0.359912 6.85878 0.834694V9.06891L9.68761 6.24008C10.0231 5.90459 10.5666 5.90459 10.9021 6.24008C11.2376 6.57556 11.2362 7.11771 10.9007 7.4532V7.45347Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1271">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/arrow_down_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.0538 9.45347L8.75952 13.7477C8.59312 13.9168 8.37304 14 8.15296 14C7.93288 14 7.71334 13.9161 7.54586 13.7484L3.25162 9.45414C2.91613 9.11865 2.91613 8.57516 3.25162 8.23967C3.5871 7.90418 4.13059 7.90418 4.46608 8.23967L7.29411 11.069V2.83469C7.29411 2.35991 7.67871 2 8.12881 2C8.5789 2 9.01181 2.35991 9.01181 2.83469V11.0689L11.8406 8.24008C12.1761 7.90459 12.7196 7.90459 13.0551 8.24008C13.3906 8.57556 13.3893 9.11771 13.0538 9.4532V9.45347Z" fill="white"/>
-</svg>

assets/icons/arrow_down_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1354)">
-<path d="M7.26716 4.96898L4.40433 7.83181C4.2934 7.94453 4.14668 8 3.99996 8C3.85324 8 3.70688 7.94409 3.59523 7.83226L0.732395 4.96943C0.508737 4.74577 0.508737 4.38344 0.732395 4.15978C0.956054 3.93612 1.31838 3.93612 1.54204 4.15978L3.42739 6.04603V0.556463C3.42739 0.239941 3.68379 0 3.98385 0C4.28392 0 4.57252 0.239941 4.57252 0.556463V6.04594L6.45841 4.16005C6.68207 3.93639 7.0444 3.93639 7.26806 4.16005C7.49172 4.38371 7.49082 4.74514 7.26716 4.9688V4.96898Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1354">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/arrow_left_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1272)">
-<path d="M12 6.00001C12 6.47507 11.6403 6.85889 11.1653 6.85889H2.93347L5.7624 9.68781C6.0979 10.0233 6.0979 10.5668 5.7624 10.9023C5.59331 11.0701 5.37322 11.1533 5.15313 11.1533C4.93305 11.1533 4.71349 11.0694 4.54601 10.9017L0.251624 6.60726C-0.0838748 6.27176 -0.0838748 5.72825 0.251624 5.39275L4.54601 1.09837C4.88151 0.762866 5.42502 0.762866 5.76052 1.09837C6.09602 1.43386 6.09602 1.97737 5.76052 2.31287L2.93347 5.14113H11.1653C11.6403 5.14113 12 5.52494 12 6.00001Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1272">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/arrow_left_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M14 8.15327C14 8.62833 13.6403 9.01215 13.1653 9.01215H4.93347L7.7624 11.8411C8.0979 12.1766 8.0979 12.7201 7.7624 13.0556C7.59331 13.2233 7.37322 13.3065 7.15313 13.3065C6.93305 13.3065 6.71349 13.2227 6.54601 13.0549L2.25162 8.76052C1.91613 8.42502 1.91613 7.88151 2.25162 7.54601L6.54601 3.25162C6.88151 2.91613 7.42502 2.91613 7.76052 3.25162C8.09602 3.58712 8.09602 4.13063 7.76052 4.46613L4.93347 7.29439H13.1653C13.6403 7.29439 14 7.6782 14 8.15327Z" fill="white"/>
-</svg>

assets/icons/arrow_left_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1355)">
-<path d="M8 4.00003C8 4.31674 7.76023 4.57261 7.44352 4.57261H1.95565L3.8416 6.45856C4.06527 6.68223 4.06527 7.04457 3.8416 7.26823C3.72887 7.38007 3.58215 7.43554 3.43542 7.43554C3.2887 7.43554 3.14233 7.37962 3.03068 7.26779L0.16775 4.40486C-0.0559165 4.1812 -0.0559165 3.81886 0.16775 3.59519L3.03068 0.732264C3.25434 0.508598 3.61668 0.508598 3.84035 0.732264C4.06401 0.95593 4.06401 1.31827 3.84035 1.54194L1.95565 3.42744H7.44352C7.76023 3.42744 8 3.68331 8 4.00003Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1355">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/arrow_right_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1273)">
-<path d="M11.749 6.60576L7.46298 10.8917C7.2969 11.0605 7.07724 11.1436 6.85758 11.1436C6.63793 11.1436 6.41881 11.0598 6.25165 10.8924C5.91681 10.5576 5.91681 10.0151 6.25165 9.68029L9.07558 6.85756H0.857198C0.383864 6.85756 0 6.4745 0 6.00036C0 5.52623 0.383596 5.14317 0.85693 5.14317H9.07531L6.25192 2.31977C5.91708 1.98493 5.91708 1.44248 6.25192 1.10764C6.58676 0.772796 7.12921 0.772796 7.46405 1.10764L11.75 5.39363C12.0835 5.72981 12.0835 6.27092 11.7487 6.60576H11.749Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1273">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/arrow_right_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.749 8.74925L9.46298 13.0352C9.2969 13.204 9.07724 13.287 8.85758 13.287C8.63793 13.287 8.41881 13.2033 8.25165 13.0359C7.91681 12.7011 7.91681 12.1586 8.25165 11.8238L11.0756 9.00106H2.8572C2.38386 9.00106 2 8.61799 2 8.14386C2 7.66972 2.3836 7.28666 2.85693 7.28666H11.0753L8.25192 4.46326C7.91708 4.12842 7.91708 3.58598 8.25192 3.25113C8.58676 2.91629 9.12921 2.91629 9.46405 3.25113L13.75 7.53712C14.0835 7.8733 14.0835 8.41441 13.7487 8.74925H13.749Z" fill="white"/>
-</svg>

assets/icons/arrow_right_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1356)">
-<path d="M7.83265 4.40382L4.97532 7.26115C4.8646 7.37365 4.71816 7.42901 4.57172 7.42901C4.42528 7.42901 4.2792 7.37321 4.16777 7.26159C3.94454 7.03836 3.94454 6.67673 4.16777 6.4535L6.05039 4.57169H0.571465C0.255909 4.57169 0 4.31631 0 4.00022C0 3.68413 0.255731 3.42876 0.571287 3.42876H6.05021L4.16795 1.54649C3.94472 1.32326 3.94472 0.961634 4.16795 0.738405C4.39117 0.515177 4.75281 0.515177 4.97603 0.738405L7.83336 3.59573C8.0557 3.81985 8.0557 4.18059 7.83247 4.40382H7.83265Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1356">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/arrow_up_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1274)">
-<path d="M10.9009 5.7604C10.7345 5.92948 10.5144 6.01268 10.2943 6.01268C10.0742 6.01268 9.85471 5.92881 9.68724 5.76107L6.85903 2.93408V11.1653C6.85903 11.6401 6.47444 12 6.02437 12C5.57429 12 5.14139 11.6404 5.14139 11.1653V2.93408L2.31346 5.76013C1.97798 6.09561 1.43451 6.09561 1.09903 5.76013C0.763558 5.42466 0.763558 4.88119 1.09903 4.54571L5.39314 0.251607C5.72861 -0.0838692 6.27209 -0.0838692 6.60756 0.251607L10.9017 4.54571C11.2363 4.88253 11.2363 5.42466 10.9009 5.76013V5.7604Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1274">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/arrow_up_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.0534 7.7604C12.887 7.92948 12.667 8.01268 12.4469 8.01268C12.2268 8.01268 12.0073 7.92881 11.8398 7.76107L9.01161 4.93408V13.1653C9.01161 13.6401 8.62702 14 8.17694 14C7.72687 14 7.29397 13.6404 7.29397 13.1653V4.93408L4.46603 7.76013C4.13056 8.09561 3.58708 8.09561 3.25161 7.76013C2.91613 7.42466 2.91613 6.88119 3.25161 6.54571L7.54571 2.25161C7.88119 1.91613 8.42466 1.91613 8.76013 2.25161L13.0542 6.54571C13.3889 6.88253 13.3889 7.42466 13.0534 7.76013V7.7604Z" fill="white"/>
-</svg>

assets/icons/arrow_up_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1357)">
-<path d="M7.26724 3.84027C7.15631 3.95299 7.0096 4.00845 6.86288 4.00845C6.71617 4.00845 6.56981 3.95254 6.45816 3.84072L4.57269 1.95605V7.44356C4.57269 7.76007 4.3163 8 4.01625 8C3.7162 8 3.4276 7.76025 3.4276 7.44356V1.95605L1.54231 3.84009C1.31866 4.06374 0.956346 4.06374 0.732695 3.84009C0.509044 3.61644 0.509044 3.25412 0.732695 3.03047L3.59543 0.167738C3.81908 -0.0559128 4.1814 -0.0559128 4.40505 0.167738L7.26778 3.03047C7.49089 3.25502 7.49089 3.61644 7.26724 3.84009V3.84027Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1357">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/backspace _12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12 8.66661C12 9.40202 11.4021 9.99992 10.6667 9.99992H4.27722C3.92306 9.99992 3.58348 9.86034 3.33348 9.61034L0.195413 6.47082C0.0702071 6.34582 0 6.17707 0 5.99999C0 5.82291 0.0702071 5.65416 0.195205 5.52917L3.33328 2.39068C3.58327 2.14048 3.92285 2.00006 4.27701 2.00006H10.6665C11.4019 2.00006 11.9998 2.59693 11.9998 3.33337V8.66661H12ZM5.64594 5.00209L6.62718 5.99999L5.64594 6.97914C5.4522 7.17497 5.4522 7.49163 5.64594 7.66871C5.84177 7.88121 6.15843 7.88121 6.33552 7.66871L7.33341 6.70623L8.31256 7.66871C8.50839 7.88121 8.82506 7.88121 9.00214 7.66871C9.21463 7.49163 9.21463 7.17497 9.00214 6.97914L8.03965 5.99999L9.00214 5.00209C9.21463 4.82501 9.21463 4.50835 9.00214 4.31252C8.82506 4.11877 8.50839 4.11877 8.31256 4.31252L7.33341 5.29375L6.33552 4.31252C6.15843 4.11877 5.84177 4.11877 5.64594 4.31252C5.4522 4.50835 5.4522 4.82501 5.64594 5.00209Z" fill="white"/>
-</svg>

assets/icons/backspace _16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M15 11.1111C15 11.969 14.3024 12.6666 13.4445 12.6666H5.99009C5.5769 12.6666 5.18073 12.5038 4.88906 12.2121L1.22798 8.54932C1.08191 8.40349 1 8.20661 1 8.00002C1 7.79343 1.08191 7.59656 1.22774 7.45072L4.88882 3.78916C5.18048 3.49725 5.57666 3.33344 5.98984 3.33344H13.4442C14.3022 3.33344 14.9998 4.02978 14.9998 4.88896V11.1111H15ZM7.58693 6.8358L8.73171 8.00002L7.58693 9.14236C7.3609 9.37083 7.3609 9.74027 7.58693 9.94686C7.8154 10.1948 8.18484 10.1948 8.39143 9.94686L9.55565 8.82396L10.698 9.94686C10.9265 10.1948 11.2959 10.1948 11.5025 9.94686C11.7504 9.74027 11.7504 9.37083 11.5025 9.14236L10.3796 8.00002L11.5025 6.8358C11.7504 6.62921 11.7504 6.25977 11.5025 6.0313C11.2959 5.80527 10.9265 5.80527 10.698 6.0313L9.55565 7.17608L8.39143 6.0313C8.18484 5.80527 7.8154 5.80527 7.58693 6.0313C7.3609 6.25977 7.3609 6.62921 7.58693 6.8358Z" fill="white"/>
-</svg>

assets/icons/backspace _8.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 5.77774C8 6.26801 7.6014 6.66661 7.11113 6.66661H2.85148C2.61537 6.66661 2.38899 6.57356 2.22232 6.4069L0.130276 4.31388C0.0468047 4.23055 0 4.11805 0 3.99999C0 3.88194 0.0468047 3.76944 0.130137 3.68611L2.22218 1.59379C2.38885 1.42698 2.61523 1.33337 2.85134 1.33337H7.11099C7.60126 1.33337 7.99986 1.73128 7.99986 2.22225V5.77774H8ZM3.76396 3.33473L4.41812 3.99999L3.76396 4.65276C3.6348 4.78331 3.6348 4.99442 3.76396 5.11247C3.89452 5.25414 4.10562 5.25414 4.22368 5.11247L4.88894 4.47082L5.54171 5.11247C5.67226 5.25414 5.88337 5.25414 6.00142 5.11247C6.14309 4.99442 6.14309 4.78331 6.00142 4.65276L5.35977 3.99999L6.00142 3.33473C6.14309 3.21667 6.14309 3.00557 6.00142 2.87501C5.88337 2.74585 5.67226 2.74585 5.54171 2.87501L4.88894 3.52917L4.22368 2.87501C4.10562 2.74585 3.89452 2.74585 3.76396 2.87501C3.6348 3.00557 3.6348 3.21667 3.76396 3.33473Z" fill="white"/>
-</svg>

assets/icons/bolt_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.01442 5.33285H9.33914C9.61643 5.33285 9.86663 5.5059 9.94586 5.76443C10.0605 6.02505 9.98756 6.31903 9.77906 6.50251L4.4416 11.1728C4.206 11.3771 3.86136 11.3938 3.6095 11.2103C3.35743 11.0268 3.26569 10.6932 3.38849 10.4076L4.99202 6.66722H2.64855C2.38939 6.66722 2.14044 6.49417 2.04266 6.23563C1.9449 5.97501 2.01868 5.68104 2.22793 5.49756L7.56476 0.827491C7.80036 0.622333 8.14438 0.606695 8.39666 0.789754C8.64894 0.972937 8.74067 1.30578 8.61766 1.59237L7.01434 5.33256L7.01442 5.33285Z" fill="white"/>
-</svg>

assets/icons/bolt_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.33035 7.12505H12.3791C12.7427 7.12505 13.0708 7.352 13.1747 7.69105C13.3251 8.03283 13.2294 8.41836 12.956 8.65898L5.95625 14.7837C5.64728 15.0517 5.19531 15.0736 4.86501 14.833C4.53443 14.5923 4.41413 14.1549 4.57517 13.7803L6.6781 8.87499H3.60478C3.26491 8.87499 2.93844 8.64804 2.8102 8.30899C2.68199 7.96721 2.77876 7.58168 3.05317 7.34106L10.0521 1.21657C10.3611 0.947515 10.8122 0.927008 11.1431 1.16708C11.4739 1.40731 11.5942 1.84381 11.4329 2.21966L9.33024 7.12467L9.33035 7.12505Z" fill="white"/>
-</svg>

assets/icons/bolt_slash_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1336)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M7.01443 5.33285H9.33914C9.61644 5.33285 9.86663 5.5059 9.94586 5.76443C10.0605 6.02505 9.98756 6.31903 9.77906 6.50251L9.11489 7.08366L11.4605 8.90799C11.7874 9.16229 11.8463 9.6335 11.592 9.96045C11.3377 10.2874 10.8665 10.3463 10.5395 10.092L1.53955 3.09201C1.21259 2.83771 1.15369 2.3665 1.40799 2.03954C1.66229 1.71258 2.1335 1.65368 2.46046 1.90799L4.50911 3.50138L7.56477 0.827491C7.80037 0.622333 8.14438 0.606695 8.39666 0.789754C8.64894 0.972937 8.74068 1.30578 8.61767 1.59237L7.01434 5.33256L7.01443 5.33285ZM2.88883 4.91923L7.49455 8.50146L4.4416 11.1728C4.206 11.3771 3.86136 11.3938 3.6095 11.2103C3.35743 11.0268 3.26569 10.6932 3.3885 10.4076L4.99203 6.66722H2.64855C2.38939 6.66722 2.14045 6.49416 2.04266 6.23563C1.9449 5.97501 2.01869 5.68104 2.22793 5.49756L2.88883 4.91923Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1336">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/bolt_slash_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.42392 5.20324L10.1066 1.21711C10.4244 0.948067 10.8885 0.92756 11.2288 1.16762C11.5691 1.40784 11.6928 1.84432 11.5269 2.22016L9.36412 7.12497H12.5C12.874 7.12497 13.2115 7.35191 13.3184 7.69094C13.4731 8.03271 13.3746 8.41823 13.0934 8.65883L11.6112 9.91928L15.5 13C15.7925 13.2242 15.8459 13.6371 15.6153 13.9214C15.3847 14.2058 14.96 14.2577 14.6675 14.0335L0.499984 3.5C0.206368 3.27635 0.1549 2.86403 0.384956 2.57859C0.615012 2.29314 1.03884 2.24311 1.33246 2.46703L5.42392 5.20324ZM2.90681 7.34135L3.35399 6.9367L5.85423 8.87522H3.50023C3.12618 8.87522 2.78869 8.64828 2.65651 8.30924C2.52714 7.96747 2.62557 7.58196 2.90681 7.34135ZM4.47333 13.7803L6.43921 9.32362L9.53006 11.6668L5.8936 14.7837C5.5758 15.0517 5.11175 15.0736 4.77144 14.8329C4.43114 14.5923 4.3074 14.1549 4.47333 13.7803Z" fill="white"/>
-</svg>

assets/icons/bolt_slash_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_702_132)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M4.67628 3.55525H6.22609C6.41096 3.55525 6.57775 3.67062 6.63057 3.84298C6.70702 4.01672 6.65837 4.21271 6.51937 4.33502L6.07659 4.72246L7.6403 5.93868C7.85828 6.10821 7.89754 6.42235 7.72801 6.64032C7.55847 6.8583 7.24434 6.89756 7.02636 6.72803L1.02636 2.06136C0.808388 1.89183 0.769121 1.57769 0.938656 1.35972C1.10819 1.14174 1.42233 1.10248 1.6403 1.27201L3.00607 2.33428L5.04318 0.551681C5.20024 0.414909 5.42959 0.404484 5.59777 0.526523C5.76596 0.648645 5.82712 0.870539 5.74511 1.0616L4.67622 3.55506L4.67628 3.55525ZM3.25023 4.62627L4.80474 5.83533L2.96106 7.44854C2.804 7.58476 2.57424 7.59588 2.40633 7.47356C2.23828 7.35125 2.17713 7.12885 2.25899 6.93843L3.25023 4.62627ZM1.73426 3.44719L3.01695 4.44483H1.7657C1.59292 4.44483 1.42696 4.32946 1.36177 4.15711C1.2966 3.98336 1.34579 3.78738 1.48528 3.66506L1.73426 3.44719Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_702_132">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/caret_down_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.6709 4.44444L6.89463 9.25449C6.62782 9.49123 6.31967 9.60772 6.01153 9.60772C5.70339 9.60772 5.39599 9.49029 5.16113 9.25542L0.384899 4.44537C0.00723509 4.10247 -0.09573 3.5837 0.0902836 3.13633C0.276448 2.68914 0.71529 2.39227 1.20118 2.39227H10.7875C11.2737 2.39227 11.7126 2.68463 11.899 3.13445C12.0854 3.58389 12.0163 4.1021 11.6706 4.44406L11.6709 4.44444Z" fill="white"/>
-</svg>

assets/icons/caret_down_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M14.616 6.18521L9.04373 11.7969C8.73246 12.0731 8.37295 12.209 8.01345 12.209C7.65395 12.209 7.29533 12.072 7.02132 11.798L1.44905 6.1863C1.00844 5.78625 0.888315 5.18101 1.10533 4.65908C1.32252 4.13736 1.8345 3.79102 2.40138 3.79102H13.5854C14.1527 3.79102 14.6648 4.1321 14.8822 4.65689C15.0997 5.18123 15.019 5.78581 14.6156 6.18477L14.616 6.18521Z" fill="white"/>
-</svg>

assets/icons/caret_down_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1359)">
-<path d="M6.36286 3.35324L4.37276 5.35315C4.26159 5.45158 4.1332 5.50002 4.0048 5.50002C3.87641 5.50002 3.74833 5.45119 3.65047 5.35354L1.66037 3.35363C1.50301 3.21106 1.46011 2.99537 1.53762 2.80936C1.61519 2.62343 1.79804 2.5 2.00049 2.5H5.99478C6.19739 2.5 6.38027 2.62156 6.45793 2.80858C6.53559 2.99545 6.50678 3.2109 6.36273 3.35309L6.36286 3.35324Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1359">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/caret_left_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.55575 11.671L2.74629 6.89533C2.51033 6.62855 2.3931 6.32045 2.3931 5.97853C2.3931 5.6366 2.51052 5.36307 2.74516 5.12823L7.55462 0.352587C7.89842 0.00841046 8.41544 -0.0945422 8.8652 0.0914486C9.31496 0.277439 9.60691 0.717805 9.60691 1.20251V10.7876C9.60691 11.2738 9.31421 11.7127 8.86482 11.899C8.41506 12.0854 7.8973 12.0163 7.55537 11.6706L7.55575 11.671Z" fill="white"/>
-</svg>

assets/icons/caret_left_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.81503 14.6161L4.20399 9.04455C3.9287 8.73331 3.79193 8.37386 3.79193 7.97495C3.79193 7.57604 3.92892 7.25691 4.20268 6.98293L9.81371 1.41135C10.2148 1.00981 10.818 0.889701 11.3427 1.10669C11.8674 1.32368 12.208 1.83744 12.208 2.40293V13.5855C12.208 14.1528 11.8666 14.6648 11.3423 14.8822C10.8176 15.0996 10.2135 15.019 9.81459 14.6157L9.81503 14.6161Z" fill="white"/>
-</svg>

assets/icons/caret_left_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1360)">
-<path d="M4.64677 6.36286L2.64687 4.37276C2.54843 4.26159 2.5 4.1332 2.5 4.0048C2.5 3.87641 2.54882 3.74833 2.64648 3.65047L4.64638 1.66037C4.78895 1.50301 5.00465 1.46011 5.19065 1.53762C5.37658 1.61519 5.50002 1.79804 5.50002 2.00049L5.50002 5.99478C5.50002 6.19739 5.37846 6.38027 5.19144 6.45793C5.00457 6.53559 4.78911 6.50678 4.64693 6.36273L4.64677 6.36286Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1360">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/caret_right_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.43898 0.355319L9.26566 5.14806C9.50322 5.41579 9.62012 5.725 9.62012 6.00027C9.62012 6.27554 9.50228 6.61793 9.2666 6.85361L4.43992 11.6463C4.09451 11.9914 3.57602 12.0951 3.12465 11.908C2.67328 11.721 2.37991 11.3469 2.37991 10.8266V1.20715C2.37991 0.719205 2.67328 0.278771 3.12465 0.0917373C3.57565 -0.0952964 4.09565 0.0091557 4.43879 0.354942L4.43898 0.355319Z" fill="white"/>
-</svg>

assets/icons/caret_right_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.1788 1.41454L11.8099 7.00607C12.0871 7.31842 12.2235 7.67916 12.2235 8.00031C12.2235 8.32146 12.086 8.72092 11.811 8.99588L6.1799 14.5874C5.77692 14.9899 5.17201 15.1109 4.64542 14.8927C4.11882 14.6745 3.77655 14.2381 3.77655 13.631V2.40834C3.77655 1.83907 4.11882 1.32523 4.64542 1.10703C5.17157 0.888821 5.77824 1.01068 6.17858 1.4141L6.1788 1.41454Z" fill="white"/>
-</svg>

assets/icons/caret_right_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1361)">
-<path d="M3.35323 1.63714L5.35313 3.62724C5.45157 3.73841 5.5 3.8668 5.5 3.9952C5.5 4.12359 5.45118 4.25167 5.35352 4.34953L3.35362 6.33963C3.21105 6.49699 2.99535 6.53989 2.80935 6.46238C2.62342 6.38481 2.49998 6.20196 2.49998 5.99951L2.49998 2.00522C2.49998 1.80261 2.62154 1.61973 2.80856 1.54207C2.99543 1.46441 3.21089 1.49322 3.35307 1.63727L3.35323 1.63714Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1361">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/caret_up_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M0.353682 7.55684L5.13301 2.74366C5.40037 2.50677 5.70871 2.3902 6.01705 2.3902C6.3254 2.3902 6.63299 2.50771 6.86801 2.74272L11.6473 7.5559C11.9914 7.89996 12.0948 8.41738 11.9083 8.86748C11.7218 9.31759 11.3149 9.60977 10.796 9.60977H1.20388C0.717302 9.60977 0.2781 9.31684 0.0915901 8.86711C-0.0949202 8.417 0.00886378 7.89883 0.353306 7.55665L0.353682 7.55684Z" fill="white"/>
-</svg>

assets/icons/caret_up_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M1.41263 9.81632L6.98851 4.20095C7.30043 3.92457 7.66016 3.78857 8.0199 3.78857C8.37963 3.78857 8.73849 3.92567 9.01268 4.19986L14.5886 9.81522C14.99 10.2166 15.1106 10.8203 14.893 11.3454C14.6754 11.8705 14.2007 12.2114 13.5953 12.2114H2.40453C1.83685 12.2114 1.32445 11.8697 1.10686 11.345C0.88926 10.8198 1.01034 10.2153 1.41219 9.8161L1.41263 9.81632Z" fill="white"/>
-</svg>

assets/icons/caret_up_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1362)">
-<path d="M1.63714 4.64676L3.62724 2.64685C3.73841 2.54842 3.8668 2.49998 3.9952 2.49998C4.12359 2.49998 4.25167 2.54881 4.34953 2.64646L6.33963 4.64637C6.49699 4.78894 6.53989 5.00463 6.46238 5.19064C6.38481 5.37657 6.20196 5.5 5.99951 5.5L2.00522 5.5C1.80261 5.5 1.61973 5.37844 1.54207 5.19142C1.46441 5.00455 1.49322 4.7891 1.63727 4.64691L1.63714 4.64676Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1362">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/channel_hash.svg πŸ”—

@@ -1,6 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<line x1="10.2795" y1="2.63847" x2="7.74785" y2="11.0142" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<line x1="6.26624" y1="2.99597" x2="3.7346" y2="11.3717" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<line x1="3.15982" y1="5.3799" x2="11.9098" y2="5.3799" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<line x1="2.0983" y1="8.62407" x2="10.8483" y2="8.62407" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-</svg>

assets/icons/check_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.7489 1.96607C12.0837 2.3009 12.0837 2.842 11.7489 3.17684L4.89143 10.0343C4.55659 10.3691 4.01549 10.3691 3.68066 10.0343L0.251131 6.60556C-0.0837057 6.27072 -0.0837057 5.72963 0.251104 5.39479C0.585887 5.05995 1.12859 5.05995 1.46343 5.39479L4.26185 8.21545L10.538 1.96607C10.8729 1.6307 11.414 1.6307 11.7488 1.96607H11.7489Z" fill="white"/>
-</svg>

assets/icons/check_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.7489 4.25153C14.0837 4.58637 14.0837 5.12746 13.7489 5.4623L6.89143 12.3197C6.55659 12.6546 6.01549 12.6546 5.68066 12.3197L2.25113 8.89102C1.91629 8.55618 1.91629 8.01509 2.2511 7.68025C2.58589 7.34542 3.12859 7.34542 3.46343 7.68025L6.26185 10.5009L12.538 4.25153C12.8729 3.91616 13.414 3.91616 13.7488 4.25153H13.7489Z" fill="white"/>
-</svg>

assets/icons/check_8.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.83258 1.31075C8.05581 1.53398 8.05581 1.89471 7.83258 2.11793L3.26095 6.68956C3.03773 6.91279 2.677 6.91279 2.45377 6.68956L0.16742 4.40375C-0.0558038 4.18052 -0.0558038 3.81979 0.167403 3.59657C0.390591 3.37334 0.752393 3.37334 0.975617 3.59657L2.84124 5.47701L7.02535 1.31075C7.24857 1.08717 7.6093 1.08717 7.83253 1.31075H7.83258Z" fill="white"/>
-</svg>

assets/icons/chevron_down_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.9996 9.42843C5.78023 9.42843 5.56087 9.34473 5.39373 9.17733L0.251105 4.0347C-0.0837016 3.69989 -0.0837016 3.15751 0.251105 2.8227C0.585911 2.48789 1.1283 2.48789 1.4631 2.8227L5.9996 7.36067L10.5369 2.82337C10.8717 2.48856 11.4141 2.48856 11.7489 2.82337C12.0837 3.15818 12.0837 3.70056 11.7489 4.03537L6.60627 9.178C6.43886 9.3454 6.21923 9.42843 5.9996 9.42843Z" fill="white"/>
-</svg>

assets/icons/chevron_down_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.99953 11.9998C7.74361 11.9998 7.48768 11.9022 7.29269 11.7069L1.29296 5.70714C0.902348 5.31653 0.902348 4.68375 1.29296 4.29314C1.68356 3.90253 2.31635 3.90253 2.70696 4.29314L7.99953 9.58743L13.293 4.29392C13.6837 3.90331 14.3164 3.90331 14.707 4.29392C15.0977 4.68453 15.0977 5.31731 14.707 5.70792L8.70731 11.7077C8.51201 11.903 8.25577 11.9998 7.99953 11.9998Z" fill="white"/>
-</svg>

assets/icons/chevron_down_8.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.99973 6.28564C3.85349 6.28564 3.70725 6.22984 3.59582 6.11824L0.167403 2.68982C-0.0558011 2.46662 -0.0558011 2.10502 0.167403 1.88182C0.390608 1.65862 0.752199 1.65862 0.975403 1.88182L3.99973 4.90713L7.0246 1.88227C7.2478 1.65906 7.60939 1.65906 7.8326 1.88227C8.0558 2.10547 8.0558 2.46706 7.8326 2.69027L4.40418 6.11868C4.29258 6.23029 4.14615 6.28564 3.99973 6.28564Z" fill="white"/>
-</svg>

assets/icons/chevron_left_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.57103 12C8.35166 12 8.13228 11.9163 7.96514 11.7489L2.82228 6.60603C2.48746 6.27121 2.48746 5.72879 2.82228 5.39397L7.96514 0.251116C8.29996 -0.0837054 8.84237 -0.0837054 9.17719 0.251116C9.51202 0.585938 9.51202 1.12835 9.17719 1.46317L4.64023 6L9.17773 10.5375C9.51255 10.8723 9.51255 11.4147 9.17773 11.7496C9.01032 11.917 8.79068 12 8.57103 12Z" fill="white"/>
-</svg>

assets/icons/chevron_left_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.9995 15C10.7436 15 10.4877 14.9023 10.2927 14.707L4.29266 8.70703C3.90204 8.31641 3.90204 7.68359 4.29266 7.29297L10.2927 1.29297C10.6833 0.902344 11.3161 0.902344 11.7067 1.29297C12.0974 1.68359 12.0974 2.31641 11.7067 2.70703L6.4136 8L11.7074 13.2937C12.098 13.6844 12.098 14.3172 11.7074 14.7078C11.512 14.9031 11.2558 15 10.9995 15Z" fill="white"/>
-</svg>

assets/icons/chevron_left_8.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.71402 8C5.56777 8 5.42152 7.9442 5.31009 7.83259L1.88152 4.40402C1.65831 4.1808 1.65831 3.8192 1.88152 3.59598L5.31009 0.167411C5.53331 -0.0558036 5.89491 -0.0558036 6.11813 0.167411C6.34134 0.390625 6.34134 0.752232 6.11813 0.975446L3.09349 4L6.11849 7.025C6.3417 7.24821 6.3417 7.60982 6.11849 7.83304C6.00688 7.94464 5.86045 8 5.71402 8Z" fill="white"/>
-</svg>

assets/icons/chevron_right_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.4284 12C3.20902 12 2.98964 11.9163 2.8225 11.7489C2.48767 11.4141 2.48767 10.8716 2.8225 10.5368L7.36059 5.99993L2.8225 1.46319C2.48767 1.12836 2.48767 0.585944 2.8225 0.251119C3.15732 -0.0837063 3.69974 -0.0837063 4.03457 0.251119L9.17748 5.39403C9.51231 5.72886 9.51231 6.27128 9.17748 6.6061L4.03457 11.749C3.86769 11.917 3.64804 12 3.4284 12Z" fill="white"/>
-</svg>

assets/icons/chevron_right_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.99982 15C4.74388 15 4.48794 14.9023 4.29294 14.707C3.90231 14.3164 3.90231 13.6836 4.29294 13.2929L9.58738 7.99992L4.29294 2.70705C3.90231 2.31642 3.90231 1.6836 4.29294 1.29297C4.68357 0.902343 5.31639 0.902343 5.70702 1.29297L11.7071 7.29304C12.0977 7.68367 12.0977 8.31649 11.7071 8.70712L5.70702 14.7072C5.51233 14.9031 5.25608 15 4.99982 15Z" fill="white"/>
-</svg>

assets/icons/chevron_right_8.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.28561 8C2.13936 8 1.99311 7.9442 1.88168 7.83259C1.65846 7.60937 1.65846 7.24776 1.88168 7.02454L4.90707 3.99996L1.88168 0.975457C1.65846 0.752241 1.65846 0.390629 1.88168 0.167413C2.10489 -0.0558042 2.4665 -0.0558042 2.68972 0.167413L6.11833 3.59602C6.34155 3.81924 6.34155 4.18085 6.11833 4.40407L2.68972 7.83268C2.57847 7.94464 2.43204 8 2.28561 8Z" fill="white"/>
-</svg>

assets/icons/chevron_up_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.1435 9.42655C10.9242 9.42655 10.7048 9.34284 10.5376 9.17543L6.00067 4.63994L1.46317 9.17744C1.12835 9.51226 0.585938 9.51226 0.251116 9.17744C-0.0837054 8.84262 -0.0837054 8.30021 0.251116 7.96538L5.39397 2.82253C5.72879 2.48771 6.27121 2.48771 6.60603 2.82253L11.7489 7.96538C12.0837 8.30021 12.0837 8.84262 11.7489 9.17744C11.5828 9.34351 11.3632 9.42655 11.1435 9.42655Z" fill="white"/>
-</svg>

assets/icons/chevron_up_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M14.0008 11.9977C13.7448 11.9977 13.4889 11.9 13.2939 11.7047L8.00078 6.41328L2.70703 11.707C2.31641 12.0977 1.68359 12.0977 1.29297 11.707C0.902344 11.3164 0.902344 10.6836 1.29297 10.293L7.29297 4.29297C7.68359 3.90234 8.31641 3.90234 8.70703 4.29297L14.707 10.293C15.0977 10.6836 15.0977 11.3164 14.707 11.707C14.5133 11.9008 14.257 11.9977 14.0008 11.9977Z" fill="white"/>
-</svg>

assets/icons/chevron_up_8.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.42902 6.28438C7.28277 6.28438 7.13652 6.22858 7.02509 6.11697L4.00045 3.09331L0.975446 6.11831C0.752232 6.34153 0.390625 6.34153 0.167411 6.11831C-0.0558036 5.8951 -0.0558036 5.53349 0.167411 5.31028L3.59598 1.88171C3.8192 1.65849 4.1808 1.65849 4.40402 1.88171L7.83259 5.31028C8.0558 5.53349 8.0558 5.8951 7.83259 6.11831C7.72188 6.22903 7.57545 6.28438 7.42902 6.28438Z" fill="white"/>
-</svg>

assets/icons/circle_check_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1285)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M6 12C9.31371 12 12 9.31371 12 6C12 2.68629 9.31371 0 6 0C2.68629 0 0 2.68629 0 6C0 9.31371 2.68629 12 6 12ZM9.53033 4.28033L8.46967 3.21967L4.875 6.81434L3.53033 5.46967L2.46967 6.53033L4.875 8.93566L9.53033 4.28033Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1285">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/circle_check_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1370)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M4 8C6.20914 8 8 6.20914 8 4C8 1.79086 6.20914 0 4 0C1.79086 0 0 1.79086 0 4C0 6.20914 1.79086 8 4 8ZM6.35355 2.85355L5.64645 2.14645L3.25 4.54289L2.35355 3.64645L1.64645 4.35355L3.25 5.95711L6.35355 2.85355Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1370">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/circle_info_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1294)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M12 6C12 9.31371 9.31371 12 6 12C2.68629 12 0 9.31371 0 6C0 2.68629 2.68629 0 6 0C9.31371 0 12 2.68629 12 6ZM5.25 6H4.5V4.5H6.75V8.25H7.5V9.75H5.25V6ZM6.75 3.75V2.25H5.25V3.75H6.75Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1294">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/circle_info_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8ZM7.125 8H6.25V6.25H8.875V10.625H9.75V12.375H7.125V8ZM8.875 5.375V3.625H7.125V5.375H8.875Z" fill="white"/>
-</svg>

assets/icons/circle_info_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1372)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8 4C8 6.20914 6.20914 8 4 8C1.79086 8 0 6.20914 0 4C0 1.79086 1.79086 0 4 0C6.20914 0 8 1.79086 8 4ZM3.5 4H3V3H4.5V5.5H5V6.5H3.5V4ZM4.5 2.5V1.5H3.5V2.5H4.5Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1372">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/circle_up_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1287)">
-<path d="M6 0C2.68594 0 0 2.68594 0 6C0 9.31406 2.68594 12 6 12C9.31406 12 12 9.31406 12 6C12 2.68594 9.31406 0 6 0ZM8.97187 5.76797C8.91328 5.90859 8.77734 6 8.625 6H7.125V8.25C7.125 8.66414 6.78914 9 6.375 9H5.625C5.21086 9 4.875 8.66414 4.875 8.25V6H3.375C3.22266 6 3.08672 5.90859 3.02812 5.76797C2.96953 5.62734 3.00234 5.46797 3.11016 5.36016L5.73516 2.73516C5.88141 2.58867 6.11906 2.58867 6.26531 2.73516L8.89031 5.36016C8.99766 5.46797 9.03047 5.62734 8.97187 5.76797Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1287">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/circle_up_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 1C4.13359 1 1 4.13359 1 8C1 11.8664 4.13359 15 8 15C11.8664 15 15 11.8664 15 8C15 4.13359 11.8664 1 8 1ZM11.4672 7.7293C11.3988 7.89336 11.2402 8 11.0625 8H9.3125V10.625C9.3125 11.1082 8.92066 11.5 8.4375 11.5H7.5625C7.07934 11.5 6.6875 11.1082 6.6875 10.625V8H4.9375C4.75977 8 4.60117 7.89336 4.53281 7.7293C4.46445 7.56523 4.50273 7.3793 4.62852 7.25352L7.69102 4.19102C7.86164 4.02012 8.13891 4.02012 8.30953 4.19102L11.372 7.25352C11.4973 7.3793 11.5355 7.56523 11.4672 7.7293Z" fill="white"/>
-</svg>

assets/icons/circle_up_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1376)">
-<path d="M4 0C1.79063 0 0 1.79063 0 4C0 6.20937 1.79063 8 4 8C6.20937 8 8 6.20937 8 4C8 1.79063 6.20937 0 4 0ZM5.98125 3.84531C5.94219 3.93906 5.85156 4 5.75 4H4.75V5.5C4.75 5.77609 4.52609 6 4.25 6H3.75C3.47391 6 3.25 5.77609 3.25 5.5V4H2.25C2.14844 4 2.05781 3.93906 2.01875 3.84531C1.97969 3.75156 2.00156 3.64531 2.07344 3.57344L3.82344 1.82344C3.92094 1.72578 4.07938 1.72578 4.17688 1.82344L5.92688 3.57344C5.99844 3.64531 6.02031 3.75156 5.98125 3.84531Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1376">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/circle_x_mark_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1292)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M6 12C9.31371 12 12 9.31371 12 6C12 2.68629 9.31371 0 6 0C2.68629 0 0 2.68629 0 6C0 9.31371 2.68629 12 6 12ZM3.21967 4.28033L4.93934 6L3.21967 7.71967L4.28033 8.78033L6 7.06066L7.71967 8.78033L8.78033 7.71967L7.06066 6L8.78033 4.28033L7.71967 3.21967L6 4.93934L4.28033 3.21967L3.21967 4.28033Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1292">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/circle_x_mark_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM4.75628 5.99372L6.76256 8L4.75628 10.0063L5.99372 11.2437L8 9.23744L10.0063 11.2437L11.2437 10.0063L9.23744 8L11.2437 5.99372L10.0063 4.75628L8 6.76256L5.99372 4.75628L4.75628 5.99372Z" fill="white"/>
-</svg>

assets/icons/circle_x_mark_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1365)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M4 8C6.20914 8 8 6.20914 8 4C8 1.79086 6.20914 0 4 0C1.79086 0 0 1.79086 0 4C0 6.20914 1.79086 8 4 8ZM2.14645 2.85355L3.29289 4L2.14645 5.14645L2.85355 5.85355L4 4.70711L5.14645 5.85355L5.85355 5.14645L4.70711 4L5.85355 2.85355L5.14645 2.14645L4 3.29289L2.85355 2.14645L2.14645 2.85355Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1365">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/cloud_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M1.80375 4.95186C1.80131 4.90124 1.8 4.85061 1.8 4.79999C1.8 3.14249 3.1425 1.79999 4.8 1.79999C5.91188 1.79999 6.88125 2.40468 7.40063 3.30374C7.68563 3.09561 8.03063 2.99999 8.4 2.99999C9.39375 2.99999 10.2 3.78936 10.2 4.79999C10.2 5.02874 10.1569 5.24624 10.08 5.44874C11.175 5.66999 12 6.63936 12 7.79999C12 9.12561 10.9256 10.2 9.6 10.2H2.7C1.20881 10.2 0 8.99061 0 7.49999C0 6.32249 0.753187 5.32124 1.80375 4.95186Z" fill="white"/>
-</svg>

assets/icons/cloud_8.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M1.2025 3.30126C1.20087 3.26751 1.2 3.23376 1.2 3.20001C1.2 2.09501 2.095 1.20001 3.2 1.20001C3.94125 1.20001 4.5875 1.60314 4.93375 2.20251C5.12375 2.06376 5.35375 2.00001 5.6 2.00001C6.2625 2.00001 6.8 2.52626 6.8 3.20001C6.8 3.35251 6.77125 3.49751 6.72 3.63251C7.45 3.78001 8 4.42626 8 5.20001C8 6.08376 7.28375 6.80001 6.4 6.80001H1.8C0.805875 6.80001 0 5.99376 0 5.00001C0 4.21501 0.502125 3.54751 1.2025 3.30126Z" fill="white"/>
-</svg>

assets/icons/cloud_slash_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1352)">
-<path d="M1.73261 1.8415C2.0976 1.44713 2.62009 1.20014 3.20007 1.20014C3.9413 1.20014 4.58753 1.60325 4.93377 2.20261C5.12376 2.06387 5.35376 2.00012 5.6 2.00012C6.26248 2.00012 6.79997 2.52635 6.79997 3.20008C6.79997 3.35258 6.77122 3.49757 6.71997 3.63257C7.44995 3.78007 7.99993 4.4263 7.99993 5.20002C7.99993 5.65751 7.80744 6.07 7.48869 6.35124L7.88493 6.66373C8.01493 6.76623 8.03868 6.95497 7.93618 7.08497C7.83368 7.21496 7.64494 7.23871 7.51494 7.13622L0.115037 1.33626C-0.0154588 1.23402 -0.0383331 1.04552 0.0639139 0.915025C0.166161 0.784529 0.35453 0.761655 0.485114 0.863902L1.73245 1.84125L1.73261 1.8415ZM1.21475 2.95759L6.09249 6.79998H1.80011C0.806017 6.79998 0.000165731 5.99375 0.000165731 5.00003C0.000165731 4.21505 0.502276 3.54757 1.20263 3.30133C1.20101 3.26758 1.20013 3.23383 1.20013 3.20008C1.20013 3.11759 1.20513 3.03634 1.21475 2.95759Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1352">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/copilot_16.svg πŸ”—

@@ -1,12 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M4.2926 3.48996C3.79162 3.79616 3.44871 4.26316 3.44871 4.93872C3.44871 5.75753 3.65302 6.19648 3.88658 6.43349C4.11948 6.66983 4.47018 6.79529 4.95638 6.79529C5.64158 6.79529 6.23176 6.65786 6.64548 6.37099C7.03216 6.10286 7.32149 5.66636 7.35698 4.91278C7.38386 4.34213 7.36863 3.96084 7.21748 3.68905C7.09721 3.47279 6.81682 3.2089 5.96976 3.11109C5.4731 3.05374 4.81346 3.17162 4.2926 3.48996ZM3.72539 2.5525C4.46348 2.10138 5.36842 1.93724 6.09436 2.02107C7.1336 2.14107 7.8142 2.51324 8.17039 3.15373C8.49569 3.73867 8.47238 4.43479 8.44743 4.96466C8.39736 6.02772 7.95809 6.7938 7.26541 7.27411C6.59976 7.73566 5.75982 7.89249 4.95638 7.89249C4.2936 7.89249 3.61755 7.71967 3.11095 7.20558C2.605 6.69216 2.35705 5.92853 2.35705 4.93872C2.35705 3.80566 2.96744 3.01576 3.72539 2.5525Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M6.69546 8.97734C7.02432 8.97734 7.29091 9.24528 7.29091 9.57581V10.8725C7.29091 11.203 7.02432 11.471 6.69546 11.471C6.3666 11.471 6.1 11.203 6.1 10.8725V9.57581C6.1 9.24528 6.3666 8.97734 6.69546 8.97734Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M2.45301 7.32072C2.56382 6.90477 2.81104 6.35118 3.40175 6.17048L3.74851 7.31556C3.74509 7.31822 3.73425 7.32798 3.71842 7.35038C3.68409 7.39897 3.64151 7.48723 3.6034 7.6303C3.52629 7.91973 3.49839 8.31081 3.4984 8.73318V10.8761C3.5122 10.9688 3.52011 11.0083 3.53501 11.0478C3.5474 11.0807 3.57295 11.1339 3.6523 11.2153C3.83266 11.4004 4.24428 11.6866 5.21016 12.1174C5.99398 12.467 6.35125 12.6243 6.68361 12.7078C6.99799 12.7869 7.30564 12.8031 7.99999 12.8031V14C7.31311 14 6.86876 13.9882 6.3946 13.869C5.95125 13.7575 5.49691 13.5549 4.78914 13.2391C4.76868 13.23 4.74801 13.2208 4.72712 13.2115C3.73729 12.77 3.14865 12.4092 2.80139 12.0527C2.61692 11.8634 2.49682 11.6721 2.42136 11.4719C2.35507 11.2961 2.33141 11.1302 2.31663 11.0266C2.31561 11.0194 2.31463 11.0126 2.31369 11.0061L2.30749 10.9632V8.73321C2.30748 8.28334 2.33457 7.76532 2.45301 7.32072Z" fill="white"/>

assets/icons/copy.svg πŸ”—

@@ -1,5 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect x="2" y="5.64062" width="6.35938" height="6.35938" rx="0.5" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
-<path d="M8.01562 3.75H5.625V2.03125H11.9375V8.39062H10.2656V6C10.2656 4.75736 9.25827 3.75 8.01562 3.75Z" fill="black" fill-opacity="0.5"/>
-<path d="M5.625 3.125V2.5C5.625 2.22386 5.84886 2 6.125 2H11.5C11.7761 2 12 2.22386 12 2.5V7.875C12 8.15114 11.7761 8.375 11.5 8.375H10.8906" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-</svg>

assets/icons/delete_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12 8.66661C12 9.40202 11.4021 9.99992 10.6667 9.99992H4.27722C3.92306 9.99992 3.58348 9.86034 3.33348 9.61034L0.195413 6.47082C0.0702071 6.34582 0 6.17707 0 5.99999C0 5.82291 0.0702071 5.65416 0.195205 5.52917L3.33328 2.39068C3.58327 2.14048 3.92285 2.00006 4.27701 2.00006H10.6665C11.4019 2.00006 11.9998 2.59693 11.9998 3.33337V8.66661H12ZM5.64594 5.00209L6.62718 5.99999L5.64594 6.97914C5.4522 7.17497 5.4522 7.49163 5.64594 7.66871C5.84177 7.88121 6.15843 7.88121 6.33552 7.66871L7.33341 6.70623L8.31256 7.66871C8.50839 7.88121 8.82506 7.88121 9.00214 7.66871C9.21463 7.49163 9.21463 7.17497 9.00214 6.97914L8.03965 5.99999L9.00214 5.00209C9.21463 4.82501 9.21463 4.50835 9.00214 4.31252C8.82506 4.11877 8.50839 4.11877 8.31256 4.31252L7.33341 5.29375L6.33552 4.31252C6.15843 4.11877 5.84177 4.11877 5.64594 4.31252C5.4522 4.50835 5.4522 4.82501 5.64594 5.00209Z" fill="white"/>
-</svg>

assets/icons/delete_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M15 11.1111C15 11.969 14.3024 12.6666 13.4445 12.6666H5.99009C5.5769 12.6666 5.18073 12.5038 4.88906 12.2121L1.22798 8.54932C1.08191 8.40349 1 8.20661 1 8.00002C1 7.79343 1.08191 7.59656 1.22774 7.45072L4.88882 3.78916C5.18048 3.49725 5.57666 3.33344 5.98984 3.33344H13.4442C14.3022 3.33344 14.9998 4.02978 14.9998 4.88896V11.1111H15ZM7.58693 6.8358L8.73171 8.00002L7.58693 9.14236C7.3609 9.37083 7.3609 9.74027 7.58693 9.94686C7.8154 10.1948 8.18484 10.1948 8.39143 9.94686L9.55565 8.82396L10.698 9.94686C10.9265 10.1948 11.2959 10.1948 11.5025 9.94686C11.7504 9.74027 11.7504 9.37083 11.5025 9.14236L10.3796 8.00002L11.5025 6.8358C11.7504 6.62921 11.7504 6.25977 11.5025 6.0313C11.2959 5.80527 10.9265 5.80527 10.698 6.0313L9.55565 7.17608L8.39143 6.0313C8.18484 5.80527 7.8154 5.80527 7.58693 6.0313C7.3609 6.25977 7.3609 6.62921 7.58693 6.8358Z" fill="white"/>
-</svg>

assets/icons/delete_8.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 5.77774C8 6.26801 7.6014 6.66661 7.11113 6.66661H2.85148C2.61537 6.66661 2.38899 6.57356 2.22232 6.4069L0.130276 4.31388C0.0468047 4.23055 0 4.11805 0 3.99999C0 3.88194 0.0468047 3.76944 0.130137 3.68611L2.22218 1.59379C2.38885 1.42698 2.61523 1.33337 2.85134 1.33337H7.11099C7.60126 1.33337 7.99986 1.73128 7.99986 2.22225V5.77774H8ZM3.76396 3.33473L4.41812 3.99999L3.76396 4.65276C3.6348 4.78331 3.6348 4.99442 3.76396 5.11247C3.89452 5.25414 4.10562 5.25414 4.22368 5.11247L4.88894 4.47082L5.54171 5.11247C5.67226 5.25414 5.88337 5.25414 6.00142 5.11247C6.14309 4.99442 6.14309 4.78331 6.00142 4.65276L5.35977 3.99999L6.00142 3.33473C6.14309 3.21667 6.14309 3.00557 6.00142 2.87501C5.88337 2.74585 5.67226 2.74585 5.54171 2.87501L4.88894 3.52917L4.22368 2.87501C4.10562 2.74585 3.89452 2.74585 3.76396 2.87501C3.6348 3.00557 3.6348 3.21667 3.76396 3.33473Z" fill="white"/>
-</svg>

assets/icons/disable_screen_sharing_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11 0.666656H1C0.447917 0.666656 0 1.11457 0 1.66666V8.33332C0 8.88541 0.447917 9.33332 1 9.33332H5L4.66667 10.3333H3.16667C2.89167 10.3333 2.66667 10.5583 2.66667 10.8333C2.66667 11.1083 2.89167 11.3333 3.16667 11.3333H8.83333C9.10938 11.3333 9.33333 11.1094 9.33333 10.8333C9.33333 10.5573 9.10938 10.3333 8.83333 10.3333H7.33333L7 9.33332H11C11.5521 9.33332 12 8.88541 12 8.33332V1.66666C12 1.11457 11.5521 0.666656 11 0.666656ZM10.6667 7.99999H1.33333V1.99999H10.6667V7.99999Z" fill="#979DB4"/>
-</svg>

assets/icons/dock_bottom_12.svg πŸ”—

@@ -1,11 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_702_214)">
-<rect x="0.5" y="0.5" width="11" height="11" rx="1" stroke="white" stroke-opacity="0.4"/>
-<path d="M12 7.5L12 10.5C12 11.3284 11.3284 12 10.5 12L1.5 12C0.671573 12 -1.67346e-07 11.3284 -1.31134e-07 10.5L0 7.5L12 7.5Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_702_214">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/dock_bottom_8.svg πŸ”—

@@ -1,11 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_610_112)">
-<rect x="0.5" y="0.5" width="7" height="7" rx="0.5" stroke="white" stroke-opacity="0.4"/>
-<path d="M8 5L8 7C8 7.55228 7.55228 8 7 8L1 8C0.447715 8 -1.11564e-07 7.55228 -8.74228e-08 7L0 5L8 5Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_610_112">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/dock_modal_12.svg πŸ”—

@@ -1,11 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_702_213)">
-<rect x="2" y="3" width="8" height="6" rx="1" fill="white"/>
-<rect x="0.5" y="0.5" width="11" height="11" rx="1" stroke="white" stroke-opacity="0.4"/>
-</g>
-<defs>
-<clipPath id="clip0_702_213">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/dock_modal_8.svg πŸ”—

@@ -1,11 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_620_186)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M7 1H1V7H7V1ZM1 0C0.447715 0 0 0.447715 0 1V7C0 7.55228 0.447715 8 1 8H7C7.55228 8 8 7.55228 8 7V1C8 0.447715 7.55228 0 7 0H1Z" fill="white" fill-opacity="0.4"/>
-<rect x="2" y="2" width="4" height="4" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_620_186">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/dock_right_12.svg πŸ”—

@@ -1,11 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_702_215)">
-<rect x="0.5" y="0.5" width="11" height="11" rx="1" stroke="white" stroke-opacity="0.4"/>
-<path d="M7.5 0H10.5C11.3284 0 12 0.671573 12 1.5V10.5C12 11.3284 11.3284 12 10.5 12H7.5V0Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_702_215">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/dock_right_8.svg πŸ”—

@@ -1,11 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_610_114)">
-<rect x="0.5" y="0.5" width="7" height="7" rx="0.5" stroke="white" stroke-opacity="0.4"/>
-<path d="M5 0H7C7.55228 0 8 0.447715 8 1V7C8 7.55228 7.55228 8 7 8H5V0Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_610_114">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/download_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1353)">
-<path d="M2.87305 0.571824L2.87305 2.85948L1.57136 2.85858C1.40045 2.85858 1.24544 2.96038 1.17758 3.11771C1.11008 3.27486 1.14222 3.45755 1.25973 3.58203L3.68846 6.15541C3.85044 6.32685 4.14992 6.32685 4.3119 6.15541L6.74153 3.58185C6.85832 3.45862 6.89029 3.27647 6.82278 3.11753C6.75581 2.96038 6.60045 2.85858 6.44508 2.85858L5.15928 2.85858L5.15928 0.571824C5.15928 0.25591 4.90337 -1.70497e-08 4.58781 -3.08431e-08L3.44488 -8.08023e-08C3.11271 -0.000178679 2.87341 0.25591 2.87341 0.571824L2.87305 0.571824ZM7.42889 7.41246C7.42891 7.11244 7.17298 6.85707 6.85743 6.85707L1.14276 6.85707C0.826487 6.85707 0.571113 7.11244 0.571113 7.41246C0.571113 7.74463 0.826487 8 1.14258 8L6.85743 8C7.17298 8 7.42889 7.74463 7.42889 7.41246V7.41246Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1353">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/ellipsis_14.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="14" height="4" viewBox="0 0 14 4" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 2C3.125 2.62132 2.62132 3.125 2 3.125C1.37868 3.125 0.875 2.62132 0.875 2C0.875 1.37868 1.37868 0.875 2 0.875C2.62132 0.875 3.125 1.37868 3.125 2ZM8.125 2C8.125 2.62132 7.62132 3.125 7 3.125C6.37868 3.125 5.875 2.62132 5.875 2C5.875 1.37868 6.37868 0.875 7 0.875C7.62132 0.875 8.125 1.37868 8.125 2ZM12 3.125C12.6213 3.125 13.125 2.62132 13.125 2C13.125 1.37868 12.6213 0.875 12 0.875C11.3787 0.875 10.875 1.37868 10.875 2C10.875 2.62132 11.3787 3.125 12 3.125Z" fill="#ABB2BF"/>
-</svg>

assets/icons/enable_screen_sharing_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.53324 9.90014H7.18324L6.88324 9.00014H7.63305L6.10211 7.80014H1.78324V4.41577L0.583236 3.47452V8.10014C0.583217 8.59702 0.986361 9.00014 1.46636 9.00014H5.04949L4.74949 9.90014H3.43324C3.1848 9.90014 2.98324 10.1017 2.98324 10.3501C2.98324 10.5986 3.1848 10.8001 3.43324 10.8001H8.51637C8.7648 10.8001 8.96637 10.5986 8.96637 10.3501C8.96637 10.1017 8.79762 9.90014 8.53324 9.90014ZM11.8276 9.99577L10.5507 8.99489C11.0234 8.96789 11.3999 8.57939 11.3999 8.09996V2.09995C11.3999 1.60308 10.9968 1.19995 10.4999 1.19995H1.5168C1.28617 1.19995 1.07786 1.28939 0.918674 1.43208L0.727799 1.29595C0.645299 1.23145 0.547423 1.19995 0.450673 1.19995C0.316986 1.19995 0.184611 1.2592 0.0961106 1.37226C-0.057452 1.56801 -0.023327 1.85095 0.172236 2.00414L11.2724 10.7041C11.4693 10.8579 11.7519 10.8226 11.9041 10.6276C12.0581 10.4321 12.0224 10.149 11.8274 9.99521L11.8276 9.99577ZM10.1832 7.80014H9.00968L2.11905 2.40014H10.1816L10.1832 7.80014Z" fill="#93A1A1"/>
-</svg>

assets/icons/exit.svg πŸ”—

@@ -1,4 +1,8 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.3594 7.00127L9.86062 4.5025M12.3594 7.00127L9.86062 9.50002M12.3594 7.00127L5 7.00127" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H6" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<svg width="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/feedback_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9083 3.19699L7.99999 10.3949L0.0916311 3.1969C0.346537 2.49164 1.10447 1.98018 2 1.98018H14C14.8943 1.98018 15.653 2.49168 15.9083 3.19699ZM16 4.7153L12.1526 8.21715L16 11.688V4.7153ZM8.52024 11.5232L11.4199 8.88404L15.9081 12.933C15.6528 13.6378 14.8941 14.1501 14 14.1501H2C1.10461 14.1501 0.346779 13.6378 0.0917535 12.9331L4.58012 8.88404L7.47975 11.5232L7.99999 11.9967L8.52024 11.5232ZM3.84742 8.21715L0 4.71532V11.688L3.84742 8.21715Z" fill="white"/>
-</svg>

assets/icons/file_12.svg πŸ”—

@@ -1,4 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.25 0H1.5V12H10.5V5.25H5.25V0Z" fill="white"/>
-<path d="M6.75 0V3.75H10.5L6.75 0Z" fill="white"/>
-</svg>

assets/icons/file_16.svg πŸ”—

@@ -1,4 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.125 1H2.75V15H13.25V7.125H7.125V1Z" fill="white"/>
-<path d="M8.875 1V5.375H13.25L8.875 1Z" fill="white"/>
-</svg>

assets/icons/file_8.svg πŸ”—

@@ -1,4 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.5 0H1V8H7V3.5H3.5V0Z" fill="white"/>
-<path d="M4.5 0V2.5H7L4.5 0Z" fill="white"/>
-</svg>

assets/icons/filter_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.6748 1.40617C10.8058 1.24248 10.6892 1 10.4796 1H1.51991C1.31028 1 1.19374 1.24248 1.32469 1.40617L4.14578 4.93255C4.34144 5.1771 4.44803 5.48097 4.44803 5.79421C4.44803 6.4689 4.44803 9.33412 4.44803 10.5017C4.44803 10.7779 4.67189 11 4.94803 11H7.05148C7.32762 11 7.55148 10.7779 7.55148 10.5017C7.55148 9.33412 7.55148 6.4689 7.55148 5.79421C7.55148 5.48097 7.6581 5.1771 7.85376 4.93255L10.6748 1.40617Z" stroke="#787D87" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/filter_14.svg πŸ”—

@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="14px" viewBox="0 0 14 14" version="1.1">
-<g id="surface1">
-<path style="fill:none;stroke-width:1.25;stroke-linecap:round;stroke-linejoin:round;stroke:rgb(47.058824%,49.019608%,52.941176%);stroke-opacity:1;stroke-miterlimit:4;" d="M 10.674107 1.40625 C 10.804688 1.242188 10.690848 1.001116 10.479911 1.001116 L 1.520089 1.001116 C 1.309152 1.001116 1.195312 1.242188 1.325893 1.40625 L 4.145089 4.93192 C 4.342634 5.176339 4.446429 5.481027 4.446429 5.795759 C 4.446429 6.46875 4.446429 9.334821 4.446429 10.503348 C 4.446429 10.777902 4.670759 10.998884 4.948661 10.998884 L 7.051339 10.998884 C 7.329241 10.998884 7.550223 10.777902 7.550223 10.503348 C 7.550223 9.334821 7.550223 6.46875 7.550223 5.795759 C 7.550223 5.481027 7.657366 5.176339 7.854911 4.93192 Z M 10.674107 1.40625 " transform="matrix(1.166667,0,0,1.166667,0,0)"/>
-</g>
-</svg>

assets/icons/folder_tree_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1286)">
-<path d="M11.3333 1.33335H9L8.33333 0.666687H6.66667C6.29958 0.666687 6 0.96627 6 1.33335V4.66669C6 5.03377 6.29958 5.33335 6.66667 5.33335H11.3333C11.7004 5.33335 12 5.03377 12 4.66669V2.00002C12 1.63294 11.7 1.33335 11.3333 1.33335ZM11.3333 7.33335H9L8.33333 6.66669H6.66667C6.29958 6.66669 6 6.96627 6 7.33335V10.6667C6 11.0338 6.29958 11.3334 6.66667 11.3334H11.3333C11.7004 11.3334 12 11.0338 12 10.6667V8.00002C12 7.63335 11.7 7.33335 11.3333 7.33335ZM1.33333 1.00002C1.33333 0.815125 1.185 0.666687 1 0.666687H0.333333C0.148438 0.666687 0 0.81502 0 1.00002V9.33335C0 9.70044 0.299583 10 0.666667 10H5.33333V8.66669H1.33333V4.00002H5.33333V2.66669H1.33333V1.00002Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1286">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/folder_tree_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M14.2222 2.55555H11.5L10.7222 1.77777H8.77778C8.34951 1.77777 8 2.12728 8 2.55555V6.44444C8 6.8727 8.34951 7.22222 8.77778 7.22222H14.2222C14.6505 7.22222 15 6.8727 15 6.44444V3.33333C15 2.90506 14.65 2.55555 14.2222 2.55555ZM14.2222 9.55555H11.5L10.7222 8.77777H8.77778C8.34951 8.77777 8 9.12729 8 9.55555V13.4444C8 13.8727 8.34951 14.2222 8.77778 14.2222H14.2222C14.6505 14.2222 15 13.8727 15 13.4444V10.3333C15 9.90555 14.65 9.55555 14.2222 9.55555ZM2.55556 2.16666C2.55556 1.95095 2.3825 1.77777 2.16667 1.77777H1.38889C1.17318 1.77777 1 1.95083 1 2.16666V11.8889C1 12.3171 1.34951 12.6667 1.77778 12.6667H7.22222V11.1111H2.55556V5.66666H7.22222V4.1111H2.55556V2.16666Z" fill="white"/>
-</svg>

assets/icons/folder_tree_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1367)">
-<path d="M7.55556 0.888902H6L5.55556 0.444458H4.44444C4.19972 0.444458 4 0.64418 4 0.888902V3.11112C4 3.35585 4.19972 3.55557 4.44444 3.55557H7.55556C7.80028 3.55557 8 3.35585 8 3.11112V1.33335C8 1.08862 7.8 0.888902 7.55556 0.888902ZM7.55556 4.8889H6L5.55556 4.44446H4.44444C4.19972 4.44446 4 4.64418 4 4.8889V7.11112C4 7.35585 4.19972 7.55557 4.44444 7.55557H7.55556C7.80028 7.55557 8 7.35585 8 7.11112V5.33335C8 5.0889 7.8 4.8889 7.55556 4.8889ZM0.888889 0.66668C0.888889 0.543416 0.79 0.444458 0.666667 0.444458H0.222222C0.0989583 0.444458 0 0.543347 0 0.66668V6.22224C0 6.46696 0.199722 6.66668 0.444444 6.66668H3.55556V5.77779H0.888889V2.66668H3.55556V1.77779H0.888889V0.66668Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1367">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/git_diff_12.svg πŸ”—

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

assets/icons/git_diff_8.svg πŸ”—

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

assets/icons/html.svg πŸ”—

@@ -1,5 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.15735 3.17108L5.84271 10.8289" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-<path d="M4 5L2 7L4 9" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 9L12 7L10 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/kebab.svg πŸ”—

@@ -1,5 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<circle cx="7" cy="7" r="1" fill="black"/>
-<circle cx="11" cy="7" r="1" fill="black"/>
-<circle cx="3" cy="7" r="1" fill="black"/>
-</svg>

assets/icons/leave_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.585786 0.335786 0.25 0.75 0.25H7.25C7.66421 0.25 8 0.585786 8 1C8 1.41421 7.66421 1.75 7.25 1.75H1.5V10.25H7.25C7.66421 10.25 8 10.5858 8 11C8 11.4142 7.66421 11.75 7.25 11.75H0.75C0.335786 11.75 0 11.4142 0 11V1ZM8.78148 2.91435C9.10493 2.65559 9.57689 2.70803 9.83565 3.03148L11.8357 5.53148C12.0548 5.80539 12.0548 6.19461 11.8357 6.46852L9.83565 8.96852C9.57689 9.29197 9.10493 9.34441 8.78148 9.08565C8.45803 8.82689 8.40559 8.35493 8.66435 8.03148L9.68953 6.75H3.75C3.33579 6.75 3 6.41421 3 6C3 5.58579 3.33579 5.25 3.75 5.25H9.68953L8.66435 3.96852C8.40559 3.64507 8.45803 3.17311 8.78148 2.91435Z" fill="#ABB2BF"/>
-</svg>

assets/icons/lock.svg πŸ”—

@@ -1,6 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect x="3" y="5" width="8" height="7" rx="0.5" stroke="black" stroke-width="1.25"/>
-<path d="M4 4C4 2.89543 4.89543 2 6 2H8C9.10457 2 10 2.89543 10 4V5H4V4Z" stroke="black" stroke-opacity="0.75" stroke-width="1.25"/>
-<circle cx="7" cy="8" r="1" fill="black"/>
-<path d="M7 8V9.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-</svg>

assets/icons/lock_8.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3V2C2 0.89543 2.89543 0 4 0C5.10457 0 6 0.895431 6 2V3H7V8H1V3H2ZM3 2C3 1.44772 3.44772 1 4 1C4.55228 1 5 1.44772 5 2V3H3V2ZM3.5 6.5V4.5H4.5V6.5H3.5Z" fill="white"/>
-</svg>

assets/icons/magnifying_glass_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1288)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M7.40624 7.54167C6.77901 8.13568 5.93205 8.5 5 8.5C3.067 8.5 1.5 6.933 1.5 5C1.5 3.067 3.067 1.5 5 1.5C6.933 1.5 8.5 3.067 8.5 5C8.5 5.93205 8.13568 6.77901 7.54167 7.40624C7.51667 7.42558 7.49261 7.44673 7.46967 7.46967C7.44673 7.49261 7.42558 7.51667 7.40624 7.54167ZM7.96544 9.0261C7.13578 9.63821 6.11014 10 5 10C2.23858 10 0 7.76142 0 5C0 2.23858 2.23858 0 5 0C7.76142 0 10 2.23858 10 5C10 6.11014 9.63821 7.13578 9.0261 7.96544L11.5303 10.4697C11.8232 10.7626 11.8232 11.2374 11.5303 11.5303C11.2374 11.8232 10.7626 11.8232 10.4697 11.5303L7.96544 9.0261Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1288">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/magnifying_glass_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M14.7861 13.8789L11.1221 10.2148C11.915 9.24414 12.3525 8.02187 12.3525 6.6875C12.3525 3.5457 9.80602 1 6.66504 1C3.52406 1 1.00188 3.54652 1.00188 6.6875C1.00188 9.82848 3.54813 12.375 6.66477 12.375C7.99887 12.375 9.22278 11.9126 10.1921 11.1434L13.8562 14.8075C14.0093 14.9371 14.1788 15 14.3456 15C14.5124 15 14.6814 14.9357 14.8097 14.8075C15.0648 14.5513 15.0648 14.1357 14.7859 13.8786L14.7861 13.8789ZM2.31465 6.6875C2.31465 4.27523 4.27738 2.3125 6.68965 2.3125C9.10192 2.3125 11.0647 4.27523 11.0647 6.6875C11.0647 9.09977 9.10192 11.0625 6.68965 11.0625C4.27738 11.0625 2.31465 9.09922 2.31465 6.6875Z" fill="white"/>
-</svg>

assets/icons/magnifying_glass_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1369)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M5.16734 5.87445C4.62987 6.26778 3.96705 6.5 3.25 6.5C1.45507 6.5 0 5.04493 0 3.25C0 1.45507 1.45507 0 3.25 0C5.04493 0 6.5 1.45507 6.5 3.25C6.5 3.96705 6.26778 4.62987 5.87445 5.16734L7.85355 7.14645C8.04882 7.34171 8.04882 7.65829 7.85355 7.85355C7.65829 8.04882 7.34171 8.04882 7.14645 7.85355L5.16734 5.87445ZM5.5 3.25C5.5 4.49264 4.49264 5.5 3.25 5.5C2.00736 5.5 1 4.49264 1 3.25C1 2.00736 2.00736 1 3.25 1C4.49264 1 5.5 2.00736 5.5 3.25Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1369">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/match_case.svg πŸ”—

@@ -1,5 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.47087 3.20502H4.93146L7.12233 10.845H6.16733L5.60557 8.91252H2.78552L2.235 10.845H1.28L3.47087 3.20502ZM5.3921 8.06988L4.24611 4.02519H4.15622L3.01023 8.06988H5.3921Z" fill="black"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M3.35784 3.05502H5.04449L7.32139 10.995H6.05473L5.49297 9.06253H2.89876L2.34823 10.995H1.08094L3.35784 3.05502ZM4.20117 4.41683L3.20863 7.91989H5.1937L4.20117 4.41683Z" fill="black"/>

assets/icons/match_word.svg πŸ”—

@@ -1,5 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M4.74677 9.48683L4.07035 6.03229L3.38589 9.48683H2.17618L1.00285 4.00778H2.27563L2.81571 7.41751L3.48443 4.01749H4.65869L5.31824 7.41173L5.8574 4.00778H7.13018L5.95684 9.48683H4.74677Z" fill="black"/>

assets/icons/maximize.svg πŸ”—

@@ -1,4 +1,4 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2 8.5V12M2 12H5.5M2 12L6.01562 7.98437" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 5.5V2M12 2L8.5 2M12 2L8.01562 5.98437" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5 1.5H13.5M13.5 1.5V5.5M13.5 1.5C12.1332 2.86683 10.3668 4.63317 9 6" stroke="white" stroke-linecap="round"/>
+<path d="M1.5 9.5V13.5M1.5 13.5L6 9M1.5 13.5H5.5" stroke="white" stroke-linecap="round"/>
 </svg>

assets/icons/maximize_8.svg πŸ”—

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

assets/icons/microphone.svg πŸ”—

@@ -1,5 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.5 8.5C10.5 8.5 9.375 10 7 10C4.625 10 3.5 8.5 3.5 8.5" stroke="black" stroke-width="1.25"/>
-<rect x="5" y="2" width="4" height="5.40625" rx="2" fill="black" fill-opacity="0.25" stroke="black" stroke-width="1.25"/>
-<path d="M7 10V12M7 12H9M7 12H5" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-</svg>

assets/icons/minimize.svg πŸ”—

@@ -1,4 +1,4 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.01563 11.4844L6.01563 7.98438M6.01563 7.98438L2.51563 7.98437M6.01563 7.98438L2 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.01562 2.48438V5.98438M8.01562 5.98438H11.5156M8.01562 5.98438L12 2" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13 6L9 6M9 6L9 2M9 6C10.3668 4.63316 12.1332 2.86683 13.5 1.5" stroke="white" stroke-linecap="round"/>
+<path d="M6 13L6 9M6 9L1.5 13.5M6 9L2 9" stroke="white" stroke-linecap="round"/>
 </svg>

assets/icons/minimize_8.svg πŸ”—

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

assets/icons/plus.svg πŸ”—

@@ -1,3 +1,8 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7 3V11M11 7H3" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+<svg width="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/plus_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_519_280)">
-<path d="M6 2.5V9.5M2.5 6H9.5" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
-</g>
-<defs>
-<clipPath id="clip0_519_280">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/plus_16.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_702_228)">
-<path d="M8.00001 3.33331V12.6666M3.33334 7.99998H12.6667" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
-</g>
-<defs>
-<clipPath id="clip0_702_228">
-<rect width="16" height="16" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/plus_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_519_287)">
-<path d="M4 1V7M1 4H7" stroke="white" stroke-linecap="round"/>
-</g>
-<defs>
-<clipPath id="clip0_519_287">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

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

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,17 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,29 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,29 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,35 +0,0 @@
-<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 πŸ”—

@@ -1,29 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,21 +0,0 @@
-<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 πŸ”—

@@ -1,29 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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-left.svg πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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/chat-bubble.svg πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,4 +0,0 @@
-<svg width="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/dimensions.svg πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,13 +0,0 @@
-<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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,6 +0,0 @@
-<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 πŸ”—

@@ -1,6 +0,0 @@
-<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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,26 +0,0 @@
-<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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,14 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,14 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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/font-bold.svg πŸ”—

@@ -1,6 +0,0 @@
-<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 πŸ”—

@@ -1,6 +0,0 @@
-<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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,26 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,78 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,7 +0,0 @@
-<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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-  <path
-    fill-rule="evenodd"
-    clip-rule="evenodd"
@@ -1,8 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-  <path
-    fill-rule="evenodd"
-    clip-rule="evenodd"
@@ -1,8 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-  <path
-    fill-rule="evenodd"
-    clip-rule="evenodd"
@@ -1,8 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-  <path
-    fill-rule="evenodd"
-    clip-rule="evenodd"
@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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/magnifying-glass.svg πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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/maximize.svg πŸ”—

@@ -1,4 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.5 1.5H13.5M13.5 1.5V5.5M13.5 1.5C12.1332 2.86683 10.3668 4.63317 9 6" stroke="white" stroke-linecap="round"/>
-<path d="M1.5 9.5V13.5M1.5 13.5L6 9M1.5 13.5H5.5" stroke="white" stroke-linecap="round"/>
-</svg>

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

@@ -1,4 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13 6L9 6M9 6L9 2M9 6C10.3668 4.63316 12.1332 2.86683 13.5 1.5" stroke="white" stroke-linecap="round"/>
-<path d="M6 13L6 9M6 9L1.5 13.5M6 9L2 9" stroke="white" stroke-linecap="round"/>
-</svg>

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

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,10 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,43 +0,0 @@
-<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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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/radiobutton.svg πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,78 +0,0 @@
-<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 πŸ”—

@@ -1,78 +0,0 @@
-<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 πŸ”—

@@ -1,43 +0,0 @@
-<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 πŸ”—

@@ -1,78 +0,0 @@
-<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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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-quiet.svg πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,6 +0,0 @@
-<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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,9 +0,0 @@
-<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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,3 +0,0 @@
-<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 πŸ”—

@@ -1,3 +0,0 @@
-<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 πŸ”—

@@ -1,3 +0,0 @@
-<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 πŸ”—

@@ -1,3 +0,0 @@
-<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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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 πŸ”—

@@ -1,8 +0,0 @@
-<svg width="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/icons/robot_14.svg πŸ”—

@@ -1,4 +0,0 @@
-<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.5 4C2.5 2.89531 3.39688 2 4.5 2H9.5C10.6031 2 11.5 2.89531 11.5 4V8C11.5 9.10312 10.6031 10 9.5 10H4.5C3.39688 10 2.5 9.10312 2.5 8V4ZM5 4C4.44687 4 4 4.44687 4 5C4 5.55313 4.44687 6 5 6C5.55313 6 6 5.55313 6 5C6 4.44687 5.55313 4 5 4ZM9 6C9.55313 6 10 5.55313 10 5C10 4.44687 9.55313 4 9 4C8.44687 4 8 4.44687 8 5C8 5.55313 8.44687 6 9 6ZM5 8.5C5.275 8.5 5.5 8.275 5.5 8C5.5 7.725 5.275 7.5 5 7.5C4.725 7.5 4.5 7.725 4.5 8C4.5 8.275 4.725 8.5 5 8.5ZM7 7.5C6.725 7.5 6.5 7.725 6.5 8C6.5 8.275 6.725 8.5 7 8.5C7.275 8.5 7.5 8.275 7.5 8C7.5 7.725 7.275 7.5 7 7.5ZM9 8.5C9.275 8.5 9.5 8.275 9.5 8C9.5 7.725 9.275 7.5 9 7.5C8.725 7.5 8.5 7.725 8.5 8C8.5 8.275 8.725 8.5 9 8.5ZM0 14C0 12.3156 1.34312 11 3 11H11C12.6562 11 14 12.3156 14 14V15C14 15.5531 13.5531 16 13 16H11V14C11 13.4469 10.5531 13 10 13H4C3.44687 13 3 13.4469 3 14V16H1C0.447812 16 0 15.5531 0 15V14Z" fill="#808080"/>
-<path d="M7.5 2H6.5V0.5C6.5 0.22375 6.725 0 7 0C7.275 0 7.5 0.22375 7.5 0.5V2ZM1.5 4.5V7.5C1.5 7.775 1.27625 8 1 8C0.72375 8 0.5 7.775 0.5 7.5V4.5C0.5 4.225 0.72375 4 1 4C1.27625 4 1.5 4.225 1.5 4.5ZM5.5 16H4.5V14.5C4.5 14.225 4.725 14 5 14C5.275 14 5.5 14.225 5.5 14.5V16ZM7.5 16H6.5V14.5C6.5 14.225 6.725 14 7 14C7.275 14 7.5 14.225 7.5 14.5V16ZM9 14C9.275 14 9.5 14.225 9.5 14.5V16H8.5V14.5C8.5 14.225 8.725 14 9 14ZM13.5 7.5C13.5 7.775 13.275 8 13 8C12.725 8 12.5 7.775 12.5 7.5V4.5C12.5 4.225 12.725 4 13 4C13.275 4 13.5 4.225 13.5 4.5V7.5Z" fill="#808080"/>
-</svg>

assets/icons/screen.svg πŸ”—

@@ -1,4 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect x="2" y="2" width="10" height="7" rx="0.5" fill="black" fill-opacity="0.25" stroke="black" stroke-width="1.25"/>
-<path d="M7 9V12M7 12H9M7 12H5" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
-</svg>

assets/icons/select-all.svg πŸ”—

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.5 7V9.5M9.5 12V9.5M12 9.5H9.5M7 9.5H9.5M9.5 9.5L11.1667 7.83333M9.5 9.5L7.83333 11.1667M9.5 9.5L11.1667 11.1667M9.5 9.5L7.83333 7.83333" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>

assets/icons/speech_bubble_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.01077 0.000234794C2.69085 0.000234794 0.000639074 2.18612 0.000639074 4.88385C0.000639074 6.0491 0.501914 7.11387 1.33823 7.95254C1.04475 9.13517 0.0640321 10.1894 0.0522927 10.2011C-0.00053487 10.2539 -0.0153266 10.3356 0.0170743 10.4061C0.0464229 10.4763 0.111459 10.5185 0.187766 10.5185C1.74324 10.5185 2.89019 9.77286 3.4889 9.31197C4.25431 9.60052 5.10894 9.76722 6.01053 9.76722C9.33045 9.76722 12 7.58063 12 4.88361C12 2.18659 9.33045 0 6.01053 0L6.01077 0.000234794Z" fill="#FAFAFA"/>
-</svg>

assets/icons/split_12.svg πŸ”—

@@ -1,12 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1301)">
-<path d="M11 1H6V11H11V1Z" fill="white" fill-opacity="0.2"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M2.1818 1C1.51237 1 1.04544 1.48778 1.04544 2V10C1.04544 10.5122 1.51237 11 2.1818 11H9.81817C10.4876 11 10.9545 10.5122 10.9545 10V2C10.9545 1.48778 10.4876 1 9.81817 1H2.1818ZM0.0454407 2C0.0454407 0.855367 1.04377 0 2.1818 0H9.81817C10.9562 0 11.9545 0.855367 11.9545 2V10C11.9545 11.1446 10.9562 12 9.81817 12H2.1818C1.04377 12 0.0454407 11.1446 0.0454407 10V2Z" fill="white"/>
-<path d="M6 1H5V11H6V1Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1301">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/stop_sharing.svg πŸ”—

@@ -1,5 +1,5 @@
 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.70312 4L7.26046 2.97338C7.10239 2.60678 6.74141 2.36933 6.34219 2.36933H2.5C2.22386 2.36933 2 2.59319 2 2.86933V4.375V8" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>
+<path d="M7.70312 4L7.26046 2.97339C7.10239 2.60679 6.74141 2.36933 6.34219 2.36933H2.5C2.22386 2.36933 2 2.59319 2 2.86933V4.375V8" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>

assets/icons/success.svg πŸ”—

@@ -1,4 +0,0 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2 2.5C2 2.22386 2.22386 2 2.5 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H2.5C2.22386 12 2 11.7761 2 11.5V2.5Z" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linejoin="round"/>
-<path d="M4.60938 7.625L6.3125 8.89062L9.35938 4.64062" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/terminal_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1295)">
-<path d="M0 2.25C0 1.42148 0.67148 0.75 1.5 0.75H10.5C11.3273 0.75 12 1.42148 12 2.25V9.75C12 10.5773 11.3273 11.25 10.5 11.25H1.5C0.67148 11.25 0 10.5773 0 9.75V2.25ZM2.39766 3.55781C2.18789 3.7875 2.20336 4.14141 2.43281 4.35234L4.23047 6L2.43281 7.64766C2.20336 7.85859 2.18789 8.2125 2.39766 8.44219C2.60859 8.65078 2.9625 8.68594 3.19219 8.47734L5.44219 6.41484C5.55937 6.30703 5.625 6.15703 5.625 5.97891C5.625 5.84297 5.55937 5.69297 5.44219 5.58516L3.19219 3.52266C2.9625 3.31406 2.60859 3.32813 2.39766 3.55781ZM5.8125 7.875C5.50078 7.875 5.25 8.12578 5.25 8.4375C5.25 8.74922 5.50078 9 5.8125 9H9.1875C9.4992 9 9.75 8.74922 9.75 8.4375C9.75 8.12578 9.4992 7.875 9.1875 7.875H5.8125Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1295">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/terminal_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M1 3.625C1 2.65839 1.78339 1.875 2.75 1.875H13.25C14.2152 1.875 15 2.65839 15 3.625V12.375C15 13.3402 14.2152 14.125 13.25 14.125H2.75C1.78339 14.125 1 13.3402 1 12.375V3.625ZM3.79727 5.15078C3.55254 5.41875 3.57059 5.83165 3.83828 6.07773L5.93555 8L3.83828 9.92227C3.57059 10.1684 3.55254 10.5813 3.79727 10.8492C4.04335 11.0926 4.45625 11.1336 4.72422 10.8902L7.34922 8.48398C7.48593 8.3582 7.5625 8.1832 7.5625 7.97539C7.5625 7.8168 7.48593 7.6418 7.34922 7.51602L4.72422 5.10977C4.45625 4.8664 4.04335 4.88282 3.79727 5.15078ZM7.78125 10.1875C7.41758 10.1875 7.125 10.4801 7.125 10.8438C7.125 11.2074 7.41758 11.5 7.78125 11.5H11.7188C12.0824 11.5 12.375 11.2074 12.375 10.8438C12.375 10.4801 12.0824 10.1875 11.7188 10.1875H7.78125Z" fill="white"/>
-</svg>

assets/icons/terminal_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1381)">
-<path d="M0 1.5C0 0.947653 0.447653 0.5 1 0.5H7C7.55153 0.5 8 0.947653 8 1.5V6.5C8 7.05153 7.55153 7.5 7 7.5H1C0.447653 7.5 0 7.05153 0 6.5V1.5ZM1.59844 2.37187C1.45859 2.525 1.46891 2.76094 1.62187 2.90156L2.82031 4L1.62187 5.09844C1.46891 5.23906 1.45859 5.475 1.59844 5.62813C1.73906 5.76719 1.975 5.79063 2.12813 5.65156L3.62813 4.27656C3.70625 4.20469 3.75 4.10469 3.75 3.98594C3.75 3.89531 3.70625 3.79531 3.62813 3.72344L2.12813 2.34844C1.975 2.20937 1.73906 2.21875 1.59844 2.37187ZM3.875 5.25C3.66719 5.25 3.5 5.41719 3.5 5.625C3.5 5.83281 3.66719 6 3.875 6H6.125C6.3328 6 6.5 5.83281 6.5 5.625C6.5 5.41719 6.3328 5.25 6.125 5.25H3.875Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1381">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/triangle_exclamation_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1297)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M12 11.625H0V9.375L5.25 0.375H6.75L12 9.375V11.625ZM5.25 3.375H6.75V7.125H5.25V3.375ZM5.25 8.625H6.75V10.125H5.25V8.625Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1297">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/triangle_exclamation_16.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M15 14.5625H1V11.9375L7.125 1.4375H8.875L15 11.9375V14.5625ZM7.125 4.9375H8.875V9.3125H7.125V4.9375ZM7.125 11.0625H8.875V12.8125H7.125V11.0625Z" fill="white"/>
-</svg>

assets/icons/triangle_exclamation_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1371)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8 7.75H0V6.25L3.5 0.25H4.5L8 6.25V7.75ZM3.5 2.25H4.5V4.75H3.5V2.25ZM3.5 5.75H4.5V6.75H3.5V5.75Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1371">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/unlock_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1383)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M5.75 1C5.33579 1 5 1.33579 5 1.75V3H6.5V8H0.5V3H4V1.75C4 0.783502 4.7835 0 5.75 0C6.7165 0 7.5 0.783502 7.5 1.75V2H6.5V1.75C6.5 1.33579 6.16421 1 5.75 1ZM4.5 5H2.5V6H4.5V5Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1383">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/user_circle_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1299)">
-<path d="M6 0C2.68594 0 0 2.68594 0 6C0 9.31406 2.68594 12 6 12C9.31406 12 12 9.31406 12 6C12 2.68594 9.31406 0 6 0ZM6 3C6.93211 3 7.6875 3.75563 7.6875 4.6875C7.6875 5.61937 6.93281 6.375 6 6.375C5.06813 6.375 4.3125 5.61937 4.3125 4.6875C4.3125 3.75563 5.06719 3 6 3ZM6 10.5C4.75945 10.5 3.63516 9.99539 2.81953 9.1807C3.19922 8.20078 4.13672 7.5 5.25 7.5H6.75C7.86422 7.5 8.80172 8.20031 9.18047 9.1807C8.36484 9.99609 7.23984 10.5 6 10.5Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1299">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/user_circle_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1382)">
-<path d="M4 0C1.79063 0 0 1.79063 0 4C0 6.20937 1.79063 8 4 8C6.20937 8 8 6.20937 8 4C8 1.79063 6.20937 0 4 0ZM4 2C4.62141 2 5.125 2.50375 5.125 3.125C5.125 3.74625 4.62187 4.25 4 4.25C3.37875 4.25 2.875 3.74625 2.875 3.125C2.875 2.50375 3.37813 2 4 2ZM4 7C3.17297 7 2.42344 6.66359 1.87969 6.12047C2.13281 5.46719 2.75781 5 3.5 5H4.5C5.24281 5 5.86781 5.46687 6.12031 6.12047C5.57656 6.66406 4.82656 7 4 7Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1382">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/user_group_12.svg πŸ”—

@@ -1,3 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.2 6.00001C5.52563 6.00001 6.6 4.92545 6.6 3.60001C6.6 2.27457 5.52563 1.20001 4.2 1.20001C2.87438 1.20001 1.8 2.27457 1.8 3.60001C1.8 4.92545 2.87438 6.00001 4.2 6.00001ZM5.15063 6.90001H3.24938C1.45444 6.90001 0 8.35501 0 10.1494C0 10.5094 0.291 10.8 0.649875 10.8H7.74938C8.10938 10.8 8.4 10.5094 8.4 10.1494C8.4 8.35501 6.945 6.90001 5.15063 6.90001ZM8.98313 7.20001H7.59844C8.46 7.90689 9 8.96439 9 10.1494C9 10.3894 8.92875 10.6106 8.8125 10.8H11.4C11.7319 10.8 12 10.53 12 10.1831C12 8.54251 10.6575 7.20001 8.98313 7.20001ZM8.1 6.00001C9.26063 6.00001 10.2 5.06064 10.2 3.90001C10.2 2.73939 9.26063 1.80001 8.1 1.80001C7.62919 1.80001 7.19925 1.96042 6.849 2.22207C7.065 2.63682 7.2 3.10126 7.2 3.60001C7.2 4.26601 6.97631 4.87764 6.60769 5.37582C6.98813 5.76001 7.515 6.00001 8.1 6.00001Z" fill="white"/>
-</svg>

assets/icons/user_group_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1375)">
-<path d="M2.8 3.99999C3.68375 3.99999 4.4 3.28361 4.4 2.39999C4.4 1.51636 3.68375 0.799988 2.8 0.799988C1.91625 0.799988 1.2 1.51636 1.2 2.39999C1.2 3.28361 1.91625 3.99999 2.8 3.99999ZM3.43375 4.59999H2.16625C0.969625 4.59999 0 5.56999 0 6.76624C0 7.00624 0.194 7.19999 0.43325 7.19999H5.16625C5.40625 7.19999 5.6 7.00624 5.6 6.76624C5.6 5.56999 4.63 4.59999 3.43375 4.59999ZM5.98875 4.79999H5.06563C5.64 5.27124 6 5.97624 6 6.76624C6 6.92624 5.9525 7.07374 5.875 7.19999H7.6C7.82125 7.19999 8 7.01999 8 6.78874C8 5.69499 7.105 4.79999 5.98875 4.79999ZM5.4 3.99999C6.17375 3.99999 6.8 3.37374 6.8 2.59999C6.8 1.82624 6.17375 1.19999 5.4 1.19999C5.08613 1.19999 4.7995 1.30693 4.566 1.48136C4.71 1.75786 4.8 2.06749 4.8 2.39999C4.8 2.84399 4.65088 3.25174 4.40513 3.58386C4.65875 3.83999 5.01 3.99999 5.4 3.99999Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1375">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/user_plus_12.svg πŸ”—

@@ -1,5 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.75062 7.09998H3.24938C1.45519 7.09998 0 8.55498 0 10.3493C0 10.7093 0.291 11 0.649875 11H8.3505C8.70938 11 9 10.7093 9 10.3493C9 8.55498 7.545 7.09998 5.75062 7.09998Z" fill="white"/>
-<path d="M7 3.5C7 4.82544 5.82562 6 4.5 6C3.17438 6 2 4.82544 2 3.5C2 2.17456 3.17438 1 4.5 1C5.82562 1 7 2.17456 7 3.5Z" fill="white"/>
-<path d="M9.5 3.75V5.5M9.5 7.25V5.5M9.5 5.5H11.25M9.5 5.5H7.75" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>

assets/icons/user_plus_16.svg πŸ”—

@@ -1,5 +0,0 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.00906 8.99999H4.79094C2.69772 8.99999 1 11.1475 1 13.2409C1 13.6609 1.3395 14 1.75819 14H10.0422C10.4609 14 10.8 13.6609 10.8 13.2409C10.8 11.1475 9.1025 8.99999 7.00906 8.99999Z" fill="white"/>
-<path d="M9 5C9 6.54634 7.44657 7.99998 5.90001 7.99998C4.35344 7.99998 3 6.54634 3 5C3 3.45366 4.45344 2 6 2C7.54656 2 9 3.45366 9 5Z" fill="white"/>
-<path d="M13.025 6H14.475C14.7659 6 15 6.20906 15 6.5C15 6.79094 14.7659 7 14.475 7H13V8.49995C13 8.7898 12.7638 9.02495 12.475 9.02495C12.1863 9.02495 11.95 8.79089 11.95 8.49995V7H10.525C10.2352 7 10 6.78875 10 6.5C10 6.21125 10.2362 6 10.525 6H11.975V4.525C11.975 4.23516 12.2091 4 12.5 4C12.7909 4 13.025 4.23625 13.025 4.525V6Z" fill="white"/>
-</svg>

assets/icons/user_plus_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1373)">
-<path d="M2.8 3.99999C3.68375 3.99999 4.4 3.28361 4.4 2.39999C4.4 1.51636 3.68375 0.799988 2.8 0.799988C1.91625 0.799988 1.2 1.51636 1.2 2.39999C1.2 3.28361 1.91625 3.99999 2.8 3.99999ZM3.43375 4.59999H2.16625C0.970125 4.59999 0 5.56999 0 6.76624C0 7.00624 0.194 7.19999 0.43325 7.19999H5.167C5.40625 7.19999 5.6 7.00624 5.6 6.76624C5.6 5.56999 4.63 4.59999 3.43375 4.59999ZM7.7 3.29999H7.1V2.69999C7.1 2.53499 6.96625 2.39999 6.8 2.39999C6.63375 2.39999 6.5 2.53436 6.5 2.69999V3.29999H5.9C5.735 3.29999 5.6 3.43499 5.6 3.59999C5.6 3.76499 5.73438 3.89999 5.9 3.89999H6.5V4.49999C6.5 4.66624 6.635 4.79999 6.8 4.79999C6.965 4.79999 7.1 4.66561 7.1 4.49999V3.89999H7.7C7.86625 3.89999 8 3.76624 8 3.59999C8 3.43374 7.86625 3.29999 7.7 3.29999Z" fill="white"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1373">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/word_search_12.svg πŸ”—

@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12px" height="12px" viewBox="0 0 12 12" version="1.1">
-<g id="surface1">
-<path style=" stroke:none;fill-rule:evenodd;fill:rgb(47.058824%,49.019608%,52.941176%);fill-opacity:1;" d="M 4.070312 8.132812 L 3.488281 5.171875 L 2.902344 8.132812 L 1.867188 8.132812 L 0.859375 3.433594 L 1.949219 3.433594 L 2.414062 6.359375 L 2.988281 3.445312 L 3.992188 3.445312 L 4.558594 6.351562 L 5.019531 3.433594 L 6.113281 3.433594 L 5.105469 8.132812 Z M 4.070312 8.132812 "/>

assets/icons/x_mark_12.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1296)">
-<path d="M9.5 2.5L6 6M2.5 9.5L6 6M2.5 2.5L6 6M6 6L9.5 9.5" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1296">
-<rect width="12" height="12" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/x_mark_16.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_702_229)">
-<path d="M11.875 3.125L7.5 7.5M3.125 11.875L7.5 7.5M3.125 3.125L7.5 7.5M7.5 7.5L11.875 11.875" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
-</g>
-<defs>
-<clipPath id="clip0_702_229">
-<rect width="15" height="15" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/icons/x_mark_8.svg πŸ”—

@@ -1,10 +0,0 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0_430_1379)">
-<path d="M6.49999 1.49999L1.49997 6.5M1.49998 1.50002L6.49999 6.50004" stroke="white" stroke-linecap="round"/>
-</g>
-<defs>
-<clipPath id="clip0_430_1379">
-<rect width="8" height="8" fill="white"/>
-</clipPath>
-</defs>
-</svg>

assets/keymaps/default.json πŸ”—

@@ -30,6 +30,7 @@
       "cmd-s": "workspace::Save",
       "cmd-shift-s": "workspace::SaveAs",
       "cmd-=": "zed::IncreaseBufferFontSize",
+      "cmd-+": "zed::IncreaseBufferFontSize",
       "cmd--": "zed::DecreaseBufferFontSize",
       "cmd-0": "zed::ResetBufferFontSize",
       "cmd-,": "zed::OpenSettings",
@@ -231,7 +232,14 @@
     }
   },
   {
-    "context": "BufferSearchBar > Editor",
+    "context": "BufferSearchBar && in_replace",
+    "bindings": {
+      "enter": "search::ReplaceNext",
+      "cmd-enter": "search::ReplaceAll"
+    }
+  },
+  {
+    "context": "BufferSearchBar && !in_replace > Editor",
     "bindings": {
       "up": "search::PreviousHistoryQuery",
       "down": "search::NextHistoryQuery"
@@ -241,7 +249,11 @@
     "context": "ProjectSearchBar",
     "bindings": {
       "escape": "project_search::ToggleFocus",
-      "alt-tab": "search::CycleMode"
+      "alt-tab": "search::CycleMode",
+      "cmd-shift-h": "search::ToggleReplace",
+      "alt-cmd-g": "search::ActivateRegexMode",
+      "alt-cmd-s": "search::ActivateSemanticMode",
+      "alt-cmd-x": "search::ActivateTextMode"
     }
   },
   {
@@ -251,11 +263,22 @@
       "down": "search::NextHistoryQuery"
     }
   },
+  {
+    "context": "ProjectSearchBar && in_replace",
+    "bindings": {
+      "enter": "search::ReplaceNext",
+      "cmd-enter": "search::ReplaceAll"
+    }
+  },
   {
     "context": "ProjectSearchView",
     "bindings": {
       "escape": "project_search::ToggleFocus",
-      "alt-tab": "search::CycleMode"
+      "alt-tab": "search::CycleMode",
+      "cmd-shift-h": "search::ToggleReplace",
+      "alt-cmd-g": "search::ActivateRegexMode",
+      "alt-cmd-s": "search::ActivateSemanticMode",
+      "alt-cmd-x": "search::ActivateTextMode"
     }
   },
   {
@@ -264,11 +287,15 @@
       "cmd-f": "project_search::ToggleFocus",
       "cmd-g": "search::SelectNextMatch",
       "cmd-shift-g": "search::SelectPrevMatch",
+      "cmd-shift-h": "search::ToggleReplace",
       "alt-enter": "search::SelectAllMatches",
       "alt-cmd-c": "search::ToggleCaseSensitive",
       "alt-cmd-w": "search::ToggleWholeWord",
       "alt-tab": "search::CycleMode",
-      "alt-cmd-f": "project_search::ToggleFilters"
+      "alt-cmd-f": "project_search::ToggleFilters",
+      "alt-cmd-g": "search::ActivateRegexMode",
+      "alt-cmd-s": "search::ActivateSemanticMode",
+      "alt-cmd-x": "search::ActivateTextMode"
     }
   },
   // Bindings from VS Code
@@ -287,6 +314,7 @@
           "replace_newest": false
         }
       ],
+      "cmd-shift-l": "editor::SelectAllMatches",
       "ctrl-cmd-d": [
         "editor::SelectPrevious",
         {
@@ -447,7 +475,6 @@
     "bindings": {
       "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",
@@ -482,6 +509,22 @@
       "cmd-k cmd-down": [
         "workspace::ActivatePaneInDirection",
         "Down"
+      ],
+      "cmd-k shift-left": [
+        "workspace::SwapPaneInDirection",
+        "Left"
+      ],
+      "cmd-k shift-right": [
+        "workspace::SwapPaneInDirection",
+        "Right"
+      ],
+      "cmd-k shift-up": [
+        "workspace::SwapPaneInDirection",
+        "Up"
+      ],
+      "cmd-k shift-down": [
+        "workspace::SwapPaneInDirection",
+        "Down"
       ]
     }
   },
@@ -546,7 +589,7 @@
     }
   },
   {
-    "context": "ProjectSearchBar",
+    "context": "ProjectSearchBar && !in_replace",
     "bindings": {
       "cmd-enter": "project_search::SearchInNew"
     }
@@ -572,12 +615,26 @@
     }
   },
   {
-    "context": "CollabPanel",
+    "context": "CollabPanel && not_editing",
     "bindings": {
       "ctrl-backspace": "collab_panel::Remove",
       "space": "menu::Confirm"
     }
   },
+  {
+    "context": "(CollabPanel && editing) > Editor",
+    "bindings": {
+      "space": "collab_panel::InsertSpace"
+    }
+  },
+  {
+    "context": "(CollabPanel && not_editing) > Editor",
+    "bindings": {
+      "cmd-c": "collab_panel::StartLinkChannel",
+      "cmd-x": "collab_panel::StartMoveChannel",
+      "cmd-v": "collab_panel::MoveOrLinkToSelected"
+    }
+  },
   {
     "context": "ChannelModal",
     "bindings": {

assets/keymaps/sublime_text.json πŸ”—

@@ -17,6 +17,7 @@
       "ctrl-shift-down": "editor::AddSelectionBelow",
       "cmd-shift-space": "editor::SelectAll",
       "ctrl-shift-m": "editor::SelectLargerSyntaxNode",
+      "cmd-shift-l": "editor::SplitSelectionIntoLines",
       "cmd-shift-a": "editor::SelectLargerSyntaxNode",
       "shift-f12": "editor::FindAllReferences",
       "alt-cmd-down": "editor::GoToDefinition",

assets/keymaps/vim.json πŸ”—

@@ -18,6 +18,7 @@
           }
         }
       ],
+      ":": "command_palette::Toggle",
       "h": "vim::Left",
       "left": "vim::Left",
       "backspace": "vim::Backspace",
@@ -32,6 +33,8 @@
       "right": "vim::Right",
       "$": "vim::EndOfLine",
       "^": "vim::FirstNonWhitespace",
+      "_": "vim::StartOfLineDownward",
+      "g _": "vim::EndOfLineDownward",
       "shift-g": "vim::EndOfDocument",
       "w": "vim::NextWordStart",
       "{": "vim::StartOfParagraph",
@@ -92,6 +95,7 @@
         }
       ],
       "ctrl-o": "pane::GoBack",
+      "ctrl-i": "pane::GoForward",
       "ctrl-]": "editor::GoToDefinition",
       "escape": [
         "vim::SwitchMode",
@@ -123,8 +127,26 @@
       "g shift-t": "pane::ActivatePrevItem",
       "g d": "editor::GoToDefinition",
       "g shift-d": "editor::GoToTypeDefinition",
+      "g n": "vim::SelectNext",
+      "g shift-n": "vim::SelectPrevious",
+      "g >": [
+        "editor::SelectNext",
+        {
+          "replace_newest": true
+        }
+      ],
+      "g <": [
+        "editor::SelectPrevious",
+        {
+          "replace_newest": true
+        }
+      ],
+      "g a": "editor::SelectAllMatches",
+      "g s": "outline::Toggle",
+      "g shift-s": "project_symbols::Toggle",
       "g .": "editor::ToggleCodeActions", // zed specific
       "g shift-a": "editor::FindAllReferences", // zed specific
+      "g space": "editor::OpenExcerpts", // zed specific
       "g *": [
         "vim::MoveToNext",
         {
@@ -201,13 +223,13 @@
       "shift-z shift-q": [
         "pane::CloseActiveItem",
         {
-          "saveBehavior": "dontSave"
+          "saveIntent": "skip"
         }
       ],
       "shift-z shift-z": [
         "pane::CloseActiveItem",
         {
-          "saveBehavior": "promptOnConflict"
+          "saveIntent": "saveAll"
         }
       ],
       // Count support
@@ -296,6 +318,38 @@
         "workspace::ActivatePaneInDirection",
         "Down"
       ],
+      "ctrl-w shift-left": [
+        "workspace::SwapPaneInDirection",
+        "Left"
+      ],
+      "ctrl-w shift-right": [
+        "workspace::SwapPaneInDirection",
+        "Right"
+      ],
+      "ctrl-w shift-up": [
+        "workspace::SwapPaneInDirection",
+        "Up"
+      ],
+      "ctrl-w shift-down": [
+        "workspace::SwapPaneInDirection",
+        "Down"
+      ],
+      "ctrl-w shift-h": [
+        "workspace::SwapPaneInDirection",
+        "Left"
+      ],
+      "ctrl-w shift-l": [
+        "workspace::SwapPaneInDirection",
+        "Right"
+      ],
+      "ctrl-w shift-k": [
+        "workspace::SwapPaneInDirection",
+        "Up"
+      ],
+      "ctrl-w shift-j": [
+        "workspace::SwapPaneInDirection",
+        "Down"
+      ],
       "ctrl-w g t": "pane::ActivateNextItem",
       "ctrl-w ctrl-g t": "pane::ActivateNextItem",
       "ctrl-w g shift-t": "pane::ActivatePrevItem",
@@ -314,7 +368,17 @@
       "ctrl-w c": "pane::CloseAllItems",
       "ctrl-w ctrl-c": "pane::CloseAllItems",
       "ctrl-w q": "pane::CloseAllItems",
-      "ctrl-w ctrl-q": "pane::CloseAllItems"
+      "ctrl-w ctrl-q": "pane::CloseAllItems",
+      "ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
+      "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
+      "ctrl-w n": [
+        "workspace::NewFileInDirection",
+        "Up"
+      ],
+      "ctrl-w ctrl-n": [
+        "workspace::NewFileInDirection",
+        "Up"
+      ]
     }
   },
   {
@@ -326,7 +390,7 @@
     }
   },
   {
-    "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
+    "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
     "bindings": {
       ".": "vim::Repeat",
       "c": [
@@ -353,6 +417,8 @@
       "o": "vim::InsertLineBelow",
       "shift-o": "vim::InsertLineAbove",
       "~": "vim::ChangeCase",
+      "ctrl-a": "vim::Increment",
+      "ctrl-x": "vim::Decrement",
       "p": "vim::Paste",
       "shift-p": [
         "vim::Paste",
@@ -389,7 +455,7 @@
     }
   },
   {
-    "context": "Editor && vim_operator == n",
+    "context": "Editor && VimCount",
     "bindings": {
       "0": [
         "vim::Number",
@@ -448,7 +514,10 @@
       "shift-o": "vim::OtherEnd",
       "d": "vim::VisualDelete",
       "x": "vim::VisualDelete",
+      "shift-d": "vim::VisualDelete",
+      "shift-x": "vim::VisualDelete",
       "y": "vim::VisualYank",
+      "shift-y": "vim::VisualYank",
       "p": "vim::Paste",
       "shift-p": [
         "vim::Paste",
@@ -461,6 +530,20 @@
       "shift-r": "vim::SubstituteLine",
       "c": "vim::Substitute",
       "~": "vim::ChangeCase",
+      "ctrl-a": "vim::Increment",
+      "ctrl-x": "vim::Decrement",
+      "g ctrl-a": [
+        "vim::Increment",
+        {
+          "step": true
+        }
+      ],
+      "g ctrl-x": [
+        "vim::Decrement",
+        {
+          "step": true
+        }
+      ],
       "shift-i": "vim::InsertBefore",
       "shift-a": "vim::InsertAfter",
       "shift-j": "vim::JoinLines",
@@ -497,15 +580,20 @@
             "around": true
           }
         }
-      ],
+      ]
     }
   },
   {
-    "context": "Editor && vim_mode == insert && !menu",
+    "context": "Editor && vim_mode == insert",
     "bindings": {
       "escape": "vim::NormalBefore",
       "ctrl-c": "vim::NormalBefore",
-      "ctrl-[": "vim::NormalBefore"
+      "ctrl-[": "vim::NormalBefore",
+      "ctrl-x ctrl-o": "editor::ShowCompletions",
+      "ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific
+      "ctrl-x ctrl-c": "copilot::Suggest", // zed specific
+      "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
+      "ctrl-x ctrl-z": "editor::Cancel"
     }
   },
   {
@@ -524,7 +612,7 @@
     }
   },
   {
-    "context": "BufferSearchBar > VimEnabled",
+    "context": "BufferSearchBar && !in_replace > VimEnabled",
     "bindings": {
       "enter": "vim::SearchSubmit",
       "escape": "buffer_search::Dismiss"

assets/settings/default.json πŸ”—

@@ -131,6 +131,14 @@
     // Default width of the channels panel.
     "default_width": 240
   },
+  "chat_panel": {
+    // Whether to show the collaboration panel button in the status bar.
+    "button": true,
+    // Where to dock channels panel. Can be 'left' or 'right'.
+    "dock": "right",
+    // Default width of the channels panel.
+    "default_width": 240
+  },
   "assistant": {
     // Whether to show the assistant panel button in the status bar.
     "button": true,
@@ -219,6 +227,11 @@
   },
   // Automatically update Zed
   "auto_update": true,
+  // Diagnostics configuration.
+  "diagnostics": {
+    // Whether to show warnings or not by default.
+    "include_warnings": true
+  },
   // Git gutter behavior configuration.
   "git": {
     // Control whether the git gutter is shown. May take 2 values:
@@ -348,7 +361,7 @@
           ".venv",
           "venv"
         ],
-        // Can also be 'csh' and 'fish'
+        // Can also be 'csh', 'fish', and `nushell`
         "activate_script": "default"
       }
     }
@@ -362,8 +375,28 @@
   },
   // Difference settings for semantic_index
   "semantic_index": {
-    "enabled": false,
-    "reindexing_delay_seconds": 600
+    "enabled": true
+  },
+  // Settings specific to our elixir integration
+  "elixir": {
+    // Change the LSP zed uses for elixir.
+    // Note that changing this setting requires a restart of Zed
+    // to take effect.
+    //
+    // May take 3 values:
+    //  1. Use the standard ElixirLS, this is the default
+    //         "lsp": "elixir_ls"
+    //  2. Use the experimental NextLs
+    //         "lsp": "next_ls",
+    //  3. Use a language server installed locally on your machine:
+    //         "lsp": {
+    //           "local": {
+    //             "path": "~/next-ls/bin/start",
+    //             "arguments": ["--stdio"]
+    //            }
+    //          },
+    //
+    "lsp": "elixir_ls"
   },
   // Different settings for specific languages.
   "languages": {

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

@@ -16,8 +16,8 @@ use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
 actions!(lsp_status, [ShowErrorMessage]);
 
-const DOWNLOAD_ICON: &str = "icons/download_12.svg";
-const WARNING_ICON: &str = "icons/triangle_exclamation_12.svg";
+const DOWNLOAD_ICON: &str = "icons/download.svg";
+const WARNING_ICON: &str = "icons/warning.svg";
 
 pub enum Event {
     ShowError { lsp_name: Arc<str>, error: String },

crates/ai/Cargo.toml πŸ”—

@@ -9,36 +9,26 @@ path = "src/ai.rs"
 doctest = false
 
 [dependencies]
-collections = { path = "../collections"}
-editor = { path = "../editor" }
-fs = { path = "../fs" }
 gpui = { path = "../gpui" }
-language = { path = "../language" }
-menu = { path = "../menu" }
-search = { path = "../search" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
 util = { path = "../util" }
-workspace = { path = "../workspace" }
-
+async-trait.workspace = true
 anyhow.workspace = true
-chrono = { version = "0.4", features = ["serde"] }
 futures.workspace = true
-indoc.workspace = true
-isahc.workspace = true
+lazy_static.workspace = true
 ordered-float.workspace = true
+parking_lot.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"
+postage.workspace = true
+rand.workspace = true
+log.workspace = true
+parse_duration = "2.1.1"
+tiktoken-rs = "0.5.0"
+matrixmultiply = "0.3.7"
+rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
+bincode = "1.3.3"
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
-
-ctor.workspace = true
-env_logger.workspace = true
-log.workspace = true
-rand.workspace = true
+gpui = { path = "../gpui", features = ["test-support"] }

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

@@ -1,292 +1,2 @@
-pub mod assistant;
-mod assistant_settings;
-mod streaming_diff;
-
-use anyhow::{anyhow, Result};
-pub use assistant::AssistantPanel;
-use assistant_settings::OpenAIModel;
-use chrono::{DateTime, Local};
-use collections::HashMap;
-use fs::Fs;
-use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
-use gpui::{executor::Background, AppContext};
-use isahc::{http::StatusCode, Request, RequestExt};
-use regex::Regex;
-use serde::{Deserialize, Serialize};
-use std::{
-    cmp::Reverse,
-    ffi::OsStr,
-    fmt::{self, Display},
-    io,
-    path::PathBuf,
-    sync::Arc,
-};
-use util::paths::CONVERSATIONS_DIR;
-
-const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
-
-// Data types for chat completion requests
-#[derive(Debug, Serialize)]
-pub 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: OpenAIModel,
-}
-
-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?;
-            if path.extension() != Some(OsStr::new("json")) {
-                continue;
-            }
-
-            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,
-    content: String,
-}
-
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
-pub struct ResponseMessage {
-    role: Option<Role>,
-    content: Option<String>,
-}
-
-#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
-#[serde(rename_all = "lowercase")]
-enum Role {
-    User,
-    Assistant,
-    System,
-}
-
-impl Role {
-    pub fn cycle(&mut self) {
-        *self = match self {
-            Role::User => Role::Assistant,
-            Role::Assistant => Role::System,
-            Role::System => Role::User,
-        }
-    }
-}
-
-impl Display for Role {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Role::User => write!(f, "User"),
-            Role::Assistant => write!(f, "Assistant"),
-            Role::System => write!(f, "System"),
-        }
-    }
-}
-
-#[derive(Deserialize, Debug)]
-pub struct OpenAIResponseStreamEvent {
-    pub id: Option<String>,
-    pub object: String,
-    pub created: u32,
-    pub model: String,
-    pub choices: Vec<ChatChoiceDelta>,
-    pub usage: Option<Usage>,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct Usage {
-    pub prompt_tokens: u32,
-    pub completion_tokens: u32,
-    pub total_tokens: u32,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct ChatChoiceDelta {
-    pub index: u32,
-    pub delta: ResponseMessage,
-    pub finish_reason: Option<String>,
-}
-
-#[derive(Deserialize, Debug)]
-struct OpenAIUsage {
-    prompt_tokens: u64,
-    completion_tokens: u64,
-    total_tokens: u64,
-}
-
-#[derive(Deserialize, Debug)]
-struct OpenAIChoice {
-    text: String,
-    index: u32,
-    logprobs: Option<serde_json::Value>,
-    finish_reason: Option<String>,
-}
-
-pub fn init(cx: &mut AppContext) {
-    assistant::init(cx);
-}
-
-pub async fn stream_completion(
-    api_key: String,
-    executor: Arc<Background>,
-    mut request: OpenAIRequest,
-) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
-    request.stream = true;
-
-    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
-
-    let json_data = serde_json::to_string(&request)?;
-    let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
-        .header("Content-Type", "application/json")
-        .header("Authorization", format!("Bearer {}", api_key))
-        .body(json_data)?
-        .send_async()
-        .await?;
-
-    let status = response.status();
-    if status == StatusCode::OK {
-        executor
-            .spawn(async move {
-                let mut lines = BufReader::new(response.body_mut()).lines();
-
-                fn parse_line(
-                    line: Result<String, io::Error>,
-                ) -> Result<Option<OpenAIResponseStreamEvent>> {
-                    if let Some(data) = line?.strip_prefix("data: ") {
-                        let event = serde_json::from_str(&data)?;
-                        Ok(Some(event))
-                    } else {
-                        Ok(None)
-                    }
-                }
-
-                while let Some(line) = lines.next().await {
-                    if let Some(event) = parse_line(line).transpose() {
-                        let done = event.as_ref().map_or(false, |event| {
-                            event
-                                .choices
-                                .last()
-                                .map_or(false, |choice| choice.finish_reason.is_some())
-                        });
-                        if tx.unbounded_send(event).is_err() {
-                            break;
-                        }
-
-                        if done {
-                            break;
-                        }
-                    }
-                }
-
-                anyhow::Ok(())
-            })
-            .detach();
-
-        Ok(rx)
-    } else {
-        let mut body = String::new();
-        response.body_mut().read_to_string(&mut body).await?;
-
-        #[derive(Deserialize)]
-        struct OpenAIResponse {
-            error: OpenAIError,
-        }
-
-        #[derive(Deserialize)]
-        struct OpenAIError {
-            message: String,
-        }
-
-        match serde_json::from_str::<OpenAIResponse>(&body) {
-            Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
-                "Failed to connect to OpenAI API: {}",
-                response.error.message,
-            )),
-
-            _ => Err(anyhow!(
-                "Failed to connect to OpenAI API: {} {}",
-                response.status(),
-                body,
-            )),
-        }
-    }
-}
-
-#[cfg(test)]
-#[ctor::ctor]
-fn init_logger() {
-    if std::env::var("RUST_LOG").is_ok() {
-        env_logger::init();
-    }
-}
+pub mod completion;
+pub mod embedding;

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

@@ -0,0 +1,212 @@
+use anyhow::{anyhow, Result};
+use futures::{
+    future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt,
+    Stream, StreamExt,
+};
+use gpui::executor::Background;
+use isahc::{http::StatusCode, Request, RequestExt};
+use serde::{Deserialize, Serialize};
+use std::{
+    fmt::{self, Display},
+    io,
+    sync::Arc,
+};
+
+pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
+
+#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[serde(rename_all = "lowercase")]
+pub enum Role {
+    User,
+    Assistant,
+    System,
+}
+
+impl Role {
+    pub fn cycle(&mut self) {
+        *self = match self {
+            Role::User => Role::Assistant,
+            Role::Assistant => Role::System,
+            Role::System => Role::User,
+        }
+    }
+}
+
+impl Display for Role {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Role::User => write!(f, "User"),
+            Role::Assistant => write!(f, "Assistant"),
+            Role::System => write!(f, "System"),
+        }
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+pub struct RequestMessage {
+    pub role: Role,
+    pub content: String,
+}
+
+#[derive(Debug, Default, Serialize)]
+pub struct OpenAIRequest {
+    pub model: String,
+    pub messages: Vec<RequestMessage>,
+    pub stream: bool,
+}
+
+#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
+pub struct ResponseMessage {
+    pub role: Option<Role>,
+    pub content: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OpenAIUsage {
+    pub prompt_tokens: u32,
+    pub completion_tokens: u32,
+    pub total_tokens: u32,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct ChatChoiceDelta {
+    pub index: u32,
+    pub delta: ResponseMessage,
+    pub finish_reason: Option<String>,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct OpenAIResponseStreamEvent {
+    pub id: Option<String>,
+    pub object: String,
+    pub created: u32,
+    pub model: String,
+    pub choices: Vec<ChatChoiceDelta>,
+    pub usage: Option<OpenAIUsage>,
+}
+
+pub async fn stream_completion(
+    api_key: String,
+    executor: Arc<Background>,
+    mut request: OpenAIRequest,
+) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
+    request.stream = true;
+
+    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
+
+    let json_data = serde_json::to_string(&request)?;
+    let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
+        .header("Content-Type", "application/json")
+        .header("Authorization", format!("Bearer {}", api_key))
+        .body(json_data)?
+        .send_async()
+        .await?;
+
+    let status = response.status();
+    if status == StatusCode::OK {
+        executor
+            .spawn(async move {
+                let mut lines = BufReader::new(response.body_mut()).lines();
+
+                fn parse_line(
+                    line: Result<String, io::Error>,
+                ) -> Result<Option<OpenAIResponseStreamEvent>> {
+                    if let Some(data) = line?.strip_prefix("data: ") {
+                        let event = serde_json::from_str(&data)?;
+                        Ok(Some(event))
+                    } else {
+                        Ok(None)
+                    }
+                }
+
+                while let Some(line) = lines.next().await {
+                    if let Some(event) = parse_line(line).transpose() {
+                        let done = event.as_ref().map_or(false, |event| {
+                            event
+                                .choices
+                                .last()
+                                .map_or(false, |choice| choice.finish_reason.is_some())
+                        });
+                        if tx.unbounded_send(event).is_err() {
+                            break;
+                        }
+
+                        if done {
+                            break;
+                        }
+                    }
+                }
+
+                anyhow::Ok(())
+            })
+            .detach();
+
+        Ok(rx)
+    } else {
+        let mut body = String::new();
+        response.body_mut().read_to_string(&mut body).await?;
+
+        #[derive(Deserialize)]
+        struct OpenAIResponse {
+            error: OpenAIError,
+        }
+
+        #[derive(Deserialize)]
+        struct OpenAIError {
+            message: String,
+        }
+
+        match serde_json::from_str::<OpenAIResponse>(&body) {
+            Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
+                "Failed to connect to OpenAI API: {}",
+                response.error.message,
+            )),
+
+            _ => Err(anyhow!(
+                "Failed to connect to OpenAI API: {} {}",
+                response.status(),
+                body,
+            )),
+        }
+    }
+}
+
+pub trait CompletionProvider {
+    fn complete(
+        &self,
+        prompt: OpenAIRequest,
+    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
+}
+
+pub struct OpenAICompletionProvider {
+    api_key: String,
+    executor: Arc<Background>,
+}
+
+impl OpenAICompletionProvider {
+    pub fn new(api_key: String, executor: Arc<Background>) -> Self {
+        Self { api_key, executor }
+    }
+}
+
+impl CompletionProvider for OpenAICompletionProvider {
+    fn complete(
+        &self,
+        prompt: OpenAIRequest,
+    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
+        let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt);
+        async move {
+            let response = request.await?;
+            let stream = response
+                .filter_map(|response| async move {
+                    match response {
+                        Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
+                        Err(error) => Some(Err(error)),
+                    }
+                })
+                .boxed();
+            Ok(stream)
+        }
+        .boxed()
+    }
+}

crates/semantic_index/src/embedding.rs β†’ crates/ai/src/embedding.rs πŸ”—

@@ -7,6 +7,7 @@ use isahc::http::StatusCode;
 use isahc::prelude::Configurable;
 use isahc::{AsyncBody, Response};
 use lazy_static::lazy_static;
+use ordered_float::OrderedFloat;
 use parking_lot::Mutex;
 use parse_duration::parse;
 use postage::watch;
@@ -26,8 +27,30 @@ lazy_static! {
 }
 
 #[derive(Debug, PartialEq, Clone)]
-pub struct Embedding(Vec<f32>);
+pub struct Embedding(pub Vec<f32>);
 
+// This is needed for semantic index functionality
+// Unfortunately it has to live wherever the "Embedding" struct is created.
+// Keeping this in here though, introduces a 'rusqlite' dependency into AI
+// which is less than ideal
+impl FromSql for Embedding {
+    fn column_result(value: ValueRef) -> FromSqlResult<Self> {
+        let bytes = value.as_blob()?;
+        let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
+        if embedding.is_err() {
+            return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
+        }
+        Ok(Embedding(embedding.unwrap()))
+    }
+}
+
+impl ToSql for Embedding {
+    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
+        let bytes = bincode::serialize(&self.0)
+            .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
+        Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
+    }
+}
 impl From<Vec<f32>> for Embedding {
     fn from(value: Vec<f32>) -> Self {
         Embedding(value)
@@ -35,7 +58,7 @@ impl From<Vec<f32>> for Embedding {
 }
 
 impl Embedding {
-    pub fn similarity(&self, other: &Self) -> f32 {
+    pub fn similarity(&self, other: &Self) -> OrderedFloat<f32> {
         let len = self.0.len();
         assert_eq!(len, other.0.len());
 
@@ -58,28 +81,28 @@ impl Embedding {
                 1,
             );
         }
-        result
-    }
-}
-
-impl FromSql for Embedding {
-    fn column_result(value: ValueRef) -> FromSqlResult<Self> {
-        let bytes = value.as_blob()?;
-        let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
-        if embedding.is_err() {
-            return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
-        }
-        Ok(Embedding(embedding.unwrap()))
+        OrderedFloat(result)
     }
 }
 
-impl ToSql for Embedding {
-    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
-        let bytes = bincode::serialize(&self.0)
-            .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
-        Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
-    }
-}
+// impl FromSql for Embedding {
+//     fn column_result(value: ValueRef) -> FromSqlResult<Self> {
+//         let bytes = value.as_blob()?;
+//         let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
+//         if embedding.is_err() {
+//             return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
+//         }
+//         Ok(Embedding(embedding.unwrap()))
+//     }
+// }
+
+// impl ToSql for Embedding {
+//     fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
+//         let bytes = bincode::serialize(&self.0)
+//             .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
+//         Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
+//     }
+// }
 
 #[derive(Clone)]
 pub struct OpenAIEmbeddings {
@@ -116,6 +139,7 @@ struct OpenAIEmbeddingUsage {
 
 #[async_trait]
 pub trait EmbeddingProvider: Sync + Send {
+    fn is_authenticated(&self) -> bool;
     async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
     fn max_tokens_per_batch(&self) -> usize;
     fn truncate(&self, span: &str) -> (String, usize);
@@ -126,6 +150,9 @@ pub struct DummyEmbeddings {}
 
 #[async_trait]
 impl EmbeddingProvider for DummyEmbeddings {
+    fn is_authenticated(&self) -> bool {
+        true
+    }
     fn rate_limit_expiration(&self) -> Option<Instant> {
         None
     }
@@ -228,6 +255,9 @@ impl OpenAIEmbeddings {
 
 #[async_trait]
 impl EmbeddingProvider for OpenAIEmbeddings {
+    fn is_authenticated(&self) -> bool {
+        OPENAI_API_KEY.as_ref().is_some()
+    }
     fn max_tokens_per_batch(&self) -> usize {
         50000
     }
@@ -379,13 +409,13 @@ mod tests {
             );
         }
 
-        fn round_to_decimals(n: f32, decimal_places: i32) -> f32 {
+        fn round_to_decimals(n: OrderedFloat<f32>, decimal_places: i32) -> f32 {
             let factor = (10.0 as f32).powi(decimal_places);
             (n * factor).round() / factor
         }
 
-        fn reference_dot(a: &[f32], b: &[f32]) -> f32 {
-            a.iter().zip(b.iter()).map(|(a, b)| a * b).sum()
+        fn reference_dot(a: &[f32], b: &[f32]) -> OrderedFloat<f32> {
+            OrderedFloat(a.iter().zip(b.iter()).map(|(a, b)| a * b).sum())
         }
     }
 }

crates/assistant/Cargo.toml πŸ”—

@@ -0,0 +1,48 @@
+[package]
+name = "assistant"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/assistant.rs"
+doctest = false
+
+[dependencies]
+ai = { path = "../ai" }
+client = { path = "../client" }
+collections = { path = "../collections"}
+editor = { path = "../editor" }
+fs = { path = "../fs" }
+gpui = { path = "../gpui" }
+language = { path = "../language" }
+menu = { path = "../menu" }
+search = { path = "../search" }
+settings = { path = "../settings" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+workspace = { path = "../workspace" }
+uuid.workspace = true
+
+anyhow.workspace = true
+chrono = { version = "0.4", features = ["serde"] }
+futures.workspace = true
+indoc.workspace = true
+isahc.workspace = true
+ordered-float.workspace = true
+parking_lot.workspace = true
+regex.workspace = true
+schemars.workspace = true
+serde.workspace = true
+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"] }
+
+ctor.workspace = true
+env_logger.workspace = true
+log.workspace = true
+rand.workspace = true

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

@@ -0,0 +1,113 @@
+pub mod assistant_panel;
+mod assistant_settings;
+mod codegen;
+mod prompts;
+mod streaming_diff;
+
+use ai::completion::Role;
+use anyhow::Result;
+pub use assistant_panel::AssistantPanel;
+use assistant_settings::OpenAIModel;
+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::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
+use util::paths::CONVERSATIONS_DIR;
+
+#[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 {
+    id: Option<String>,
+    zed: String,
+    version: String,
+    text: String,
+    messages: Vec<SavedMessage>,
+    message_metadata: HashMap<MessageId, MessageMetadata>,
+    summary: String,
+    model: OpenAIModel,
+}
+
+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?;
+            if path.extension() != Some(OsStr::new("json")) {
+                continue;
+            }
+
+            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)
+    }
+}
+
+pub fn init(cx: &mut AppContext) {
+    assistant_panel::init(cx);
+}
+
+#[cfg(test)]
+#[ctor::ctor]
+fn init_logger() {
+    if std::env::var("RUST_LOG").is_ok() {
+        env_logger::init();
+    }
+}

crates/ai/src/assistant.rs β†’ crates/assistant/src/assistant_panel.rs πŸ”—

@@ -1,22 +1,26 @@
 use crate::{
     assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
-    stream_completion,
-    streaming_diff::{Hunk, StreamingDiff},
-    MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage, Role,
-    SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL,
+    codegen::{self, Codegen, CodegenKind},
+    prompts::generate_content_prompt,
+    MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
+    SavedMessage,
+};
+use ai::completion::{
+    stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
 };
 use anyhow::{anyhow, Result};
 use chrono::{DateTime, Local};
+use client::{telemetry::AssistantKind, ClickhouseEvent, TelemetrySettings};
 use collections::{hash_map, HashMap, HashSet, VecDeque};
 use editor::{
     display_map::{
         BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
     },
     scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
-    Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
+    Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset,
 };
 use fs::Fs;
-use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
+use futures::StreamExt;
 use gpui::{
     actions,
     elements::{
@@ -30,17 +34,14 @@ use gpui::{
     ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
     WindowContext,
 };
-use language::{
-    language_settings::SoftWrap, Buffer, LanguageRegistry, Point, Rope, ToOffset as _,
-    TransactionId,
-};
+use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
 use search::BufferSearchBar;
 use settings::SettingsStore;
 use std::{
     cell::{Cell, RefCell},
     cmp, env,
     fmt::Write,
-    future, iter,
+    iter,
     ops::Range,
     path::{Path, PathBuf},
     rc::Rc,
@@ -52,6 +53,7 @@ use theme::{
     AssistantStyle,
 };
 use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
+use uuid::Uuid;
 use workspace::{
     dock::{DockPosition, Panel},
     searchable::Direction,
@@ -266,23 +268,45 @@ impl AssistantPanel {
     }
 
     fn new_inline_assist(&mut self, editor: &ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+        let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
+            api_key
+        } else {
+            return;
+        };
+
+        let selection = editor.read(cx).selections.newest_anchor().clone();
+        if selection.start.excerpt_id() != selection.end.excerpt_id() {
+            return;
+        }
+
         let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
         let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
-        let selection = editor.read(cx).selections.newest_anchor().clone();
-        let range = selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot);
-        let assist_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
-            InlineAssistKind::Generate
+        let provider = Arc::new(OpenAICompletionProvider::new(
+            api_key,
+            cx.background().clone(),
+        ));
+        let codegen_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
+            CodegenKind::Generate {
+                position: selection.start,
+            }
         } else {
-            InlineAssistKind::Transform
+            CodegenKind::Transform {
+                range: selection.start..selection.end,
+            }
         };
+        let codegen = cx.add_model(|cx| {
+            Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
+        });
+
         let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
         let inline_assistant = cx.add_view(|cx| {
             let assistant = InlineAssistant::new(
                 inline_assist_id,
-                assist_kind,
                 measurements.clone(),
                 self.include_conversation_in_next_inline_assist,
                 self.inline_prompt_history.clone(),
+                codegen.clone(),
+                self.workspace.clone(),
                 cx,
             );
             cx.focus_self();
@@ -321,45 +345,63 @@ impl AssistantPanel {
         self.pending_inline_assists.insert(
             inline_assist_id,
             PendingInlineAssist {
-                kind: assist_kind,
                 editor: editor.downgrade(),
-                range,
-                highlighted_ranges: Default::default(),
                 inline_assistant: Some((block_id, inline_assistant.clone())),
-                code_generation: Task::ready(None),
-                transaction_id: None,
+                codegen: codegen.clone(),
                 _subscriptions: vec![
                     cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
                     cx.subscribe(editor, {
                         let inline_assistant = inline_assistant.downgrade();
-                        move |this, editor, event, cx| {
+                        move |_, editor, event, cx| {
                             if let Some(inline_assistant) = inline_assistant.upgrade(cx) {
-                                match event {
-                                    editor::Event::SelectionsChanged { local } => {
-                                        if *local && inline_assistant.read(cx).has_focus {
-                                            cx.focus(&editor);
-                                        }
+                                if let editor::Event::SelectionsChanged { local } = event {
+                                    if *local && inline_assistant.read(cx).has_focus {
+                                        cx.focus(&editor);
                                     }
-                                    editor::Event::TransactionUndone {
-                                        transaction_id: tx_id,
-                                    } => {
-                                        if let Some(pending_assist) =
-                                            this.pending_inline_assists.get(&inline_assist_id)
-                                        {
-                                            if pending_assist.transaction_id == Some(*tx_id) {
-                                                // Notice we are supplying `undo: false` here. This
-                                                // is because there's no need to undo the transaction
-                                                // because the user just did so.
-                                                this.close_inline_assist(
-                                                    inline_assist_id,
-                                                    false,
-                                                    cx,
-                                                );
-                                            }
-                                        }
+                                }
+                            }
+                        }
+                    }),
+                    cx.observe(&codegen, {
+                        let editor = editor.downgrade();
+                        move |this, _, cx| {
+                            if let Some(editor) = editor.upgrade(cx) {
+                                this.update_highlights_for_editor(&editor, cx);
+                            }
+                        }
+                    }),
+                    cx.subscribe(&codegen, move |this, codegen, event, cx| match event {
+                        codegen::Event::Undone => {
+                            this.finish_inline_assist(inline_assist_id, false, cx)
+                        }
+                        codegen::Event::Finished => {
+                            let pending_assist = if let Some(pending_assist) =
+                                this.pending_inline_assists.get(&inline_assist_id)
+                            {
+                                pending_assist
+                            } else {
+                                return;
+                            };
+
+                            let error = codegen
+                                .read(cx)
+                                .error()
+                                .map(|error| format!("Inline assistant error: {}", error));
+                            if let Some(error) = error {
+                                if pending_assist.inline_assistant.is_none() {
+                                    if let Some(workspace) = this.workspace.upgrade(cx) {
+                                        workspace.update(cx, |workspace, cx| {
+                                            workspace.show_toast(
+                                                Toast::new(inline_assist_id, error),
+                                                cx,
+                                            );
+                                        })
                                     }
-                                    _ => {}
+
+                                    this.finish_inline_assist(inline_assist_id, false, cx);
                                 }
+                            } else {
+                                this.finish_inline_assist(inline_assist_id, false, cx);
                             }
                         }
                     }),
@@ -388,7 +430,7 @@ impl AssistantPanel {
                 self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx);
             }
             InlineAssistantEvent::Canceled => {
-                self.close_inline_assist(assist_id, true, cx);
+                self.finish_inline_assist(assist_id, true, cx);
             }
             InlineAssistantEvent::Dismissed => {
                 self.hide_inline_assist(assist_id, cx);
@@ -417,7 +459,7 @@ impl AssistantPanel {
                         .get(&editor.downgrade())
                         .and_then(|assist_ids| assist_ids.last().copied())
                     {
-                        panel.close_inline_assist(assist_id, true, cx);
+                        panel.finish_inline_assist(assist_id, true, cx);
                         true
                     } else {
                         false
@@ -432,7 +474,7 @@ impl AssistantPanel {
         cx.propagate_action();
     }
 
-    fn close_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
+    fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
         self.hide_inline_assist(assist_id, cx);
 
         if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) {
@@ -450,13 +492,9 @@ impl AssistantPanel {
                 self.update_highlights_for_editor(&editor, cx);
 
                 if undo {
-                    if let Some(transaction_id) = pending_assist.transaction_id {
-                        editor.update(cx, |editor, cx| {
-                            editor.buffer().update(cx, |buffer, cx| {
-                                buffer.undo_transaction(transaction_id, cx)
-                            });
-                        });
-                    }
+                    pending_assist
+                        .codegen
+                        .update(cx, |codegen, cx| codegen.undo(cx));
                 }
             }
         }
@@ -481,12 +519,6 @@ impl AssistantPanel {
         include_conversation: bool,
         cx: &mut ViewContext<Self>,
     ) {
-        let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
-            api_key
-        } else {
-            return;
-        };
-
         let conversation = if include_conversation {
             self.active_editor()
                 .map(|editor| editor.read(cx).conversation.clone())
@@ -514,58 +546,26 @@ impl AssistantPanel {
             self.inline_prompt_history.pop_front();
         }
 
-        let range = pending_assist.range.clone();
+        let codegen = pending_assist.codegen.clone();
         let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
-        let selected_text = snapshot
-            .text_for_range(range.start..range.end)
-            .collect::<Rope>();
-
-        let selection_start = range.start.to_point(&snapshot);
-        let selection_end = range.end.to_point(&snapshot);
-
-        let mut base_indent: Option<language::IndentSize> = None;
-        let mut start_row = selection_start.row;
-        if snapshot.is_line_blank(start_row) {
-            if let Some(prev_non_blank_row) = snapshot.prev_non_blank_row(start_row) {
-                start_row = prev_non_blank_row;
-            }
-        }
-        for row in start_row..=selection_end.row {
-            if snapshot.is_line_blank(row) {
-                continue;
-            }
-
-            let line_indent = snapshot.indent_size_for_line(row);
-            if let Some(base_indent) = base_indent.as_mut() {
-                if line_indent.len < base_indent.len {
-                    *base_indent = line_indent;
-                }
+        let range = codegen.read(cx).range();
+        let start = snapshot.point_to_buffer_offset(range.start);
+        let end = snapshot.point_to_buffer_offset(range.end);
+        let (buffer, range) = if let Some((start, end)) = start.zip(end) {
+            let (start_buffer, start_buffer_offset) = start;
+            let (end_buffer, end_buffer_offset) = end;
+            if start_buffer.remote_id() == end_buffer.remote_id() {
+                (start_buffer.clone(), start_buffer_offset..end_buffer_offset)
             } else {
-                base_indent = Some(line_indent);
-            }
-        }
-
-        let mut normalized_selected_text = selected_text.clone();
-        if let Some(base_indent) = base_indent {
-            for row in selection_start.row..=selection_end.row {
-                let selection_row = row - selection_start.row;
-                let line_start =
-                    normalized_selected_text.point_to_offset(Point::new(selection_row, 0));
-                let indent_len = if row == selection_start.row {
-                    base_indent.len.saturating_sub(selection_start.column)
-                } else {
-                    let line_len = normalized_selected_text.line_len(selection_row);
-                    cmp::min(line_len, base_indent.len)
-                };
-                let indent_end = cmp::min(
-                    line_start + indent_len as usize,
-                    normalized_selected_text.len(),
-                );
-                normalized_selected_text.replace(line_start..indent_end, "");
+                self.finish_inline_assist(inline_assist_id, false, cx);
+                return;
             }
-        }
+        } else {
+            self.finish_inline_assist(inline_assist_id, false, cx);
+            return;
+        };
 
-        let language = snapshot.language_at(range.start);
+        let language = buffer.language_at(range.start);
         let language_name = if let Some(language) = language.as_ref() {
             if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
                 None
@@ -575,96 +575,13 @@ impl AssistantPanel {
         } else {
             None
         };
-        let language_name = language_name.as_deref();
-
-        let mut prompt = String::new();
-        if let Some(language_name) = language_name {
-            writeln!(prompt, "You're an expert {language_name} engineer.").unwrap();
-        }
-        match pending_assist.kind {
-            InlineAssistKind::Transform => {
-                writeln!(
-                    prompt,
-                    "You're currently working inside an editor on this file:"
-                )
-                .unwrap();
-                if let Some(language_name) = language_name {
-                    writeln!(prompt, "```{language_name}").unwrap();
-                } else {
-                    writeln!(prompt, "```").unwrap();
-                }
-                for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) {
-                    write!(prompt, "{chunk}").unwrap();
-                }
-                writeln!(prompt, "```").unwrap();
-
-                writeln!(
-                    prompt,
-                    "In particular, the user has selected the following text:"
-                )
-                .unwrap();
-                if let Some(language_name) = language_name {
-                    writeln!(prompt, "```{language_name}").unwrap();
-                } else {
-                    writeln!(prompt, "```").unwrap();
-                }
-                writeln!(prompt, "{normalized_selected_text}").unwrap();
-                writeln!(prompt, "```").unwrap();
-                writeln!(prompt).unwrap();
-                writeln!(
-                    prompt,
-                    "Modify the selected text given the user prompt: {user_prompt}"
-                )
-                .unwrap();
-                writeln!(
-                    prompt,
-                    "You MUST reply only with the edited selected text, not the entire file."
-                )
-                .unwrap();
-            }
-            InlineAssistKind::Generate => {
-                writeln!(
-                    prompt,
-                    "You're currently working inside an editor on this file:"
-                )
-                .unwrap();
-                if let Some(language_name) = language_name {
-                    writeln!(prompt, "```{language_name}").unwrap();
-                } else {
-                    writeln!(prompt, "```").unwrap();
-                }
-                for chunk in snapshot.text_for_range(Anchor::min()..range.start) {
-                    write!(prompt, "{chunk}").unwrap();
-                }
-                write!(prompt, "<|>").unwrap();
-                for chunk in snapshot.text_for_range(range.start..Anchor::max()) {
-                    write!(prompt, "{chunk}").unwrap();
-                }
-                writeln!(prompt).unwrap();
-                writeln!(prompt, "```").unwrap();
-                writeln!(
-                    prompt,
-                    "Assume the cursor is located where the `<|>` marker is."
-                )
-                .unwrap();
-                writeln!(
-                    prompt,
-                    "Text can't be replaced, so assume your answer will be inserted at the cursor."
-                )
-                .unwrap();
-                writeln!(
-                    prompt,
-                    "Complete the text given the user prompt: {user_prompt}"
-                )
-                .unwrap();
-            }
-        }
-        if let Some(language_name) = language_name {
-            writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap();
-        }
-        writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap();
-        writeln!(prompt, "Never make remarks about the output.").unwrap();
 
+        let codegen_kind = codegen.read(cx).kind().clone();
+        let user_prompt = user_prompt.to_string();
+        let prompt = cx.background().spawn(async move {
+            let language_name = language_name.as_deref();
+            generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
+        });
         let mut messages = Vec::new();
         let mut model = settings::get::<AssistantSettings>(cx)
             .default_open_ai_model
@@ -680,218 +597,21 @@ impl AssistantPanel {
             model = conversation.model.clone();
         }
 
-        messages.push(RequestMessage {
-            role: Role::User,
-            content: prompt,
-        });
-        let request = OpenAIRequest {
-            model: model.full_name().into(),
-            messages,
-            stream: true,
-        };
-        let response = stream_completion(api_key, cx.background().clone(), request);
-        let editor = editor.downgrade();
-
-        pending_assist.code_generation = cx.spawn(|this, mut cx| {
-            async move {
-                let mut edit_start = range.start.to_offset(&snapshot);
-
-                let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
-                let diff = cx.background().spawn(async move {
-                    let chunks = strip_markdown_codeblock(response.await?.filter_map(
-                        |message| async move {
-                            match message {
-                                Ok(mut message) => Some(Ok(message.choices.pop()?.delta.content?)),
-                                Err(error) => Some(Err(error)),
-                            }
-                        },
-                    ));
-                    futures::pin_mut!(chunks);
-                    let mut diff = StreamingDiff::new(selected_text.to_string());
-
-                    let mut indent_len;
-                    let indent_text;
-                    if let Some(base_indent) = base_indent {
-                        indent_len = base_indent.len;
-                        indent_text = match base_indent.kind {
-                            language::IndentKind::Space => " ",
-                            language::IndentKind::Tab => "\t",
-                        };
-                    } else {
-                        indent_len = 0;
-                        indent_text = "";
-                    };
-
-                    let mut first_line_len = 0;
-                    let mut first_line_non_whitespace_char_ix = None;
-                    let mut first_line = true;
-                    let mut new_text = String::new();
-
-                    while let Some(chunk) = chunks.next().await {
-                        let chunk = chunk?;
-
-                        let mut lines = chunk.split('\n');
-                        if let Some(mut line) = lines.next() {
-                            if first_line {
-                                if first_line_non_whitespace_char_ix.is_none() {
-                                    if let Some(mut char_ix) =
-                                        line.find(|ch: char| !ch.is_whitespace())
-                                    {
-                                        line = &line[char_ix..];
-                                        char_ix += first_line_len;
-                                        first_line_non_whitespace_char_ix = Some(char_ix);
-                                        let first_line_indent = char_ix
-                                            .saturating_sub(selection_start.column as usize)
-                                            as usize;
-                                        new_text.push_str(&indent_text.repeat(first_line_indent));
-                                        indent_len = indent_len.saturating_sub(char_ix as u32);
-                                    }
-                                }
-                                first_line_len += line.len();
-                            }
-
-                            if first_line_non_whitespace_char_ix.is_some() {
-                                new_text.push_str(line);
-                            }
-                        }
-
-                        for line in lines {
-                            first_line = false;
-                            new_text.push('\n');
-                            if !line.is_empty() {
-                                new_text.push_str(&indent_text.repeat(indent_len as usize));
-                            }
-                            new_text.push_str(line);
-                        }
-
-                        let hunks = diff.push_new(&new_text);
-                        hunks_tx.send(hunks).await?;
-                        new_text.clear();
-                    }
-                    hunks_tx.send(diff.finish()).await?;
-
-                    anyhow::Ok(())
-                });
-
-                while let Some(hunks) = hunks_rx.next().await {
-                    let editor = if let Some(editor) = editor.upgrade(&cx) {
-                        editor
-                    } else {
-                        break;
-                    };
-
-                    let this = if let Some(this) = this.upgrade(&cx) {
-                        this
-                    } else {
-                        break;
-                    };
-
-                    this.update(&mut cx, |this, cx| {
-                        let pending_assist = if let Some(pending_assist) =
-                            this.pending_inline_assists.get_mut(&inline_assist_id)
-                        {
-                            pending_assist
-                        } else {
-                            return;
-                        };
-
-                        pending_assist.highlighted_ranges.clear();
-                        editor.update(cx, |editor, cx| {
-                            let transaction = editor.buffer().update(cx, |buffer, cx| {
-                                // Avoid grouping assistant edits with user edits.
-                                buffer.finalize_last_transaction(cx);
-
-                                buffer.start_transaction(cx);
-                                buffer.edit(
-                                    hunks.into_iter().filter_map(|hunk| match hunk {
-                                        Hunk::Insert { text } => {
-                                            let edit_start = snapshot.anchor_after(edit_start);
-                                            Some((edit_start..edit_start, text))
-                                        }
-                                        Hunk::Remove { len } => {
-                                            let edit_end = edit_start + len;
-                                            let edit_range = snapshot.anchor_after(edit_start)
-                                                ..snapshot.anchor_before(edit_end);
-                                            edit_start = edit_end;
-                                            Some((edit_range, String::new()))
-                                        }
-                                        Hunk::Keep { len } => {
-                                            let edit_end = edit_start + len;
-                                            let edit_range = snapshot.anchor_after(edit_start)
-                                                ..snapshot.anchor_before(edit_end);
-                                            edit_start += len;
-                                            pending_assist.highlighted_ranges.push(edit_range);
-                                            None
-                                        }
-                                    }),
-                                    None,
-                                    cx,
-                                );
-
-                                buffer.end_transaction(cx)
-                            });
-
-                            if let Some(transaction) = transaction {
-                                if let Some(first_transaction) = pending_assist.transaction_id {
-                                    // Group all assistant edits into the first transaction.
-                                    editor.buffer().update(cx, |buffer, cx| {
-                                        buffer.merge_transactions(
-                                            transaction,
-                                            first_transaction,
-                                            cx,
-                                        )
-                                    });
-                                } else {
-                                    pending_assist.transaction_id = Some(transaction);
-                                    editor.buffer().update(cx, |buffer, cx| {
-                                        buffer.finalize_last_transaction(cx)
-                                    });
-                                }
-                            }
-                        });
-
-                        this.update_highlights_for_editor(&editor, cx);
-                    });
-                }
-
-                if let Err(error) = diff.await {
-                    this.update(&mut cx, |this, cx| {
-                        let pending_assist = if let Some(pending_assist) =
-                            this.pending_inline_assists.get_mut(&inline_assist_id)
-                        {
-                            pending_assist
-                        } else {
-                            return;
-                        };
-
-                        if let Some((_, inline_assistant)) =
-                            pending_assist.inline_assistant.as_ref()
-                        {
-                            inline_assistant.update(cx, |inline_assistant, cx| {
-                                inline_assistant.set_error(error, cx);
-                            });
-                        } else if let Some(workspace) = this.workspace.upgrade(cx) {
-                            workspace.update(cx, |workspace, cx| {
-                                workspace.show_toast(
-                                    Toast::new(
-                                        inline_assist_id,
-                                        format!("Inline assistant error: {}", error),
-                                    ),
-                                    cx,
-                                );
-                            })
-                        }
-                    })?;
-                } else {
-                    let _ = this.update(&mut cx, |this, cx| {
-                        this.close_inline_assist(inline_assist_id, false, cx)
-                    });
-                }
+        cx.spawn(|_, mut cx| async move {
+            let prompt = prompt.await;
 
-                anyhow::Ok(())
-            }
-            .log_err()
-        });
+            messages.push(RequestMessage {
+                role: Role::User,
+                content: prompt,
+            });
+            let request = OpenAIRequest {
+                model: model.full_name().into(),
+                messages,
+                stream: true,
+            };
+            codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx));
+        })
+        .detach();
     }
 
     fn update_highlights_for_editor(
@@ -909,8 +629,9 @@ impl AssistantPanel {
 
         for inline_assist_id in inline_assist_ids {
             if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) {
-                background_ranges.push(pending_assist.range.clone());
-                foreground_ranges.extend(pending_assist.highlighted_ranges.iter().cloned());
+                let codegen = pending_assist.codegen.read(cx);
+                background_ranges.push(codegen.range());
+                foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
             }
         }
 
@@ -929,7 +650,7 @@ impl AssistantPanel {
             }
 
             if foreground_ranges.is_empty() {
-                editor.clear_text_highlights::<PendingInlineAssist>(cx);
+                editor.clear_highlights::<PendingInlineAssist>(cx);
             } else {
                 editor.highlight_text::<PendingInlineAssist>(
                     foreground_ranges,
@@ -949,6 +670,7 @@ impl AssistantPanel {
                 self.api_key.clone(),
                 self.languages.clone(),
                 self.fs.clone(),
+                self.workspace.clone(),
                 cx,
             )
         });
@@ -1284,6 +1006,7 @@ impl AssistantPanel {
         }
 
         let fs = self.fs.clone();
+        let workspace = self.workspace.clone();
         let api_key = self.api_key.clone();
         let languages = self.languages.clone();
         cx.spawn(|this, mut cx| async move {
@@ -1298,8 +1021,9 @@ impl AssistantPanel {
                 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));
+                    let editor = cx.add_view(|cx| {
+                        ConversationEditor::for_conversation(conversation, fs, workspace, cx)
+                    });
                     this.add_conversation(editor, cx);
                 }
             })?;
@@ -1573,6 +1297,7 @@ struct Summary {
 }
 
 struct Conversation {
+    id: Option<String>,
     buffer: ModelHandle<Buffer>,
     message_anchors: Vec<MessageAnchor>,
     messages_metadata: HashMap<MessageId, MessageMetadata>,
@@ -1623,6 +1348,7 @@ impl Conversation {
         let model = settings.default_open_ai_model.clone();
 
         let mut this = Self {
+            id: Some(Uuid::new_v4().to_string()),
             message_anchors: Default::default(),
             messages_metadata: Default::default(),
             next_message_id: Default::default(),
@@ -1660,6 +1386,7 @@ impl Conversation {
 
     fn serialize(&self, cx: &AppContext) -> SavedConversation {
         SavedConversation {
+            id: self.id.clone(),
             zed: "conversation".into(),
             version: SavedConversation::VERSION.into(),
             text: self.buffer.read(cx).text(),
@@ -1687,6 +1414,10 @@ impl Conversation {
         language_registry: Arc<LanguageRegistry>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
+        let id = match saved_conversation.id {
+            Some(id) => Some(id),
+            None => Some(Uuid::new_v4().to_string()),
+        };
         let model = saved_conversation.model;
         let markdown = language_registry.language_for_name("Markdown");
         let mut message_anchors = Vec::new();
@@ -1716,6 +1447,7 @@ impl Conversation {
         });
 
         let mut this = Self {
+            id,
             message_anchors,
             messages_metadata: saved_conversation.message_metadata,
             next_message_id,
@@ -2333,6 +2065,7 @@ struct ScrollPosition {
 struct ConversationEditor {
     conversation: ModelHandle<Conversation>,
     fs: Arc<dyn Fs>,
+    workspace: WeakViewHandle<Workspace>,
     editor: ViewHandle<Editor>,
     blocks: HashSet<BlockId>,
     scroll_position: Option<ScrollPosition>,
@@ -2344,15 +2077,17 @@ impl ConversationEditor {
         api_key: Rc<RefCell<Option<String>>>,
         language_registry: Arc<LanguageRegistry>,
         fs: Arc<dyn Fs>,
+        workspace: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let conversation = cx.add_model(|cx| Conversation::new(api_key, language_registry, cx));
-        Self::for_conversation(conversation, fs, cx)
+        Self::for_conversation(conversation, fs, workspace, cx)
     }
 
     fn for_conversation(
         conversation: ModelHandle<Conversation>,
         fs: Arc<dyn Fs>,
+        workspace: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let editor = cx.add_view(|cx| {
@@ -2375,6 +2110,7 @@ impl ConversationEditor {
             blocks: Default::default(),
             scroll_position: None,
             fs,
+            workspace,
             _subscriptions,
         };
         this.update_message_headers(cx);
@@ -2382,6 +2118,13 @@ impl ConversationEditor {
     }
 
     fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
+        report_assistant_event(
+            self.workspace.clone(),
+            self.conversation.read(cx).id.clone(),
+            AssistantKind::Panel,
+            cx,
+        );
+
         let cursors = self.cursors(cx);
 
         let user_messages = self.conversation.update(cx, |conversation, cx| {
@@ -2601,7 +2344,7 @@ impl ConversationEditor {
                                 .with_children(
                                     if let MessageStatus::Error(error) = &message.status {
                                         Some(
-                                            Svg::new("icons/circle_x_mark_12.svg")
+                                            Svg::new("icons/error.svg")
                                                 .with_color(style.error_icon.color)
                                                 .constrained()
                                                 .with_width(style.error_icon.width)
@@ -2887,24 +2630,19 @@ enum InlineAssistantEvent {
     },
 }
 
-#[derive(Copy, Clone)]
-enum InlineAssistKind {
-    Transform,
-    Generate,
-}
-
 struct InlineAssistant {
     id: usize,
     prompt_editor: ViewHandle<Editor>,
+    workspace: WeakViewHandle<Workspace>,
     confirmed: bool,
     has_focus: bool,
     include_conversation: bool,
     measurements: Rc<Cell<BlockMeasurements>>,
-    error: Option<anyhow::Error>,
     prompt_history: VecDeque<String>,
     prompt_history_ix: Option<usize>,
     pending_prompt: String,
-    _subscription: Subscription,
+    codegen: ModelHandle<Codegen>,
+    _subscriptions: Vec<Subscription>,
 }
 
 impl Entity for InlineAssistant {
@@ -2933,9 +2671,9 @@ impl View for InlineAssistant {
                             .element()
                             .aligned(),
                     )
-                    .with_children(if let Some(error) = self.error.as_ref() {
+                    .with_children(if let Some(error) = self.codegen.read(cx).error() {
                         Some(
-                            Svg::new("icons/circle_x_mark_12.svg")
+                            Svg::new("icons/error.svg")
                                 .with_color(theme.assistant.error_icon.color)
                                 .constrained()
                                 .with_width(theme.assistant.error_icon.width)
@@ -3007,10 +2745,11 @@ impl View for InlineAssistant {
 impl InlineAssistant {
     fn new(
         id: usize,
-        kind: InlineAssistKind,
         measurements: Rc<Cell<BlockMeasurements>>,
         include_conversation: bool,
         prompt_history: VecDeque<String>,
+        codegen: ModelHandle<Codegen>,
+        workspace: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let prompt_editor = cx.add_view(|cx| {
@@ -3018,26 +2757,30 @@ impl InlineAssistant {
                 Some(Arc::new(|theme| theme.assistant.inline.editor.clone())),
                 cx,
             );
-            let placeholder = match kind {
-                InlineAssistKind::Transform => "Enter transformation prompt…",
-                InlineAssistKind::Generate => "Enter generation prompt…",
+            let placeholder = match codegen.read(cx).kind() {
+                CodegenKind::Transform { .. } => "Enter transformation prompt…",
+                CodegenKind::Generate { .. } => "Enter generation prompt…",
             };
             editor.set_placeholder_text(placeholder, cx);
             editor
         });
-        let subscription = cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events);
+        let subscriptions = vec![
+            cx.observe(&codegen, Self::handle_codegen_changed),
+            cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
+        ];
         Self {
             id,
             prompt_editor,
+            workspace,
             confirmed: false,
             has_focus: false,
             include_conversation,
             measurements,
-            error: None,
             prompt_history,
             prompt_history_ix: None,
             pending_prompt: String::new(),
-            _subscription: subscription,
+            codegen,
+            _subscriptions: subscriptions,
         }
     }
 
@@ -3053,6 +2796,32 @@ impl InlineAssistant {
         }
     }
 
+    fn handle_codegen_changed(&mut self, _: ModelHandle<Codegen>, cx: &mut ViewContext<Self>) {
+        let is_read_only = !self.codegen.read(cx).idle();
+        self.prompt_editor.update(cx, |editor, cx| {
+            let was_read_only = editor.read_only();
+            if was_read_only != is_read_only {
+                if is_read_only {
+                    editor.set_read_only(true);
+                    editor.set_field_editor_style(
+                        Some(Arc::new(|theme| {
+                            theme.assistant.inline.disabled_editor.clone()
+                        })),
+                        cx,
+                    );
+                } else {
+                    self.confirmed = false;
+                    editor.set_read_only(false);
+                    editor.set_field_editor_style(
+                        Some(Arc::new(|theme| theme.assistant.inline.editor.clone())),
+                        cx,
+                    );
+                }
+            }
+        });
+        cx.notify();
+    }
+
     fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
         cx.emit(InlineAssistantEvent::Canceled);
     }
@@ -3061,6 +2830,8 @@ impl InlineAssistant {
         if self.confirmed {
             cx.emit(InlineAssistantEvent::Dismissed);
         } else {
+            report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx);
+
             let prompt = self.prompt_editor.read(cx).text(cx);
             self.prompt_editor.update(cx, |editor, cx| {
                 editor.set_read_only(true);
@@ -3076,7 +2847,6 @@ impl InlineAssistant {
                 include_conversation: self.include_conversation,
             });
             self.confirmed = true;
-            self.error = None;
             cx.notify();
         }
     }
@@ -3093,19 +2863,6 @@ impl InlineAssistant {
         cx.notify();
     }
 
-    fn set_error(&mut self, error: anyhow::Error, cx: &mut ViewContext<Self>) {
-        self.error = Some(error);
-        self.confirmed = false;
-        self.prompt_editor.update(cx, |editor, cx| {
-            editor.set_read_only(false);
-            editor.set_field_editor_style(
-                Some(Arc::new(|theme| theme.assistant.inline.editor.clone())),
-                cx,
-            );
-        });
-        cx.notify();
-    }
-
     fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
         if let Some(ix) = self.prompt_history_ix {
             if ix > 0 {
@@ -3152,13 +2909,9 @@ struct BlockMeasurements {
 }
 
 struct PendingInlineAssist {
-    kind: InlineAssistKind,
     editor: WeakViewHandle<Editor>,
-    range: Range<Anchor>,
-    highlighted_ranges: Vec<Range<Anchor>>,
     inline_assistant: Option<(BlockId, ViewHandle<InlineAssistant>)>,
-    code_generation: Task<Option<()>>,
-    transaction_id: Option<TransactionId>,
+    codegen: ModelHandle<Codegen>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -3184,65 +2937,10 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
     }
 }
 
-fn strip_markdown_codeblock(
-    stream: impl Stream<Item = Result<String>>,
-) -> impl Stream<Item = Result<String>> {
-    let mut first_line = true;
-    let mut buffer = String::new();
-    let mut starts_with_fenced_code_block = false;
-    stream.filter_map(move |chunk| {
-        let chunk = match chunk {
-            Ok(chunk) => chunk,
-            Err(err) => return future::ready(Some(Err(err))),
-        };
-        buffer.push_str(&chunk);
-
-        if first_line {
-            if buffer == "" || buffer == "`" || buffer == "``" {
-                return future::ready(None);
-            } else if buffer.starts_with("```") {
-                starts_with_fenced_code_block = true;
-                if let Some(newline_ix) = buffer.find('\n') {
-                    buffer.replace_range(..newline_ix + 1, "");
-                    first_line = false;
-                } else {
-                    return future::ready(None);
-                }
-            }
-        }
-
-        let text = if starts_with_fenced_code_block {
-            buffer
-                .strip_suffix("\n```\n")
-                .or_else(|| buffer.strip_suffix("\n```"))
-                .or_else(|| buffer.strip_suffix("\n``"))
-                .or_else(|| buffer.strip_suffix("\n`"))
-                .or_else(|| buffer.strip_suffix('\n'))
-                .unwrap_or(&buffer)
-        } else {
-            &buffer
-        };
-
-        if text.contains('\n') {
-            first_line = false;
-        }
-
-        let remainder = buffer.split_off(text.len());
-        let result = if buffer.is_empty() {
-            None
-        } else {
-            Some(Ok(buffer.clone()))
-        };
-        buffer = remainder;
-        future::ready(result)
-    })
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
     use crate::MessageId;
-    use futures::stream;
     use gpui::AppContext;
 
     #[gpui::test]

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

@@ -0,0 +1,663 @@
+use crate::streaming_diff::{Hunk, StreamingDiff};
+use ai::completion::{CompletionProvider, OpenAIRequest};
+use anyhow::Result;
+use editor::{
+    multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+};
+use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
+use gpui::{Entity, ModelContext, ModelHandle, Task};
+use language::{Rope, TransactionId};
+use std::{cmp, future, ops::Range, sync::Arc};
+
+pub enum Event {
+    Finished,
+    Undone,
+}
+
+#[derive(Clone)]
+pub enum CodegenKind {
+    Transform { range: Range<Anchor> },
+    Generate { position: Anchor },
+}
+
+pub struct Codegen {
+    provider: Arc<dyn CompletionProvider>,
+    buffer: ModelHandle<MultiBuffer>,
+    snapshot: MultiBufferSnapshot,
+    kind: CodegenKind,
+    last_equal_ranges: Vec<Range<Anchor>>,
+    transaction_id: Option<TransactionId>,
+    error: Option<anyhow::Error>,
+    generation: Task<()>,
+    idle: bool,
+    _subscription: gpui::Subscription,
+}
+
+impl Entity for Codegen {
+    type Event = Event;
+}
+
+impl Codegen {
+    pub fn new(
+        buffer: ModelHandle<MultiBuffer>,
+        mut kind: CodegenKind,
+        provider: Arc<dyn CompletionProvider>,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        let snapshot = buffer.read(cx).snapshot(cx);
+        match &mut kind {
+            CodegenKind::Transform { range } => {
+                let mut point_range = range.to_point(&snapshot);
+                point_range.start.column = 0;
+                if point_range.end.column > 0 || point_range.start.row == point_range.end.row {
+                    point_range.end.column = snapshot.line_len(point_range.end.row);
+                }
+                range.start = snapshot.anchor_before(point_range.start);
+                range.end = snapshot.anchor_after(point_range.end);
+            }
+            CodegenKind::Generate { position } => {
+                *position = position.bias_right(&snapshot);
+            }
+        }
+
+        Self {
+            provider,
+            buffer: buffer.clone(),
+            snapshot,
+            kind,
+            last_equal_ranges: Default::default(),
+            transaction_id: Default::default(),
+            error: Default::default(),
+            idle: true,
+            generation: Task::ready(()),
+            _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
+        }
+    }
+
+    fn handle_buffer_event(
+        &mut self,
+        _buffer: ModelHandle<MultiBuffer>,
+        event: &multi_buffer::Event,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
+            if self.transaction_id == Some(*transaction_id) {
+                self.transaction_id = None;
+                self.generation = Task::ready(());
+                cx.emit(Event::Undone);
+            }
+        }
+    }
+
+    pub fn range(&self) -> Range<Anchor> {
+        match &self.kind {
+            CodegenKind::Transform { range } => range.clone(),
+            CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
+        }
+    }
+
+    pub fn kind(&self) -> &CodegenKind {
+        &self.kind
+    }
+
+    pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
+        &self.last_equal_ranges
+    }
+
+    pub fn idle(&self) -> bool {
+        self.idle
+    }
+
+    pub fn error(&self) -> Option<&anyhow::Error> {
+        self.error.as_ref()
+    }
+
+    pub fn start(&mut self, prompt: OpenAIRequest, cx: &mut ModelContext<Self>) {
+        let range = self.range();
+        let snapshot = self.snapshot.clone();
+        let selected_text = snapshot
+            .text_for_range(range.start..range.end)
+            .collect::<Rope>();
+
+        let selection_start = range.start.to_point(&snapshot);
+        let suggested_line_indent = snapshot
+            .suggested_indents(selection_start.row..selection_start.row + 1, cx)
+            .into_values()
+            .next()
+            .unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row));
+
+        let response = self.provider.complete(prompt);
+        self.generation = cx.spawn_weak(|this, mut cx| {
+            async move {
+                let generate = async {
+                    let mut edit_start = range.start.to_offset(&snapshot);
+
+                    let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
+                    let diff = cx.background().spawn(async move {
+                        let chunks = strip_markdown_codeblock(response.await?);
+                        futures::pin_mut!(chunks);
+                        let mut diff = StreamingDiff::new(selected_text.to_string());
+
+                        let mut new_text = String::new();
+                        let mut base_indent = None;
+                        let mut line_indent = None;
+                        let mut first_line = true;
+
+                        while let Some(chunk) = chunks.next().await {
+                            let chunk = chunk?;
+
+                            let mut lines = chunk.split('\n').peekable();
+                            while let Some(line) = lines.next() {
+                                new_text.push_str(line);
+                                if line_indent.is_none() {
+                                    if let Some(non_whitespace_ch_ix) =
+                                        new_text.find(|ch: char| !ch.is_whitespace())
+                                    {
+                                        line_indent = Some(non_whitespace_ch_ix);
+                                        base_indent = base_indent.or(line_indent);
+
+                                        let line_indent = line_indent.unwrap();
+                                        let base_indent = base_indent.unwrap();
+                                        let indent_delta = line_indent as i32 - base_indent as i32;
+                                        let mut corrected_indent_len = cmp::max(
+                                            0,
+                                            suggested_line_indent.len as i32 + indent_delta,
+                                        )
+                                            as usize;
+                                        if first_line {
+                                            corrected_indent_len = corrected_indent_len
+                                                .saturating_sub(selection_start.column as usize);
+                                        }
+
+                                        let indent_char = suggested_line_indent.char();
+                                        let mut indent_buffer = [0; 4];
+                                        let indent_str =
+                                            indent_char.encode_utf8(&mut indent_buffer);
+                                        new_text.replace_range(
+                                            ..line_indent,
+                                            &indent_str.repeat(corrected_indent_len),
+                                        );
+                                    }
+                                }
+
+                                if line_indent.is_some() {
+                                    hunks_tx.send(diff.push_new(&new_text)).await?;
+                                    new_text.clear();
+                                }
+
+                                if lines.peek().is_some() {
+                                    hunks_tx.send(diff.push_new("\n")).await?;
+                                    line_indent = None;
+                                    first_line = false;
+                                }
+                            }
+                        }
+                        hunks_tx.send(diff.push_new(&new_text)).await?;
+                        hunks_tx.send(diff.finish()).await?;
+
+                        anyhow::Ok(())
+                    });
+
+                    while let Some(hunks) = hunks_rx.next().await {
+                        let this = if let Some(this) = this.upgrade(&cx) {
+                            this
+                        } else {
+                            break;
+                        };
+
+                        this.update(&mut cx, |this, cx| {
+                            this.last_equal_ranges.clear();
+
+                            let transaction = this.buffer.update(cx, |buffer, cx| {
+                                // Avoid grouping assistant edits with user edits.
+                                buffer.finalize_last_transaction(cx);
+
+                                buffer.start_transaction(cx);
+                                buffer.edit(
+                                    hunks.into_iter().filter_map(|hunk| match hunk {
+                                        Hunk::Insert { text } => {
+                                            let edit_start = snapshot.anchor_after(edit_start);
+                                            Some((edit_start..edit_start, text))
+                                        }
+                                        Hunk::Remove { len } => {
+                                            let edit_end = edit_start + len;
+                                            let edit_range = snapshot.anchor_after(edit_start)
+                                                ..snapshot.anchor_before(edit_end);
+                                            edit_start = edit_end;
+                                            Some((edit_range, String::new()))
+                                        }
+                                        Hunk::Keep { len } => {
+                                            let edit_end = edit_start + len;
+                                            let edit_range = snapshot.anchor_after(edit_start)
+                                                ..snapshot.anchor_before(edit_end);
+                                            edit_start = edit_end;
+                                            this.last_equal_ranges.push(edit_range);
+                                            None
+                                        }
+                                    }),
+                                    None,
+                                    cx,
+                                );
+
+                                buffer.end_transaction(cx)
+                            });
+
+                            if let Some(transaction) = transaction {
+                                if let Some(first_transaction) = this.transaction_id {
+                                    // Group all assistant edits into the first transaction.
+                                    this.buffer.update(cx, |buffer, cx| {
+                                        buffer.merge_transactions(
+                                            transaction,
+                                            first_transaction,
+                                            cx,
+                                        )
+                                    });
+                                } else {
+                                    this.transaction_id = Some(transaction);
+                                    this.buffer.update(cx, |buffer, cx| {
+                                        buffer.finalize_last_transaction(cx)
+                                    });
+                                }
+                            }
+
+                            cx.notify();
+                        });
+                    }
+
+                    diff.await?;
+                    anyhow::Ok(())
+                };
+
+                let result = generate.await;
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| {
+                        this.last_equal_ranges.clear();
+                        this.idle = true;
+                        if let Err(error) = result {
+                            this.error = Some(error);
+                        }
+                        cx.emit(Event::Finished);
+                        cx.notify();
+                    });
+                }
+            }
+        });
+        self.error.take();
+        self.idle = false;
+        cx.notify();
+    }
+
+    pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
+        if let Some(transaction_id) = self.transaction_id {
+            self.buffer
+                .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
+        }
+    }
+}
+
+fn strip_markdown_codeblock(
+    stream: impl Stream<Item = Result<String>>,
+) -> impl Stream<Item = Result<String>> {
+    let mut first_line = true;
+    let mut buffer = String::new();
+    let mut starts_with_fenced_code_block = false;
+    stream.filter_map(move |chunk| {
+        let chunk = match chunk {
+            Ok(chunk) => chunk,
+            Err(err) => return future::ready(Some(Err(err))),
+        };
+        buffer.push_str(&chunk);
+
+        if first_line {
+            if buffer == "" || buffer == "`" || buffer == "``" {
+                return future::ready(None);
+            } else if buffer.starts_with("```") {
+                starts_with_fenced_code_block = true;
+                if let Some(newline_ix) = buffer.find('\n') {
+                    buffer.replace_range(..newline_ix + 1, "");
+                    first_line = false;
+                } else {
+                    return future::ready(None);
+                }
+            }
+        }
+
+        let text = if starts_with_fenced_code_block {
+            buffer
+                .strip_suffix("\n```\n")
+                .or_else(|| buffer.strip_suffix("\n```"))
+                .or_else(|| buffer.strip_suffix("\n``"))
+                .or_else(|| buffer.strip_suffix("\n`"))
+                .or_else(|| buffer.strip_suffix('\n'))
+                .unwrap_or(&buffer)
+        } else {
+            &buffer
+        };
+
+        if text.contains('\n') {
+            first_line = false;
+        }
+
+        let remainder = buffer.split_off(text.len());
+        let result = if buffer.is_empty() {
+            None
+        } else {
+            Some(Ok(buffer.clone()))
+        };
+        buffer = remainder;
+        future::ready(result)
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use futures::{
+        future::BoxFuture,
+        stream::{self, BoxStream},
+    };
+    use gpui::{executor::Deterministic, TestAppContext};
+    use indoc::indoc;
+    use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
+    use parking_lot::Mutex;
+    use rand::prelude::*;
+    use settings::SettingsStore;
+    use smol::future::FutureExt;
+
+    #[gpui::test(iterations = 10)]
+    async fn test_transform_autoindent(
+        cx: &mut TestAppContext,
+        mut rng: StdRng,
+        deterministic: Arc<Deterministic>,
+    ) {
+        cx.set_global(cx.read(SettingsStore::test));
+        cx.update(language_settings::init);
+
+        let text = indoc! {"
+            fn main() {
+                let x = 0;
+                for _ in 0..10 {
+                    x += 1;
+                }
+            }
+        "};
+        let buffer =
+            cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        let range = buffer.read_with(cx, |buffer, cx| {
+            let snapshot = buffer.snapshot(cx);
+            snapshot.anchor_before(Point::new(1, 4))..snapshot.anchor_after(Point::new(4, 4))
+        });
+        let provider = Arc::new(TestCompletionProvider::new());
+        let codegen = cx.add_model(|cx| {
+            Codegen::new(
+                buffer.clone(),
+                CodegenKind::Transform { range },
+                provider.clone(),
+                cx,
+            )
+        });
+        codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx));
+
+        let mut new_text = concat!(
+            "       let mut x = 0;\n",
+            "       while x < 10 {\n",
+            "           x += 1;\n",
+            "       }",
+        );
+        while !new_text.is_empty() {
+            let max_len = cmp::min(new_text.len(), 10);
+            let len = rng.gen_range(1..=max_len);
+            let (chunk, suffix) = new_text.split_at(len);
+            provider.send_completion(chunk);
+            new_text = suffix;
+            deterministic.run_until_parked();
+        }
+        provider.finish_completion();
+        deterministic.run_until_parked();
+
+        assert_eq!(
+            buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
+            indoc! {"
+                fn main() {
+                    let mut x = 0;
+                    while x < 10 {
+                        x += 1;
+                    }
+                }
+            "}
+        );
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_autoindent_when_generating_past_indentation(
+        cx: &mut TestAppContext,
+        mut rng: StdRng,
+        deterministic: Arc<Deterministic>,
+    ) {
+        cx.set_global(cx.read(SettingsStore::test));
+        cx.update(language_settings::init);
+
+        let text = indoc! {"
+            fn main() {
+                le
+            }
+        "};
+        let buffer =
+            cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        let position = buffer.read_with(cx, |buffer, cx| {
+            let snapshot = buffer.snapshot(cx);
+            snapshot.anchor_before(Point::new(1, 6))
+        });
+        let provider = Arc::new(TestCompletionProvider::new());
+        let codegen = cx.add_model(|cx| {
+            Codegen::new(
+                buffer.clone(),
+                CodegenKind::Generate { position },
+                provider.clone(),
+                cx,
+            )
+        });
+        codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx));
+
+        let mut new_text = concat!(
+            "t mut x = 0;\n",
+            "while x < 10 {\n",
+            "    x += 1;\n",
+            "}", //
+        );
+        while !new_text.is_empty() {
+            let max_len = cmp::min(new_text.len(), 10);
+            let len = rng.gen_range(1..=max_len);
+            let (chunk, suffix) = new_text.split_at(len);
+            provider.send_completion(chunk);
+            new_text = suffix;
+            deterministic.run_until_parked();
+        }
+        provider.finish_completion();
+        deterministic.run_until_parked();
+
+        assert_eq!(
+            buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
+            indoc! {"
+                fn main() {
+                    let mut x = 0;
+                    while x < 10 {
+                        x += 1;
+                    }
+                }
+            "}
+        );
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_autoindent_when_generating_before_indentation(
+        cx: &mut TestAppContext,
+        mut rng: StdRng,
+        deterministic: Arc<Deterministic>,
+    ) {
+        cx.set_global(cx.read(SettingsStore::test));
+        cx.update(language_settings::init);
+
+        let text = concat!(
+            "fn main() {\n",
+            "  \n",
+            "}\n" //
+        );
+        let buffer =
+            cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        let position = buffer.read_with(cx, |buffer, cx| {
+            let snapshot = buffer.snapshot(cx);
+            snapshot.anchor_before(Point::new(1, 2))
+        });
+        let provider = Arc::new(TestCompletionProvider::new());
+        let codegen = cx.add_model(|cx| {
+            Codegen::new(
+                buffer.clone(),
+                CodegenKind::Generate { position },
+                provider.clone(),
+                cx,
+            )
+        });
+        codegen.update(cx, |codegen, cx| codegen.start(Default::default(), cx));
+
+        let mut new_text = concat!(
+            "let mut x = 0;\n",
+            "while x < 10 {\n",
+            "    x += 1;\n",
+            "}", //
+        );
+        while !new_text.is_empty() {
+            let max_len = cmp::min(new_text.len(), 10);
+            let len = rng.gen_range(1..=max_len);
+            let (chunk, suffix) = new_text.split_at(len);
+            provider.send_completion(chunk);
+            new_text = suffix;
+            deterministic.run_until_parked();
+        }
+        provider.finish_completion();
+        deterministic.run_until_parked();
+
+        assert_eq!(
+            buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
+            indoc! {"
+                fn main() {
+                    let mut x = 0;
+                    while x < 10 {
+                        x += 1;
+                    }
+                }
+            "}
+        );
+    }
+
+    #[gpui::test]
+    async fn test_strip_markdown_codeblock() {
+        assert_eq!(
+            strip_markdown_codeblock(chunks("Lorem ipsum dolor", 2))
+                .map(|chunk| chunk.unwrap())
+                .collect::<String>()
+                .await,
+            "Lorem ipsum dolor"
+        );
+        assert_eq!(
+            strip_markdown_codeblock(chunks("```\nLorem ipsum dolor", 2))
+                .map(|chunk| chunk.unwrap())
+                .collect::<String>()
+                .await,
+            "Lorem ipsum dolor"
+        );
+        assert_eq!(
+            strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
+                .map(|chunk| chunk.unwrap())
+                .collect::<String>()
+                .await,
+            "Lorem ipsum dolor"
+        );
+        assert_eq!(
+            strip_markdown_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
+                .map(|chunk| chunk.unwrap())
+                .collect::<String>()
+                .await,
+            "Lorem ipsum dolor"
+        );
+        assert_eq!(
+            strip_markdown_codeblock(chunks("```html\n```js\nLorem ipsum dolor\n```\n```", 2))
+                .map(|chunk| chunk.unwrap())
+                .collect::<String>()
+                .await,
+            "```js\nLorem ipsum dolor\n```"
+        );
+        assert_eq!(
+            strip_markdown_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
+                .map(|chunk| chunk.unwrap())
+                .collect::<String>()
+                .await,
+            "``\nLorem ipsum dolor\n```"
+        );
+
+        fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
+            stream::iter(
+                text.chars()
+                    .collect::<Vec<_>>()
+                    .chunks(size)
+                    .map(|chunk| Ok(chunk.iter().collect::<String>()))
+                    .collect::<Vec<_>>(),
+            )
+        }
+    }
+
+    struct TestCompletionProvider {
+        last_completion_tx: Mutex<Option<mpsc::Sender<String>>>,
+    }
+
+    impl TestCompletionProvider {
+        fn new() -> Self {
+            Self {
+                last_completion_tx: Mutex::new(None),
+            }
+        }
+
+        fn send_completion(&self, completion: impl Into<String>) {
+            let mut tx = self.last_completion_tx.lock();
+            tx.as_mut().unwrap().try_send(completion.into()).unwrap();
+        }
+
+        fn finish_completion(&self) {
+            self.last_completion_tx.lock().take().unwrap();
+        }
+    }
+
+    impl CompletionProvider for TestCompletionProvider {
+        fn complete(
+            &self,
+            _prompt: OpenAIRequest,
+        ) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
+            let (tx, rx) = mpsc::channel(1);
+            *self.last_completion_tx.lock() = Some(tx);
+            async move { Ok(rx.map(|rx| Ok(rx)).boxed()) }.boxed()
+        }
+    }
+
+    fn rust_lang() -> Language {
+        Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_indents_query(
+            r#"
+            (call_expression) @indent
+            (field_expression) @indent
+            (_ "(" ")" @end) @indent
+            (_ "{" "}" @end) @indent
+            "#,
+        )
+        .unwrap()
+    }
+}

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

@@ -0,0 +1,404 @@
+use crate::codegen::CodegenKind;
+use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
+use std::cmp::{self, Reverse};
+use std::fmt::Write;
+use std::ops::Range;
+
+fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
+    #[derive(Debug)]
+    struct Match {
+        collapse: Range<usize>,
+        keep: Vec<Range<usize>>,
+    }
+
+    let selected_range = selected_range.to_offset(buffer);
+    let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| {
+        Some(&grammar.embedding_config.as_ref()?.query)
+    });
+    let configs = ts_matches
+        .grammars()
+        .iter()
+        .map(|g| g.embedding_config.as_ref().unwrap())
+        .collect::<Vec<_>>();
+    let mut matches = Vec::new();
+    while let Some(mat) = ts_matches.peek() {
+        let config = &configs[mat.grammar_index];
+        if let Some(collapse) = mat.captures.iter().find_map(|cap| {
+            if Some(cap.index) == config.collapse_capture_ix {
+                Some(cap.node.byte_range())
+            } else {
+                None
+            }
+        }) {
+            let mut keep = Vec::new();
+            for capture in mat.captures.iter() {
+                if Some(capture.index) == config.keep_capture_ix {
+                    keep.push(capture.node.byte_range());
+                } else {
+                    continue;
+                }
+            }
+            ts_matches.advance();
+            matches.push(Match { collapse, keep });
+        } else {
+            ts_matches.advance();
+        }
+    }
+    matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end)));
+    let mut matches = matches.into_iter().peekable();
+
+    let mut summary = String::new();
+    let mut offset = 0;
+    let mut flushed_selection = false;
+    while let Some(mat) = matches.next() {
+        // Keep extending the collapsed range if the next match surrounds
+        // the current one.
+        while let Some(next_mat) = matches.peek() {
+            if mat.collapse.start <= next_mat.collapse.start
+                && mat.collapse.end >= next_mat.collapse.end
+            {
+                matches.next().unwrap();
+            } else {
+                break;
+            }
+        }
+
+        if offset > mat.collapse.start {
+            // Skip collapsed nodes that have already been summarized.
+            offset = cmp::max(offset, mat.collapse.end);
+            continue;
+        }
+
+        if offset <= selected_range.start && selected_range.start <= mat.collapse.end {
+            if !flushed_selection {
+                // The collapsed node ends after the selection starts, so we'll flush the selection first.
+                summary.extend(buffer.text_for_range(offset..selected_range.start));
+                summary.push_str("<|START|");
+                if selected_range.end == selected_range.start {
+                    summary.push_str(">");
+                } else {
+                    summary.extend(buffer.text_for_range(selected_range.clone()));
+                    summary.push_str("|END|>");
+                }
+                offset = selected_range.end;
+                flushed_selection = true;
+            }
+
+            // If the selection intersects the collapsed node, we won't collapse it.
+            if selected_range.end >= mat.collapse.start {
+                continue;
+            }
+        }
+
+        summary.extend(buffer.text_for_range(offset..mat.collapse.start));
+        for keep in mat.keep {
+            summary.extend(buffer.text_for_range(keep));
+        }
+        offset = mat.collapse.end;
+    }
+
+    // Flush selection if we haven't already done so.
+    if !flushed_selection && offset <= selected_range.start {
+        summary.extend(buffer.text_for_range(offset..selected_range.start));
+        summary.push_str("<|START|");
+        if selected_range.end == selected_range.start {
+            summary.push_str(">");
+        } else {
+            summary.extend(buffer.text_for_range(selected_range.clone()));
+            summary.push_str("|END|>");
+        }
+        offset = selected_range.end;
+    }
+
+    summary.extend(buffer.text_for_range(offset..buffer.len()));
+    summary
+}
+
+pub fn generate_content_prompt(
+    user_prompt: String,
+    language_name: Option<&str>,
+    buffer: &BufferSnapshot,
+    range: Range<impl ToOffset>,
+    kind: CodegenKind,
+) -> String {
+    let mut prompt = String::new();
+
+    // General Preamble
+    if let Some(language_name) = language_name {
+        writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap();
+    } else {
+        writeln!(prompt, "You're an expert engineer.\n").unwrap();
+    }
+
+    let outline = summarize(buffer, range);
+    writeln!(
+        prompt,
+        "The file you are currently working on has the following outline:"
+    )
+    .unwrap();
+    if let Some(language_name) = language_name {
+        let language_name = language_name.to_lowercase();
+        writeln!(prompt, "```{language_name}\n{outline}\n```").unwrap();
+    } else {
+        writeln!(prompt, "```\n{outline}\n```").unwrap();
+    }
+
+    match kind {
+        CodegenKind::Generate { position: _ } => {
+            writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap();
+            writeln!(
+                prompt,
+                "Assume the cursor is located where the `<|START|` marker is."
+            )
+            .unwrap();
+            writeln!(
+                prompt,
+                "Text can't be replaced, so assume your answer will be inserted at the cursor."
+            )
+            .unwrap();
+            writeln!(
+                prompt,
+                "Generate text based on the users prompt: {user_prompt}"
+            )
+            .unwrap();
+        }
+        CodegenKind::Transform { range: _ } => {
+            writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap();
+            writeln!(
+                prompt,
+                "Modify the users code selected text based upon the users prompt: {user_prompt}"
+            )
+            .unwrap();
+            writeln!(
+                prompt,
+                "You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file."
+            )
+            .unwrap();
+        }
+    }
+
+    if let Some(language_name) = language_name {
+        writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap();
+    }
+    writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap();
+    writeln!(prompt, "Never make remarks about the output.").unwrap();
+
+    prompt
+}
+
+#[cfg(test)]
+pub(crate) mod tests {
+
+    use super::*;
+    use std::sync::Arc;
+
+    use gpui::AppContext;
+    use indoc::indoc;
+    use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
+    use settings::SettingsStore;
+
+    pub(crate) fn rust_lang() -> Language {
+        Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )
+        .with_embedding_query(
+            r#"
+            (
+                [(line_comment) (attribute_item)]* @context
+                .
+                [
+                    (struct_item
+                        name: (_) @name)
+
+                    (enum_item
+                        name: (_) @name)
+
+                    (impl_item
+                        trait: (_)? @name
+                        "for"? @name
+                        type: (_) @name)
+
+                    (trait_item
+                        name: (_) @name)
+
+                    (function_item
+                        name: (_) @name
+                        body: (block
+                            "{" @keep
+                            "}" @keep) @collapse)
+
+                    (macro_definition
+                        name: (_) @name)
+                    ] @item
+                )
+            "#,
+        )
+        .unwrap()
+    }
+
+    #[gpui::test]
+    fn test_outline_for_prompt(cx: &mut AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        language_settings::init(cx);
+        let text = indoc! {"
+            struct X {
+                a: usize,
+                b: usize,
+            }
+
+            impl X {
+
+                fn new() -> Self {
+                    let a = 1;
+                    let b = 2;
+                    Self { a, b }
+                }
+
+                pub fn a(&self, param: bool) -> usize {
+                    self.a
+                }
+
+                pub fn b(&self) -> usize {
+                    self.b
+                }
+            }
+        "};
+        let buffer =
+            cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
+        let snapshot = buffer.read(cx).snapshot();
+
+        assert_eq!(
+            summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)),
+            indoc! {"
+                struct X {
+                    <|START|>a: usize,
+                    b: usize,
+                }
+
+                impl X {
+
+                    fn new() -> Self {}
+
+                    pub fn a(&self, param: bool) -> usize {}
+
+                    pub fn b(&self) -> usize {}
+                }
+            "}
+        );
+
+        assert_eq!(
+            summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)),
+            indoc! {"
+                struct X {
+                    a: usize,
+                    b: usize,
+                }
+
+                impl X {
+
+                    fn new() -> Self {
+                        let <|START|a |END|>= 1;
+                        let b = 2;
+                        Self { a, b }
+                    }
+
+                    pub fn a(&self, param: bool) -> usize {}
+
+                    pub fn b(&self) -> usize {}
+                }
+            "}
+        );
+
+        assert_eq!(
+            summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)),
+            indoc! {"
+                struct X {
+                    a: usize,
+                    b: usize,
+                }
+
+                impl X {
+                <|START|>
+                    fn new() -> Self {}
+
+                    pub fn a(&self, param: bool) -> usize {}
+
+                    pub fn b(&self) -> usize {}
+                }
+            "}
+        );
+
+        assert_eq!(
+            summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)),
+            indoc! {"
+                struct X {
+                    a: usize,
+                    b: usize,
+                }
+
+                impl X {
+
+                    fn new() -> Self {}
+
+                    pub fn a(&self, param: bool) -> usize {}
+
+                    pub fn b(&self) -> usize {}
+                }
+                <|START|>"}
+        );
+
+        // Ensure nested functions get collapsed properly.
+        let text = indoc! {"
+            struct X {
+                a: usize,
+                b: usize,
+            }
+
+            impl X {
+
+                fn new() -> Self {
+                    let a = 1;
+                    let b = 2;
+                    Self { a, b }
+                }
+
+                pub fn a(&self, param: bool) -> usize {
+                    let a = 30;
+                    fn nested() -> usize {
+                        3
+                    }
+                    self.a + nested()
+                }
+
+                pub fn b(&self) -> usize {
+                    self.b
+                }
+            }
+        "};
+        buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
+        let snapshot = buffer.read(cx).snapshot();
+        assert_eq!(
+            summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)),
+            indoc! {"
+                <|START|>struct X {
+                    a: usize,
+                    b: usize,
+                }
+
+                impl X {
+
+                    fn new() -> Self {}
+
+                    pub fn a(&self, param: bool) -> usize {}
+
+                    pub fn b(&self) -> usize {}
+                }
+            "}
+        );
+    }
+}

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

@@ -115,13 +115,15 @@ pub fn check(_: &Check, cx: &mut AppContext) {
 
 fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
     if let Some(auto_updater) = AutoUpdater::get(cx) {
-        let server_url = &auto_updater.read(cx).server_url;
+        let auto_updater = auto_updater.read(cx);
+        let server_url = &auto_updater.server_url;
+        let current_version = auto_updater.current_version;
         let latest_release_url = if cx.has_global::<ReleaseChannel>()
             && *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
         {
-            format!("{server_url}/releases/preview/latest")
+            format!("{server_url}/releases/preview/{current_version}")
         } else {
-            format!("{server_url}/releases/stable/latest")
+            format!("{server_url}/releases/stable/{current_version}")
         };
         cx.platform().open_url(&latest_release_url);
     }

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

@@ -50,7 +50,7 @@ impl View for UpdateNotification {
                         .with_child(
                             MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
                                 let style = theme.dismiss_button.style_for(state);
-                                Svg::new("icons/x_mark_8.svg")
+                                Svg::new("icons/x.svg")
                                     .with_color(style.color)
                                     .constrained()
                                     .with_width(style.icon_width)

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

@@ -2,22 +2,23 @@ pub mod call_settings;
 pub mod participant;
 pub mod room;
 
-use std::sync::Arc;
-
 use anyhow::{anyhow, Result};
 use audio::Audio;
 use call_settings::CallSettings;
 use channel::ChannelId;
-use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
+use client::{
+    proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
+    ZED_ALWAYS_ACTIVE,
+};
 use collections::HashSet;
 use futures::{future::Shared, FutureExt};
-use postage::watch;
-
 use gpui::{
     AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
     WeakModelHandle,
 };
+use postage::watch;
 use project::Project;
+use std::sync::Arc;
 
 pub use participant::ParticipantLocation;
 pub use room::Room;
@@ -68,6 +69,7 @@ impl ActiveCall {
             location: None,
             pending_invites: Default::default(),
             incoming_call: watch::channel(),
+
             _subscriptions: vec![
                 client.add_request_handler(cx.handle(), Self::handle_incoming_call),
                 client.add_message_handler(cx.handle(), Self::handle_call_canceled),
@@ -206,9 +208,14 @@ impl ActiveCall {
 
         cx.spawn(|this, mut cx| async move {
             let result = invite.await;
+            if result.is_ok() {
+                this.update(&mut cx, |this, cx| this.report_call_event("invite", cx));
+            } else {
+                // TODO: Resport collaboration error
+            }
+
             this.update(&mut cx, |this, cx| {
                 this.pending_invites.remove(&called_user_id);
-                this.report_call_event("invite", cx);
                 cx.notify();
             });
             result
@@ -273,13 +280,7 @@ impl ActiveCall {
             .borrow_mut()
             .take()
             .ok_or_else(|| anyhow!("no incoming call"))?;
-        Self::report_call_event_for_room(
-            "decline incoming",
-            Some(call.room_id),
-            None,
-            &self.client,
-            cx,
-        );
+        report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx);
         self.client.send(proto::DeclineCall {
             room_id: call.room_id,
         })?;
@@ -290,10 +291,10 @@ impl ActiveCall {
         &mut self,
         channel_id: u64,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<()>> {
+    ) -> Task<Result<ModelHandle<Room>>> {
         if let Some(room) = self.room().cloned() {
             if room.read(cx).channel_id() == Some(channel_id) {
-                return Task::ready(Ok(()));
+                return Task::ready(Ok(room));
             } else {
                 room.update(cx, |room, cx| room.clear_state(cx));
             }
@@ -308,7 +309,7 @@ impl ActiveCall {
             this.update(&mut cx, |this, cx| {
                 this.report_call_event("join channel", cx)
             });
-            Ok(())
+            Ok(room)
         })
     }
 
@@ -349,17 +350,22 @@ impl ActiveCall {
         }
     }
 
+    pub fn location(&self) -> Option<&WeakModelHandle<Project>> {
+        self.location.as_ref()
+    }
+
     pub fn set_location(
         &mut self,
         project: Option<&ModelHandle<Project>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
-        self.location = project.map(|project| project.downgrade());
-        if let Some((room, _)) = self.room.as_ref() {
-            room.update(cx, |room, cx| room.set_location(project, cx))
-        } else {
-            Task::ready(Ok(()))
+        if project.is_some() || !*ZED_ALWAYS_ACTIVE {
+            self.location = project.map(|project| project.downgrade());
+            if let Some((room, _)) = self.room.as_ref() {
+                return room.update(cx, |room, cx| room.set_location(project, cx));
+            }
         }
+        Task::ready(Ok(()))
     }
 
     fn set_room(
@@ -409,31 +415,46 @@ impl ActiveCall {
         &self.pending_invites
     }
 
-    fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
-        let (room_id, channel_id) = match self.room() {
-            Some(room) => {
-                let room = room.read(cx);
-                (Some(room.id()), room.channel_id())
-            }
-            None => (None, None),
-        };
-        Self::report_call_event_for_room(operation, room_id, channel_id, &self.client, cx)
+    pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
+        if let Some(room) = self.room() {
+            let room = room.read(cx);
+            report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
+        }
     }
+}
 
-    pub fn report_call_event_for_room(
-        operation: &'static str,
-        room_id: Option<u64>,
-        channel_id: Option<u64>,
-        client: &Arc<Client>,
-        cx: &AppContext,
-    ) {
-        let telemetry = client.telemetry();
-        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
-        let event = ClickhouseEvent::Call {
-            operation,
-            room_id,
-            channel_id,
-        };
-        telemetry.report_clickhouse_event(event, telemetry_settings);
-    }
+pub fn report_call_event_for_room(
+    operation: &'static str,
+    room_id: u64,
+    channel_id: Option<u64>,
+    client: &Arc<Client>,
+    cx: &AppContext,
+) {
+    let telemetry = client.telemetry();
+    let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+    let event = ClickhouseEvent::Call {
+        operation,
+        room_id: Some(room_id),
+        channel_id,
+    };
+    telemetry.report_clickhouse_event(event, telemetry_settings);
+}
+
+pub fn report_call_event_for_channel(
+    operation: &'static str,
+    channel_id: u64,
+    client: &Arc<Client>,
+    cx: &AppContext,
+) {
+    let room = ActiveCall::global(cx).read(cx).room();
+
+    let telemetry = client.telemetry();
+    let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+
+    let event = ClickhouseEvent::Call {
+        operation,
+        room_id: room.map(|r| r.read(cx).id()),
+        channel_id: Some(channel_id),
+    };
+    telemetry.report_clickhouse_event(event, telemetry_settings);
 }

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

@@ -1,4 +1,5 @@
 use anyhow::{anyhow, Result};
+use client::ParticipantIndex;
 use client::{proto, User};
 use collections::HashMap;
 use gpui::WeakModelHandle;
@@ -43,6 +44,7 @@ pub struct RemoteParticipant {
     pub peer_id: proto::PeerId,
     pub projects: Vec<proto::ParticipantProject>,
     pub location: ParticipantLocation,
+    pub participant_index: ParticipantIndex,
     pub muted: bool,
     pub speaking: bool,
     pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,

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

@@ -7,7 +7,7 @@ use anyhow::{anyhow, Result};
 use audio::{Audio, Sound};
 use client::{
     proto::{self, PeerId},
-    Client, TypedEnvelope, User, UserStore,
+    Client, ParticipantIndex, TypedEnvelope, User, UserStore,
 };
 use collections::{BTreeMap, HashMap, HashSet};
 use fs::Fs;
@@ -44,6 +44,12 @@ pub enum Event {
     RemoteProjectUnshared {
         project_id: u64,
     },
+    RemoteProjectJoined {
+        project_id: u64,
+    },
+    RemoteProjectInvitationDiscarded {
+        project_id: u64,
+    },
     Left,
 }
 
@@ -98,6 +104,10 @@ impl Room {
         self.channel_id
     }
 
+    pub fn is_sharing_project(&self) -> bool {
+        !self.shared_projects.is_empty()
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn is_connected(&self) -> bool {
         if let Some(live_kit) = self.live_kit.as_ref() {
@@ -172,7 +182,7 @@ impl Room {
             cx.spawn(|this, mut cx| async move {
                 connect.await?;
 
-                if !cx.read(|cx| settings::get::<CallSettings>(cx).mute_on_join) {
+                if !cx.read(Self::mute_on_join) {
                     this.update(&mut cx, |this, cx| this.share_microphone(cx))
                         .await?;
                 }
@@ -301,6 +311,10 @@ impl Room {
         })
     }
 
+    pub fn mute_on_join(cx: &AppContext) -> bool {
+        settings::get::<CallSettings>(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
+    }
+
     fn from_join_response(
         response: proto::JoinRoomResponse,
         client: Arc<Client>,
@@ -584,6 +598,31 @@ impl Room {
             .map_or(&[], |v| v.as_slice())
     }
 
+    /// Returns the most 'active' projects, defined as most people in the project
+    pub fn most_active_project(&self) -> Option<(u64, u64)> {
+        let mut projects = HashMap::default();
+        let mut hosts = HashMap::default();
+        for participant in self.remote_participants.values() {
+            match participant.location {
+                ParticipantLocation::SharedProject { project_id } => {
+                    *projects.entry(project_id).or_insert(0) += 1;
+                }
+                ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
+            }
+            for project in &participant.projects {
+                *projects.entry(project.id).or_insert(0) += 1;
+                hosts.insert(project.id, participant.user.id);
+            }
+        }
+
+        let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect();
+        pairs.sort_by_key(|(_, count)| *count as i32);
+
+        pairs
+            .first()
+            .map(|(project_id, _)| (*project_id, hosts[&project_id]))
+    }
+
     async fn handle_room_updated(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<proto::RoomUpdated>,
@@ -710,6 +749,9 @@ impl Room {
                                 participant.user_id,
                                 RemoteParticipant {
                                     user: user.clone(),
+                                    participant_index: ParticipantIndex(
+                                        participant.participant_index,
+                                    ),
                                     peer_id,
                                     projects: participant.projects,
                                     location,
@@ -803,6 +845,15 @@ impl Room {
                     let _ = this.leave(cx);
                 }
 
+                this.user_store.update(cx, |user_store, cx| {
+                    let participant_indices_by_user_id = this
+                        .remote_participants
+                        .iter()
+                        .map(|(user_id, participant)| (*user_id, participant.participant_index))
+                        .collect();
+                    user_store.set_participant_indices(participant_indices_by_user_id, cx);
+                });
+
                 this.check_invariants();
                 cx.notify();
             });
@@ -999,6 +1050,7 @@ impl Room {
     ) -> Task<Result<ModelHandle<Project>>> {
         let client = self.client.clone();
         let user_store = self.user_store.clone();
+        cx.emit(Event::RemoteProjectJoined { project_id: id });
         cx.spawn(|this, mut cx| async move {
             let project =
                 Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
@@ -1124,7 +1176,7 @@ impl Room {
         self.live_kit
             .as_ref()
             .and_then(|live_kit| match &live_kit.microphone_track {
-                LocalTrack::None => Some(settings::get::<CallSettings>(cx).mute_on_join),
+                LocalTrack::None => Some(Self::mute_on_join(cx)),
                 LocalTrack::Pending { muted, .. } => Some(*muted),
                 LocalTrack::Published { muted, .. } => Some(*muted),
             })

crates/channel/Cargo.toml πŸ”—

@@ -23,11 +23,13 @@ language = { path = "../language" }
 settings = { path = "../settings" }
 feature_flags = { path = "../feature_flags" }
 sum_tree = { path = "../sum_tree" }
+clock = { path = "../clock" }
 
 anyhow.workspace = true
 futures.workspace = true
 image = "0.23"
 lazy_static.workspace = true
+smallvec.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
@@ -37,7 +39,7 @@ smol.workspace = true
 thiserror.workspace = true
 time.workspace = true
 tiny_http = "0.8"
-uuid = { version = "1.1.2", features = ["v4"] }
+uuid.workspace = true
 url = "2.2"
 serde.workspace = true
 serde_derive.workspace = true
@@ -47,5 +49,6 @@ tempfile = "3"
 collections = { path = "../collections", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
+client = { path = "../client", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }

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

@@ -1,14 +1,20 @@
+mod channel_buffer;
+mod channel_chat;
 mod channel_store;
 
-pub mod channel_buffer;
-use std::sync::Arc;
+pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
+pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
+pub use channel_store::{
+    Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
+};
 
-pub use channel_store::*;
 use client::Client;
+use std::sync::Arc;
 
 #[cfg(test)]
 mod channel_store_tests;
 
 pub fn init(client: &Arc<Client>) {
     channel_buffer::init(client);
+    channel_chat::init(client);
 }

crates/channel/src/channel_buffer.rs πŸ”—

@@ -1,38 +1,49 @@
 use crate::Channel;
 use anyhow::Result;
-use client::Client;
-use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle};
-use rpc::{proto, TypedEnvelope};
-use std::sync::Arc;
+use client::{Client, Collaborator, UserStore};
+use collections::HashMap;
+use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
+use language::proto::serialize_version;
+use rpc::{
+    proto::{self, PeerId},
+    TypedEnvelope,
+};
+use std::{sync::Arc, time::Duration};
 use util::ResultExt;
 
+pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250);
+
 pub(crate) fn init(client: &Arc<Client>) {
     client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
-    client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator);
-    client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator);
-    client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator);
+    client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
 }
 
 pub struct ChannelBuffer {
     pub(crate) channel: Arc<Channel>,
     connected: bool,
-    collaborators: Vec<proto::Collaborator>,
+    collaborators: HashMap<PeerId, Collaborator>,
+    user_store: ModelHandle<UserStore>,
     buffer: ModelHandle<language::Buffer>,
     buffer_epoch: u64,
     client: Arc<Client>,
     subscription: Option<client::Subscription>,
+    acknowledge_task: Option<Task<Result<()>>>,
 }
 
-pub enum Event {
+pub enum ChannelBufferEvent {
     CollaboratorsChanged,
     Disconnected,
+    BufferEdited,
 }
 
 impl Entity for ChannelBuffer {
-    type Event = Event;
+    type Event = ChannelBufferEvent;
 
     fn release(&mut self, _: &mut AppContext) {
         if self.connected {
+            if let Some(task) = self.acknowledge_task.take() {
+                task.detach();
+            }
             self.client
                 .send(proto::LeaveChannelBuffer {
                     channel_id: self.channel.id,
@@ -46,6 +57,7 @@ impl ChannelBuffer {
     pub(crate) async fn new(
         channel: Arc<Channel>,
         client: Arc<Client>,
+        user_store: ModelHandle<UserStore>,
         mut cx: AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
         let response = client
@@ -61,8 +73,6 @@ impl ChannelBuffer {
             .map(language::proto::deserialize_operation)
             .collect::<Result<Vec<_>, _>>()?;
 
-        let collaborators = response.collaborators;
-
         let buffer = cx.add_model(|_| {
             language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
         });
@@ -73,35 +83,47 @@ impl ChannelBuffer {
         anyhow::Ok(cx.add_model(|cx| {
             cx.subscribe(&buffer, Self::on_buffer_update).detach();
 
-            Self {
+            let mut this = Self {
                 buffer,
                 buffer_epoch: response.epoch,
                 client,
                 connected: true,
-                collaborators,
+                collaborators: Default::default(),
+                acknowledge_task: None,
                 channel,
                 subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
-            }
+                user_store,
+            };
+            this.replace_collaborators(response.collaborators, cx);
+            this
         }))
     }
 
+    pub fn user_store(&self) -> &ModelHandle<UserStore> {
+        &self.user_store
+    }
+
     pub(crate) fn replace_collaborators(
         &mut self,
         collaborators: Vec<proto::Collaborator>,
         cx: &mut ModelContext<Self>,
     ) {
-        for old_collaborator in &self.collaborators {
-            if collaborators
-                .iter()
-                .any(|c| c.replica_id == old_collaborator.replica_id)
-            {
+        let mut new_collaborators = HashMap::default();
+        for collaborator in collaborators {
+            if let Ok(collaborator) = Collaborator::from_proto(collaborator) {
+                new_collaborators.insert(collaborator.peer_id, collaborator);
+            }
+        }
+
+        for (_, old_collaborator) in &self.collaborators {
+            if !new_collaborators.contains_key(&old_collaborator.peer_id) {
                 self.buffer.update(cx, |buffer, cx| {
                     buffer.remove_peer(old_collaborator.replica_id as u16, cx)
                 });
             }
         }
-        self.collaborators = collaborators;
-        cx.emit(Event::CollaboratorsChanged);
+        self.collaborators = new_collaborators;
+        cx.emit(ChannelBufferEvent::CollaboratorsChanged);
         cx.notify();
     }
 
@@ -127,65 +149,15 @@ impl ChannelBuffer {
         Ok(())
     }
 
-    async fn handle_add_channel_buffer_collaborator(
-        this: ModelHandle<Self>,
-        envelope: TypedEnvelope<proto::AddChannelBufferCollaborator>,
-        _: Arc<Client>,
-        mut cx: AsyncAppContext,
-    ) -> Result<()> {
-        let collaborator = envelope.payload.collaborator.ok_or_else(|| {
-            anyhow::anyhow!(
-                "Should have gotten a collaborator in the AddChannelBufferCollaborator message"
-            )
-        })?;
-
-        this.update(&mut cx, |this, cx| {
-            this.collaborators.push(collaborator);
-            cx.emit(Event::CollaboratorsChanged);
-            cx.notify();
-        });
-
-        Ok(())
-    }
-
-    async fn handle_remove_channel_buffer_collaborator(
+    async fn handle_update_channel_buffer_collaborators(
         this: ModelHandle<Self>,
-        message: TypedEnvelope<proto::RemoveChannelBufferCollaborator>,
+        message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
     ) -> Result<()> {
         this.update(&mut cx, |this, cx| {
-            this.collaborators.retain(|collaborator| {
-                if collaborator.peer_id == message.payload.peer_id {
-                    this.buffer.update(cx, |buffer, cx| {
-                        buffer.remove_peer(collaborator.replica_id as u16, cx)
-                    });
-                    false
-                } else {
-                    true
-                }
-            });
-            cx.emit(Event::CollaboratorsChanged);
-            cx.notify();
-        });
-
-        Ok(())
-    }
-
-    async fn handle_update_channel_buffer_collaborator(
-        this: ModelHandle<Self>,
-        message: TypedEnvelope<proto::UpdateChannelBufferCollaborator>,
-        _: Arc<Client>,
-        mut cx: AsyncAppContext,
-    ) -> Result<()> {
-        this.update(&mut cx, |this, cx| {
-            for collaborator in &mut this.collaborators {
-                if collaborator.peer_id == message.payload.old_peer_id {
-                    collaborator.peer_id = message.payload.new_peer_id;
-                    break;
-                }
-            }
-            cx.emit(Event::CollaboratorsChanged);
+            this.replace_collaborators(message.payload.collaborators, cx);
+            cx.emit(ChannelBufferEvent::CollaboratorsChanged);
             cx.notify();
         });
 
@@ -196,19 +168,45 @@ impl ChannelBuffer {
         &mut self,
         _: ModelHandle<language::Buffer>,
         event: &language::Event,
-        _: &mut ModelContext<Self>,
+        cx: &mut ModelContext<Self>,
     ) {
-        if let language::Event::Operation(operation) = event {
-            let operation = language::proto::serialize_operation(operation);
-            self.client
-                .send(proto::UpdateChannelBuffer {
-                    channel_id: self.channel.id,
-                    operations: vec![operation],
-                })
-                .log_err();
+        match event {
+            language::Event::Operation(operation) => {
+                let operation = language::proto::serialize_operation(operation);
+                self.client
+                    .send(proto::UpdateChannelBuffer {
+                        channel_id: self.channel.id,
+                        operations: vec![operation],
+                    })
+                    .log_err();
+            }
+            language::Event::Edited => {
+                cx.emit(ChannelBufferEvent::BufferEdited);
+            }
+            _ => {}
         }
     }
 
+    pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) {
+        let buffer = self.buffer.read(cx);
+        let version = buffer.version();
+        let buffer_id = buffer.remote_id();
+        let client = self.client.clone();
+        let epoch = self.epoch();
+
+        self.acknowledge_task = Some(cx.spawn_weak(|_, cx| async move {
+            cx.background().timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL).await;
+            client
+                .send(proto::AckBufferOperation {
+                    buffer_id,
+                    epoch,
+                    version: serialize_version(&version),
+                })
+                .ok();
+            Ok(())
+        }));
+    }
+
     pub fn epoch(&self) -> u64 {
         self.buffer_epoch
     }
@@ -217,7 +215,7 @@ impl ChannelBuffer {
         self.buffer.clone()
     }
 
-    pub fn collaborators(&self) -> &[proto::Collaborator] {
+    pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
         &self.collaborators
     }
 
@@ -230,7 +228,7 @@ impl ChannelBuffer {
         if self.connected {
             self.connected = false;
             self.subscription.take();
-            cx.emit(Event::Disconnected);
+            cx.emit(ChannelBufferEvent::Disconnected);
             cx.notify()
         }
     }

crates/channel/src/channel_chat.rs πŸ”—

@@ -0,0 +1,540 @@
+use crate::{Channel, ChannelId, ChannelStore};
+use anyhow::{anyhow, Result};
+use client::{
+    proto,
+    user::{User, UserStore},
+    Client, Subscription, TypedEnvelope,
+};
+use futures::lock::Mutex;
+use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
+use rand::prelude::*;
+use std::{collections::HashSet, mem, ops::Range, sync::Arc};
+use sum_tree::{Bias, SumTree};
+use time::OffsetDateTime;
+use util::{post_inc, ResultExt as _, TryFutureExt};
+
+pub struct ChannelChat {
+    channel: Arc<Channel>,
+    messages: SumTree<ChannelMessage>,
+    channel_store: ModelHandle<ChannelStore>,
+    loaded_all_messages: bool,
+    last_acknowledged_id: Option<u64>,
+    next_pending_message_id: usize,
+    user_store: ModelHandle<UserStore>,
+    rpc: Arc<Client>,
+    outgoing_messages_lock: Arc<Mutex<()>>,
+    rng: StdRng,
+    _subscription: Subscription,
+}
+
+#[derive(Clone, Debug)]
+pub struct ChannelMessage {
+    pub id: ChannelMessageId,
+    pub body: String,
+    pub timestamp: OffsetDateTime,
+    pub sender: Arc<User>,
+    pub nonce: u128,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum ChannelMessageId {
+    Saved(u64),
+    Pending(usize),
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct ChannelMessageSummary {
+    max_id: ChannelMessageId,
+    count: usize,
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
+struct Count(usize);
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum ChannelChatEvent {
+    MessagesUpdated {
+        old_range: Range<usize>,
+        new_count: usize,
+    },
+    NewMessage {
+        channel_id: ChannelId,
+        message_id: u64,
+    },
+}
+
+pub fn init(client: &Arc<Client>) {
+    client.add_model_message_handler(ChannelChat::handle_message_sent);
+    client.add_model_message_handler(ChannelChat::handle_message_removed);
+}
+
+impl Entity for ChannelChat {
+    type Event = ChannelChatEvent;
+
+    fn release(&mut self, _: &mut AppContext) {
+        self.rpc
+            .send(proto::LeaveChannelChat {
+                channel_id: self.channel.id,
+            })
+            .log_err();
+    }
+}
+
+impl ChannelChat {
+    pub async fn new(
+        channel: Arc<Channel>,
+        channel_store: ModelHandle<ChannelStore>,
+        user_store: ModelHandle<UserStore>,
+        client: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<ModelHandle<Self>> {
+        let channel_id = channel.id;
+        let subscription = client.subscribe_to_entity(channel_id).unwrap();
+
+        let response = client
+            .request(proto::JoinChannelChat { channel_id })
+            .await?;
+        let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
+        let loaded_all_messages = response.done;
+
+        Ok(cx.add_model(|cx| {
+            let mut this = Self {
+                channel,
+                user_store,
+                channel_store,
+                rpc: client,
+                outgoing_messages_lock: Default::default(),
+                messages: Default::default(),
+                loaded_all_messages,
+                next_pending_message_id: 0,
+                last_acknowledged_id: None,
+                rng: StdRng::from_entropy(),
+                _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
+            };
+            this.insert_messages(messages, cx);
+            this
+        }))
+    }
+
+    pub fn channel(&self) -> &Arc<Channel> {
+        &self.channel
+    }
+
+    pub fn send_message(
+        &mut self,
+        body: String,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<Task<Result<()>>> {
+        if body.is_empty() {
+            Err(anyhow!("message body can't be empty"))?;
+        }
+
+        let current_user = self
+            .user_store
+            .read(cx)
+            .current_user()
+            .ok_or_else(|| anyhow!("current_user is not present"))?;
+
+        let channel_id = self.channel.id;
+        let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
+        let nonce = self.rng.gen();
+        self.insert_messages(
+            SumTree::from_item(
+                ChannelMessage {
+                    id: pending_id,
+                    body: body.clone(),
+                    sender: current_user,
+                    timestamp: OffsetDateTime::now_utc(),
+                    nonce,
+                },
+                &(),
+            ),
+            cx,
+        );
+        let user_store = self.user_store.clone();
+        let rpc = self.rpc.clone();
+        let outgoing_messages_lock = self.outgoing_messages_lock.clone();
+        Ok(cx.spawn(|this, mut cx| async move {
+            let outgoing_message_guard = outgoing_messages_lock.lock().await;
+            let request = rpc.request(proto::SendChannelMessage {
+                channel_id,
+                body,
+                nonce: Some(nonce.into()),
+            });
+            let response = request.await?;
+            drop(outgoing_message_guard);
+            let message = ChannelMessage::from_proto(
+                response.message.ok_or_else(|| anyhow!("invalid message"))?,
+                &user_store,
+                &mut cx,
+            )
+            .await?;
+            this.update(&mut cx, |this, cx| {
+                this.insert_messages(SumTree::from_item(message, &()), cx);
+                Ok(())
+            })
+        }))
+    }
+
+    pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
+        let response = self.rpc.request(proto::RemoveChannelMessage {
+            channel_id: self.channel.id,
+            message_id: id,
+        });
+        cx.spawn(|this, mut cx| async move {
+            response.await?;
+
+            this.update(&mut cx, |this, cx| {
+                this.message_removed(id, cx);
+                Ok(())
+            })
+        })
+    }
+
+    pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> bool {
+        if !self.loaded_all_messages {
+            let rpc = self.rpc.clone();
+            let user_store = self.user_store.clone();
+            let channel_id = self.channel.id;
+            if let Some(before_message_id) =
+                self.messages.first().and_then(|message| match message.id {
+                    ChannelMessageId::Saved(id) => Some(id),
+                    ChannelMessageId::Pending(_) => None,
+                })
+            {
+                cx.spawn(|this, mut cx| {
+                    async move {
+                        let response = rpc
+                            .request(proto::GetChannelMessages {
+                                channel_id,
+                                before_message_id,
+                            })
+                            .await?;
+                        let loaded_all_messages = response.done;
+                        let messages =
+                            messages_from_proto(response.messages, &user_store, &mut cx).await?;
+                        this.update(&mut cx, |this, cx| {
+                            this.loaded_all_messages = loaded_all_messages;
+                            this.insert_messages(messages, cx);
+                        });
+                        anyhow::Ok(())
+                    }
+                    .log_err()
+                })
+                .detach();
+                return true;
+            }
+        }
+        false
+    }
+
+    pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
+        if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id {
+            if self
+                .last_acknowledged_id
+                .map_or(true, |acknowledged_id| acknowledged_id < latest_message_id)
+            {
+                self.rpc
+                    .send(proto::AckChannelMessage {
+                        channel_id: self.channel.id,
+                        message_id: latest_message_id,
+                    })
+                    .ok();
+                self.last_acknowledged_id = Some(latest_message_id);
+                self.channel_store.update(cx, |store, cx| {
+                    store.acknowledge_message_id(self.channel.id, latest_message_id, cx);
+                });
+            }
+        }
+    }
+
+    pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
+        let user_store = self.user_store.clone();
+        let rpc = self.rpc.clone();
+        let channel_id = self.channel.id;
+        cx.spawn(|this, mut cx| {
+            async move {
+                let response = rpc.request(proto::JoinChannelChat { channel_id }).await?;
+                let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
+                let loaded_all_messages = response.done;
+
+                let pending_messages = this.update(&mut cx, |this, cx| {
+                    if let Some((first_new_message, last_old_message)) =
+                        messages.first().zip(this.messages.last())
+                    {
+                        if first_new_message.id > last_old_message.id {
+                            let old_messages = mem::take(&mut this.messages);
+                            cx.emit(ChannelChatEvent::MessagesUpdated {
+                                old_range: 0..old_messages.summary().count,
+                                new_count: 0,
+                            });
+                            this.loaded_all_messages = loaded_all_messages;
+                        }
+                    }
+
+                    this.insert_messages(messages, cx);
+                    if loaded_all_messages {
+                        this.loaded_all_messages = loaded_all_messages;
+                    }
+
+                    this.pending_messages().cloned().collect::<Vec<_>>()
+                });
+
+                for pending_message in pending_messages {
+                    let request = rpc.request(proto::SendChannelMessage {
+                        channel_id,
+                        body: pending_message.body,
+                        nonce: Some(pending_message.nonce.into()),
+                    });
+                    let response = request.await?;
+                    let message = ChannelMessage::from_proto(
+                        response.message.ok_or_else(|| anyhow!("invalid message"))?,
+                        &user_store,
+                        &mut cx,
+                    )
+                    .await?;
+                    this.update(&mut cx, |this, cx| {
+                        this.insert_messages(SumTree::from_item(message, &()), cx);
+                    });
+                }
+
+                anyhow::Ok(())
+            }
+            .log_err()
+        })
+        .detach();
+    }
+
+    pub fn message_count(&self) -> usize {
+        self.messages.summary().count
+    }
+
+    pub fn messages(&self) -> &SumTree<ChannelMessage> {
+        &self.messages
+    }
+
+    pub fn message(&self, ix: usize) -> &ChannelMessage {
+        let mut cursor = self.messages.cursor::<Count>();
+        cursor.seek(&Count(ix), Bias::Right, &());
+        cursor.item().unwrap()
+    }
+
+    pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
+        let mut cursor = self.messages.cursor::<Count>();
+        cursor.seek(&Count(range.start), Bias::Right, &());
+        cursor.take(range.len())
+    }
+
+    pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
+        let mut cursor = self.messages.cursor::<ChannelMessageId>();
+        cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
+        cursor
+    }
+
+    async fn handle_message_sent(
+        this: ModelHandle<Self>,
+        message: TypedEnvelope<proto::ChannelMessageSent>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
+        let message = message
+            .payload
+            .message
+            .ok_or_else(|| anyhow!("empty message"))?;
+        let message_id = message.id;
+
+        let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
+        this.update(&mut cx, |this, cx| {
+            this.insert_messages(SumTree::from_item(message, &()), cx);
+            cx.emit(ChannelChatEvent::NewMessage {
+                channel_id: this.channel.id,
+                message_id,
+            })
+        });
+
+        Ok(())
+    }
+
+    async fn handle_message_removed(
+        this: ModelHandle<Self>,
+        message: TypedEnvelope<proto::RemoveChannelMessage>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            this.message_removed(message.payload.message_id, cx)
+        });
+        Ok(())
+    }
+
+    fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
+        if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
+            let nonces = messages
+                .cursor::<()>()
+                .map(|m| m.nonce)
+                .collect::<HashSet<_>>();
+
+            let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>();
+            let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &());
+            let start_ix = old_cursor.start().1 .0;
+            let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &());
+            let removed_count = removed_messages.summary().count;
+            let new_count = messages.summary().count;
+            let end_ix = start_ix + removed_count;
+
+            new_messages.append(messages, &());
+
+            let mut ranges = Vec::<Range<usize>>::new();
+            if new_messages.last().unwrap().is_pending() {
+                new_messages.append(old_cursor.suffix(&()), &());
+            } else {
+                new_messages.append(
+                    old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()),
+                    &(),
+                );
+
+                while let Some(message) = old_cursor.item() {
+                    let message_ix = old_cursor.start().1 .0;
+                    if nonces.contains(&message.nonce) {
+                        if ranges.last().map_or(false, |r| r.end == message_ix) {
+                            ranges.last_mut().unwrap().end += 1;
+                        } else {
+                            ranges.push(message_ix..message_ix + 1);
+                        }
+                    } else {
+                        new_messages.push(message.clone(), &());
+                    }
+                    old_cursor.next(&());
+                }
+            }
+
+            drop(old_cursor);
+            self.messages = new_messages;
+
+            for range in ranges.into_iter().rev() {
+                cx.emit(ChannelChatEvent::MessagesUpdated {
+                    old_range: range,
+                    new_count: 0,
+                });
+            }
+            cx.emit(ChannelChatEvent::MessagesUpdated {
+                old_range: start_ix..end_ix,
+                new_count,
+            });
+
+            cx.notify();
+        }
+    }
+
+    fn message_removed(&mut self, id: u64, cx: &mut ModelContext<Self>) {
+        let mut cursor = self.messages.cursor::<ChannelMessageId>();
+        let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &());
+        if let Some(item) = cursor.item() {
+            if item.id == ChannelMessageId::Saved(id) {
+                let ix = messages.summary().count;
+                cursor.next(&());
+                messages.append(cursor.suffix(&()), &());
+                drop(cursor);
+                self.messages = messages;
+                cx.emit(ChannelChatEvent::MessagesUpdated {
+                    old_range: ix..ix + 1,
+                    new_count: 0,
+                });
+            }
+        }
+    }
+}
+
+async fn messages_from_proto(
+    proto_messages: Vec<proto::ChannelMessage>,
+    user_store: &ModelHandle<UserStore>,
+    cx: &mut AsyncAppContext,
+) -> Result<SumTree<ChannelMessage>> {
+    let unique_user_ids = proto_messages
+        .iter()
+        .map(|m| m.sender_id)
+        .collect::<HashSet<_>>()
+        .into_iter()
+        .collect();
+    user_store
+        .update(cx, |user_store, cx| {
+            user_store.get_users(unique_user_ids, cx)
+        })
+        .await?;
+
+    let mut messages = Vec::with_capacity(proto_messages.len());
+    for message in proto_messages {
+        messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
+    }
+    let mut result = SumTree::new();
+    result.extend(messages, &());
+    Ok(result)
+}
+
+impl ChannelMessage {
+    pub async fn from_proto(
+        message: proto::ChannelMessage,
+        user_store: &ModelHandle<UserStore>,
+        cx: &mut AsyncAppContext,
+    ) -> Result<Self> {
+        let sender = user_store
+            .update(cx, |user_store, cx| {
+                user_store.get_user(message.sender_id, cx)
+            })
+            .await?;
+        Ok(ChannelMessage {
+            id: ChannelMessageId::Saved(message.id),
+            body: message.body,
+            timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
+            sender,
+            nonce: message
+                .nonce
+                .ok_or_else(|| anyhow!("nonce is required"))?
+                .into(),
+        })
+    }
+
+    pub fn is_pending(&self) -> bool {
+        matches!(self.id, ChannelMessageId::Pending(_))
+    }
+}
+
+impl sum_tree::Item for ChannelMessage {
+    type Summary = ChannelMessageSummary;
+
+    fn summary(&self) -> Self::Summary {
+        ChannelMessageSummary {
+            max_id: self.id,
+            count: 1,
+        }
+    }
+}
+
+impl Default for ChannelMessageId {
+    fn default() -> Self {
+        Self::Saved(0)
+    }
+}
+
+impl sum_tree::Summary for ChannelMessageSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, summary: &Self, _: &()) {
+        self.max_id = summary.max_id;
+        self.count += summary.count;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId {
+    fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
+        debug_assert!(summary.max_id > *self);
+        *self = summary.max_id;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
+    fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
+        self.0 += summary.count;
+    }
+}

crates/channel/src/channel_store.rs πŸ”—

@@ -1,26 +1,34 @@
-use crate::channel_buffer::ChannelBuffer;
+mod channel_index;
+
+use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
 use anyhow::{anyhow, Result};
 use client::{Client, Subscription, User, UserId, UserStore};
 use collections::{hash_map, HashMap, HashSet};
 use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
-use rpc::{proto, TypedEnvelope};
-use std::{mem, sync::Arc, time::Duration};
+use rpc::{
+    proto::{self, ChannelEdge, ChannelPermission},
+    TypedEnvelope,
+};
+use serde_derive::{Deserialize, Serialize};
+use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration};
 use util::ResultExt;
 
+use self::channel_index::ChannelIndex;
+
 pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 
 pub type ChannelId = u64;
 
 pub struct ChannelStore {
-    channels_by_id: HashMap<ChannelId, Arc<Channel>>,
-    channel_paths: Vec<Vec<ChannelId>>,
+    channel_index: ChannelIndex,
     channel_invitations: Vec<Arc<Channel>>,
     channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
     channels_with_admin_privileges: HashSet<ChannelId>,
     outgoing_invites: HashSet<(ChannelId, UserId)>,
     update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
-    opened_buffers: HashMap<ChannelId, OpenedChannelBuffer>,
+    opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
+    opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
     client: Arc<Client>,
     user_store: ModelHandle<UserStore>,
     _rpc_subscription: Subscription,
@@ -29,12 +37,19 @@ pub struct ChannelStore {
     _update_channels: Task<()>,
 }
 
+pub type ChannelData = (Channel, ChannelPath);
+
 #[derive(Clone, Debug, PartialEq)]
 pub struct Channel {
     pub id: ChannelId,
     pub name: String,
+    pub unseen_note_version: Option<(u64, clock::Global)>,
+    pub unseen_message_id: Option<u64>,
 }
 
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
+pub struct ChannelPath(Arc<[ChannelId]>);
+
 pub struct ChannelMembership {
     pub user: Arc<User>,
     pub kind: proto::channel_member::Kind,
@@ -50,15 +65,9 @@ impl Entity for ChannelStore {
     type Event = ChannelEvent;
 }
 
-pub enum ChannelMemberStatus {
-    Invited,
-    Member,
-    NotMember,
-}
-
-enum OpenedChannelBuffer {
-    Open(WeakModelHandle<ChannelBuffer>),
-    Loading(Shared<Task<Result<ModelHandle<ChannelBuffer>, Arc<anyhow::Error>>>>),
+enum OpenedModelHandle<E: Entity> {
+    Open(WeakModelHandle<E>),
+    Loading(Shared<Task<Result<ModelHandle<E>, Arc<anyhow::Error>>>>),
 }
 
 impl ChannelStore {
@@ -87,13 +96,13 @@ impl ChannelStore {
         });
 
         Self {
-            channels_by_id: HashMap::default(),
             channel_invitations: Vec::default(),
-            channel_paths: Vec::default(),
+            channel_index: ChannelIndex::default(),
             channel_participants: Default::default(),
             channels_with_admin_privileges: Default::default(),
             outgoing_invites: Default::default(),
             opened_buffers: Default::default(),
+            opened_chats: Default::default(),
             update_channels_tx,
             client,
             user_store,
@@ -115,8 +124,12 @@ impl ChannelStore {
         }
     }
 
+    pub fn client(&self) -> Arc<Client> {
+        self.client.clone()
+    }
+
     pub fn has_children(&self, channel_id: ChannelId) -> bool {
-        self.channel_paths.iter().any(|path| {
+        self.channel_index.iter().any(|path| {
             if let Some(ix) = path.iter().position(|id| *id == channel_id) {
                 path.len() > ix + 1
             } else {
@@ -125,23 +138,43 @@ impl ChannelStore {
         })
     }
 
+    /// Returns the number of unique channels in the store
     pub fn channel_count(&self) -> usize {
-        self.channel_paths.len()
+        self.channel_index.by_id().len()
+    }
+
+    /// Returns the index of a channel ID in the list of unique channels
+    pub fn index_of_channel(&self, channel_id: ChannelId) -> Option<usize> {
+        self.channel_index
+            .by_id()
+            .keys()
+            .position(|id| *id == channel_id)
     }
 
-    pub fn channels(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
-        self.channel_paths.iter().map(move |path| {
+    /// Returns an iterator over all unique channels
+    pub fn channels(&self) -> impl '_ + Iterator<Item = &Arc<Channel>> {
+        self.channel_index.by_id().values()
+    }
+
+    /// Iterate over all entries in the channel DAG
+    pub fn channel_dag_entries(&self) -> impl '_ + Iterator<Item = (usize, &Arc<Channel>)> {
+        self.channel_index.iter().map(move |path| {
             let id = path.last().unwrap();
             let channel = self.channel_for_id(*id).unwrap();
             (path.len() - 1, channel)
         })
     }
 
-    pub fn channel_at_index(&self, ix: usize) -> Option<(usize, &Arc<Channel>)> {
-        let path = self.channel_paths.get(ix)?;
+    pub fn channel_dag_entry_at(&self, ix: usize) -> Option<(&Arc<Channel>, &ChannelPath)> {
+        let path = self.channel_index.get(ix)?;
         let id = path.last().unwrap();
         let channel = self.channel_for_id(*id).unwrap();
-        Some((path.len() - 1, channel))
+
+        Some((channel, path))
+    }
+
+    pub fn channel_at(&self, ix: usize) -> Option<&Arc<Channel>> {
+        self.channel_index.by_id().values().nth(ix)
     }
 
     pub fn channel_invitations(&self) -> &[Arc<Channel>] {
@@ -149,12 +182,12 @@ impl ChannelStore {
     }
 
     pub fn channel_for_id(&self, channel_id: ChannelId) -> Option<&Arc<Channel>> {
-        self.channels_by_id.get(&channel_id)
+        self.channel_index.by_id().get(&channel_id)
     }
 
     pub fn has_open_channel_buffer(&self, channel_id: ChannelId, cx: &AppContext) -> bool {
         if let Some(buffer) = self.opened_buffers.get(&channel_id) {
-            if let OpenedChannelBuffer::Open(buffer) = buffer {
+            if let OpenedModelHandle::Open(buffer) = buffer {
                 return buffer.upgrade(cx).is_some();
             }
         }
@@ -166,24 +199,122 @@ impl ChannelStore {
         channel_id: ChannelId,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<ChannelBuffer>>> {
-        // Make sure that a given channel buffer is only opened once per
-        // app instance, even if this method is called multiple times
-        // with the same channel id while the first task is still running.
+        let client = self.client.clone();
+        let user_store = self.user_store.clone();
+        self.open_channel_resource(
+            channel_id,
+            |this| &mut this.opened_buffers,
+            |channel, cx| ChannelBuffer::new(channel, client, user_store, cx),
+            cx,
+        )
+    }
+
+    pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option<bool> {
+        self.channel_index
+            .by_id()
+            .get(&channel_id)
+            .map(|channel| channel.unseen_note_version.is_some())
+    }
+
+    pub fn has_new_messages(&self, channel_id: ChannelId) -> Option<bool> {
+        self.channel_index
+            .by_id()
+            .get(&channel_id)
+            .map(|channel| channel.unseen_message_id.is_some())
+    }
+
+    pub fn notes_changed(
+        &mut self,
+        channel_id: ChannelId,
+        epoch: u64,
+        version: &clock::Global,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.channel_index.note_changed(channel_id, epoch, version);
+        cx.notify();
+    }
+
+    pub fn new_message(
+        &mut self,
+        channel_id: ChannelId,
+        message_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.channel_index.new_message(channel_id, message_id);
+        cx.notify();
+    }
+
+    pub fn acknowledge_message_id(
+        &mut self,
+        channel_id: ChannelId,
+        message_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.channel_index
+            .acknowledge_message_id(channel_id, message_id);
+        cx.notify();
+    }
+
+    pub fn acknowledge_notes_version(
+        &mut self,
+        channel_id: ChannelId,
+        epoch: u64,
+        version: &clock::Global,
+        cx: &mut ModelContext<Self>,
+    ) {
+        self.channel_index
+            .acknowledge_note_version(channel_id, epoch, version);
+        cx.notify();
+    }
+
+    pub fn open_channel_chat(
+        &mut self,
+        channel_id: ChannelId,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<ChannelChat>>> {
+        let client = self.client.clone();
+        let user_store = self.user_store.clone();
+        let this = cx.handle();
+        self.open_channel_resource(
+            channel_id,
+            |this| &mut this.opened_chats,
+            |channel, cx| ChannelChat::new(channel, this, user_store, client, cx),
+            cx,
+        )
+    }
+
+    /// Asynchronously open a given resource associated with a channel.
+    ///
+    /// Make sure that the resource is only opened once, even if this method
+    /// is called multiple times with the same channel id while the first task
+    /// is still running.
+    fn open_channel_resource<T: Entity, F, Fut>(
+        &mut self,
+        channel_id: ChannelId,
+        get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenedModelHandle<T>>,
+        load: F,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<T>>>
+    where
+        F: 'static + FnOnce(Arc<Channel>, AsyncAppContext) -> Fut,
+        Fut: Future<Output = Result<ModelHandle<T>>>,
+    {
         let task = loop {
-            match self.opened_buffers.entry(channel_id) {
+            match get_map(self).entry(channel_id) {
                 hash_map::Entry::Occupied(e) => match e.get() {
-                    OpenedChannelBuffer::Open(buffer) => {
-                        if let Some(buffer) = buffer.upgrade(cx) {
-                            break Task::ready(Ok(buffer)).shared();
+                    OpenedModelHandle::Open(model) => {
+                        if let Some(model) = model.upgrade(cx) {
+                            break Task::ready(Ok(model)).shared();
                         } else {
-                            self.opened_buffers.remove(&channel_id);
+                            get_map(self).remove(&channel_id);
                             continue;
                         }
                     }
-                    OpenedChannelBuffer::Loading(task) => break task.clone(),
+                    OpenedModelHandle::Loading(task) => {
+                        break task.clone();
+                    }
                 },
                 hash_map::Entry::Vacant(e) => {
-                    let client = self.client.clone();
                     let task = cx
                         .spawn(|this, cx| async move {
                             let channel = this.read_with(&cx, |this, _| {
@@ -192,30 +323,24 @@ impl ChannelStore {
                                 })
                             })?;
 
-                            ChannelBuffer::new(channel, client, cx)
-                                .await
-                                .map_err(Arc::new)
+                            load(channel, cx).await.map_err(Arc::new)
                         })
                         .shared();
-                    e.insert(OpenedChannelBuffer::Loading(task.clone()));
+
+                    e.insert(OpenedModelHandle::Loading(task.clone()));
                     cx.spawn({
                         let task = task.clone();
                         |this, mut cx| async move {
                             let result = task.await;
-                            this.update(&mut cx, |this, cx| match result {
-                                Ok(buffer) => {
-                                    cx.observe_release(&buffer, move |this, _, _| {
-                                        this.opened_buffers.remove(&channel_id);
-                                    })
-                                    .detach();
-                                    this.opened_buffers.insert(
+                            this.update(&mut cx, |this, _| match result {
+                                Ok(model) => {
+                                    get_map(this).insert(
                                         channel_id,
-                                        OpenedChannelBuffer::Open(buffer.downgrade()),
+                                        OpenedModelHandle::Open(model.downgrade()),
                                     );
                                 }
-                                Err(error) => {
-                                    log::error!("failed to open channel buffer {error:?}");
-                                    this.opened_buffers.remove(&channel_id);
+                                Err(_) => {
+                                    get_map(this).remove(&channel_id);
                                 }
                             });
                         }
@@ -230,7 +355,7 @@ impl ChannelStore {
     }
 
     pub fn is_user_admin(&self, channel_id: ChannelId) -> bool {
-        self.channel_paths.iter().any(|path| {
+        self.channel_index.iter().any(|path| {
             if let Some(ix) = path.iter().position(|id| *id == channel_id) {
                 path[..=ix]
                     .iter()
@@ -256,18 +381,33 @@ impl ChannelStore {
         let client = self.client.clone();
         let name = name.trim_start_matches("#").to_owned();
         cx.spawn(|this, mut cx| async move {
-            let channel = client
+            let response = client
                 .request(proto::CreateChannel { name, parent_id })
-                .await?
+                .await?;
+
+            let channel = response
                 .channel
                 .ok_or_else(|| anyhow!("missing channel in response"))?;
-
             let channel_id = channel.id;
 
+            let parent_edge = if let Some(parent_id) = parent_id {
+                vec![ChannelEdge {
+                    channel_id: channel.id,
+                    parent_id,
+                }]
+            } else {
+                vec![]
+            };
+
             this.update(&mut cx, |this, cx| {
                 let task = this.update_channels(
                     proto::UpdateChannels {
                         channels: vec![channel],
+                        insert_edge: parent_edge,
+                        channel_permissions: vec![ChannelPermission {
+                            channel_id,
+                            is_admin: true,
+                        }],
                         ..Default::default()
                     },
                     cx,
@@ -285,6 +425,59 @@ impl ChannelStore {
         })
     }
 
+    pub fn link_channel(
+        &mut self,
+        channel_id: ChannelId,
+        to: ChannelId,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let client = self.client.clone();
+        cx.spawn(|_, _| async move {
+            let _ = client
+                .request(proto::LinkChannel { channel_id, to })
+                .await?;
+
+            Ok(())
+        })
+    }
+
+    pub fn unlink_channel(
+        &mut self,
+        channel_id: ChannelId,
+        from: ChannelId,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let client = self.client.clone();
+        cx.spawn(|_, _| async move {
+            let _ = client
+                .request(proto::UnlinkChannel { channel_id, from })
+                .await?;
+
+            Ok(())
+        })
+    }
+
+    pub fn move_channel(
+        &mut self,
+        channel_id: ChannelId,
+        from: ChannelId,
+        to: ChannelId,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<()>> {
+        let client = self.client.clone();
+        cx.spawn(|_, _| async move {
+            let _ = client
+                .request(proto::MoveChannel {
+                    channel_id,
+                    from,
+                    to,
+                })
+                .await?;
+
+            Ok(())
+        })
+    }
+
     pub fn invite_member(
         &mut self,
         channel_id: ChannelId,
@@ -464,7 +657,7 @@ impl ChannelStore {
     pub fn remove_channel(&self, channel_id: ChannelId) -> impl Future<Output = Result<()>> {
         let client = self.client.clone();
         async move {
-            client.request(proto::RemoveChannel { channel_id }).await?;
+            client.request(proto::DeleteChannel { channel_id }).await?;
             Ok(())
         }
     }
@@ -494,9 +687,19 @@ impl ChannelStore {
     fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         self.disconnect_channel_buffers_task.take();
 
+        for chat in self.opened_chats.values() {
+            if let OpenedModelHandle::Open(chat) = chat {
+                if let Some(chat) = chat.upgrade(cx) {
+                    chat.update(cx, |chat, cx| {
+                        chat.rejoin(cx);
+                    });
+                }
+            }
+        }
+
         let mut buffer_versions = Vec::new();
         for buffer in self.opened_buffers.values() {
-            if let OpenedChannelBuffer::Open(buffer) = buffer {
+            if let OpenedModelHandle::Open(buffer) = buffer {
                 if let Some(buffer) = buffer.upgrade(cx) {
                     let channel_buffer = buffer.read(cx);
                     let buffer = channel_buffer.buffer().read(cx);
@@ -522,7 +725,7 @@ impl ChannelStore {
 
             this.update(&mut cx, |this, cx| {
                 this.opened_buffers.retain(|_, buffer| match buffer {
-                    OpenedChannelBuffer::Open(channel_buffer) => {
+                    OpenedModelHandle::Open(channel_buffer) => {
                         let Some(channel_buffer) = channel_buffer.upgrade(cx) else {
                             return false;
                         };
@@ -583,7 +786,7 @@ impl ChannelStore {
                             false
                         })
                     }
-                    OpenedChannelBuffer::Loading(_) => true,
+                    OpenedModelHandle::Loading(_) => true,
                 });
             });
             anyhow::Ok(())
@@ -591,11 +794,11 @@ impl ChannelStore {
     }
 
     fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
-        self.channels_by_id.clear();
+        self.channel_index.clear();
         self.channel_invitations.clear();
         self.channel_participants.clear();
         self.channels_with_admin_privileges.clear();
-        self.channel_paths.clear();
+        self.channel_index.clear();
         self.outgoing_invites.clear();
         cx.notify();
 
@@ -605,7 +808,7 @@ impl ChannelStore {
                 if let Some(this) = this.upgrade(&cx) {
                     this.update(&mut cx, |this, cx| {
                         for (_, buffer) in this.opened_buffers.drain() {
-                            if let OpenedChannelBuffer::Open(buffer) = buffer {
+                            if let OpenedModelHandle::Open(buffer) = buffer {
                                 if let Some(buffer) = buffer.upgrade(cx) {
                                     buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
                                 }
@@ -637,24 +840,31 @@ impl ChannelStore {
                     Arc::new(Channel {
                         id: channel.id,
                         name: channel.name,
+                        unseen_note_version: None,
+                        unseen_message_id: None,
                     }),
                 ),
             }
         }
 
-        let channels_changed = !payload.channels.is_empty() || !payload.remove_channels.is_empty();
+        let channels_changed = !payload.channels.is_empty()
+            || !payload.delete_channels.is_empty()
+            || !payload.insert_edge.is_empty()
+            || !payload.delete_edge.is_empty()
+            || !payload.unseen_channel_messages.is_empty()
+            || !payload.unseen_channel_buffer_changes.is_empty();
+
         if channels_changed {
-            if !payload.remove_channels.is_empty() {
-                self.channels_by_id
-                    .retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
+            if !payload.delete_channels.is_empty() {
+                self.channel_index.delete_channels(&payload.delete_channels);
                 self.channel_participants
-                    .retain(|channel_id, _| !payload.remove_channels.contains(channel_id));
+                    .retain(|channel_id, _| !payload.delete_channels.contains(channel_id));
                 self.channels_with_admin_privileges
-                    .retain(|channel_id| !payload.remove_channels.contains(channel_id));
+                    .retain(|channel_id| !payload.delete_channels.contains(channel_id));
 
-                for channel_id in &payload.remove_channels {
+                for channel_id in &payload.delete_channels {
                     let channel_id = *channel_id;
-                    if let Some(OpenedChannelBuffer::Open(buffer)) =
+                    if let Some(OpenedModelHandle::Open(buffer)) =
                         self.opened_buffers.remove(&channel_id)
                     {
                         if let Some(buffer) = buffer.upgrade(cx) {
@@ -664,44 +874,34 @@ impl ChannelStore {
                 }
             }
 
-            for channel_proto in payload.channels {
-                if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
-                    Arc::make_mut(existing_channel).name = channel_proto.name;
-                } else {
-                    let channel = Arc::new(Channel {
-                        id: channel_proto.id,
-                        name: channel_proto.name,
-                    });
-                    self.channels_by_id.insert(channel.id, channel.clone());
-
-                    if let Some(parent_id) = channel_proto.parent_id {
-                        let mut ix = 0;
-                        while ix < self.channel_paths.len() {
-                            let path = &self.channel_paths[ix];
-                            if path.ends_with(&[parent_id]) {
-                                let mut new_path = path.clone();
-                                new_path.push(channel.id);
-                                self.channel_paths.insert(ix + 1, new_path);
-                                ix += 1;
-                            }
-                            ix += 1;
-                        }
-                    } else {
-                        self.channel_paths.push(vec![channel.id]);
-                    }
-                }
+            let mut index = self.channel_index.bulk_insert();
+            for channel in payload.channels {
+                index.insert(channel)
             }
 
-            self.channel_paths.sort_by(|a, b| {
-                let a = Self::channel_path_sorting_key(a, &self.channels_by_id);
-                let b = Self::channel_path_sorting_key(b, &self.channels_by_id);
-                a.cmp(b)
-            });
-            self.channel_paths.dedup();
-            self.channel_paths.retain(|path| {
-                path.iter()
-                    .all(|channel_id| self.channels_by_id.contains_key(channel_id))
-            });
+            for unseen_buffer_change in payload.unseen_channel_buffer_changes {
+                let version = language::proto::deserialize_version(&unseen_buffer_change.version);
+                index.note_changed(
+                    unseen_buffer_change.channel_id,
+                    unseen_buffer_change.epoch,
+                    &version,
+                );
+            }
+
+            for unseen_channel_message in payload.unseen_channel_messages {
+                index.new_messages(
+                    unseen_channel_message.channel_id,
+                    unseen_channel_message.message_id,
+                );
+            }
+
+            for edge in payload.insert_edge {
+                index.insert_edge(edge.channel_id, edge.parent_id);
+            }
+
+            for edge in payload.delete_edge {
+                index.delete_edge(edge.parent_id, edge.channel_id);
+            }
         }
 
         for permission in payload.channel_permissions {
@@ -759,12 +959,45 @@ impl ChannelStore {
             anyhow::Ok(())
         }))
     }
+}
+
+impl Deref for ChannelPath {
+    type Target = [ChannelId];
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl ChannelPath {
+    pub fn new(path: Arc<[ChannelId]>) -> Self {
+        debug_assert!(path.len() >= 1);
+        Self(path)
+    }
+
+    pub fn parent_id(&self) -> Option<ChannelId> {
+        self.0.len().checked_sub(2).map(|i| self.0[i])
+    }
+
+    pub fn channel_id(&self) -> ChannelId {
+        self.0[self.0.len() - 1]
+    }
+}
+
+impl From<ChannelPath> for Cow<'static, ChannelPath> {
+    fn from(value: ChannelPath) -> Self {
+        Cow::Owned(value)
+    }
+}
+
+impl<'a> From<&'a ChannelPath> for Cow<'a, ChannelPath> {
+    fn from(value: &'a ChannelPath) -> Self {
+        Cow::Borrowed(value)
+    }
+}
 
-    fn channel_path_sorting_key<'a>(
-        path: &'a [ChannelId],
-        channels_by_id: &'a HashMap<ChannelId, Arc<Channel>>,
-    ) -> impl 'a + Iterator<Item = Option<&'a str>> {
-        path.iter()
-            .map(|id| Some(channels_by_id.get(id)?.name.as_str()))
+impl Default for ChannelPath {
+    fn default() -> Self {
+        ChannelPath(Arc::from([]))
     }
 }

crates/channel/src/channel_store/channel_index.rs πŸ”—

@@ -0,0 +1,238 @@
+use std::{ops::Deref, sync::Arc};
+
+use crate::{Channel, ChannelId};
+use collections::BTreeMap;
+use rpc::proto;
+
+use super::ChannelPath;
+
+#[derive(Default, Debug)]
+pub struct ChannelIndex {
+    paths: Vec<ChannelPath>,
+    channels_by_id: BTreeMap<ChannelId, Arc<Channel>>,
+}
+
+impl ChannelIndex {
+    pub fn by_id(&self) -> &BTreeMap<ChannelId, Arc<Channel>> {
+        &self.channels_by_id
+    }
+
+    pub fn clear(&mut self) {
+        self.paths.clear();
+        self.channels_by_id.clear();
+    }
+
+    /// Delete the given channels from this index.
+    pub fn delete_channels(&mut self, channels: &[ChannelId]) {
+        self.channels_by_id
+            .retain(|channel_id, _| !channels.contains(channel_id));
+        self.paths.retain(|path| {
+            path.iter()
+                .all(|channel_id| self.channels_by_id.contains_key(channel_id))
+        });
+    }
+
+    pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard {
+        ChannelPathsInsertGuard {
+            paths: &mut self.paths,
+            channels_by_id: &mut self.channels_by_id,
+        }
+    }
+
+    pub fn acknowledge_note_version(
+        &mut self,
+        channel_id: ChannelId,
+        epoch: u64,
+        version: &clock::Global,
+    ) {
+        if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
+            let channel = Arc::make_mut(channel);
+            if let Some((unseen_epoch, unseen_version)) = &channel.unseen_note_version {
+                if epoch > *unseen_epoch
+                    || epoch == *unseen_epoch && version.observed_all(unseen_version)
+                {
+                    channel.unseen_note_version = None;
+                }
+            }
+        }
+    }
+
+    pub fn acknowledge_message_id(&mut self, channel_id: ChannelId, message_id: u64) {
+        if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
+            let channel = Arc::make_mut(channel);
+            if let Some(unseen_message_id) = channel.unseen_message_id {
+                if message_id >= unseen_message_id {
+                    channel.unseen_message_id = None;
+                }
+            }
+        }
+    }
+
+    pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
+        insert_note_changed(&mut self.channels_by_id, channel_id, epoch, version);
+    }
+
+    pub fn new_message(&mut self, channel_id: ChannelId, message_id: u64) {
+        insert_new_message(&mut self.channels_by_id, channel_id, message_id)
+    }
+}
+
+impl Deref for ChannelIndex {
+    type Target = [ChannelPath];
+
+    fn deref(&self) -> &Self::Target {
+        &self.paths
+    }
+}
+
+/// A guard for ensuring that the paths index maintains its sort and uniqueness
+/// invariants after a series of insertions
+#[derive(Debug)]
+pub struct ChannelPathsInsertGuard<'a> {
+    paths: &'a mut Vec<ChannelPath>,
+    channels_by_id: &'a mut BTreeMap<ChannelId, Arc<Channel>>,
+}
+
+impl<'a> ChannelPathsInsertGuard<'a> {
+    /// Remove the given edge from this index. This will not remove the channel.
+    /// If this operation would result in a dangling edge, re-insert it.
+    pub fn delete_edge(&mut self, parent_id: ChannelId, channel_id: ChannelId) {
+        self.paths.retain(|path| {
+            !path
+                .windows(2)
+                .any(|window| window == [parent_id, channel_id])
+        });
+
+        // Ensure that there is at least one channel path in the index
+        if !self
+            .paths
+            .iter()
+            .any(|path| path.iter().any(|id| id == &channel_id))
+        {
+            self.insert_root(channel_id);
+        }
+    }
+
+    pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
+        insert_note_changed(&mut self.channels_by_id, channel_id, epoch, &version);
+    }
+
+    pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) {
+        insert_new_message(&mut self.channels_by_id, channel_id, message_id)
+    }
+
+    pub fn insert(&mut self, channel_proto: proto::Channel) {
+        if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
+            Arc::make_mut(existing_channel).name = channel_proto.name;
+        } else {
+            self.channels_by_id.insert(
+                channel_proto.id,
+                Arc::new(Channel {
+                    id: channel_proto.id,
+                    name: channel_proto.name,
+                    unseen_note_version: None,
+                    unseen_message_id: None,
+                }),
+            );
+            self.insert_root(channel_proto.id);
+        }
+    }
+
+    pub fn insert_edge(&mut self, channel_id: ChannelId, parent_id: ChannelId) {
+        let mut parents = Vec::new();
+        let mut descendants = Vec::new();
+        let mut ixs_to_remove = Vec::new();
+
+        for (ix, path) in self.paths.iter().enumerate() {
+            if path
+                .windows(2)
+                .any(|window| window[0] == parent_id && window[1] == channel_id)
+            {
+                // We already have this edge in the index
+                return;
+            }
+            if path.ends_with(&[parent_id]) {
+                parents.push(path);
+            } else if let Some(position) = path.iter().position(|id| id == &channel_id) {
+                if position == 0 {
+                    ixs_to_remove.push(ix);
+                }
+                descendants.push(path.split_at(position).1);
+            }
+        }
+
+        let mut new_paths = Vec::new();
+        for parent in parents.iter() {
+            if descendants.is_empty() {
+                let mut new_path = Vec::with_capacity(parent.len() + 1);
+                new_path.extend_from_slice(parent);
+                new_path.push(channel_id);
+                new_paths.push(ChannelPath::new(new_path.into()));
+            } else {
+                for descendant in descendants.iter() {
+                    let mut new_path = Vec::with_capacity(parent.len() + descendant.len());
+                    new_path.extend_from_slice(parent);
+                    new_path.extend_from_slice(descendant);
+                    new_paths.push(ChannelPath::new(new_path.into()));
+                }
+            }
+        }
+
+        for ix in ixs_to_remove.into_iter().rev() {
+            self.paths.swap_remove(ix);
+        }
+        self.paths.extend(new_paths)
+    }
+
+    fn insert_root(&mut self, channel_id: ChannelId) {
+        self.paths.push(ChannelPath::new(Arc::from([channel_id])));
+    }
+}
+
+impl<'a> Drop for ChannelPathsInsertGuard<'a> {
+    fn drop(&mut self) {
+        self.paths.sort_by(|a, b| {
+            let a = channel_path_sorting_key(a, &self.channels_by_id);
+            let b = channel_path_sorting_key(b, &self.channels_by_id);
+            a.cmp(b)
+        });
+        self.paths.dedup();
+    }
+}
+
+fn channel_path_sorting_key<'a>(
+    path: &'a [ChannelId],
+    channels_by_id: &'a BTreeMap<ChannelId, Arc<Channel>>,
+) -> impl 'a + Iterator<Item = Option<&'a str>> {
+    path.iter()
+        .map(|id| Some(channels_by_id.get(id)?.name.as_str()))
+}
+
+fn insert_note_changed(
+    channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
+    channel_id: u64,
+    epoch: u64,
+    version: &clock::Global,
+) {
+    if let Some(channel) = channels_by_id.get_mut(&channel_id) {
+        let unseen_version = Arc::make_mut(channel)
+            .unseen_note_version
+            .get_or_insert((0, clock::Global::new()));
+        if epoch > unseen_version.0 {
+            *unseen_version = (epoch, version.clone());
+        } else {
+            unseen_version.1.join(&version);
+        }
+    }
+}
+
+fn insert_new_message(
+    channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
+    channel_id: u64,
+    message_id: u64,
+) {
+    if let Some(channel) = channels_by_id.get_mut(&channel_id) {
+        let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0);
+        *unseen_message_id = message_id.max(*unseen_message_id);
+    }
+}

crates/channel/src/channel_store_tests.rs πŸ”—

@@ -1,16 +1,15 @@
+use crate::channel_chat::ChannelChatEvent;
+
 use super::*;
-use client::{Client, UserStore};
-use gpui::{AppContext, ModelHandle};
+use client::{test::FakeServer, Client, UserStore};
+use gpui::{AppContext, ModelHandle, TestAppContext};
 use rpc::proto;
+use settings::SettingsStore;
 use util::http::FakeHttpClient;
 
 #[gpui::test]
 fn test_update_channels(cx: &mut AppContext) {
-    let http = FakeHttpClient::with_404_response();
-    let client = Client::new(http.clone(), cx);
-    let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-
-    let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
+    let channel_store = init_test(cx);
 
     update_channels(
         &channel_store,
@@ -19,12 +18,10 @@ fn test_update_channels(cx: &mut AppContext) {
                 proto::Channel {
                     id: 1,
                     name: "b".to_string(),
-                    parent_id: None,
                 },
                 proto::Channel {
                     id: 2,
                     name: "a".to_string(),
-                    parent_id: None,
                 },
             ],
             channel_permissions: vec![proto::ChannelPermission {
@@ -52,12 +49,20 @@ fn test_update_channels(cx: &mut AppContext) {
                 proto::Channel {
                     id: 3,
                     name: "x".to_string(),
-                    parent_id: Some(1),
                 },
                 proto::Channel {
                     id: 4,
                     name: "y".to_string(),
-                    parent_id: Some(2),
+                },
+            ],
+            insert_edge: vec![
+                proto::ChannelEdge {
+                    parent_id: 1,
+                    channel_id: 3,
+                },
+                proto::ChannelEdge {
+                    parent_id: 2,
+                    channel_id: 4,
                 },
             ],
             ..Default::default()
@@ -78,11 +83,7 @@ fn test_update_channels(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_dangling_channel_paths(cx: &mut AppContext) {
-    let http = FakeHttpClient::with_404_response();
-    let client = Client::new(http.clone(), cx);
-    let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-
-    let channel_store = cx.add_model(|cx| ChannelStore::new(client, user_store, cx));
+    let channel_store = init_test(cx);
 
     update_channels(
         &channel_store,
@@ -91,17 +92,24 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
                 proto::Channel {
                     id: 0,
                     name: "a".to_string(),
-                    parent_id: None,
                 },
                 proto::Channel {
                     id: 1,
                     name: "b".to_string(),
-                    parent_id: Some(0),
                 },
                 proto::Channel {
                     id: 2,
                     name: "c".to_string(),
-                    parent_id: Some(1),
+                },
+            ],
+            insert_edge: vec![
+                proto::ChannelEdge {
+                    parent_id: 0,
+                    channel_id: 1,
+                },
+                proto::ChannelEdge {
+                    parent_id: 1,
+                    channel_id: 2,
                 },
             ],
             channel_permissions: vec![proto::ChannelPermission {
@@ -127,7 +135,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
     update_channels(
         &channel_store,
         proto::UpdateChannels {
-            remove_channels: vec![1, 2],
+            delete_channels: vec![1, 2],
             ..Default::default()
         },
         cx,
@@ -137,6 +145,207 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
     assert_channels(&channel_store, &[(0, "a".to_string(), true)], cx);
 }
 
+#[gpui::test]
+async fn test_channel_messages(cx: &mut TestAppContext) {
+    let user_id = 5;
+    let channel_id = 5;
+    let channel_store = cx.update(init_test);
+    let client = channel_store.read_with(cx, |s, _| s.client());
+    let server = FakeServer::for_client(user_id, &client, cx).await;
+
+    // Get the available channels.
+    server.send(proto::UpdateChannels {
+        channels: vec![proto::Channel {
+            id: channel_id,
+            name: "the-channel".to_string(),
+        }],
+        ..Default::default()
+    });
+    cx.foreground().run_until_parked();
+    cx.read(|cx| {
+        assert_channels(&channel_store, &[(0, "the-channel".to_string(), false)], cx);
+    });
+
+    let get_users = server.receive::<proto::GetUsers>().await.unwrap();
+    assert_eq!(get_users.payload.user_ids, vec![5]);
+    server.respond(
+        get_users.receipt(),
+        proto::UsersResponse {
+            users: vec![proto::User {
+                id: 5,
+                github_login: "nathansobo".into(),
+                avatar_url: "http://avatar.com/nathansobo".into(),
+            }],
+        },
+    );
+
+    // Join a channel and populate its existing messages.
+    let channel = channel_store.update(cx, |store, cx| {
+        let channel_id = store.channel_dag_entries().next().unwrap().1.id;
+        store.open_channel_chat(channel_id, cx)
+    });
+    let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
+    server.respond(
+        join_channel.receipt(),
+        proto::JoinChannelChatResponse {
+            messages: vec![
+                proto::ChannelMessage {
+                    id: 10,
+                    body: "a".into(),
+                    timestamp: 1000,
+                    sender_id: 5,
+                    nonce: Some(1.into()),
+                },
+                proto::ChannelMessage {
+                    id: 11,
+                    body: "b".into(),
+                    timestamp: 1001,
+                    sender_id: 6,
+                    nonce: Some(2.into()),
+                },
+            ],
+            done: false,
+        },
+    );
+
+    cx.foreground().start_waiting();
+
+    // Client requests all users for the received messages
+    let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
+    get_users.payload.user_ids.sort();
+    assert_eq!(get_users.payload.user_ids, vec![6]);
+    server.respond(
+        get_users.receipt(),
+        proto::UsersResponse {
+            users: vec![proto::User {
+                id: 6,
+                github_login: "maxbrunsfeld".into(),
+                avatar_url: "http://avatar.com/maxbrunsfeld".into(),
+            }],
+        },
+    );
+
+    let channel = channel.await.unwrap();
+    channel.read_with(cx, |channel, _| {
+        assert_eq!(
+            channel
+                .messages_in_range(0..2)
+                .map(|message| (message.sender.github_login.clone(), message.body.clone()))
+                .collect::<Vec<_>>(),
+            &[
+                ("nathansobo".into(), "a".into()),
+                ("maxbrunsfeld".into(), "b".into())
+            ]
+        );
+    });
+
+    // Receive a new message.
+    server.send(proto::ChannelMessageSent {
+        channel_id,
+        message: Some(proto::ChannelMessage {
+            id: 12,
+            body: "c".into(),
+            timestamp: 1002,
+            sender_id: 7,
+            nonce: Some(3.into()),
+        }),
+    });
+
+    // Client requests user for message since they haven't seen them yet
+    let get_users = server.receive::<proto::GetUsers>().await.unwrap();
+    assert_eq!(get_users.payload.user_ids, vec![7]);
+    server.respond(
+        get_users.receipt(),
+        proto::UsersResponse {
+            users: vec![proto::User {
+                id: 7,
+                github_login: "as-cii".into(),
+                avatar_url: "http://avatar.com/as-cii".into(),
+            }],
+        },
+    );
+
+    assert_eq!(
+        channel.next_event(cx).await,
+        ChannelChatEvent::MessagesUpdated {
+            old_range: 2..2,
+            new_count: 1,
+        }
+    );
+    channel.read_with(cx, |channel, _| {
+        assert_eq!(
+            channel
+                .messages_in_range(2..3)
+                .map(|message| (message.sender.github_login.clone(), message.body.clone()))
+                .collect::<Vec<_>>(),
+            &[("as-cii".into(), "c".into())]
+        )
+    });
+
+    // Scroll up to view older messages.
+    channel.update(cx, |channel, cx| {
+        assert!(channel.load_more_messages(cx));
+    });
+    let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
+    assert_eq!(get_messages.payload.channel_id, 5);
+    assert_eq!(get_messages.payload.before_message_id, 10);
+    server.respond(
+        get_messages.receipt(),
+        proto::GetChannelMessagesResponse {
+            done: true,
+            messages: vec![
+                proto::ChannelMessage {
+                    id: 8,
+                    body: "y".into(),
+                    timestamp: 998,
+                    sender_id: 5,
+                    nonce: Some(4.into()),
+                },
+                proto::ChannelMessage {
+                    id: 9,
+                    body: "z".into(),
+                    timestamp: 999,
+                    sender_id: 6,
+                    nonce: Some(5.into()),
+                },
+            ],
+        },
+    );
+
+    assert_eq!(
+        channel.next_event(cx).await,
+        ChannelChatEvent::MessagesUpdated {
+            old_range: 0..0,
+            new_count: 2,
+        }
+    );
+    channel.read_with(cx, |channel, _| {
+        assert_eq!(
+            channel
+                .messages_in_range(0..2)
+                .map(|message| (message.sender.github_login.clone(), message.body.clone()))
+                .collect::<Vec<_>>(),
+            &[
+                ("nathansobo".into(), "y".into()),
+                ("maxbrunsfeld".into(), "z".into())
+            ]
+        );
+    });
+}
+
+fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
+    let http = FakeHttpClient::with_404_response();
+    let client = Client::new(http.clone(), cx);
+    let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+
+    cx.foreground().forbid_parking();
+    cx.set_global(SettingsStore::test(cx));
+    crate::init(&client);
+    client::init(&client, cx);
+
+    cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
+}
+
 fn update_channels(
     channel_store: &ModelHandle<ChannelStore>,
     message: proto::UpdateChannels,
@@ -154,7 +363,7 @@ fn assert_channels(
 ) {
     let actual = channel_store.read_with(cx, |store, _| {
         store
-            .channels()
+            .channel_dag_entries()
             .map(|(depth, channel)| {
                 (
                     depth,

crates/client/Cargo.toml πŸ”—

@@ -33,15 +33,16 @@ parking_lot.workspace = true
 postage.workspace = true
 rand.workspace = true
 schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
 smol.workspace = true
+sysinfo.workspace = true
+tempfile = "3"
 thiserror.workspace = true
 time.workspace = true
 tiny_http = "0.8"
-uuid = { version = "1.1.2", features = ["v4"] }
+uuid.workspace = true
 url = "2.2"
-serde.workspace = true
-serde_derive.workspace = true
-tempfile = "3"
 
 [dev-dependencies]
 collections = { path = "../collections", features = ["test-support"] }

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

@@ -34,7 +34,7 @@ use std::{
     future::Future,
     marker::PhantomData,
     path::PathBuf,
-    sync::{Arc, Weak},
+    sync::{atomic::AtomicU64, Arc, Weak},
     time::{Duration, Instant},
 };
 use telemetry::Telemetry;
@@ -62,6 +62,8 @@ lazy_static! {
         .and_then(|v| v.parse().ok());
     pub static ref ZED_APP_PATH: Option<PathBuf> =
         std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
+    pub static ref ZED_ALWAYS_ACTIVE: bool =
+        std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| e.len() > 0);
 }
 
 pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
@@ -103,7 +105,7 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
 }
 
 pub struct Client {
-    id: usize,
+    id: AtomicU64,
     peer: Arc<Peer>,
     http: Arc<dyn HttpClient>,
     telemetry: Arc<Telemetry>,
@@ -372,7 +374,7 @@ impl settings::Setting for TelemetrySettings {
 impl Client {
     pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
         Arc::new(Self {
-            id: 0,
+            id: AtomicU64::new(0),
             peer: Peer::new(0),
             telemetry: Telemetry::new(http.clone(), cx),
             http,
@@ -385,17 +387,16 @@ impl Client {
         })
     }
 
-    pub fn id(&self) -> usize {
-        self.id
+    pub fn id(&self) -> u64 {
+        self.id.load(std::sync::atomic::Ordering::SeqCst)
     }
 
     pub fn http_client(&self) -> Arc<dyn HttpClient> {
         self.http.clone()
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn set_id(&mut self, id: usize) -> &Self {
-        self.id = id;
+    pub fn set_id(&self, id: u64) -> &Self {
+        self.id.store(id, std::sync::atomic::Ordering::SeqCst);
         self
     }
 
@@ -452,7 +453,7 @@ impl Client {
     }
 
     fn set_status(self: &Arc<Self>, status: Status, cx: &AsyncAppContext) {
-        log::info!("set status on client {}: {:?}", self.id, status);
+        log::info!("set status on client {}: {:?}", self.id(), status);
         let mut state = self.state.write();
         *state.status.0.borrow_mut() = status;
 
@@ -803,6 +804,7 @@ impl Client {
             }
         }
         let credentials = credentials.unwrap();
+        self.set_id(credentials.user_id);
 
         if was_disconnected {
             self.set_status(Status::Connecting, cx);
@@ -1219,7 +1221,7 @@ impl Client {
     }
 
     pub fn send<T: EnvelopedMessage>(&self, message: T) -> Result<()> {
-        log::debug!("rpc send. client_id:{}, name:{}", self.id, T::NAME);
+        log::debug!("rpc send. client_id:{}, name:{}", self.id(), T::NAME);
         self.peer.send(self.connection_id()?, message)
     }
 
@@ -1235,7 +1237,7 @@ impl Client {
         &self,
         request: T,
     ) -> impl Future<Output = Result<TypedEnvelope<T::Response>>> {
-        let client_id = self.id;
+        let client_id = self.id();
         log::debug!(
             "rpc request start. client_id:{}. name:{}",
             client_id,
@@ -1256,7 +1258,7 @@ impl Client {
     }
 
     fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
-        log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
+        log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
         self.peer.respond(receipt, response)
     }
 
@@ -1265,7 +1267,7 @@ impl Client {
         receipt: Receipt<T>,
         error: proto::Error,
     ) -> Result<()> {
-        log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
+        log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
         self.peer.respond_with_error(receipt, error)
     }
 
@@ -1334,7 +1336,7 @@ impl Client {
 
         if let Some(handler) = handler {
             let future = handler(subscriber, message, &self, cx.clone());
-            let client_id = self.id;
+            let client_id = self.id();
             log::debug!(
                 "rpc message received. client_id:{}, sender_id:{:?}, type:{}",
                 client_id,

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

@@ -4,9 +4,11 @@ use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use serde::Serialize;
 use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
+use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
 use tempfile::NamedTempFile;
 use util::http::HttpClient;
 use util::{channel::ReleaseChannel, TryFutureExt};
+use uuid::Uuid;
 
 pub struct Telemetry {
     http_client: Arc<dyn HttpClient>,
@@ -17,7 +19,8 @@ pub struct Telemetry {
 #[derive(Default)]
 struct TelemetryState {
     metrics_id: Option<Arc<str>>,      // Per logged-in user
-    installation_id: Option<Arc<str>>, // Per app installation
+    installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
+    session_id: String,                // Per app launch
     app_version: Option<Arc<str>>,
     release_channel: Option<&'static str>,
     os_name: &'static str,
@@ -40,6 +43,7 @@ lazy_static! {
 struct ClickhouseEventRequestBody {
     token: &'static str,
     installation_id: Option<Arc<str>>,
+    session_id: String,
     is_staff: Option<bool>,
     app_version: Option<Arc<str>>,
     os_name: &'static str,
@@ -56,6 +60,13 @@ struct ClickhouseEventWrapper {
     event: ClickhouseEvent,
 }
 
+#[derive(Serialize, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum AssistantKind {
+    Panel,
+    Inline,
+}
+
 #[derive(Serialize, Debug)]
 #[serde(tag = "type")]
 pub enum ClickhouseEvent {
@@ -76,6 +87,19 @@ pub enum ClickhouseEvent {
         room_id: Option<u64>,
         channel_id: Option<u64>,
     },
+    Assistant {
+        conversation_id: Option<String>,
+        kind: AssistantKind,
+        model: &'static str,
+    },
+    Cpu {
+        usage_as_percentage: f32,
+        core_count: u32,
+    },
+    Memory {
+        memory_in_bytes: u64,
+        virtual_memory_in_bytes: u64,
+    },
 }
 
 #[cfg(debug_assertions)]
@@ -110,6 +134,7 @@ impl Telemetry {
                 release_channel,
                 installation_id: None,
                 metrics_id: None,
+                session_id: Uuid::new_v4().to_string(),
                 clickhouse_events_queue: Default::default(),
                 flush_clickhouse_events_task: Default::default(),
                 log_file: None,
@@ -124,7 +149,7 @@ impl Telemetry {
         Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
     }
 
-    pub fn start(self: &Arc<Self>, installation_id: Option<String>) {
+    pub fn start(self: &Arc<Self>, installation_id: Option<String>, cx: &mut AppContext) {
         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();
@@ -133,6 +158,46 @@ impl Telemetry {
         if has_clickhouse_events {
             self.flush_clickhouse_events();
         }
+
+        let this = self.clone();
+        cx.spawn(|mut cx| async move {
+            let mut system = System::new_all();
+            system.refresh_all();
+
+            loop {
+                // Waiting some amount of time before the first query is important to get a reasonable value
+                // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
+                const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
+                smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
+
+                system.refresh_memory();
+                system.refresh_processes();
+
+                let current_process = Pid::from_u32(std::process::id());
+                let Some(process) = system.processes().get(&current_process) else {
+                    let process = current_process;
+                    log::error!("Failed to find own process {process:?} in system process table");
+                    // TODO: Fire an error telemetry event
+                    return;
+                };
+
+                let memory_event = ClickhouseEvent::Memory {
+                    memory_in_bytes: process.memory(),
+                    virtual_memory_in_bytes: process.virtual_memory(),
+                };
+
+                let cpu_event = ClickhouseEvent::Cpu {
+                    usage_as_percentage: process.cpu_usage(),
+                    core_count: system.cpus().len() as u32,
+                };
+
+                let telemetry_settings = cx.update(|cx| *settings::get::<TelemetrySettings>(cx));
+
+                this.report_clickhouse_event(memory_event, telemetry_settings);
+                this.report_clickhouse_event(cpu_event, telemetry_settings);
+            }
+        })
+        .detach();
     }
 
     pub fn set_authenticated_user_info(
@@ -224,6 +289,7 @@ impl Telemetry {
                             &ClickhouseEventRequestBody {
                                 token: ZED_SECRET_CLIENT_TOKEN,
                                 installation_id: state.installation_id.clone(),
+                                session_id: state.session_id.clone(),
                                 is_staff: state.is_staff.clone(),
                                 app_version: state.app_version.clone(),
                                 os_name: state.os_name,

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

@@ -170,8 +170,7 @@ impl FakeServer {
                         staff: false,
                         flags: Default::default(),
                     },
-                )
-                .await;
+                );
                 continue;
             }
 
@@ -182,11 +181,7 @@ impl FakeServer {
         }
     }
 
-    pub async fn respond<T: proto::RequestMessage>(
-        &self,
-        receipt: Receipt<T>,
-        response: T::Response,
-    ) {
+    pub fn respond<T: proto::RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) {
         self.peer.respond(receipt, response).unwrap()
     }
 

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

@@ -7,11 +7,15 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
 use postage::{sink::Sink, watch};
 use rpc::proto::{RequestMessage, UsersResponse};
 use std::sync::{Arc, Weak};
+use text::ReplicaId;
 use util::http::HttpClient;
 use util::TryFutureExt as _;
 
 pub type UserId = u64;
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct ParticipantIndex(pub u32);
+
 #[derive(Default, Debug)]
 pub struct User {
     pub id: UserId,
@@ -19,6 +23,13 @@ pub struct User {
     pub avatar: Option<Arc<ImageData>>,
 }
 
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Collaborator {
+    pub peer_id: proto::PeerId,
+    pub replica_id: ReplicaId,
+    pub user_id: UserId,
+}
+
 impl PartialOrd for User {
     fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
         Some(self.cmp(other))
@@ -56,6 +67,7 @@ pub enum ContactRequestStatus {
 
 pub struct UserStore {
     users: HashMap<u64, Arc<User>>,
+    participant_indices: HashMap<u64, ParticipantIndex>,
     update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
     current_user: watch::Receiver<Option<Arc<User>>>,
     contacts: Vec<Arc<Contact>>,
@@ -81,6 +93,7 @@ pub enum Event {
         kind: ContactEventKind,
     },
     ShowContacts,
+    ParticipantIndicesChanged,
 }
 
 #[derive(Clone, Copy)]
@@ -118,6 +131,7 @@ impl UserStore {
             current_user: current_user_rx,
             contacts: Default::default(),
             incoming_contact_requests: Default::default(),
+            participant_indices: Default::default(),
             outgoing_contact_requests: Default::default(),
             invite_info: None,
             client: Arc::downgrade(&client),
@@ -581,6 +595,10 @@ impl UserStore {
         self.load_users(proto::FuzzySearchUsers { query }, cx)
     }
 
+    pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
+        self.users.get(&user_id).cloned()
+    }
+
     pub fn get_user(
         &mut self,
         user_id: u64,
@@ -641,6 +659,21 @@ impl UserStore {
             }
         })
     }
+
+    pub fn set_participant_indices(
+        &mut self,
+        participant_indices: HashMap<u64, ParticipantIndex>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if participant_indices != self.participant_indices {
+            self.participant_indices = participant_indices;
+            cx.emit(Event::ParticipantIndicesChanged);
+        }
+    }
+
+    pub fn participant_indices(&self) -> &HashMap<u64, ParticipantIndex> {
+        &self.participant_indices
+    }
 }
 
 impl User {
@@ -672,6 +705,16 @@ impl Contact {
     }
 }
 
+impl Collaborator {
+    pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
+        Ok(Self {
+            peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
+            replica_id: message.replica_id as ReplicaId,
+            user_id: message.user_id as UserId,
+        })
+    }
+}
+
 async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
     let mut response = http
         .get(url, Default::default(), true)

crates/collab/Cargo.toml πŸ”—

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.20.0"
+version = "0.23.2"
 publish = false
 
 [[bin]]
@@ -41,14 +41,13 @@ prost.workspace = true
 rand.workspace = true
 reqwest = { version = "0.11", features = ["json"], optional = true }
 scrypt = "0.7"
-# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released.
-sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
-sea-query = "0.27"
+smallvec.workspace = true
+sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
 sha-1 = "0.9"
-sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
+sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
 time.workspace = true
 tokio = { version = "1", features = ["full"] }
 tokio-tungstenite = "0.17"
@@ -58,6 +57,7 @@ toml.workspace = true
 tracing = "0.1.34"
 tracing-log = "0.1.3"
 tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
+uuid.workspace = true
 
 [dev-dependencies]
 audio = { path = "../audio" }
@@ -72,7 +72,6 @@ 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.workspace = true
 project = { path = "../project", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
@@ -81,14 +80,15 @@ workspace = { path = "../workspace", features = ["test-support"] }
 collab_ui = { path = "../collab_ui", features = ["test-support"] }
 
 async-trait.workspace = true
+pretty_assertions.workspace = true
 ctor.workspace = true
 env_logger.workspace = true
 indoc.workspace = true
 util = { path = "../util" }
 lazy_static.workspace = true
-sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
+sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
 serde_json.workspace = true
-sqlx = { version = "0.6", features = ["sqlite"] }
+sqlx = { version = "0.7", features = ["sqlite"] }
 unindent.workspace = true
 
 [features]

crates/collab/admin_api.conf πŸ”—

@@ -0,0 +1,4 @@
+db-uri = "postgres://postgres@localhost/zed"
+server-port = 8081
+jwt-secret = "the-postgrest-jwt-secret-for-authorization"
+log-level = "info"

crates/collab/k8s/manifest.template.yml πŸ”—

@@ -3,6 +3,7 @@ apiVersion: v1
 kind: Namespace
 metadata:
   name: ${ZED_KUBE_NAMESPACE}
+
 ---
 kind: Service
 apiVersion: v1
@@ -11,7 +12,7 @@ metadata:
   name: collab
   annotations:
     service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
-    service.beta.kubernetes.io/do-loadbalancer-certificate-id: "08d9d8ce-761f-4ab3-bc78-4923ab5b0e33"
+    service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID}
 spec:
   type: LoadBalancer
   selector:
@@ -21,6 +22,26 @@ spec:
       protocol: TCP
       port: 443
       targetPort: 8080
+
+---
+kind: Service
+apiVersion: v1
+metadata:
+  namespace: ${ZED_KUBE_NAMESPACE}
+  name: pgadmin
+  annotations:
+    service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
+    service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID}
+spec:
+  type: LoadBalancer
+  selector:
+    app: postgrest
+  ports:
+    - name: web
+      protocol: TCP
+      port: 443
+      targetPort: 8080
+
 ---
 apiVersion: apps/v1
 kind: Deployment
@@ -117,3 +138,40 @@ spec:
               # FIXME - Switch to the more restrictive `PERFMON` capability.
               # This capability isn't yet available in a stable version of Debian.
               add: ["SYS_ADMIN"]
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  namespace: ${ZED_KUBE_NAMESPACE}
+  name: postgrest
+
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: postgrest
+  template:
+    metadata:
+      labels:
+        app: postgrest
+    spec:
+      containers:
+        - name: postgrest
+          image: "postgrest/postgrest"
+          ports:
+            - containerPort: 8080
+              protocol: TCP
+          env:
+            - name: PGRST_SERVER_PORT
+              value: "8080"
+            - name: PGRST_DB_URI
+              valueFrom:
+                secretKeyRef:
+                  name: database
+                  key: url
+            - name: PGRST_JWT_SECRET
+              valueFrom:
+                secretKeyRef:
+                  name: postgrest
+                  key: jwt_secret

crates/collab/migrations.sqlite/20221109000000_test_schema.sql πŸ”—

@@ -158,7 +158,8 @@ CREATE TABLE "room_participants" (
     "initial_project_id" INTEGER,
     "calling_user_id" INTEGER NOT NULL REFERENCES users (id),
     "calling_connection_id" INTEGER NOT NULL,
-    "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL
+    "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL,
+    "participant_index" INTEGER
 );
 CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
 CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
@@ -192,6 +193,26 @@ CREATE TABLE "channels" (
     "created_at" TIMESTAMP NOT NULL DEFAULT now
 );
 
+CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
+    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+    "user_id" INTEGER NOT NULL REFERENCES users (id),
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "connection_id" INTEGER NOT NULL,
+    "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE
+);
+CREATE INDEX "index_channel_chat_participants_on_channel_id" ON "channel_chat_participants" ("channel_id");
+
+CREATE TABLE IF NOT EXISTS "channel_messages" (
+    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "sender_id" INTEGER NOT NULL REFERENCES users (id),
+    "body" TEXT NOT NULL,
+    "sent_at" TIMESTAMP,
+    "nonce" BLOB NOT NULL
+);
+CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id");
+CREATE UNIQUE INDEX "index_channel_messages_on_nonce" ON "channel_messages" ("nonce");
+
 CREATE TABLE "channel_paths" (
     "id_path" TEXT NOT NULL PRIMARY KEY,
     "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE
@@ -268,3 +289,24 @@ CREATE TABLE "user_features" (
 CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id");
 CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id");
 CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id");
+
+
+CREATE TABLE "observed_buffer_edits" (
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
+    "epoch" INTEGER NOT NULL,
+    "lamport_timestamp" INTEGER NOT NULL,
+    "replica_id" INTEGER NOT NULL,
+    PRIMARY KEY (user_id, buffer_id)
+);
+
+CREATE UNIQUE INDEX "index_observed_buffers_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id");
+
+CREATE TABLE IF NOT EXISTS "observed_channel_messages" (
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "channel_message_id" INTEGER NOT NULL,
+    PRIMARY KEY (user_id, channel_id)
+);
+
+CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id");

crates/collab/migrations/20230907114200_add_channel_messages.sql πŸ”—

@@ -0,0 +1,19 @@
+CREATE TABLE IF NOT EXISTS "channel_messages" (
+    "id" SERIAL PRIMARY KEY,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "sender_id" INTEGER NOT NULL REFERENCES users (id),
+    "body" TEXT NOT NULL,
+    "sent_at" TIMESTAMP,
+    "nonce" UUID NOT NULL
+);
+CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id");
+CREATE UNIQUE INDEX "index_channel_messages_on_nonce" ON "channel_messages" ("nonce");
+
+CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
+    "id" SERIAL PRIMARY KEY,
+    "user_id" INTEGER NOT NULL REFERENCES users (id),
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "connection_id" INTEGER NOT NULL,
+    "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE
+);
+CREATE INDEX "index_channel_chat_participants_on_channel_id" ON "channel_chat_participants" ("channel_id");

crates/collab/migrations/20230925210437_add_channel_changes.sql πŸ”—

@@ -0,0 +1,19 @@
+CREATE TABLE IF NOT EXISTS "observed_buffer_edits" (
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
+    "epoch" INTEGER NOT NULL,
+    "lamport_timestamp" INTEGER NOT NULL,
+    "replica_id" INTEGER NOT NULL,
+    PRIMARY KEY (user_id, buffer_id)
+);
+
+CREATE UNIQUE INDEX "index_observed_buffer_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id");
+
+CREATE TABLE IF NOT EXISTS "observed_channel_messages" (
+    "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+    "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
+    "channel_message_id" INTEGER NOT NULL,
+    PRIMARY KEY (user_id, channel_id)
+);
+
+CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id");

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

@@ -1,8 +1,7 @@
 use crate::{
     auth,
-    db::{Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary},
-    rpc::{self, ResultExt},
-    AppState, Error, Result,
+    db::{User, UserId},
+    rpc, AppState, Error, Result,
 };
 use anyhow::anyhow;
 use axum::{
@@ -11,7 +10,7 @@ use axum::{
     http::{self, Request, StatusCode},
     middleware::{self, Next},
     response::IntoResponse,
-    routing::{get, post, put},
+    routing::{get, post},
     Extension, Json, Router,
 };
 use axum_extra::response::ErasedJson;
@@ -23,18 +22,9 @@ use tracing::instrument;
 pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
     Router::new()
         .route("/user", get(get_authenticated_user))
-        .route("/users", get(get_users).post(create_user))
-        .route("/users/:id", put(update_user).delete(destroy_user))
         .route("/users/:id/access_tokens", post(create_access_token))
-        .route("/users_with_no_invites", get(get_users_with_no_invites))
-        .route("/invite_codes/:code", get(get_user_for_invite_code))
         .route("/panic", post(trace_panic))
         .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
-        .route("/signups", post(create_signup))
-        .route("/signups_summary", get(get_waitlist_summary))
-        .route("/user_invites", post(create_invite_from_code))
-        .route("/unsent_invites", get(get_unsent_invites))
-        .route("/sent_invites", post(record_sent_invites))
         .layer(
             ServiceBuilder::new()
                 .layer(Extension(state))
@@ -104,28 +94,6 @@ async fn get_authenticated_user(
     return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
 }
 
-#[derive(Debug, Deserialize)]
-struct GetUsersQueryParams {
-    query: Option<String>,
-    page: Option<u32>,
-    limit: Option<u32>,
-}
-
-async fn get_users(
-    Query(params): Query<GetUsersQueryParams>,
-    Extension(app): Extension<Arc<AppState>>,
-) -> Result<Json<Vec<User>>> {
-    let limit = params.limit.unwrap_or(100);
-    let users = if let Some(query) = params.query {
-        app.db.fuzzy_search_users(&query, limit).await?
-    } else {
-        app.db
-            .get_all_users(params.page.unwrap_or(0), limit)
-            .await?
-    };
-    Ok(Json(users))
-}
-
 #[derive(Deserialize, Debug)]
 struct CreateUserParams {
     github_user_id: i32,
@@ -145,119 +113,6 @@ struct CreateUserResponse {
     metrics_id: String,
 }
 
-async fn create_user(
-    Json(params): Json<CreateUserParams>,
-    Extension(app): Extension<Arc<AppState>>,
-    Extension(rpc_server): Extension<Arc<rpc::Server>>,
-) -> Result<Json<Option<CreateUserResponse>>> {
-    let user = NewUserParams {
-        github_login: params.github_login,
-        github_user_id: params.github_user_id,
-        invite_count: params.invite_count,
-    };
-
-    // Creating a user via the normal signup process
-    let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
-        if let Some(result) = app
-            .db
-            .create_user_from_invite(
-                &Invite {
-                    email_address: params.email_address,
-                    email_confirmation_code,
-                },
-                user,
-            )
-            .await?
-        {
-            result
-        } else {
-            return Ok(Json(None));
-        }
-    }
-    // Creating a user as an admin
-    else if params.admin {
-        app.db
-            .create_user(&params.email_address, false, user)
-            .await?
-    } else {
-        Err(Error::Http(
-            StatusCode::UNPROCESSABLE_ENTITY,
-            "email confirmation code is required".into(),
-        ))?
-    };
-
-    if let Some(inviter_id) = result.inviting_user_id {
-        rpc_server
-            .invite_code_redeemed(inviter_id, result.user_id)
-            .await
-            .trace_err();
-    }
-
-    let user = app
-        .db
-        .get_user_by_id(result.user_id)
-        .await?
-        .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
-
-    Ok(Json(Some(CreateUserResponse {
-        user,
-        metrics_id: result.metrics_id,
-        signup_device_id: result.signup_device_id,
-    })))
-}
-
-#[derive(Deserialize)]
-struct UpdateUserParams {
-    admin: Option<bool>,
-    invite_count: Option<i32>,
-}
-
-async fn update_user(
-    Path(user_id): Path<i32>,
-    Json(params): Json<UpdateUserParams>,
-    Extension(app): Extension<Arc<AppState>>,
-    Extension(rpc_server): Extension<Arc<rpc::Server>>,
-) -> Result<()> {
-    let user_id = UserId(user_id);
-
-    if let Some(admin) = params.admin {
-        app.db.set_user_is_admin(user_id, admin).await?;
-    }
-
-    if let Some(invite_count) = params.invite_count {
-        app.db
-            .set_invite_count_for_user(user_id, invite_count)
-            .await?;
-        rpc_server.invite_count_updated(user_id).await.trace_err();
-    }
-
-    Ok(())
-}
-
-async fn destroy_user(
-    Path(user_id): Path<i32>,
-    Extension(app): Extension<Arc<AppState>>,
-) -> Result<()> {
-    app.db.destroy_user(UserId(user_id)).await?;
-    Ok(())
-}
-
-#[derive(Debug, Deserialize)]
-struct GetUsersWithNoInvites {
-    invited_by_another_user: bool,
-}
-
-async fn get_users_with_no_invites(
-    Query(params): Query<GetUsersWithNoInvites>,
-    Extension(app): Extension<Arc<AppState>>,
-) -> Result<Json<Vec<User>>> {
-    Ok(Json(
-        app.db
-            .get_users_with_no_invites(params.invited_by_another_user)
-            .await?,
-    ))
-}
-
 #[derive(Debug, Deserialize)]
 struct Panic {
     version: String,
@@ -327,69 +182,3 @@ async fn create_access_token(
         encrypted_access_token,
     }))
 }
-
-async fn get_user_for_invite_code(
-    Path(code): Path<String>,
-    Extension(app): Extension<Arc<AppState>>,
-) -> Result<Json<User>> {
-    Ok(Json(app.db.get_user_for_invite_code(&code).await?))
-}
-
-async fn create_signup(
-    Json(params): Json<NewSignup>,
-    Extension(app): Extension<Arc<AppState>>,
-) -> Result<()> {
-    app.db.create_signup(&params).await?;
-    Ok(())
-}
-
-async fn get_waitlist_summary(
-    Extension(app): Extension<Arc<AppState>>,
-) -> Result<Json<WaitlistSummary>> {
-    Ok(Json(app.db.get_waitlist_summary().await?))
-}
-
-#[derive(Deserialize)]
-pub struct CreateInviteFromCodeParams {
-    invite_code: String,
-    email_address: String,
-    device_id: Option<String>,
-    #[serde(default)]
-    added_to_mailing_list: bool,
-}
-
-async fn create_invite_from_code(
-    Json(params): Json<CreateInviteFromCodeParams>,
-    Extension(app): Extension<Arc<AppState>>,
-) -> Result<Json<Invite>> {
-    Ok(Json(
-        app.db
-            .create_invite_from_code(
-                &params.invite_code,
-                &params.email_address,
-                params.device_id.as_deref(),
-                params.added_to_mailing_list,
-            )
-            .await?,
-    ))
-}
-
-#[derive(Deserialize)]
-pub struct GetUnsentInvitesParams {
-    pub count: usize,
-}
-
-async fn get_unsent_invites(
-    Query(params): Query<GetUnsentInvitesParams>,
-    Extension(app): Extension<Arc<AppState>>,
-) -> Result<Json<Vec<Invite>>> {
-    Ok(Json(app.db.get_unsent_invites(params.count).await?))
-}
-
-async fn record_sent_invites(
-    Json(params): Json<Vec<Invite>>,
-    Extension(app): Extension<Arc<AppState>>,
-) -> Result<()> {
-    app.db.record_sent_invites(&params).await?;
-    Ok(())
-}

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

@@ -14,13 +14,17 @@ use collections::{BTreeMap, HashMap, HashSet};
 use dashmap::DashMap;
 use futures::StreamExt;
 use rand::{prelude::StdRng, Rng, SeedableRng};
-use rpc::{proto, ConnectionId};
+use rpc::{
+    proto::{self},
+    ConnectionId,
+};
 use sea_orm::{
-    entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection,
-    DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType,
-    QueryOrder, QuerySelect, Statement, TransactionTrait,
+    entity::prelude::*,
+    sea_query::{Alias, Expr, OnConflict, Query},
+    ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr,
+    FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement,
+    TransactionTrait,
 };
-use sea_query::{Alias, Expr, OnConflict, Query};
 use serde::{Deserialize, Serialize};
 use sqlx::{
     migrate::{Migrate, Migration, MigrationSource},
@@ -43,6 +47,8 @@ pub use ids::*;
 pub use sea_orm::ConnectOptions;
 pub use tables::user::Model as User;
 
+use self::queries::channels::ChannelGraph;
+
 pub struct Database {
     options: ConnectOptions,
     pool: DatabaseConnection,
@@ -57,6 +63,7 @@ pub struct Database {
 // separate files in the `queries` folder.
 impl Database {
     pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
+        sqlx::any::install_default_drivers();
         Ok(Self {
             options: options.clone(),
             pool: sea_orm::Database::connect(options).await?,
@@ -114,7 +121,7 @@ impl Database {
         Ok(new_migrations)
     }
 
-    async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
+    pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
     where
         F: Send + Fn(TransactionHandle) -> Fut,
         Fut: Send + Future<Output = Result<T>>,
@@ -316,7 +323,7 @@ fn is_serialization_error(error: &Error) -> bool {
     }
 }
 
-struct TransactionHandle(Arc<Option<DatabaseTransaction>>);
+pub struct TransactionHandle(Arc<Option<DatabaseTransaction>>);
 
 impl Deref for TransactionHandle {
     type Target = DatabaseTransaction;
@@ -421,18 +428,19 @@ pub struct NewUserResult {
     pub signup_device_id: Option<String>,
 }
 
-#[derive(FromQueryResult, Debug, PartialEq)]
+#[derive(FromQueryResult, Debug, PartialEq, Eq, Hash)]
 pub struct Channel {
     pub id: ChannelId,
     pub name: String,
-    pub parent_id: Option<ChannelId>,
 }
 
 #[derive(Debug, PartialEq)]
 pub struct ChannelsForUser {
-    pub channels: Vec<Channel>,
+    pub channels: ChannelGraph,
     pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
     pub channels_with_admin_privileges: HashSet<ChannelId>,
+    pub unseen_buffer_changes: Vec<proto::UnseenChannelBufferChange>,
+    pub channel_messages: Vec<proto::UnseenChannelMessage>,
 }
 
 #[derive(Debug)]
@@ -506,7 +514,7 @@ pub struct RefreshedRoom {
 
 pub struct RefreshedChannelBuffer {
     pub connection_ids: Vec<ConnectionId>,
-    pub removed_collaborators: Vec<proto::RemoveChannelBufferCollaborator>,
+    pub collaborators: Vec<proto::Collaborator>,
 }
 
 pub struct Project {

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

@@ -1,6 +1,5 @@
 use crate::Result;
-use sea_orm::DbErr;
-use sea_query::{Value, ValueTypeErr};
+use sea_orm::{entity::prelude::*, DbErr};
 use serde::{Deserialize, Serialize};
 
 macro_rules! id_type {
@@ -17,6 +16,7 @@ macro_rules! id_type {
             Hash,
             Serialize,
             Deserialize,
+            DeriveValueType,
         )]
         #[serde(transparent)]
         pub struct $name(pub i32);
@@ -42,40 +42,6 @@ macro_rules! id_type {
             }
         }
 
-        impl From<$name> for sea_query::Value {
-            fn from(value: $name) -> Self {
-                sea_query::Value::Int(Some(value.0))
-            }
-        }
-
-        impl sea_orm::TryGetable for $name {
-            fn try_get(
-                res: &sea_orm::QueryResult,
-                pre: &str,
-                col: &str,
-            ) -> Result<Self, sea_orm::TryGetError> {
-                Ok(Self(i32::try_get(res, pre, col)?))
-            }
-        }
-
-        impl sea_query::ValueType for $name {
-            fn try_from(v: Value) -> Result<Self, sea_query::ValueTypeErr> {
-                Ok(Self(value_to_integer(v)?))
-            }
-
-            fn type_name() -> String {
-                stringify!($name).into()
-            }
-
-            fn array_type() -> sea_query::ArrayType {
-                sea_query::ArrayType::Int
-            }
-
-            fn column_type() -> sea_query::ColumnType {
-                sea_query::ColumnType::Integer(None)
-            }
-        }
-
         impl sea_orm::TryFromU64 for $name {
             fn try_from_u64(n: u64) -> Result<Self, DbErr> {
                 Ok(Self(n.try_into().map_err(|_| {
@@ -88,7 +54,7 @@ macro_rules! id_type {
             }
         }
 
-        impl sea_query::Nullable for $name {
+        impl sea_orm::sea_query::Nullable for $name {
             fn null() -> Value {
                 Value::Int(None)
             }
@@ -96,24 +62,12 @@ macro_rules! id_type {
     };
 }
 
-fn value_to_integer(v: Value) -> Result<i32, ValueTypeErr> {
-    match v {
-        Value::TinyInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
-        Value::SmallInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
-        Value::Int(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
-        Value::BigInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
-        Value::TinyUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
-        Value::SmallUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
-        Value::Unsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
-        Value::BigUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
-        _ => Err(ValueTypeErr),
-    }
-}
-
 id_type!(BufferId);
 id_type!(AccessTokenId);
+id_type!(ChannelChatParticipantId);
 id_type!(ChannelId);
 id_type!(ChannelMemberId);
+id_type!(MessageId);
 id_type!(ContactId);
 id_type!(FollowerId);
 id_type!(RoomId);

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

@@ -4,8 +4,8 @@ pub mod access_tokens;
 pub mod buffers;
 pub mod channels;
 pub mod contacts;
+pub mod messages;
 pub mod projects;
 pub mod rooms;
 pub mod servers;
-pub mod signups;
 pub mod users;

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

@@ -2,6 +2,12 @@ use super::*;
 use prost::Message;
 use text::{EditOperation, UndoOperation};
 
+pub struct LeftChannelBuffer {
+    pub channel_id: ChannelId,
+    pub collaborators: Vec<proto::Collaborator>,
+    pub connections: Vec<ConnectionId>,
+}
+
 impl Database {
     pub async fn join_channel_buffer(
         &self,
@@ -68,7 +74,32 @@ impl Database {
             .await?;
             collaborators.push(collaborator);
 
-            let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?;
+            let (base_text, operations, max_operation) =
+                self.get_buffer_state(&buffer, &tx).await?;
+
+            // Save the last observed operation
+            if let Some(op) = max_operation {
+                observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel {
+                    user_id: ActiveValue::Set(user_id),
+                    buffer_id: ActiveValue::Set(buffer.id),
+                    epoch: ActiveValue::Set(op.epoch),
+                    lamport_timestamp: ActiveValue::Set(op.lamport_timestamp),
+                    replica_id: ActiveValue::Set(op.replica_id),
+                })
+                .on_conflict(
+                    OnConflict::columns([
+                        observed_buffer_edits::Column::UserId,
+                        observed_buffer_edits::Column::BufferId,
+                    ])
+                    .update_columns([
+                        observed_buffer_edits::Column::Epoch,
+                        observed_buffer_edits::Column::LamportTimestamp,
+                    ])
+                    .to_owned(),
+                )
+                .exec(&*tx)
+                .await?;
+            }
 
             Ok(proto::JoinChannelBufferResponse {
                 buffer_id: buffer.id.to_proto(),
@@ -204,23 +235,26 @@ impl Database {
         server_id: ServerId,
     ) -> Result<RefreshedChannelBuffer> {
         self.transaction(|tx| async move {
-            let collaborators = channel_buffer_collaborator::Entity::find()
+            let db_collaborators = channel_buffer_collaborator::Entity::find()
                 .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
                 .all(&*tx)
                 .await?;
 
             let mut connection_ids = Vec::new();
-            let mut removed_collaborators = Vec::new();
+            let mut collaborators = Vec::new();
             let mut collaborator_ids_to_remove = Vec::new();
-            for collaborator in &collaborators {
-                if !collaborator.connection_lost && collaborator.connection_server_id == server_id {
-                    connection_ids.push(collaborator.connection());
+            for db_collaborator in &db_collaborators {
+                if !db_collaborator.connection_lost
+                    && db_collaborator.connection_server_id == server_id
+                {
+                    connection_ids.push(db_collaborator.connection());
+                    collaborators.push(proto::Collaborator {
+                        peer_id: Some(db_collaborator.connection().into()),
+                        replica_id: db_collaborator.replica_id.0 as u32,
+                        user_id: db_collaborator.user_id.to_proto(),
+                    })
                 } else {
-                    removed_collaborators.push(proto::RemoveChannelBufferCollaborator {
-                        channel_id: channel_id.to_proto(),
-                        peer_id: Some(collaborator.connection().into()),
-                    });
-                    collaborator_ids_to_remove.push(collaborator.id);
+                    collaborator_ids_to_remove.push(db_collaborator.id);
                 }
             }
 
@@ -231,7 +265,7 @@ impl Database {
 
             Ok(RefreshedChannelBuffer {
                 connection_ids,
-                removed_collaborators,
+                collaborators,
             })
         })
         .await
@@ -241,7 +275,7 @@ impl Database {
         &self,
         channel_id: ChannelId,
         connection: ConnectionId,
-    ) -> Result<Vec<ConnectionId>> {
+    ) -> Result<LeftChannelBuffer> {
         self.transaction(|tx| async move {
             self.leave_channel_buffer_internal(channel_id, connection, &*tx)
                 .await
@@ -249,10 +283,33 @@ impl Database {
         .await
     }
 
+    pub async fn channel_buffer_connection_lost(
+        &self,
+        connection: ConnectionId,
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        channel_buffer_collaborator::Entity::update_many()
+            .filter(
+                Condition::all()
+                    .add(channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32))
+                    .add(
+                        channel_buffer_collaborator::Column::ConnectionServerId
+                            .eq(connection.owner_id as i32),
+                    ),
+            )
+            .set(channel_buffer_collaborator::ActiveModel {
+                connection_lost: ActiveValue::set(true),
+                ..Default::default()
+            })
+            .exec(&*tx)
+            .await?;
+        Ok(())
+    }
+
     pub async fn leave_channel_buffers(
         &self,
         connection: ConnectionId,
-    ) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> {
+    ) -> Result<Vec<LeftChannelBuffer>> {
         self.transaction(|tx| async move {
             #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
             enum QueryChannelIds {
@@ -271,10 +328,10 @@ impl Database {
 
             let mut result = Vec::new();
             for channel_id in channel_ids {
-                let collaborators = self
+                let left_channel_buffer = self
                     .leave_channel_buffer_internal(channel_id, connection, &*tx)
                     .await?;
-                result.push((channel_id, collaborators));
+                result.push(left_channel_buffer);
             }
 
             Ok(result)
@@ -287,7 +344,7 @@ impl Database {
         channel_id: ChannelId,
         connection: ConnectionId,
         tx: &DatabaseTransaction,
-    ) -> Result<Vec<ConnectionId>> {
+    ) -> Result<LeftChannelBuffer> {
         let result = channel_buffer_collaborator::Entity::delete_many()
             .filter(
                 Condition::all()
@@ -304,6 +361,7 @@ impl Database {
             Err(anyhow!("not a collaborator on this project"))?;
         }
 
+        let mut collaborators = Vec::new();
         let mut connections = Vec::new();
         let mut rows = channel_buffer_collaborator::Entity::find()
             .filter(
@@ -313,19 +371,26 @@ impl Database {
             .await?;
         while let Some(row) = rows.next().await {
             let row = row?;
-            connections.push(ConnectionId {
-                id: row.connection_id as u32,
-                owner_id: row.connection_server_id.0 as u32,
+            let connection = row.connection();
+            connections.push(connection);
+            collaborators.push(proto::Collaborator {
+                peer_id: Some(connection.into()),
+                replica_id: row.replica_id.0 as u32,
+                user_id: row.user_id.to_proto(),
             });
         }
 
         drop(rows);
 
-        if connections.is_empty() {
+        if collaborators.is_empty() {
             self.snapshot_channel_buffer(channel_id, &tx).await?;
         }
 
-        Ok(connections)
+        Ok(LeftChannelBuffer {
+            channel_id,
+            collaborators,
+            connections,
+        })
     }
 
     pub async fn get_channel_buffer_collaborators(
@@ -333,33 +398,46 @@ impl Database {
         channel_id: ChannelId,
     ) -> Result<Vec<UserId>> {
         self.transaction(|tx| async move {
-            #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
-            enum QueryUserIds {
-                UserId,
-            }
-
-            let users: Vec<UserId> = channel_buffer_collaborator::Entity::find()
-                .select_only()
-                .column(channel_buffer_collaborator::Column::UserId)
-                .filter(
-                    Condition::all()
-                        .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
-                )
-                .into_values::<_, QueryUserIds>()
-                .all(&*tx)
-                .await?;
-
-            Ok(users)
+            self.get_channel_buffer_collaborators_internal(channel_id, &*tx)
+                .await
         })
         .await
     }
 
+    async fn get_channel_buffer_collaborators_internal(
+        &self,
+        channel_id: ChannelId,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<UserId>> {
+        #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
+        enum QueryUserIds {
+            UserId,
+        }
+
+        let users: Vec<UserId> = channel_buffer_collaborator::Entity::find()
+            .select_only()
+            .column(channel_buffer_collaborator::Column::UserId)
+            .filter(
+                Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
+            )
+            .into_values::<_, QueryUserIds>()
+            .all(&*tx)
+            .await?;
+
+        Ok(users)
+    }
+
     pub async fn update_channel_buffer(
         &self,
         channel_id: ChannelId,
         user: UserId,
         operations: &[proto::Operation],
-    ) -> Result<Vec<ConnectionId>> {
+    ) -> Result<(
+        Vec<ConnectionId>,
+        Vec<UserId>,
+        i32,
+        Vec<proto::VectorClockEntry>,
+    )> {
         self.transaction(move |tx| async move {
             self.check_user_is_channel_member(channel_id, user, &*tx)
                 .await?;
@@ -378,7 +456,38 @@ impl Database {
                 .iter()
                 .filter_map(|op| operation_to_storage(op, &buffer, serialization_version))
                 .collect::<Vec<_>>();
+
+            let mut channel_members;
+            let max_version;
+
             if !operations.is_empty() {
+                let max_operation = operations
+                    .iter()
+                    .max_by_key(|op| (op.lamport_timestamp.as_ref(), op.replica_id.as_ref()))
+                    .unwrap();
+
+                max_version = vec![proto::VectorClockEntry {
+                    replica_id: *max_operation.replica_id.as_ref() as u32,
+                    timestamp: *max_operation.lamport_timestamp.as_ref() as u32,
+                }];
+
+                // get current channel participants and save the max operation above
+                self.save_max_operation(
+                    user,
+                    buffer.id,
+                    buffer.epoch,
+                    *max_operation.replica_id.as_ref(),
+                    *max_operation.lamport_timestamp.as_ref(),
+                    &*tx,
+                )
+                .await?;
+
+                channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
+                let collaborators = self
+                    .get_channel_buffer_collaborators_internal(channel_id, &*tx)
+                    .await?;
+                channel_members.retain(|member| !collaborators.contains(member));
+
                 buffer_operation::Entity::insert_many(operations)
                     .on_conflict(
                         OnConflict::columns([
@@ -392,6 +501,9 @@ impl Database {
                     )
                     .exec(&*tx)
                     .await?;
+            } else {
+                channel_members = Vec::new();
+                max_version = Vec::new();
             }
 
             let mut connections = Vec::new();
@@ -410,11 +522,53 @@ impl Database {
                 });
             }
 
-            Ok(connections)
+            Ok((connections, channel_members, buffer.epoch, max_version))
         })
         .await
     }
 
+    async fn save_max_operation(
+        &self,
+        user_id: UserId,
+        buffer_id: BufferId,
+        epoch: i32,
+        replica_id: i32,
+        lamport_timestamp: i32,
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        use observed_buffer_edits::Column;
+
+        observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel {
+            user_id: ActiveValue::Set(user_id),
+            buffer_id: ActiveValue::Set(buffer_id),
+            epoch: ActiveValue::Set(epoch),
+            replica_id: ActiveValue::Set(replica_id),
+            lamport_timestamp: ActiveValue::Set(lamport_timestamp),
+        })
+        .on_conflict(
+            OnConflict::columns([Column::UserId, Column::BufferId])
+                .update_columns([Column::Epoch, Column::LamportTimestamp, Column::ReplicaId])
+                .action_cond_where(
+                    Condition::any().add(Column::Epoch.lt(epoch)).add(
+                        Condition::all().add(Column::Epoch.eq(epoch)).add(
+                            Condition::any()
+                                .add(Column::LamportTimestamp.lt(lamport_timestamp))
+                                .add(
+                                    Column::LamportTimestamp
+                                        .eq(lamport_timestamp)
+                                        .and(Column::ReplicaId.lt(replica_id)),
+                                ),
+                        ),
+                    ),
+                )
+                .to_owned(),
+        )
+        .exec_without_returning(tx)
+        .await?;
+
+        Ok(())
+    }
+
     async fn get_buffer_operation_serialization_version(
         &self,
         buffer_id: BufferId,
@@ -432,7 +586,7 @@ impl Database {
             .ok_or_else(|| anyhow!("missing buffer snapshot"))?)
     }
 
-    async fn get_channel_buffer(
+    pub async fn get_channel_buffer(
         &self,
         channel_id: ChannelId,
         tx: &DatabaseTransaction,
@@ -451,7 +605,11 @@ impl Database {
         &self,
         buffer: &buffer::Model,
         tx: &DatabaseTransaction,
-    ) -> Result<(String, Vec<proto::Operation>)> {
+    ) -> Result<(
+        String,
+        Vec<proto::Operation>,
+        Option<buffer_operation::Model>,
+    )> {
         let id = buffer.id;
         let (base_text, version) = if buffer.epoch > 0 {
             let snapshot = buffer_snapshot::Entity::find()
@@ -476,16 +634,28 @@ impl Database {
                     .eq(id)
                     .and(buffer_operation::Column::Epoch.eq(buffer.epoch)),
             )
+            .order_by_asc(buffer_operation::Column::LamportTimestamp)
+            .order_by_asc(buffer_operation::Column::ReplicaId)
             .stream(&*tx)
             .await?;
+
         let mut operations = Vec::new();
+        let mut last_row = None;
         while let Some(row) = rows.next().await {
+            let row = row?;
+            last_row = Some(buffer_operation::Model {
+                buffer_id: row.buffer_id,
+                epoch: row.epoch,
+                lamport_timestamp: row.lamport_timestamp,
+                replica_id: row.lamport_timestamp,
+                value: Default::default(),
+            });
             operations.push(proto::Operation {
-                variant: Some(operation_from_storage(row?, version)?),
-            })
+                variant: Some(operation_from_storage(row, version)?),
+            });
         }
 
-        Ok((base_text, operations))
+        Ok((base_text, operations, last_row))
     }
 
     async fn snapshot_channel_buffer(
@@ -494,7 +664,7 @@ impl Database {
         tx: &DatabaseTransaction,
     ) -> Result<()> {
         let buffer = self.get_channel_buffer(channel_id, tx).await?;
-        let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?;
+        let (base_text, operations, _) = self.get_buffer_state(&buffer, tx).await?;
         if operations.is_empty() {
             return Ok(());
         }
@@ -527,6 +697,150 @@ impl Database {
 
         Ok(())
     }
+
+    pub async fn observe_buffer_version(
+        &self,
+        buffer_id: BufferId,
+        user_id: UserId,
+        epoch: i32,
+        version: &[proto::VectorClockEntry],
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            // For now, combine concurrent operations.
+            let Some(component) = version.iter().max_by_key(|version| version.timestamp) else {
+                return Ok(());
+            };
+            self.save_max_operation(
+                user_id,
+                buffer_id,
+                epoch,
+                component.replica_id as i32,
+                component.timestamp as i32,
+                &*tx,
+            )
+            .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn unseen_channel_buffer_changes(
+        &self,
+        user_id: UserId,
+        channel_ids: &[ChannelId],
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<proto::UnseenChannelBufferChange>> {
+        #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
+        enum QueryIds {
+            ChannelId,
+            Id,
+        }
+
+        let mut channel_ids_by_buffer_id = HashMap::default();
+        let mut rows = buffer::Entity::find()
+            .filter(buffer::Column::ChannelId.is_in(channel_ids.iter().copied()))
+            .stream(&*tx)
+            .await?;
+        while let Some(row) = rows.next().await {
+            let row = row?;
+            channel_ids_by_buffer_id.insert(row.id, row.channel_id);
+        }
+        drop(rows);
+
+        let mut observed_edits_by_buffer_id = HashMap::default();
+        let mut rows = observed_buffer_edits::Entity::find()
+            .filter(observed_buffer_edits::Column::UserId.eq(user_id))
+            .filter(
+                observed_buffer_edits::Column::BufferId
+                    .is_in(channel_ids_by_buffer_id.keys().copied()),
+            )
+            .stream(&*tx)
+            .await?;
+        while let Some(row) = rows.next().await {
+            let row = row?;
+            observed_edits_by_buffer_id.insert(row.buffer_id, row);
+        }
+        drop(rows);
+
+        let latest_operations = self
+            .get_latest_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx)
+            .await?;
+
+        let mut changes = Vec::default();
+        for latest in latest_operations {
+            if let Some(observed) = observed_edits_by_buffer_id.get(&latest.buffer_id) {
+                if (
+                    observed.epoch,
+                    observed.lamport_timestamp,
+                    observed.replica_id,
+                ) >= (latest.epoch, latest.lamport_timestamp, latest.replica_id)
+                {
+                    continue;
+                }
+            }
+
+            if let Some(channel_id) = channel_ids_by_buffer_id.get(&latest.buffer_id) {
+                changes.push(proto::UnseenChannelBufferChange {
+                    channel_id: channel_id.to_proto(),
+                    epoch: latest.epoch as u64,
+                    version: vec![proto::VectorClockEntry {
+                        replica_id: latest.replica_id as u32,
+                        timestamp: latest.lamport_timestamp as u32,
+                    }],
+                });
+            }
+        }
+
+        Ok(changes)
+    }
+
+    pub async fn get_latest_operations_for_buffers(
+        &self,
+        buffer_ids: impl IntoIterator<Item = BufferId>,
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<buffer_operation::Model>> {
+        let mut values = String::new();
+        for id in buffer_ids {
+            if !values.is_empty() {
+                values.push_str(", ");
+            }
+            write!(&mut values, "({})", id).unwrap();
+        }
+
+        if values.is_empty() {
+            return Ok(Vec::default());
+        }
+
+        let sql = format!(
+            r#"
+            SELECT
+                *
+            FROM
+            (
+                SELECT
+                    *,
+                    row_number() OVER (
+                        PARTITION BY buffer_id
+                        ORDER BY
+                            epoch DESC,
+                            lamport_timestamp DESC,
+                            replica_id DESC
+                    ) as row_number
+                FROM buffer_operations
+                WHERE
+                    buffer_id in ({values})
+            ) AS last_operations
+            WHERE
+                row_number = 1
+            "#,
+        );
+
+        let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
+        Ok(buffer_operation::Entity::find()
+            .from_raw_sql(stmt)
+            .all(&*tx)
+            .await?)
+    }
 }
 
 fn operation_to_storage(

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

@@ -1,4 +1,8 @@
 use super::*;
+use rpc::proto::ChannelEdge;
+use smallvec::SmallVec;
+
+type ChannelDescendants = HashMap<ChannelId, SmallSet<ChannelId>>;
 
 impl Database {
     #[cfg(test)]
@@ -46,7 +50,6 @@ impl Database {
             .insert(&*tx)
             .await?;
 
-            let channel_paths_stmt;
             if let Some(parent) = parent {
                 let sql = r#"
                     INSERT INTO channel_paths
@@ -58,7 +61,7 @@ impl Database {
                     WHERE
                         channel_id = $3
                 "#;
-                channel_paths_stmt = Statement::from_sql_and_values(
+                let channel_paths_stmt = Statement::from_sql_and_values(
                     self.pool.get_database_backend(),
                     sql,
                     [
@@ -100,7 +103,7 @@ impl Database {
         .await
     }
 
-    pub async fn remove_channel(
+    pub async fn delete_channel(
         &self,
         channel_id: ChannelId,
         user_id: UserId,
@@ -149,6 +152,19 @@ impl Database {
                 .exec(&*tx)
                 .await?;
 
+            // Delete any other paths that include this channel
+            let sql = r#"
+                    DELETE FROM channel_paths
+                    WHERE
+                        id_path LIKE '%' || $1 || '%'
+                "#;
+            let channel_paths_stmt = Statement::from_sql_and_values(
+                self.pool.get_database_backend(),
+                sql,
+                [channel_id.to_proto().into()],
+            );
+            tx.execute(channel_paths_stmt).await?;
+
             Ok((channels_to_remove.into_keys().collect(), members_to_notify))
         })
         .await
@@ -310,7 +326,6 @@ impl Database {
                 .map(|channel| Channel {
                     id: channel.id,
                     name: channel.name,
-                    parent_id: None,
                 })
                 .collect();
 
@@ -319,6 +334,49 @@ impl Database {
         .await
     }
 
+    async fn get_channel_graph(
+        &self,
+        parents_by_child_id: ChannelDescendants,
+        trim_dangling_parents: bool,
+        tx: &DatabaseTransaction,
+    ) -> Result<ChannelGraph> {
+        let mut channels = Vec::with_capacity(parents_by_child_id.len());
+        {
+            let mut rows = channel::Entity::find()
+                .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied()))
+                .stream(&*tx)
+                .await?;
+            while let Some(row) = rows.next().await {
+                let row = row?;
+                channels.push(Channel {
+                    id: row.id,
+                    name: row.name,
+                })
+            }
+        }
+
+        let mut edges = Vec::with_capacity(parents_by_child_id.len());
+        for (channel, parents) in parents_by_child_id.iter() {
+            for parent in parents.into_iter() {
+                if trim_dangling_parents {
+                    if parents_by_child_id.contains_key(parent) {
+                        edges.push(ChannelEdge {
+                            channel_id: channel.to_proto(),
+                            parent_id: parent.to_proto(),
+                        });
+                    }
+                } else {
+                    edges.push(ChannelEdge {
+                        channel_id: channel.to_proto(),
+                        parent_id: parent.to_proto(),
+                    });
+                }
+            }
+        }
+
+        Ok(ChannelGraph { channels, edges })
+    }
+
     pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
         self.transaction(|tx| async move {
             let tx = tx;
@@ -332,61 +390,94 @@ impl Database {
                 .all(&*tx)
                 .await?;
 
-            let parents_by_child_id = self
-                .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx)
+            self.get_user_channels(user_id, channel_memberships, &tx)
+                .await
+        })
+        .await
+    }
+
+    pub async fn get_channel_for_user(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+    ) -> Result<ChannelsForUser> {
+        self.transaction(|tx| async move {
+            let tx = tx;
+
+            let channel_membership = channel_member::Entity::find()
+                .filter(
+                    channel_member::Column::UserId
+                        .eq(user_id)
+                        .and(channel_member::Column::ChannelId.eq(channel_id))
+                        .and(channel_member::Column::Accepted.eq(true)),
+                )
+                .all(&*tx)
                 .await?;
 
-            let channels_with_admin_privileges = channel_memberships
-                .iter()
-                .filter_map(|membership| membership.admin.then_some(membership.channel_id))
-                .collect();
+            self.get_user_channels(user_id, channel_membership, &tx)
+                .await
+        })
+        .await
+    }
 
-            let mut channels = Vec::with_capacity(parents_by_child_id.len());
-            {
-                let mut rows = channel::Entity::find()
-                    .filter(channel::Column::Id.is_in(parents_by_child_id.keys().copied()))
-                    .stream(&*tx)
-                    .await?;
-                while let Some(row) = rows.next().await {
-                    let row = row?;
-                    channels.push(Channel {
-                        id: row.id,
-                        name: row.name,
-                        parent_id: parents_by_child_id.get(&row.id).copied().flatten(),
-                    });
-                }
-            }
+    pub async fn get_user_channels(
+        &self,
+        user_id: UserId,
+        channel_memberships: Vec<channel_member::Model>,
+        tx: &DatabaseTransaction,
+    ) -> Result<ChannelsForUser> {
+        let parents_by_child_id = self
+            .get_channel_descendants(channel_memberships.iter().map(|m| m.channel_id), &*tx)
+            .await?;
 
-            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
-            enum QueryUserIdsAndChannelIds {
-                ChannelId,
-                UserId,
-            }
+        let channels_with_admin_privileges = channel_memberships
+            .iter()
+            .filter_map(|membership| membership.admin.then_some(membership.channel_id))
+            .collect();
 
-            let mut channel_participants: HashMap<ChannelId, Vec<UserId>> = HashMap::default();
-            {
-                let mut rows = room_participant::Entity::find()
-                    .inner_join(room::Entity)
-                    .filter(room::Column::ChannelId.is_in(channels.iter().map(|c| c.id)))
-                    .select_only()
-                    .column(room::Column::ChannelId)
-                    .column(room_participant::Column::UserId)
-                    .into_values::<_, QueryUserIdsAndChannelIds>()
-                    .stream(&*tx)
-                    .await?;
-                while let Some(row) = rows.next().await {
-                    let row: (ChannelId, UserId) = row?;
-                    channel_participants.entry(row.0).or_default().push(row.1)
-                }
+        let graph = self
+            .get_channel_graph(parents_by_child_id, true, &tx)
+            .await?;
+
+        #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+        enum QueryUserIdsAndChannelIds {
+            ChannelId,
+            UserId,
+        }
+
+        let mut channel_participants: HashMap<ChannelId, Vec<UserId>> = HashMap::default();
+        {
+            let mut rows = room_participant::Entity::find()
+                .inner_join(room::Entity)
+                .filter(room::Column::ChannelId.is_in(graph.channels.iter().map(|c| c.id)))
+                .select_only()
+                .column(room::Column::ChannelId)
+                .column(room_participant::Column::UserId)
+                .into_values::<_, QueryUserIdsAndChannelIds>()
+                .stream(&*tx)
+                .await?;
+            while let Some(row) = rows.next().await {
+                let row: (ChannelId, UserId) = row?;
+                channel_participants.entry(row.0).or_default().push(row.1)
             }
+        }
+
+        let channel_ids = graph.channels.iter().map(|c| c.id).collect::<Vec<_>>();
+        let channel_buffer_changes = self
+            .unseen_channel_buffer_changes(user_id, &channel_ids, &*tx)
+            .await?;
 
-            Ok(ChannelsForUser {
-                channels,
-                channel_participants,
-                channels_with_admin_privileges,
-            })
+        let unseen_messages = self
+            .unseen_channel_messages(user_id, &channel_ids, &*tx)
+            .await?;
+
+        Ok(ChannelsForUser {
+            channels: graph,
+            channel_participants,
+            channels_with_admin_privileges,
+            unseen_buffer_changes: channel_buffer_changes,
+            channel_messages: unseen_messages,
         })
-        .await
     }
 
     pub async fn get_channel_members(&self, id: ChannelId) -> Result<Vec<UserId>> {
@@ -559,6 +650,7 @@ impl Database {
         Ok(())
     }
 
+    /// Returns the channel ancestors, deepest first
     pub async fn get_channel_ancestors(
         &self,
         channel_id: ChannelId,
@@ -566,6 +658,7 @@ impl Database {
     ) -> Result<Vec<ChannelId>> {
         let paths = channel_path::Entity::find()
             .filter(channel_path::Column::ChannelId.eq(channel_id))
+            .order_by(channel_path::Column::IdPath, sea_orm::Order::Desc)
             .all(tx)
             .await?;
         let mut channel_ids = Vec::new();
@@ -582,11 +675,25 @@ impl Database {
         Ok(channel_ids)
     }
 
+    /// Returns the channel descendants,
+    /// Structured as a map from child ids to their parent ids
+    /// For example, the descendants of 'a' in this DAG:
+    ///
+    ///   /- b -\
+    /// a -- c -- d
+    ///
+    /// would be:
+    /// {
+    ///     a: [],
+    ///     b: [a],
+    ///     c: [a],
+    ///     d: [a, c],
+    /// }
     async fn get_channel_descendants(
         &self,
         channel_ids: impl IntoIterator<Item = ChannelId>,
         tx: &DatabaseTransaction,
-    ) -> Result<HashMap<ChannelId, Option<ChannelId>>> {
+    ) -> Result<ChannelDescendants> {
         let mut values = String::new();
         for id in channel_ids {
             if !values.is_empty() {
@@ -613,7 +720,7 @@ impl Database {
 
         let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
 
-        let mut parents_by_child_id = HashMap::default();
+        let mut parents_by_child_id: ChannelDescendants = HashMap::default();
         let mut paths = channel_path::Entity::find()
             .from_raw_sql(stmt)
             .stream(tx)
@@ -632,7 +739,10 @@ impl Database {
                     parent_id = Some(id);
                 }
             }
-            parents_by_child_id.insert(path.channel_id, parent_id);
+            let entry = parents_by_child_id.entry(path.channel_id).or_default();
+            if let Some(parent_id) = parent_id {
+                entry.insert(parent_id);
+            }
         }
 
         Ok(parents_by_child_id)
@@ -677,7 +787,6 @@ impl Database {
                     Channel {
                         id: channel.id,
                         name: channel.name,
-                        parent_id: None,
                     },
                     is_accepted,
                 )))
@@ -703,9 +812,276 @@ impl Database {
         })
         .await
     }
+
+    // Insert an edge from the given channel to the given other channel.
+    pub async fn link_channel(
+        &self,
+        user: UserId,
+        channel: ChannelId,
+        to: ChannelId,
+    ) -> Result<ChannelGraph> {
+        self.transaction(|tx| async move {
+            // Note that even with these maxed permissions, this linking operation
+            // is still insecure because you can't remove someone's permissions to a
+            // channel if they've linked the channel to one where they're an admin.
+            self.check_user_is_channel_admin(channel, user, &*tx)
+                .await?;
+
+            self.link_channel_internal(user, channel, to, &*tx).await
+        })
+        .await
+    }
+
+    pub async fn link_channel_internal(
+        &self,
+        user: UserId,
+        channel: ChannelId,
+        to: ChannelId,
+        tx: &DatabaseTransaction,
+    ) -> Result<ChannelGraph> {
+        self.check_user_is_channel_admin(to, user, &*tx).await?;
+
+        let paths = channel_path::Entity::find()
+            .filter(channel_path::Column::IdPath.like(&format!("%/{}/%", channel)))
+            .all(tx)
+            .await?;
+
+        let mut new_path_suffixes = HashSet::default();
+        for path in paths {
+            if let Some(start_offset) = path.id_path.find(&format!("/{}/", channel)) {
+                new_path_suffixes.insert((
+                    path.channel_id,
+                    path.id_path[(start_offset + 1)..].to_string(),
+                ));
+            }
+        }
+
+        let paths_to_new_parent = channel_path::Entity::find()
+            .filter(channel_path::Column::ChannelId.eq(to))
+            .all(tx)
+            .await?;
+
+        let mut new_paths = Vec::new();
+        for path in paths_to_new_parent {
+            if path.id_path.contains(&format!("/{}/", channel)) {
+                Err(anyhow!("cycle"))?;
+            }
+
+            new_paths.extend(new_path_suffixes.iter().map(|(channel_id, path_suffix)| {
+                channel_path::ActiveModel {
+                    channel_id: ActiveValue::Set(*channel_id),
+                    id_path: ActiveValue::Set(format!("{}{}", &path.id_path, path_suffix)),
+                }
+            }));
+        }
+
+        channel_path::Entity::insert_many(new_paths)
+            .exec(&*tx)
+            .await?;
+
+        // remove any root edges for the channel we just linked
+        {
+            channel_path::Entity::delete_many()
+                .filter(channel_path::Column::IdPath.like(&format!("/{}/%", channel)))
+                .exec(&*tx)
+                .await?;
+        }
+
+        let mut channel_descendants = self.get_channel_descendants([channel], &*tx).await?;
+        if let Some(channel) = channel_descendants.get_mut(&channel) {
+            // Remove the other parents
+            channel.clear();
+            channel.insert(to);
+        }
+
+        let channels = self
+            .get_channel_graph(channel_descendants, false, &*tx)
+            .await?;
+
+        Ok(channels)
+    }
+
+    /// Unlink a channel from a given parent. This will add in a root edge if
+    /// the channel has no other parents after this operation.
+    pub async fn unlink_channel(
+        &self,
+        user: UserId,
+        channel: ChannelId,
+        from: ChannelId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            // Note that even with these maxed permissions, this linking operation
+            // is still insecure because you can't remove someone's permissions to a
+            // channel if they've linked the channel to one where they're an admin.
+            self.check_user_is_channel_admin(channel, user, &*tx)
+                .await?;
+
+            self.unlink_channel_internal(user, channel, from, &*tx)
+                .await?;
+
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn unlink_channel_internal(
+        &self,
+        user: UserId,
+        channel: ChannelId,
+        from: ChannelId,
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        self.check_user_is_channel_admin(from, user, &*tx).await?;
+
+        let sql = r#"
+            DELETE FROM channel_paths
+            WHERE
+                id_path LIKE '%/' || $1 || '/' || $2 || '/%'
+            RETURNING id_path, channel_id
+        "#;
+
+        let paths = channel_path::Entity::find()
+            .from_raw_sql(Statement::from_sql_and_values(
+                self.pool.get_database_backend(),
+                sql,
+                [from.to_proto().into(), channel.to_proto().into()],
+            ))
+            .all(&*tx)
+            .await?;
+
+        let is_stranded = channel_path::Entity::find()
+            .filter(channel_path::Column::ChannelId.eq(channel))
+            .count(&*tx)
+            .await?
+            == 0;
+
+        // Make sure that there is always at least one path to the channel
+        if is_stranded {
+            let root_paths: Vec<_> = paths
+                .iter()
+                .map(|path| {
+                    let start_offset = path.id_path.find(&format!("/{}/", channel)).unwrap();
+                    channel_path::ActiveModel {
+                        channel_id: ActiveValue::Set(path.channel_id),
+                        id_path: ActiveValue::Set(path.id_path[start_offset..].to_string()),
+                    }
+                })
+                .collect();
+            channel_path::Entity::insert_many(root_paths)
+                .exec(&*tx)
+                .await?;
+        }
+
+        Ok(())
+    }
+
+    /// Move a channel from one parent to another, returns the
+    /// Channels that were moved for notifying clients
+    pub async fn move_channel(
+        &self,
+        user: UserId,
+        channel: ChannelId,
+        from: ChannelId,
+        to: ChannelId,
+    ) -> Result<ChannelGraph> {
+        if from == to {
+            return Ok(ChannelGraph {
+                channels: vec![],
+                edges: vec![],
+            });
+        }
+
+        self.transaction(|tx| async move {
+            self.check_user_is_channel_admin(channel, user, &*tx)
+                .await?;
+
+            let moved_channels = self.link_channel_internal(user, channel, to, &*tx).await?;
+
+            self.unlink_channel_internal(user, channel, from, &*tx)
+                .await?;
+
+            Ok(moved_channels)
+        })
+        .await
+    }
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
 enum QueryUserIds {
     UserId,
 }
+
+#[derive(Debug)]
+pub struct ChannelGraph {
+    pub channels: Vec<Channel>,
+    pub edges: Vec<ChannelEdge>,
+}
+
+impl ChannelGraph {
+    pub fn is_empty(&self) -> bool {
+        self.channels.is_empty() && self.edges.is_empty()
+    }
+}
+
+#[cfg(test)]
+impl PartialEq for ChannelGraph {
+    fn eq(&self, other: &Self) -> bool {
+        // Order independent comparison for tests
+        let channels_set = self.channels.iter().collect::<HashSet<_>>();
+        let other_channels_set = other.channels.iter().collect::<HashSet<_>>();
+        let edges_set = self
+            .edges
+            .iter()
+            .map(|edge| (edge.channel_id, edge.parent_id))
+            .collect::<HashSet<_>>();
+        let other_edges_set = other
+            .edges
+            .iter()
+            .map(|edge| (edge.channel_id, edge.parent_id))
+            .collect::<HashSet<_>>();
+
+        channels_set == other_channels_set && edges_set == other_edges_set
+    }
+}
+
+#[cfg(not(test))]
+impl PartialEq for ChannelGraph {
+    fn eq(&self, other: &Self) -> bool {
+        self.channels == other.channels && self.edges == other.edges
+    }
+}
+
+struct SmallSet<T>(SmallVec<[T; 1]>);
+
+impl<T> Deref for SmallSet<T> {
+    type Target = [T];
+
+    fn deref(&self) -> &Self::Target {
+        self.0.deref()
+    }
+}
+
+impl<T> Default for SmallSet<T> {
+    fn default() -> Self {
+        Self(SmallVec::new())
+    }
+}
+
+impl<T> SmallSet<T> {
+    fn insert(&mut self, value: T) -> bool
+    where
+        T: Ord,
+    {
+        match self.binary_search(&value) {
+            Ok(_) => false,
+            Err(ix) => {
+                self.0.insert(ix, value);
+                true
+            }
+        }
+    }
+
+    fn clear(&mut self) {
+        self.0.clear();
+    }
+}

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

@@ -18,12 +18,12 @@ impl Database {
             let user_b_participant = Alias::new("user_b_participant");
             let mut db_contacts = contact::Entity::find()
                 .column_as(
-                    Expr::tbl(user_a_participant.clone(), room_participant::Column::Id)
+                    Expr::col((user_a_participant.clone(), room_participant::Column::Id))
                         .is_not_null(),
                     "user_a_busy",
                 )
                 .column_as(
-                    Expr::tbl(user_b_participant.clone(), room_participant::Column::Id)
+                    Expr::col((user_b_participant.clone(), room_participant::Column::Id))
                         .is_not_null(),
                     "user_b_busy",
                 )

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

@@ -0,0 +1,347 @@
+use super::*;
+use time::OffsetDateTime;
+
+impl Database {
+    pub async fn join_channel_chat(
+        &self,
+        channel_id: ChannelId,
+        connection_id: ConnectionId,
+        user_id: UserId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            self.check_user_is_channel_member(channel_id, user_id, &*tx)
+                .await?;
+            channel_chat_participant::ActiveModel {
+                id: ActiveValue::NotSet,
+                channel_id: ActiveValue::Set(channel_id),
+                user_id: ActiveValue::Set(user_id),
+                connection_id: ActiveValue::Set(connection_id.id as i32),
+                connection_server_id: ActiveValue::Set(ServerId(connection_id.owner_id as i32)),
+            }
+            .insert(&*tx)
+            .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn channel_chat_connection_lost(
+        &self,
+        connection_id: ConnectionId,
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        channel_chat_participant::Entity::delete_many()
+            .filter(
+                Condition::all()
+                    .add(
+                        channel_chat_participant::Column::ConnectionServerId
+                            .eq(connection_id.owner_id),
+                    )
+                    .add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id)),
+            )
+            .exec(tx)
+            .await?;
+        Ok(())
+    }
+
+    pub async fn leave_channel_chat(
+        &self,
+        channel_id: ChannelId,
+        connection_id: ConnectionId,
+        _user_id: UserId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            channel_chat_participant::Entity::delete_many()
+                .filter(
+                    Condition::all()
+                        .add(
+                            channel_chat_participant::Column::ConnectionServerId
+                                .eq(connection_id.owner_id),
+                        )
+                        .add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id))
+                        .add(channel_chat_participant::Column::ChannelId.eq(channel_id)),
+                )
+                .exec(&*tx)
+                .await?;
+
+            Ok(())
+        })
+        .await
+    }
+
+    pub async fn get_channel_messages(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        count: usize,
+        before_message_id: Option<MessageId>,
+    ) -> Result<Vec<proto::ChannelMessage>> {
+        self.transaction(|tx| async move {
+            self.check_user_is_channel_member(channel_id, user_id, &*tx)
+                .await?;
+
+            let mut condition =
+                Condition::all().add(channel_message::Column::ChannelId.eq(channel_id));
+
+            if let Some(before_message_id) = before_message_id {
+                condition = condition.add(channel_message::Column::Id.lt(before_message_id));
+            }
+
+            let mut rows = channel_message::Entity::find()
+                .filter(condition)
+                .order_by_asc(channel_message::Column::Id)
+                .limit(count as u64)
+                .stream(&*tx)
+                .await?;
+
+            let mut messages = Vec::new();
+            while let Some(row) = rows.next().await {
+                let row = row?;
+                let nonce = row.nonce.as_u64_pair();
+                messages.push(proto::ChannelMessage {
+                    id: row.id.to_proto(),
+                    sender_id: row.sender_id.to_proto(),
+                    body: row.body,
+                    timestamp: row.sent_at.assume_utc().unix_timestamp() as u64,
+                    nonce: Some(proto::Nonce {
+                        upper_half: nonce.0,
+                        lower_half: nonce.1,
+                    }),
+                });
+            }
+            drop(rows);
+            Ok(messages)
+        })
+        .await
+    }
+
+    pub async fn create_channel_message(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        body: &str,
+        timestamp: OffsetDateTime,
+        nonce: u128,
+    ) -> Result<(MessageId, Vec<ConnectionId>, Vec<UserId>)> {
+        self.transaction(|tx| async move {
+            let mut rows = channel_chat_participant::Entity::find()
+                .filter(channel_chat_participant::Column::ChannelId.eq(channel_id))
+                .stream(&*tx)
+                .await?;
+
+            let mut is_participant = false;
+            let mut participant_connection_ids = Vec::new();
+            let mut participant_user_ids = Vec::new();
+            while let Some(row) = rows.next().await {
+                let row = row?;
+                if row.user_id == user_id {
+                    is_participant = true;
+                }
+                participant_user_ids.push(row.user_id);
+                participant_connection_ids.push(row.connection());
+            }
+            drop(rows);
+
+            if !is_participant {
+                Err(anyhow!("not a chat participant"))?;
+            }
+
+            let timestamp = timestamp.to_offset(time::UtcOffset::UTC);
+            let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time());
+
+            let message = channel_message::Entity::insert(channel_message::ActiveModel {
+                channel_id: ActiveValue::Set(channel_id),
+                sender_id: ActiveValue::Set(user_id),
+                body: ActiveValue::Set(body.to_string()),
+                sent_at: ActiveValue::Set(timestamp),
+                nonce: ActiveValue::Set(Uuid::from_u128(nonce)),
+                id: ActiveValue::NotSet,
+            })
+            .on_conflict(
+                OnConflict::column(channel_message::Column::Nonce)
+                    .update_column(channel_message::Column::Nonce)
+                    .to_owned(),
+            )
+            .exec(&*tx)
+            .await?;
+
+            #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
+            enum QueryConnectionId {
+                ConnectionId,
+            }
+
+            // Observe this message for the sender
+            self.observe_channel_message_internal(
+                channel_id,
+                user_id,
+                message.last_insert_id,
+                &*tx,
+            )
+            .await?;
+
+            let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
+            channel_members.retain(|member| !participant_user_ids.contains(member));
+
+            Ok((
+                message.last_insert_id,
+                participant_connection_ids,
+                channel_members,
+            ))
+        })
+        .await
+    }
+
+    pub async fn observe_channel_message(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        message_id: MessageId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
+                .await?;
+            Ok(())
+        })
+        .await
+    }
+
+    async fn observe_channel_message_internal(
+        &self,
+        channel_id: ChannelId,
+        user_id: UserId,
+        message_id: MessageId,
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        observed_channel_messages::Entity::insert(observed_channel_messages::ActiveModel {
+            user_id: ActiveValue::Set(user_id),
+            channel_id: ActiveValue::Set(channel_id),
+            channel_message_id: ActiveValue::Set(message_id),
+        })
+        .on_conflict(
+            OnConflict::columns([
+                observed_channel_messages::Column::ChannelId,
+                observed_channel_messages::Column::UserId,
+            ])
+            .update_column(observed_channel_messages::Column::ChannelMessageId)
+            .action_cond_where(observed_channel_messages::Column::ChannelMessageId.lt(message_id))
+            .to_owned(),
+        )
+        // TODO: Try to upgrade SeaORM so we don't have to do this hack around their bug
+        .exec_without_returning(&*tx)
+        .await?;
+        Ok(())
+    }
+
+    pub async fn unseen_channel_messages(
+        &self,
+        user_id: UserId,
+        channel_ids: &[ChannelId],
+        tx: &DatabaseTransaction,
+    ) -> Result<Vec<proto::UnseenChannelMessage>> {
+        let mut observed_messages_by_channel_id = HashMap::default();
+        let mut rows = observed_channel_messages::Entity::find()
+            .filter(observed_channel_messages::Column::UserId.eq(user_id))
+            .filter(observed_channel_messages::Column::ChannelId.is_in(channel_ids.iter().copied()))
+            .stream(&*tx)
+            .await?;
+
+        while let Some(row) = rows.next().await {
+            let row = row?;
+            observed_messages_by_channel_id.insert(row.channel_id, row);
+        }
+        drop(rows);
+        let mut values = String::new();
+        for id in channel_ids {
+            if !values.is_empty() {
+                values.push_str(", ");
+            }
+            write!(&mut values, "({})", id).unwrap();
+        }
+
+        if values.is_empty() {
+            return Ok(Default::default());
+        }
+
+        let sql = format!(
+            r#"
+            SELECT
+                *
+            FROM (
+                SELECT
+                    *,
+                    row_number() OVER (
+                        PARTITION BY channel_id
+                        ORDER BY id DESC
+                    ) as row_number
+                FROM channel_messages
+                WHERE
+                    channel_id in ({values})
+            ) AS messages
+            WHERE
+                row_number = 1
+            "#,
+        );
+
+        let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
+        let last_messages = channel_message::Model::find_by_statement(stmt)
+            .all(&*tx)
+            .await?;
+
+        let mut changes = Vec::new();
+        for last_message in last_messages {
+            if let Some(observed_message) =
+                observed_messages_by_channel_id.get(&last_message.channel_id)
+            {
+                if observed_message.channel_message_id == last_message.id {
+                    continue;
+                }
+            }
+            changes.push(proto::UnseenChannelMessage {
+                channel_id: last_message.channel_id.to_proto(),
+                message_id: last_message.id.to_proto(),
+            });
+        }
+
+        Ok(changes)
+    }
+
+    pub async fn remove_channel_message(
+        &self,
+        channel_id: ChannelId,
+        message_id: MessageId,
+        user_id: UserId,
+    ) -> Result<Vec<ConnectionId>> {
+        self.transaction(|tx| async move {
+            let mut rows = channel_chat_participant::Entity::find()
+                .filter(channel_chat_participant::Column::ChannelId.eq(channel_id))
+                .stream(&*tx)
+                .await?;
+
+            let mut is_participant = false;
+            let mut participant_connection_ids = Vec::new();
+            while let Some(row) = rows.next().await {
+                let row = row?;
+                if row.user_id == user_id {
+                    is_participant = true;
+                }
+                participant_connection_ids.push(row.connection());
+            }
+            drop(rows);
+
+            if !is_participant {
+                Err(anyhow!("not a chat participant"))?;
+            }
+
+            let result = channel_message::Entity::delete_by_id(message_id)
+                .filter(channel_message::Column::SenderId.eq(user_id))
+                .exec(&*tx)
+                .await?;
+            if result.rows_affected == 0 {
+                Err(anyhow!("no such message"))?;
+            }
+
+            Ok(participant_connection_ids)
+        })
+        .await
+    }
+}

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

@@ -738,7 +738,7 @@ impl Database {
                     Condition::any()
                         .add(
                             Condition::all()
-                                .add(follower::Column::ProjectId.eq(project_id))
+                                .add(follower::Column::ProjectId.eq(Some(project_id)))
                                 .add(
                                     follower::Column::LeaderConnectionServerId
                                         .eq(connection.owner_id),
@@ -747,7 +747,7 @@ impl Database {
                         )
                         .add(
                             Condition::all()
-                                .add(follower::Column::ProjectId.eq(project_id))
+                                .add(follower::Column::ProjectId.eq(Some(project_id)))
                                 .add(
                                     follower::Column::FollowerConnectionServerId
                                         .eq(connection.owner_id),
@@ -862,13 +862,46 @@ impl Database {
         .await
     }
 
+    pub async fn check_room_participants(
+        &self,
+        room_id: RoomId,
+        leader_id: ConnectionId,
+        follower_id: ConnectionId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            use room_participant::Column;
+
+            let count = room_participant::Entity::find()
+                .filter(
+                    Condition::all().add(Column::RoomId.eq(room_id)).add(
+                        Condition::any()
+                            .add(Column::AnsweringConnectionId.eq(leader_id.id as i32).and(
+                                Column::AnsweringConnectionServerId.eq(leader_id.owner_id as i32),
+                            ))
+                            .add(Column::AnsweringConnectionId.eq(follower_id.id as i32).and(
+                                Column::AnsweringConnectionServerId.eq(follower_id.owner_id as i32),
+                            )),
+                    ),
+                )
+                .count(&*tx)
+                .await?;
+
+            if count < 2 {
+                Err(anyhow!("not room participants"))?;
+            }
+
+            Ok(())
+        })
+        .await
+    }
+
     pub async fn follow(
         &self,
+        room_id: RoomId,
         project_id: ProjectId,
         leader_connection: ConnectionId,
         follower_connection: ConnectionId,
     ) -> Result<RoomGuard<proto::Room>> {
-        let room_id = self.room_id_for_project(project_id).await?;
         self.room_transaction(room_id, |tx| async move {
             follower::ActiveModel {
                 room_id: ActiveValue::set(room_id),
@@ -894,15 +927,16 @@ impl Database {
 
     pub async fn unfollow(
         &self,
+        room_id: RoomId,
         project_id: ProjectId,
         leader_connection: ConnectionId,
         follower_connection: ConnectionId,
     ) -> Result<RoomGuard<proto::Room>> {
-        let room_id = self.room_id_for_project(project_id).await?;
         self.room_transaction(room_id, |tx| async move {
             follower::Entity::delete_many()
                 .filter(
                     Condition::all()
+                        .add(follower::Column::RoomId.eq(room_id))
                         .add(follower::Column::ProjectId.eq(project_id))
                         .add(
                             follower::Column::LeaderConnectionServerId

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

@@ -128,6 +128,7 @@ impl Database {
                 calling_connection_server_id: ActiveValue::set(Some(ServerId(
                     connection.owner_id as i32,
                 ))),
+                participant_index: ActiveValue::set(Some(0)),
                 ..Default::default()
             }
             .insert(&*tx)
@@ -152,6 +153,7 @@ impl Database {
                 room_id: ActiveValue::set(room_id),
                 user_id: ActiveValue::set(called_user_id),
                 answering_connection_lost: ActiveValue::set(false),
+                participant_index: ActiveValue::NotSet,
                 calling_user_id: ActiveValue::set(calling_user_id),
                 calling_connection_id: ActiveValue::set(calling_connection.id as i32),
                 calling_connection_server_id: ActiveValue::set(Some(ServerId(
@@ -283,6 +285,26 @@ impl Database {
                 .await?
                 .ok_or_else(|| anyhow!("no such room"))?;
 
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryParticipantIndices {
+                ParticipantIndex,
+            }
+            let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
+                .filter(
+                    room_participant::Column::RoomId
+                        .eq(room_id)
+                        .and(room_participant::Column::ParticipantIndex.is_not_null()),
+                )
+                .select_only()
+                .column(room_participant::Column::ParticipantIndex)
+                .into_values::<_, QueryParticipantIndices>()
+                .all(&*tx)
+                .await?;
+            let mut participant_index = 0;
+            while existing_participant_indices.contains(&participant_index) {
+                participant_index += 1;
+            }
+
             if let Some(channel_id) = channel_id {
                 self.check_user_is_channel_member(channel_id, user_id, &*tx)
                     .await?;
@@ -300,6 +322,7 @@ impl Database {
                     calling_connection_server_id: ActiveValue::set(Some(ServerId(
                         connection.owner_id as i32,
                     ))),
+                    participant_index: ActiveValue::Set(Some(participant_index)),
                     ..Default::default()
                 }])
                 .on_conflict(
@@ -308,6 +331,7 @@ impl Database {
                             room_participant::Column::AnsweringConnectionId,
                             room_participant::Column::AnsweringConnectionServerId,
                             room_participant::Column::AnsweringConnectionLost,
+                            room_participant::Column::ParticipantIndex,
                         ])
                         .to_owned(),
                 )
@@ -322,6 +346,7 @@ impl Database {
                             .add(room_participant::Column::AnsweringConnectionId.is_null()),
                     )
                     .set(room_participant::ActiveModel {
+                        participant_index: ActiveValue::Set(Some(participant_index)),
                         answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
                         answering_connection_server_id: ActiveValue::set(Some(ServerId(
                             connection.owner_id as i32,
@@ -890,54 +915,43 @@ impl Database {
 
     pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> {
         self.transaction(|tx| async move {
-            let participant = room_participant::Entity::find()
-                .filter(
-                    Condition::all()
-                        .add(
-                            room_participant::Column::AnsweringConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            room_participant::Column::AnsweringConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .one(&*tx)
-                .await?;
-
-            if let Some(participant) = participant {
-                room_participant::Entity::update(room_participant::ActiveModel {
-                    answering_connection_lost: ActiveValue::set(true),
-                    ..participant.into_active_model()
-                })
-                .exec(&*tx)
-                .await?;
-            }
-
-            channel_buffer_collaborator::Entity::update_many()
-                .filter(
-                    Condition::all()
-                        .add(
-                            channel_buffer_collaborator::Column::ConnectionId
-                                .eq(connection.id as i32),
-                        )
-                        .add(
-                            channel_buffer_collaborator::Column::ConnectionServerId
-                                .eq(connection.owner_id as i32),
-                        ),
-                )
-                .set(channel_buffer_collaborator::ActiveModel {
-                    connection_lost: ActiveValue::set(true),
-                    ..Default::default()
-                })
-                .exec(&*tx)
+            self.room_connection_lost(connection, &*tx).await?;
+            self.channel_buffer_connection_lost(connection, &*tx)
                 .await?;
-
+            self.channel_chat_connection_lost(connection, &*tx).await?;
             Ok(())
         })
         .await
     }
 
+    pub async fn room_connection_lost(
+        &self,
+        connection: ConnectionId,
+        tx: &DatabaseTransaction,
+    ) -> Result<()> {
+        let participant = room_participant::Entity::find()
+            .filter(
+                Condition::all()
+                    .add(room_participant::Column::AnsweringConnectionId.eq(connection.id as i32))
+                    .add(
+                        room_participant::Column::AnsweringConnectionServerId
+                            .eq(connection.owner_id as i32),
+                    ),
+            )
+            .one(&*tx)
+            .await?;
+
+        if let Some(participant) = participant {
+            room_participant::Entity::update(room_participant::ActiveModel {
+                answering_connection_lost: ActiveValue::set(true),
+                ..participant.into_active_model()
+            })
+            .exec(&*tx)
+            .await?;
+        }
+        Ok(())
+    }
+
     fn build_incoming_call(
         room: &proto::Room,
         called_user_id: UserId,
@@ -971,6 +985,39 @@ impl Database {
         Ok(room)
     }
 
+    pub async fn room_connection_ids(
+        &self,
+        room_id: RoomId,
+        connection_id: ConnectionId,
+    ) -> Result<RoomGuard<HashSet<ConnectionId>>> {
+        self.room_transaction(room_id, |tx| async move {
+            let mut participants = room_participant::Entity::find()
+                .filter(room_participant::Column::RoomId.eq(room_id))
+                .stream(&*tx)
+                .await?;
+
+            let mut is_participant = false;
+            let mut connection_ids = HashSet::default();
+            while let Some(participant) = participants.next().await {
+                let participant = participant?;
+                if let Some(answering_connection) = participant.answering_connection() {
+                    if answering_connection == connection_id {
+                        is_participant = true;
+                    } else {
+                        connection_ids.insert(answering_connection);
+                    }
+                }
+            }
+
+            if !is_participant {
+                Err(anyhow!("not a room participant"))?;
+            }
+
+            Ok(connection_ids)
+        })
+        .await
+    }
+
     async fn get_channel_room(
         &self,
         room_id: RoomId,
@@ -989,10 +1036,15 @@ impl Database {
         let mut pending_participants = Vec::new();
         while let Some(db_participant) = db_participants.next().await {
             let db_participant = db_participant?;
-            if let Some((answering_connection_id, answering_connection_server_id)) = db_participant
-                .answering_connection_id
-                .zip(db_participant.answering_connection_server_id)
-            {
+            if let (
+                Some(answering_connection_id),
+                Some(answering_connection_server_id),
+                Some(participant_index),
+            ) = (
+                db_participant.answering_connection_id,
+                db_participant.answering_connection_server_id,
+                db_participant.participant_index,
+            ) {
                 let location = match (
                     db_participant.location_kind,
                     db_participant.location_project_id,
@@ -1023,6 +1075,7 @@ impl Database {
                         peer_id: Some(answering_connection.into()),
                         projects: Default::default(),
                         location: Some(proto::ParticipantLocation { variant: location }),
+                        participant_index: participant_index as u32,
                     },
                 );
             } else {

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

@@ -1,349 +0,0 @@
-use super::*;
-use hyper::StatusCode;
-
-impl Database {
-    pub async fn create_invite_from_code(
-        &self,
-        code: &str,
-        email_address: &str,
-        device_id: Option<&str>,
-        added_to_mailing_list: bool,
-    ) -> Result<Invite> {
-        self.transaction(|tx| async move {
-            let existing_user = user::Entity::find()
-                .filter(user::Column::EmailAddress.eq(email_address))
-                .one(&*tx)
-                .await?;
-
-            if existing_user.is_some() {
-                Err(anyhow!("email address is already in use"))?;
-            }
-
-            let inviting_user_with_invites = match user::Entity::find()
-                .filter(
-                    user::Column::InviteCode
-                        .eq(code)
-                        .and(user::Column::InviteCount.gt(0)),
-                )
-                .one(&*tx)
-                .await?
-            {
-                Some(inviting_user) => inviting_user,
-                None => {
-                    return Err(Error::Http(
-                        StatusCode::UNAUTHORIZED,
-                        "unable to find an invite code with invites remaining".to_string(),
-                    ))?
-                }
-            };
-            user::Entity::update_many()
-                .filter(
-                    user::Column::Id
-                        .eq(inviting_user_with_invites.id)
-                        .and(user::Column::InviteCount.gt(0)),
-                )
-                .col_expr(
-                    user::Column::InviteCount,
-                    Expr::col(user::Column::InviteCount).sub(1),
-                )
-                .exec(&*tx)
-                .await?;
-
-            let signup = signup::Entity::insert(signup::ActiveModel {
-                email_address: ActiveValue::set(email_address.into()),
-                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
-                email_confirmation_sent: ActiveValue::set(false),
-                inviting_user_id: ActiveValue::set(Some(inviting_user_with_invites.id)),
-                platform_linux: ActiveValue::set(false),
-                platform_mac: ActiveValue::set(false),
-                platform_windows: ActiveValue::set(false),
-                platform_unknown: ActiveValue::set(true),
-                device_id: ActiveValue::set(device_id.map(|device_id| device_id.into())),
-                added_to_mailing_list: ActiveValue::set(added_to_mailing_list),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::column(signup::Column::EmailAddress)
-                    .update_column(signup::Column::InvitingUserId)
-                    .to_owned(),
-            )
-            .exec_with_returning(&*tx)
-            .await?;
-
-            Ok(Invite {
-                email_address: signup.email_address,
-                email_confirmation_code: signup.email_confirmation_code,
-            })
-        })
-        .await
-    }
-
-    pub async fn create_user_from_invite(
-        &self,
-        invite: &Invite,
-        user: NewUserParams,
-    ) -> Result<Option<NewUserResult>> {
-        self.transaction(|tx| async {
-            let tx = tx;
-            let signup = signup::Entity::find()
-                .filter(
-                    signup::Column::EmailAddress
-                        .eq(invite.email_address.as_str())
-                        .and(
-                            signup::Column::EmailConfirmationCode
-                                .eq(invite.email_confirmation_code.as_str()),
-                        ),
-                )
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "no such invite".to_string()))?;
-
-            if signup.user_id.is_some() {
-                return Ok(None);
-            }
-
-            let user = user::Entity::insert(user::ActiveModel {
-                email_address: ActiveValue::set(Some(invite.email_address.clone())),
-                github_login: ActiveValue::set(user.github_login.clone()),
-                github_user_id: ActiveValue::set(Some(user.github_user_id)),
-                admin: ActiveValue::set(false),
-                invite_count: ActiveValue::set(user.invite_count),
-                invite_code: ActiveValue::set(Some(random_invite_code())),
-                metrics_id: ActiveValue::set(Uuid::new_v4()),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::column(user::Column::GithubLogin)
-                    .update_columns([
-                        user::Column::EmailAddress,
-                        user::Column::GithubUserId,
-                        user::Column::Admin,
-                    ])
-                    .to_owned(),
-            )
-            .exec_with_returning(&*tx)
-            .await?;
-
-            let mut signup = signup.into_active_model();
-            signup.user_id = ActiveValue::set(Some(user.id));
-            let signup = signup.update(&*tx).await?;
-
-            if let Some(inviting_user_id) = signup.inviting_user_id {
-                let (user_id_a, user_id_b, a_to_b) = if inviting_user_id < user.id {
-                    (inviting_user_id, user.id, true)
-                } else {
-                    (user.id, inviting_user_id, false)
-                };
-
-                contact::Entity::insert(contact::ActiveModel {
-                    user_id_a: ActiveValue::set(user_id_a),
-                    user_id_b: ActiveValue::set(user_id_b),
-                    a_to_b: ActiveValue::set(a_to_b),
-                    should_notify: ActiveValue::set(true),
-                    accepted: ActiveValue::set(true),
-                    ..Default::default()
-                })
-                .on_conflict(OnConflict::new().do_nothing().to_owned())
-                .exec_without_returning(&*tx)
-                .await?;
-            }
-
-            Ok(Some(NewUserResult {
-                user_id: user.id,
-                metrics_id: user.metrics_id.to_string(),
-                inviting_user_id: signup.inviting_user_id,
-                signup_device_id: signup.device_id,
-            }))
-        })
-        .await
-    }
-
-    pub async fn set_invite_count_for_user(&self, id: UserId, count: i32) -> Result<()> {
-        self.transaction(|tx| async move {
-            if count > 0 {
-                user::Entity::update_many()
-                    .filter(
-                        user::Column::Id
-                            .eq(id)
-                            .and(user::Column::InviteCode.is_null()),
-                    )
-                    .set(user::ActiveModel {
-                        invite_code: ActiveValue::set(Some(random_invite_code())),
-                        ..Default::default()
-                    })
-                    .exec(&*tx)
-                    .await?;
-            }
-
-            user::Entity::update_many()
-                .filter(user::Column::Id.eq(id))
-                .set(user::ActiveModel {
-                    invite_count: ActiveValue::set(count),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn get_invite_code_for_user(&self, id: UserId) -> Result<Option<(String, i32)>> {
-        self.transaction(|tx| async move {
-            match user::Entity::find_by_id(id).one(&*tx).await? {
-                Some(user) if user.invite_code.is_some() => {
-                    Ok(Some((user.invite_code.unwrap(), user.invite_count)))
-                }
-                _ => Ok(None),
-            }
-        })
-        .await
-    }
-
-    pub async fn get_user_for_invite_code(&self, code: &str) -> Result<User> {
-        self.transaction(|tx| async move {
-            user::Entity::find()
-                .filter(user::Column::InviteCode.eq(code))
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| {
-                    Error::Http(
-                        StatusCode::NOT_FOUND,
-                        "that invite code does not exist".to_string(),
-                    )
-                })
-        })
-        .await
-    }
-
-    pub async fn create_signup(&self, signup: &NewSignup) -> Result<()> {
-        self.transaction(|tx| async move {
-            signup::Entity::insert(signup::ActiveModel {
-                email_address: ActiveValue::set(signup.email_address.clone()),
-                email_confirmation_code: ActiveValue::set(random_email_confirmation_code()),
-                email_confirmation_sent: ActiveValue::set(false),
-                platform_mac: ActiveValue::set(signup.platform_mac),
-                platform_windows: ActiveValue::set(signup.platform_windows),
-                platform_linux: ActiveValue::set(signup.platform_linux),
-                platform_unknown: ActiveValue::set(false),
-                editor_features: ActiveValue::set(Some(signup.editor_features.clone())),
-                programming_languages: ActiveValue::set(Some(signup.programming_languages.clone())),
-                device_id: ActiveValue::set(signup.device_id.clone()),
-                added_to_mailing_list: ActiveValue::set(signup.added_to_mailing_list),
-                ..Default::default()
-            })
-            .on_conflict(
-                OnConflict::column(signup::Column::EmailAddress)
-                    .update_columns([
-                        signup::Column::PlatformMac,
-                        signup::Column::PlatformWindows,
-                        signup::Column::PlatformLinux,
-                        signup::Column::EditorFeatures,
-                        signup::Column::ProgrammingLanguages,
-                        signup::Column::DeviceId,
-                        signup::Column::AddedToMailingList,
-                    ])
-                    .to_owned(),
-            )
-            .exec(&*tx)
-            .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn get_signup(&self, email_address: &str) -> Result<signup::Model> {
-        self.transaction(|tx| async move {
-            let signup = signup::Entity::find()
-                .filter(signup::Column::EmailAddress.eq(email_address))
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| {
-                    anyhow!("signup with email address {} doesn't exist", email_address)
-                })?;
-
-            Ok(signup)
-        })
-        .await
-    }
-
-    pub async fn get_waitlist_summary(&self) -> Result<WaitlistSummary> {
-        self.transaction(|tx| async move {
-            let query = "
-                SELECT
-                    COUNT(*) as count,
-                    COALESCE(SUM(CASE WHEN platform_linux THEN 1 ELSE 0 END), 0) as linux_count,
-                    COALESCE(SUM(CASE WHEN platform_mac THEN 1 ELSE 0 END), 0) as mac_count,
-                    COALESCE(SUM(CASE WHEN platform_windows THEN 1 ELSE 0 END), 0) as windows_count,
-                    COALESCE(SUM(CASE WHEN platform_unknown THEN 1 ELSE 0 END), 0) as unknown_count
-                FROM (
-                    SELECT *
-                    FROM signups
-                    WHERE
-                        NOT email_confirmation_sent
-                ) AS unsent
-            ";
-            Ok(
-                WaitlistSummary::find_by_statement(Statement::from_sql_and_values(
-                    self.pool.get_database_backend(),
-                    query.into(),
-                    vec![],
-                ))
-                .one(&*tx)
-                .await?
-                .ok_or_else(|| anyhow!("invalid result"))?,
-            )
-        })
-        .await
-    }
-
-    pub async fn record_sent_invites(&self, invites: &[Invite]) -> Result<()> {
-        let emails = invites
-            .iter()
-            .map(|s| s.email_address.as_str())
-            .collect::<Vec<_>>();
-        self.transaction(|tx| async {
-            let tx = tx;
-            signup::Entity::update_many()
-                .filter(signup::Column::EmailAddress.is_in(emails.iter().copied()))
-                .set(signup::ActiveModel {
-                    email_confirmation_sent: ActiveValue::set(true),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
-    pub async fn get_unsent_invites(&self, count: usize) -> Result<Vec<Invite>> {
-        self.transaction(|tx| async move {
-            Ok(signup::Entity::find()
-                .select_only()
-                .column(signup::Column::EmailAddress)
-                .column(signup::Column::EmailConfirmationCode)
-                .filter(
-                    signup::Column::EmailConfirmationSent.eq(false).and(
-                        signup::Column::PlatformMac
-                            .eq(true)
-                            .or(signup::Column::PlatformUnknown.eq(true)),
-                    ),
-                )
-                .order_by_asc(signup::Column::CreatedAt)
-                .limit(count as u64)
-                .into_model()
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-}
-
-fn random_invite_code() -> String {
-    nanoid::nanoid!(16)
-}
-
-fn random_email_confirmation_code() -> String {
-    nanoid::nanoid!(64)
-}

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

@@ -123,27 +123,6 @@ impl Database {
         .await
     }
 
-    pub async fn get_users_with_no_invites(
-        &self,
-        invited_by_another_user: bool,
-    ) -> Result<Vec<User>> {
-        self.transaction(|tx| async move {
-            Ok(user::Entity::find()
-                .filter(
-                    user::Column::InviteCount
-                        .eq(0)
-                        .and(if invited_by_another_user {
-                            user::Column::InviterId.is_not_null()
-                        } else {
-                            user::Column::InviterId.is_null()
-                        }),
-                )
-                .all(&*tx)
-                .await?)
-        })
-        .await
-    }
-
     pub async fn get_user_metrics_id(&self, id: UserId) -> Result<String> {
         #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
         enum QueryAs {
@@ -163,21 +142,6 @@ impl Database {
         .await
     }
 
-    pub async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()> {
-        self.transaction(|tx| async move {
-            user::Entity::update_many()
-                .filter(user::Column::Id.eq(id))
-                .set(user::ActiveModel {
-                    admin: ActiveValue::set(is_admin),
-                    ..Default::default()
-                })
-                .exec(&*tx)
-                .await?;
-            Ok(())
-        })
-        .await
-    }
-
     pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> {
         self.transaction(|tx| async move {
             user::Entity::update_many()
@@ -220,7 +184,7 @@ impl Database {
             Ok(user::Entity::find()
                 .from_raw_sql(Statement::from_sql_and_values(
                     self.pool.get_database_backend(),
-                    query.into(),
+                    query,
                     vec![like_string.into(), name_query.into(), limit.into()],
                 ))
                 .all(&*tx)

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

@@ -4,12 +4,16 @@ pub mod buffer_operation;
 pub mod buffer_snapshot;
 pub mod channel;
 pub mod channel_buffer_collaborator;
+pub mod channel_chat_participant;
 pub mod channel_member;
+pub mod channel_message;
 pub mod channel_path;
 pub mod contact;
 pub mod feature_flag;
 pub mod follower;
 pub mod language_server;
+pub mod observed_buffer_edits;
+pub mod observed_channel_messages;
 pub mod project;
 pub mod project_collaborator;
 pub mod room;

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

@@ -21,6 +21,8 @@ pub enum Relation {
     Member,
     #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")]
     BufferCollaborators,
+    #[sea_orm(has_many = "super::channel_chat_participant::Entity")]
+    ChatParticipants,
 }
 
 impl Related<super::channel_member::Entity> for Entity {
@@ -46,3 +48,9 @@ impl Related<super::channel_buffer_collaborator::Entity> for Entity {
         Relation::BufferCollaborators.def()
     }
 }
+
+impl Related<super::channel_chat_participant::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::ChatParticipants.def()
+    }
+}

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

@@ -0,0 +1,41 @@
+use crate::db::{ChannelChatParticipantId, ChannelId, ServerId, UserId};
+use rpc::ConnectionId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "channel_chat_participants")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: ChannelChatParticipantId,
+    pub channel_id: ChannelId,
+    pub user_id: UserId,
+    pub connection_id: i32,
+    pub connection_server_id: ServerId,
+}
+
+impl Model {
+    pub fn connection(&self) -> ConnectionId {
+        ConnectionId {
+            owner_id: self.connection_server_id.0 as u32,
+            id: self.connection_id as u32,
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::channel::Entity",
+        from = "Column::ChannelId",
+        to = "super::channel::Column::Id"
+    )]
+    Channel,
+}
+
+impl Related<super::channel::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Channel.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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

@@ -0,0 +1,45 @@
+use crate::db::{ChannelId, MessageId, UserId};
+use sea_orm::entity::prelude::*;
+use time::PrimitiveDateTime;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "channel_messages")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: MessageId,
+    pub channel_id: ChannelId,
+    pub sender_id: UserId,
+    pub body: String,
+    pub sent_at: PrimitiveDateTime,
+    pub nonce: Uuid,
+}
+
+impl ActiveModelBehavior for ActiveModel {}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::channel::Entity",
+        from = "Column::ChannelId",
+        to = "super::channel::Column::Id"
+    )]
+    Channel,
+    #[sea_orm(
+        belongs_to = "super::user::Entity",
+        from = "Column::SenderId",
+        to = "super::user::Column::Id"
+    )]
+    Sender,
+}
+
+impl Related<super::channel::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Channel.def()
+    }
+}
+
+impl Related<super::user::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Sender.def()
+    }
+}

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

@@ -0,0 +1,43 @@
+use crate::db::{BufferId, UserId};
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "observed_buffer_edits")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub user_id: UserId,
+    pub buffer_id: BufferId,
+    pub epoch: i32,
+    pub lamport_timestamp: i32,
+    pub replica_id: i32,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::buffer::Entity",
+        from = "Column::BufferId",
+        to = "super::buffer::Column::Id"
+    )]
+    Buffer,
+    #[sea_orm(
+        belongs_to = "super::user::Entity",
+        from = "Column::UserId",
+        to = "super::user::Column::Id"
+    )]
+    User,
+}
+
+impl Related<super::buffer::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Buffer.def()
+    }
+}
+
+impl Related<super::user::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::User.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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

@@ -0,0 +1,41 @@
+use crate::db::{ChannelId, MessageId, UserId};
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "observed_channel_messages")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub user_id: UserId,
+    pub channel_id: ChannelId,
+    pub channel_message_id: MessageId,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {
+    #[sea_orm(
+        belongs_to = "super::channel::Entity",
+        from = "Column::ChannelId",
+        to = "super::channel::Column::Id"
+    )]
+    Channel,
+    #[sea_orm(
+        belongs_to = "super::user::Entity",
+        from = "Column::UserId",
+        to = "super::user::Column::Id"
+    )]
+    User,
+}
+
+impl Related<super::channel::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Channel.def()
+    }
+}
+
+impl Related<super::user::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::User.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}

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

@@ -1,4 +1,5 @@
 use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
+use rpc::ConnectionId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -17,6 +18,16 @@ pub struct Model {
     pub calling_user_id: UserId,
     pub calling_connection_id: i32,
     pub calling_connection_server_id: Option<ServerId>,
+    pub participant_index: Option<i32>,
+}
+
+impl Model {
+    pub fn answering_connection(&self) -> Option<ConnectionId> {
+        Some(ConnectionId {
+            owner_id: self.answering_connection_server_id?.0 as u32,
+            id: self.answering_connection_id? as u32,
+        })
+    }
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

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

@@ -1,10 +1,13 @@
 mod buffer_tests;
+mod channel_tests;
 mod db_tests;
 mod feature_flag_tests;
+mod message_tests;
 
 use super::*;
 use gpui::executor::Background;
 use parking_lot::Mutex;
+use rpc::proto::ChannelEdge;
 use sea_orm::ConnectionTrait;
 use sqlx::migrate::MigrateDatabase;
 use std::sync::Arc;
@@ -36,7 +39,7 @@ impl TestDb {
             db.pool
                 .execute(sea_orm::Statement::from_string(
                     db.pool.get_database_backend(),
-                    sql.into(),
+                    sql,
                 ))
                 .await
                 .unwrap();
@@ -131,7 +134,7 @@ impl Drop for TestDb {
                 db.pool
                     .execute(sea_orm::Statement::from_string(
                         db.pool.get_database_backend(),
-                        query.into(),
+                        query,
                     ))
                     .await
                     .log_err();
@@ -142,3 +145,27 @@ impl Drop for TestDb {
         }
     }
 }
+
+/// The second tuples are (channel_id, parent)
+fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)]) -> ChannelGraph {
+    let mut graph = ChannelGraph {
+        channels: vec![],
+        edges: vec![],
+    };
+
+    for (id, name) in channels {
+        graph.channels.push(Channel {
+            id: *id,
+            name: name.to_string(),
+        })
+    }
+
+    for (channel, parent) in edges {
+        graph.edges.push(ChannelEdge {
+            channel_id: channel.to_proto(),
+            parent_id: parent.to_proto(),
+        })
+    }
+
+    graph
+}

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

@@ -1,6 +1,6 @@
 use super::*;
 use crate::test_both_dbs;
-use language::proto;
+use language::proto::{self, serialize_version};
 use text::Buffer;
 
 test_both_dbs!(
@@ -134,12 +134,12 @@ async fn test_channel_buffers(db: &Arc<Database>) {
     let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
     assert_eq!(zed_collaborats, &[a_id, b_id]);
 
-    let collaborators = db
+    let left_buffer = db
         .leave_channel_buffer(zed_id, connection_id_b)
         .await
         .unwrap();
 
-    assert_eq!(collaborators, &[connection_id_a],);
+    assert_eq!(left_buffer.connections, &[connection_id_a],);
 
     let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
     let _ = db
@@ -163,3 +163,349 @@ async fn test_channel_buffers(db: &Arc<Database>) {
     assert_eq!(buffer_response_b.base_text, "hello, cruel world");
     assert_eq!(buffer_response_b.operations, &[]);
 }
+
+test_both_dbs!(
+    test_channel_buffers_last_operations,
+    test_channel_buffers_last_operations_postgres,
+    test_channel_buffers_last_operations_sqlite
+);
+
+async fn test_channel_buffers_last_operations(db: &Database) {
+    let user_id = db
+        .create_user(
+            "user_a@example.com",
+            false,
+            NewUserParams {
+                github_login: "user_a".into(),
+                github_user_id: 101,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let observer_id = db
+        .create_user(
+            "user_b@example.com",
+            false,
+            NewUserParams {
+                github_login: "user_b".into(),
+                github_user_id: 102,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let owner_id = db.create_server("production").await.unwrap().0 as u32;
+    let connection_id = ConnectionId {
+        owner_id,
+        id: user_id.0 as u32,
+    };
+
+    let mut buffers = Vec::new();
+    let mut text_buffers = Vec::new();
+    for i in 0..3 {
+        let channel = db
+            .create_root_channel(&format!("channel-{i}"), &format!("room-{i}"), user_id)
+            .await
+            .unwrap();
+
+        db.invite_channel_member(channel, observer_id, user_id, false)
+            .await
+            .unwrap();
+        db.respond_to_channel_invite(channel, observer_id, true)
+            .await
+            .unwrap();
+
+        db.join_channel_buffer(channel, user_id, connection_id)
+            .await
+            .unwrap();
+
+        buffers.push(
+            db.transaction(|tx| async move { db.get_channel_buffer(channel, &*tx).await })
+                .await
+                .unwrap(),
+        );
+
+        text_buffers.push(Buffer::new(0, 0, "".to_string()));
+    }
+
+    let operations = db
+        .transaction(|tx| {
+            let buffers = &buffers;
+            async move {
+                db.get_latest_operations_for_buffers([buffers[0].id, buffers[2].id], &*tx)
+                    .await
+            }
+        })
+        .await
+        .unwrap();
+
+    assert!(operations.is_empty());
+
+    update_buffer(
+        buffers[0].channel_id,
+        user_id,
+        db,
+        vec![
+            text_buffers[0].edit([(0..0, "a")]),
+            text_buffers[0].edit([(0..0, "b")]),
+            text_buffers[0].edit([(0..0, "c")]),
+        ],
+    )
+    .await;
+
+    update_buffer(
+        buffers[1].channel_id,
+        user_id,
+        db,
+        vec![
+            text_buffers[1].edit([(0..0, "d")]),
+            text_buffers[1].edit([(1..1, "e")]),
+            text_buffers[1].edit([(2..2, "f")]),
+        ],
+    )
+    .await;
+
+    // cause buffer 1's epoch to increment.
+    db.leave_channel_buffer(buffers[1].channel_id, connection_id)
+        .await
+        .unwrap();
+    db.join_channel_buffer(buffers[1].channel_id, user_id, connection_id)
+        .await
+        .unwrap();
+    text_buffers[1] = Buffer::new(1, 0, "def".to_string());
+    update_buffer(
+        buffers[1].channel_id,
+        user_id,
+        db,
+        vec![
+            text_buffers[1].edit([(0..0, "g")]),
+            text_buffers[1].edit([(0..0, "h")]),
+        ],
+    )
+    .await;
+
+    update_buffer(
+        buffers[2].channel_id,
+        user_id,
+        db,
+        vec![text_buffers[2].edit([(0..0, "i")])],
+    )
+    .await;
+
+    let operations = db
+        .transaction(|tx| {
+            let buffers = &buffers;
+            async move {
+                db.get_latest_operations_for_buffers([buffers[1].id, buffers[2].id], &*tx)
+                    .await
+            }
+        })
+        .await
+        .unwrap();
+    assert_operations(
+        &operations,
+        &[
+            (buffers[1].id, 1, &text_buffers[1]),
+            (buffers[2].id, 0, &text_buffers[2]),
+        ],
+    );
+
+    let operations = db
+        .transaction(|tx| {
+            let buffers = &buffers;
+            async move {
+                db.get_latest_operations_for_buffers([buffers[0].id, buffers[1].id], &*tx)
+                    .await
+            }
+        })
+        .await
+        .unwrap();
+    assert_operations(
+        &operations,
+        &[
+            (buffers[0].id, 0, &text_buffers[0]),
+            (buffers[1].id, 1, &text_buffers[1]),
+        ],
+    );
+
+    let buffer_changes = db
+        .transaction(|tx| {
+            let buffers = &buffers;
+            async move {
+                db.unseen_channel_buffer_changes(
+                    observer_id,
+                    &[
+                        buffers[0].channel_id,
+                        buffers[1].channel_id,
+                        buffers[2].channel_id,
+                    ],
+                    &*tx,
+                )
+                .await
+            }
+        })
+        .await
+        .unwrap();
+
+    pretty_assertions::assert_eq!(
+        buffer_changes,
+        [
+            rpc::proto::UnseenChannelBufferChange {
+                channel_id: buffers[0].channel_id.to_proto(),
+                epoch: 0,
+                version: serialize_version(&text_buffers[0].version()),
+            },
+            rpc::proto::UnseenChannelBufferChange {
+                channel_id: buffers[1].channel_id.to_proto(),
+                epoch: 1,
+                version: serialize_version(&text_buffers[1].version())
+                    .into_iter()
+                    .filter(|vector| vector.replica_id
+                        == buffer_changes[1].version.first().unwrap().replica_id)
+                    .collect::<Vec<_>>(),
+            },
+            rpc::proto::UnseenChannelBufferChange {
+                channel_id: buffers[2].channel_id.to_proto(),
+                epoch: 0,
+                version: serialize_version(&text_buffers[2].version()),
+            },
+        ]
+    );
+
+    db.observe_buffer_version(
+        buffers[1].id,
+        observer_id,
+        1,
+        serialize_version(&text_buffers[1].version()).as_slice(),
+    )
+    .await
+    .unwrap();
+
+    let buffer_changes = db
+        .transaction(|tx| {
+            let buffers = &buffers;
+            async move {
+                db.unseen_channel_buffer_changes(
+                    observer_id,
+                    &[
+                        buffers[0].channel_id,
+                        buffers[1].channel_id,
+                        buffers[2].channel_id,
+                    ],
+                    &*tx,
+                )
+                .await
+            }
+        })
+        .await
+        .unwrap();
+
+    assert_eq!(
+        buffer_changes,
+        [
+            rpc::proto::UnseenChannelBufferChange {
+                channel_id: buffers[0].channel_id.to_proto(),
+                epoch: 0,
+                version: serialize_version(&text_buffers[0].version()),
+            },
+            rpc::proto::UnseenChannelBufferChange {
+                channel_id: buffers[2].channel_id.to_proto(),
+                epoch: 0,
+                version: serialize_version(&text_buffers[2].version()),
+            },
+        ]
+    );
+
+    // Observe an earlier version of the buffer.
+    db.observe_buffer_version(
+        buffers[1].id,
+        observer_id,
+        1,
+        &[rpc::proto::VectorClockEntry {
+            replica_id: 0,
+            timestamp: 0,
+        }],
+    )
+    .await
+    .unwrap();
+
+    let buffer_changes = db
+        .transaction(|tx| {
+            let buffers = &buffers;
+            async move {
+                db.unseen_channel_buffer_changes(
+                    observer_id,
+                    &[
+                        buffers[0].channel_id,
+                        buffers[1].channel_id,
+                        buffers[2].channel_id,
+                    ],
+                    &*tx,
+                )
+                .await
+            }
+        })
+        .await
+        .unwrap();
+
+    assert_eq!(
+        buffer_changes,
+        [
+            rpc::proto::UnseenChannelBufferChange {
+                channel_id: buffers[0].channel_id.to_proto(),
+                epoch: 0,
+                version: serialize_version(&text_buffers[0].version()),
+            },
+            rpc::proto::UnseenChannelBufferChange {
+                channel_id: buffers[2].channel_id.to_proto(),
+                epoch: 0,
+                version: serialize_version(&text_buffers[2].version()),
+            },
+        ]
+    );
+}
+
+async fn update_buffer(
+    channel_id: ChannelId,
+    user_id: UserId,
+    db: &Database,
+    operations: Vec<text::Operation>,
+) {
+    let operations = operations
+        .into_iter()
+        .map(|op| proto::serialize_operation(&language::Operation::Buffer(op)))
+        .collect::<Vec<_>>();
+    db.update_channel_buffer(channel_id, user_id, &operations)
+        .await
+        .unwrap();
+}
+
+fn assert_operations(
+    operations: &[buffer_operation::Model],
+    expected: &[(BufferId, i32, &text::Buffer)],
+) {
+    let actual = operations
+        .iter()
+        .map(|op| buffer_operation::Model {
+            buffer_id: op.buffer_id,
+            epoch: op.epoch,
+            lamport_timestamp: op.lamport_timestamp,
+            replica_id: op.replica_id,
+            value: vec![],
+        })
+        .collect::<Vec<_>>();
+    let expected = expected
+        .iter()
+        .map(|(buffer_id, epoch, buffer)| buffer_operation::Model {
+            buffer_id: *buffer_id,
+            epoch: *epoch,
+            lamport_timestamp: buffer.lamport_clock.value as i32 - 1,
+            replica_id: buffer.replica_id() as i32,
+            value: vec![],
+        })
+        .collect::<Vec<_>>();
+    assert_eq!(actual, expected, "unexpected operations")
+}

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

@@ -0,0 +1,875 @@
+use collections::{HashMap, HashSet};
+use rpc::{
+    proto::{self},
+    ConnectionId,
+};
+
+use crate::{
+    db::{queries::channels::ChannelGraph, tests::graph, ChannelId, Database, NewUserParams},
+    test_both_dbs,
+};
+use std::sync::Arc;
+
+test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
+
+async fn test_channels(db: &Arc<Database>) {
+    let a_id = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let b_id = db
+        .create_user(
+            "user2@example.com",
+            false,
+            NewUserParams {
+                github_login: "user2".into(),
+                github_user_id: 6,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
+
+    // Make sure that people cannot read channels they haven't been invited to
+    assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
+
+    db.invite_channel_member(zed_id, b_id, a_id, false)
+        .await
+        .unwrap();
+
+    db.respond_to_channel_invite(zed_id, b_id, true)
+        .await
+        .unwrap();
+
+    let crdb_id = db
+        .create_channel("crdb", Some(zed_id), "2", a_id)
+        .await
+        .unwrap();
+    let livestreaming_id = db
+        .create_channel("livestreaming", Some(zed_id), "3", a_id)
+        .await
+        .unwrap();
+    let replace_id = db
+        .create_channel("replace", Some(zed_id), "4", a_id)
+        .await
+        .unwrap();
+
+    let mut members = db.get_channel_members(replace_id).await.unwrap();
+    members.sort();
+    assert_eq!(members, &[a_id, b_id]);
+
+    let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
+    let cargo_id = db
+        .create_channel("cargo", Some(rust_id), "6", a_id)
+        .await
+        .unwrap();
+
+    let cargo_ra_id = db
+        .create_channel("cargo-ra", Some(cargo_id), "7", a_id)
+        .await
+        .unwrap();
+
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_eq!(
+        result.channels,
+        graph(
+            &[
+                (zed_id, "zed"),
+                (crdb_id, "crdb"),
+                (livestreaming_id, "livestreaming"),
+                (replace_id, "replace"),
+                (rust_id, "rust"),
+                (cargo_id, "cargo"),
+                (cargo_ra_id, "cargo-ra")
+            ],
+            &[
+                (crdb_id, zed_id),
+                (livestreaming_id, zed_id),
+                (replace_id, zed_id),
+                (cargo_id, rust_id),
+                (cargo_ra_id, cargo_id),
+            ]
+        )
+    );
+
+    let result = db.get_channels_for_user(b_id).await.unwrap();
+    assert_eq!(
+        result.channels,
+        graph(
+            &[
+                (zed_id, "zed"),
+                (crdb_id, "crdb"),
+                (livestreaming_id, "livestreaming"),
+                (replace_id, "replace")
+            ],
+            &[
+                (crdb_id, zed_id),
+                (livestreaming_id, zed_id),
+                (replace_id, zed_id)
+            ]
+        )
+    );
+
+    // Update member permissions
+    let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
+    assert!(set_subchannel_admin.is_err());
+    let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
+    assert!(set_channel_admin.is_ok());
+
+    let result = db.get_channels_for_user(b_id).await.unwrap();
+    assert_eq!(
+        result.channels,
+        graph(
+            &[
+                (zed_id, "zed"),
+                (crdb_id, "crdb"),
+                (livestreaming_id, "livestreaming"),
+                (replace_id, "replace")
+            ],
+            &[
+                (crdb_id, zed_id),
+                (livestreaming_id, zed_id),
+                (replace_id, zed_id)
+            ]
+        )
+    );
+
+    // Remove a single channel
+    db.delete_channel(crdb_id, a_id).await.unwrap();
+    assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
+
+    // Remove a channel tree
+    let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap();
+    channel_ids.sort();
+    assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
+    assert_eq!(user_ids, &[a_id]);
+
+    assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
+    assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
+    assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
+}
+
+test_both_dbs!(
+    test_joining_channels,
+    test_joining_channels_postgres,
+    test_joining_channels_sqlite
+);
+
+async fn test_joining_channels(db: &Arc<Database>) {
+    let owner_id = db.create_server("test").await.unwrap().0 as u32;
+
+    let user_1 = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let user_2 = db
+        .create_user(
+            "user2@example.com",
+            false,
+            NewUserParams {
+                github_login: "user2".into(),
+                github_user_id: 6,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let channel_1 = db
+        .create_root_channel("channel_1", "1", user_1)
+        .await
+        .unwrap();
+    let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
+
+    // can join a room with membership to its channel
+    let joined_room = db
+        .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
+        .await
+        .unwrap();
+    assert_eq!(joined_room.room.participants.len(), 1);
+
+    drop(joined_room);
+    // cannot join a room without membership to its channel
+    assert!(db
+        .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
+        .await
+        .is_err());
+}
+
+test_both_dbs!(
+    test_channel_invites,
+    test_channel_invites_postgres,
+    test_channel_invites_sqlite
+);
+
+async fn test_channel_invites(db: &Arc<Database>) {
+    db.create_server("test").await.unwrap();
+
+    let user_1 = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let user_2 = db
+        .create_user(
+            "user2@example.com",
+            false,
+            NewUserParams {
+                github_login: "user2".into(),
+                github_user_id: 6,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let user_3 = db
+        .create_user(
+            "user3@example.com",
+            false,
+            NewUserParams {
+                github_login: "user3".into(),
+                github_user_id: 7,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let channel_1_1 = db
+        .create_root_channel("channel_1", "1", user_1)
+        .await
+        .unwrap();
+
+    let channel_1_2 = db
+        .create_root_channel("channel_2", "2", user_1)
+        .await
+        .unwrap();
+
+    db.invite_channel_member(channel_1_1, user_2, user_1, false)
+        .await
+        .unwrap();
+    db.invite_channel_member(channel_1_2, user_2, user_1, false)
+        .await
+        .unwrap();
+    db.invite_channel_member(channel_1_1, user_3, user_1, true)
+        .await
+        .unwrap();
+
+    let user_2_invites = db
+        .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
+        .await
+        .unwrap()
+        .into_iter()
+        .map(|channel| channel.id)
+        .collect::<Vec<_>>();
+
+    assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
+
+    let user_3_invites = db
+        .get_channel_invites_for_user(user_3) // -> [channel_1_1]
+        .await
+        .unwrap()
+        .into_iter()
+        .map(|channel| channel.id)
+        .collect::<Vec<_>>();
+
+    assert_eq!(user_3_invites, &[channel_1_1]);
+
+    let members = db
+        .get_channel_member_details(channel_1_1, user_1)
+        .await
+        .unwrap();
+    assert_eq!(
+        members,
+        &[
+            proto::ChannelMember {
+                user_id: user_1.to_proto(),
+                kind: proto::channel_member::Kind::Member.into(),
+                admin: true,
+            },
+            proto::ChannelMember {
+                user_id: user_2.to_proto(),
+                kind: proto::channel_member::Kind::Invitee.into(),
+                admin: false,
+            },
+            proto::ChannelMember {
+                user_id: user_3.to_proto(),
+                kind: proto::channel_member::Kind::Invitee.into(),
+                admin: true,
+            },
+        ]
+    );
+
+    db.respond_to_channel_invite(channel_1_1, user_2, true)
+        .await
+        .unwrap();
+
+    let channel_1_3 = db
+        .create_channel("channel_3", Some(channel_1_1), "1", user_1)
+        .await
+        .unwrap();
+
+    let members = db
+        .get_channel_member_details(channel_1_3, user_1)
+        .await
+        .unwrap();
+    assert_eq!(
+        members,
+        &[
+            proto::ChannelMember {
+                user_id: user_1.to_proto(),
+                kind: proto::channel_member::Kind::Member.into(),
+                admin: true,
+            },
+            proto::ChannelMember {
+                user_id: user_2.to_proto(),
+                kind: proto::channel_member::Kind::AncestorMember.into(),
+                admin: false,
+            },
+        ]
+    );
+}
+
+test_both_dbs!(
+    test_channel_renames,
+    test_channel_renames_postgres,
+    test_channel_renames_sqlite
+);
+
+async fn test_channel_renames(db: &Arc<Database>) {
+    db.create_server("test").await.unwrap();
+
+    let user_1 = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let user_2 = db
+        .create_user(
+            "user2@example.com",
+            false,
+            NewUserParams {
+                github_login: "user2".into(),
+                github_user_id: 6,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
+
+    db.rename_channel(zed_id, user_1, "#zed-archive")
+        .await
+        .unwrap();
+
+    let zed_archive_id = zed_id;
+
+    let (channel, _) = db
+        .get_channel(zed_archive_id, user_1)
+        .await
+        .unwrap()
+        .unwrap();
+    assert_eq!(channel.name, "zed-archive");
+
+    let non_permissioned_rename = db
+        .rename_channel(zed_archive_id, user_2, "hacked-lol")
+        .await;
+    assert!(non_permissioned_rename.is_err());
+
+    let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
+    assert!(bad_name_rename.is_err())
+}
+
+test_both_dbs!(
+    test_db_channel_moving,
+    test_channels_moving_postgres,
+    test_channels_moving_sqlite
+);
+
+async fn test_db_channel_moving(db: &Arc<Database>) {
+    let a_id = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
+
+    let crdb_id = db
+        .create_channel("crdb", Some(zed_id), "2", a_id)
+        .await
+        .unwrap();
+
+    let gpui2_id = db
+        .create_channel("gpui2", Some(zed_id), "3", a_id)
+        .await
+        .unwrap();
+
+    let livestreaming_id = db
+        .create_channel("livestreaming", Some(crdb_id), "4", a_id)
+        .await
+        .unwrap();
+
+    let livestreaming_dag_id = db
+        .create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id)
+        .await
+        .unwrap();
+
+    // ========================================================================
+    // sanity check
+    // Initial DAG:
+    //     /- gpui2
+    // zed -- crdb - livestreaming - livestreaming_dag
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, Some(zed_id)),
+            (gpui2_id, Some(zed_id)),
+            (livestreaming_id, Some(crdb_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+        ],
+    );
+
+    // Attempt to make a cycle
+    assert!(db
+        .link_channel(a_id, zed_id, livestreaming_id)
+        .await
+        .is_err());
+
+    // ========================================================================
+    // Make a link
+    db.link_channel(a_id, livestreaming_id, zed_id)
+        .await
+        .unwrap();
+
+    // DAG is now:
+    //     /- gpui2
+    // zed -- crdb - livestreaming - livestreaming_dag
+    //    \---------/
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, Some(zed_id)),
+            (gpui2_id, Some(zed_id)),
+            (livestreaming_id, Some(zed_id)),
+            (livestreaming_id, Some(crdb_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+        ],
+    );
+
+    // ========================================================================
+    // Create a new channel below a channel with multiple parents
+    let livestreaming_dag_sub_id = db
+        .create_channel(
+            "livestreaming_dag_sub",
+            Some(livestreaming_dag_id),
+            "6",
+            a_id,
+        )
+        .await
+        .unwrap();
+
+    // DAG is now:
+    //     /- gpui2
+    // zed -- crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id
+    //    \---------/
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, Some(zed_id)),
+            (gpui2_id, Some(zed_id)),
+            (livestreaming_id, Some(zed_id)),
+            (livestreaming_id, Some(crdb_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+        ],
+    );
+
+    // ========================================================================
+    // Test a complex DAG by making another link
+    let returned_channels = db
+        .link_channel(a_id, livestreaming_dag_sub_id, livestreaming_id)
+        .await
+        .unwrap();
+
+    // DAG is now:
+    //    /- gpui2                /---------------------\
+    // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub_id
+    //    \--------/
+
+    // make sure we're getting just the new link
+    // Not using the assert_dag helper because we want to make sure we're returning the full data
+    pretty_assertions::assert_eq!(
+        returned_channels,
+        graph(
+            &[(livestreaming_dag_sub_id, "livestreaming_dag_sub")],
+            &[(livestreaming_dag_sub_id, livestreaming_id)]
+        )
+    );
+
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, Some(zed_id)),
+            (gpui2_id, Some(zed_id)),
+            (livestreaming_id, Some(zed_id)),
+            (livestreaming_id, Some(crdb_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+        ],
+    );
+
+    // ========================================================================
+    // Test a complex DAG by making another link
+    let returned_channels = db
+        .link_channel(a_id, livestreaming_id, gpui2_id)
+        .await
+        .unwrap();
+
+    // DAG is now:
+    //    /- gpui2 -\             /---------------------\
+    // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub_id
+    //    \---------/
+
+    // Make sure that we're correctly getting the full sub-dag
+    pretty_assertions::assert_eq!(
+        returned_channels,
+        graph(
+            &[
+                (livestreaming_id, "livestreaming"),
+                (livestreaming_dag_id, "livestreaming_dag"),
+                (livestreaming_dag_sub_id, "livestreaming_dag_sub"),
+            ],
+            &[
+                (livestreaming_id, gpui2_id),
+                (livestreaming_dag_id, livestreaming_id),
+                (livestreaming_dag_sub_id, livestreaming_id),
+                (livestreaming_dag_sub_id, livestreaming_dag_id),
+            ]
+        )
+    );
+
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, Some(zed_id)),
+            (gpui2_id, Some(zed_id)),
+            (livestreaming_id, Some(zed_id)),
+            (livestreaming_id, Some(crdb_id)),
+            (livestreaming_id, Some(gpui2_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+        ],
+    );
+
+    // ========================================================================
+    // Test unlinking in a complex DAG by removing the inner link
+    db.unlink_channel(a_id, livestreaming_dag_sub_id, livestreaming_id)
+        .await
+        .unwrap();
+
+    // DAG is now:
+    //    /- gpui2 -\
+    // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
+    //    \---------/
+
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, Some(zed_id)),
+            (gpui2_id, Some(zed_id)),
+            (livestreaming_id, Some(gpui2_id)),
+            (livestreaming_id, Some(zed_id)),
+            (livestreaming_id, Some(crdb_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+        ],
+    );
+
+    // ========================================================================
+    // Test unlinking in a complex DAG by removing the inner link
+    db.unlink_channel(a_id, livestreaming_id, gpui2_id)
+        .await
+        .unwrap();
+
+    // DAG is now:
+    //    /- gpui2
+    // zed - crdb -- livestreaming - livestreaming_dag - livestreaming_dag_sub
+    //    \---------/
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, Some(zed_id)),
+            (gpui2_id, Some(zed_id)),
+            (livestreaming_id, Some(zed_id)),
+            (livestreaming_id, Some(crdb_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+        ],
+    );
+
+    // ========================================================================
+    // Test moving DAG nodes by moving livestreaming to be below gpui2
+    db.move_channel(a_id, livestreaming_id, crdb_id, gpui2_id)
+        .await
+        .unwrap();
+
+    // DAG is now:
+    //    /- gpui2 -- livestreaming - livestreaming_dag - livestreaming_dag_sub
+    // zed - crdb    /
+    //    \---------/
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, Some(zed_id)),
+            (gpui2_id, Some(zed_id)),
+            (livestreaming_id, Some(zed_id)),
+            (livestreaming_id, Some(gpui2_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+        ],
+    );
+
+    // ========================================================================
+    // Deleting a channel should not delete children that still have other parents
+    db.delete_channel(gpui2_id, a_id).await.unwrap();
+
+    // DAG is now:
+    // zed - crdb
+    //    \- livestreaming - livestreaming_dag - livestreaming_dag_sub
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, Some(zed_id)),
+            (livestreaming_id, Some(zed_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+        ],
+    );
+
+    // ========================================================================
+    // Unlinking a channel from it's parent should automatically promote it to a root channel
+    db.unlink_channel(a_id, crdb_id, zed_id).await.unwrap();
+
+    // DAG is now:
+    // crdb
+    // zed
+    //    \- livestreaming - livestreaming_dag - livestreaming_dag_sub
+
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, None),
+            (livestreaming_id, Some(zed_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+        ],
+    );
+
+    // ========================================================================
+    // You should be able to move a root channel into a non-root channel
+    db.link_channel(a_id, crdb_id, zed_id).await.unwrap();
+
+    // DAG is now:
+    // zed - crdb
+    //    \- livestreaming - livestreaming_dag - livestreaming_dag_sub
+
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, Some(zed_id)),
+            (livestreaming_id, Some(zed_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+        ],
+    );
+
+    // ========================================================================
+    // Prep for DAG deletion test
+    db.link_channel(a_id, livestreaming_id, crdb_id)
+        .await
+        .unwrap();
+
+    // DAG is now:
+    // zed - crdb - livestreaming - livestreaming_dag - livestreaming_dag_sub
+    //    \--------/
+
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (crdb_id, Some(zed_id)),
+            (livestreaming_id, Some(zed_id)),
+            (livestreaming_id, Some(crdb_id)),
+            (livestreaming_dag_id, Some(livestreaming_id)),
+            (livestreaming_dag_sub_id, Some(livestreaming_dag_id)),
+        ],
+    );
+
+    // Deleting the parent of a DAG should delete the whole DAG:
+    db.delete_channel(zed_id, a_id).await.unwrap();
+    let result = db.get_channels_for_user(a_id).await.unwrap();
+
+    assert!(result.channels.is_empty())
+}
+
+test_both_dbs!(
+    test_db_channel_moving_bugs,
+    test_db_channel_moving_bugs_postgres,
+    test_db_channel_moving_bugs_sqlite
+);
+
+async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
+    let user_id = db
+        .create_user(
+            "user1@example.com",
+            false,
+            NewUserParams {
+                github_login: "user1".into(),
+                github_user_id: 5,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let zed_id = db.create_root_channel("zed", "1", user_id).await.unwrap();
+
+    let projects_id = db
+        .create_channel("projects", Some(zed_id), "2", user_id)
+        .await
+        .unwrap();
+
+    let livestreaming_id = db
+        .create_channel("livestreaming", Some(projects_id), "3", user_id)
+        .await
+        .unwrap();
+
+    // Dag is: zed - projects - livestreaming
+
+    // Move to same parent should be a no-op
+    assert!(db
+        .move_channel(user_id, projects_id, zed_id, zed_id)
+        .await
+        .unwrap()
+        .is_empty());
+
+    // Stranding a channel should retain it's sub channels
+    db.unlink_channel(user_id, projects_id, zed_id)
+        .await
+        .unwrap();
+
+    let result = db.get_channels_for_user(user_id).await.unwrap();
+    assert_dag(
+        result.channels,
+        &[
+            (zed_id, None),
+            (projects_id, None),
+            (livestreaming_id, Some(projects_id)),
+        ],
+    );
+}
+
+#[track_caller]
+fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
+    let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
+    for channel in actual.channels {
+        actual_map.insert(channel.id, HashSet::default());
+    }
+    for edge in actual.edges {
+        actual_map
+            .get_mut(&ChannelId::from_proto(edge.channel_id))
+            .unwrap()
+            .insert(ChannelId::from_proto(edge.parent_id));
+    }
+
+    let mut expected_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
+
+    for (child, parent) in expected {
+        let entry = expected_map.entry(*child).or_default();
+        if let Some(parent) = parent {
+            entry.insert(*parent);
+        }
+    }
+
+    pretty_assertions::assert_eq!(actual_map, expected_map)
+}

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

@@ -575,999 +575,6 @@ async fn test_fuzzy_search_users() {
     }
 }
 
-#[gpui::test]
-async fn test_invite_codes() {
-    let test_db = TestDb::postgres(build_background_executor());
-    let db = test_db.db();
-
-    let NewUserResult { user_id: user1, .. } = db
-        .create_user(
-            "user1@example.com",
-            false,
-            NewUserParams {
-                github_login: "user1".into(),
-                github_user_id: 0,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap();
-
-    // Initially, user 1 has no invite code
-    assert_eq!(db.get_invite_code_for_user(user1).await.unwrap(), None);
-
-    // Setting invite count to 0 when no code is assigned does not assign a new code
-    db.set_invite_count_for_user(user1, 0).await.unwrap();
-    assert!(db.get_invite_code_for_user(user1).await.unwrap().is_none());
-
-    // User 1 creates an invite code that can be used twice.
-    db.set_invite_count_for_user(user1, 2).await.unwrap();
-    let (invite_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(invite_count, 2);
-
-    // User 2 redeems the invite code and becomes a contact of user 1.
-    let user2_invite = db
-        .create_invite_from_code(
-            &invite_code,
-            "user2@example.com",
-            Some("user-2-device-id"),
-            true,
-        )
-        .await
-        .unwrap();
-    let NewUserResult {
-        user_id: user2,
-        inviting_user_id,
-        signup_device_id,
-        metrics_id,
-    } = db
-        .create_user_from_invite(
-            &user2_invite,
-            NewUserParams {
-                github_login: "user2".into(),
-                github_user_id: 2,
-                invite_count: 7,
-            },
-        )
-        .await
-        .unwrap()
-        .unwrap();
-    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(invite_count, 1);
-    assert_eq!(inviting_user_id, Some(user1));
-    assert_eq!(signup_device_id.unwrap(), "user-2-device-id");
-    assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id);
-    assert_eq!(
-        db.get_contacts(user1).await.unwrap(),
-        [Contact::Accepted {
-            user_id: user2,
-            should_notify: true,
-            busy: false,
-        }]
-    );
-    assert_eq!(
-        db.get_contacts(user2).await.unwrap(),
-        [Contact::Accepted {
-            user_id: user1,
-            should_notify: false,
-            busy: false,
-        }]
-    );
-    assert!(db.has_contact(user1, user2).await.unwrap());
-    assert!(db.has_contact(user2, user1).await.unwrap());
-    assert_eq!(
-        db.get_invite_code_for_user(user2).await.unwrap().unwrap().1,
-        7
-    );
-
-    // User 3 redeems the invite code and becomes a contact of user 1.
-    let user3_invite = db
-        .create_invite_from_code(&invite_code, "user3@example.com", None, true)
-        .await
-        .unwrap();
-    let NewUserResult {
-        user_id: user3,
-        inviting_user_id,
-        signup_device_id,
-        ..
-    } = db
-        .create_user_from_invite(
-            &user3_invite,
-            NewUserParams {
-                github_login: "user-3".into(),
-                github_user_id: 3,
-                invite_count: 3,
-            },
-        )
-        .await
-        .unwrap()
-        .unwrap();
-    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(invite_count, 0);
-    assert_eq!(inviting_user_id, Some(user1));
-    assert!(signup_device_id.is_none());
-    assert_eq!(
-        db.get_contacts(user1).await.unwrap(),
-        [
-            Contact::Accepted {
-                user_id: user2,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user3,
-                should_notify: true,
-                busy: false,
-            }
-        ]
-    );
-    assert_eq!(
-        db.get_contacts(user3).await.unwrap(),
-        [Contact::Accepted {
-            user_id: user1,
-            should_notify: false,
-            busy: false,
-        }]
-    );
-    assert!(db.has_contact(user1, user3).await.unwrap());
-    assert!(db.has_contact(user3, user1).await.unwrap());
-    assert_eq!(
-        db.get_invite_code_for_user(user3).await.unwrap().unwrap().1,
-        3
-    );
-
-    // Trying to reedem the code for the third time results in an error.
-    db.create_invite_from_code(
-        &invite_code,
-        "user4@example.com",
-        Some("user-4-device-id"),
-        true,
-    )
-    .await
-    .unwrap_err();
-
-    // Invite count can be updated after the code has been created.
-    db.set_invite_count_for_user(user1, 2).await.unwrap();
-    let (latest_code, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(latest_code, invite_code); // Invite code doesn't change when we increment above 0
-    assert_eq!(invite_count, 2);
-
-    // User 4 can now redeem the invite code and becomes a contact of user 1.
-    let user4_invite = db
-        .create_invite_from_code(
-            &invite_code,
-            "user4@example.com",
-            Some("user-4-device-id"),
-            true,
-        )
-        .await
-        .unwrap();
-    let user4 = db
-        .create_user_from_invite(
-            &user4_invite,
-            NewUserParams {
-                github_login: "user-4".into(),
-                github_user_id: 4,
-                invite_count: 5,
-            },
-        )
-        .await
-        .unwrap()
-        .unwrap()
-        .user_id;
-
-    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(invite_count, 1);
-    assert_eq!(
-        db.get_contacts(user1).await.unwrap(),
-        [
-            Contact::Accepted {
-                user_id: user2,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user3,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user4,
-                should_notify: true,
-                busy: false,
-            }
-        ]
-    );
-    assert_eq!(
-        db.get_contacts(user4).await.unwrap(),
-        [Contact::Accepted {
-            user_id: user1,
-            should_notify: false,
-            busy: false,
-        }]
-    );
-    assert!(db.has_contact(user1, user4).await.unwrap());
-    assert!(db.has_contact(user4, user1).await.unwrap());
-    assert_eq!(
-        db.get_invite_code_for_user(user4).await.unwrap().unwrap().1,
-        5
-    );
-
-    // An existing user cannot redeem invite codes.
-    db.create_invite_from_code(
-        &invite_code,
-        "user2@example.com",
-        Some("user-2-device-id"),
-        true,
-    )
-    .await
-    .unwrap_err();
-    let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
-    assert_eq!(invite_count, 1);
-
-    // A newer user can invite an existing one via a different email address
-    // than the one they used to sign up.
-    let user5 = db
-        .create_user(
-            "user5@example.com",
-            false,
-            NewUserParams {
-                github_login: "user5".into(),
-                github_user_id: 5,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-    db.set_invite_count_for_user(user5, 5).await.unwrap();
-    let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap();
-    let user5_invite_to_user1 = db
-        .create_invite_from_code(&user5_invite_code, "user1@different.com", None, true)
-        .await
-        .unwrap();
-    let user1_2 = db
-        .create_user_from_invite(
-            &user5_invite_to_user1,
-            NewUserParams {
-                github_login: "user1".into(),
-                github_user_id: 1,
-                invite_count: 5,
-            },
-        )
-        .await
-        .unwrap()
-        .unwrap()
-        .user_id;
-    assert_eq!(user1_2, user1);
-    assert_eq!(
-        db.get_contacts(user1).await.unwrap(),
-        [
-            Contact::Accepted {
-                user_id: user2,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user3,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user4,
-                should_notify: true,
-                busy: false,
-            },
-            Contact::Accepted {
-                user_id: user5,
-                should_notify: false,
-                busy: false,
-            }
-        ]
-    );
-    assert_eq!(
-        db.get_contacts(user5).await.unwrap(),
-        [Contact::Accepted {
-            user_id: user1,
-            should_notify: true,
-            busy: false,
-        }]
-    );
-    assert!(db.has_contact(user1, user5).await.unwrap());
-    assert!(db.has_contact(user5, user1).await.unwrap());
-}
-
-test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
-
-async fn test_channels(db: &Arc<Database>) {
-    let a_id = db
-        .create_user(
-            "user1@example.com",
-            false,
-            NewUserParams {
-                github_login: "user1".into(),
-                github_user_id: 5,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-
-    let b_id = db
-        .create_user(
-            "user2@example.com",
-            false,
-            NewUserParams {
-                github_login: "user2".into(),
-                github_user_id: 6,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-
-    let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
-
-    // Make sure that people cannot read channels they haven't been invited to
-    assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
-
-    db.invite_channel_member(zed_id, b_id, a_id, false)
-        .await
-        .unwrap();
-
-    db.respond_to_channel_invite(zed_id, b_id, true)
-        .await
-        .unwrap();
-
-    let crdb_id = db
-        .create_channel("crdb", Some(zed_id), "2", a_id)
-        .await
-        .unwrap();
-    let livestreaming_id = db
-        .create_channel("livestreaming", Some(zed_id), "3", a_id)
-        .await
-        .unwrap();
-    let replace_id = db
-        .create_channel("replace", Some(zed_id), "4", a_id)
-        .await
-        .unwrap();
-
-    let mut members = db.get_channel_members(replace_id).await.unwrap();
-    members.sort();
-    assert_eq!(members, &[a_id, b_id]);
-
-    let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
-    let cargo_id = db
-        .create_channel("cargo", Some(rust_id), "6", a_id)
-        .await
-        .unwrap();
-
-    let cargo_ra_id = db
-        .create_channel("cargo-ra", Some(cargo_id), "7", a_id)
-        .await
-        .unwrap();
-
-    let result = db.get_channels_for_user(a_id).await.unwrap();
-    assert_eq!(
-        result.channels,
-        vec![
-            Channel {
-                id: zed_id,
-                name: "zed".to_string(),
-                parent_id: None,
-            },
-            Channel {
-                id: crdb_id,
-                name: "crdb".to_string(),
-                parent_id: Some(zed_id),
-            },
-            Channel {
-                id: livestreaming_id,
-                name: "livestreaming".to_string(),
-                parent_id: Some(zed_id),
-            },
-            Channel {
-                id: replace_id,
-                name: "replace".to_string(),
-                parent_id: Some(zed_id),
-            },
-            Channel {
-                id: rust_id,
-                name: "rust".to_string(),
-                parent_id: None,
-            },
-            Channel {
-                id: cargo_id,
-                name: "cargo".to_string(),
-                parent_id: Some(rust_id),
-            },
-            Channel {
-                id: cargo_ra_id,
-                name: "cargo-ra".to_string(),
-                parent_id: Some(cargo_id),
-            }
-        ]
-    );
-
-    let result = db.get_channels_for_user(b_id).await.unwrap();
-    assert_eq!(
-        result.channels,
-        vec![
-            Channel {
-                id: zed_id,
-                name: "zed".to_string(),
-                parent_id: None,
-            },
-            Channel {
-                id: crdb_id,
-                name: "crdb".to_string(),
-                parent_id: Some(zed_id),
-            },
-            Channel {
-                id: livestreaming_id,
-                name: "livestreaming".to_string(),
-                parent_id: Some(zed_id),
-            },
-            Channel {
-                id: replace_id,
-                name: "replace".to_string(),
-                parent_id: Some(zed_id),
-            },
-        ]
-    );
-
-    // Update member permissions
-    let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
-    assert!(set_subchannel_admin.is_err());
-    let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
-    assert!(set_channel_admin.is_ok());
-
-    let result = db.get_channels_for_user(b_id).await.unwrap();
-    assert_eq!(
-        result.channels,
-        vec![
-            Channel {
-                id: zed_id,
-                name: "zed".to_string(),
-                parent_id: None,
-            },
-            Channel {
-                id: crdb_id,
-                name: "crdb".to_string(),
-                parent_id: Some(zed_id),
-            },
-            Channel {
-                id: livestreaming_id,
-                name: "livestreaming".to_string(),
-                parent_id: Some(zed_id),
-            },
-            Channel {
-                id: replace_id,
-                name: "replace".to_string(),
-                parent_id: Some(zed_id),
-            },
-        ]
-    );
-
-    // Remove a single channel
-    db.remove_channel(crdb_id, a_id).await.unwrap();
-    assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
-
-    // Remove a channel tree
-    let (mut channel_ids, user_ids) = db.remove_channel(rust_id, a_id).await.unwrap();
-    channel_ids.sort();
-    assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
-    assert_eq!(user_ids, &[a_id]);
-
-    assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
-    assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
-    assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
-}
-
-test_both_dbs!(
-    test_joining_channels,
-    test_joining_channels_postgres,
-    test_joining_channels_sqlite
-);
-
-async fn test_joining_channels(db: &Arc<Database>) {
-    let owner_id = db.create_server("test").await.unwrap().0 as u32;
-
-    let user_1 = db
-        .create_user(
-            "user1@example.com",
-            false,
-            NewUserParams {
-                github_login: "user1".into(),
-                github_user_id: 5,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-    let user_2 = db
-        .create_user(
-            "user2@example.com",
-            false,
-            NewUserParams {
-                github_login: "user2".into(),
-                github_user_id: 6,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-
-    let channel_1 = db
-        .create_root_channel("channel_1", "1", user_1)
-        .await
-        .unwrap();
-    let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
-
-    // can join a room with membership to its channel
-    let joined_room = db
-        .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
-        .await
-        .unwrap();
-    assert_eq!(joined_room.room.participants.len(), 1);
-
-    drop(joined_room);
-    // cannot join a room without membership to its channel
-    assert!(db
-        .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
-        .await
-        .is_err());
-}
-
-test_both_dbs!(
-    test_channel_invites,
-    test_channel_invites_postgres,
-    test_channel_invites_sqlite
-);
-
-async fn test_channel_invites(db: &Arc<Database>) {
-    db.create_server("test").await.unwrap();
-
-    let user_1 = db
-        .create_user(
-            "user1@example.com",
-            false,
-            NewUserParams {
-                github_login: "user1".into(),
-                github_user_id: 5,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-    let user_2 = db
-        .create_user(
-            "user2@example.com",
-            false,
-            NewUserParams {
-                github_login: "user2".into(),
-                github_user_id: 6,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-
-    let user_3 = db
-        .create_user(
-            "user3@example.com",
-            false,
-            NewUserParams {
-                github_login: "user3".into(),
-                github_user_id: 7,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-
-    let channel_1_1 = db
-        .create_root_channel("channel_1", "1", user_1)
-        .await
-        .unwrap();
-
-    let channel_1_2 = db
-        .create_root_channel("channel_2", "2", user_1)
-        .await
-        .unwrap();
-
-    db.invite_channel_member(channel_1_1, user_2, user_1, false)
-        .await
-        .unwrap();
-    db.invite_channel_member(channel_1_2, user_2, user_1, false)
-        .await
-        .unwrap();
-    db.invite_channel_member(channel_1_1, user_3, user_1, true)
-        .await
-        .unwrap();
-
-    let user_2_invites = db
-        .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
-        .await
-        .unwrap()
-        .into_iter()
-        .map(|channel| channel.id)
-        .collect::<Vec<_>>();
-
-    assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
-
-    let user_3_invites = db
-        .get_channel_invites_for_user(user_3) // -> [channel_1_1]
-        .await
-        .unwrap()
-        .into_iter()
-        .map(|channel| channel.id)
-        .collect::<Vec<_>>();
-
-    assert_eq!(user_3_invites, &[channel_1_1]);
-
-    let members = db
-        .get_channel_member_details(channel_1_1, user_1)
-        .await
-        .unwrap();
-    assert_eq!(
-        members,
-        &[
-            proto::ChannelMember {
-                user_id: user_1.to_proto(),
-                kind: proto::channel_member::Kind::Member.into(),
-                admin: true,
-            },
-            proto::ChannelMember {
-                user_id: user_2.to_proto(),
-                kind: proto::channel_member::Kind::Invitee.into(),
-                admin: false,
-            },
-            proto::ChannelMember {
-                user_id: user_3.to_proto(),
-                kind: proto::channel_member::Kind::Invitee.into(),
-                admin: true,
-            },
-        ]
-    );
-
-    db.respond_to_channel_invite(channel_1_1, user_2, true)
-        .await
-        .unwrap();
-
-    let channel_1_3 = db
-        .create_channel("channel_3", Some(channel_1_1), "1", user_1)
-        .await
-        .unwrap();
-
-    let members = db
-        .get_channel_member_details(channel_1_3, user_1)
-        .await
-        .unwrap();
-    assert_eq!(
-        members,
-        &[
-            proto::ChannelMember {
-                user_id: user_1.to_proto(),
-                kind: proto::channel_member::Kind::Member.into(),
-                admin: true,
-            },
-            proto::ChannelMember {
-                user_id: user_2.to_proto(),
-                kind: proto::channel_member::Kind::AncestorMember.into(),
-                admin: false,
-            },
-        ]
-    );
-}
-
-test_both_dbs!(
-    test_channel_renames,
-    test_channel_renames_postgres,
-    test_channel_renames_sqlite
-);
-
-async fn test_channel_renames(db: &Arc<Database>) {
-    db.create_server("test").await.unwrap();
-
-    let user_1 = db
-        .create_user(
-            "user1@example.com",
-            false,
-            NewUserParams {
-                github_login: "user1".into(),
-                github_user_id: 5,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-
-    let user_2 = db
-        .create_user(
-            "user2@example.com",
-            false,
-            NewUserParams {
-                github_login: "user2".into(),
-                github_user_id: 6,
-                invite_count: 0,
-            },
-        )
-        .await
-        .unwrap()
-        .user_id;
-
-    let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
-
-    db.rename_channel(zed_id, user_1, "#zed-archive")
-        .await
-        .unwrap();
-
-    let zed_archive_id = zed_id;
-
-    let (channel, _) = db
-        .get_channel(zed_archive_id, user_1)
-        .await
-        .unwrap()
-        .unwrap();
-    assert_eq!(channel.name, "zed-archive");
-
-    let non_permissioned_rename = db
-        .rename_channel(zed_archive_id, user_2, "hacked-lol")
-        .await;
-    assert!(non_permissioned_rename.is_err());
-
-    let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await;
-    assert!(bad_name_rename.is_err())
-}
-
-#[gpui::test]
-async fn test_multiple_signup_overwrite() {
-    let test_db = TestDb::postgres(build_background_executor());
-    let db = test_db.db();
-
-    let email_address = "user_1@example.com".to_string();
-
-    let initial_signup_created_at_milliseconds = 0;
-
-    let initial_signup = NewSignup {
-        email_address: email_address.clone(),
-        platform_mac: false,
-        platform_linux: true,
-        platform_windows: false,
-        editor_features: vec!["speed".into()],
-        programming_languages: vec!["rust".into(), "c".into()],
-        device_id: Some(format!("device_id")),
-        added_to_mailing_list: false,
-        created_at: Some(
-            DateTime::from_timestamp_millis(initial_signup_created_at_milliseconds).unwrap(),
-        ),
-    };
-
-    db.create_signup(&initial_signup).await.unwrap();
-
-    let initial_signup_from_db = db.get_signup(&email_address).await.unwrap();
-
-    assert_eq!(
-        initial_signup_from_db.clone(),
-        signup::Model {
-            email_address: initial_signup.email_address,
-            platform_mac: initial_signup.platform_mac,
-            platform_linux: initial_signup.platform_linux,
-            platform_windows: initial_signup.platform_windows,
-            editor_features: Some(initial_signup.editor_features),
-            programming_languages: Some(initial_signup.programming_languages),
-            added_to_mailing_list: initial_signup.added_to_mailing_list,
-            ..initial_signup_from_db
-        }
-    );
-
-    let subsequent_signup = NewSignup {
-        email_address: email_address.clone(),
-        platform_mac: true,
-        platform_linux: false,
-        platform_windows: true,
-        editor_features: vec!["git integration".into(), "clean design".into()],
-        programming_languages: vec!["d".into(), "elm".into()],
-        device_id: Some(format!("different_device_id")),
-        added_to_mailing_list: true,
-        // subsequent signup happens next day
-        created_at: Some(
-            DateTime::from_timestamp_millis(
-                initial_signup_created_at_milliseconds + (1000 * 60 * 60 * 24),
-            )
-            .unwrap(),
-        ),
-    };
-
-    db.create_signup(&subsequent_signup).await.unwrap();
-
-    let subsequent_signup_from_db = db.get_signup(&email_address).await.unwrap();
-
-    assert_eq!(
-        subsequent_signup_from_db.clone(),
-        signup::Model {
-            platform_mac: subsequent_signup.platform_mac,
-            platform_linux: subsequent_signup.platform_linux,
-            platform_windows: subsequent_signup.platform_windows,
-            editor_features: Some(subsequent_signup.editor_features),
-            programming_languages: Some(subsequent_signup.programming_languages),
-            device_id: subsequent_signup.device_id,
-            added_to_mailing_list: subsequent_signup.added_to_mailing_list,
-            // shouldn't overwrite their creation Datetime - user shouldn't lose their spot in line
-            created_at: initial_signup_from_db.created_at,
-            ..subsequent_signup_from_db
-        }
-    );
-}
-
-#[gpui::test]
-async fn test_signups() {
-    let test_db = TestDb::postgres(build_background_executor());
-    let db = test_db.db();
-
-    let usernames = (0..8).map(|i| format!("person-{i}")).collect::<Vec<_>>();
-
-    let all_signups = usernames
-        .iter()
-        .enumerate()
-        .map(|(i, username)| NewSignup {
-            email_address: format!("{username}@example.com"),
-            platform_mac: true,
-            platform_linux: i % 2 == 0,
-            platform_windows: i % 4 == 0,
-            editor_features: vec!["speed".into()],
-            programming_languages: vec!["rust".into(), "c".into()],
-            device_id: Some(format!("device_id_{i}")),
-            added_to_mailing_list: i != 0, // One user failed to subscribe
-            created_at: Some(DateTime::from_timestamp_millis(i as i64).unwrap()), // Signups are consecutive
-        })
-        .collect::<Vec<NewSignup>>();
-
-    // people sign up on the waitlist
-    for signup in &all_signups {
-        // users can sign up multiple times without issues
-        for _ in 0..2 {
-            db.create_signup(&signup).await.unwrap();
-        }
-    }
-
-    assert_eq!(
-        db.get_waitlist_summary().await.unwrap(),
-        WaitlistSummary {
-            count: 8,
-            mac_count: 8,
-            linux_count: 4,
-            windows_count: 2,
-            unknown_count: 0,
-        }
-    );
-
-    // retrieve the next batch of signup emails to send
-    let signups_batch1 = db.get_unsent_invites(3).await.unwrap();
-    let addresses = signups_batch1
-        .iter()
-        .map(|s| &s.email_address)
-        .collect::<Vec<_>>();
-    assert_eq!(
-        addresses,
-        &[
-            all_signups[0].email_address.as_str(),
-            all_signups[1].email_address.as_str(),
-            all_signups[2].email_address.as_str()
-        ]
-    );
-    assert_ne!(
-        signups_batch1[0].email_confirmation_code,
-        signups_batch1[1].email_confirmation_code
-    );
-
-    // the waitlist isn't updated until we record that the emails
-    // were successfully sent.
-    let signups_batch = db.get_unsent_invites(3).await.unwrap();
-    assert_eq!(signups_batch, signups_batch1);
-
-    // once the emails go out, we can retrieve the next batch
-    // of signups.
-    db.record_sent_invites(&signups_batch1).await.unwrap();
-    let signups_batch2 = db.get_unsent_invites(3).await.unwrap();
-    let addresses = signups_batch2
-        .iter()
-        .map(|s| &s.email_address)
-        .collect::<Vec<_>>();
-    assert_eq!(
-        addresses,
-        &[
-            all_signups[3].email_address.as_str(),
-            all_signups[4].email_address.as_str(),
-            all_signups[5].email_address.as_str()
-        ]
-    );
-
-    // the sent invites are excluded from the summary.
-    assert_eq!(
-        db.get_waitlist_summary().await.unwrap(),
-        WaitlistSummary {
-            count: 5,
-            mac_count: 5,
-            linux_count: 2,
-            windows_count: 1,
-            unknown_count: 0,
-        }
-    );
-
-    // user completes the signup process by providing their
-    // github account.
-    let NewUserResult {
-        user_id,
-        inviting_user_id,
-        signup_device_id,
-        ..
-    } = db
-        .create_user_from_invite(
-            &Invite {
-                ..signups_batch1[0].clone()
-            },
-            NewUserParams {
-                github_login: usernames[0].clone(),
-                github_user_id: 0,
-                invite_count: 5,
-            },
-        )
-        .await
-        .unwrap()
-        .unwrap();
-    let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
-    assert!(inviting_user_id.is_none());
-    assert_eq!(user.github_login, usernames[0]);
-    assert_eq!(
-        user.email_address,
-        Some(all_signups[0].email_address.clone())
-    );
-    assert_eq!(user.invite_count, 5);
-    assert_eq!(signup_device_id.unwrap(), "device_id_0");
-
-    // cannot redeem the same signup again.
-    assert!(db
-        .create_user_from_invite(
-            &Invite {
-                email_address: signups_batch1[0].email_address.clone(),
-                email_confirmation_code: signups_batch1[0].email_confirmation_code.clone(),
-            },
-            NewUserParams {
-                github_login: "some-other-github_account".into(),
-                github_user_id: 1,
-                invite_count: 5,
-            },
-        )
-        .await
-        .unwrap()
-        .is_none());
-
-    // cannot redeem a signup with the wrong confirmation code.
-    db.create_user_from_invite(
-        &Invite {
-            email_address: signups_batch1[1].email_address.clone(),
-            email_confirmation_code: "the-wrong-code".to_string(),
-        },
-        NewUserParams {
-            github_login: usernames[1].clone(),
-            github_user_id: 2,
-            invite_count: 5,
-        },
-    )
-    .await
-    .unwrap_err();
-}
-
 fn build_background_executor() -> Arc<Background> {
     Deterministic::new(0).build_background()
 }

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

@@ -0,0 +1,244 @@
+use crate::{
+    db::{Database, NewUserParams},
+    test_both_dbs,
+};
+use std::sync::Arc;
+use time::OffsetDateTime;
+
+test_both_dbs!(
+    test_channel_message_nonces,
+    test_channel_message_nonces_postgres,
+    test_channel_message_nonces_sqlite
+);
+
+async fn test_channel_message_nonces(db: &Arc<Database>) {
+    let user = db
+        .create_user(
+            "user@example.com",
+            false,
+            NewUserParams {
+                github_login: "user".into(),
+                github_user_id: 1,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let channel = db
+        .create_channel("channel", None, "room", user)
+        .await
+        .unwrap();
+
+    let owner_id = db.create_server("test").await.unwrap().0 as u32;
+
+    db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
+        .await
+        .unwrap();
+
+    let msg1_id = db
+        .create_channel_message(channel, user, "1", OffsetDateTime::now_utc(), 1)
+        .await
+        .unwrap();
+    let msg2_id = db
+        .create_channel_message(channel, user, "2", OffsetDateTime::now_utc(), 2)
+        .await
+        .unwrap();
+    let msg3_id = db
+        .create_channel_message(channel, user, "3", OffsetDateTime::now_utc(), 1)
+        .await
+        .unwrap();
+    let msg4_id = db
+        .create_channel_message(channel, user, "4", OffsetDateTime::now_utc(), 2)
+        .await
+        .unwrap();
+
+    assert_ne!(msg1_id, msg2_id);
+    assert_eq!(msg1_id, msg3_id);
+    assert_eq!(msg2_id, msg4_id);
+}
+
+test_both_dbs!(
+    test_channel_message_new_notification,
+    test_channel_message_new_notification_postgres,
+    test_channel_message_new_notification_sqlite
+);
+
+async fn test_channel_message_new_notification(db: &Arc<Database>) {
+    let user = db
+        .create_user(
+            "user_a@example.com",
+            false,
+            NewUserParams {
+                github_login: "user_a".into(),
+                github_user_id: 1,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let observer = db
+        .create_user(
+            "user_b@example.com",
+            false,
+            NewUserParams {
+                github_login: "user_b".into(),
+                github_user_id: 1,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+
+    let channel_1 = db
+        .create_channel("channel", None, "room", user)
+        .await
+        .unwrap();
+
+    let channel_2 = db
+        .create_channel("channel-2", None, "room", user)
+        .await
+        .unwrap();
+
+    db.invite_channel_member(channel_1, observer, user, false)
+        .await
+        .unwrap();
+
+    db.respond_to_channel_invite(channel_1, observer, true)
+        .await
+        .unwrap();
+
+    db.invite_channel_member(channel_2, observer, user, false)
+        .await
+        .unwrap();
+
+    db.respond_to_channel_invite(channel_2, observer, true)
+        .await
+        .unwrap();
+
+    let owner_id = db.create_server("test").await.unwrap().0 as u32;
+    let user_connection_id = rpc::ConnectionId { owner_id, id: 0 };
+
+    db.join_channel_chat(channel_1, user_connection_id, user)
+        .await
+        .unwrap();
+
+    let _ = db
+        .create_channel_message(channel_1, user, "1_1", OffsetDateTime::now_utc(), 1)
+        .await
+        .unwrap();
+
+    let (second_message, _, _) = db
+        .create_channel_message(channel_1, user, "1_2", OffsetDateTime::now_utc(), 2)
+        .await
+        .unwrap();
+
+    let (third_message, _, _) = db
+        .create_channel_message(channel_1, user, "1_3", OffsetDateTime::now_utc(), 3)
+        .await
+        .unwrap();
+
+    db.join_channel_chat(channel_2, user_connection_id, user)
+        .await
+        .unwrap();
+
+    let (fourth_message, _, _) = db
+        .create_channel_message(channel_2, user, "2_1", OffsetDateTime::now_utc(), 4)
+        .await
+        .unwrap();
+
+    // Check that observer has new messages
+    let unseen_messages = db
+        .transaction(|tx| async move {
+            db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
+                .await
+        })
+        .await
+        .unwrap();
+
+    assert_eq!(
+        unseen_messages,
+        [
+            rpc::proto::UnseenChannelMessage {
+                channel_id: channel_1.to_proto(),
+                message_id: third_message.to_proto(),
+            },
+            rpc::proto::UnseenChannelMessage {
+                channel_id: channel_2.to_proto(),
+                message_id: fourth_message.to_proto(),
+            },
+        ]
+    );
+
+    // Observe the second message
+    db.observe_channel_message(channel_1, observer, second_message)
+        .await
+        .unwrap();
+
+    // Make sure the observer still has a new message
+    let unseen_messages = db
+        .transaction(|tx| async move {
+            db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
+                .await
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        unseen_messages,
+        [
+            rpc::proto::UnseenChannelMessage {
+                channel_id: channel_1.to_proto(),
+                message_id: third_message.to_proto(),
+            },
+            rpc::proto::UnseenChannelMessage {
+                channel_id: channel_2.to_proto(),
+                message_id: fourth_message.to_proto(),
+            },
+        ]
+    );
+
+    // Observe the third message,
+    db.observe_channel_message(channel_1, observer, third_message)
+        .await
+        .unwrap();
+
+    // Make sure the observer does not have a new method
+    let unseen_messages = db
+        .transaction(|tx| async move {
+            db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
+                .await
+        })
+        .await
+        .unwrap();
+
+    assert_eq!(
+        unseen_messages,
+        [rpc::proto::UnseenChannelMessage {
+            channel_id: channel_2.to_proto(),
+            message_id: fourth_message.to_proto(),
+        }]
+    );
+
+    // Observe the second message again, should not regress our observed state
+    db.observe_channel_message(channel_1, observer, second_message)
+        .await
+        .unwrap();
+
+    // Make sure the observer does not have a new message
+    let unseen_messages = db
+        .transaction(|tx| async move {
+            db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
+                .await
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        unseen_messages,
+        [rpc::proto::UnseenChannelMessage {
+            channel_id: channel_2.to_proto(),
+            message_id: fourth_message.to_proto(),
+        }]
+    );
+}

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

@@ -2,7 +2,10 @@ mod connection_pool;
 
 use crate::{
     auth,
-    db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId},
+    db::{
+        self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId,
+        ServerId, User, UserId,
+    },
     executor::Executor,
     AppState, Result,
 };
@@ -35,8 +38,8 @@ use lazy_static::lazy_static;
 use prometheus::{register_int_gauge, IntGauge};
 use rpc::{
     proto::{
-        self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage,
-        LiveKitConnectionInfo, RequestMessage,
+        self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage,
+        LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators,
     },
     Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
 };
@@ -56,6 +59,7 @@ use std::{
     },
     time::{Duration, Instant},
 };
+use time::OffsetDateTime;
 use tokio::sync::{watch, Semaphore};
 use tower::ServiceBuilder;
 use tracing::{info_span, instrument, Instrument};
@@ -63,6 +67,9 @@ use tracing::{info_span, instrument, Instrument};
 pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
 
+const MESSAGE_COUNT_PER_PAGE: usize = 100;
+const MAX_MESSAGE_LEN: usize = 1024;
+
 lazy_static! {
     static ref METRIC_CONNECTIONS: IntGauge =
         register_int_gauge!("connections", "number of connections").unwrap();
@@ -243,7 +250,7 @@ impl Server {
             .add_request_handler(remove_contact)
             .add_request_handler(respond_to_contact_request)
             .add_request_handler(create_channel)
-            .add_request_handler(remove_channel)
+            .add_request_handler(delete_channel)
             .add_request_handler(invite_channel_member)
             .add_request_handler(remove_channel_member)
             .add_request_handler(set_channel_member_admin)
@@ -255,11 +262,21 @@ impl Server {
             .add_request_handler(get_channel_members)
             .add_request_handler(respond_to_channel_invite)
             .add_request_handler(join_channel)
+            .add_request_handler(join_channel_chat)
+            .add_message_handler(leave_channel_chat)
+            .add_request_handler(send_channel_message)
+            .add_request_handler(remove_channel_message)
+            .add_request_handler(get_channel_messages)
+            .add_request_handler(link_channel)
+            .add_request_handler(unlink_channel)
+            .add_request_handler(move_channel)
             .add_request_handler(follow)
             .add_message_handler(unfollow)
             .add_message_handler(update_followers)
             .add_message_handler(update_diff_base)
-            .add_request_handler(get_private_user_info);
+            .add_request_handler(get_private_user_info)
+            .add_message_handler(acknowledge_channel_message)
+            .add_message_handler(acknowledge_buffer_version);
 
         Arc::new(server)
     }
@@ -298,9 +315,16 @@ impl Server {
                             .trace_err()
                         {
                             for connection_id in refreshed_channel_buffer.connection_ids {
-                                for message in &refreshed_channel_buffer.removed_collaborators {
-                                    peer.send(connection_id, message.clone()).trace_err();
-                                }
+                                peer.send(
+                                    connection_id,
+                                    proto::UpdateChannelBufferCollaborators {
+                                        channel_id: channel_id.to_proto(),
+                                        collaborators: refreshed_channel_buffer
+                                            .collaborators
+                                            .clone(),
+                                    },
+                                )
+                                .trace_err();
                             }
                         }
                     }
@@ -553,9 +577,8 @@ impl Server {
                 this.app_state.db.set_user_connected_once(user_id, true).await?;
             }
 
-            let (contacts, invite_code, channels_for_user, channel_invites) = future::try_join4(
+            let (contacts, channels_for_user, channel_invites) = future::try_join3(
                 this.app_state.db.get_contacts(user_id),
-                this.app_state.db.get_invite_code_for_user(user_id),
                 this.app_state.db.get_channels_for_user(user_id),
                 this.app_state.db.get_channel_invites_for_user(user_id)
             ).await?;
@@ -568,13 +591,6 @@ impl Server {
                     channels_for_user,
                     channel_invites
                 ))?;
-
-                if let Some((code, count)) = invite_code {
-                    this.peer.send(connection_id, proto::UpdateInviteInfo {
-                        url: format!("{}{}", this.app_state.config.invite_link_prefix, code),
-                        count: count as u32,
-                    })?;
-                }
             }
 
             if let Some(incoming_call) = this.app_state.db.incoming_call_for_user(user_id).await? {
@@ -893,9 +909,8 @@ async fn connection_lost(
                     room_updated(&room, &session.peer);
                 }
             }
-            update_user_contacts(session.user_id, &session).await?;
-
 
+            update_user_contacts(session.user_id, &session).await?;
         }
         _ = teardown.changed().fuse() => {}
     }
@@ -1877,94 +1892,94 @@ async fn follow(
     response: Response<proto::Follow>,
     session: Session,
 ) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
+    let room_id = RoomId::from_proto(request.room_id);
+    let project_id = request.project_id.map(ProjectId::from_proto);
     let leader_id = request
         .leader_id
         .ok_or_else(|| anyhow!("invalid leader id"))?
         .into();
     let follower_id = session.connection_id;
 
-    {
-        let project_connection_ids = session
-            .db()
-            .await
-            .project_connection_ids(project_id, session.connection_id)
-            .await?;
-
-        if !project_connection_ids.contains(&leader_id) {
-            Err(anyhow!("no such peer"))?;
-        }
-    }
+    session
+        .db()
+        .await
+        .check_room_participants(room_id, leader_id, session.connection_id)
+        .await?;
 
-    let mut response_payload = session
+    let response_payload = session
         .peer
         .forward_request(session.connection_id, leader_id, request)
         .await?;
-    response_payload
-        .views
-        .retain(|view| view.leader_id != Some(follower_id.into()));
     response.send(response_payload)?;
 
-    let room = session
-        .db()
-        .await
-        .follow(project_id, leader_id, follower_id)
-        .await?;
-    room_updated(&room, &session.peer);
+    if let Some(project_id) = project_id {
+        let room = session
+            .db()
+            .await
+            .follow(room_id, project_id, leader_id, follower_id)
+            .await?;
+        room_updated(&room, &session.peer);
+    }
 
     Ok(())
 }
 
 async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
+    let room_id = RoomId::from_proto(request.room_id);
+    let project_id = request.project_id.map(ProjectId::from_proto);
     let leader_id = request
         .leader_id
         .ok_or_else(|| anyhow!("invalid leader id"))?
         .into();
     let follower_id = session.connection_id;
 
-    if !session
+    session
         .db()
         .await
-        .project_connection_ids(project_id, session.connection_id)
-        .await?
-        .contains(&leader_id)
-    {
-        Err(anyhow!("no such peer"))?;
-    }
+        .check_room_participants(room_id, leader_id, session.connection_id)
+        .await?;
 
     session
         .peer
         .forward_send(session.connection_id, leader_id, request)?;
 
-    let room = session
-        .db()
-        .await
-        .unfollow(project_id, leader_id, follower_id)
-        .await?;
-    room_updated(&room, &session.peer);
+    if let Some(project_id) = project_id {
+        let room = session
+            .db()
+            .await
+            .unfollow(room_id, project_id, leader_id, follower_id)
+            .await?;
+        room_updated(&room, &session.peer);
+    }
 
     Ok(())
 }
 
 async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
-    let project_connection_ids = session
-        .db
-        .lock()
-        .await
-        .project_connection_ids(project_id, session.connection_id)
-        .await?;
+    let room_id = RoomId::from_proto(request.room_id);
+    let database = session.db.lock().await;
+
+    let connection_ids = if let Some(project_id) = request.project_id {
+        let project_id = ProjectId::from_proto(project_id);
+        database
+            .project_connection_ids(project_id, session.connection_id)
+            .await?
+    } else {
+        database
+            .room_connection_ids(room_id, session.connection_id)
+            .await?
+    };
 
-    let leader_id = request.variant.as_ref().and_then(|variant| match variant {
-        proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
+    // For now, don't send view update messages back to that view's current leader.
+    let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
         proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
-        proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
+        _ => None,
     });
+
     for follower_peer_id in request.follower_ids.iter().copied() {
         let follower_connection_id = follower_peer_id.into();
-        if project_connection_ids.contains(&follower_connection_id)
-            && Some(follower_peer_id) != leader_id
+        if Some(follower_peer_id) != connection_id_to_omit
+            && connection_ids.contains(&follower_connection_id)
         {
             session.peer.forward_send(
                 session.connection_id,
@@ -2194,56 +2209,58 @@ async fn create_channel(
     let channel = proto::Channel {
         id: id.to_proto(),
         name: request.name,
-        parent_id: request.parent_id,
     };
 
-    response.send(proto::ChannelResponse {
+    response.send(proto::CreateChannelResponse {
         channel: Some(channel.clone()),
+        parent_id: request.parent_id,
     })?;
 
-    let mut update = proto::UpdateChannels::default();
-    update.channels.push(channel);
+    let Some(parent_id) = parent_id else {
+        return Ok(());
+    };
 
-    let user_ids_to_notify = if let Some(parent_id) = parent_id {
-        db.get_channel_members(parent_id).await?
-    } else {
-        vec![session.user_id]
+    let update = proto::UpdateChannels {
+        channels: vec![channel],
+        insert_edge: vec![ChannelEdge {
+            parent_id: parent_id.to_proto(),
+            channel_id: id.to_proto(),
+        }],
+        ..Default::default()
     };
 
+    let user_ids_to_notify = db.get_channel_members(parent_id).await?;
+
     let connection_pool = session.connection_pool().await;
     for user_id in user_ids_to_notify {
         for connection_id in connection_pool.user_connection_ids(user_id) {
-            let mut update = update.clone();
             if user_id == session.user_id {
-                update.channel_permissions.push(proto::ChannelPermission {
-                    channel_id: id.to_proto(),
-                    is_admin: true,
-                });
+                continue;
             }
-            session.peer.send(connection_id, update)?;
+            session.peer.send(connection_id, update.clone())?;
         }
     }
 
     Ok(())
 }
 
-async fn remove_channel(
-    request: proto::RemoveChannel,
-    response: Response<proto::RemoveChannel>,
+async fn delete_channel(
+    request: proto::DeleteChannel,
+    response: Response<proto::DeleteChannel>,
     session: Session,
 ) -> Result<()> {
     let db = session.db().await;
 
     let channel_id = request.channel_id;
     let (removed_channels, member_ids) = db
-        .remove_channel(ChannelId::from_proto(channel_id), session.user_id)
+        .delete_channel(ChannelId::from_proto(channel_id), session.user_id)
         .await?;
     response.send(proto::Ack {})?;
 
     // Notify members of removed channels
     let mut update = proto::UpdateChannels::default();
     update
-        .remove_channels
+        .delete_channels
         .extend(removed_channels.into_iter().map(|id| id.to_proto()));
 
     let connection_pool = session.connection_pool().await;
@@ -2276,7 +2293,6 @@ async fn invite_channel_member(
     update.channel_invitations.push(proto::Channel {
         id: channel.id.to_proto(),
         name: channel.name,
-        parent_id: None,
     });
     for connection_id in session
         .connection_pool()
@@ -2303,7 +2319,7 @@ async fn remove_channel_member(
         .await?;
 
     let mut update = proto::UpdateChannels::default();
-    update.remove_channels.push(channel_id.to_proto());
+    update.delete_channels.push(channel_id.to_proto());
 
     for connection_id in session
         .connection_pool()
@@ -2367,9 +2383,8 @@ async fn rename_channel(
     let channel = proto::Channel {
         id: request.channel_id,
         name: new_name,
-        parent_id: None,
     };
-    response.send(proto::ChannelResponse {
+    response.send(proto::RenameChannelResponse {
         channel: Some(channel.clone()),
     })?;
     let mut update = proto::UpdateChannels::default();
@@ -2387,6 +2402,132 @@ async fn rename_channel(
     Ok(())
 }
 
+async fn link_channel(
+    request: proto::LinkChannel,
+    response: Response<proto::LinkChannel>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let to = ChannelId::from_proto(request.to);
+    let channels_to_send = db.link_channel(session.user_id, channel_id, to).await?;
+
+    let members = db.get_channel_members(to).await?;
+    let connection_pool = session.connection_pool().await;
+    let update = proto::UpdateChannels {
+        channels: channels_to_send
+            .channels
+            .into_iter()
+            .map(|channel| proto::Channel {
+                id: channel.id.to_proto(),
+                name: channel.name,
+            })
+            .collect(),
+        insert_edge: channels_to_send.edges,
+        ..Default::default()
+    };
+    for member_id in members {
+        for connection_id in connection_pool.user_connection_ids(member_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
+    }
+
+    response.send(Ack {})?;
+
+    Ok(())
+}
+
+async fn unlink_channel(
+    request: proto::UnlinkChannel,
+    response: Response<proto::UnlinkChannel>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let from = ChannelId::from_proto(request.from);
+
+    db.unlink_channel(session.user_id, channel_id, from).await?;
+
+    let members = db.get_channel_members(from).await?;
+
+    let update = proto::UpdateChannels {
+        delete_edge: vec![proto::ChannelEdge {
+            channel_id: channel_id.to_proto(),
+            parent_id: from.to_proto(),
+        }],
+        ..Default::default()
+    };
+    let connection_pool = session.connection_pool().await;
+    for member_id in members {
+        for connection_id in connection_pool.user_connection_ids(member_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
+    }
+
+    response.send(Ack {})?;
+
+    Ok(())
+}
+
+async fn move_channel(
+    request: proto::MoveChannel,
+    response: Response<proto::MoveChannel>,
+    session: Session,
+) -> Result<()> {
+    let db = session.db().await;
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let from_parent = ChannelId::from_proto(request.from);
+    let to = ChannelId::from_proto(request.to);
+
+    let channels_to_send = db
+        .move_channel(session.user_id, channel_id, from_parent, to)
+        .await?;
+
+    if channels_to_send.is_empty() {
+        response.send(Ack {})?;
+        return Ok(());
+    }
+
+    let members_from = db.get_channel_members(from_parent).await?;
+    let members_to = db.get_channel_members(to).await?;
+
+    let update = proto::UpdateChannels {
+        delete_edge: vec![proto::ChannelEdge {
+            channel_id: channel_id.to_proto(),
+            parent_id: from_parent.to_proto(),
+        }],
+        ..Default::default()
+    };
+    let connection_pool = session.connection_pool().await;
+    for member_id in members_from {
+        for connection_id in connection_pool.user_connection_ids(member_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
+    }
+
+    let update = proto::UpdateChannels {
+        channels: channels_to_send
+            .channels
+            .into_iter()
+            .map(|channel| proto::Channel {
+                id: channel.id.to_proto(),
+                name: channel.name,
+            })
+            .collect(),
+        insert_edge: channels_to_send.edges,
+        ..Default::default()
+    };
+    for member_id in members_to {
+        for connection_id in connection_pool.user_connection_ids(member_id) {
+            session.peer.send(connection_id, update.clone())?;
+        }
+    }
+
+    response.send(Ack {})?;
+
+    Ok(())
+}
+
 async fn get_channel_members(
     request: proto::GetChannelMembers,
     response: Response<proto::GetChannelMembers>,
@@ -2416,14 +2557,22 @@ async fn respond_to_channel_invite(
         .remove_channel_invitations
         .push(channel_id.to_proto());
     if request.accept {
-        let result = db.get_channels_for_user(session.user_id).await?;
+        let result = db.get_channel_for_user(channel_id, session.user_id).await?;
         update
             .channels
-            .extend(result.channels.into_iter().map(|channel| proto::Channel {
-                id: channel.id.to_proto(),
-                name: channel.name,
-                parent_id: channel.parent_id.map(ChannelId::to_proto),
-            }));
+            .extend(
+                result
+                    .channels
+                    .channels
+                    .into_iter()
+                    .map(|channel| proto::Channel {
+                        id: channel.id.to_proto(),
+                        name: channel.name,
+                    }),
+            );
+        update.unseen_channel_messages = result.channel_messages;
+        update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
+        update.insert_edge = result.channels.edges;
         update
             .channel_participants
             .extend(
@@ -2520,18 +2669,12 @@ async fn join_channel_buffer(
         .join_channel_buffer(channel_id, session.user_id, session.connection_id)
         .await?;
 
-    let replica_id = open_response.replica_id;
     let collaborators = open_response.collaborators.clone();
-
     response.send(open_response)?;
 
-    let update = AddChannelBufferCollaborator {
+    let update = UpdateChannelBufferCollaborators {
         channel_id: channel_id.to_proto(),
-        collaborator: Some(proto::Collaborator {
-            user_id: session.user_id.to_proto(),
-            peer_id: Some(session.connection_id.into()),
-            replica_id,
-        }),
+        collaborators: collaborators.clone(),
     };
     channel_buffer_updated(
         session.connection_id,
@@ -2552,7 +2695,7 @@ async fn update_channel_buffer(
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
 
-    let collaborators = db
+    let (collaborators, non_collaborators, epoch, version) = db
         .update_channel_buffer(channel_id, session.user_id, &request.operations)
         .await?;
 
@@ -2565,6 +2708,29 @@ async fn update_channel_buffer(
         },
         &session.peer,
     );
+
+    let pool = &*session.connection_pool().await;
+
+    broadcast(
+        None,
+        non_collaborators
+            .iter()
+            .flat_map(|user_id| pool.user_connection_ids(*user_id)),
+        |peer_id| {
+            session.peer.send(
+                peer_id.into(),
+                proto::UpdateChannels {
+                    unseen_channel_buffer_changes: vec![proto::UnseenChannelBufferChange {
+                        channel_id: channel_id.to_proto(),
+                        epoch: epoch as u64,
+                        version: version.clone(),
+                    }],
+                    ..Default::default()
+                },
+            )
+        },
+    );
+
     Ok(())
 }
 
@@ -2578,8 +2744,8 @@ async fn rejoin_channel_buffers(
         .rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id)
         .await?;
 
-    for buffer in &buffers {
-        let collaborators_to_notify = buffer
+    for rejoined_buffer in &buffers {
+        let collaborators_to_notify = rejoined_buffer
             .buffer
             .collaborators
             .iter()
@@ -2587,10 +2753,9 @@ async fn rejoin_channel_buffers(
         channel_buffer_updated(
             session.connection_id,
             collaborators_to_notify,
-            &proto::UpdateChannelBufferCollaborator {
-                channel_id: buffer.buffer.channel_id,
-                old_peer_id: Some(buffer.old_connection_id.into()),
-                new_peer_id: Some(session.connection_id.into()),
+            &proto::UpdateChannelBufferCollaborators {
+                channel_id: rejoined_buffer.buffer.channel_id,
+                collaborators: rejoined_buffer.buffer.collaborators.clone(),
             },
             &session.peer,
         );
@@ -2611,7 +2776,7 @@ async fn leave_channel_buffer(
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
 
-    let collaborators_to_notify = db
+    let left_buffer = db
         .leave_channel_buffer(channel_id, session.connection_id)
         .await?;
 
@@ -2619,10 +2784,10 @@ async fn leave_channel_buffer(
 
     channel_buffer_updated(
         session.connection_id,
-        collaborators_to_notify,
-        &proto::RemoveChannelBufferCollaborator {
+        left_buffer.connections,
+        &proto::UpdateChannelBufferCollaborators {
             channel_id: channel_id.to_proto(),
-            peer_id: Some(session.connection_id.into()),
+            collaborators: left_buffer.collaborators,
         },
         &session.peer,
     );
@@ -2641,6 +2806,184 @@ fn channel_buffer_updated<T: EnvelopedMessage>(
     });
 }
 
+async fn send_channel_message(
+    request: proto::SendChannelMessage,
+    response: Response<proto::SendChannelMessage>,
+    session: Session,
+) -> Result<()> {
+    // Validate the message body.
+    let body = request.body.trim().to_string();
+    if body.len() > MAX_MESSAGE_LEN {
+        return Err(anyhow!("message is too long"))?;
+    }
+    if body.is_empty() {
+        return Err(anyhow!("message can't be blank"))?;
+    }
+
+    let timestamp = OffsetDateTime::now_utc();
+    let nonce = request
+        .nonce
+        .ok_or_else(|| anyhow!("nonce can't be blank"))?;
+
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let (message_id, connection_ids, non_participants) = session
+        .db()
+        .await
+        .create_channel_message(
+            channel_id,
+            session.user_id,
+            &body,
+            timestamp,
+            nonce.clone().into(),
+        )
+        .await?;
+    let message = proto::ChannelMessage {
+        sender_id: session.user_id.to_proto(),
+        id: message_id.to_proto(),
+        body,
+        timestamp: timestamp.unix_timestamp() as u64,
+        nonce: Some(nonce),
+    };
+    broadcast(Some(session.connection_id), connection_ids, |connection| {
+        session.peer.send(
+            connection,
+            proto::ChannelMessageSent {
+                channel_id: channel_id.to_proto(),
+                message: Some(message.clone()),
+            },
+        )
+    });
+    response.send(proto::SendChannelMessageResponse {
+        message: Some(message),
+    })?;
+
+    let pool = &*session.connection_pool().await;
+    broadcast(
+        None,
+        non_participants
+            .iter()
+            .flat_map(|user_id| pool.user_connection_ids(*user_id)),
+        |peer_id| {
+            session.peer.send(
+                peer_id.into(),
+                proto::UpdateChannels {
+                    unseen_channel_messages: vec![proto::UnseenChannelMessage {
+                        channel_id: channel_id.to_proto(),
+                        message_id: message_id.to_proto(),
+                    }],
+                    ..Default::default()
+                },
+            )
+        },
+    );
+
+    Ok(())
+}
+
+async fn remove_channel_message(
+    request: proto::RemoveChannelMessage,
+    response: Response<proto::RemoveChannelMessage>,
+    session: Session,
+) -> Result<()> {
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let message_id = MessageId::from_proto(request.message_id);
+    let connection_ids = session
+        .db()
+        .await
+        .remove_channel_message(channel_id, message_id, session.user_id)
+        .await?;
+    broadcast(Some(session.connection_id), connection_ids, |connection| {
+        session.peer.send(connection, request.clone())
+    });
+    response.send(proto::Ack {})?;
+    Ok(())
+}
+
+async fn acknowledge_channel_message(
+    request: proto::AckChannelMessage,
+    session: Session,
+) -> Result<()> {
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let message_id = MessageId::from_proto(request.message_id);
+    session
+        .db()
+        .await
+        .observe_channel_message(channel_id, session.user_id, message_id)
+        .await?;
+    Ok(())
+}
+
+async fn acknowledge_buffer_version(
+    request: proto::AckBufferOperation,
+    session: Session,
+) -> Result<()> {
+    let buffer_id = BufferId::from_proto(request.buffer_id);
+    session
+        .db()
+        .await
+        .observe_buffer_version(
+            buffer_id,
+            session.user_id,
+            request.epoch as i32,
+            &request.version,
+        )
+        .await?;
+    Ok(())
+}
+
+async fn join_channel_chat(
+    request: proto::JoinChannelChat,
+    response: Response<proto::JoinChannelChat>,
+    session: Session,
+) -> Result<()> {
+    let channel_id = ChannelId::from_proto(request.channel_id);
+
+    let db = session.db().await;
+    db.join_channel_chat(channel_id, session.connection_id, session.user_id)
+        .await?;
+    let messages = db
+        .get_channel_messages(channel_id, session.user_id, MESSAGE_COUNT_PER_PAGE, None)
+        .await?;
+    response.send(proto::JoinChannelChatResponse {
+        done: messages.len() < MESSAGE_COUNT_PER_PAGE,
+        messages,
+    })?;
+    Ok(())
+}
+
+async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) -> Result<()> {
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    session
+        .db()
+        .await
+        .leave_channel_chat(channel_id, session.connection_id, session.user_id)
+        .await?;
+    Ok(())
+}
+
+async fn get_channel_messages(
+    request: proto::GetChannelMessages,
+    response: Response<proto::GetChannelMessages>,
+    session: Session,
+) -> Result<()> {
+    let channel_id = ChannelId::from_proto(request.channel_id);
+    let messages = session
+        .db()
+        .await
+        .get_channel_messages(
+            channel_id,
+            session.user_id,
+            MESSAGE_COUNT_PER_PAGE,
+            Some(MessageId::from_proto(request.before_message_id)),
+        )
+        .await?;
+    response.send(proto::GetChannelMessagesResponse {
+        done: messages.len() < MESSAGE_COUNT_PER_PAGE,
+        messages,
+    })?;
+    Ok(())
+}
+
 async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> {
     let project_id = ProjectId::from_proto(request.project_id);
     let project_connection_ids = session
@@ -2716,14 +3059,17 @@ fn build_initial_channels_update(
 ) -> proto::UpdateChannels {
     let mut update = proto::UpdateChannels::default();
 
-    for channel in channels.channels {
+    for channel in channels.channels.channels {
         update.channels.push(proto::Channel {
             id: channel.id.to_proto(),
             name: channel.name,
-            parent_id: channel.parent_id.map(|id| id.to_proto()),
         });
     }
 
+    update.unseen_channel_buffer_changes = channels.unseen_buffer_changes;
+    update.unseen_channel_messages = channels.channel_messages;
+    update.insert_edge = channels.channels.edges;
+
     for (channel_id, participants) in channels.channel_participants {
         update
             .channel_participants
@@ -2749,7 +3095,6 @@ fn build_initial_channels_update(
         update.channel_invitations.push(proto::Channel {
             id: channel.id.to_proto(),
             name: channel.name,
-            parent_id: None,
         });
     }
 
@@ -2972,13 +3317,13 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
         .leave_channel_buffers(session.connection_id)
         .await?;
 
-    for (channel_id, connections) in left_channel_buffers {
+    for left_buffer in left_channel_buffers {
         channel_buffer_updated(
             session.connection_id,
-            connections,
-            &proto::RemoveChannelBufferCollaborator {
-                channel_id: channel_id.to_proto(),
-                peer_id: Some(session.connection_id.into()),
+            left_buffer.connections,
+            &proto::UpdateChannelBufferCollaborators {
+                channel_id: left_buffer.channel_id.to_proto(),
+                collaborators: left_buffer.collaborators,
             },
             &session.peer,
         );

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

@@ -2,7 +2,9 @@ use call::Room;
 use gpui::{ModelHandle, TestAppContext};
 
 mod channel_buffer_tests;
+mod channel_message_tests;
 mod channel_tests;
+mod following_tests;
 mod integration_tests;
 mod random_channel_buffer_tests;
 mod random_project_collaboration_tests;

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

@@ -3,15 +3,17 @@ use crate::{
     tests::TestServer,
 };
 use call::ActiveCall;
-use channel::Channel;
-use client::UserId;
+use channel::{Channel, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
+use client::ParticipantIndex;
+use client::{Collaborator, UserId};
 use collab_ui::channel_view::ChannelView;
 use collections::HashMap;
+use editor::{Anchor, Editor, ToOffset};
 use futures::future;
-use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
-use rpc::{proto, RECEIVE_TIMEOUT};
+use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
+use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
 use serde_json::json;
-use std::sync::Arc;
+use std::{ops::Range, sync::Arc};
 
 #[gpui::test]
 async fn test_core_channel_buffers(
@@ -25,7 +27,7 @@ async fn test_core_channel_buffers(
     let client_b = server.create_client(cx_b, "user_b").await;
 
     let channel_id = server
-        .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
         .await;
 
     // Client A joins the channel buffer
@@ -100,7 +102,7 @@ async fn test_core_channel_buffers(
     channel_buffer_b.read_with(cx_b, |buffer, _| {
         assert_collaborators(
             &buffer.collaborators(),
-            &[client_b.user_id(), client_a.user_id()],
+            &[client_a.user_id(), client_b.user_id()],
         );
     });
 
@@ -120,10 +122,10 @@ async fn test_core_channel_buffers(
 }
 
 #[gpui::test]
-async fn test_channel_buffer_replica_ids(
+async fn test_channel_notes_participant_indices(
     deterministic: Arc<Deterministic>,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
+    mut cx_a: &mut TestAppContext,
+    mut cx_b: &mut TestAppContext,
     cx_c: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
@@ -132,154 +134,195 @@ async fn test_channel_buffer_replica_ids(
     let client_b = server.create_client(cx_b, "user_b").await;
     let client_c = server.create_client(cx_c, "user_c").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_c.update(editor::init);
+
     let channel_id = server
         .make_channel(
             "the-channel",
+            None,
             (&client_a, cx_a),
             &mut [(&client_b, cx_b), (&client_c, cx_c)],
         )
         .await;
 
-    let active_call_a = cx_a.read(ActiveCall::global);
-    let active_call_b = cx_b.read(ActiveCall::global);
-    let active_call_c = cx_c.read(ActiveCall::global);
-
-    // Clients A and B join a channel.
-    active_call_a
-        .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
-        .await
-        .unwrap();
-    active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_id, cx))
-        .await
-        .unwrap();
-
-    // Clients A, B, and C join a channel buffer
-    // C first so that the replica IDs in the project and the channel buffer are different
-    let channel_buffer_c = client_c
-        .channel_store()
-        .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-    let channel_buffer_b = client_b
-        .channel_store()
-        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
+    client_a
+        .fs()
+        .insert_tree("/root", json!({"file.txt": "123"}))
+        .await;
+    let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await;
+    let project_b = client_b.build_empty_local_project(cx_b);
+    let project_c = client_c.build_empty_local_project(cx_c);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+    let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
+
+    // Clients A, B, and C open the channel notes
+    let channel_view_a = cx_a
+        .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
         .await
         .unwrap();
-    let channel_buffer_a = client_a
-        .channel_store()
-        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
+    let channel_view_b = cx_b
+        .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
         .await
         .unwrap();
-
-    // Client B shares a project
-    client_b
-        .fs()
-        .insert_tree("/dir", json!({ "file.txt": "contents" }))
-        .await;
-    let (project_b, _) = client_b.build_local_project("/dir", cx_b).await;
-    let shared_project_id = active_call_b
-        .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
+    let channel_view_c = cx_c
+        .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
         .await
         .unwrap();
 
-    // Client A joins the project
-    let project_a = client_a.build_remote_project(shared_project_id, cx_a).await;
-    deterministic.run_until_parked();
-
-    // Client C is in a separate project.
-    client_c.fs().insert_tree("/dir", json!({})).await;
-    let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await;
-
-    // Note that each user has a different replica id in the projects vs the
-    // channel buffer.
-    channel_buffer_a.read_with(cx_a, |channel_buffer, cx| {
-        assert_eq!(project_a.read(cx).replica_id(), 1);
-        assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2);
+    // Clients A, B, and C all insert and select some text
+    channel_view_a.update(cx_a, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            editor.insert("a", cx);
+            editor.change_selections(None, cx, |selections| {
+                selections.select_ranges(vec![0..1]);
+            });
+        });
     });
-    channel_buffer_b.read_with(cx_b, |channel_buffer, cx| {
-        assert_eq!(project_b.read(cx).replica_id(), 0);
-        assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1);
+    deterministic.run_until_parked();
+    channel_view_b.update(cx_b, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            editor.move_down(&Default::default(), cx);
+            editor.insert("b", cx);
+            editor.change_selections(None, cx, |selections| {
+                selections.select_ranges(vec![1..2]);
+            });
+        });
     });
-    channel_buffer_c.read_with(cx_c, |channel_buffer, cx| {
-        // C is not in the project
-        assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0);
+    deterministic.run_until_parked();
+    channel_view_c.update(cx_c, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            editor.move_down(&Default::default(), cx);
+            editor.insert("c", cx);
+            editor.change_selections(None, cx, |selections| {
+                selections.select_ranges(vec![2..3]);
+            });
+        });
     });
 
-    let channel_window_a =
-        cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx));
-    let channel_window_b =
-        cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx));
-    let channel_window_c = cx_c.add_window(|cx| {
-        ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx)
+    // Client A sees clients B and C without assigned colors, because they aren't
+    // in a call together.
+    deterministic.run_until_parked();
+    channel_view_a.update(cx_a, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
+        });
     });
 
-    let channel_view_a = channel_window_a.root(cx_a);
-    let channel_view_b = channel_window_b.root(cx_b);
-    let channel_view_c = channel_window_c.root(cx_c);
+    // Clients A and B join the same call.
+    for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
+        call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
+            .await
+            .unwrap();
+    }
 
-    // For clients A and B, the replica ids in the channel buffer are mapped
-    // so that they match the same users' replica ids in their shared project.
-    channel_view_a.read_with(cx_a, |view, cx| {
-        assert_eq!(
-            view.editor.read(cx).replica_id_map().unwrap(),
-            &[(1, 0), (2, 1)].into_iter().collect::<HashMap<_, _>>()
-        );
+    // Clients A and B see each other with two different assigned colors. Client C
+    // still doesn't have a color.
+    deterministic.run_until_parked();
+    channel_view_a.update(cx_a, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            assert_remote_selections(
+                editor,
+                &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
+                cx,
+            );
+        });
     });
-    channel_view_b.read_with(cx_b, |view, cx| {
-        assert_eq!(
-            view.editor.read(cx).replica_id_map().unwrap(),
-            &[(1, 0), (2, 1)].into_iter().collect::<HashMap<u16, u16>>(),
-        )
+    channel_view_b.update(cx_b, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            assert_remote_selections(
+                editor,
+                &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
+                cx,
+            );
+        });
     });
 
-    // Client C only sees themself, as they're not part of any shared project
-    channel_view_c.read_with(cx_c, |view, cx| {
-        assert_eq!(
-            view.editor.read(cx).replica_id_map().unwrap(),
-            &[(0, 0)].into_iter().collect::<HashMap<u16, u16>>(),
-        );
-    });
+    // Client A shares a project, and client B joins.
+    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;
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
 
-    // Client C joins the project that clients A and B are in.
-    active_call_c
-        .update(cx_c, |call, cx| call.join_channel(channel_id, cx))
+    // Clients A and B open the same file.
+    let editor_a = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
+        })
         .await
+        .unwrap()
+        .downcast::<Editor>()
         .unwrap();
-    let project_c = client_c.build_remote_project(shared_project_id, cx_c).await;
-    deterministic.run_until_parked();
-    project_c.read_with(cx_c, |project, _| {
-        assert_eq!(project.replica_id(), 2);
+    let editor_b = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    editor_a.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |selections| {
+            selections.select_ranges(vec![0..1]);
+        });
+    });
+    editor_b.update(cx_b, |editor, cx| {
+        editor.change_selections(None, cx, |selections| {
+            selections.select_ranges(vec![2..3]);
+        });
     });
+    deterministic.run_until_parked();
 
-    // For clients A and B, client C's replica id in the channel buffer is
-    // now mapped to their replica id in the shared project.
-    channel_view_a.read_with(cx_a, |view, cx| {
-        assert_eq!(
-            view.editor.read(cx).replica_id_map().unwrap(),
-            &[(1, 0), (2, 1), (0, 2)]
-                .into_iter()
-                .collect::<HashMap<_, _>>()
-        );
+    // Clients A and B see each other with the same colors as in the channel notes.
+    editor_a.update(cx_a, |editor, cx| {
+        assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
     });
-    channel_view_b.read_with(cx_b, |view, cx| {
-        assert_eq!(
-            view.editor.read(cx).replica_id_map().unwrap(),
-            &[(1, 0), (2, 1), (0, 2)]
-                .into_iter()
-                .collect::<HashMap<_, _>>(),
-        )
+    editor_b.update(cx_b, |editor, cx| {
+        assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
     });
 }
 
+#[track_caller]
+fn assert_remote_selections(
+    editor: &mut Editor,
+    expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
+    cx: &mut ViewContext<Editor>,
+) {
+    let snapshot = editor.snapshot(cx);
+    let range = Anchor::min()..Anchor::max();
+    let remote_selections = snapshot
+        .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
+        .map(|s| {
+            let start = s.selection.start.to_offset(&snapshot.buffer_snapshot);
+            let end = s.selection.end.to_offset(&snapshot.buffer_snapshot);
+            (s.participant_index, start..end)
+        })
+        .collect::<Vec<_>>();
+    assert_eq!(
+        remote_selections, expected_selections,
+        "incorrect remote selections"
+    );
+}
+
 #[gpui::test]
-async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
+async fn test_multiple_handles_to_channel_buffer(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+) {
     deterministic.forbid_parking();
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
 
     let channel_id = server
-        .make_channel("the-channel", (&client_a, cx_a), &mut [])
+        .make_channel("the-channel", None, (&client_a, cx_a), &mut [])
         .await;
 
     let channel_buffer_1 = client_a
@@ -341,7 +384,12 @@ async fn test_channel_buffer_disconnect(
     let client_b = server.create_client(cx_b, "user_b").await;
 
     let channel_id = server
-        .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .make_channel(
+            "the-channel",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b)],
+        )
         .await;
 
     let channel_buffer_a = client_a
@@ -362,10 +410,7 @@ async fn test_channel_buffer_disconnect(
     channel_buffer_a.update(cx_a, |buffer, _| {
         assert_eq!(
             buffer.channel().as_ref(),
-            &Channel {
-                id: channel_id,
-                name: "the-channel".to_string()
-            }
+            &channel(channel_id, "the-channel")
         );
         assert!(!buffer.is_connected());
     });
@@ -390,15 +435,21 @@ async fn test_channel_buffer_disconnect(
     channel_buffer_b.update(cx_b, |buffer, _| {
         assert_eq!(
             buffer.channel().as_ref(),
-            &Channel {
-                id: channel_id,
-                name: "the-channel".to_string()
-            }
+            &channel(channel_id, "the-channel")
         );
         assert!(!buffer.is_connected());
     });
 }
 
+fn channel(id: u64, name: &'static str) -> Channel {
+    Channel {
+        id,
+        name: name.to_string(),
+        unseen_note_version: None,
+        unseen_message_id: None,
+    }
+}
+
 #[gpui::test]
 async fn test_rejoin_channel_buffer(
     deterministic: Arc<Deterministic>,
@@ -411,7 +462,12 @@ async fn test_rejoin_channel_buffer(
     let client_b = server.create_client(cx_b, "user_b").await;
 
     let channel_id = server
-        .make_channel("the-channel", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .make_channel(
+            "the-channel",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b)],
+        )
         .await;
 
     let channel_buffer_a = client_a
@@ -491,6 +547,7 @@ async fn test_channel_buffers_and_server_restarts(
     let channel_id = server
         .make_channel(
             "the-channel",
+            None,
             (&client_a, cx_a),
             &mut [(&client_b, cx_b), (&client_c, cx_c)],
         )
@@ -553,26 +610,284 @@ async fn test_channel_buffers_and_server_restarts(
 
     channel_buffer_a.read_with(cx_a, |buffer_a, _| {
         channel_buffer_b.read_with(cx_b, |buffer_b, _| {
-            assert_eq!(
-                buffer_a
-                    .collaborators()
-                    .iter()
-                    .map(|c| c.user_id)
-                    .collect::<Vec<_>>(),
-                vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()]
+            assert_collaborators(
+                buffer_a.collaborators(),
+                &[client_a.user_id(), client_b.user_id()],
             );
             assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
         });
     });
 }
 
+#[gpui::test(iterations = 10)]
+async fn test_following_to_channel_notes_without_a_shared_project(
+    deterministic: Arc<Deterministic>,
+    mut cx_a: &mut TestAppContext,
+    mut cx_b: &mut TestAppContext,
+    mut cx_c: &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;
+
+    let client_c = server.create_client(cx_c, "user_c").await;
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+    cx_c.update(editor::init);
+    cx_a.update(collab_ui::channel_view::init);
+    cx_b.update(collab_ui::channel_view::init);
+    cx_c.update(collab_ui::channel_view::init);
+
+    let channel_1_id = server
+        .make_channel(
+            "channel-1",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
+        )
+        .await;
+    let channel_2_id = server
+        .make_channel(
+            "channel-2",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
+        )
+        .await;
+
+    // Clients A, B, and C join a channel.
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+    let active_call_c = cx_c.read(ActiveCall::global);
+    for (call, cx) in [
+        (&active_call_a, &mut cx_a),
+        (&active_call_b, &mut cx_b),
+        (&active_call_c, &mut cx_c),
+    ] {
+        call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
+            .await
+            .unwrap();
+    }
+    deterministic.run_until_parked();
+
+    // Clients A, B, and C all open their own unshared projects.
+    client_a.fs().insert_tree("/a", json!({})).await;
+    client_b.fs().insert_tree("/b", json!({})).await;
+    client_c.fs().insert_tree("/c", json!({})).await;
+    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
+    let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
+    let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+    let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
+
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+
+    // Client A opens the notes for channel 1.
+    let channel_view_1_a = cx_a
+        .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
+        .await
+        .unwrap();
+    channel_view_1_a.update(cx_a, |notes, cx| {
+        assert_eq!(notes.channel(cx).name, "channel-1");
+        notes.editor.update(cx, |editor, cx| {
+            editor.insert("Hello from A.", cx);
+            editor.change_selections(None, cx, |selections| {
+                selections.select_ranges(vec![3..4]);
+            });
+        });
+    });
+
+    // Client B follows client A.
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    // Client B is taken to the notes for channel 1, with the same
+    // text selected as client A.
+    deterministic.run_until_parked();
+    let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(
+            workspace.leader_for_pane(workspace.active_pane()),
+            Some(client_a.peer_id().unwrap())
+        );
+        workspace
+            .active_item(cx)
+            .expect("no active item")
+            .downcast::<ChannelView>()
+            .expect("active item is not a channel view")
+    });
+    channel_view_1_b.read_with(cx_b, |notes, cx| {
+        assert_eq!(notes.channel(cx).name, "channel-1");
+        let editor = notes.editor.read(cx);
+        assert_eq!(editor.text(cx), "Hello from A.");
+        assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
+    });
+
+    // Client A opens the notes for channel 2.
+    let channel_view_2_a = cx_a
+        .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
+        .await
+        .unwrap();
+    channel_view_2_a.read_with(cx_a, |notes, cx| {
+        assert_eq!(notes.channel(cx).name, "channel-2");
+    });
+
+    // Client B is taken to the notes for channel 2.
+    deterministic.run_until_parked();
+    let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(
+            workspace.leader_for_pane(workspace.active_pane()),
+            Some(client_a.peer_id().unwrap())
+        );
+        workspace
+            .active_item(cx)
+            .expect("no active item")
+            .downcast::<ChannelView>()
+            .expect("active item is not a channel view")
+    });
+    channel_view_2_b.read_with(cx_b, |notes, cx| {
+        assert_eq!(notes.channel(cx).name, "channel-2");
+    });
+}
+
+#[gpui::test]
+async fn test_channel_buffer_changes(
+    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;
+
+    let channel_id = server
+        .make_channel(
+            "the-channel",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b)],
+        )
+        .await;
+
+    let channel_buffer_a = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
+        .await
+        .unwrap();
+
+    // Client A makes an edit, and client B should see that the note has changed.
+    channel_buffer_a.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "1")], None, cx);
+        })
+    });
+    deterministic.run_until_parked();
+
+    let has_buffer_changed = cx_b.read(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_channel_buffer_changed(channel_id)
+            .unwrap()
+    });
+    assert!(has_buffer_changed);
+
+    // Opening the buffer should clear the changed flag.
+    let project_b = client_b.build_empty_local_project(cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+    let channel_view_b = cx_b
+        .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+
+    let has_buffer_changed = cx_b.read(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_channel_buffer_changed(channel_id)
+            .unwrap()
+    });
+    assert!(!has_buffer_changed);
+
+    // Editing the channel while the buffer is open should not show that the buffer has changed.
+    channel_buffer_a.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "2")], None, cx);
+        })
+    });
+    deterministic.run_until_parked();
+
+    let has_buffer_changed = cx_b.read(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_channel_buffer_changed(channel_id)
+            .unwrap()
+    });
+    assert!(!has_buffer_changed);
+
+    deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL);
+
+    // Test that the server is tracking things correctly, and we retain our 'not changed'
+    // state across a disconnect
+    server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic);
+    let has_buffer_changed = cx_b.read(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_channel_buffer_changed(channel_id)
+            .unwrap()
+    });
+    assert!(!has_buffer_changed);
+
+    // Closing the buffer should re-enable change tracking
+    cx_b.update(|cx| {
+        workspace_b.update(cx, |workspace, cx| {
+            workspace.close_all_items_and_panes(&Default::default(), cx)
+        });
+
+        drop(channel_view_b)
+    });
+
+    deterministic.run_until_parked();
+
+    channel_buffer_a.update(cx_a, |buffer, cx| {
+        buffer.buffer().update(cx, |buffer, cx| {
+            buffer.edit([(0..0, "3")], None, cx);
+        })
+    });
+    deterministic.run_until_parked();
+
+    let has_buffer_changed = cx_b.read(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_channel_buffer_changed(channel_id)
+            .unwrap()
+    });
+    assert!(has_buffer_changed);
+}
+
 #[track_caller]
-fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option<UserId>]) {
+fn assert_collaborators(collaborators: &HashMap<PeerId, Collaborator>, ids: &[Option<UserId>]) {
+    let mut user_ids = collaborators
+        .values()
+        .map(|collaborator| collaborator.user_id)
+        .collect::<Vec<_>>();
+    user_ids.sort();
     assert_eq!(
-        collaborators
-            .into_iter()
-            .map(|collaborator| collaborator.user_id)
-            .collect::<Vec<_>>(),
+        user_ids,
         ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
     );
 }

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

@@ -0,0 +1,360 @@
+use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
+use channel::{ChannelChat, ChannelMessageId};
+use collab_ui::chat_panel::ChatPanel;
+use gpui::{executor::Deterministic, BorrowAppContext, ModelHandle, TestAppContext};
+use std::sync::Arc;
+use workspace::dock::Panel;
+
+#[gpui::test]
+async fn test_basic_channel_messages(
+    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;
+
+    let channel_id = server
+        .make_channel(
+            "the-channel",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b)],
+        )
+        .await;
+
+    let channel_chat_a = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
+        .await
+        .unwrap();
+    let channel_chat_b = client_b
+        .channel_store()
+        .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
+        .await
+        .unwrap();
+
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
+        .await
+        .unwrap();
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    channel_chat_b
+        .update(cx_b, |c, cx| c.send_message("three".into(), cx).unwrap())
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    channel_chat_a.update(cx_a, |c, _| {
+        assert_eq!(
+            c.messages()
+                .iter()
+                .map(|m| m.body.as_str())
+                .collect::<Vec<_>>(),
+            vec!["one", "two", "three"]
+        );
+    })
+}
+
+#[gpui::test]
+async fn test_rejoin_channel_chat(
+    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;
+
+    let channel_id = server
+        .make_channel(
+            "the-channel",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b)],
+        )
+        .await;
+
+    let channel_chat_a = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
+        .await
+        .unwrap();
+    let channel_chat_b = client_b
+        .channel_store()
+        .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
+        .await
+        .unwrap();
+
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
+        .await
+        .unwrap();
+    channel_chat_b
+        .update(cx_b, |c, cx| c.send_message("two".into(), cx).unwrap())
+        .await
+        .unwrap();
+
+    server.forbid_connections();
+    server.disconnect_client(client_a.peer_id().unwrap());
+
+    // While client A is disconnected, clients A and B both send new messages.
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
+        .await
+        .unwrap_err();
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
+        .await
+        .unwrap_err();
+    channel_chat_b
+        .update(cx_b, |c, cx| c.send_message("five".into(), cx).unwrap())
+        .await
+        .unwrap();
+    channel_chat_b
+        .update(cx_b, |c, cx| c.send_message("six".into(), cx).unwrap())
+        .await
+        .unwrap();
+
+    // Client A reconnects.
+    server.allow_connections();
+    deterministic.advance_clock(RECONNECT_TIMEOUT);
+
+    // Client A fetches the messages that were sent while they were disconnected
+    // and resends their own messages which failed to send.
+    let expected_messages = &["one", "two", "five", "six", "three", "four"];
+    assert_messages(&channel_chat_a, expected_messages, cx_a);
+    assert_messages(&channel_chat_b, expected_messages, cx_b);
+}
+
+#[gpui::test]
+async fn test_remove_channel_message(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &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;
+    let client_c = server.create_client(cx_c, "user_c").await;
+
+    let channel_id = server
+        .make_channel(
+            "the-channel",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
+        )
+        .await;
+
+    let channel_chat_a = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
+        .await
+        .unwrap();
+    let channel_chat_b = client_b
+        .channel_store()
+        .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
+        .await
+        .unwrap();
+
+    // Client A sends some messages.
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
+        .await
+        .unwrap();
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
+        .await
+        .unwrap();
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
+        .await
+        .unwrap();
+
+    // Clients A and B see all of the messages.
+    deterministic.run_until_parked();
+    let expected_messages = &["one", "two", "three"];
+    assert_messages(&channel_chat_a, expected_messages, cx_a);
+    assert_messages(&channel_chat_b, expected_messages, cx_b);
+
+    // Client A deletes one of their messages.
+    channel_chat_a
+        .update(cx_a, |c, cx| {
+            let ChannelMessageId::Saved(id) = c.message(1).id else {
+                panic!("message not saved")
+            };
+            c.remove_message(id, cx)
+        })
+        .await
+        .unwrap();
+
+    // Client B sees that the message is gone.
+    deterministic.run_until_parked();
+    let expected_messages = &["one", "three"];
+    assert_messages(&channel_chat_a, expected_messages, cx_a);
+    assert_messages(&channel_chat_b, expected_messages, cx_b);
+
+    // Client C joins the channel chat, and does not see the deleted message.
+    let channel_chat_c = client_c
+        .channel_store()
+        .update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx))
+        .await
+        .unwrap();
+    assert_messages(&channel_chat_c, expected_messages, cx_c);
+}
+
+#[track_caller]
+fn assert_messages(chat: &ModelHandle<ChannelChat>, messages: &[&str], cx: &mut TestAppContext) {
+    assert_eq!(
+        chat.read_with(cx, |chat, _| chat
+            .messages()
+            .iter()
+            .map(|m| m.body.clone())
+            .collect::<Vec<_>>(),),
+        messages
+    );
+}
+
+#[gpui::test]
+async fn test_channel_message_changes(
+    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;
+
+    let channel_id = server
+        .make_channel(
+            "the-channel",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b)],
+        )
+        .await;
+
+    // Client A sends a message, client B should see that there is a new message.
+    let channel_chat_a = client_a
+        .channel_store()
+        .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
+        .await
+        .unwrap();
+
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    let b_has_messages = cx_b.read_with(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_new_messages(channel_id)
+            .unwrap()
+    });
+
+    assert!(b_has_messages);
+
+    // Opening the chat should clear the changed flag.
+    cx_b.update(|cx| {
+        collab_ui::init(&client_b.app_state, cx);
+    });
+    let project_b = client_b.build_empty_local_project(cx_b);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+    let chat_panel_b = workspace_b.update(cx_b, |workspace, cx| ChatPanel::new(workspace, cx));
+    chat_panel_b
+        .update(cx_b, |chat_panel, cx| {
+            chat_panel.set_active(true, cx);
+            chat_panel.select_channel(channel_id, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    let b_has_messages = cx_b.read_with(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_new_messages(channel_id)
+            .unwrap()
+    });
+
+    assert!(!b_has_messages);
+
+    // Sending a message while the chat is open should not change the flag.
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    let b_has_messages = cx_b.read_with(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_new_messages(channel_id)
+            .unwrap()
+    });
+
+    assert!(!b_has_messages);
+
+    // Sending a message while the chat is closed should change the flag.
+    chat_panel_b.update(cx_b, |chat_panel, cx| {
+        chat_panel.set_active(false, cx);
+    });
+
+    // Sending a message while the chat is open should not change the flag.
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    let b_has_messages = cx_b.read_with(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_new_messages(channel_id)
+            .unwrap()
+    });
+
+    assert!(b_has_messages);
+
+    // Closing the chat should re-enable change tracking
+    cx_b.update(|_| drop(chat_panel_b));
+
+    channel_chat_a
+        .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+
+    let b_has_messages = cx_b.read_with(|cx| {
+        client_b
+            .channel_store()
+            .read(cx)
+            .has_new_messages(channel_id)
+            .unwrap()
+    });
+
+    assert!(b_has_messages);
+}

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

@@ -56,7 +56,10 @@ async fn test_core_channels(
     );
 
     client_b.channel_store().read_with(cx_b, |channels, _| {
-        assert!(channels.channels().collect::<Vec<_>>().is_empty())
+        assert!(channels
+            .channel_dag_entries()
+            .collect::<Vec<_>>()
+            .is_empty())
     });
 
     // Invite client B to channel A as client A.
@@ -326,7 +329,7 @@ async fn test_joining_channel_ancestor_member(
     let client_b = server.create_client(cx_b, "user_b").await;
 
     let parent_id = server
-        .make_channel("parent", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .make_channel("parent", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
         .await;
 
     let sub_id = client_a
@@ -361,6 +364,7 @@ async fn test_channel_room(
     let zed_id = server
         .make_channel(
             "zed",
+            None,
             (&client_a, cx_a),
             &mut [(&client_b, cx_b), (&client_c, cx_c)],
         )
@@ -544,9 +548,11 @@ async fn test_channel_jumping(deterministic: Arc<Deterministic>, cx_a: &mut Test
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
 
-    let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await;
+    let zed_id = server
+        .make_channel("zed", None, (&client_a, cx_a), &mut [])
+        .await;
     let rust_id = server
-        .make_channel("rust", (&client_a, cx_a), &mut [])
+        .make_channel("rust", None, (&client_a, cx_a), &mut [])
         .await;
 
     let active_call_a = cx_a.read(ActiveCall::global);
@@ -597,7 +603,7 @@ async fn test_permissions_update_while_invited(
     let client_b = server.create_client(cx_b, "user_b").await;
 
     let rust_id = server
-        .make_channel("rust", (&client_a, cx_a), &mut [])
+        .make_channel("rust", None, (&client_a, cx_a), &mut [])
         .await;
 
     client_a
@@ -658,7 +664,7 @@ async fn test_channel_rename(
     let client_b = server.create_client(cx_b, "user_b").await;
 
     let rust_id = server
-        .make_channel("rust", (&client_a, cx_a), &mut [(&client_b, cx_b)])
+        .make_channel("rust", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
         .await;
 
     // Rename the channel
@@ -716,6 +722,7 @@ async fn test_call_from_channel(
     let channel_id = server
         .make_channel(
             "x",
+            None,
             (&client_a, cx_a),
             &mut [(&client_b, cx_b), (&client_c, cx_c)],
         )
@@ -786,7 +793,9 @@ async fn test_lost_channel_creation(
         .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
         .await;
 
-    let channel_id = server.make_channel("x", (&client_a, cx_a), &mut []).await;
+    let channel_id = server
+        .make_channel("x", None, (&client_a, cx_a), &mut [])
+        .await;
 
     // Invite a member
     client_a
@@ -874,6 +883,253 @@ async fn test_lost_channel_creation(
     );
 }
 
+#[gpui::test]
+async fn test_channel_moving(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &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;
+    let client_c = server.create_client(cx_c, "user_c").await;
+
+    let channels = server
+        .make_channel_tree(
+            &[
+                ("channel-a", None),
+                ("channel-b", Some("channel-a")),
+                ("channel-c", Some("channel-b")),
+                ("channel-d", Some("channel-c")),
+            ],
+            (&client_a, cx_a),
+        )
+        .await;
+    let channel_a_id = channels[0];
+    let channel_b_id = channels[1];
+    let channel_c_id = channels[2];
+    let channel_d_id = channels[3];
+
+    // Current shape:
+    // a - b - c - d
+    assert_channels_list_shape(
+        client_a.channel_store(),
+        cx_a,
+        &[
+            (channel_a_id, 0),
+            (channel_b_id, 1),
+            (channel_c_id, 2),
+            (channel_d_id, 3),
+        ],
+    );
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.move_channel(channel_d_id, channel_c_id, channel_b_id, cx)
+        })
+        .await
+        .unwrap();
+
+    // Current shape:
+    //       /- d
+    // a - b -- c
+    assert_channels_list_shape(
+        client_a.channel_store(),
+        cx_a,
+        &[
+            (channel_a_id, 0),
+            (channel_b_id, 1),
+            (channel_c_id, 2),
+            (channel_d_id, 2),
+        ],
+    );
+
+    client_a
+        .channel_store()
+        .update(cx_a, |channel_store, cx| {
+            channel_store.link_channel(channel_d_id, channel_c_id, cx)
+        })
+        .await
+        .unwrap();
+
+    // Current shape for A:
+    //      /------\
+    // a - b -- c -- d
+    assert_channels_list_shape(
+        client_a.channel_store(),
+        cx_a,
+        &[
+            (channel_a_id, 0),
+            (channel_b_id, 1),
+            (channel_c_id, 2),
+            (channel_d_id, 3),
+            (channel_d_id, 2),
+        ],
+    );
+
+    let b_channels = server
+        .make_channel_tree(
+            &[
+                ("channel-mu", None),
+                ("channel-gamma", Some("channel-mu")),
+                ("channel-epsilon", Some("channel-mu")),
+            ],
+            (&client_b, cx_b),
+        )
+        .await;
+    let channel_mu_id = b_channels[0];
+    let channel_ga_id = b_channels[1];
+    let channel_ep_id = b_channels[2];
+
+    // Current shape for B:
+    //    /- ep
+    // mu -- ga
+    assert_channels_list_shape(
+        client_b.channel_store(),
+        cx_b,
+        &[(channel_mu_id, 0), (channel_ep_id, 1), (channel_ga_id, 1)],
+    );
+
+    client_a
+        .add_admin_to_channel((&client_b, cx_b), channel_b_id, cx_a)
+        .await;
+
+    // Current shape for B:
+    //    /- ep
+    // mu -- ga
+    //  /---------\
+    // b  -- c  -- d
+    assert_channels_list_shape(
+        client_b.channel_store(),
+        cx_b,
+        &[
+            // New channels from a
+            (channel_b_id, 0),
+            (channel_c_id, 1),
+            (channel_d_id, 2),
+            (channel_d_id, 1),
+            // B's old channels
+            (channel_mu_id, 0),
+            (channel_ep_id, 1),
+            (channel_ga_id, 1),
+        ],
+    );
+
+    client_b
+        .add_admin_to_channel((&client_c, cx_c), channel_ep_id, cx_b)
+        .await;
+
+    // Current shape for C:
+    // - ep
+    assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]);
+
+    client_b
+        .channel_store()
+        .update(cx_b, |channel_store, cx| {
+            channel_store.link_channel(channel_b_id, channel_ep_id, cx)
+        })
+        .await
+        .unwrap();
+
+    // Current shape for B:
+    //              /---------\
+    //    /- ep -- b  -- c  -- d
+    // mu -- ga
+    assert_channels_list_shape(
+        client_b.channel_store(),
+        cx_b,
+        &[
+            (channel_mu_id, 0),
+            (channel_ep_id, 1),
+            (channel_b_id, 2),
+            (channel_c_id, 3),
+            (channel_d_id, 4),
+            (channel_d_id, 3),
+            (channel_ga_id, 1),
+        ],
+    );
+
+    // Current shape for C:
+    //        /---------\
+    // ep -- b  -- c  -- d
+    assert_channels_list_shape(
+        client_c.channel_store(),
+        cx_c,
+        &[
+            (channel_ep_id, 0),
+            (channel_b_id, 1),
+            (channel_c_id, 2),
+            (channel_d_id, 3),
+            (channel_d_id, 2),
+        ],
+    );
+
+    client_b
+        .channel_store()
+        .update(cx_b, |channel_store, cx| {
+            channel_store.link_channel(channel_ga_id, channel_b_id, cx)
+        })
+        .await
+        .unwrap();
+
+    // Current shape for B:
+    //              /---------\
+    //    /- ep -- b  -- c  -- d
+    //   /          \
+    // mu ---------- ga
+    assert_channels_list_shape(
+        client_b.channel_store(),
+        cx_b,
+        &[
+            (channel_mu_id, 0),
+            (channel_ep_id, 1),
+            (channel_b_id, 2),
+            (channel_c_id, 3),
+            (channel_d_id, 4),
+            (channel_d_id, 3),
+            (channel_ga_id, 3),
+            (channel_ga_id, 1),
+        ],
+    );
+
+    // Current shape for A:
+    //      /------\
+    // a - b -- c -- d
+    //      \-- ga
+    assert_channels_list_shape(
+        client_a.channel_store(),
+        cx_a,
+        &[
+            (channel_a_id, 0),
+            (channel_b_id, 1),
+            (channel_c_id, 2),
+            (channel_d_id, 3),
+            (channel_d_id, 2),
+            (channel_ga_id, 2),
+        ],
+    );
+
+    // Current shape for C:
+    //        /-------\
+    // ep -- b -- c -- d
+    //        \-- ga
+    assert_channels_list_shape(
+        client_c.channel_store(),
+        cx_c,
+        &[
+            (channel_ep_id, 0),
+            (channel_b_id, 1),
+            (channel_c_id, 2),
+            (channel_d_id, 3),
+            (channel_d_id, 2),
+            (channel_ga_id, 2),
+        ],
+    );
+}
+
 #[derive(Debug, PartialEq)]
 struct ExpectedChannel {
     depth: usize,
@@ -911,7 +1167,7 @@ fn assert_channels(
 ) {
     let actual = channel_store.read_with(cx, |store, _| {
         store
-            .channels()
+            .channel_dag_entries()
             .map(|(depth, channel)| ExpectedChannel {
                 depth,
                 name: channel.name.clone(),
@@ -920,5 +1176,22 @@ fn assert_channels(
             })
             .collect::<Vec<_>>()
     });
-    assert_eq!(actual, expected_channels);
+    pretty_assertions::assert_eq!(actual, expected_channels);
+}
+
+#[track_caller]
+fn assert_channels_list_shape(
+    channel_store: &ModelHandle<ChannelStore>,
+    cx: &TestAppContext,
+    expected_channels: &[(u64, usize)],
+) {
+    cx.foreground().run_until_parked();
+
+    let actual = channel_store.read_with(cx, |store, _| {
+        store
+            .channel_dag_entries()
+            .map(|(depth, channel)| (channel.id, depth))
+            .collect::<Vec<_>>()
+    });
+    pretty_assertions::assert_eq!(actual, expected_channels);
 }

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

@@ -0,0 +1,1699 @@
+use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
+use call::ActiveCall;
+use collab_ui::project_shared_notification::ProjectSharedNotification;
+use editor::{Editor, ExcerptRange, MultiBuffer};
+use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
+use live_kit_client::MacOSDisplay;
+use rpc::proto::PeerId;
+use serde_json::json;
+use std::{borrow::Cow, sync::Arc};
+use workspace::{
+    dock::{test::TestPanel, DockPosition},
+    item::{test::TestItem, ItemHandle as _},
+    shared_screen::SharedScreen,
+    SplitDirection, Workspace,
+};
+
+#[gpui::test(iterations = 10)]
+async fn test_basic_following(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+    cx_d: &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;
+    let client_c = server.create_client(cx_c, "user_c").await;
+    let client_d = server.create_client(cx_d, "user_d").await;
+    server
+        .create_room(&mut [
+            (&client_a, cx_a),
+            (&client_b, cx_b),
+            (&client_c, cx_c),
+            (&client_d, cx_d),
+        ])
+        .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);
+
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one\none\none",
+                "2.txt": "two\ntwo\ntwo",
+                "3.txt": "three\nthree\nthree",
+            }),
+        )
+        .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 window_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = window_a.root(cx_a);
+    let window_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = window_b.root(cx_b);
+
+    // Client A opens some editors.
+    let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+    let editor_a1 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+    let editor_a2 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B opens an editor.
+    let editor_b1 = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    let peer_id_a = client_a.peer_id().unwrap();
+    let peer_id_b = client_b.peer_id().unwrap();
+    let peer_id_c = client_c.peer_id().unwrap();
+    let peer_id_d = client_d.peer_id().unwrap();
+
+    // Client A updates their selections in those editors
+    editor_a1.update(cx_a, |editor, cx| {
+        editor.handle_input("a", cx);
+        editor.handle_input("b", cx);
+        editor.handle_input("c", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+    });
+    editor_a2.update(cx_a, |editor, cx| {
+        editor.handle_input("d", cx);
+        editor.handle_input("e", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![2..1]);
+    });
+
+    // When client B starts following client A, all visible view states are replicated to client B.
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(peer_id_a, cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    cx_c.foreground().run_until_parked();
+    let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    assert_eq!(
+        cx_b.read(|cx| editor_b2.project_path(cx)),
+        Some((worktree_id, "2.txt").into())
+    );
+    assert_eq!(
+        editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![2..1]
+    );
+    assert_eq!(
+        editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![3..2]
+    );
+
+    cx_c.foreground().run_until_parked();
+    let active_call_c = cx_c.read(ActiveCall::global);
+    let project_c = client_c.build_remote_project(project_id, cx_c).await;
+    let window_c = client_c.build_workspace(&project_c, cx_c);
+    let workspace_c = window_c.root(cx_c);
+    active_call_c
+        .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
+        .await
+        .unwrap();
+    drop(project_c);
+
+    // Client C also follows client A.
+    workspace_c
+        .update(cx_c, |workspace, cx| {
+            workspace.follow(peer_id_a, cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    cx_d.foreground().run_until_parked();
+    let active_call_d = cx_d.read(ActiveCall::global);
+    let project_d = client_d.build_remote_project(project_id, cx_d).await;
+    let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
+    active_call_d
+        .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
+        .await
+        .unwrap();
+    drop(project_d);
+
+    // All clients see that clients B and C are following client A.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b, peer_id_c],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // Client C unfollows client A.
+    workspace_c.update(cx_c, |workspace, cx| {
+        workspace.unfollow(&workspace.active_pane().clone(), cx);
+    });
+
+    // All clients see that clients B is following client A.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // Client C re-follows client A.
+    workspace_c.update(cx_c, |workspace, cx| {
+        workspace.follow(peer_id_a, cx);
+    });
+
+    // All clients see that clients B and C are following client A.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b, peer_id_c],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // Client D follows client C.
+    workspace_d
+        .update(cx_d, |workspace, cx| {
+            workspace.follow(peer_id_c, cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    // All clients see that D is following C
+    cx_d.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_c, project_id),
+                &[peer_id_d],
+                "checking followers for C as {name}"
+            );
+        });
+    }
+
+    // Client C closes the project.
+    window_c.remove(cx_c);
+    cx_c.drop_last(workspace_c);
+
+    // Clients A and B see that client B is following A, and client C is not present in the followers.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // All clients see that no-one is following C
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_c, project_id),
+                &[],
+                "checking followers for C as {name}"
+            );
+        });
+    }
+
+    // When client A activates a different editor, client B does so as well.
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.activate_item(&editor_a1, cx)
+    });
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+    });
+
+    // When client A opens a multibuffer, client B does so as well.
+    let multibuffer_a = cx_a.add_model(|cx| {
+        let buffer_a1 = project_a.update(cx, |project, cx| {
+            project
+                .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
+                .unwrap()
+        });
+        let buffer_a2 = project_a.update(cx, |project, cx| {
+            project
+                .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
+                .unwrap()
+        });
+        let mut result = MultiBuffer::new(0);
+        result.push_excerpts(
+            buffer_a1,
+            [ExcerptRange {
+                context: 0..3,
+                primary: None,
+            }],
+            cx,
+        );
+        result.push_excerpts(
+            buffer_a2,
+            [ExcerptRange {
+                context: 4..7,
+                primary: None,
+            }],
+            cx,
+        );
+        result
+    });
+    let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
+        let editor =
+            cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
+        workspace.add_item(Box::new(editor.clone()), cx);
+        editor
+    });
+    deterministic.run_until_parked();
+    let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    assert_eq!(
+        multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
+        multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
+    );
+
+    // When client A navigates back and forth, client B does so as well.
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.go_back(workspace.active_pane().downgrade(), cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+    });
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.go_back(workspace.active_pane().downgrade(), cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
+    });
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.go_forward(workspace.active_pane().downgrade(), cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+    });
+
+    // Changes to client A's editor are reflected on client B.
+    editor_a1.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
+    });
+    deterministic.run_until_parked();
+    editor_b1.read_with(cx_b, |editor, cx| {
+        assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
+    });
+
+    editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
+    deterministic.run_until_parked();
+    editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
+
+    editor_a1.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
+        editor.set_scroll_position(vec2f(0., 100.), cx);
+    });
+    deterministic.run_until_parked();
+    editor_b1.read_with(cx_b, |editor, cx| {
+        assert_eq!(editor.selections.ranges(cx), &[3..3]);
+    });
+
+    // After unfollowing, client B stops receiving updates from client A.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.unfollow(&workspace.active_pane().clone(), cx)
+    });
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.activate_item(&editor_a2, cx)
+    });
+    deterministic.run_until_parked();
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .id()),
+        editor_b1.id()
+    );
+
+    // Client A starts following client B.
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.follow(peer_id_b, cx).unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+        Some(peer_id_b)
+    );
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .id()),
+        editor_a1.id()
+    );
+
+    // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
+    let display = MacOSDisplay::new();
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(None, cx))
+        .await
+        .unwrap();
+    active_call_b
+        .update(cx_b, |call, cx| {
+            call.room().unwrap().update(cx, |room, cx| {
+                room.set_display_sources(vec![display.clone()]);
+                room.share_screen(cx)
+            })
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .expect("no active item")
+            .downcast::<SharedScreen>()
+            .expect("active item isn't a shared screen")
+    });
+
+    // Client B activates Zed again, which causes the previous editor to become focused again.
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    workspace_a.read_with(cx_a, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
+    });
+
+    // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.activate_item(&multibuffer_editor_b, 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 a panel, and the previously-opened screen-sharing item gets activated.
+    let panel = window_b.add_view(cx_b, |_| 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 = window_b.add_view(cx_b, |_| 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
+            .active_item(cx)
+            .unwrap()
+            .id()),
+        shared_screen.id()
+    );
+
+    // Following interrupts when client B disconnects.
+    client_b.disconnect(&cx_b.to_async());
+    deterministic.advance_clock(RECONNECT_TIMEOUT);
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+        None
+    );
+}
+
+#[gpui::test]
+async fn test_following_tab_order(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    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);
+
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+            }),
+        )
+        .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).root(cx_a);
+    let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+    let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+
+    let client_b_id = project_a.read_with(cx_a, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+
+    //Open 1, 3 in that order on client A
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
+        pane.update(cx, |pane, cx| {
+            pane.items()
+                .map(|item| {
+                    item.project_path(cx)
+                        .unwrap()
+                        .path
+                        .to_str()
+                        .unwrap()
+                        .to_owned()
+                })
+                .collect::<Vec<_>>()
+        })
+    };
+
+    //Verify that the tabs opened in the order we expect
+    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
+
+    //Follow client B as client A
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.follow(client_b_id, cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    //Open just 2 on client B
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+
+    // Verify that newly opened followed file is at the end
+    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
+
+    //Open just 1 on client B
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
+    deterministic.run_until_parked();
+
+    // Verify that following into 1 did not reorder
+    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_peers_following_each_other(
+    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);
+
+    // Client A shares a project.
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+                "4.txt": "four",
+            }),
+        )
+        .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();
+
+    // Client B joins the project.
+    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();
+
+    // Client A opens a file.
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B opens a different file.
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Clients A and B follow each other in split panes
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
+    });
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.follow(client_b.peer_id().unwrap(), cx).unwrap()
+        })
+        .await
+        .unwrap();
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
+    });
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    // Clients A and B return focus to the original files they had open
+    workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
+    workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+    deterministic.run_until_parked();
+
+    // Both clients see the other client's focused file in their right pane.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(true, "1.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_b.peer_id(),
+                items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(true, "2.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_a.peer_id(),
+                items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
+            },
+        ]
+    );
+
+    // Clients A and B each open a new file.
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "4.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+
+    // Both client's see the other client open the new file, but keep their
+    // focus on their own active pane.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (true, "3.txt".into())
+                ]
+            },
+        ]
+    );
+
+    // Client A focuses their right pane, in which they're following client B.
+    workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
+    deterministic.run_until_parked();
+
+    // Client B sees that client A is now looking at the same file as them.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (false, "3.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+
+    // Client B focuses their right pane, in which they're following client A,
+    // who is following them.
+    workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+    deterministic.run_until_parked();
+
+    // Client A sees that client B is now looking at the same file as them.
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (false, "3.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+
+    // Client B focuses a file that they previously followed A to, breaking
+    // the follow.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
+    });
+    deterministic.run_until_parked();
+
+    // Both clients see that client B is looking at that previous file.
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (true, "3.txt".into()),
+                    (false, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (true, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+
+    // Client B closes tabs, some of which were originally opened by client A,
+    // and some of which were originally opened by client B.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.close_inactive_items(&Default::default(), cx)
+                .unwrap()
+                .detach();
+        });
+    });
+
+    deterministic.run_until_parked();
+
+    // Both clients see that Client B is looking at the previous tab.
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(true, "3.txt".into()),]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (true, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+
+    // Client B follows client A again.
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    // Client A cycles through some tabs.
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
+    });
+    deterministic.run_until_parked();
+
+    // Client B follows client A into those tabs.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into()),
+                    (false, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
+            },
+        ]
+    );
+
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
+    });
+    deterministic.run_until_parked();
+
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (false, "1.txt".into()),
+                    (true, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (false, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "3.txt".into()),
+                    (false, "4.txt".into()),
+                    (true, "2.txt".into())
+                ]
+            },
+        ]
+    );
+
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
+    });
+    deterministic.run_until_parked();
+
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (true, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (false, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "3.txt".into()),
+                    (false, "4.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "1.txt".into()),
+                ]
+            },
+        ]
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_auto_unfollowing(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+
+    // 2 clients connect to a server.
+    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);
+
+    // Client A shares a project.
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+            }),
+        )
+        .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();
+
+    // Client A opens some editors.
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let _editor_a1 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B starts following client A.
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+    let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+    let leader_id = project_b.read_with(cx_b, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(leader_id, cx).unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+    let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+
+    // When client B moves, it automatically stops following client A.
+    editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(leader_id, cx).unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+
+    // When client B edits, it automatically stops following client A.
+    editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(leader_id, cx).unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+
+    // When client B scrolls, it automatically stops following client A.
+    editor_b2.update(cx_b, |editor, cx| {
+        editor.set_scroll_position(vec2f(0., 3.), cx)
+    });
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(leader_id, cx).unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+
+    // When client B activates a different pane, it continues following client A in the original pane.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
+    });
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+
+    workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        Some(leader_id)
+    );
+
+    // When client B activates a different item in the original pane, it automatically stops following client A.
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_peers_simultaneously_following_each_other(
+    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);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    client_a.fs().insert_tree("/a", json!({})).await;
+    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    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;
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+
+    deterministic.run_until_parked();
+    let client_a_id = project_b.read_with(cx_b, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+    let client_b_id = project_a.read_with(cx_a, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+
+    let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
+        workspace.follow(client_b_id, cx).unwrap()
+    });
+    let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
+        workspace.follow(client_a_id, cx).unwrap()
+    });
+
+    futures::try_join!(a_follow_b, b_follow_a).unwrap();
+    workspace_a.read_with(cx_a, |workspace, _| {
+        assert_eq!(
+            workspace.leader_for_pane(workspace.active_pane()),
+            Some(client_b_id)
+        );
+    });
+    workspace_b.read_with(cx_b, |workspace, _| {
+        assert_eq!(
+            workspace.leader_for_pane(workspace.active_pane()),
+            Some(client_a_id)
+        );
+    });
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_following_across_workspaces(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    // a and b join a channel/call
+    // a shares project 1
+    // b shares project 2
+    //
+    // b follows a: causes project 2 to be joined, and b to follow a.
+    // b opens a different file in project 2, a follows b
+    // b opens a different file in project 1, a cannot follow b
+    // b shares the project, a joins the project and follows b
+    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;
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "w.rs": "",
+                "x.rs": "",
+            }),
+        )
+        .await;
+
+    client_b
+        .fs()
+        .insert_tree(
+            "/b",
+            json!({
+                "y.rs": "",
+                "z.rs": "",
+            }),
+        )
+        .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);
+
+    let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
+    let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
+
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+
+    cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
+    cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
+
+    active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    assert_eq!(visible_push_notifications(cx_b).len(), 1);
+
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace
+            .follow(client_a.peer_id().unwrap(), cx)
+            .unwrap()
+            .detach()
+    });
+
+    deterministic.run_until_parked();
+    let workspace_b_project_a = cx_b
+        .windows()
+        .iter()
+        .max_by_key(|window| window.id())
+        .unwrap()
+        .downcast::<Workspace>()
+        .unwrap()
+        .root(cx_b);
+
+    // assert that b is following a in project a in w.rs
+    workspace_b_project_a.update(cx_b, |workspace, cx| {
+        assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
+        assert_eq!(
+            client_a.peer_id(),
+            workspace.leader_for_pane(workspace.active_pane())
+        );
+        let item = workspace.active_item(cx).unwrap();
+        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs"));
+    });
+
+    // TODO: in app code, this would be done by the collab_ui.
+    active_call_b
+        .update(cx_b, |call, cx| {
+            let project = workspace_b_project_a.read(cx).project().clone();
+            call.set_location(Some(&project), cx)
+        })
+        .await
+        .unwrap();
+
+    // assert that there are no share notifications open
+    assert_eq!(visible_push_notifications(cx_b).len(), 0);
+
+    // b moves to x.rs in a's project, and a follows
+    workspace_b_project_a
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    workspace_b_project_a.update(cx_b, |workspace, cx| {
+        let item = workspace.active_item(cx).unwrap();
+        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
+    });
+
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace
+            .follow(client_b.peer_id().unwrap(), cx)
+            .unwrap()
+            .detach()
+    });
+
+    deterministic.run_until_parked();
+    workspace_a.update(cx_a, |workspace, cx| {
+        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
+        assert_eq!(
+            client_b.peer_id(),
+            workspace.leader_for_pane(workspace.active_pane())
+        );
+        let item = workspace.active_pane().read(cx).active_item().unwrap();
+        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
+    });
+
+    // b moves to y.rs in b's project, a is still following but can't yet see
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    // TODO: in app code, this would be done by the collab_ui.
+    active_call_b
+        .update(cx_b, |call, cx| {
+            let project = workspace_b.read(cx).project().clone();
+            call.set_location(Some(&project), cx)
+        })
+        .await
+        .unwrap();
+
+    let project_b_id = active_call_b
+        .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    assert_eq!(visible_push_notifications(cx_a).len(), 1);
+    cx_a.update(|cx| {
+        workspace::join_remote_project(
+            project_b_id,
+            client_b.user_id().unwrap(),
+            client_a.app_state.clone(),
+            cx,
+        )
+    })
+    .await
+    .unwrap();
+
+    deterministic.run_until_parked();
+
+    assert_eq!(visible_push_notifications(cx_a).len(), 0);
+    let workspace_a_project_b = cx_a
+        .windows()
+        .iter()
+        .max_by_key(|window| window.id())
+        .unwrap()
+        .downcast::<Workspace>()
+        .unwrap()
+        .root(cx_a);
+
+    workspace_a_project_b.update(cx_a, |workspace, cx| {
+        assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
+        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
+        assert_eq!(
+            client_b.peer_id(),
+            workspace.leader_for_pane(workspace.active_pane())
+        );
+        let item = workspace.active_item(cx).unwrap();
+        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
+    });
+}
+
+fn visible_push_notifications(
+    cx: &mut TestAppContext,
+) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
+    let mut ret = Vec::new();
+    for window in cx.windows() {
+        window.read_with(cx, |window| {
+            if let Some(handle) = window
+                .root_view()
+                .clone()
+                .downcast::<ProjectSharedNotification>()
+            {
+                ret.push(handle)
+            }
+        });
+    }
+    ret
+}
+
+#[derive(Debug, PartialEq, Eq)]
+struct PaneSummary {
+    active: bool,
+    leader: Option<PeerId>,
+    items: Vec<(bool, String)>,
+}
+
+fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
+    workspace.read_with(cx, |workspace, cx| {
+        let active_pane = workspace.active_pane();
+        workspace
+            .panes()
+            .iter()
+            .map(|pane| {
+                let leader = workspace.leader_for_pane(pane);
+                let active = pane == active_pane;
+                let pane = pane.read(cx);
+                let active_ix = pane.active_item_index();
+                PaneSummary {
+                    active,
+                    leader,
+                    items: pane
+                        .items()
+                        .enumerate()
+                        .map(|(ix, item)| {
+                            (
+                                ix == active_ix,
+                                item.tab_description(0, cx)
+                                    .map_or(String::new(), |s| s.to_string()),
+                            )
+                        })
+                        .collect(),
+                }
+            })
+            .collect()
+    })
+}

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

@@ -7,14 +7,11 @@ use client::{User, RECEIVE_TIMEOUT};
 use collections::{HashMap, HashSet};
 use editor::{
     test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
-    ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo,
+    ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo,
 };
 use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions};
 use futures::StreamExt as _;
-use gpui::{
-    executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle,
-    TestAppContext, ViewHandle,
-};
+use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, TestAppContext};
 use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
@@ -38,12 +35,7 @@ use std::{
     },
 };
 use unindent::Unindent as _;
-use workspace::{
-    dock::{test::TestPanel, DockPosition},
-    item::{test::TestItem, ItemHandle as _},
-    shared_screen::SharedScreen,
-    SplitDirection, Workspace,
-};
+use workspace::Workspace;
 
 #[ctor::ctor]
 fn init_logger() {
@@ -3146,6 +3138,7 @@ async fn test_local_settings(
         )
         .await;
     let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
+    deterministic.run_until_parked();
     let project_id = active_call_a
         .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
         .await
@@ -4824,7 +4817,7 @@ async fn test_project_search(
     let mut results = HashMap::default();
     let mut search_rx = project_b.update(cx_b, |project, cx| {
         project.search(
-            SearchQuery::text("world", false, false, Vec::new(), Vec::new()),
+            SearchQuery::text("world", false, false, Vec::new(), Vec::new()).unwrap(),
             cx,
         )
     });
@@ -6387,455 +6380,49 @@ async fn test_contact_requests(
 }
 
 #[gpui::test(iterations = 10)]
-async fn test_basic_following(
+async fn test_join_call_after_screen_was_shared(
     deterministic: Arc<Deterministic>,
     cx_a: &mut TestAppContext,
     cx_b: &mut TestAppContext,
-    cx_c: &mut TestAppContext,
-    cx_d: &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;
-    let client_c = server.create_client(cx_c, "user_c").await;
-    let client_d = server.create_client(cx_d, "user_d").await;
     server
-        .create_room(&mut [
-            (&client_a, cx_a),
-            (&client_b, cx_b),
-            (&client_c, cx_c),
-            (&client_d, cx_d),
-        ])
+        .make_contacts(&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);
-
-    client_a
-        .fs()
-        .insert_tree(
-            "/a",
-            json!({
-                "1.txt": "one\none\none",
-                "2.txt": "two\ntwo\ntwo",
-                "3.txt": "three\nthree\nthree",
-            }),
-        )
-        .await;
-    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    // Call users B and C from client A.
     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 window_a = client_a.build_workspace(&project_a, cx_a);
-    let workspace_a = window_a.root(cx_a);
-    let window_b = client_b.build_workspace(&project_b, cx_b);
-    let workspace_b = window_b.root(cx_b);
-
-    // Client A opens some editors.
-    let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
-    let editor_a1 = workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-    let editor_a2 = workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
-    // Client B opens an editor.
-    let editor_b1 = workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
-    let peer_id_a = client_a.peer_id().unwrap();
-    let peer_id_b = client_b.peer_id().unwrap();
-    let peer_id_c = client_c.peer_id().unwrap();
-    let peer_id_d = client_d.peer_id().unwrap();
-
-    // Client A updates their selections in those editors
-    editor_a1.update(cx_a, |editor, cx| {
-        editor.handle_input("a", cx);
-        editor.handle_input("b", cx);
-        editor.handle_input("c", cx);
-        editor.select_left(&Default::default(), cx);
-        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
-    });
-    editor_a2.update(cx_a, |editor, cx| {
-        editor.handle_input("d", cx);
-        editor.handle_input("e", cx);
-        editor.select_left(&Default::default(), cx);
-        assert_eq!(editor.selections.ranges(cx), vec![2..1]);
-    });
-
-    // When client B starts following client A, all visible view states are replicated to client B.
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.toggle_follow(peer_id_a, cx).unwrap()
-        })
-        .await
-        .unwrap();
-
-    cx_c.foreground().run_until_parked();
-    let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
-        workspace
-            .active_item(cx)
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap()
-    });
-    assert_eq!(
-        cx_b.read(|cx| editor_b2.project_path(cx)),
-        Some((worktree_id, "2.txt").into())
-    );
-    assert_eq!(
-        editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
-        vec![2..1]
-    );
-    assert_eq!(
-        editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
-        vec![3..2]
-    );
-
-    cx_c.foreground().run_until_parked();
-    let active_call_c = cx_c.read(ActiveCall::global);
-    let project_c = client_c.build_remote_project(project_id, cx_c).await;
-    let window_c = client_c.build_workspace(&project_c, cx_c);
-    let workspace_c = window_c.root(cx_c);
-    active_call_c
-        .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
-        .await
-        .unwrap();
-    drop(project_c);
-
-    // Client C also follows client A.
-    workspace_c
-        .update(cx_c, |workspace, cx| {
-            workspace.toggle_follow(peer_id_a, cx).unwrap()
-        })
-        .await
-        .unwrap();
-
-    cx_d.foreground().run_until_parked();
-    let active_call_d = cx_d.read(ActiveCall::global);
-    let project_d = client_d.build_remote_project(project_id, cx_d).await;
-    let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
-    active_call_d
-        .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
-        .await
-        .unwrap();
-    drop(project_d);
-
-    // All clients see that clients B and C are following client A.
-    cx_c.foreground().run_until_parked();
-    for (name, active_call, cx) in [
-        ("A", &active_call_a, &cx_a),
-        ("B", &active_call_b, &cx_b),
-        ("C", &active_call_c, &cx_c),
-        ("D", &active_call_d, &cx_d),
-    ] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_a, project_id),
-                &[peer_id_b, peer_id_c],
-                "checking followers for A as {name}"
-            );
-        });
-    }
-
-    // Client C unfollows client A.
-    workspace_c.update(cx_c, |workspace, cx| {
-        workspace.toggle_follow(peer_id_a, cx);
-    });
-
-    // All clients see that clients B is following client A.
-    cx_c.foreground().run_until_parked();
-    for (name, active_call, cx) in [
-        ("A", &active_call_a, &cx_a),
-        ("B", &active_call_b, &cx_b),
-        ("C", &active_call_c, &cx_c),
-        ("D", &active_call_d, &cx_d),
-    ] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_a, project_id),
-                &[peer_id_b],
-                "checking followers for A as {name}"
-            );
-        });
-    }
-
-    // Client C re-follows client A.
-    workspace_c.update(cx_c, |workspace, cx| {
-        workspace.toggle_follow(peer_id_a, cx);
-    });
-
-    // All clients see that clients B and C are following client A.
-    cx_c.foreground().run_until_parked();
-    for (name, active_call, cx) in [
-        ("A", &active_call_a, &cx_a),
-        ("B", &active_call_b, &cx_b),
-        ("C", &active_call_c, &cx_c),
-        ("D", &active_call_d, &cx_d),
-    ] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_a, project_id),
-                &[peer_id_b, peer_id_c],
-                "checking followers for A as {name}"
-            );
-        });
-    }
-
-    // Client D follows client C.
-    workspace_d
-        .update(cx_d, |workspace, cx| {
-            workspace.toggle_follow(peer_id_c, cx).unwrap()
-        })
-        .await
-        .unwrap();
-
-    // All clients see that D is following C
-    cx_d.foreground().run_until_parked();
-    for (name, active_call, cx) in [
-        ("A", &active_call_a, &cx_a),
-        ("B", &active_call_b, &cx_b),
-        ("C", &active_call_c, &cx_c),
-        ("D", &active_call_d, &cx_d),
-    ] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_c, project_id),
-                &[peer_id_d],
-                "checking followers for C as {name}"
-            );
-        });
-    }
-
-    // Client C closes the project.
-    window_c.remove(cx_c);
-    cx_c.drop_last(workspace_c);
-
-    // Clients A and B see that client B is following A, and client C is not present in the followers.
-    cx_c.foreground().run_until_parked();
-    for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_a, project_id),
-                &[peer_id_b],
-                "checking followers for A as {name}"
-            );
-        });
-    }
-
-    // All clients see that no-one is following C
-    for (name, active_call, cx) in [
-        ("A", &active_call_a, &cx_a),
-        ("B", &active_call_b, &cx_b),
-        ("C", &active_call_c, &cx_c),
-        ("D", &active_call_d, &cx_d),
-    ] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_c, project_id),
-                &[],
-                "checking followers for C as {name}"
-            );
-        });
-    }
-
-    // When client A activates a different editor, client B does so as well.
-    workspace_a.update(cx_a, |workspace, cx| {
-        workspace.activate_item(&editor_a1, cx)
-    });
-    deterministic.run_until_parked();
-    workspace_b.read_with(cx_b, |workspace, cx| {
-        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
-    });
-
-    // When client A opens a multibuffer, client B does so as well.
-    let multibuffer_a = cx_a.add_model(|cx| {
-        let buffer_a1 = project_a.update(cx, |project, cx| {
-            project
-                .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
-                .unwrap()
-        });
-        let buffer_a2 = project_a.update(cx, |project, cx| {
-            project
-                .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
-                .unwrap()
-        });
-        let mut result = MultiBuffer::new(0);
-        result.push_excerpts(
-            buffer_a1,
-            [ExcerptRange {
-                context: 0..3,
-                primary: None,
-            }],
-            cx,
-        );
-        result.push_excerpts(
-            buffer_a2,
-            [ExcerptRange {
-                context: 4..7,
-                primary: None,
-            }],
-            cx,
-        );
-        result
-    });
-    let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
-        let editor =
-            cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
-        workspace.add_item(Box::new(editor.clone()), cx);
-        editor
-    });
-    deterministic.run_until_parked();
-    let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
-        workspace
-            .active_item(cx)
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap()
-    });
-    assert_eq!(
-        multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
-        multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
-    );
-
-    // When client A navigates back and forth, client B does so as well.
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.go_back(workspace.active_pane().downgrade(), cx)
-        })
-        .await
-        .unwrap();
-    deterministic.run_until_parked();
-    workspace_b.read_with(cx_b, |workspace, cx| {
-        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
-    });
-
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.go_back(workspace.active_pane().downgrade(), cx)
-        })
-        .await
-        .unwrap();
-    deterministic.run_until_parked();
-    workspace_b.read_with(cx_b, |workspace, cx| {
-        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
-    });
-
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.go_forward(workspace.active_pane().downgrade(), cx)
+        .update(cx_a, |call, cx| {
+            call.invite(client_b.user_id().unwrap(), None, cx)
         })
         .await
         .unwrap();
-    deterministic.run_until_parked();
-    workspace_b.read_with(cx_b, |workspace, cx| {
-        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
-    });
-
-    // Changes to client A's editor are reflected on client B.
-    editor_a1.update(cx_a, |editor, cx| {
-        editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
-    });
-    deterministic.run_until_parked();
-    editor_b1.read_with(cx_b, |editor, cx| {
-        assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
-    });
-
-    editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
-    deterministic.run_until_parked();
-    editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
-
-    editor_a1.update(cx_a, |editor, cx| {
-        editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
-        editor.set_scroll_position(vec2f(0., 100.), cx);
-    });
-    deterministic.run_until_parked();
-    editor_b1.read_with(cx_b, |editor, cx| {
-        assert_eq!(editor.selections.ranges(cx), &[3..3]);
-    });
-
-    // After unfollowing, client B stops receiving updates from client A.
-    workspace_b.update(cx_b, |workspace, cx| {
-        workspace.unfollow(&workspace.active_pane().clone(), cx)
-    });
-    workspace_a.update(cx_a, |workspace, cx| {
-        workspace.activate_item(&editor_a2, cx)
-    });
+    let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
     deterministic.run_until_parked();
     assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, cx| workspace
-            .active_item(cx)
-            .unwrap()
-            .id()),
-        editor_b1.id()
+        room_participants(&room_a, cx_a),
+        RoomParticipants {
+            remote: Default::default(),
+            pending: vec!["user_b".to_string()]
+        }
     );
 
-    // Client A starts following client B.
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.toggle_follow(peer_id_b, cx).unwrap()
-        })
-        .await
-        .unwrap();
-    assert_eq!(
-        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-        Some(peer_id_b)
-    );
-    assert_eq!(
-        workspace_a.read_with(cx_a, |workspace, cx| workspace
-            .active_item(cx)
-            .unwrap()
-            .id()),
-        editor_a1.id()
-    );
+    // User B receives the call.
+    let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
+    let call_b = incoming_call_b.next().await.unwrap().unwrap();
+    assert_eq!(call_b.calling_user.github_login, "user_a");
 
-    // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
+    // User A shares their screen
     let display = MacOSDisplay::new();
-    active_call_b
-        .update(cx_b, |call, cx| call.set_location(None, cx))
-        .await
-        .unwrap();
-    active_call_b
-        .update(cx_b, |call, cx| {
+    active_call_a
+        .update(cx_a, |call, cx| {
             call.room().unwrap().update(cx, |room, cx| {
                 room.set_display_sources(vec![display.clone()]);
                 room.share_screen(cx)
@@ -6843,153 +6430,18 @@ async fn test_basic_following(
         })
         .await
         .unwrap();
-    deterministic.run_until_parked();
-    let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
-        workspace
-            .active_item(cx)
-            .unwrap()
-            .downcast::<SharedScreen>()
-            .unwrap()
+
+    client_b.user_store().update(cx_b, |user_store, _| {
+        user_store.clear_cache();
     });
 
-    // Client B activates Zed again, which causes the previous editor to become focused again.
+    // User B joins the room
     active_call_b
-        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .update(cx_b, |call, cx| call.accept_incoming(cx))
         .await
         .unwrap();
-    deterministic.run_until_parked();
-    workspace_a.read_with(cx_a, |workspace, cx| {
-        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
-    });
-
-    // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
-    workspace_b.update(cx_b, |workspace, cx| {
-        workspace.activate_item(&multibuffer_editor_b, 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 a panel, and the previously-opened screen-sharing item gets activated.
-    let panel = window_b.add_view(cx_b, |_| 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 = window_b.add_view(cx_b, |_| 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
-            .active_item(cx)
-            .unwrap()
-            .id()),
-        shared_screen.id()
-    );
-
-    // Following interrupts when client B disconnects.
-    client_b.disconnect(&cx_b.to_async());
-    deterministic.advance_clock(RECONNECT_TIMEOUT);
-    assert_eq!(
-        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-        None
-    );
-}
-
-#[gpui::test(iterations = 10)]
-async fn test_join_call_after_screen_was_shared(
-    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
-        .make_contacts(&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);
-
-    // Call users B and C from client A.
-    active_call_a
-        .update(cx_a, |call, cx| {
-            call.invite(client_b.user_id().unwrap(), None, cx)
-        })
-        .await
-        .unwrap();
-    let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
-    deterministic.run_until_parked();
-    assert_eq!(
-        room_participants(&room_a, cx_a),
-        RoomParticipants {
-            remote: Default::default(),
-            pending: vec!["user_b".to_string()]
-        }
-    );
-
-    // User B receives the call.
-    let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
-    let call_b = incoming_call_b.next().await.unwrap().unwrap();
-    assert_eq!(call_b.calling_user.github_login, "user_a");
-
-    // User A shares their screen
-    let display = MacOSDisplay::new();
-    active_call_a
-        .update(cx_a, |call, cx| {
-            call.room().unwrap().update(cx, |room, cx| {
-                room.set_display_sources(vec![display.clone()]);
-                room.share_screen(cx)
-            })
-        })
-        .await
-        .unwrap();
-
-    client_b.user_store().update(cx_b, |user_store, _| {
-        user_store.clear_cache();
-    });
-
-    // User B joins the room
-    active_call_b
-        .update(cx_b, |call, cx| call.accept_incoming(cx))
-        .await
-        .unwrap();
-    let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
-    assert!(incoming_call_b.next().await.unwrap().is_none());
+    let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
+    assert!(incoming_call_b.next().await.unwrap().is_none());
 
     deterministic.run_until_parked();
     assert_eq!(
@@ -7020,526 +6472,6 @@ async fn test_join_call_after_screen_was_shared(
     });
 }
 
-#[gpui::test]
-async fn test_following_tab_order(
-    deterministic: Arc<Deterministic>,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
-) {
-    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);
-
-    client_a
-        .fs()
-        .insert_tree(
-            "/a",
-            json!({
-                "1.txt": "one",
-                "2.txt": "two",
-                "3.txt": "three",
-            }),
-        )
-        .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).root(cx_a);
-    let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
-
-    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-    let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
-
-    let client_b_id = project_a.read_with(cx_a, |project, _| {
-        project.collaborators().values().next().unwrap().peer_id
-    });
-
-    //Open 1, 3 in that order on client A
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-
-    let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
-        pane.update(cx, |pane, cx| {
-            pane.items()
-                .map(|item| {
-                    item.project_path(cx)
-                        .unwrap()
-                        .path
-                        .to_str()
-                        .unwrap()
-                        .to_owned()
-                })
-                .collect::<Vec<_>>()
-        })
-    };
-
-    //Verify that the tabs opened in the order we expect
-    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
-
-    //Follow client B as client A
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.toggle_follow(client_b_id, cx).unwrap()
-        })
-        .await
-        .unwrap();
-
-    //Open just 2 on client B
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-    deterministic.run_until_parked();
-
-    // Verify that newly opened followed file is at the end
-    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
-
-    //Open just 1 on client B
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-    assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
-    deterministic.run_until_parked();
-
-    // Verify that following into 1 did not reorder
-    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
-}
-
-#[gpui::test(iterations = 10)]
-async fn test_peers_following_each_other(
-    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);
-
-    // Client A shares a project.
-    client_a
-        .fs()
-        .insert_tree(
-            "/a",
-            json!({
-                "1.txt": "one",
-                "2.txt": "two",
-                "3.txt": "three",
-                "4.txt": "four",
-            }),
-        )
-        .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();
-
-    // Client B joins the project.
-    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();
-
-    // Client A opens some editors.
-    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-    let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
-    let _editor_a1 = workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
-    // Client B opens an editor.
-    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-    let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
-    let _editor_b1 = workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
-    // Clients A and B follow each other in split panes
-    workspace_a.update(cx_a, |workspace, cx| {
-        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
-    });
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            assert_ne!(*workspace.active_pane(), pane_a1);
-            let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
-            workspace.toggle_follow(leader_id, cx).unwrap()
-        })
-        .await
-        .unwrap();
-    workspace_b.update(cx_b, |workspace, cx| {
-        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
-    });
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            assert_ne!(*workspace.active_pane(), pane_b1);
-            let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
-            workspace.toggle_follow(leader_id, cx).unwrap()
-        })
-        .await
-        .unwrap();
-
-    workspace_a.update(cx_a, |workspace, cx| {
-        workspace.activate_next_pane(cx);
-    });
-    // Wait for focus effects to be fully flushed
-    workspace_a.update(cx_a, |workspace, _| {
-        assert_eq!(*workspace.active_pane(), pane_a1);
-    });
-
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-    workspace_b.update(cx_b, |workspace, cx| {
-        workspace.activate_next_pane(cx);
-    });
-
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            assert_eq!(*workspace.active_pane(), pane_b1);
-            workspace.open_path((worktree_id, "4.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-    cx_a.foreground().run_until_parked();
-
-    // Ensure leader updates don't change the active pane of followers
-    workspace_a.read_with(cx_a, |workspace, _| {
-        assert_eq!(*workspace.active_pane(), pane_a1);
-    });
-    workspace_b.read_with(cx_b, |workspace, _| {
-        assert_eq!(*workspace.active_pane(), pane_b1);
-    });
-
-    // Ensure peers following each other doesn't cause an infinite loop.
-    assert_eq!(
-        workspace_a.read_with(cx_a, |workspace, cx| workspace
-            .active_item(cx)
-            .unwrap()
-            .project_path(cx)),
-        Some((worktree_id, "3.txt").into())
-    );
-    workspace_a.update(cx_a, |workspace, cx| {
-        assert_eq!(
-            workspace.active_item(cx).unwrap().project_path(cx),
-            Some((worktree_id, "3.txt").into())
-        );
-        workspace.activate_next_pane(cx);
-    });
-
-    workspace_a.update(cx_a, |workspace, cx| {
-        assert_eq!(
-            workspace.active_item(cx).unwrap().project_path(cx),
-            Some((worktree_id, "4.txt").into())
-        );
-    });
-
-    workspace_b.update(cx_b, |workspace, cx| {
-        assert_eq!(
-            workspace.active_item(cx).unwrap().project_path(cx),
-            Some((worktree_id, "4.txt").into())
-        );
-        workspace.activate_next_pane(cx);
-    });
-
-    workspace_b.update(cx_b, |workspace, cx| {
-        assert_eq!(
-            workspace.active_item(cx).unwrap().project_path(cx),
-            Some((worktree_id, "3.txt").into())
-        );
-    });
-}
-
-#[gpui::test(iterations = 10)]
-async fn test_auto_unfollowing(
-    deterministic: Arc<Deterministic>,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
-) {
-    deterministic.forbid_parking();
-
-    // 2 clients connect to a server.
-    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);
-
-    // Client A shares a project.
-    client_a
-        .fs()
-        .insert_tree(
-            "/a",
-            json!({
-                "1.txt": "one",
-                "2.txt": "two",
-                "3.txt": "three",
-            }),
-        )
-        .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();
-
-    // Client A opens some editors.
-    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-    let _editor_a1 = workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
-    // Client B starts following client A.
-    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-    let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
-    let leader_id = project_b.read_with(cx_b, |project, _| {
-        project.collaborators().values().next().unwrap().peer_id
-    });
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.toggle_follow(leader_id, cx).unwrap()
-        })
-        .await
-        .unwrap();
-    assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
-    );
-    let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
-        workspace
-            .active_item(cx)
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap()
-    });
-
-    // When client B moves, it automatically stops following client A.
-    editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
-    assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        None
-    );
-
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.toggle_follow(leader_id, cx).unwrap()
-        })
-        .await
-        .unwrap();
-    assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
-    );
-
-    // When client B edits, it automatically stops following client A.
-    editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
-    assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        None
-    );
-
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.toggle_follow(leader_id, cx).unwrap()
-        })
-        .await
-        .unwrap();
-    assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
-    );
-
-    // When client B scrolls, it automatically stops following client A.
-    editor_b2.update(cx_b, |editor, cx| {
-        editor.set_scroll_position(vec2f(0., 3.), cx)
-    });
-    assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        None
-    );
-
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.toggle_follow(leader_id, cx).unwrap()
-        })
-        .await
-        .unwrap();
-    assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
-    );
-
-    // When client B activates a different pane, it continues following client A in the original pane.
-    workspace_b.update(cx_b, |workspace, cx| {
-        workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
-    });
-    assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
-    );
-
-    workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
-    assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        Some(leader_id)
-    );
-
-    // When client B activates a different item in the original pane, it automatically stops following client A.
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-    assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        None
-    );
-}
-
-#[gpui::test(iterations = 10)]
-async fn test_peers_simultaneously_following_each_other(
-    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);
-
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
-
-    client_a.fs().insert_tree("/a", json!({})).await;
-    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
-    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-    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;
-    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-
-    deterministic.run_until_parked();
-    let client_a_id = project_b.read_with(cx_b, |project, _| {
-        project.collaborators().values().next().unwrap().peer_id
-    });
-    let client_b_id = project_a.read_with(cx_a, |project, _| {
-        project.collaborators().values().next().unwrap().peer_id
-    });
-
-    let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
-        workspace.toggle_follow(client_b_id, cx).unwrap()
-    });
-    let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
-        workspace.toggle_follow(client_a_id, cx).unwrap()
-    });
-
-    futures::try_join!(a_follow_b, b_follow_a).unwrap();
-    workspace_a.read_with(cx_a, |workspace, _| {
-        assert_eq!(
-            workspace.leader_for_pane(workspace.active_pane()),
-            Some(client_b_id)
-        );
-    });
-    workspace_b.read_with(cx_b, |workspace, _| {
-        assert_eq!(
-            workspace.leader_for_pane(workspace.active_pane()),
-            Some(client_a_id)
-        );
-    });
-}
-
 #[gpui::test(iterations = 10)]
 async fn test_on_input_format_from_host_to_guest(
     deterministic: Arc<Deterministic>,

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

@@ -86,7 +86,7 @@ impl RandomizedTest for RandomChannelBufferTest {
             match rng.gen_range(0..100_u32) {
                 0..=29 => {
                     let channel_name = client.channel_store().read_with(cx, |store, cx| {
-                        store.channels().find_map(|(_, channel)| {
+                        store.channel_dag_entries().find_map(|(_, channel)| {
                             if store.has_open_channel_buffer(channel.id, cx) {
                                 None
                             } else {
@@ -133,7 +133,7 @@ impl RandomizedTest for RandomChannelBufferTest {
             ChannelBufferOperation::JoinChannelNotes { channel_name } => {
                 let buffer = client.channel_store().update(cx, |store, cx| {
                     let channel_id = store
-                        .channels()
+                        .channel_dag_entries()
                         .find(|(_, c)| c.name == channel_name)
                         .unwrap()
                         .1
@@ -273,7 +273,7 @@ impl RandomizedTest for RandomChannelBufferTest {
                         // channel buffer.
                         let collaborators = channel_buffer.collaborators();
                         let mut user_ids =
-                            collaborators.iter().map(|c| c.user_id).collect::<Vec<_>>();
+                            collaborators.values().map(|c| c.user_id).collect::<Vec<_>>();
                         user_ids.sort();
                         assert_eq!(
                             user_ids,

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

@@ -869,7 +869,7 @@ impl RandomizedTest for ProjectCollaborationTest {
 
                 let mut search = project.update(cx, |project, cx| {
                     project.search(
-                        SearchQuery::text(query, false, false, Vec::new(), Vec::new()),
+                        SearchQuery::text(query, false, false, Vec::new(), Vec::new()).unwrap(),
                         cx,
                     )
                 });

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

@@ -1,12 +1,12 @@
 use crate::{
     db::{tests::TestDb, NewUserParams, UserId},
     executor::Executor,
-    rpc::{Server, CLEANUP_TIMEOUT},
+    rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
     AppState,
 };
 use anyhow::anyhow;
 use call::ActiveCall;
-use channel::{channel_buffer::ChannelBuffer, ChannelStore};
+use channel::{ChannelBuffer, ChannelStore};
 use client::{
     self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
 };
@@ -17,6 +17,7 @@ use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHan
 use language::LanguageRegistry;
 use parking_lot::Mutex;
 use project::{Project, WorktreeId};
+use rpc::RECEIVE_TIMEOUT;
 use settings::SettingsStore;
 use std::{
     cell::{Ref, RefCell, RefMut},
@@ -29,7 +30,7 @@ use std::{
     },
 };
 use util::http::FakeHttpClient;
-use workspace::Workspace;
+use workspace::{Workspace, WorkspaceStore};
 
 pub struct TestServer {
     pub app_state: Arc<AppState>,
@@ -151,12 +152,12 @@ impl TestServer {
 
         Arc::get_mut(&mut client)
             .unwrap()
-            .set_id(user_id.0 as usize)
+            .set_id(user_id.to_proto())
             .override_authenticate(move |cx| {
                 cx.spawn(|_| async move {
                     let access_token = "the-token".to_string();
                     Ok(Credentials {
-                        user_id: user_id.0 as u64,
+                        user_id: user_id.to_proto(),
                         access_token,
                     })
                 })
@@ -204,13 +205,17 @@ impl TestServer {
 
         let fs = FakeFs::new(cx.background());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+        let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
         let channel_store =
             cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+        let mut language_registry = LanguageRegistry::test();
+        language_registry.set_executor(cx.background());
         let app_state = Arc::new(workspace::AppState {
             client: client.clone(),
             user_store: user_store.clone(),
+            workspace_store,
             channel_store: channel_store.clone(),
-            languages: Arc::new(LanguageRegistry::test()),
+            languages: Arc::new(language_registry),
             fs: fs.clone(),
             build_window_options: |_, _, _| Default::default(),
             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
@@ -251,6 +256,19 @@ impl TestServer {
             .store(true, SeqCst);
     }
 
+    pub fn simulate_long_connection_interruption(
+        &self,
+        peer_id: PeerId,
+        deterministic: &Arc<Deterministic>,
+    ) {
+        self.forbid_connections();
+        self.disconnect_client(peer_id);
+        deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+        self.allow_connections();
+        deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
+        deterministic.run_until_parked();
+    }
+
     pub fn forbid_connections(&self) {
         self.forbid_connections.store(true, SeqCst);
     }
@@ -288,6 +306,7 @@ impl TestServer {
     pub async fn make_channel(
         &self,
         channel: &str,
+        parent: Option<u64>,
         admin: (&TestClient, &mut TestAppContext),
         members: &mut [(&TestClient, &mut TestAppContext)],
     ) -> u64 {
@@ -296,7 +315,7 @@ impl TestServer {
             .app_state
             .channel_store
             .update(admin_cx, |channel_store, cx| {
-                channel_store.create_channel(channel, None, cx)
+                channel_store.create_channel(channel, parent, cx)
             })
             .await
             .unwrap();
@@ -331,6 +350,39 @@ impl TestServer {
         channel_id
     }
 
+    pub async fn make_channel_tree(
+        &self,
+        channels: &[(&str, Option<&str>)],
+        creator: (&TestClient, &mut TestAppContext),
+    ) -> Vec<u64> {
+        let mut observed_channels = HashMap::default();
+        let mut result = Vec::new();
+        for (channel, parent) in channels {
+            let id;
+            if let Some(parent) = parent {
+                if let Some(parent_id) = observed_channels.get(parent) {
+                    id = self
+                        .make_channel(channel, Some(*parent_id), (creator.0, creator.1), &mut [])
+                        .await;
+                } else {
+                    panic!(
+                        "Edge {}->{} referenced before {} was created",
+                        parent, channel, parent
+                    )
+                }
+            } else {
+                id = self
+                    .make_channel(channel, None, (creator.0, creator.1), &mut [])
+                    .await;
+            }
+
+            observed_channels.insert(channel, id);
+            result.push(id);
+        }
+
+        result
+    }
+
     pub async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
         self.make_contacts(clients).await;
 
@@ -502,15 +554,7 @@ impl TestClient {
         root_path: impl AsRef<Path>,
         cx: &mut TestAppContext,
     ) -> (ModelHandle<Project>, WorktreeId) {
-        let project = cx.update(|cx| {
-            Project::local(
-                self.client().clone(),
-                self.app_state.user_store.clone(),
-                self.app_state.languages.clone(),
-                self.app_state.fs.clone(),
-                cx,
-            )
-        });
+        let project = self.build_empty_local_project(cx);
         let (worktree, _) = project
             .update(cx, |p, cx| {
                 p.find_or_create_local_worktree(root_path, true, cx)
@@ -523,6 +567,18 @@ impl TestClient {
         (project, worktree.read_with(cx, |tree, _| tree.id()))
     }
 
+    pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> ModelHandle<Project> {
+        cx.update(|cx| {
+            Project::local(
+                self.client().clone(),
+                self.app_state.user_store.clone(),
+                self.app_state.languages.clone(),
+                self.app_state.fs.clone(),
+                cx,
+            )
+        })
+    }
+
     pub async fn build_remote_project(
         &self,
         host_project_id: u64,
@@ -549,6 +605,34 @@ impl TestClient {
     ) -> WindowHandle<Workspace> {
         cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
     }
+
+    pub async fn add_admin_to_channel(
+        &self,
+        user: (&TestClient, &mut TestAppContext),
+        channel: u64,
+        cx_self: &mut TestAppContext,
+    ) {
+        let (other_client, other_cx) = user;
+
+        self.app_state
+            .channel_store
+            .update(cx_self, |channel_store, cx| {
+                channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
+            })
+            .await
+            .unwrap();
+
+        cx_self.foreground().run_until_parked();
+
+        other_client
+            .app_state
+            .channel_store
+            .update(other_cx, |channels, _| {
+                channels.respond_to_channel_invite(channel, true)
+            })
+            .await
+            .unwrap();
+    }
 }
 
 impl Drop for TestClient {

crates/collab_ui/Cargo.toml πŸ”—

@@ -30,12 +30,14 @@ channel = { path = "../channel" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
 context_menu = { path = "../context_menu" }
+drag_and_drop = { path = "../drag_and_drop" }
 editor = { path = "../editor" }
 feedback = { path = "../feedback" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 menu = { path = "../menu" }
+rich_text = { path = "../rich_text" }
 picker = { path = "../picker" }
 project = { path = "../project" }
 recent_projects = {path = "../recent_projects"}
@@ -55,6 +57,7 @@ schemars.workspace = true
 postage.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
+time.workspace = true
 
 [dev-dependencies]
 call = { path = "../call", features = ["test-support"] }

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

@@ -1,12 +1,12 @@
 use anyhow::{anyhow, Result};
-use channel::{
-    channel_buffer::{self, ChannelBuffer},
-    ChannelId,
+use call::report_call_event_for_channel;
+use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
+use client::{
+    proto::{self, PeerId},
+    Collaborator, ParticipantIndex,
 };
-use client::proto;
-use clock::ReplicaId;
 use collections::HashMap;
-use editor::Editor;
+use editor::{CollaborationHub, Editor};
 use gpui::{
     actions,
     elements::{ChildView, Label},
@@ -15,7 +15,11 @@ use gpui::{
     ViewContext, ViewHandle,
 };
 use project::Project;
-use std::any::{Any, TypeId};
+use std::{
+    any::{Any, TypeId},
+    sync::Arc,
+};
+use util::ResultExt;
 use workspace::{
     item::{FollowableItem, Item, ItemHandle},
     register_followable_item,
@@ -25,13 +29,14 @@ use workspace::{
 
 actions!(channel_view, [Deploy]);
 
-pub(crate) fn init(cx: &mut AppContext) {
+pub fn init(cx: &mut AppContext) {
     register_followable_item::<ChannelView>(cx)
 }
 
 pub struct ChannelView {
     pub editor: ViewHandle<Editor>,
     project: ModelHandle<Project>,
+    channel_store: ModelHandle<ChannelStore>,
     channel_buffer: ModelHandle<ChannelBuffer>,
     remote_id: Option<ViewId>,
     _editor_event_subscription: Subscription,
@@ -39,6 +44,28 @@ pub struct ChannelView {
 
 impl ChannelView {
     pub fn open(
+        channel_id: ChannelId,
+        workspace: ViewHandle<Workspace>,
+        cx: &mut AppContext,
+    ) -> Task<Result<ViewHandle<Self>>> {
+        let pane = workspace.read(cx).active_pane().clone();
+        let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
+        cx.spawn(|mut cx| async move {
+            let channel_view = channel_view.await?;
+            pane.update(&mut cx, |pane, cx| {
+                report_call_event_for_channel(
+                    "open channel notes",
+                    channel_id,
+                    &workspace.read(cx).app_state().client,
+                    cx,
+                );
+                pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
+            });
+            anyhow::Ok(channel_view)
+        })
+    }
+
+    pub fn open_in_pane(
         channel_id: ChannelId,
         pane: ViewHandle<Pane>,
         workspace: ViewHandle<Workspace>,
@@ -56,17 +83,25 @@ impl ChannelView {
 
         cx.spawn(|mut cx| async move {
             let channel_buffer = channel_buffer.await?;
-            let markdown = markdown.await?;
-            channel_buffer.update(&mut cx, |buffer, cx| {
-                buffer.buffer().update(cx, |buffer, cx| {
-                    buffer.set_language(Some(markdown), cx);
-                })
-            });
+
+            if let Some(markdown) = markdown.await.log_err() {
+                channel_buffer.update(&mut cx, |buffer, cx| {
+                    buffer.buffer().update(cx, |buffer, cx| {
+                        buffer.set_language(Some(markdown), cx);
+                    })
+                });
+            }
 
             pane.update(&mut cx, |pane, cx| {
                 pane.items_of_type::<Self>()
                     .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
-                    .unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx)))
+                    .unwrap_or_else(|| {
+                        cx.add_view(|cx| {
+                            let mut this = Self::new(project, channel_store, channel_buffer, cx);
+                            this.acknowledge_buffer_version(cx);
+                            this
+                        })
+                    })
             })
             .ok_or_else(|| anyhow!("pane was dropped"))
         })
@@ -74,96 +109,79 @@ impl ChannelView {
 
     pub fn new(
         project: ModelHandle<Project>,
+        channel_store: ModelHandle<ChannelStore>,
         channel_buffer: ModelHandle<ChannelBuffer>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = channel_buffer.read(cx).buffer();
-        // buffer.update(cx, |buffer, cx| buffer.set_language(language, cx));
-        let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
+        let editor = cx.add_view(|cx| {
+            let mut editor = Editor::for_buffer(buffer, None, cx);
+            editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
+                channel_buffer.clone(),
+            )));
+            editor
+        });
         let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
 
-        cx.subscribe(&project, Self::handle_project_event).detach();
         cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
             .detach();
 
-        let this = Self {
+        Self {
             editor,
             project,
+            channel_store,
             channel_buffer,
             remote_id: None,
             _editor_event_subscription,
-        };
-        this.refresh_replica_id_map(cx);
-        this
+        }
     }
 
-    fn handle_project_event(
-        &mut self,
-        _: ModelHandle<Project>,
-        event: &project::Event,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            project::Event::RemoteIdChanged(_) => {}
-            project::Event::DisconnectedFromHost => {}
-            project::Event::Closed => {}
-            project::Event::CollaboratorUpdated { .. } => {}
-            project::Event::CollaboratorLeft(_) => {}
-            project::Event::CollaboratorJoined(_) => {}
-            _ => return,
-        }
-        self.refresh_replica_id_map(cx);
+    pub fn channel(&self, cx: &AppContext) -> Arc<Channel> {
+        self.channel_buffer.read(cx).channel()
     }
 
     fn handle_channel_buffer_event(
         &mut self,
         _: ModelHandle<ChannelBuffer>,
-        event: &channel_buffer::Event,
+        event: &ChannelBufferEvent,
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            channel_buffer::Event::CollaboratorsChanged => {
-                self.refresh_replica_id_map(cx);
-            }
-            channel_buffer::Event::Disconnected => self.editor.update(cx, |editor, cx| {
+            ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
                 editor.set_read_only(true);
                 cx.notify();
             }),
+            ChannelBufferEvent::BufferEdited => {
+                if cx.is_self_focused() || self.editor.is_focused(cx) {
+                    self.acknowledge_buffer_version(cx);
+                } else {
+                    self.channel_store.update(cx, |store, cx| {
+                        let channel_buffer = self.channel_buffer.read(cx);
+                        store.notes_changed(
+                            channel_buffer.channel().id,
+                            channel_buffer.epoch(),
+                            &channel_buffer.buffer().read(cx).version(),
+                            cx,
+                        )
+                    });
+                }
+            }
+            _ => {}
         }
     }
 
-    /// Build a mapping of channel buffer replica ids to the corresponding
-    /// replica ids in the current project.
-    ///
-    /// Using this mapping, a given user can be displayed with the same color
-    /// in the channel buffer as in other files in the project. Users who are
-    /// in the channel buffer but not the project will not have a color.
-    fn refresh_replica_id_map(&self, cx: &mut ViewContext<Self>) {
-        let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default();
-        let project = self.project.read(cx);
-        let channel_buffer = self.channel_buffer.read(cx);
-        project_replica_ids_by_channel_buffer_replica_id
-            .insert(channel_buffer.replica_id(cx), project.replica_id());
-        project_replica_ids_by_channel_buffer_replica_id.extend(
-            channel_buffer
-                .collaborators()
-                .iter()
-                .filter_map(|channel_buffer_collaborator| {
-                    project
-                        .collaborators()
-                        .values()
-                        .find_map(|project_collaborator| {
-                            (project_collaborator.user_id == channel_buffer_collaborator.user_id)
-                                .then_some((
-                                    channel_buffer_collaborator.replica_id as ReplicaId,
-                                    project_collaborator.replica_id,
-                                ))
-                        })
-                }),
-        );
-
-        self.editor.update(cx, |editor, cx| {
-            editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx)
+    fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
+        self.channel_store.update(cx, |store, cx| {
+            let channel_buffer = self.channel_buffer.read(cx);
+            store.acknowledge_notes_version(
+                channel_buffer.channel().id,
+                channel_buffer.epoch(),
+                &channel_buffer.buffer().read(cx).version(),
+                cx,
+            )
+        });
+        self.channel_buffer.update(cx, |buffer, cx| {
+            buffer.acknowledge_buffer_version(cx);
         });
     }
 }
@@ -183,6 +201,7 @@ impl View for ChannelView {
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
         if cx.is_self_focused() {
+            self.acknowledge_buffer_version(cx);
             cx.focus(self.editor.as_any())
         }
     }
@@ -222,6 +241,7 @@ impl Item for ChannelView {
     fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
         Some(Self::new(
             self.project.clone(),
+            self.channel_store.clone(),
             self.channel_buffer.clone(),
             cx,
         ))
@@ -294,7 +314,7 @@ impl FollowableItem for ChannelView {
             unreachable!()
         };
 
-        let open = ChannelView::open(state.channel_id, pane, workspace, cx);
+        let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
 
         Some(cx.spawn(|mut cx| async move {
             let this = open.await?;
@@ -354,17 +374,32 @@ impl FollowableItem for ChannelView {
         })
     }
 
-    fn set_leader_replica_id(
-        &mut self,
-        leader_replica_id: Option<u16>,
-        cx: &mut ViewContext<Self>,
-    ) {
+    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
         self.editor.update(cx, |editor, cx| {
-            editor.set_leader_replica_id(leader_replica_id, cx)
+            editor.set_leader_peer_id(leader_peer_id, cx)
         })
     }
 
     fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
         Editor::should_unfollow_on_event(event, cx)
     }
+
+    fn is_project_item(&self, _cx: &AppContext) -> bool {
+        false
+    }
+}
+
+struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
+
+impl CollaborationHub for ChannelBufferCollaborationHub {
+    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+        self.0.read(cx).collaborators()
+    }
+
+    fn user_participant_indices<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> &'a HashMap<u64, ParticipantIndex> {
+        self.0.read(cx).user_store().read(cx).participant_indices()
+    }
 }

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

@@ -0,0 +1,885 @@
+use crate::{channel_view::ChannelView, ChatPanelSettings};
+use anyhow::Result;
+use call::ActiveCall;
+use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
+use client::Client;
+use collections::HashMap;
+use db::kvp::KEY_VALUE_STORE;
+use editor::Editor;
+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
+use gpui::{
+    actions,
+    elements::*,
+    platform::{CursorStyle, MouseButton},
+    serde_json,
+    views::{ItemType, Select, SelectStyle},
+    AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task,
+    View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use language::{language_settings::SoftWrap, LanguageRegistry};
+use menu::Confirm;
+use project::Fs;
+use rich_text::RichText;
+use serde::{Deserialize, Serialize};
+use settings::SettingsStore;
+use std::sync::Arc;
+use theme::{IconButton, Theme};
+use time::{OffsetDateTime, UtcOffset};
+use util::{ResultExt, TryFutureExt};
+use workspace::{
+    dock::{DockPosition, Panel},
+    Workspace,
+};
+
+const MESSAGE_LOADING_THRESHOLD: usize = 50;
+const CHAT_PANEL_KEY: &'static str = "ChatPanel";
+
+pub struct ChatPanel {
+    client: Arc<Client>,
+    channel_store: ModelHandle<ChannelStore>,
+    languages: Arc<LanguageRegistry>,
+    active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
+    message_list: ListState<ChatPanel>,
+    input_editor: ViewHandle<Editor>,
+    channel_select: ViewHandle<Select>,
+    local_timezone: UtcOffset,
+    fs: Arc<dyn Fs>,
+    width: Option<f32>,
+    active: bool,
+    pending_serialization: Task<Option<()>>,
+    subscriptions: Vec<gpui::Subscription>,
+    workspace: WeakViewHandle<Workspace>,
+    has_focus: bool,
+    markdown_data: HashMap<ChannelMessageId, RichText>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedChatPanel {
+    width: Option<f32>,
+}
+
+#[derive(Debug)]
+pub enum Event {
+    DockPositionChanged,
+    Focus,
+    Dismissed,
+}
+
+actions!(
+    chat_panel,
+    [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
+);
+
+pub fn init(cx: &mut AppContext) {
+    cx.add_action(ChatPanel::send);
+    cx.add_action(ChatPanel::load_more_messages);
+    cx.add_action(ChatPanel::open_notes);
+    cx.add_action(ChatPanel::join_call);
+}
+
+impl ChatPanel {
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+        let fs = workspace.app_state().fs.clone();
+        let client = workspace.app_state().client.clone();
+        let channel_store = workspace.app_state().channel_store.clone();
+        let languages = workspace.app_state().languages.clone();
+
+        let input_editor = cx.add_view(|cx| {
+            let mut editor = Editor::auto_height(
+                4,
+                Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
+                cx,
+            );
+            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+            editor
+        });
+
+        let workspace_handle = workspace.weak_handle();
+
+        let channel_select = cx.add_view(|cx| {
+            let channel_store = channel_store.clone();
+            let workspace = workspace_handle.clone();
+            Select::new(0, cx, {
+                move |ix, item_type, is_hovered, cx| {
+                    Self::render_channel_name(
+                        &channel_store,
+                        ix,
+                        item_type,
+                        is_hovered,
+                        workspace,
+                        cx,
+                    )
+                }
+            })
+            .with_style(move |cx| {
+                let style = &theme::current(cx).chat_panel.channel_select;
+                SelectStyle {
+                    header: Default::default(),
+                    menu: style.menu,
+                }
+            })
+        });
+
+        let mut message_list =
+            ListState::<Self>::new(0, Orientation::Bottom, 1000., move |this, ix, cx| {
+                this.render_message(ix, cx)
+            });
+        message_list.set_scroll_handler(|visible_range, this, cx| {
+            if visible_range.start < MESSAGE_LOADING_THRESHOLD {
+                this.load_more_messages(&LoadMoreMessages, cx);
+            }
+        });
+
+        cx.add_view(|cx| {
+            let mut this = Self {
+                fs,
+                client,
+                channel_store,
+                languages,
+
+                active_chat: Default::default(),
+                pending_serialization: Task::ready(None),
+                message_list,
+                input_editor,
+                channel_select,
+                local_timezone: cx.platform().local_timezone(),
+                has_focus: false,
+                subscriptions: Vec::new(),
+                workspace: workspace_handle,
+                active: false,
+                width: None,
+                markdown_data: Default::default(),
+            };
+
+            let mut old_dock_position = this.position(cx);
+            this.subscriptions
+                .push(
+                    cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
+                        let new_dock_position = this.position(cx);
+                        if new_dock_position != old_dock_position {
+                            old_dock_position = new_dock_position;
+                            cx.emit(Event::DockPositionChanged);
+                        }
+                        cx.notify();
+                    }),
+                );
+
+            this.update_channel_count(cx);
+            cx.observe(&this.channel_store, |this, _, cx| {
+                this.update_channel_count(cx)
+            })
+            .detach();
+
+            cx.observe(&this.channel_select, |this, channel_select, cx| {
+                let selected_ix = channel_select.read(cx).selected_index();
+
+                let selected_channel_id = this
+                    .channel_store
+                    .read(cx)
+                    .channel_at(selected_ix)
+                    .map(|e| e.id);
+                if let Some(selected_channel_id) = selected_channel_id {
+                    this.select_channel(selected_channel_id, cx)
+                        .detach_and_log_err(cx);
+                }
+            })
+            .detach();
+
+            let markdown = this.languages.language_for_name("Markdown");
+            cx.spawn(|this, mut cx| async move {
+                let markdown = markdown.await?;
+
+                this.update(&mut cx, |this, cx| {
+                    this.input_editor.update(cx, |editor, cx| {
+                        editor.buffer().update(cx, |multi_buffer, cx| {
+                            multi_buffer
+                                .as_singleton()
+                                .unwrap()
+                                .update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx))
+                        })
+                    })
+                })?;
+
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+
+            this
+        })
+    }
+
+    pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
+        self.active_chat.as_ref().map(|(chat, _)| chat.clone())
+    }
+
+    pub fn load(
+        workspace: WeakViewHandle<Workspace>,
+        cx: AsyncAppContext,
+    ) -> Task<Result<ViewHandle<Self>>> {
+        cx.spawn(|mut cx| async move {
+            let serialized_panel = if let Some(panel) = cx
+                .background()
+                .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
+                .await
+                .log_err()
+                .flatten()
+            {
+                Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
+            } else {
+                None
+            };
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let panel = Self::new(workspace, cx);
+                if let Some(serialized_panel) = serialized_panel {
+                    panel.update(cx, |panel, cx| {
+                        panel.width = serialized_panel.width;
+                        cx.notify();
+                    });
+                }
+                panel
+            })
+        })
+    }
+
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let width = self.width;
+        self.pending_serialization = cx.background().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        CHAT_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedChatPanel { width })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
+
+    fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
+        let channel_count = self.channel_store.read(cx).channel_count();
+        self.channel_select.update(cx, |select, cx| {
+            select.set_item_count(channel_count, cx);
+        });
+    }
+
+    fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
+        if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
+            let id = chat.read(cx).channel().id;
+            {
+                let chat = chat.read(cx);
+                self.message_list.reset(chat.message_count());
+                let placeholder = format!("Message #{}", chat.channel().name);
+                self.input_editor.update(cx, move |editor, cx| {
+                    editor.set_placeholder_text(placeholder, cx);
+                });
+            }
+            let subscription = cx.subscribe(&chat, Self::channel_did_change);
+            self.active_chat = Some((chat, subscription));
+            self.acknowledge_last_message(cx);
+            self.channel_select.update(cx, |select, cx| {
+                if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) {
+                    select.set_selected_index(ix, cx);
+                }
+            });
+            cx.notify();
+        }
+    }
+
+    fn channel_did_change(
+        &mut self,
+        _: ModelHandle<ChannelChat>,
+        event: &ChannelChatEvent,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            ChannelChatEvent::MessagesUpdated {
+                old_range,
+                new_count,
+            } => {
+                self.message_list.splice(old_range.clone(), *new_count);
+                if self.active {
+                    self.acknowledge_last_message(cx);
+                }
+            }
+            ChannelChatEvent::NewMessage {
+                channel_id,
+                message_id,
+            } => {
+                if !self.active {
+                    self.channel_store.update(cx, |store, cx| {
+                        store.new_message(*channel_id, *message_id, cx)
+                    })
+                }
+            }
+        }
+        cx.notify();
+    }
+
+    fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
+        if self.active {
+            if let Some((chat, _)) = &self.active_chat {
+                chat.update(cx, |chat, cx| {
+                    chat.acknowledge_last_message(cx);
+                });
+            }
+        }
+    }
+
+    fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let theme = theme::current(cx);
+        Flex::column()
+            .with_child(
+                ChildView::new(&self.channel_select, cx)
+                    .contained()
+                    .with_style(theme.chat_panel.channel_select.container),
+            )
+            .with_child(self.render_active_channel_messages(&theme))
+            .with_child(self.render_input_box(&theme, cx))
+            .into_any()
+    }
+
+    fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
+        let messages = if self.active_chat.is_some() {
+            List::new(self.message_list.clone())
+                .contained()
+                .with_style(theme.chat_panel.list)
+                .into_any()
+        } else {
+            Empty::new().into_any()
+        };
+
+        messages.flex(1., true).into_any()
+    }
+
+    fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let (message, is_continuation, is_last) = {
+            let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
+            let last_message = active_chat.message(ix.saturating_sub(1));
+            let this_message = active_chat.message(ix);
+            let is_continuation = last_message.id != this_message.id
+                && this_message.sender.id == last_message.sender.id;
+
+            (
+                active_chat.message(ix).clone(),
+                is_continuation,
+                active_chat.message_count() == ix + 1,
+            )
+        };
+
+        let is_pending = message.is_pending();
+        let text = self
+            .markdown_data
+            .entry(message.id)
+            .or_insert_with(|| rich_text::render_markdown(message.body, &self.languages, None));
+
+        let now = OffsetDateTime::now_utc();
+        let theme = theme::current(cx);
+        let style = if is_pending {
+            &theme.chat_panel.pending_message
+        } else if is_continuation {
+            &theme.chat_panel.continuation_message
+        } else {
+            &theme.chat_panel.message
+        };
+
+        let belongs_to_user = Some(message.sender.id) == self.client.user_id();
+        let message_id_to_remove =
+            if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user) {
+                Some(id)
+            } else {
+                None
+            };
+
+        enum MessageBackgroundHighlight {}
+        MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
+            let container = style.container.style_for(state);
+            if is_continuation {
+                Flex::row()
+                    .with_child(
+                        text.element(
+                            theme.editor.syntax.clone(),
+                            style.body.clone(),
+                            theme.editor.document_highlight_read_background,
+                            cx,
+                        )
+                        .flex(1., true),
+                    )
+                    .with_child(render_remove(message_id_to_remove, cx, &theme))
+                    .contained()
+                    .with_style(*container)
+                    .with_margin_bottom(if is_last {
+                        theme.chat_panel.last_message_bottom_spacing
+                    } else {
+                        0.
+                    })
+                    .into_any()
+            } else {
+                Flex::column()
+                    .with_child(
+                        Flex::row()
+                            .with_child(
+                                Flex::row()
+                                    .with_child(render_avatar(
+                                        message.sender.avatar.clone(),
+                                        &theme,
+                                    ))
+                                    .with_child(
+                                        Label::new(
+                                            message.sender.github_login.clone(),
+                                            style.sender.text.clone(),
+                                        )
+                                        .contained()
+                                        .with_style(style.sender.container),
+                                    )
+                                    .with_child(
+                                        Label::new(
+                                            format_timestamp(
+                                                message.timestamp,
+                                                now,
+                                                self.local_timezone,
+                                            ),
+                                            style.timestamp.text.clone(),
+                                        )
+                                        .contained()
+                                        .with_style(style.timestamp.container),
+                                    )
+                                    .align_children_center()
+                                    .flex(1., true),
+                            )
+                            .with_child(render_remove(message_id_to_remove, cx, &theme))
+                            .align_children_center(),
+                    )
+                    .with_child(
+                        Flex::row()
+                            .with_child(
+                                text.element(
+                                    theme.editor.syntax.clone(),
+                                    style.body.clone(),
+                                    theme.editor.document_highlight_read_background,
+                                    cx,
+                                )
+                                .flex(1., true),
+                            )
+                            // Add a spacer to make everything line up
+                            .with_child(render_remove(None, cx, &theme)),
+                    )
+                    .contained()
+                    .with_style(*container)
+                    .with_margin_bottom(if is_last {
+                        theme.chat_panel.last_message_bottom_spacing
+                    } else {
+                        0.
+                    })
+                    .into_any()
+            }
+        })
+        .into_any()
+    }
+
+    fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
+        ChildView::new(&self.input_editor, cx)
+            .contained()
+            .with_style(theme.chat_panel.input_editor.container)
+            .into_any()
+    }
+
+    fn render_channel_name(
+        channel_store: &ModelHandle<ChannelStore>,
+        ix: usize,
+        item_type: ItemType,
+        is_hovered: bool,
+        workspace: WeakViewHandle<Workspace>,
+        cx: &mut ViewContext<Select>,
+    ) -> AnyElement<Select> {
+        let theme = theme::current(cx);
+        let tooltip_style = &theme.tooltip;
+        let theme = &theme.chat_panel;
+        let style = match (&item_type, is_hovered) {
+            (ItemType::Header, _) => &theme.channel_select.header,
+            (ItemType::Selected, _) => &theme.channel_select.active_item,
+            (ItemType::Unselected, false) => &theme.channel_select.item,
+            (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
+        };
+
+        let channel = &channel_store.read(cx).channel_at(ix).unwrap();
+        let channel_id = channel.id;
+
+        let mut row = Flex::row()
+            .with_child(
+                Label::new("#".to_string(), style.hash.text.clone())
+                    .contained()
+                    .with_style(style.hash.container),
+            )
+            .with_child(Label::new(channel.name.clone(), style.name.clone()));
+
+        if matches!(item_type, ItemType::Header) {
+            row.add_children([
+                MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
+                    render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
+                })
+                .on_click(MouseButton::Left, move |_, _, cx| {
+                    if let Some(workspace) = workspace.upgrade(cx) {
+                        ChannelView::open(channel_id, workspace, cx).detach();
+                    }
+                })
+                .with_tooltip::<OpenChannelNotes>(
+                    channel_id as usize,
+                    "Open Notes",
+                    Some(Box::new(OpenChannelNotes)),
+                    tooltip_style.clone(),
+                    cx,
+                )
+                .flex_float(),
+                MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
+                    render_icon_button(
+                        theme.icon_button.style_for(mouse_state),
+                        "icons/speaker-loud.svg",
+                    )
+                })
+                .on_click(MouseButton::Left, move |_, _, cx| {
+                    ActiveCall::global(cx)
+                        .update(cx, |call, cx| call.join_channel(channel_id, cx))
+                        .detach_and_log_err(cx);
+                })
+                .with_tooltip::<ActiveCall>(
+                    channel_id as usize,
+                    "Join Call",
+                    Some(Box::new(JoinCall)),
+                    tooltip_style.clone(),
+                    cx,
+                )
+                .flex_float(),
+            ]);
+        }
+
+        row.align_children_center()
+            .contained()
+            .with_style(style.container)
+            .into_any()
+    }
+
+    fn render_sign_in_prompt(
+        &self,
+        theme: &Arc<Theme>,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        enum SignInPromptLabel {}
+
+        MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
+            Label::new(
+                "Sign in to use chat".to_string(),
+                theme
+                    .chat_panel
+                    .sign_in_prompt
+                    .style_for(mouse_state)
+                    .clone(),
+            )
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, this, cx| {
+            let client = this.client.clone();
+            cx.spawn(|this, mut cx| async move {
+                if client
+                    .authenticate_and_connect(true, &cx)
+                    .log_err()
+                    .await
+                    .is_some()
+                {
+                    this.update(&mut cx, |this, cx| {
+                        if cx.handle().is_focused(cx) {
+                            cx.focus(&this.input_editor);
+                        }
+                    })
+                    .ok();
+                }
+            })
+            .detach();
+        })
+        .aligned()
+        .into_any()
+    }
+
+    fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = self.active_chat.as_ref() {
+            let body = self.input_editor.update(cx, |editor, cx| {
+                let body = editor.text(cx);
+                editor.clear(cx);
+                body
+            });
+
+            if let Some(task) = chat
+                .update(cx, |chat, cx| chat.send_message(body, cx))
+                .log_err()
+            {
+                task.detach();
+            }
+        }
+    }
+
+    fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = self.active_chat.as_ref() {
+            chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
+        }
+    }
+
+    fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = self.active_chat.as_ref() {
+            chat.update(cx, |channel, cx| {
+                channel.load_more_messages(cx);
+            })
+        }
+    }
+
+    pub fn select_channel(
+        &mut self,
+        selected_channel_id: u64,
+        cx: &mut ViewContext<ChatPanel>,
+    ) -> Task<Result<()>> {
+        if let Some((chat, _)) = &self.active_chat {
+            if chat.read(cx).channel().id == selected_channel_id {
+                return Task::ready(Ok(()));
+            }
+        }
+
+        let open_chat = self.channel_store.update(cx, |store, cx| {
+            store.open_channel_chat(selected_channel_id, cx)
+        });
+        cx.spawn(|this, mut cx| async move {
+            let chat = open_chat.await?;
+            this.update(&mut cx, |this, cx| {
+                this.markdown_data = Default::default();
+                this.set_active_chat(chat, cx);
+            })
+        })
+    }
+
+    fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = &self.active_chat {
+            let channel_id = chat.read(cx).channel().id;
+            if let Some(workspace) = self.workspace.upgrade(cx) {
+                ChannelView::open(channel_id, workspace, cx).detach();
+            }
+        }
+    }
+
+    fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
+        if let Some((chat, _)) = &self.active_chat {
+            let channel_id = chat.read(cx).channel().id;
+            ActiveCall::global(cx)
+                .update(cx, |call, cx| call.join_channel(channel_id, cx))
+                .detach_and_log_err(cx);
+        }
+    }
+}
+
+fn render_avatar(avatar: Option<Arc<ImageData>>, theme: &Arc<Theme>) -> AnyElement<ChatPanel> {
+    let avatar_style = theme.chat_panel.avatar;
+
+    avatar
+        .map(|avatar| {
+            Image::from_data(avatar)
+                .with_style(avatar_style.image)
+                .aligned()
+                .contained()
+                .with_corner_radius(avatar_style.outer_corner_radius)
+                .constrained()
+                .with_width(avatar_style.outer_width)
+                .with_height(avatar_style.outer_width)
+                .into_any()
+        })
+        .unwrap_or_else(|| {
+            Empty::new()
+                .constrained()
+                .with_width(avatar_style.outer_width)
+                .into_any()
+        })
+        .contained()
+        .with_style(theme.chat_panel.avatar_container)
+        .into_any()
+}
+
+fn render_remove(
+    message_id_to_remove: Option<u64>,
+    cx: &mut ViewContext<'_, '_, ChatPanel>,
+    theme: &Arc<Theme>,
+) -> AnyElement<ChatPanel> {
+    enum DeleteMessage {}
+
+    message_id_to_remove
+        .map(|id| {
+            MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
+                let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
+                render_icon_button(button_style, "icons/x.svg")
+                    .aligned()
+                    .into_any()
+            })
+            .with_padding(Padding::uniform(2.))
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                this.remove_message(id, cx);
+            })
+            .flex_float()
+            .into_any()
+        })
+        .unwrap_or_else(|| {
+            let style = theme.chat_panel.icon_button.default;
+
+            Empty::new()
+                .constrained()
+                .with_width(style.icon_width)
+                .aligned()
+                .constrained()
+                .with_width(style.button_width)
+                .with_height(style.button_width)
+                .contained()
+                .with_uniform_padding(2.)
+                .flex_float()
+                .into_any()
+        })
+}
+
+impl Entity for ChatPanel {
+    type Event = Event;
+}
+
+impl View for ChatPanel {
+    fn ui_name() -> &'static str {
+        "ChatPanel"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let theme = theme::current(cx);
+        let element = if self.client.user_id().is_some() {
+            self.render_channel(cx)
+        } else {
+            self.render_sign_in_prompt(&theme, cx)
+        };
+        element
+            .contained()
+            .with_style(theme.chat_panel.container)
+            .constrained()
+            .with_min_width(150.)
+            .into_any()
+    }
+
+    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+        self.has_focus = true;
+        if matches!(
+            *self.client.status().borrow(),
+            client::Status::Connected { .. }
+        ) {
+            cx.focus(&self.input_editor);
+        }
+    }
+
+    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
+}
+
+impl Panel for ChatPanel {
+    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+        settings::get::<ChatPanelSettings>(cx).dock
+    }
+
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        matches!(position, DockPosition::Left | DockPosition::Right)
+    }
+
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
+            settings.dock = Some(position)
+        });
+    }
+
+    fn size(&self, cx: &gpui::WindowContext) -> f32 {
+        self.width
+            .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
+    }
+
+    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+        self.width = size;
+        self.serialize(cx);
+        cx.notify();
+    }
+
+    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
+        self.active = active;
+        if active {
+            self.acknowledge_last_message(cx);
+            if !is_chat_feature_enabled(cx) {
+                cx.emit(Event::Dismissed);
+            }
+        }
+    }
+
+    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
+        (settings::get::<ChatPanelSettings>(cx).button && is_chat_feature_enabled(cx))
+            .then(|| "icons/conversations.svg")
+    }
+
+    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+        ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
+    }
+
+    fn should_change_position_on_event(event: &Self::Event) -> bool {
+        matches!(event, Event::DockPositionChanged)
+    }
+
+    fn should_close_on_event(event: &Self::Event) -> bool {
+        matches!(event, Event::Dismissed)
+    }
+
+    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
+        self.has_focus
+    }
+
+    fn is_focus_event(event: &Self::Event) -> bool {
+        matches!(event, Event::Focus)
+    }
+}
+
+fn is_chat_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
+    cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
+}
+
+fn format_timestamp(
+    mut timestamp: OffsetDateTime,
+    mut now: OffsetDateTime,
+    local_timezone: UtcOffset,
+) -> String {
+    timestamp = timestamp.to_offset(local_timezone);
+    now = now.to_offset(local_timezone);
+
+    let today = now.date();
+    let date = timestamp.date();
+    let mut hour = timestamp.hour();
+    let mut part = "am";
+    if hour > 12 {
+        hour -= 12;
+        part = "pm";
+    }
+    if date == today {
+        format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
+    } else if date.next_day() == Some(today) {
+        format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
+    } else {
+        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
+    }
+}
+
+fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
+    Svg::new(svg_path)
+        .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)
+}

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

@@ -1,24 +1,31 @@
 mod channel_modal;
 mod contact_finder;
-mod panel_settings;
 
+use crate::{
+    channel_view::{self, ChannelView},
+    chat_panel::ChatPanel,
+    face_pile::FacePile,
+    panel_settings, CollaborationPanelSettings,
+};
 use anyhow::Result;
 use call::ActiveCall;
-use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
+use channel_modal::ChannelModal;
 use client::{proto::PeerId, Client, Contact, User, UserStore};
+use contact_finder::ContactFinder;
 use context_menu::{ContextMenu, ContextMenuItem};
 use db::kvp::KEY_VALUE_STORE;
+use drag_and_drop::{DragAndDrop, Draggable};
 use editor::{Cancel, Editor};
-
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use futures::StreamExt;
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
     actions,
     elements::{
-        Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState,
-        MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable,
-        Stack, Svg,
+        Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
+        ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
+        SafeStylable, Stack, Svg,
     },
     fonts::TextStyle,
     geometry::{
@@ -31,62 +38,98 @@ use gpui::{
     Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
-use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings};
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
 use settings::SettingsStore;
-use std::{borrow::Cow, mem, sync::Arc};
-use theme::{components::ComponentExt, IconButton};
+use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
+use theme::{components::ComponentExt, IconButton, Interactive};
 use util::{iife, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
     item::ItemHandle,
-    Workspace,
+    FollowNextCollaborator, Workspace,
 };
 
-use crate::{
-    channel_view::{self, ChannelView},
-    face_pile::FacePile,
-};
-use channel_modal::ChannelModal;
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct ToggleCollapse {
+    location: ChannelPath,
+}
 
-use self::contact_finder::ContactFinder;
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct NewChannel {
+    location: ChannelPath,
+}
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RemoveChannel {
-    channel_id: u64,
+struct RenameChannel {
+    location: ChannelPath,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ToggleCollapse {
-    channel_id: u64,
+struct ToggleSelectedIx {
+    ix: usize,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct NewChannel {
-    channel_id: u64,
+struct RemoveChannel {
+    channel_id: ChannelId,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct InviteMembers {
-    channel_id: u64,
+    channel_id: ChannelId,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 struct ManageMembers {
-    channel_id: u64,
+    channel_id: ChannelId,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RenameChannel {
-    channel_id: u64,
+pub struct OpenChannelNotes {
+    pub channel_id: ChannelId,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct JoinChannelCall {
+    pub channel_id: u64,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct JoinChannelChat {
+    pub channel_id: u64,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct StartMoveChannelFor {
+    channel_id: ChannelId,
+    parent_id: Option<ChannelId>,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct StartLinkChannelFor {
+    channel_id: ChannelId,
+    parent_id: Option<ChannelId>,
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct OpenChannelBuffer {
-    channel_id: u64,
+struct LinkChannel {
+    to: ChannelId,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct MoveChannel {
+    to: ChannelId,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+struct UnlinkChannel {
+    channel_id: ChannelId,
+    parent_id: ChannelId,
+}
+
+type DraggedChannel = (Channel, Option<ChannelId>);
+
 actions!(
     collab_panel,
     [
@@ -94,7 +137,11 @@ actions!(
         Remove,
         Secondary,
         CollapseSelectedChannel,
-        ExpandSelectedChannel
+        ExpandSelectedChannel,
+        StartMoveChannel,
+        StartLinkChannel,
+        MoveOrLinkToSelected,
+        InsertSpace,
     ]
 );
 
@@ -107,13 +154,34 @@ impl_actions!(
         ManageMembers,
         RenameChannel,
         ToggleCollapse,
-        OpenChannelBuffer
+        OpenChannelNotes,
+        JoinChannelCall,
+        JoinChannelChat,
+        LinkChannel,
+        StartMoveChannelFor,
+        StartLinkChannelFor,
+        MoveChannel,
+        UnlinkChannel,
+        ToggleSelectedIx
     ]
 );
 
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+struct ChannelMoveClipboard {
+    channel_id: ChannelId,
+    parent_id: Option<ChannelId>,
+    intent: ClipboardIntent,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+enum ClipboardIntent {
+    Move,
+    Link,
+}
+
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 
-pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
+pub fn init(cx: &mut AppContext) {
     settings::register::<panel_settings::CollaborationPanelSettings>(cx);
     contact_finder::init(cx);
     channel_modal::init(cx);
@@ -123,6 +191,7 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
     cx.add_action(CollabPanel::select_next);
     cx.add_action(CollabPanel::select_prev);
     cx.add_action(CollabPanel::confirm);
+    cx.add_action(CollabPanel::insert_space);
     cx.add_action(CollabPanel::remove);
     cx.add_action(CollabPanel::remove_selected_channel);
     cx.add_action(CollabPanel::show_inline_context_menu);
@@ -131,20 +200,149 @@ pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
     cx.add_action(CollabPanel::manage_members);
     cx.add_action(CollabPanel::rename_selected_channel);
     cx.add_action(CollabPanel::rename_channel);
-    cx.add_action(CollabPanel::toggle_channel_collapsed);
+    cx.add_action(CollabPanel::toggle_channel_collapsed_action);
     cx.add_action(CollabPanel::collapse_selected_channel);
     cx.add_action(CollabPanel::expand_selected_channel);
-    cx.add_action(CollabPanel::open_channel_buffer);
+    cx.add_action(CollabPanel::open_channel_notes);
+    cx.add_action(CollabPanel::join_channel_chat);
+
+    cx.add_action(
+        |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
+            if panel.selection.take() != Some(action.ix) {
+                panel.selection = Some(action.ix)
+            }
+
+            cx.notify();
+        },
+    );
+
+    cx.add_action(
+        |panel: &mut CollabPanel,
+         action: &StartMoveChannelFor,
+         _: &mut ViewContext<CollabPanel>| {
+            panel.channel_clipboard = Some(ChannelMoveClipboard {
+                channel_id: action.channel_id,
+                parent_id: action.parent_id,
+                intent: ClipboardIntent::Move,
+            });
+        },
+    );
+
+    cx.add_action(
+        |panel: &mut CollabPanel,
+         action: &StartLinkChannelFor,
+         _: &mut ViewContext<CollabPanel>| {
+            panel.channel_clipboard = Some(ChannelMoveClipboard {
+                channel_id: action.channel_id,
+                parent_id: action.parent_id,
+                intent: ClipboardIntent::Link,
+            })
+        },
+    );
+
+    cx.add_action(
+        |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
+            if let Some((_, path)) = panel.selected_channel() {
+                panel.channel_clipboard = Some(ChannelMoveClipboard {
+                    channel_id: path.channel_id(),
+                    parent_id: path.parent_id(),
+                    intent: ClipboardIntent::Move,
+                })
+            }
+        },
+    );
+
+    cx.add_action(
+        |panel: &mut CollabPanel, _: &StartLinkChannel, _: &mut ViewContext<CollabPanel>| {
+            if let Some((_, path)) = panel.selected_channel() {
+                panel.channel_clipboard = Some(ChannelMoveClipboard {
+                    channel_id: path.channel_id(),
+                    parent_id: path.parent_id(),
+                    intent: ClipboardIntent::Link,
+                })
+            }
+        },
+    );
+
+    cx.add_action(
+        |panel: &mut CollabPanel, _: &MoveOrLinkToSelected, cx: &mut ViewContext<CollabPanel>| {
+            let clipboard = panel.channel_clipboard.take();
+            if let Some(((selected_channel, _), clipboard)) =
+                panel.selected_channel().zip(clipboard)
+            {
+                match clipboard.intent {
+                    ClipboardIntent::Move if clipboard.parent_id.is_some() => {
+                        let parent_id = clipboard.parent_id.unwrap();
+                        panel.channel_store.update(cx, |channel_store, cx| {
+                            channel_store
+                                .move_channel(
+                                    clipboard.channel_id,
+                                    parent_id,
+                                    selected_channel.id,
+                                    cx,
+                                )
+                                .detach_and_log_err(cx)
+                        })
+                    }
+                    _ => panel.channel_store.update(cx, |channel_store, cx| {
+                        channel_store
+                            .link_channel(clipboard.channel_id, selected_channel.id, cx)
+                            .detach_and_log_err(cx)
+                    }),
+                }
+            }
+        },
+    );
+
+    cx.add_action(
+        |panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext<CollabPanel>| {
+            if let Some(clipboard) = panel.channel_clipboard.take() {
+                panel.channel_store.update(cx, |channel_store, cx| {
+                    channel_store
+                        .link_channel(clipboard.channel_id, action.to, cx)
+                        .detach_and_log_err(cx)
+                })
+            }
+        },
+    );
+
+    cx.add_action(
+        |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
+            if let Some(clipboard) = panel.channel_clipboard.take() {
+                panel.channel_store.update(cx, |channel_store, cx| {
+                    if let Some(parent) = clipboard.parent_id {
+                        channel_store
+                            .move_channel(clipboard.channel_id, parent, action.to, cx)
+                            .detach_and_log_err(cx)
+                    } else {
+                        channel_store
+                            .link_channel(clipboard.channel_id, action.to, cx)
+                            .detach_and_log_err(cx)
+                    }
+                })
+            }
+        },
+    );
+
+    cx.add_action(
+        |panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext<CollabPanel>| {
+            panel.channel_store.update(cx, |channel_store, cx| {
+                channel_store
+                    .unlink_channel(action.channel_id, action.parent_id, cx)
+                    .detach_and_log_err(cx)
+            })
+        },
+    );
 }
 
 #[derive(Debug)]
 pub enum ChannelEditingState {
     Create {
-        parent_id: Option<u64>,
+        location: Option<ChannelPath>,
         pending_name: Option<String>,
     },
     Rename {
-        channel_id: u64,
+        location: ChannelPath,
         pending_name: Option<String>,
     },
 }
@@ -162,6 +360,7 @@ pub struct CollabPanel {
     width: Option<f32>,
     fs: Arc<dyn Fs>,
     has_focus: bool,
+    channel_clipboard: Option<ChannelMoveClipboard>,
     pending_serialization: Task<Option<()>>,
     context_menu: ViewHandle<ContextMenu>,
     filter_editor: ViewHandle<Editor>,
@@ -177,7 +376,8 @@ pub struct CollabPanel {
     list_state: ListState<Self>,
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
-    collapsed_channels: Vec<ChannelId>,
+    collapsed_channels: Vec<ChannelPath>,
+    drag_target_channel: Option<ChannelData>,
     workspace: WeakViewHandle<Workspace>,
     context_menu_on_selected: bool,
 }
@@ -185,7 +385,7 @@ pub struct CollabPanel {
 #[derive(Serialize, Deserialize)]
 struct SerializedCollabPanel {
     width: Option<f32>,
-    collapsed_channels: Option<Vec<ChannelId>>,
+    collapsed_channels: Option<Vec<ChannelPath>>,
 }
 
 #[derive(Debug)]
@@ -208,9 +408,10 @@ enum Section {
 
 #[derive(Clone, Debug)]
 enum ListEntry {
-    Header(Section, usize),
+    Header(Section),
     CallParticipant {
         user: Arc<User>,
+        peer_id: Option<PeerId>,
         is_pending: bool,
     },
     ParticipantProject {
@@ -229,6 +430,7 @@ enum ListEntry {
     Channel {
         channel: Arc<Channel>,
         depth: usize,
+        path: ChannelPath,
     },
     ChannelNotes {
         channel_id: ChannelId,
@@ -274,7 +476,13 @@ impl CollabPanel {
                         this.selection = this
                             .entries
                             .iter()
-                            .position(|entry| !matches!(entry, ListEntry::Header(_, _)));
+                            .position(|entry| !matches!(entry, ListEntry::Header(_)));
+                    }
+                } else if let editor::Event::Blurred = event {
+                    let query = this.filter_editor.read(cx).text(cx);
+                    if query.is_empty() {
+                        this.selection.take();
+                        this.update_entries(true, cx);
                     }
                 }
             })
@@ -310,25 +518,23 @@ impl CollabPanel {
                     let current_project_id = this.project.read(cx).remote_id();
 
                     match &this.entries[ix] {
-                        ListEntry::Header(section, depth) => {
+                        ListEntry::Header(section) => {
                             let is_collapsed = this.collapsed_sections.contains(section);
-                            this.render_header(
-                                *section,
-                                &theme,
-                                *depth,
-                                is_selected,
-                                is_collapsed,
-                                cx,
-                            )
-                        }
-                        ListEntry::CallParticipant { user, is_pending } => {
-                            Self::render_call_participant(
-                                user,
-                                *is_pending,
-                                is_selected,
-                                &theme.collab_panel,
-                            )
+                            this.render_header(*section, &theme, is_selected, is_collapsed, cx)
                         }
+                        ListEntry::CallParticipant {
+                            user,
+                            peer_id,
+                            is_pending,
+                        } => Self::render_call_participant(
+                            user,
+                            *peer_id,
+                            this.user_store.clone(),
+                            *is_pending,
+                            is_selected,
+                            &theme,
+                            cx,
+                        ),
                         ListEntry::ParticipantProject {
                             project_id,
                             worktree_root_names,
@@ -341,7 +547,7 @@ impl CollabPanel {
                             Some(*project_id) == current_project_id,
                             *is_last,
                             is_selected,
-                            &theme.collab_panel,
+                            &theme,
                             cx,
                         ),
                         ListEntry::ParticipantScreen { peer_id, is_last } => {
@@ -353,12 +559,18 @@ impl CollabPanel {
                                 cx,
                             )
                         }
-                        ListEntry::Channel { channel, depth } => {
+                        ListEntry::Channel {
+                            channel,
+                            depth,
+                            path,
+                        } => {
                             let channel_row = this.render_channel(
                                 &*channel,
                                 *depth,
-                                &theme.collab_panel,
+                                path.to_owned(),
+                                &theme,
                                 is_selected,
+                                ix,
                                 cx,
                             );
 
@@ -380,6 +592,7 @@ impl CollabPanel {
                             *channel_id,
                             &theme.collab_panel,
                             is_selected,
+                            ix,
                             cx,
                         ),
                         ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
@@ -409,7 +622,7 @@ impl CollabPanel {
                             contact,
                             *calling,
                             &this.project,
-                            &theme.collab_panel,
+                            &theme,
                             is_selected,
                             cx,
                         ),
@@ -425,6 +638,7 @@ impl CollabPanel {
             let mut this = Self {
                 width: None,
                 has_focus: false,
+                channel_clipboard: None,
                 fs: workspace.app_state().fs.clone(),
                 pending_serialization: Task::ready(None),
                 context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
@@ -443,6 +657,7 @@ impl CollabPanel {
                 workspace: workspace.weak_handle(),
                 client: workspace.app_state().client.clone(),
                 context_menu_on_selected: true,
+                drag_target_channel: None,
                 list_state,
             };
 
@@ -452,7 +667,7 @@ impl CollabPanel {
             let mut old_dock_position = this.position(cx);
             this.subscriptions
                 .push(
-                    cx.observe_global::<SettingsStore, _>(move |this: &mut CollabPanel, cx| {
+                    cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
                         let new_dock_position = this.position(cx);
                         if new_dock_position != old_dock_position {
                             old_dock_position = new_dock_position;
@@ -512,7 +727,13 @@ impl CollabPanel {
                 .log_err()
                 .flatten()
             {
-                Some(serde_json::from_str::<SerializedCollabPanel>(&panel)?)
+                match serde_json::from_str::<SerializedCollabPanel>(&panel) {
+                    Ok(panel) => Some(panel),
+                    Err(err) => {
+                        log::error!("Failed to deserialize collaboration panel: {}", err);
+                        None
+                    }
+                }
             } else {
                 None
             };
@@ -561,9 +782,16 @@ impl CollabPanel {
 
         let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
         let old_entries = mem::take(&mut self.entries);
+        let mut scroll_to_top = false;
 
         if let Some(room) = ActiveCall::global(cx).read(cx).room() {
-            self.entries.push(ListEntry::Header(Section::ActiveCall, 0));
+            self.entries.push(ListEntry::Header(Section::ActiveCall));
+            if !old_entries
+                .iter()
+                .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
+            {
+                scroll_to_top = true;
+            }
 
             if !self.collapsed_sections.contains(&Section::ActiveCall) {
                 let room = room.read(cx);
@@ -592,6 +820,7 @@ impl CollabPanel {
                         let user_id = user.id;
                         self.entries.push(ListEntry::CallParticipant {
                             user,
+                            peer_id: None,
                             is_pending: false,
                         });
                         let mut projects = room.local_participant().projects.iter().peekable();
@@ -629,6 +858,7 @@ impl CollabPanel {
                     let participant = &room.remote_participants()[&user_id];
                     self.entries.push(ListEntry::CallParticipant {
                         user: participant.user.clone(),
+                        peer_id: Some(participant.peer_id),
                         is_pending: false,
                     });
                     let mut projects = participant.projects.iter().peekable();
@@ -670,6 +900,7 @@ impl CollabPanel {
                 self.entries
                     .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
                         user: room.pending_participants()[mat.candidate_id].clone(),
+                        peer_id: None,
                         is_pending: true,
                     }));
             }
@@ -678,21 +909,18 @@ impl CollabPanel {
         let mut request_entries = Vec::new();
 
         if cx.has_flag::<ChannelsAlpha>() {
-            self.entries.push(ListEntry::Header(Section::Channels, 0));
+            self.entries.push(ListEntry::Header(Section::Channels));
 
             if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
                 self.match_candidates.clear();
                 self.match_candidates
-                    .extend(
-                        channel_store
-                            .channels()
-                            .enumerate()
-                            .map(|(ix, (_, channel))| StringMatchCandidate {
-                                id: ix,
-                                string: channel.name.clone(),
-                                char_bag: channel.name.chars().collect(),
-                            }),
-                    );
+                    .extend(channel_store.channel_dag_entries().enumerate().map(
+                        |(ix, (_, channel))| StringMatchCandidate {
+                            id: ix,
+                            string: channel.name.clone(),
+                            char_bag: channel.name.chars().collect(),
+                        },
+                    ));
                 let matches = executor.block(match_strings(
                     &self.match_candidates,
                     &query,
@@ -702,28 +930,24 @@ impl CollabPanel {
                     executor.clone(),
                 ));
                 if let Some(state) = &self.channel_editing_state {
-                    if matches!(
-                        state,
-                        ChannelEditingState::Create {
-                            parent_id: None,
-                            ..
-                        }
-                    ) {
+                    if matches!(state, ChannelEditingState::Create { location: None, .. }) {
                         self.entries.push(ListEntry::ChannelEditor { depth: 0 });
                     }
                 }
                 let mut collapse_depth = None;
                 for mat in matches {
-                    let (depth, channel) =
-                        channel_store.channel_at_index(mat.candidate_id).unwrap();
+                    let (channel, path) = channel_store
+                        .channel_dag_entry_at(mat.candidate_id)
+                        .unwrap();
+                    let depth = path.len() - 1;
 
-                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+                    if collapse_depth.is_none() && self.is_channel_collapsed(path) {
                         collapse_depth = Some(depth);
                     } else if let Some(collapsed_depth) = collapse_depth {
                         if depth > collapsed_depth {
                             continue;
                         }
-                        if self.is_channel_collapsed(channel.id) {
+                        if self.is_channel_collapsed(path) {
                             collapse_depth = Some(depth);
                         } else {
                             collapse_depth = None;
@@ -731,25 +955,29 @@ impl CollabPanel {
                     }
 
                     match &self.channel_editing_state {
-                        Some(ChannelEditingState::Create { parent_id, .. })
-                            if *parent_id == Some(channel.id) =>
-                        {
+                        Some(ChannelEditingState::Create {
+                            location: parent_path,
+                            ..
+                        }) if parent_path.as_ref() == Some(path) => {
                             self.entries.push(ListEntry::Channel {
                                 channel: channel.clone(),
                                 depth,
+                                path: path.clone(),
                             });
                             self.entries
                                 .push(ListEntry::ChannelEditor { depth: depth + 1 });
                         }
-                        Some(ChannelEditingState::Rename { channel_id, .. })
-                            if *channel_id == channel.id =>
-                        {
+                        Some(ChannelEditingState::Rename {
+                            location: parent_path,
+                            ..
+                        }) if parent_path == path => {
                             self.entries.push(ListEntry::ChannelEditor { depth });
                         }
                         _ => {
                             self.entries.push(ListEntry::Channel {
                                 channel: channel.clone(),
                                 depth,
+                                path: path.clone(),
                             });
                         }
                     }
@@ -781,7 +1009,7 @@ impl CollabPanel {
 
                 if !request_entries.is_empty() {
                     self.entries
-                        .push(ListEntry::Header(Section::ChannelInvites, 1));
+                        .push(ListEntry::Header(Section::ChannelInvites));
                     if !self.collapsed_sections.contains(&Section::ChannelInvites) {
                         self.entries.append(&mut request_entries);
                     }
@@ -789,7 +1017,7 @@ impl CollabPanel {
             }
         }
 
-        self.entries.push(ListEntry::Header(Section::Contacts, 0));
+        self.entries.push(ListEntry::Header(Section::Contacts));
 
         request_entries.clear();
         let incoming = user_store.incoming_contact_requests();
@@ -852,7 +1080,7 @@ impl CollabPanel {
 
         if !request_entries.is_empty() {
             self.entries
-                .push(ListEntry::Header(Section::ContactRequests, 1));
+                .push(ListEntry::Header(Section::ContactRequests));
             if !self.collapsed_sections.contains(&Section::ContactRequests) {
                 self.entries.append(&mut request_entries);
             }
@@ -891,7 +1119,7 @@ impl CollabPanel {
                 (offline_contacts, Section::Offline),
             ] {
                 if !matches.is_empty() {
-                    self.entries.push(ListEntry::Header(section, 1));
+                    self.entries.push(ListEntry::Header(section));
                     if !self.collapsed_sections.contains(&section) {
                         let active_call = &ActiveCall::global(cx).read(cx);
                         for mat in matches {
@@ -931,44 +1159,49 @@ impl CollabPanel {
         }
 
         let old_scroll_top = self.list_state.logical_scroll_top();
+
         self.list_state.reset(self.entries.len());
 
-        // Attempt to maintain the same scroll position.
-        if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
-            let new_scroll_top = self
-                .entries
-                .iter()
-                .position(|entry| entry == old_top_entry)
-                .map(|item_ix| ListOffset {
-                    item_ix,
-                    offset_in_item: old_scroll_top.offset_in_item,
-                })
-                .or_else(|| {
-                    let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
-                    let item_ix = self
-                        .entries
-                        .iter()
-                        .position(|entry| entry == entry_after_old_top)?;
-                    Some(ListOffset {
+        if scroll_to_top {
+            self.list_state.scroll_to(ListOffset::default());
+        } else {
+            // Attempt to maintain the same scroll position.
+            if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
+                let new_scroll_top = self
+                    .entries
+                    .iter()
+                    .position(|entry| entry == old_top_entry)
+                    .map(|item_ix| ListOffset {
                         item_ix,
-                        offset_in_item: 0.,
+                        offset_in_item: old_scroll_top.offset_in_item,
                     })
-                })
-                .or_else(|| {
-                    let entry_before_old_top =
-                        old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
-                    let item_ix = self
-                        .entries
-                        .iter()
-                        .position(|entry| entry == entry_before_old_top)?;
-                    Some(ListOffset {
-                        item_ix,
-                        offset_in_item: 0.,
+                    .or_else(|| {
+                        let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
+                        let item_ix = self
+                            .entries
+                            .iter()
+                            .position(|entry| entry == entry_after_old_top)?;
+                        Some(ListOffset {
+                            item_ix,
+                            offset_in_item: 0.,
+                        })
                     })
-                });
+                    .or_else(|| {
+                        let entry_before_old_top =
+                            old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
+                        let item_ix = self
+                            .entries
+                            .iter()
+                            .position(|entry| entry == entry_before_old_top)?;
+                        Some(ListOffset {
+                            item_ix,
+                            offset_in_item: 0.,
+                        })
+                    });
 
-            self.list_state
-                .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
+                self.list_state
+                    .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
+            }
         }
 
         cx.notify();
@@ -976,46 +1209,97 @@ impl CollabPanel {
 
     fn render_call_participant(
         user: &User,
+        peer_id: Option<PeerId>,
+        user_store: ModelHandle<UserStore>,
         is_pending: bool,
         is_selected: bool,
-        theme: &theme::CollabPanel,
+        theme: &theme::Theme,
+        cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
-        Flex::row()
-            .with_children(user.avatar.clone().map(|avatar| {
-                Image::from_data(avatar)
-                    .with_style(theme.contact_avatar)
-                    .aligned()
-                    .left()
-            }))
-            .with_child(
-                Label::new(
-                    user.github_login.clone(),
-                    theme.contact_username.text.clone(),
-                )
-                .contained()
-                .with_style(theme.contact_username.container)
-                .aligned()
-                .left()
-                .flex(1., true),
-            )
-            .with_children(if is_pending {
-                Some(
-                    Label::new("Calling", theme.calling_indicator.text.clone())
+        enum CallParticipant {}
+        enum CallParticipantTooltip {}
+
+        let collab_theme = &theme.collab_panel;
+
+        let is_current_user =
+            user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
+
+        let content =
+            MouseEventHandler::new::<CallParticipant, _>(user.id as usize, cx, |mouse_state, _| {
+                let style = if is_current_user {
+                    *collab_theme
+                        .contact_row
+                        .in_state(is_selected)
+                        .style_for(&mut Default::default())
+                } else {
+                    *collab_theme
+                        .contact_row
+                        .in_state(is_selected)
+                        .style_for(mouse_state)
+                };
+
+                Flex::row()
+                    .with_children(user.avatar.clone().map(|avatar| {
+                        Image::from_data(avatar)
+                            .with_style(collab_theme.contact_avatar)
+                            .aligned()
+                            .left()
+                    }))
+                    .with_child(
+                        Label::new(
+                            user.github_login.clone(),
+                            collab_theme.contact_username.text.clone(),
+                        )
                         .contained()
-                        .with_style(theme.calling_indicator.container)
-                        .aligned(),
-                )
-            } else {
-                None
+                        .with_style(collab_theme.contact_username.container)
+                        .aligned()
+                        .left()
+                        .flex(1., true),
+                    )
+                    .with_children(if is_pending {
+                        Some(
+                            Label::new("Calling", collab_theme.calling_indicator.text.clone())
+                                .contained()
+                                .with_style(collab_theme.calling_indicator.container)
+                                .aligned(),
+                        )
+                    } else if is_current_user {
+                        Some(
+                            Label::new("You", collab_theme.calling_indicator.text.clone())
+                                .contained()
+                                .with_style(collab_theme.calling_indicator.container)
+                                .aligned(),
+                        )
+                    } else {
+                        None
+                    })
+                    .constrained()
+                    .with_height(collab_theme.row_height)
+                    .contained()
+                    .with_style(style)
+            });
+
+        if is_current_user || is_pending || peer_id.is_none() {
+            return content.into_any();
+        }
+
+        let tooltip = format!("Follow {}", user.github_login);
+
+        content
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                if let Some(workspace) = this.workspace.upgrade(cx) {
+                    workspace
+                        .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
+                        .map(|task| task.detach_and_log_err(cx));
+                }
             })
-            .constrained()
-            .with_height(theme.row_height)
-            .contained()
-            .with_style(
-                *theme
-                    .contact_row
-                    .in_state(is_selected)
-                    .style_for(&mut Default::default()),
+            .with_cursor_style(CursorStyle::PointingHand)
+            .with_tooltip::<CallParticipantTooltip>(
+                user.id as usize,
+                tooltip,
+                Some(Box::new(FollowNextCollaborator)),
+                theme.tooltip.clone(),
+                cx,
             )
             .into_any()
     }
@@ -1027,74 +1311,91 @@ impl CollabPanel {
         is_current: bool,
         is_last: bool,
         is_selected: bool,
-        theme: &theme::CollabPanel,
+        theme: &theme::Theme,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
         enum JoinProject {}
+        enum JoinProjectTooltip {}
 
-        let host_avatar_width = theme
+        let collab_theme = &theme.collab_panel;
+        let host_avatar_width = collab_theme
             .contact_avatar
             .width
-            .or(theme.contact_avatar.height)
+            .or(collab_theme.contact_avatar.height)
             .unwrap_or(0.);
-        let tree_branch = theme.tree_branch;
+        let tree_branch = collab_theme.tree_branch;
         let project_name = if worktree_root_names.is_empty() {
             "untitled".to_string()
         } else {
             worktree_root_names.join(", ")
         };
 
-        MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
-            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);
+        let content =
+            MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
+                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+                let row = if is_current {
+                    collab_theme
+                        .project_row
+                        .in_state(true)
+                        .style_for(&mut Default::default())
+                } else {
+                    collab_theme
+                        .project_row
+                        .in_state(is_selected)
+                        .style_for(mouse_state)
+                };
 
-            Flex::row()
-                .with_child(render_tree_branch(
-                    tree_branch,
-                    &row.name.text,
-                    is_last,
-                    vec2f(host_avatar_width, theme.row_height),
-                    cx.font_cache(),
-                ))
-                .with_child(
-                    Svg::new("icons/file_icons/folder.svg")
-                        .with_color(theme.channel_hash.color)
-                        .constrained()
-                        .with_width(theme.channel_hash.width)
-                        .aligned()
-                        .left(),
-                )
-                .with_child(
-                    Label::new(project_name, row.name.text.clone())
-                        .aligned()
-                        .left()
-                        .contained()
-                        .with_style(row.name.container)
-                        .flex(1., false),
-                )
-                .constrained()
-                .with_height(theme.row_height)
-                .contained()
-                .with_style(row.container)
-        })
-        .with_cursor_style(if !is_current {
-            CursorStyle::PointingHand
-        } else {
-            CursorStyle::Arrow
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            if !is_current {
+                Flex::row()
+                    .with_child(render_tree_branch(
+                        tree_branch,
+                        &row.name.text,
+                        is_last,
+                        vec2f(host_avatar_width, collab_theme.row_height),
+                        cx.font_cache(),
+                    ))
+                    .with_child(
+                        Svg::new("icons/file_icons/folder.svg")
+                            .with_color(collab_theme.channel_hash.color)
+                            .constrained()
+                            .with_width(collab_theme.channel_hash.width)
+                            .aligned()
+                            .left(),
+                    )
+                    .with_child(
+                        Label::new(project_name.clone(), row.name.text.clone())
+                            .aligned()
+                            .left()
+                            .contained()
+                            .with_style(row.name.container)
+                            .flex(1., false),
+                    )
+                    .constrained()
+                    .with_height(collab_theme.row_height)
+                    .contained()
+                    .with_style(row.container)
+            });
+
+        if is_current {
+            return content.into_any();
+        }
+
+        content
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, move |_, this, cx| {
                 if let Some(workspace) = this.workspace.upgrade(cx) {
                     let app_state = workspace.read(cx).app_state().clone();
                     workspace::join_remote_project(project_id, host_user_id, app_state, cx)
                         .detach_and_log_err(cx);
                 }
-            }
-        })
-        .into_any()
+            })
+            .with_tooltip::<JoinProjectTooltip>(
+                project_id as usize,
+                format!("Open {}", project_name),
+                None,
+                theme.tooltip.clone(),
+                cx,
+            )
+            .into_any()
     }
 
     fn render_participant_screen(

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

@@ -209,9 +209,9 @@ impl PickerDelegate for ContactFinderDelegate {
 
         let icon_path = match request_status {
             ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
-                Some("icons/check_8.svg")
+                Some("icons/check.svg")
             }
-            ContactRequestStatus::RequestSent => Some("icons/x_mark_8.svg"),
+            ContactRequestStatus::RequestSent => Some("icons/x.svg"),
             ContactRequestStatus::RequestAccepted => None,
         };
         let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {

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

@@ -215,7 +215,13 @@ impl CollabTitlebarItem {
         let git_style = theme.titlebar.git_menu_button.clone();
         let item_spacing = theme.titlebar.item_spacing;
 
-        let mut ret = Flex::row().with_child(
+        let mut ret = Flex::row();
+
+        if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
+            ret = ret.with_child(project_host)
+        }
+
+        ret = ret.with_child(
             Stack::new()
                 .with_child(
                     MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
@@ -283,6 +289,71 @@ impl CollabTitlebarItem {
         ret.into_any()
     }
 
+    fn collect_project_host(
+        &self,
+        theme: Arc<Theme>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<AnyElement<Self>> {
+        if ActiveCall::global(cx).read(cx).room().is_none() {
+            return None;
+        }
+        let project = self.project.read(cx);
+        let user_store = self.user_store.read(cx);
+
+        if project.is_local() {
+            return None;
+        }
+
+        let Some(host) = project.host() else {
+            return None;
+        };
+        let (Some(host_user), Some(participant_index)) = (
+            user_store.get_cached_user(host.user_id),
+            user_store.participant_indices().get(&host.user_id),
+        ) else {
+            return None;
+        };
+
+        enum ProjectHost {}
+        enum ProjectHostTooltip {}
+
+        let host_style = theme.titlebar.project_host.clone();
+        let selection_style = theme
+            .editor
+            .selection_style_for_room_participant(participant_index.0);
+        let peer_id = host.peer_id.clone();
+
+        Some(
+            MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
+                let mut host_style = host_style.style_for(mouse_state).clone();
+                host_style.text.color = selection_style.cursor;
+                Label::new(host_user.github_login.clone(), host_style.text)
+                    .contained()
+                    .with_style(host_style.container)
+                    .aligned()
+                    .left()
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                if let Some(workspace) = this.workspace.upgrade(cx) {
+                    if let Some(task) =
+                        workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+                    {
+                        task.detach_and_log_err(cx);
+                    }
+                }
+            })
+            .with_tooltip::<ProjectHostTooltip>(
+                0,
+                host_user.github_login.clone() + " is sharing this project. Click to follow.",
+                None,
+                theme.tooltip.clone(),
+                cx,
+            )
+            .into_any_named("project-host"),
+        )
+    }
+
     fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         let project = if active {
             Some(self.project.clone())
@@ -483,10 +554,10 @@ impl CollabTitlebarItem {
         let icon;
         let tooltip;
         if room.read(cx).is_screen_sharing() {
-            icon = "icons/radix/desktop.svg";
+            icon = "icons/desktop.svg";
             tooltip = "Stop Sharing Screen"
         } else {
-            icon = "icons/radix/desktop.svg";
+            icon = "icons/desktop.svg";
             tooltip = "Share Screen";
         }
 
@@ -533,10 +604,10 @@ impl CollabTitlebarItem {
         let tooltip;
         let is_muted = room.read(cx).is_muted(cx);
         if is_muted {
-            icon = "icons/radix/mic-mute.svg";
+            icon = "icons/mic-mute.svg";
             tooltip = "Unmute microphone";
         } else {
-            icon = "icons/radix/mic.svg";
+            icon = "icons/mic.svg";
             tooltip = "Mute microphone";
         }
 
@@ -586,10 +657,10 @@ impl CollabTitlebarItem {
         let tooltip;
         let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
         if is_deafened {
-            icon = "icons/radix/speaker-off.svg";
+            icon = "icons/speaker-off.svg";
             tooltip = "Unmute speakers";
         } else {
-            icon = "icons/radix/speaker-loud.svg";
+            icon = "icons/speaker-loud.svg";
             tooltip = "Mute speakers";
         }
 
@@ -625,7 +696,7 @@ impl CollabTitlebarItem {
         .into_any()
     }
     fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let icon = "icons/radix/exit.svg";
+        let icon = "icons/exit.svg";
         let tooltip = "Leave call";
 
         let titlebar = &theme.titlebar;
@@ -748,7 +819,7 @@ impl CollabTitlebarItem {
 
                     dropdown
                         .with_child(
-                            Svg::new("icons/caret_down_8.svg")
+                            Svg::new("icons/caret_down.svg")
                                 .with_color(user_menu_button_style.icon.color)
                                 .constrained()
                                 .with_width(user_menu_button_style.icon.width)
@@ -877,7 +948,7 @@ impl CollabTitlebarItem {
     fn render_face_pile(
         &self,
         user: &User,
-        replica_id: Option<ReplicaId>,
+        _replica_id: Option<ReplicaId>,
         peer_id: PeerId,
         location: Option<ParticipantLocation>,
         muted: bool,
@@ -886,23 +957,20 @@ impl CollabTitlebarItem {
         theme: &Theme,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
+        let user_id = user.id;
         let project_id = workspace.read(cx).project().read(cx).remote_id();
-        let room = ActiveCall::global(cx).read(cx).room();
-        let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
-        let followed_by_self = room
-            .and_then(|room| {
-                Some(
-                    is_being_followed
-                        && room
-                            .read(cx)
-                            .followers_for(peer_id, project_id?)
-                            .iter()
-                            .any(|&follower| {
-                                Some(follower) == workspace.read(cx).client().peer_id()
-                            }),
-                )
-            })
-            .unwrap_or(false);
+        let room = ActiveCall::global(cx).read(cx).room().cloned();
+        let self_peer_id = workspace.read(cx).client().peer_id();
+        let self_following = workspace.read(cx).is_being_followed(peer_id);
+        let self_following_initialized = self_following
+            && room.as_ref().map_or(false, |room| match project_id {
+                None => true,
+                Some(project_id) => room
+                    .read(cx)
+                    .followers_for(peer_id, project_id)
+                    .iter()
+                    .any(|&follower| Some(follower) == self_peer_id),
+            });
 
         let leader_style = theme.titlebar.leader_avatar;
         let follower_style = theme.titlebar.follower_avatar;
@@ -921,147 +989,131 @@ impl CollabTitlebarItem {
             .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;
+        let participant_index = self
+            .user_store
+            .read(cx)
+            .participant_indices()
+            .get(&user_id)
+            .copied();
+        if let Some(participant_index) = participant_index {
+            if self_following_initialized {
+                let selection = theme
+                    .editor
+                    .selection_style_for_room_participant(participant_index.0)
+                    .selection;
                 background_color = Color::blend(selection, background_color);
                 background_color.a = 255;
             }
         }
 
-        let mut content = Stack::new()
-            .with_children(user.avatar.as_ref().map(|avatar| {
-                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(
-                        (|| {
-                            let project_id = project_id?;
-                            let room = room?.read(cx);
-                            let followers = room.followers_for(peer_id, project_id);
-
-                            Some(followers.into_iter().flat_map(|&follower| {
-                                let remote_participant =
-                                    room.remote_participant_for_peer_id(follower);
-
-                                let avatar = remote_participant
-                                    .and_then(|p| p.user.avatar.clone())
-                                    .or_else(|| {
-                                        if follower == workspace.read(cx).client().peer_id()? {
-                                            workspace
-                                                .read(cx)
-                                                .user_store()
-                                                .read(cx)
-                                                .current_user()?
-                                                .avatar
-                                                .clone()
-                                        } else {
-                                            None
-                                        }
-                                    })?;
-
-                                Some(Self::render_face(
-                                    avatar.clone(),
-                                    follower_style,
-                                    background_color,
-                                    None,
-                                ))
-                            }))
-                        })()
-                        .into_iter()
-                        .flatten(),
-                    );
-
-                let mut container = face_pile
-                    .contained()
-                    .with_style(theme.titlebar.leader_selection);
-
-                if let Some(replica_id) = replica_id {
-                    if followed_by_self {
-                        let color = theme.editor.replica_selection_style(replica_id).selection;
-                        container = container.with_background_color(color);
-                    }
-                }
-
-                container
-            }))
-            .with_children((|| {
-                let replica_id = replica_id?;
-                let color = theme.editor.replica_selection_style(replica_id).cursor;
-                Some(
-                    AvatarRibbon::new(color)
-                        .constrained()
-                        .with_width(theme.titlebar.avatar_ribbon.width)
-                        .with_height(theme.titlebar.avatar_ribbon.height)
-                        .aligned()
-                        .bottom(),
-                )
-            })())
-            .into_any();
-
-        if let Some(location) = location {
-            if let Some(replica_id) = replica_id {
-                enum ToggleFollow {}
+        enum TitlebarParticipant {}
 
-                content = MouseEventHandler::new::<ToggleFollow, _>(
-                    replica_id.into(),
-                    cx,
-                    move |_, _| content,
-                )
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, item, cx| {
-                    if let Some(workspace) = item.workspace.upgrade(cx) {
-                        if let Some(task) = workspace
-                            .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
-                        {
-                            task.detach_and_log_err(cx);
+        let content = MouseEventHandler::new::<TitlebarParticipant, _>(
+            peer_id.as_u64() as usize,
+            cx,
+            move |_, cx| {
+                Stack::new()
+                    .with_children(user.avatar.as_ref().map(|avatar| {
+                        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(
+                                (|| {
+                                    let project_id = project_id?;
+                                    let room = room?.read(cx);
+                                    let followers = room.followers_for(peer_id, project_id);
+                                    Some(followers.into_iter().filter_map(|&follower| {
+                                        if Some(follower) == self_peer_id {
+                                            return None;
+                                        }
+                                        let participant =
+                                            room.remote_participant_for_peer_id(follower)?;
+                                        Some(Self::render_face(
+                                            participant.user.avatar.clone()?,
+                                            follower_style,
+                                            background_color,
+                                            None,
+                                        ))
+                                    }))
+                                })()
+                                .into_iter()
+                                .flatten(),
+                            )
+                            .with_children(
+                                self_following_initialized
+                                    .then(|| self.user_store.read(cx).current_user())
+                                    .and_then(|user| {
+                                        Some(Self::render_face(
+                                            user?.avatar.clone()?,
+                                            follower_style,
+                                            background_color,
+                                            None,
+                                        ))
+                                    }),
+                            );
+
+                        let mut container = face_pile
+                            .contained()
+                            .with_style(theme.titlebar.leader_selection);
+
+                        if let Some(participant_index) = participant_index {
+                            if self_following_initialized {
+                                let color = theme
+                                    .editor
+                                    .selection_style_for_room_participant(participant_index.0)
+                                    .selection;
+                                container = container.with_background_color(color);
+                            }
                         }
-                    }
-                })
-                .with_tooltip::<ToggleFollow>(
-                    peer_id.as_u64() as usize,
-                    if is_being_followed {
-                        format!("Unfollow {}", user.github_login)
-                    } else {
-                        format!("Follow {}", user.github_login)
-                    },
-                    Some(Box::new(FollowNextCollaborator)),
-                    theme.tooltip.clone(),
-                    cx,
-                )
-                .into_any();
-            } else if let ParticipantLocation::SharedProject { project_id } = location {
-                enum JoinProject {}
 
-                let user_id = user.id;
-                content = MouseEventHandler::new::<JoinProject, _>(
-                    peer_id.as_u64() as usize,
-                    cx,
-                    move |_, _| content,
-                )
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    if let Some(workspace) = this.workspace.upgrade(cx) {
-                        let app_state = workspace.read(cx).app_state().clone();
-                        workspace::join_remote_project(project_id, user_id, app_state, cx)
-                            .detach_and_log_err(cx);
-                    }
-                })
-                .with_tooltip::<JoinProject>(
-                    peer_id.as_u64() as usize,
-                    format!("Follow {} into external project", user.github_login),
-                    Some(Box::new(FollowNextCollaborator)),
-                    theme.tooltip.clone(),
-                    cx,
-                )
-                .into_any();
-            }
+                        container
+                    }))
+                    .with_children((|| {
+                        let participant_index = participant_index?;
+                        let color = theme
+                            .editor
+                            .selection_style_for_room_participant(participant_index.0)
+                            .cursor;
+                        Some(
+                            AvatarRibbon::new(color)
+                                .constrained()
+                                .with_width(theme.titlebar.avatar_ribbon.width)
+                                .with_height(theme.titlebar.avatar_ribbon.height)
+                                .aligned()
+                                .bottom(),
+                        )
+                    })())
+            },
+        );
+
+        if Some(peer_id) == self_peer_id {
+            return content.into_any();
         }
+
         content
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                let Some(workspace) = this.workspace.upgrade(cx) else {
+                    return;
+                };
+                if let Some(task) =
+                    workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+                {
+                    task.detach_and_log_err(cx);
+                }
+            })
+            .with_tooltip::<TitlebarParticipant>(
+                peer_id.as_u64() as usize,
+                format!("Follow {}", user.github_login),
+                Some(Box::new(FollowNextCollaborator)),
+                theme.tooltip.clone(),
+                cx,
+            )
+            .into_any()
     }
 
     fn location_style(
@@ -1116,7 +1168,7 @@ impl CollabTitlebarItem {
             | client::Status::Reauthenticating { .. }
             | client::Status::Reconnecting { .. }
             | client::Status::ReconnectionError { .. } => Some(
-                Svg::new("icons/cloud_slash_12.svg")
+                Svg::new("icons/disconnected.svg")
                     .with_color(theme.titlebar.offline_icon.color)
                     .constrained()
                     .with_width(theme.titlebar.offline_icon.width)

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

@@ -1,15 +1,16 @@
 pub mod channel_view;
+pub mod chat_panel;
 pub mod collab_panel;
 mod collab_titlebar_item;
 mod contact_notification;
 mod face_pile;
 mod incoming_call_notification;
 mod notifications;
-mod project_shared_notification;
+mod panel_settings;
+pub mod project_shared_notification;
 mod sharing_status_indicator;
 
-use call::{ActiveCall, Room};
-pub use collab_titlebar_item::CollabTitlebarItem;
+use call::{report_call_event_for_room, ActiveCall, Room};
 use gpui::{
     actions,
     geometry::{
@@ -23,15 +24,22 @@ use std::{rc::Rc, sync::Arc};
 use util::ResultExt;
 use workspace::AppState;
 
+pub use collab_titlebar_item::CollabTitlebarItem;
+pub use panel_settings::{ChatPanelSettings, CollaborationPanelSettings};
+
 actions!(
     collab,
     [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
 );
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    settings::register::<CollaborationPanelSettings>(cx);
+    settings::register::<ChatPanelSettings>(cx);
+
     vcs_menu::init(cx);
     collab_titlebar_item::init(cx);
-    collab_panel::init(app_state.client.clone(), cx);
+    collab_panel::init(cx);
+    chat_panel::init(cx);
     incoming_call_notification::init(&app_state, cx);
     project_shared_notification::init(&app_state, cx);
     sharing_status_indicator::init(cx);
@@ -47,18 +55,18 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
         let client = call.client();
         let toggle_screen_sharing = room.update(cx, |room, cx| {
             if room.is_screen_sharing() {
-                ActiveCall::report_call_event_for_room(
+                report_call_event_for_room(
                     "disable screen share",
-                    Some(room.id()),
+                    room.id(),
                     room.channel_id(),
                     &client,
                     cx,
                 );
                 Task::ready(room.unshare_screen(cx))
             } else {
-                ActiveCall::report_call_event_for_room(
+                report_call_event_for_room(
                     "enable screen share",
-                    Some(room.id()),
+                    room.id(),
                     room.channel_id(),
                     &client,
                     cx,
@@ -75,23 +83,13 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
     if let Some(room) = call.room().cloned() {
         let client = call.client();
         room.update(cx, |room, cx| {
-            if room.is_muted(cx) {
-                ActiveCall::report_call_event_for_room(
-                    "enable microphone",
-                    Some(room.id()),
-                    room.channel_id(),
-                    &client,
-                    cx,
-                );
+            let operation = if room.is_muted(cx) {
+                "enable microphone"
             } else {
-                ActiveCall::report_call_event_for_room(
-                    "disable microphone",
-                    Some(room.id()),
-                    room.channel_id(),
-                    &client,
-                    cx,
-                );
-            }
+                "disable microphone"
+            };
+            report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
+
             room.toggle_mute(cx)
         })
         .map(|task| task.detach_and_log_err(cx))

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

@@ -53,7 +53,7 @@ where
                 .with_child(
                     MouseEventHandler::new::<Dismiss, _>(user.id as usize, cx, |state, _| {
                         let style = theme.dismiss_button.style_for(state);
-                        Svg::new("icons/x_mark_8.svg")
+                        Svg::new("icons/x.svg")
                             .with_color(style.color)
                             .constrained()
                             .with_width(style.icon_width)

crates/collab_ui/src/collab_panel/panel_settings.rs β†’ crates/collab_ui/src/panel_settings.rs πŸ”—

@@ -2,32 +2,47 @@ use anyhow;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
 use settings::Setting;
+use workspace::dock::DockPosition;
 
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum CollaborationPanelDockPosition {
-    Left,
-    Right,
+#[derive(Deserialize, Debug)]
+pub struct CollaborationPanelSettings {
+    pub button: bool,
+    pub dock: DockPosition,
+    pub default_width: f32,
 }
 
 #[derive(Deserialize, Debug)]
-pub struct CollaborationPanelSettings {
+pub struct ChatPanelSettings {
     pub button: bool,
-    pub dock: CollaborationPanelDockPosition,
+    pub dock: DockPosition,
     pub default_width: f32,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-pub struct CollaborationPanelSettingsContent {
+pub struct PanelSettingsContent {
     pub button: Option<bool>,
-    pub dock: Option<CollaborationPanelDockPosition>,
+    pub dock: Option<DockPosition>,
     pub default_width: Option<f32>,
 }
 
 impl Setting for CollaborationPanelSettings {
     const KEY: Option<&'static str> = Some("collaboration_panel");
 
-    type FileContent = CollaborationPanelSettingsContent;
+    type FileContent = PanelSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
+impl Setting for ChatPanelSettings {
+    const KEY: Option<&'static str> = Some("chat_panel");
+
+    type FileContent = PanelSettingsContent;
 
     fn load(
         default_value: &Self::FileContent,

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

@@ -40,7 +40,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
                     .push(window);
             }
         }
-        room::Event::RemoteProjectUnshared { project_id } => {
+        room::Event::RemoteProjectUnshared { project_id }
+        | room::Event::RemoteProjectJoined { project_id }
+        | room::Event::RemoteProjectInvitationDiscarded { project_id } => {
             if let Some(windows) = notification_windows.remove(&project_id) {
                 for window in windows {
                     window.remove(cx);
@@ -82,7 +84,6 @@ impl ProjectSharedNotification {
     }
 
     fn join(&mut self, cx: &mut ViewContext<Self>) {
-        cx.remove_window();
         if let Some(app_state) = self.app_state.upgrade() {
             workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
                 .detach_and_log_err(cx);
@@ -90,7 +91,15 @@ impl ProjectSharedNotification {
     }
 
     fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
-        cx.remove_window();
+        if let Some(active_room) =
+            ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
+        {
+            active_room.update(cx, |_, cx| {
+                cx.emit(room::Event::RemoteProjectInvitationDiscarded {
+                    project_id: self.project_id,
+                });
+            });
+        }
     }
 
     fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {

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

@@ -48,7 +48,7 @@ impl View for SharingStatusIndicator {
         };
 
         MouseEventHandler::new::<Self, _>(0, cx, |_, _| {
-            Svg::new("icons/disable_screen_sharing_12.svg")
+            Svg::new("icons/desktop.svg")
                 .with_color(color)
                 .constrained()
                 .with_width(18.)

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

@@ -18,6 +18,15 @@ actions!(command_palette, [Toggle]);
 
 pub type CommandPalette = Picker<CommandPaletteDelegate>;
 
+pub type CommandPaletteInterceptor =
+    Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>;
+
+pub struct CommandInterceptResult {
+    pub action: Box<dyn Action>,
+    pub string: String,
+    pub positions: Vec<usize>,
+}
+
 pub struct CommandPaletteDelegate {
     actions: Vec<Command>,
     matches: Vec<StringMatch>,
@@ -117,7 +126,7 @@ impl PickerDelegate for CommandPaletteDelegate {
                     }
                 })
                 .collect::<Vec<_>>();
-            let actions = cx.read(move |cx| {
+            let mut actions = cx.read(move |cx| {
                 let hit_counts = cx.optional_global::<HitCounts>();
                 actions.sort_by_key(|action| {
                     (
@@ -136,7 +145,7 @@ impl PickerDelegate for CommandPaletteDelegate {
                     char_bag: command.name.chars().collect(),
                 })
                 .collect::<Vec<_>>();
-            let matches = if query.is_empty() {
+            let mut matches = if query.is_empty() {
                 candidates
                     .into_iter()
                     .enumerate()
@@ -158,6 +167,40 @@ impl PickerDelegate for CommandPaletteDelegate {
                 )
                 .await
             };
+            let intercept_result = cx.read(|cx| {
+                if cx.has_global::<CommandPaletteInterceptor>() {
+                    cx.global::<CommandPaletteInterceptor>()(&query, cx)
+                } else {
+                    None
+                }
+            });
+            if let Some(CommandInterceptResult {
+                action,
+                string,
+                positions,
+            }) = intercept_result
+            {
+                if let Some(idx) = matches
+                    .iter()
+                    .position(|m| actions[m.candidate_id].action.id() == action.id())
+                {
+                    matches.remove(idx);
+                }
+                actions.push(Command {
+                    name: string.clone(),
+                    action,
+                    keystrokes: vec![],
+                });
+                matches.insert(
+                    0,
+                    StringMatch {
+                        candidate_id: actions.len() - 1,
+                        string,
+                        positions,
+                        score: 0.0,
+                    },
+                )
+            }
             picker
                 .update(&mut cx, |picker, _| {
                     let delegate = picker.delegate_mut();
@@ -222,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate {
                     .with_children(
                         [
                             (keystroke.ctrl, "^"),
-                            (keystroke.alt, "βŽ‡"),
+                            (keystroke.alt, "βŒ₯"),
                             (keystroke.cmd, "⌘"),
                             (keystroke.shift, "⇧"),
                         ]

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

@@ -11,13 +11,12 @@ use gpui::{
 };
 use language::{
     language_settings::{all_language_settings, language_settings},
-    point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
-    ToPointUtf16,
+    point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language,
+    LanguageServerName, PointUtf16, ToPointUtf16,
 };
-use log::{debug, error};
 use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
 use node_runtime::NodeRuntime;
-use request::{LogMessage, StatusNotification};
+use request::StatusNotification;
 use settings::SettingsStore;
 use smol::{fs, io::BufReader, stream::StreamExt};
 use std::{
@@ -41,10 +40,15 @@ actions!(
     [Suggest, NextSuggestion, PreviousSuggestion, Reinstall]
 );
 
-pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<dyn NodeRuntime>, cx: &mut AppContext) {
+pub fn init(
+    new_server_id: LanguageServerId,
+    http: Arc<dyn HttpClient>,
+    node_runtime: Arc<dyn NodeRuntime>,
+    cx: &mut AppContext,
+) {
     let copilot = cx.add_model({
         let node_runtime = node_runtime.clone();
-        move |cx| Copilot::start(http, node_runtime, cx)
+        move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
     });
     cx.set_global(copilot.clone());
 
@@ -125,6 +129,7 @@ impl CopilotServer {
 }
 
 struct RunningCopilotServer {
+    name: LanguageServerName,
     lsp: Arc<LanguageServer>,
     sign_in_status: SignInStatus,
     registered_buffers: HashMap<usize, RegisteredBuffer>,
@@ -268,10 +273,15 @@ pub struct Copilot {
     node_runtime: Arc<dyn NodeRuntime>,
     server: CopilotServer,
     buffers: HashSet<WeakModelHandle<Buffer>>,
+    server_id: LanguageServerId,
+}
+
+pub enum Event {
+    CopilotLanguageServerStarted,
 }
 
 impl Entity for Copilot {
-    type Event = ();
+    type Event = Event;
 
     fn app_will_quit(
         &mut self,
@@ -298,11 +308,13 @@ impl Copilot {
     }
 
     fn start(
+        new_server_id: LanguageServerId,
         http: Arc<dyn HttpClient>,
         node_runtime: Arc<dyn NodeRuntime>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let mut this = Self {
+            server_id: new_server_id,
             http,
             node_runtime,
             server: CopilotServer::Disabled,
@@ -315,13 +327,16 @@ impl Copilot {
     }
 
     fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
+        let server_id = self.server_id;
         let http = self.http.clone();
         let node_runtime = self.node_runtime.clone();
         if all_language_settings(None, cx).copilot_enabled(None, None) {
             if matches!(self.server, CopilotServer::Disabled) {
                 let start_task = cx
                     .spawn({
-                        move |this, cx| Self::start_language_server(http, node_runtime, this, cx)
+                        move |this, cx| {
+                            Self::start_language_server(server_id, http, node_runtime, this, cx)
+                        }
                     })
                     .shared();
                 self.server = CopilotServer::Starting { task: start_task };
@@ -342,9 +357,11 @@ impl Copilot {
         let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
         let node_runtime = FakeNodeRuntime::new();
         let this = cx.add_model(|_| Self {
+            server_id: LanguageServerId(0),
             http: http.clone(),
             node_runtime,
             server: CopilotServer::Running(RunningCopilotServer {
+                name: LanguageServerName(Arc::from("copilot")),
                 lsp: Arc::new(server),
                 sign_in_status: SignInStatus::Authorized,
                 registered_buffers: Default::default(),
@@ -355,6 +372,7 @@ impl Copilot {
     }
 
     fn start_language_server(
+        new_server_id: LanguageServerId,
         http: Arc<dyn HttpClient>,
         node_runtime: Arc<dyn NodeRuntime>,
         this: ModelHandle<Self>,
@@ -369,27 +387,8 @@ impl Copilot {
                     path: node_path,
                     arguments,
                 };
-                let server = LanguageServer::new(
-                    LanguageServerId(0),
-                    binary,
-                    Path::new("/"),
-                    None,
-                    cx.clone(),
-                )?;
-
-                server
-                    .on_notification::<LogMessage, _>(|params, _cx| {
-                        match params.level {
-                            // Copilot is pretty aggressive about logging
-                            0 => debug!("copilot: {}", params.message),
-                            1 => debug!("copilot: {}", params.message),
-                            _ => error!("copilot: {}", params.message),
-                        }
-
-                        debug!("copilot metadata: {}", params.metadata_str);
-                        debug!("copilot extra: {:?}", params.extra);
-                    })
-                    .detach();
+                let server =
+                    LanguageServer::new(new_server_id, binary, Path::new("/"), None, cx.clone())?;
 
                 server
                     .on_notification::<StatusNotification, _>(
@@ -427,10 +426,12 @@ impl Copilot {
                 match server {
                     Ok((server, status)) => {
                         this.server = CopilotServer::Running(RunningCopilotServer {
+                            name: LanguageServerName(Arc::from("copilot")),
                             lsp: server,
                             sign_in_status: SignInStatus::SignedOut,
                             registered_buffers: Default::default(),
                         });
+                        cx.emit(Event::CopilotLanguageServerStarted);
                         this.update_sign_in_status(status, cx);
                     }
                     Err(error) => {
@@ -547,9 +548,10 @@ impl Copilot {
             .spawn({
                 let http = self.http.clone();
                 let node_runtime = self.node_runtime.clone();
+                let server_id = self.server_id;
                 move |this, cx| async move {
                     clear_copilot_dir().await;
-                    Self::start_language_server(http, node_runtime, this, cx).await
+                    Self::start_language_server(server_id, http, node_runtime, this, cx).await
                 }
             })
             .shared();
@@ -563,6 +565,14 @@ impl Copilot {
         cx.foreground().spawn(start_task)
     }
 
+    pub fn language_server(&self) -> Option<(&LanguageServerName, &Arc<LanguageServer>)> {
+        if let CopilotServer::Running(server) = &self.server {
+            Some((&server.name, &server.lsp))
+        } else {
+            None
+        }
+    }
+
     pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
         let weak_buffer = buffer.downgrade();
         self.buffers.insert(weak_buffer.clone());

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

@@ -78,15 +78,15 @@ impl View for CopilotButton {
                             .with_child(
                                 Svg::new({
                                     match status {
-                                        Status::Error(_) => "icons/copilot_error_16.svg",
+                                        Status::Error(_) => "icons/copilot_error.svg",
                                         Status::Authorized => {
                                             if enabled {
-                                                "icons/copilot_16.svg"
+                                                "icons/copilot.svg"
                                             } else {
-                                                "icons/copilot_disabled_16.svg"
+                                                "icons/copilot_disabled.svg"
                                             }
                                         }
-                                        _ => "icons/copilot_init_16.svg",
+                                        _ => "icons/copilot_init.svg",
                                     }
                                 })
                                 .with_color(style.icon_color)

crates/diagnostics/Cargo.toml πŸ”—

@@ -21,6 +21,9 @@ util = { path = "../util" }
 workspace = { path = "../workspace" }
 
 anyhow.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
 smallvec.workspace = true
 postage.workspace = true
 

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

@@ -1,4 +1,6 @@
 pub mod items;
+mod project_diagnostics_settings;
+mod toolbar_controls;
 
 use anyhow::Result;
 use collections::{BTreeSet, HashSet};
@@ -19,6 +21,7 @@ use language::{
 };
 use lsp::LanguageServerId;
 use project::{DiagnosticSummary, Project, ProjectPath};
+use project_diagnostics_settings::ProjectDiagnosticsSettings;
 use serde_json::json;
 use smallvec::SmallVec;
 use std::{
@@ -30,18 +33,21 @@ use std::{
     sync::Arc,
 };
 use theme::ThemeSettings;
+pub use toolbar_controls::ToolbarControls;
 use util::TryFutureExt;
 use workspace::{
     item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
     ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
 };
 
-actions!(diagnostics, [Deploy]);
+actions!(diagnostics, [Deploy, ToggleWarnings]);
 
 const CONTEXT_LINE_COUNT: u32 = 1;
 
 pub fn init(cx: &mut AppContext) {
+    settings::register::<ProjectDiagnosticsSettings>(cx);
     cx.add_action(ProjectDiagnosticsEditor::deploy);
+    cx.add_action(ProjectDiagnosticsEditor::toggle_warnings);
     items::init(cx);
 }
 
@@ -55,6 +61,7 @@ struct ProjectDiagnosticsEditor {
     excerpts: ModelHandle<MultiBuffer>,
     path_states: Vec<PathState>,
     paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
+    include_warnings: bool,
 }
 
 struct PathState {
@@ -187,6 +194,7 @@ impl ProjectDiagnosticsEditor {
             editor,
             path_states: Default::default(),
             paths_to_update,
+            include_warnings: settings::get::<ProjectDiagnosticsSettings>(cx).include_warnings,
         };
         this.update_excerpts(None, cx);
         this
@@ -204,6 +212,18 @@ impl ProjectDiagnosticsEditor {
         }
     }
 
+    fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
+        self.include_warnings = !self.include_warnings;
+        self.paths_to_update = self
+            .project
+            .read(cx)
+            .diagnostic_summaries(cx)
+            .map(|(path, server_id, _)| (path, server_id))
+            .collect();
+        self.update_excerpts(None, cx);
+        cx.notify();
+    }
+
     fn update_excerpts(
         &mut self,
         language_server_id: Option<LanguageServerId>,
@@ -277,14 +297,18 @@ impl ProjectDiagnosticsEditor {
         let mut blocks_to_add = Vec::new();
         let mut blocks_to_remove = HashSet::default();
         let mut first_excerpt_id = None;
+        let max_severity = if self.include_warnings {
+            DiagnosticSeverity::WARNING
+        } else {
+            DiagnosticSeverity::ERROR
+        };
         let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
             let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
             let mut new_groups = snapshot
                 .diagnostic_groups(language_server_id)
                 .into_iter()
                 .filter(|(_, group)| {
-                    group.entries[group.primary_ix].diagnostic.severity
-                        <= DiagnosticSeverity::WARNING
+                    group.entries[group.primary_ix].diagnostic.severity <= max_severity
                 })
                 .peekable();
             loop {
@@ -686,11 +710,9 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
         let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
         let icon_width = cx.em_width * style.icon_width_factor;
         let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
-            Svg::new("icons/circle_x_mark_12.svg")
-                .with_color(theme.error_diagnostic.message.text.color)
+            Svg::new("icons/error.svg").with_color(theme.error_diagnostic.message.text.color)
         } else {
-            Svg::new("icons/triangle_exclamation_12.svg")
-                .with_color(theme.warning_diagnostic.message.text.color)
+            Svg::new("icons/warning.svg").with_color(theme.warning_diagnostic.message.text.color)
         };
 
         Flex::row()
@@ -748,7 +770,7 @@ pub(crate) fn render_summary<T: 'static>(
         let summary_spacing = theme.tab_summary_spacing;
         Flex::row()
             .with_child(
-                Svg::new("icons/circle_x_mark_12.svg")
+                Svg::new("icons/error.svg")
                     .with_color(text_style.color)
                     .constrained()
                     .with_width(icon_width)
@@ -767,7 +789,7 @@ pub(crate) fn render_summary<T: 'static>(
                 .aligned(),
             )
             .with_child(
-                Svg::new("icons/triangle_exclamation_12.svg")
+                Svg::new("icons/warning.svg")
                     .with_color(text_style.color)
                     .constrained()
                     .with_width(icon_width)
@@ -1503,6 +1525,7 @@ mod tests {
             client::init_settings(cx);
             workspace::init_settings(cx);
             Project::init_settings(cx);
+            crate::init(cx);
         });
     }
 

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

@@ -32,7 +32,8 @@ impl DiagnosticIndicator {
                 this.in_progress_checks.insert(*language_server_id);
                 cx.notify();
             }
-            project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
+            project::Event::DiskBasedDiagnosticsFinished { language_server_id }
+            | project::Event::LanguageServerRemoved(language_server_id) => {
                 this.summary = project.read(cx).diagnostic_summary(cx);
                 this.in_progress_checks.remove(language_server_id);
                 cx.notify();

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

@@ -0,0 +1,28 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+#[derive(Deserialize, Debug)]
+pub struct ProjectDiagnosticsSettings {
+    pub include_warnings: bool,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct ProjectDiagnosticsSettingsContent {
+    include_warnings: Option<bool>,
+}
+
+impl settings::Setting for ProjectDiagnosticsSettings {
+    const KEY: Option<&'static str> = Some("diagnostics");
+    type FileContent = ProjectDiagnosticsSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _cx: &gpui::AppContext,
+    ) -> anyhow::Result<Self>
+    where
+        Self: Sized,
+    {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

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

@@ -0,0 +1,115 @@
+use crate::{ProjectDiagnosticsEditor, ToggleWarnings};
+use gpui::{
+    elements::*,
+    platform::{CursorStyle, MouseButton},
+    Action, Entity, EventContext, View, ViewContext, WeakViewHandle,
+};
+use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
+
+pub struct ToolbarControls {
+    editor: Option<WeakViewHandle<ProjectDiagnosticsEditor>>,
+}
+
+impl Entity for ToolbarControls {
+    type Event = ();
+}
+
+impl View for ToolbarControls {
+    fn ui_name() -> &'static str {
+        "ToolbarControls"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let include_warnings = self
+            .editor
+            .as_ref()
+            .and_then(|editor| editor.upgrade(cx))
+            .map(|editor| editor.read(cx).include_warnings)
+            .unwrap_or(false);
+        let tooltip = if include_warnings {
+            "Exclude Warnings".into()
+        } else {
+            "Include Warnings".into()
+        };
+        Flex::row()
+            .with_child(render_toggle_button(
+                0,
+                "icons/warning.svg",
+                include_warnings,
+                (tooltip, Some(Box::new(ToggleWarnings))),
+                cx,
+                move |this, cx| {
+                    if let Some(editor) = this.editor.and_then(|editor| editor.upgrade(cx)) {
+                        editor.update(cx, |editor, cx| {
+                            editor.toggle_warnings(&Default::default(), cx)
+                        });
+                    }
+                },
+            ))
+            .into_any()
+    }
+}
+
+impl ToolbarItemView for ToolbarControls {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        _: &mut ViewContext<Self>,
+    ) -> ToolbarItemLocation {
+        if let Some(pane_item) = active_pane_item.as_ref() {
+            if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
+                self.editor = Some(editor.downgrade());
+                ToolbarItemLocation::PrimaryRight { flex: None }
+            } else {
+                ToolbarItemLocation::Hidden
+            }
+        } else {
+            ToolbarItemLocation::Hidden
+        }
+    }
+}
+
+impl ToolbarControls {
+    pub fn new() -> Self {
+        ToolbarControls { editor: None }
+    }
+}
+
+fn render_toggle_button<
+    F: 'static + Fn(&mut ToolbarControls, &mut EventContext<ToolbarControls>),
+>(
+    index: usize,
+    icon: &'static str,
+    toggled: bool,
+    tooltip: (String, Option<Box<dyn Action>>),
+    cx: &mut ViewContext<ToolbarControls>,
+    on_click: F,
+) -> AnyElement<ToolbarControls> {
+    enum Button {}
+
+    let theme = theme::current(cx);
+    let (tooltip_text, action) = tooltip;
+
+    MouseEventHandler::new::<Button, _>(index, cx, |mouse_state, _| {
+        let style = theme
+            .workspace
+            .toolbar
+            .toggleable_tool
+            .in_state(toggled)
+            .style_for(mouse_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 |_, view, cx| on_click(view, cx))
+    .with_tooltip::<Button>(index, tooltip_text, action, theme.tooltip.clone(), cx)
+    .into_any_named("quick action bar button")
+}

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

@@ -4,7 +4,7 @@ use collections::HashSet;
 use gpui::{
     elements::{Empty, MouseEventHandler, Overlay},
     geometry::{rect::RectF, vector::Vector2F},
-    platform::{CursorStyle, MouseButton},
+    platform::{CursorStyle, Modifiers, MouseButton},
     scene::{MouseDown, MouseDrag},
     AnyElement, AnyWindowHandle, Element, View, ViewContext, WeakViewHandle, WindowContext,
 };
@@ -21,12 +21,13 @@ enum State<V> {
         region: RectF,
     },
     Dragging {
+        modifiers: Modifiers,
         window: AnyWindowHandle,
         position: Vector2F,
         region_offset: Vector2F,
         region: RectF,
         payload: Rc<dyn Any + 'static>,
-        render: Rc<dyn Fn(Rc<dyn Any>, &mut ViewContext<V>) -> AnyElement<V>>,
+        render: Rc<dyn Fn(&Modifiers, Rc<dyn Any>, &mut ViewContext<V>) -> AnyElement<V>>,
     },
     Canceled,
 }
@@ -49,6 +50,7 @@ impl<V> Clone for State<V> {
                 region,
             },
             State::Dragging {
+                modifiers,
                 window,
                 position,
                 region_offset,
@@ -62,6 +64,7 @@ impl<V> Clone for State<V> {
                 region: region.clone(),
                 payload: payload.clone(),
                 render: render.clone(),
+                modifiers: modifiers.clone(),
             },
             State::Canceled => State::Canceled,
         }
@@ -111,6 +114,27 @@ impl<V: 'static> DragAndDrop<V> {
         })
     }
 
+    pub fn any_currently_dragged(&self, window: AnyWindowHandle) -> bool {
+        self.currently_dragged
+            .as_ref()
+            .map(|state| {
+                if let State::Dragging {
+                    window: window_dragged_from,
+                    ..
+                } = state
+                {
+                    if &window != window_dragged_from {
+                        return false;
+                    }
+
+                    true
+                } else {
+                    false
+                }
+            })
+            .unwrap_or(false)
+    }
+
     pub fn drag_started(event: MouseDown, cx: &mut WindowContext) {
         cx.update_global(|this: &mut Self, _| {
             this.currently_dragged = Some(State::Down {
@@ -124,7 +148,7 @@ impl<V: 'static> DragAndDrop<V> {
         event: MouseDrag,
         payload: Rc<T>,
         cx: &mut WindowContext,
-        render: Rc<impl 'static + Fn(&T, &mut ViewContext<V>) -> AnyElement<V>>,
+        render: Rc<impl 'static + Fn(&Modifiers, &T, &mut ViewContext<V>) -> AnyElement<V>>,
     ) {
         let window = cx.window();
         cx.update_global(|this: &mut Self, cx| {
@@ -141,13 +165,14 @@ impl<V: 'static> DragAndDrop<V> {
                 }) => {
                     if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE {
                         this.currently_dragged = Some(State::Dragging {
+                            modifiers: event.modifiers,
                             window,
                             region_offset,
                             region,
                             position: event.position,
                             payload,
-                            render: Rc::new(move |payload, cx| {
-                                render(payload.downcast_ref::<T>().unwrap(), cx)
+                            render: Rc::new(move |modifiers, payload, cx| {
+                                render(modifiers, payload.downcast_ref::<T>().unwrap(), cx)
                             }),
                         });
                     } else {
@@ -160,16 +185,18 @@ impl<V: 'static> DragAndDrop<V> {
                 Some(&State::Dragging {
                     region_offset,
                     region,
+                    modifiers,
                     ..
                 }) => {
                     this.currently_dragged = Some(State::Dragging {
+                        modifiers,
                         window,
                         region_offset,
                         region,
                         position: event.position,
                         payload,
-                        render: Rc::new(move |payload, cx| {
-                            render(payload.downcast_ref::<T>().unwrap(), cx)
+                        render: Rc::new(move |modifiers, payload, cx| {
+                            render(modifiers, payload.downcast_ref::<T>().unwrap(), cx)
                         }),
                     });
                 }
@@ -178,6 +205,25 @@ impl<V: 'static> DragAndDrop<V> {
         });
     }
 
+    pub fn update_modifiers(new_modifiers: Modifiers, cx: &mut ViewContext<V>) -> bool {
+        let result = cx.update_global(|this: &mut Self, _| match &mut this.currently_dragged {
+            Some(state) => match state {
+                State::Dragging { modifiers, .. } => {
+                    *modifiers = new_modifiers;
+                    true
+                }
+                _ => false,
+            },
+            None => false,
+        });
+
+        if result {
+            cx.notify();
+        }
+
+        result
+    }
+
     pub fn render(cx: &mut ViewContext<V>) -> Option<AnyElement<V>> {
         enum DraggedElementHandler {}
         cx.global::<Self>()
@@ -188,6 +234,7 @@ impl<V: 'static> DragAndDrop<V> {
                     State::Down { .. } => None,
                     State::DeadZone { .. } => None,
                     State::Dragging {
+                        modifiers,
                         window,
                         region_offset,
                         position,
@@ -205,7 +252,7 @@ impl<V: 'static> DragAndDrop<V> {
                                 MouseEventHandler::new::<DraggedElementHandler, _>(
                                     0,
                                     cx,
-                                    |_, cx| render(payload, cx),
+                                    |_, cx| render(&modifiers, payload, cx),
                                 )
                                 .with_cursor_style(CursorStyle::Arrow)
                                 .on_up(MouseButton::Left, |_, _, cx| {
@@ -295,7 +342,7 @@ pub trait Draggable<V> {
     fn as_draggable<D: View, P: Any>(
         self,
         payload: P,
-        render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
+        render: impl 'static + Fn(&Modifiers, &P, &mut ViewContext<D>) -> AnyElement<D>,
     ) -> Self
     where
         Self: Sized;
@@ -305,7 +352,7 @@ impl<V: 'static> Draggable<V> for MouseEventHandler<V> {
     fn as_draggable<D: View, P: Any>(
         self,
         payload: P,
-        render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
+        render: impl 'static + Fn(&Modifiers, &P, &mut ViewContext<D>) -> AnyElement<D>,
     ) -> Self
     where
         Self: Sized,
@@ -317,9 +364,15 @@ impl<V: 'static> Draggable<V> for MouseEventHandler<V> {
             DragAndDrop::<D>::drag_started(e, cx);
         })
         .on_drag(MouseButton::Left, move |e, _, cx| {
-            let payload = payload.clone();
-            let render = render.clone();
-            DragAndDrop::<D>::dragging(e, payload, cx, render)
+            if e.end {
+                cx.update_global::<DragAndDrop<D>, _, _>(|drag_and_drop, cx| {
+                    drag_and_drop.finish_dragging(cx)
+                })
+            } else {
+                let payload = payload.clone();
+                let render = render.clone();
+                DragAndDrop::<D>::dragging(e, payload, cx, render)
+            }
         })
     }
 }

crates/editor/Cargo.toml πŸ”—

@@ -36,6 +36,7 @@ language = { path = "../language" }
 lsp = { path = "../lsp" }
 project = { path = "../project" }
 rpc = { path = "../rpc" }
+rich_text = { path = "../rich_text" }
 settings = { path = "../settings" }
 snippet = { path = "../snippet" }
 sum_tree = { path = "../sum_tree" }
@@ -45,7 +46,7 @@ util = { path = "../util" }
 sqlez = { path = "../sqlez" }
 workspace = { path = "../workspace" }
 
-aho-corasick = "0.7"
+aho-corasick = "1.1"
 anyhow.workspace = true
 convert_case = "0.6.0"
 futures.workspace = true

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

@@ -5,11 +5,11 @@ mod tab_map;
 mod wrap_map;
 
 use crate::{
-    link_go_to_definition::{DocumentRange, InlayRange},
-    Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+    link_go_to_definition::InlayHighlight, Anchor, AnchorRangeExt, InlayId, MultiBuffer,
+    MultiBufferSnapshot, ToOffset, ToPoint,
 };
 pub use block_map::{BlockMap, BlockPoint};
-use collections::{HashMap, HashSet};
+use collections::{BTreeMap, HashMap, HashSet};
 use fold_map::FoldMap;
 use gpui::{
     color::Color,
@@ -43,7 +43,8 @@ pub trait ToDisplayPoint {
     fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
 }
 
-type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<DocumentRange>)>>;
+type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
+type InlayHighlights = BTreeMap<TypeId, HashMap<InlayId, (HighlightStyle, InlayHighlight)>>;
 
 pub struct DisplayMap {
     buffer: ModelHandle<MultiBuffer>,
@@ -54,6 +55,7 @@ pub struct DisplayMap {
     wrap_map: ModelHandle<WrapMap>,
     block_map: BlockMap,
     text_highlights: TextHighlights,
+    inlay_highlights: InlayHighlights,
     pub clip_at_line_ends: bool,
 }
 
@@ -89,6 +91,7 @@ impl DisplayMap {
             wrap_map,
             block_map,
             text_highlights: Default::default(),
+            inlay_highlights: Default::default(),
             clip_at_line_ends: false,
         }
     }
@@ -113,6 +116,7 @@ impl DisplayMap {
             wrap_snapshot,
             block_snapshot,
             text_highlights: self.text_highlights.clone(),
+            inlay_highlights: self.inlay_highlights.clone(),
             clip_at_line_ends: self.clip_at_line_ends,
         }
     }
@@ -215,37 +219,32 @@ impl DisplayMap {
         ranges: Vec<Range<Anchor>>,
         style: HighlightStyle,
     ) {
-        self.text_highlights.insert(
-            Some(type_id),
-            Arc::new((style, ranges.into_iter().map(DocumentRange::Text).collect())),
-        );
+        self.text_highlights
+            .insert(Some(type_id), Arc::new((style, ranges)));
     }
 
     pub fn highlight_inlays(
         &mut self,
         type_id: TypeId,
-        ranges: Vec<InlayRange>,
+        highlights: Vec<InlayHighlight>,
         style: HighlightStyle,
     ) {
-        self.text_highlights.insert(
-            Some(type_id),
-            Arc::new((
-                style,
-                ranges.into_iter().map(DocumentRange::Inlay).collect(),
-            )),
-        );
+        for highlight in highlights {
+            self.inlay_highlights
+                .entry(type_id)
+                .or_default()
+                .insert(highlight.inlay, (style, highlight));
+        }
     }
 
-    pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[DocumentRange])> {
+    pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
         let highlights = self.text_highlights.get(&Some(type_id))?;
         Some((highlights.0, &highlights.1))
     }
-
-    pub fn clear_text_highlights(
-        &mut self,
-        type_id: TypeId,
-    ) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
-        self.text_highlights.remove(&Some(type_id))
+    pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
+        let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some();
+        cleared |= self.inlay_highlights.remove(&type_id).is_none();
+        cleared
     }
 
     pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
@@ -309,6 +308,14 @@ impl DisplayMap {
     }
 }
 
+#[derive(Debug, Default)]
+pub struct Highlights<'a> {
+    pub text_highlights: Option<&'a TextHighlights>,
+    pub inlay_highlights: Option<&'a InlayHighlights>,
+    pub inlay_highlight_style: Option<HighlightStyle>,
+    pub suggestion_highlight_style: Option<HighlightStyle>,
+}
+
 pub struct DisplaySnapshot {
     pub buffer_snapshot: MultiBufferSnapshot,
     pub fold_snapshot: fold_map::FoldSnapshot,
@@ -317,6 +324,7 @@ pub struct DisplaySnapshot {
     wrap_snapshot: wrap_map::WrapSnapshot,
     block_snapshot: block_map::BlockSnapshot,
     text_highlights: TextHighlights,
+    inlay_highlights: InlayHighlights,
     clip_at_line_ends: bool,
 }
 
@@ -422,15 +430,6 @@ impl DisplaySnapshot {
             .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
     }
 
-    pub fn inlay_offset_to_display_point(&self, offset: InlayOffset, bias: Bias) -> DisplayPoint {
-        let inlay_point = self.inlay_snapshot.to_point(offset);
-        let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
-        let tab_point = self.tab_snapshot.to_tab_point(fold_point);
-        let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
-        let block_point = self.block_snapshot.to_block_point(wrap_point);
-        DisplayPoint(block_point)
-    }
-
     fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
         let block_point = point.0;
         let wrap_point = self.block_snapshot.to_wrap_point(block_point);
@@ -463,9 +462,7 @@ impl DisplaySnapshot {
             .chunks(
                 display_row..self.max_point().row() + 1,
                 false,
-                None,
-                None,
-                None,
+                Highlights::default(),
             )
             .map(|h| h.text)
     }
@@ -474,7 +471,7 @@ impl DisplaySnapshot {
     pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
         (0..=display_row).into_iter().rev().flat_map(|row| {
             self.block_snapshot
-                .chunks(row..row + 1, false, None, None, None)
+                .chunks(row..row + 1, false, Highlights::default())
                 .map(|h| h.text)
                 .collect::<Vec<_>>()
                 .into_iter()
@@ -482,19 +479,22 @@ impl DisplaySnapshot {
         })
     }
 
-    pub fn chunks(
-        &self,
+    pub fn chunks<'a>(
+        &'a self,
         display_rows: Range<u32>,
         language_aware: bool,
-        hint_highlight_style: Option<HighlightStyle>,
+        inlay_highlight_style: Option<HighlightStyle>,
         suggestion_highlight_style: Option<HighlightStyle>,
     ) -> DisplayChunks<'_> {
         self.block_snapshot.chunks(
             display_rows,
             language_aware,
-            Some(&self.text_highlights),
-            hint_highlight_style,
-            suggestion_highlight_style,
+            Highlights {
+                text_highlights: Some(&self.text_highlights),
+                inlay_highlights: Some(&self.inlay_highlights),
+                inlay_highlight_style,
+                suggestion_highlight_style,
+            },
         )
     }
 
@@ -752,12 +752,20 @@ impl DisplaySnapshot {
     }
 
     #[cfg(any(test, feature = "test-support"))]
-    pub fn highlight_ranges<Tag: ?Sized + 'static>(
+    pub fn text_highlight_ranges<Tag: ?Sized + 'static>(
         &self,
-    ) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
+    ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
         let type_id = TypeId::of::<Tag>();
         self.text_highlights.get(&Some(type_id)).cloned()
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn inlay_highlights<Tag: ?Sized + 'static>(
+        &self,
+    ) -> Option<&HashMap<InlayId, (HighlightStyle, InlayHighlight)>> {
+        let type_id = TypeId::of::<Tag>();
+        self.inlay_highlights.get(&type_id)
+    }
 }
 
 #[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]

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

@@ -1,10 +1,10 @@
 use super::{
     wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
-    TextHighlights,
+    Highlights,
 };
 use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _};
 use collections::{Bound, HashMap, HashSet};
-use gpui::{fonts::HighlightStyle, AnyElement, ViewContext};
+use gpui::{AnyElement, ViewContext};
 use language::{BufferSnapshot, Chunk, Patch, Point};
 use parking_lot::Mutex;
 use std::{
@@ -576,9 +576,7 @@ impl BlockSnapshot {
         self.chunks(
             0..self.transforms.summary().output_rows,
             false,
-            None,
-            None,
-            None,
+            Highlights::default(),
         )
         .map(|chunk| chunk.text)
         .collect()
@@ -588,9 +586,7 @@ impl BlockSnapshot {
         &'a self,
         rows: Range<u32>,
         language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        hint_highlight_style: Option<HighlightStyle>,
-        suggestion_highlight_style: Option<HighlightStyle>,
+        highlights: Highlights<'a>,
     ) -> BlockChunks<'a> {
         let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
         let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
@@ -622,9 +618,7 @@ impl BlockSnapshot {
             input_chunks: self.wrap_snapshot.chunks(
                 input_start..input_end,
                 language_aware,
-                text_highlights,
-                hint_highlight_style,
-                suggestion_highlight_style,
+                highlights,
             ),
             input_chunk: Default::default(),
             transforms: cursor,
@@ -1501,9 +1495,7 @@ mod tests {
                     .chunks(
                         start_row as u32..blocks_snapshot.max_point().row + 1,
                         false,
-                        None,
-                        None,
-                        None,
+                        Highlights::default(),
                     )
                     .map(|chunk| chunk.text)
                     .collect::<String>();

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

@@ -1,6 +1,6 @@
 use super::{
     inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
-    TextHighlights,
+    Highlights,
 };
 use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
 use gpui::{color::Color, fonts::HighlightStyle};
@@ -475,7 +475,7 @@ pub struct FoldSnapshot {
 impl FoldSnapshot {
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(FoldOffset(0)..self.len(), false, None, None, None)
+        self.chunks(FoldOffset(0)..self.len(), false, Highlights::default())
             .map(|c| c.text)
             .collect()
     }
@@ -651,9 +651,7 @@ impl FoldSnapshot {
         &'a self,
         range: Range<FoldOffset>,
         language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        hint_highlight_style: Option<HighlightStyle>,
-        suggestion_highlight_style: Option<HighlightStyle>,
+        highlights: Highlights<'a>,
     ) -> FoldChunks<'a> {
         let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
 
@@ -674,9 +672,7 @@ impl FoldSnapshot {
             inlay_chunks: self.inlay_snapshot.chunks(
                 inlay_start..inlay_end,
                 language_aware,
-                text_highlights,
-                hint_highlight_style,
-                suggestion_highlight_style,
+                highlights,
             ),
             inlay_chunk: None,
             inlay_offset: inlay_start,
@@ -687,8 +683,12 @@ impl FoldSnapshot {
     }
 
     pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
-        self.chunks(start.to_offset(self)..self.len(), false, None, None, None)
-            .flat_map(|chunk| chunk.text.chars())
+        self.chunks(
+            start.to_offset(self)..self.len(),
+            false,
+            Highlights::default(),
+        )
+        .flat_map(|chunk| chunk.text.chars())
     }
 
     #[cfg(test)]
@@ -1496,7 +1496,7 @@ mod tests {
                 let text = &expected_text[start.0..end.0];
                 assert_eq!(
                     snapshot
-                        .chunks(start..end, false, None, None, None)
+                        .chunks(start..end, false, Highlights::default())
                         .map(|c| c.text)
                         .collect::<String>(),
                     text,

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

@@ -1,5 +1,4 @@
 use crate::{
-    link_go_to_definition::DocumentRange,
     multi_buffer::{MultiBufferChunks, MultiBufferRows},
     Anchor, InlayId, MultiBufferSnapshot, ToOffset,
 };
@@ -11,12 +10,13 @@ use std::{
     cmp,
     iter::Peekable,
     ops::{Add, AddAssign, Range, Sub, SubAssign},
+    sync::Arc,
     vec,
 };
-use sum_tree::{Bias, Cursor, SumTree};
+use sum_tree::{Bias, Cursor, SumTree, TreeMap};
 use text::{Patch, Rope};
 
-use super::TextHighlights;
+use super::Highlights;
 
 pub struct InlayMap {
     snapshot: InlaySnapshot,
@@ -214,10 +214,11 @@ pub struct InlayChunks<'a> {
     inlay_chunk: Option<&'a str>,
     output_offset: InlayOffset,
     max_output_offset: InlayOffset,
-    hint_highlight_style: Option<HighlightStyle>,
+    inlay_highlight_style: Option<HighlightStyle>,
     suggestion_highlight_style: Option<HighlightStyle>,
     highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
     active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
+    highlights: Highlights<'a>,
     snapshot: &'a InlaySnapshot,
 }
 
@@ -293,8 +294,41 @@ impl<'a> Iterator for InlayChunks<'a> {
                 prefix
             }
             Transform::Inlay(inlay) => {
+                let mut inlay_style_and_highlight = None;
+                if let Some(inlay_highlights) = self.highlights.inlay_highlights {
+                    for (_, inlay_id_to_data) in inlay_highlights.iter() {
+                        let style_and_highlight = inlay_id_to_data.get(&inlay.id);
+                        if style_and_highlight.is_some() {
+                            inlay_style_and_highlight = style_and_highlight;
+                            break;
+                        }
+                    }
+                }
+
+                let mut highlight_style = match inlay.id {
+                    InlayId::Suggestion(_) => self.suggestion_highlight_style,
+                    InlayId::Hint(_) => self.inlay_highlight_style,
+                };
+                let next_inlay_highlight_endpoint;
+                let offset_in_inlay = self.output_offset - self.transforms.start().0;
+                if let Some((style, highlight)) = inlay_style_and_highlight {
+                    let range = &highlight.range;
+                    if offset_in_inlay.0 < range.start {
+                        next_inlay_highlight_endpoint = range.start - offset_in_inlay.0;
+                    } else if offset_in_inlay.0 >= range.end {
+                        next_inlay_highlight_endpoint = usize::MAX;
+                    } else {
+                        next_inlay_highlight_endpoint = range.end - offset_in_inlay.0;
+                        highlight_style
+                            .get_or_insert_with(|| Default::default())
+                            .highlight(style.clone());
+                    }
+                } else {
+                    next_inlay_highlight_endpoint = usize::MAX;
+                }
+
                 let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
-                    let start = self.output_offset - self.transforms.start().0;
+                    let start = offset_in_inlay;
                     let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0)
                         - self.transforms.start().0;
                     inlay.text.chunks_in_range(start.0..end.0)
@@ -302,21 +336,15 @@ impl<'a> Iterator for InlayChunks<'a> {
                 let inlay_chunk = self
                     .inlay_chunk
                     .get_or_insert_with(|| inlay_chunks.next().unwrap());
-                let (chunk, remainder) = inlay_chunk.split_at(
-                    inlay_chunk
-                        .len()
-                        .min(next_highlight_endpoint.0 - self.output_offset.0),
-                );
+                let (chunk, remainder) =
+                    inlay_chunk.split_at(inlay_chunk.len().min(next_inlay_highlight_endpoint));
                 *inlay_chunk = remainder;
                 if inlay_chunk.is_empty() {
                     self.inlay_chunk = None;
                 }
 
                 self.output_offset.0 += chunk.len();
-                let mut highlight_style = match inlay.id {
-                    InlayId::Suggestion(_) => self.suggestion_highlight_style,
-                    InlayId::Hint(_) => self.hint_highlight_style,
-                };
+
                 if !self.active_highlights.is_empty() {
                     for active_highlight in self.active_highlights.values() {
                         highlight_style
@@ -625,18 +653,20 @@ impl InlayMap {
                     .filter(|ch| *ch != '\r')
                     .take(len)
                     .collect::<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))
                 };
+                log::info!(
+                    "creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}",
+                    inlay_id,
+                    position,
+                    bias,
+                    text
+                );
+
                 to_insert.push(Inlay {
                     id: inlay_id,
                     position: snapshot.buffer.anchor_at(position, bias),
@@ -992,77 +1022,24 @@ impl InlaySnapshot {
         &'a self,
         range: Range<InlayOffset>,
         language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        hint_highlight_style: Option<HighlightStyle>,
-        suggestion_highlight_style: Option<HighlightStyle>,
+        highlights: Highlights<'a>,
     ) -> InlayChunks<'a> {
         let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
         cursor.seek(&range.start, Bias::Right, &());
 
         let mut highlight_endpoints = Vec::new();
-        if let Some(text_highlights) = text_highlights {
+        if let Some(text_highlights) = highlights.text_highlights {
             if !text_highlights.is_empty() {
-                while cursor.start().0 < range.end {
-                    let transform_start = self.buffer.anchor_after(
-                        self.to_buffer_offset(cmp::max(range.start, cursor.start().0)),
-                    );
-                    let transform_start =
-                        self.to_inlay_offset(transform_start.to_offset(&self.buffer));
-
-                    let transform_end = {
-                        let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
-                        self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
-                            cursor.end(&()).0,
-                            cursor.start().0 + overshoot,
-                        )))
-                    };
-                    let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer));
-
-                    for (tag, text_highlights) in text_highlights.iter() {
-                        let style = text_highlights.0;
-                        let ranges = &text_highlights.1;
-
-                        let start_ix = match ranges.binary_search_by(|probe| {
-                            let cmp = self
-                                .document_to_inlay_range(probe)
-                                .end
-                                .cmp(&transform_start);
-                            if cmp.is_gt() {
-                                cmp::Ordering::Greater
-                            } else {
-                                cmp::Ordering::Less
-                            }
-                        }) {
-                            Ok(i) | Err(i) => i,
-                        };
-                        for range in &ranges[start_ix..] {
-                            let range = self.document_to_inlay_range(range);
-                            if range.start.cmp(&transform_end).is_ge() {
-                                break;
-                            }
-
-                            highlight_endpoints.push(HighlightEndpoint {
-                                offset: range.start,
-                                is_start: true,
-                                tag: *tag,
-                                style,
-                            });
-                            highlight_endpoints.push(HighlightEndpoint {
-                                offset: range.end,
-                                is_start: false,
-                                tag: *tag,
-                                style,
-                            });
-                        }
-                    }
-
-                    cursor.next(&());
-                }
-                highlight_endpoints.sort();
+                self.apply_text_highlights(
+                    &mut cursor,
+                    &range,
+                    text_highlights,
+                    &mut highlight_endpoints,
+                );
                 cursor.seek(&range.start, Bias::Right, &());
             }
         }
-
+        highlight_endpoints.sort();
         let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
         let buffer_chunks = self.buffer.chunks(buffer_range, language_aware);
 
@@ -1074,29 +1051,76 @@ impl InlaySnapshot {
             buffer_chunk: None,
             output_offset: range.start,
             max_output_offset: range.end,
-            hint_highlight_style,
-            suggestion_highlight_style,
+            inlay_highlight_style: highlights.inlay_highlight_style,
+            suggestion_highlight_style: highlights.suggestion_highlight_style,
             highlight_endpoints: highlight_endpoints.into_iter().peekable(),
             active_highlights: Default::default(),
+            highlights,
             snapshot: self,
         }
     }
 
-    fn document_to_inlay_range(&self, range: &DocumentRange) -> Range<InlayOffset> {
-        match range {
-            DocumentRange::Text(text_range) => {
-                self.to_inlay_offset(text_range.start.to_offset(&self.buffer))
-                    ..self.to_inlay_offset(text_range.end.to_offset(&self.buffer))
-            }
-            DocumentRange::Inlay(inlay_range) => {
-                inlay_range.highlight_start..inlay_range.highlight_end
+    fn apply_text_highlights(
+        &self,
+        cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>,
+        range: &Range<InlayOffset>,
+        text_highlights: &TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
+        highlight_endpoints: &mut Vec<HighlightEndpoint>,
+    ) {
+        while cursor.start().0 < range.end {
+            let transform_start = self
+                .buffer
+                .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0)));
+            let transform_end =
+                {
+                    let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
+                    self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
+                        cursor.end(&()).0,
+                        cursor.start().0 + overshoot,
+                    )))
+                };
+
+            for (tag, text_highlights) in text_highlights.iter() {
+                let style = text_highlights.0;
+                let ranges = &text_highlights.1;
+
+                let start_ix = match ranges.binary_search_by(|probe| {
+                    let cmp = probe.end.cmp(&transform_start, &self.buffer);
+                    if cmp.is_gt() {
+                        cmp::Ordering::Greater
+                    } else {
+                        cmp::Ordering::Less
+                    }
+                }) {
+                    Ok(i) | Err(i) => i,
+                };
+                for range in &ranges[start_ix..] {
+                    if range.start.cmp(&transform_end, &self.buffer).is_ge() {
+                        break;
+                    }
+
+                    highlight_endpoints.push(HighlightEndpoint {
+                        offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)),
+                        is_start: true,
+                        tag: *tag,
+                        style,
+                    });
+                    highlight_endpoints.push(HighlightEndpoint {
+                        offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
+                        is_start: false,
+                        tag: *tag,
+                        style,
+                    });
+                }
             }
+
+            cursor.next(&());
         }
     }
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(Default::default()..self.len(), false, None, None, None)
+        self.chunks(Default::default()..self.len(), false, Highlights::default())
             .map(|chunk| chunk.text)
             .collect()
     }
@@ -1144,7 +1168,11 @@ fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{link_go_to_definition::InlayRange, InlayId, MultiBuffer};
+    use crate::{
+        display_map::{InlayHighlights, TextHighlights},
+        link_go_to_definition::InlayHighlight,
+        InlayId, MultiBuffer,
+    };
     use gpui::AppContext;
     use project::{InlayHint, InlayHintLabel, ResolveState};
     use rand::prelude::*;
@@ -1619,8 +1647,8 @@ mod tests {
                 })
                 .collect::<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());
+            for (offset, inlay) in inlays.iter().rev() {
+                expected_text.replace(*offset..*offset, &inlay.text.to_string());
             }
             assert_eq!(inlay_snapshot.text(), expected_text.to_string());
 
@@ -1640,51 +1668,87 @@ mod tests {
                 );
             }
 
-            let mut highlights = TextHighlights::default();
-            let highlight_count = rng.gen_range(0_usize..10);
-            let mut highlight_ranges = (0..highlight_count)
+            let mut text_highlights = TextHighlights::default();
+            let text_highlight_count = rng.gen_range(0_usize..10);
+            let mut text_highlight_ranges = (0..text_highlight_count)
                 .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
                 .collect::<Vec<_>>();
-            highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
-            log::info!("highlighting ranges {:?}", highlight_ranges);
-            let highlight_ranges = if rng.gen_bool(0.5) {
-                highlight_ranges
-                    .into_iter()
-                    .map(|range| InlayRange {
-                        inlay_position: buffer_snapshot.anchor_before(range.start),
-                        highlight_start: inlay_snapshot.to_inlay_offset(range.start),
-                        highlight_end: inlay_snapshot.to_inlay_offset(range.end),
-                    })
-                    .map(DocumentRange::Inlay)
-                    .collect::<Vec<_>>()
-            } else {
-                highlight_ranges
-                    .into_iter()
-                    .map(|range| {
-                        buffer_snapshot.anchor_before(range.start)
-                            ..buffer_snapshot.anchor_after(range.end)
-                    })
-                    .map(DocumentRange::Text)
-                    .collect::<Vec<_>>()
-            };
-            highlights.insert(
+            text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
+            log::info!("highlighting text ranges {text_highlight_ranges:?}");
+            text_highlights.insert(
                 Some(TypeId::of::<()>()),
-                Arc::new((HighlightStyle::default(), highlight_ranges)),
+                Arc::new((
+                    HighlightStyle::default(),
+                    text_highlight_ranges
+                        .into_iter()
+                        .map(|range| {
+                            buffer_snapshot.anchor_before(range.start)
+                                ..buffer_snapshot.anchor_after(range.end)
+                        })
+                        .collect(),
+                )),
             );
 
+            let mut inlay_highlights = InlayHighlights::default();
+            if !inlays.is_empty() {
+                let inlay_highlight_count = rng.gen_range(0..inlays.len());
+                let mut inlay_indices = BTreeSet::default();
+                while inlay_indices.len() < inlay_highlight_count {
+                    inlay_indices.insert(rng.gen_range(0..inlays.len()));
+                }
+                let new_highlights = inlay_indices
+                    .into_iter()
+                    .filter_map(|i| {
+                        let (_, inlay) = &inlays[i];
+                        let inlay_text_len = inlay.text.len();
+                        match inlay_text_len {
+                            0 => None,
+                            1 => Some(InlayHighlight {
+                                inlay: inlay.id,
+                                inlay_position: inlay.position,
+                                range: 0..1,
+                            }),
+                            n => {
+                                let inlay_text = inlay.text.to_string();
+                                let mut highlight_end = rng.gen_range(1..n);
+                                let mut highlight_start = rng.gen_range(0..highlight_end);
+                                while !inlay_text.is_char_boundary(highlight_end) {
+                                    highlight_end += 1;
+                                }
+                                while !inlay_text.is_char_boundary(highlight_start) {
+                                    highlight_start -= 1;
+                                }
+                                Some(InlayHighlight {
+                                    inlay: inlay.id,
+                                    inlay_position: inlay.position,
+                                    range: highlight_start..highlight_end,
+                                })
+                            }
+                        }
+                    })
+                    .map(|highlight| (highlight.inlay, (HighlightStyle::default(), highlight)))
+                    .collect();
+                log::info!("highlighting inlay ranges {new_highlights:?}");
+                inlay_highlights.insert(TypeId::of::<()>(), new_highlights);
+            }
+
             for _ in 0..5 {
                 let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
                 end = expected_text.clip_offset(end, Bias::Right);
                 let mut start = rng.gen_range(0..=end);
                 start = expected_text.clip_offset(start, Bias::Right);
 
+                let range = InlayOffset(start)..InlayOffset(end);
+                log::info!("calling inlay_snapshot.chunks({range:?})");
                 let actual_text = inlay_snapshot
                     .chunks(
-                        InlayOffset(start)..InlayOffset(end),
+                        range,
                         false,
-                        Some(&highlights),
-                        None,
-                        None,
+                        Highlights {
+                            text_highlights: Some(&text_highlights),
+                            inlay_highlights: Some(&inlay_highlights),
+                            ..Highlights::default()
+                        },
                     )
                     .map(|chunk| chunk.text)
                     .collect::<String>();

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

@@ -1,9 +1,8 @@
 use super::{
     fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
-    TextHighlights,
+    Highlights,
 };
 use crate::MultiBufferSnapshot;
-use gpui::fonts::HighlightStyle;
 use language::{Chunk, Point};
 use std::{cmp, mem, num::NonZeroU32, ops::Range};
 use sum_tree::Bias;
@@ -68,9 +67,7 @@ impl TabMap {
                 'outer: for chunk in old_snapshot.fold_snapshot.chunks(
                     fold_edit.old.end..old_end_row_successor_offset,
                     false,
-                    None,
-                    None,
-                    None,
+                    Highlights::default(),
                 ) {
                     for (ix, _) in chunk.text.match_indices('\t') {
                         let offset_from_edit = offset_from_edit + (ix as u32);
@@ -183,7 +180,7 @@ impl TabSnapshot {
             self.max_point()
         };
         for c in self
-            .chunks(range.start..line_end, false, None, None, None)
+            .chunks(range.start..line_end, false, Highlights::default())
             .flat_map(|chunk| chunk.text.chars())
         {
             if c == '\n' {
@@ -200,9 +197,7 @@ impl TabSnapshot {
                 .chunks(
                     TabPoint::new(range.end.row(), 0)..range.end,
                     false,
-                    None,
-                    None,
-                    None,
+                    Highlights::default(),
                 )
                 .flat_map(|chunk| chunk.text.chars())
             {
@@ -223,9 +218,7 @@ impl TabSnapshot {
         &'a self,
         range: Range<TabPoint>,
         language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        hint_highlight_style: Option<HighlightStyle>,
-        suggestion_highlight_style: Option<HighlightStyle>,
+        highlights: Highlights<'a>,
     ) -> TabChunks<'a> {
         let (input_start, expanded_char_column, to_next_stop) =
             self.to_fold_point(range.start, Bias::Left);
@@ -245,9 +238,7 @@ impl TabSnapshot {
             fold_chunks: self.fold_snapshot.chunks(
                 input_start..input_end,
                 language_aware,
-                text_highlights,
-                hint_highlight_style,
-                suggestion_highlight_style,
+                highlights,
             ),
             input_column,
             column: expanded_char_column,
@@ -270,9 +261,13 @@ impl TabSnapshot {
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None)
-            .map(|chunk| chunk.text)
-            .collect()
+        self.chunks(
+            TabPoint::zero()..self.max_point(),
+            false,
+            Highlights::default(),
+        )
+        .map(|chunk| chunk.text)
+        .collect()
     }
 
     pub fn max_point(&self) -> TabPoint {
@@ -597,9 +592,7 @@ mod tests {
                     .chunks(
                         TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
                         false,
-                        None,
-                        None,
-                        None,
+                        Highlights::default(),
                     )
                     .map(|c| c.text)
                     .collect::<String>(),
@@ -674,7 +667,8 @@ mod tests {
             let mut chunks = Vec::new();
             let mut was_tab = false;
             let mut text = String::new();
-            for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) {
+            for chunk in snapshot.chunks(start..snapshot.max_point(), false, Highlights::default())
+            {
                 if chunk.is_tab != was_tab {
                     if !text.is_empty() {
                         chunks.push((mem::take(&mut text), was_tab));
@@ -743,7 +737,7 @@ mod tests {
             let expected_summary = TextSummary::from(expected_text.as_str());
             assert_eq!(
                 tabs_snapshot
-                    .chunks(start..end, false, None, None, None)
+                    .chunks(start..end, false, Highlights::default())
                     .map(|c| c.text)
                     .collect::<String>(),
                 expected_text,

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

@@ -1,13 +1,11 @@
 use super::{
     fold_map::FoldBufferRows,
     tab_map::{self, TabEdit, TabPoint, TabSnapshot},
-    TextHighlights,
+    Highlights,
 };
 use crate::MultiBufferSnapshot;
 use gpui::{
-    fonts::{FontId, HighlightStyle},
-    text_layout::LineWrapper,
-    AppContext, Entity, ModelContext, ModelHandle, Task,
+    fonts::FontId, text_layout::LineWrapper, AppContext, Entity, ModelContext, ModelHandle, Task,
 };
 use language::{Chunk, Point};
 use lazy_static::lazy_static;
@@ -444,9 +442,7 @@ impl WrapSnapshot {
                 let mut chunks = new_tab_snapshot.chunks(
                     TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
                     false,
-                    None,
-                    None,
-                    None,
+                    Highlights::default(),
                 );
                 let mut edit_transforms = Vec::<Transform>::new();
                 for _ in edit.new_rows.start..edit.new_rows.end {
@@ -575,9 +571,7 @@ impl WrapSnapshot {
         &'a self,
         rows: Range<u32>,
         language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        hint_highlight_style: Option<HighlightStyle>,
-        suggestion_highlight_style: Option<HighlightStyle>,
+        highlights: Highlights<'a>,
     ) -> WrapChunks<'a> {
         let output_start = WrapPoint::new(rows.start, 0);
         let output_end = WrapPoint::new(rows.end, 0);
@@ -594,9 +588,7 @@ impl WrapSnapshot {
             input_chunks: self.tab_snapshot.chunks(
                 input_start..input_end,
                 language_aware,
-                text_highlights,
-                hint_highlight_style,
-                suggestion_highlight_style,
+                highlights,
             ),
             input_chunk: Default::default(),
             output_position: output_start,
@@ -1323,9 +1315,7 @@ mod tests {
             self.chunks(
                 wrap_row..self.max_point().row() + 1,
                 false,
-                None,
-                None,
-                None,
+                Highlights::default(),
             )
             .map(|h| h.text)
         }
@@ -1350,7 +1340,7 @@ mod tests {
                 }
 
                 let actual_text = self
-                    .chunks(start_row..end_row, true, None, None, None)
+                    .chunks(start_row..end_row, true, Highlights::default())
                     .map(|c| c.text)
                     .collect::<String>();
                 assert_eq!(

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

@@ -25,7 +25,7 @@ use ::git::diff::DiffHunk;
 use aho_corasick::AhoCorasick;
 use anyhow::{anyhow, Context, Result};
 use blink_manager::BlinkManager;
-use client::{ClickhouseEvent, TelemetrySettings};
+use client::{ClickhouseEvent, Collaborator, ParticipantIndex, TelemetrySettings};
 use clock::{Global, ReplicaId};
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 use convert_case::{Case, Casing};
@@ -66,7 +66,7 @@ use language::{
     TransactionId,
 };
 use link_go_to_definition::{
-    hide_link_definition, show_link_definition, DocumentRange, GoToDefinitionLink, InlayRange,
+    hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight,
     LinkGoToDefinitionState,
 };
 use log::error;
@@ -79,6 +79,7 @@ pub use multi_buffer::{
 use ordered_float::OrderedFloat;
 use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
 use rand::{seq::SliceRandom, thread_rng};
+use rpc::proto::PeerId;
 use scroll::{
     autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
 };
@@ -99,10 +100,11 @@ use std::{
     time::{Duration, Instant},
 };
 pub use sum_tree::Bias;
+use sum_tree::TreeMap;
 use text::Rope;
 use theme::{DiagnosticStyle, Theme, ThemeSettings};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
-use workspace::{ItemNavHistory, ViewId, Workspace};
+use workspace::{ItemNavHistory, SplitDirection, ViewId, Workspace};
 
 use crate::git::diff_hunk_to_display;
 
@@ -128,6 +130,12 @@ pub struct SelectPrevious {
     pub replace_newest: bool,
 }
 
+#[derive(Clone, Deserialize, PartialEq, Default)]
+pub struct SelectAllMatches {
+    #[serde(default)]
+    pub replace_newest: bool,
+}
+
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct SelectToBeginningOfLine {
     #[serde(default)]
@@ -324,6 +332,7 @@ impl_actions!(
     [
         SelectNext,
         SelectPrevious,
+        SelectAllMatches,
         SelectToBeginningOfLine,
         SelectToEndOfLine,
         ToggleCodeActions,
@@ -355,6 +364,7 @@ pub fn init_settings(cx: &mut AppContext) {
 pub fn init(cx: &mut AppContext) {
     init_settings(cx);
     cx.add_action(Editor::new_file);
+    cx.add_action(Editor::new_file_in_direction);
     cx.add_action(Editor::cancel);
     cx.add_action(Editor::newline);
     cx.add_action(Editor::newline_above);
@@ -426,6 +436,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(Editor::select_to_beginning);
     cx.add_action(Editor::select_to_end);
     cx.add_action(Editor::select_all);
+    cx.add_action(Editor::select_all_matches);
     cx.add_action(Editor::select_line);
     cx.add_action(Editor::split_selection_into_lines);
     cx.add_action(Editor::add_selection_above);
@@ -548,7 +559,8 @@ type CompletionId = usize;
 type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
 type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
 
-type BackgroundHighlight = (fn(&Theme) -> Color, Vec<DocumentRange>);
+type BackgroundHighlight = (fn(&Theme) -> Color, Vec<Range<Anchor>>);
+type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec<InlayHighlight>);
 
 pub struct Editor {
     handle: WeakViewHandle<Self>,
@@ -570,16 +582,17 @@ pub struct Editor {
     get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
     override_text_style: Option<Box<OverrideTextStyle>>,
     project: Option<ModelHandle<Project>>,
+    collaboration_hub: Option<Box<dyn CollaborationHub>>,
     focused: bool,
     blink_manager: ModelHandle<BlinkManager>,
     pub show_local_selections: bool,
     mode: EditorMode,
-    replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
     show_gutter: bool,
     show_wrap_guides: Option<bool>,
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
     background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
+    inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
     nav_history: Option<ItemNavHistory>,
     context_menu: Option<ContextMenu>,
     mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
@@ -597,7 +610,7 @@ pub struct Editor {
     keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
     input_enabled: bool,
     read_only: bool,
-    leader_replica_id: Option<u16>,
+    leader_peer_id: Option<PeerId>,
     remote_id: Option<ViewId>,
     hover_state: HoverState,
     gutter_hovered: bool,
@@ -619,6 +632,15 @@ pub struct EditorSnapshot {
     ongoing_scroll: OngoingScroll,
 }
 
+pub struct RemoteSelection {
+    pub replica_id: ReplicaId,
+    pub selection: Selection<Anchor>,
+    pub cursor_shape: CursorShape,
+    pub peer_id: PeerId,
+    pub line_mode: bool,
+    pub participant_index: Option<ParticipantIndex>,
+}
+
 #[derive(Clone, Debug)]
 struct SelectionHistoryEntry {
     selections: Arc<[Selection<Anchor>]>,
@@ -721,13 +743,22 @@ struct AddSelectionsState {
     stack: Vec<usize>,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone)]
 struct SelectNextState {
     query: AhoCorasick,
     wordwise: bool,
     done: bool,
 }
 
+impl std::fmt::Debug for SelectNextState {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct(std::any::type_name::<Self>())
+            .field("wordwise", &self.wordwise)
+            .field("done", &self.done)
+            .finish()
+    }
+}
+
 #[derive(Debug)]
 struct AutocloseRegion {
     selection_id: usize,
@@ -1026,7 +1057,8 @@ impl CompletionsMenu {
                                     item_ix: Some(item_ix),
                                 },
                                 cx,
-                            );
+                            )
+                            .map(|task| task.detach());
                         })
                         .into_any(),
                     );
@@ -1111,12 +1143,14 @@ struct CodeActionsMenu {
 impl CodeActionsMenu {
     fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
         self.selected_item = 0;
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
         cx.notify()
     }
 
     fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
         if self.selected_item > 0 {
             self.selected_item -= 1;
+            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
             cx.notify()
         }
     }
@@ -1124,12 +1158,14 @@ impl CodeActionsMenu {
     fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
         if self.selected_item + 1 < self.actions.len() {
             self.selected_item += 1;
+            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
             cx.notify()
         }
     }
 
     fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
         self.selected_item = self.actions.len() - 1;
+        self.list.scroll_to(ScrollTarget::Show(self.selected_item));
         cx.notify()
     }
 
@@ -1182,7 +1218,9 @@ impl CodeActionsMenu {
                                     workspace.update(cx, |workspace, cx| {
                                         if let Some(task) = Editor::confirm_code_action(
                                             workspace,
-                                            &Default::default(),
+                                            &ConfirmCodeAction {
+                                                item_ix: Some(item_ix),
+                                            },
                                             cx,
                                         ) {
                                             task.detach_and_log_err(cx);
@@ -1512,17 +1550,18 @@ impl Editor {
             active_diagnostics: None,
             soft_wrap_mode_override,
             get_field_editor_theme,
+            collaboration_hub: project.clone().map(|project| Box::new(project) as _),
             project,
             focused: false,
             blink_manager: blink_manager.clone(),
             show_local_selections: true,
             mode,
-            replica_id_mapping: None,
             show_gutter: mode == EditorMode::Full,
             show_wrap_guides: None,
             placeholder_text: None,
             highlighted_rows: None,
             background_highlights: Default::default(),
+            inlay_background_highlights: Default::default(),
             nav_history: None,
             context_menu: None,
             mouse_context_menu: cx
@@ -1543,7 +1582,7 @@ impl Editor {
             keymap_context_layers: Default::default(),
             input_enabled: true,
             read_only: false,
-            leader_replica_id: None,
+            leader_peer_id: None,
             remote_id: None,
             hover_state: Default::default(),
             link_go_to_definition_state: Default::default(),
@@ -1606,12 +1645,32 @@ impl Editor {
         }
     }
 
+    pub fn new_file_in_direction(
+        workspace: &mut Workspace,
+        action: &workspace::NewFileInDirection,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let project = workspace.project().clone();
+        if project.read(cx).is_remote() {
+            cx.propagate_action();
+        } else if let Some(buffer) = project
+            .update(cx, |project, cx| project.create_buffer("", None, cx))
+            .log_err()
+        {
+            workspace.split_item(
+                action.0,
+                Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
+                cx,
+            );
+        }
+    }
+
     pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
         self.buffer.read(cx).replica_id()
     }
 
-    pub fn leader_replica_id(&self) -> Option<ReplicaId> {
-        self.leader_replica_id
+    pub fn leader_peer_id(&self) -> Option<PeerId> {
+        self.leader_peer_id
     }
 
     pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
@@ -1675,6 +1734,14 @@ impl Editor {
         self.mode
     }
 
+    pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> {
+        self.collaboration_hub.as_deref()
+    }
+
+    pub fn set_collaboration_hub(&mut self, hub: Box<dyn CollaborationHub>) {
+        self.collaboration_hub = Some(hub);
+    }
+
     pub fn set_placeholder_text(
         &mut self,
         placeholder_text: impl Into<Arc<str>>,
@@ -1734,6 +1801,10 @@ impl Editor {
         }
     }
 
+    pub fn read_only(&self) -> bool {
+        self.read_only
+    }
+
     pub fn set_read_only(&mut self, read_only: bool) {
         self.read_only = read_only;
     }
@@ -1747,26 +1818,13 @@ impl Editor {
         cx.notify();
     }
 
-    pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
-        self.replica_id_mapping.as_ref()
-    }
-
-    pub fn set_replica_id_map(
-        &mut self,
-        mapping: Option<HashMap<ReplicaId, ReplicaId>>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.replica_id_mapping = mapping;
-        cx.notify();
-    }
-
     fn selections_did_change(
         &mut self,
         local: bool,
         old_cursor_position: &Anchor,
         cx: &mut ViewContext<Self>,
     ) {
-        if self.focused && self.leader_replica_id.is_none() {
+        if self.focused && self.leader_peer_id.is_none() {
             self.buffer.update(cx, |buffer, cx| {
                 buffer.set_active_selections(
                     &self.selections.disjoint_anchors(),
@@ -2285,14 +2343,18 @@ impl Editor {
                 // bracket of any of this language's bracket pairs.
                 let mut bracket_pair = None;
                 let mut is_bracket_pair_start = false;
-                for (pair, enabled) in scope.brackets() {
-                    if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
-                        bracket_pair = Some(pair.clone());
-                        is_bracket_pair_start = true;
-                        break;
-                    } else if pair.end.as_str() == text.as_ref() {
-                        bracket_pair = Some(pair.clone());
-                        break;
+                if !text.is_empty() {
+                    // `text` can be empty when an user is using IME (e.g. Chinese Wubi Simplified)
+                    //  and they are removing the character that triggered IME popup.
+                    for (pair, enabled) in scope.brackets() {
+                        if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
+                            bracket_pair = Some(pair.clone());
+                            is_bracket_pair_start = true;
+                            break;
+                        } else if pair.end.as_str() == text.as_ref() {
+                            bracket_pair = Some(pair.clone());
+                            break;
+                        }
                     }
                 }
 
@@ -2392,7 +2454,13 @@ impl Editor {
             let snapshot = this.buffer.read(cx).read(cx);
             let new_selections = resolve_multiple::<usize, _>(new_anchor_selections, &snapshot)
                 .zip(new_selection_deltas)
-                .map(|(selection, delta)| selection.map(|e| e + delta))
+                .map(|(selection, delta)| Selection {
+                    id: selection.id,
+                    start: selection.start + delta,
+                    end: selection.end + delta,
+                    reversed: selection.reversed,
+                    goal: SelectionGoal::None,
+                })
                 .collect::<Vec<_>>();
 
             let mut i = 0;
@@ -3815,7 +3883,7 @@ impl Editor {
             enum CodeActions {}
             Some(
                 MouseEventHandler::new::<CodeActions, _>(0, cx, |state, _| {
-                    Svg::new("icons/bolt_8.svg").with_color(
+                    Svg::new("icons/bolt.svg").with_color(
                         style
                             .code_actions
                             .indicator
@@ -4597,7 +4665,13 @@ impl Editor {
     }
 
     pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext<Self>) {
-        self.manipulate_text(cx, |text| text.to_case(Case::Title))
+        self.manipulate_text(cx, |text| {
+            // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
+            // https://github.com/rutrum/convert-case/issues/16
+            text.split("\n")
+                .map(|line| line.to_case(Case::Title))
+                .join("\n")
+        })
     }
 
     pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext<Self>) {
@@ -4613,7 +4687,13 @@ impl Editor {
         _: &ConvertToUpperCamelCase,
         cx: &mut ViewContext<Self>,
     ) {
-        self.manipulate_text(cx, |text| text.to_case(Case::UpperCamel))
+        self.manipulate_text(cx, |text| {
+            // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
+            // https://github.com/rutrum/convert-case/issues/16
+            text.split("\n")
+                .map(|line| line.to_case(Case::UpperCamel))
+                .join("\n")
+        })
     }
 
     pub fn convert_to_lower_camel_case(
@@ -5121,9 +5201,6 @@ impl Editor {
             self.unmark_text(cx);
             self.refresh_copilot_suggestions(true, cx);
             cx.emit(Event::Edited);
-            cx.emit(Event::TransactionUndone {
-                transaction_id: tx_id,
-            });
         }
     }
 
@@ -5927,9 +6004,29 @@ impl Editor {
         }
     }
 
-    pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext<Self>) {
-        self.push_to_selection_history();
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+    pub fn select_next_match_internal(
+        &mut self,
+        display_map: &DisplaySnapshot,
+        replace_newest: bool,
+        autoscroll: Option<Autoscroll>,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()> {
+        fn select_next_match_ranges(
+            this: &mut Editor,
+            range: Range<usize>,
+            replace_newest: bool,
+            auto_scroll: Option<Autoscroll>,
+            cx: &mut ViewContext<Editor>,
+        ) {
+            this.unfold_ranges([range.clone()], false, true, cx);
+            this.change_selections(auto_scroll, cx, |s| {
+                if replace_newest {
+                    s.delete(s.newest_anchor().id);
+                }
+                s.insert_range(range.clone());
+            });
+        }
+
         let buffer = &display_map.buffer_snapshot;
         let mut selections = self.selections.all::<usize>(cx);
         if let Some(mut select_next_state) = self.select_next_state.take() {
@@ -5950,6 +6047,7 @@ impl Editor {
                             .stream_find_iter(bytes_before_first_selection)
                             .map(|result| (0, result)),
                     );
+
                 for (start_offset, query_match) in query_matches {
                     let query_match = query_match.unwrap(); // can only fail due to I/O
                     let offset_range =
@@ -5961,19 +6059,25 @@ impl Editor {
                         || (!movement::is_inside_word(&display_map, display_range.start)
                             && !movement::is_inside_word(&display_map, display_range.end))
                     {
-                        next_selected_range = Some(offset_range);
-                        break;
+                        if selections
+                            .iter()
+                            .find(|selection| selection.equals(&offset_range))
+                            .is_none()
+                        {
+                            next_selected_range = Some(offset_range);
+                            break;
+                        }
                     }
                 }
 
                 if let Some(next_selected_range) = next_selected_range {
-                    self.unfold_ranges([next_selected_range.clone()], false, true, cx);
-                    self.change_selections(Some(Autoscroll::newest()), cx, |s| {
-                        if action.replace_newest {
-                            s.delete(s.newest_anchor().id);
-                        }
-                        s.insert_range(next_selected_range);
-                    });
+                    select_next_match_ranges(
+                        self,
+                        next_selected_range,
+                        replace_newest,
+                        autoscroll,
+                        cx,
+                    );
                 } else {
                     select_next_state.done = true;
                 }
@@ -5995,31 +6099,77 @@ impl Editor {
                 let query = buffer
                     .text_for_range(selection.start..selection.end)
                     .collect::<String>();
+
+                let is_empty = query.is_empty();
                 let select_state = SelectNextState {
-                    query: AhoCorasick::new_auto_configured(&[query]),
+                    query: AhoCorasick::new(&[query])?,
                     wordwise: true,
-                    done: false,
+                    done: is_empty,
                 };
-                self.unfold_ranges([selection.start..selection.end], false, true, cx);
-                self.change_selections(Some(Autoscroll::newest()), cx, |s| {
-                    s.select(selections);
-                });
+                select_next_match_ranges(
+                    self,
+                    selection.start..selection.end,
+                    replace_newest,
+                    autoscroll,
+                    cx,
+                );
                 self.select_next_state = Some(select_state);
             } else {
                 let query = buffer
                     .text_for_range(selection.start..selection.end)
                     .collect::<String>();
                 self.select_next_state = Some(SelectNextState {
-                    query: AhoCorasick::new_auto_configured(&[query]),
+                    query: AhoCorasick::new(&[query])?,
                     wordwise: false,
                     done: false,
                 });
-                self.select_next(action, cx);
+                self.select_next_match_internal(display_map, replace_newest, autoscroll, cx)?;
+            }
+        }
+        Ok(())
+    }
+
+    pub fn select_all_matches(
+        &mut self,
+        action: &SelectAllMatches,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()> {
+        self.push_to_selection_history();
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        loop {
+            self.select_next_match_internal(&display_map, action.replace_newest, None, cx)?;
+
+            if self
+                .select_next_state
+                .as_ref()
+                .map(|selection_state| selection_state.done)
+                .unwrap_or(true)
+            {
+                break;
             }
         }
+
+        Ok(())
+    }
+
+    pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext<Self>) -> Result<()> {
+        self.push_to_selection_history();
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        self.select_next_match_internal(
+            &display_map,
+            action.replace_newest,
+            Some(Autoscroll::newest()),
+            cx,
+        )?;
+        Ok(())
     }
 
-    pub fn select_previous(&mut self, action: &SelectPrevious, cx: &mut ViewContext<Self>) {
+    pub fn select_previous(
+        &mut self,
+        action: &SelectPrevious,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()> {
         self.push_to_selection_history();
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
@@ -6090,7 +6240,7 @@ impl Editor {
                     .collect::<String>();
                 let query = query.chars().rev().collect::<String>();
                 let select_state = SelectNextState {
-                    query: AhoCorasick::new_auto_configured(&[query]),
+                    query: AhoCorasick::new(&[query])?,
                     wordwise: true,
                     done: false,
                 };
@@ -6105,13 +6255,14 @@ impl Editor {
                     .collect::<String>();
                 let query = query.chars().rev().collect::<String>();
                 self.select_prev_state = Some(SelectNextState {
-                    query: AhoCorasick::new_auto_configured(&[query]),
+                    query: AhoCorasick::new(&[query])?,
                     wordwise: false,
                     done: false,
                 });
-                self.select_previous(action, cx);
+                self.select_previous(action, cx)?;
             }
         }
+        Ok(())
     }
 
     pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
@@ -7030,7 +7181,7 @@ impl Editor {
             );
         });
         if split {
-            workspace.split_item(Box::new(editor), cx);
+            workspace.split_item(SplitDirection::Right, Box::new(editor), cx);
         } else {
             workspace.add_item(Box::new(editor), cx);
         }
@@ -7065,16 +7216,8 @@ impl Editor {
             } else {
                 this.update(&mut cx, |this, cx| {
                     let buffer = this.buffer.read(cx).snapshot(cx);
-                    let display_snapshot = this
-                        .display_map
-                        .update(cx, |display_map, cx| display_map.snapshot(cx));
                     let mut buffer_highlights = this
-                        .document_highlights_for_position(
-                            selection.head(),
-                            &buffer,
-                            &display_snapshot,
-                        )
-                        .filter_map(|highlight| highlight.as_text_range())
+                        .document_highlights_for_position(selection.head(), &buffer)
                         .filter(|highlight| {
                             highlight.start.excerpt_id() == selection.head().excerpt_id()
                                 && highlight.end.excerpt_id() == selection.head().excerpt_id()
@@ -7129,15 +7272,11 @@ impl Editor {
                     let ranges = this
                         .clear_background_highlights::<DocumentHighlightWrite>(cx)
                         .into_iter()
-                        .flat_map(|(_, ranges)| {
-                            ranges.into_iter().filter_map(|range| range.as_text_range())
-                        })
+                        .flat_map(|(_, ranges)| ranges.into_iter())
                         .chain(
                             this.clear_background_highlights::<DocumentHighlightRead>(cx)
                                 .into_iter()
-                                .flat_map(|(_, ranges)| {
-                                    ranges.into_iter().filter_map(|range| range.as_text_range())
-                                }),
+                                .flat_map(|(_, ranges)| ranges.into_iter()),
                         )
                         .collect();
 
@@ -7238,7 +7377,7 @@ impl Editor {
             Some(Autoscroll::fit()),
             cx,
         );
-        self.clear_text_highlights::<Rename>(cx);
+        self.clear_highlights::<Rename>(cx);
         self.show_local_selections = true;
 
         if moving_cursor {
@@ -7815,29 +7954,20 @@ impl Editor {
         color_fetcher: fn(&Theme) -> Color,
         cx: &mut ViewContext<Self>,
     ) {
-        self.background_highlights.insert(
-            TypeId::of::<T>(),
-            (
-                color_fetcher,
-                ranges.into_iter().map(DocumentRange::Text).collect(),
-            ),
-        );
+        self.background_highlights
+            .insert(TypeId::of::<T>(), (color_fetcher, ranges));
         cx.notify();
     }
 
     pub fn highlight_inlay_background<T: 'static>(
         &mut self,
-        ranges: Vec<InlayRange>,
+        ranges: Vec<InlayHighlight>,
         color_fetcher: fn(&Theme) -> Color,
         cx: &mut ViewContext<Self>,
     ) {
-        self.background_highlights.insert(
-            TypeId::of::<T>(),
-            (
-                color_fetcher,
-                ranges.into_iter().map(DocumentRange::Inlay).collect(),
-            ),
-        );
+        // TODO: no actual highlights happen for inlays currently, find a way to do that
+        self.inlay_background_highlights
+            .insert(Some(TypeId::of::<T>()), (color_fetcher, ranges));
         cx.notify();
     }
 
@@ -7845,15 +7975,18 @@ impl Editor {
         &mut self,
         cx: &mut ViewContext<Self>,
     ) -> Option<BackgroundHighlight> {
-        let highlights = self.background_highlights.remove(&TypeId::of::<T>());
-        if highlights.is_some() {
+        let text_highlights = self.background_highlights.remove(&TypeId::of::<T>());
+        let inlay_highlights = self
+            .inlay_background_highlights
+            .remove(&Some(TypeId::of::<T>()));
+        if text_highlights.is_some() || inlay_highlights.is_some() {
             cx.notify();
         }
-        highlights
+        text_highlights
     }
 
     #[cfg(feature = "test-support")]
-    pub fn all_background_highlights(
+    pub fn all_text_background_highlights(
         &mut self,
         cx: &mut ViewContext<Self>,
     ) -> Vec<(Range<DisplayPoint>, Color)> {
@@ -7869,8 +8002,7 @@ impl Editor {
         &'a self,
         position: Anchor,
         buffer: &'a MultiBufferSnapshot,
-        display_snapshot: &'a DisplaySnapshot,
-    ) -> impl 'a + Iterator<Item = &DocumentRange> {
+    ) -> impl 'a + Iterator<Item = &Range<Anchor>> {
         let read_highlights = self
             .background_highlights
             .get(&TypeId::of::<DocumentHighlightRead>())
@@ -7879,16 +8011,14 @@ impl Editor {
             .background_highlights
             .get(&TypeId::of::<DocumentHighlightWrite>())
             .map(|h| &h.1);
-        let left_position = display_snapshot.anchor_to_inlay_offset(position.bias_left(buffer));
-        let right_position = display_snapshot.anchor_to_inlay_offset(position.bias_right(buffer));
+        let left_position = position.bias_left(buffer);
+        let right_position = position.bias_right(buffer);
         read_highlights
             .into_iter()
             .chain(write_highlights)
             .flat_map(move |ranges| {
                 let start_ix = match ranges.binary_search_by(|probe| {
-                    let cmp = document_to_inlay_range(probe, display_snapshot)
-                        .end
-                        .cmp(&left_position);
+                    let cmp = probe.end.cmp(&left_position, buffer);
                     if cmp.is_ge() {
                         Ordering::Greater
                     } else {
@@ -7899,12 +8029,9 @@ impl Editor {
                 };
 
                 let right_position = right_position.clone();
-                ranges[start_ix..].iter().take_while(move |range| {
-                    document_to_inlay_range(range, display_snapshot)
-                        .start
-                        .cmp(&right_position)
-                        .is_le()
-                })
+                ranges[start_ix..]
+                    .iter()
+                    .take_while(move |range| range.start.cmp(&right_position, buffer).is_le())
             })
     }
 
@@ -7914,15 +8041,13 @@ impl Editor {
         display_snapshot: &DisplaySnapshot,
         theme: &Theme,
     ) -> Vec<(Range<DisplayPoint>, Color)> {
-        let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start)
-            ..display_snapshot.anchor_to_inlay_offset(search_range.end);
         let mut results = Vec::new();
         for (color_fetcher, ranges) in self.background_highlights.values() {
             let color = color_fetcher(theme);
             let start_ix = match ranges.binary_search_by(|probe| {
-                let cmp = document_to_inlay_range(probe, display_snapshot)
+                let cmp = probe
                     .end
-                    .cmp(&search_range.start);
+                    .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
                 if cmp.is_gt() {
                     Ordering::Greater
                 } else {
@@ -7932,13 +8057,16 @@ impl Editor {
                 Ok(i) | Err(i) => i,
             };
             for range in &ranges[start_ix..] {
-                let range = document_to_inlay_range(range, display_snapshot);
-                if range.start.cmp(&search_range.end).is_ge() {
+                if range
+                    .start
+                    .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
+                    .is_ge()
+                {
                     break;
                 }
 
-                let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left);
-                let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right);
+                let start = range.start.to_display_point(&display_snapshot);
+                let end = range.end.to_display_point(&display_snapshot);
                 results.push((start..end, color))
             }
         }
@@ -7951,17 +8079,15 @@ impl Editor {
         display_snapshot: &DisplaySnapshot,
         count: usize,
     ) -> Vec<RangeInclusive<DisplayPoint>> {
-        let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start)
-            ..display_snapshot.anchor_to_inlay_offset(search_range.end);
         let mut results = Vec::new();
         let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::<T>()) else {
             return vec![];
         };
 
         let start_ix = match ranges.binary_search_by(|probe| {
-            let cmp = document_to_inlay_range(probe, display_snapshot)
+            let cmp = probe
                 .end
-                .cmp(&search_range.start);
+                .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
             if cmp.is_gt() {
                 Ordering::Greater
             } else {
@@ -7984,22 +8110,20 @@ impl Editor {
             return Vec::new();
         }
         for range in &ranges[start_ix..] {
-            let range = document_to_inlay_range(range, display_snapshot);
-            if range.start.cmp(&search_range.end).is_ge() {
+            if range
+                .start
+                .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
+                .is_ge()
+            {
                 break;
             }
-            let end = display_snapshot
-                .inlay_offset_to_display_point(range.end, Bias::Right)
-                .to_point(display_snapshot);
+            let end = range.end.to_point(&display_snapshot.buffer_snapshot);
             if let Some(current_row) = &end_row {
                 if end.row == current_row.row {
                     continue;
                 }
             }
-            let start = display_snapshot
-                .inlay_offset_to_display_point(range.start, Bias::Left)
-                .to_point(display_snapshot);
-
+            let start = range.start.to_point(&display_snapshot.buffer_snapshot);
             if start_row.is_none() {
                 assert_eq!(end_row, None);
                 start_row = Some(start);
@@ -8038,12 +8162,12 @@ impl Editor {
 
     pub fn highlight_inlays<T: 'static>(
         &mut self,
-        ranges: Vec<InlayRange>,
+        highlights: Vec<InlayHighlight>,
         style: HighlightStyle,
         cx: &mut ViewContext<Self>,
     ) {
         self.display_map.update(cx, |map, _| {
-            map.highlight_inlays(TypeId::of::<T>(), ranges, style)
+            map.highlight_inlays(TypeId::of::<T>(), highlights, style)
         });
         cx.notify();
     }
@@ -8051,15 +8175,15 @@ impl Editor {
     pub fn text_highlights<'a, T: 'static>(
         &'a self,
         cx: &'a AppContext,
-    ) -> Option<(HighlightStyle, &'a [DocumentRange])> {
+    ) -> Option<(HighlightStyle, &'a [Range<Anchor>])> {
         self.display_map.read(cx).text_highlights(TypeId::of::<T>())
     }
 
-    pub fn clear_text_highlights<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
-        let text_highlights = self
+    pub fn clear_highlights<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
+        let cleared = self
             .display_map
-            .update(cx, |map, _| map.clear_text_highlights(TypeId::of::<T>()));
-        if text_highlights.is_some() {
+            .update(cx, |map, _| map.clear_highlights(TypeId::of::<T>()));
+        if cleared {
             cx.notify();
         }
     }
@@ -8276,7 +8400,6 @@ impl Editor {
         Some(
             ranges
                 .iter()
-                .filter_map(|range| range.as_text_range())
                 .map(move |range| {
                     range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot)
                 })
@@ -8489,18 +8612,49 @@ impl Editor {
 
         self.handle_input(text, cx);
     }
+
+    pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool {
+        let Some(project) = self.project.as_ref() else {
+            return false;
+        };
+        let project = project.read(cx);
+
+        let mut supports = false;
+        self.buffer().read(cx).for_each_buffer(|buffer| {
+            if !supports {
+                supports = project
+                    .language_servers_for_buffer(buffer.read(cx), cx)
+                    .any(
+                        |(_, server)| match server.capabilities().inlay_hint_provider {
+                            Some(lsp::OneOf::Left(enabled)) => enabled,
+                            Some(lsp::OneOf::Right(_)) => true,
+                            None => false,
+                        },
+                    )
+            }
+        });
+        supports
+    }
 }
 
-fn document_to_inlay_range(
-    range: &DocumentRange,
-    snapshot: &DisplaySnapshot,
-) -> Range<InlayOffset> {
-    match range {
-        DocumentRange::Text(text_range) => {
-            snapshot.anchor_to_inlay_offset(text_range.start)
-                ..snapshot.anchor_to_inlay_offset(text_range.end)
-        }
-        DocumentRange::Inlay(inlay_range) => inlay_range.highlight_start..inlay_range.highlight_end,
+pub trait CollaborationHub {
+    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator>;
+    fn user_participant_indices<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> &'a HashMap<u64, ParticipantIndex>;
+}
+
+impl CollaborationHub for ModelHandle<Project> {
+    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+        self.read(cx).collaborators()
+    }
+
+    fn user_participant_indices<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> &'a HashMap<u64, ParticipantIndex> {
+        self.read(cx).user_store().read(cx).participant_indices()
     }
 }
 
@@ -8547,6 +8701,34 @@ fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot)
 }
 
 impl EditorSnapshot {
+    pub fn remote_selections_in_range<'a>(
+        &'a self,
+        range: &'a Range<Anchor>,
+        collaboration_hub: &dyn CollaborationHub,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = RemoteSelection> {
+        let participant_indices = collaboration_hub.user_participant_indices(cx);
+        let collaborators_by_peer_id = collaboration_hub.collaborators(cx);
+        let collaborators_by_replica_id = collaborators_by_peer_id
+            .iter()
+            .map(|(_, collaborator)| (collaborator.replica_id, collaborator))
+            .collect::<HashMap<_, _>>();
+        self.buffer_snapshot
+            .remote_selections_in_range(range)
+            .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| {
+                let collaborator = collaborators_by_replica_id.get(&replica_id)?;
+                let participant_index = participant_indices.get(&collaborator.user_id).copied();
+                Some(RemoteSelection {
+                    replica_id,
+                    selection,
+                    cursor_shape,
+                    line_mode,
+                    participant_index,
+                    peer_id: collaborator.peer_id,
+                })
+            })
+    }
+
     pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
         self.display_snapshot.buffer_snapshot.language_at(position)
     }
@@ -8605,9 +8787,6 @@ pub enum Event {
         local: bool,
         autoscroll: bool,
     },
-    TransactionUndone {
-        transaction_id: TransactionId,
-    },
     Closed,
 }
 
@@ -8663,7 +8842,7 @@ impl View for Editor {
             self.focused = true;
             self.buffer.update(cx, |buffer, cx| {
                 buffer.finalize_last_transaction(cx);
-                if self.leader_replica_id.is_none() {
+                if self.leader_peer_id.is_none() {
                     buffer.set_active_selections(
                         &self.selections.disjoint_anchors(),
                         self.selections.line_mode,

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

@@ -1429,7 +1429,7 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
         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.));
+        assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.));
         editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
         assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
     });
@@ -2792,6 +2792,34 @@ async fn test_manipulate_text(cx: &mut TestAppContext) {
         «hello worldˇ»
     "});
 
+    // Test multiple line, single selection case
+    // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
+    cx.set_state(indoc! {"
+        Β«The quick brown
+        fox jumps over
+        the lazy dogˇ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
+    cx.assert_editor_state(indoc! {"
+        Β«The Quick Brown
+        Fox Jumps Over
+        The Lazy Dogˇ»
+    "});
+
+    // Test multiple line, single selection case
+    // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
+    cx.set_state(indoc! {"
+        Β«The quick brown
+        fox jumps over
+        the lazy dogˇ»
+    "});
+    cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
+    cx.assert_editor_state(indoc! {"
+        Β«TheQuickBrown
+        FoxJumpsOver
+        TheLazyDogˇ»
+    "});
+
     // From here on out, test more complex cases of manipulate_text()
 
     // Test no selection case - should affect words cursors are in
@@ -3669,10 +3697,12 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
     let mut cx = EditorTestContext::new(cx).await;
     cx.set_state("abc\nˇabc abc\ndefabc\nabc");
 
-    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
     cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
 
-    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
     cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
 
     cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
@@ -3681,10 +3711,12 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
     cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
     cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
 
-    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
     cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
 
-    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
     cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
 }
 
@@ -3696,10 +3728,12 @@ async fn test_select_previous(cx: &mut gpui::TestAppContext) {
         let mut cx = EditorTestContext::new(cx).await;
         cx.set_state("abc\nˇabc abc\ndefabc\nabc");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
         cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
         cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
 
         cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
@@ -3708,10 +3742,12 @@ async fn test_select_previous(cx: &mut gpui::TestAppContext) {
         cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
         cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
         cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
         cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
     }
     {
@@ -3719,10 +3755,12 @@ async fn test_select_previous(cx: &mut gpui::TestAppContext) {
         let mut cx = EditorTestContext::new(cx).await;
         cx.set_state("abc\nΒ«Λ‡abcΒ» abc\ndefabc\nabc");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
         cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
         cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
 
         cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
@@ -3731,10 +3769,12 @@ async fn test_select_previous(cx: &mut gpui::TestAppContext) {
         cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
         cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
         cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
+        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+            .unwrap();
         cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
     }
 }

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

@@ -17,7 +17,6 @@ use crate::{
     },
     mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
 };
-use clock::ReplicaId;
 use collections::{BTreeMap, HashMap};
 use git::diff::DiffHunkStatus;
 use gpui::{
@@ -55,6 +54,7 @@ use std::{
     sync::Arc,
 };
 use text::Point;
+use theme::SelectionStyle;
 use workspace::item::Item;
 
 enum FoldMarkers {}
@@ -868,14 +868,7 @@ impl EditorElement {
         let corner_radius = 0.15 * layout.position_map.line_height;
         let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
 
-        for (replica_id, selections) in &layout.selections {
-            let replica_id = *replica_id;
-            let selection_style = if let Some(replica_id) = replica_id {
-                style.replica_selection_style(replica_id)
-            } else {
-                &style.absent_selection
-            };
-
+        for (selection_style, selections) in &layout.selections {
             for selection in selections {
                 self.paint_highlighted_range(
                     selection.range.clone(),
@@ -1735,7 +1728,7 @@ impl EditorElement {
                         enum JumpIcon {}
                         MouseEventHandler::new::<JumpIcon, _>((*id).into(), cx, |state, _| {
                             let style = style.jump_icon.style_for(state);
-                            Svg::new("icons/arrow_up_right_8.svg")
+                            Svg::new("icons/arrow_up_right.svg")
                                 .with_color(style.color)
                                 .constrained()
                                 .with_width(style.icon_width)
@@ -2193,7 +2186,7 @@ impl Element<Editor> for EditorElement {
                 .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
         };
 
-        let mut selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)> = Vec::new();
+        let mut selections: Vec<(SelectionStyle, Vec<SelectionLayout>)> = Vec::new();
         let mut active_rows = BTreeMap::new();
         let mut fold_ranges = Vec::new();
         let is_singleton = editor.is_singleton(cx);
@@ -2219,35 +2212,6 @@ impl Element<Editor> for EditorElement {
                 }),
         );
 
-        let mut remote_selections = HashMap::default();
-        for (replica_id, line_mode, cursor_shape, selection) in snapshot
-            .buffer_snapshot
-            .remote_selections_in_range(&(start_anchor..end_anchor))
-        {
-            let replica_id = if let Some(mapping) = &editor.replica_id_mapping {
-                mapping.get(&replica_id).copied()
-            } else {
-                Some(replica_id)
-            };
-
-            // The local selections match the leader's selections.
-            if replica_id.is_some() && replica_id == editor.leader_replica_id {
-                continue;
-            }
-            remote_selections
-                .entry(replica_id)
-                .or_insert(Vec::new())
-                .push(SelectionLayout::new(
-                    selection,
-                    line_mode,
-                    cursor_shape,
-                    &snapshot.display_snapshot,
-                    false,
-                    false,
-                ));
-        }
-        selections.extend(remote_selections);
-
         let mut newest_selection_head = None;
 
         if editor.show_local_selections {
@@ -2282,19 +2246,58 @@ impl Element<Editor> for EditorElement {
                 layouts.push(layout);
             }
 
-            // Render the local selections in the leader's color when following.
-            let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id {
-                leader_replica_id
-            } else {
-                let replica_id = editor.replica_id(cx);
-                if let Some(mapping) = &editor.replica_id_mapping {
-                    mapping.get(&replica_id).copied().unwrap_or(replica_id)
+            selections.push((style.selection, layouts));
+        }
+
+        if let Some(collaboration_hub) = &editor.collaboration_hub {
+            // When following someone, render the local selections in their color.
+            if let Some(leader_id) = editor.leader_peer_id {
+                if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
+                    if let Some(participant_index) = collaboration_hub
+                        .user_participant_indices(cx)
+                        .get(&collaborator.user_id)
+                    {
+                        if let Some((local_selection_style, _)) = selections.first_mut() {
+                            *local_selection_style =
+                                style.selection_style_for_room_participant(participant_index.0);
+                        }
+                    }
+                }
+            }
+
+            let mut remote_selections = HashMap::default();
+            for selection in snapshot.remote_selections_in_range(
+                &(start_anchor..end_anchor),
+                collaboration_hub.as_ref(),
+                cx,
+            ) {
+                let selection_style = if let Some(participant_index) = selection.participant_index {
+                    style.selection_style_for_room_participant(participant_index.0)
                 } else {
-                    replica_id
+                    style.absent_selection
+                };
+
+                // Don't re-render the leader's selections, since the local selections
+                // match theirs.
+                if Some(selection.peer_id) == editor.leader_peer_id {
+                    continue;
                 }
-            };
 
-            selections.push((Some(local_replica_id), layouts));
+                remote_selections
+                    .entry(selection.replica_id)
+                    .or_insert((selection_style, Vec::new()))
+                    .1
+                    .push(SelectionLayout::new(
+                        selection.selection,
+                        selection.line_mode,
+                        selection.cursor_shape,
+                        &snapshot.display_snapshot,
+                        false,
+                        false,
+                    ));
+            }
+
+            selections.extend(remote_selections.into_values());
         }
 
         let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
@@ -2686,7 +2689,7 @@ pub struct LayoutState {
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
     fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
-    selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)>,
+    selections: Vec<(SelectionStyle, Vec<SelectionLayout>)>,
     scrollbar_row_range: Range<f32>,
     show_scrollbars: bool,
     is_singleton: bool,

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

@@ -1,6 +1,6 @@
 use crate::{
     display_map::{InlayOffset, ToDisplayPoint},
-    link_go_to_definition::{DocumentRange, InlayRange},
+    link_go_to_definition::{InlayHighlight, RangeInEditor},
     Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
     ExcerptId, RangeToAnchorExt,
 };
@@ -8,12 +8,12 @@ use futures::FutureExt;
 use gpui::{
     actions,
     elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
-    fonts::{HighlightStyle, Underline, Weight},
     platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
+    AnyElement, AppContext, Element, ModelHandle, Task, ViewContext,
 };
 use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
 use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
+use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText};
 use std::{ops::Range, sync::Arc, time::Duration};
 use util::TryFutureExt;
 
@@ -50,19 +50,18 @@ pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewC
 
 pub struct InlayHover {
     pub excerpt: ExcerptId,
-    pub triggered_from: InlayOffset,
-    pub range: InlayRange,
+    pub range: InlayHighlight,
     pub tooltip: HoverBlock,
 }
 
 pub fn find_hovered_hint_part(
     label_parts: Vec<InlayHintLabelPart>,
-    hint_range: Range<InlayOffset>,
+    hint_start: InlayOffset,
     hovered_offset: InlayOffset,
 ) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
-    if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end {
-        let mut hovered_character = (hovered_offset - hint_range.start).0;
-        let mut part_start = hint_range.start;
+    if hovered_offset >= hint_start {
+        let mut hovered_character = (hovered_offset - hint_start).0;
+        let mut part_start = hint_start;
         for part in label_parts {
             let part_len = part.value.chars().count();
             if hovered_character > part_len {
@@ -88,10 +87,8 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
         };
 
         if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
-            if let DocumentRange::Inlay(range) = symbol_range {
-                if (range.highlight_start..range.highlight_end)
-                    .contains(&inlay_hover.triggered_from)
-                {
+            if let RangeInEditor::Inlay(range) = symbol_range {
+                if range == &inlay_hover.range {
                     // Hover triggered from same location as last time. Don't show again.
                     return;
                 }
@@ -99,18 +96,6 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
             hide_hover(editor, cx);
         }
 
-        let snapshot = editor.snapshot(cx);
-        // Don't request again if the location is the same as the previous request
-        if let Some(triggered_from) = editor.hover_state.triggered_from {
-            if inlay_hover.triggered_from
-                == snapshot
-                    .display_snapshot
-                    .anchor_to_inlay_offset(triggered_from)
-            {
-                return;
-            }
-        }
-
         let task = cx.spawn(|this, mut cx| {
             async move {
                 cx.background()
@@ -122,7 +107,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
 
                 let hover_popover = InfoPopover {
                     project: project.clone(),
-                    symbol_range: DocumentRange::Inlay(inlay_hover.range),
+                    symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
                     blocks: vec![inlay_hover.tooltip],
                     language: None,
                     rendered_content: None,
@@ -326,7 +311,7 @@ fn show_hover(
 
                 Some(InfoPopover {
                     project: project.clone(),
-                    symbol_range: DocumentRange::Text(range),
+                    symbol_range: RangeInEditor::Text(range),
                     blocks: hover_result.contents,
                     language: hover_result.language,
                     rendered_content: None,
@@ -361,158 +346,25 @@ fn show_hover(
 }
 
 fn render_blocks(
-    theme_id: usize,
     blocks: &[HoverBlock],
     language_registry: &Arc<LanguageRegistry>,
     language: Option<&Arc<Language>>,
-    style: &EditorStyle,
-) -> RenderedInfo {
-    let mut text = String::new();
-    let mut highlights = Vec::new();
-    let mut region_ranges = Vec::new();
-    let mut regions = Vec::new();
+) -> RichText {
+    let mut data = RichText {
+        text: Default::default(),
+        highlights: Default::default(),
+        region_ranges: Default::default(),
+        regions: Default::default(),
+    };
 
     for block in blocks {
         match &block.kind {
             HoverBlockKind::PlainText => {
-                new_paragraph(&mut text, &mut Vec::new());
-                text.push_str(&block.text);
+                new_paragraph(&mut data.text, &mut Vec::new());
+                data.text.push_str(&block.text);
             }
             HoverBlockKind::Markdown => {
-                use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
-
-                let mut bold_depth = 0;
-                let mut italic_depth = 0;
-                let mut link_url = None;
-                let mut current_language = None;
-                let mut list_stack = Vec::new();
-
-                for event in Parser::new_ext(&block.text, Options::all()) {
-                    let prev_len = text.len();
-                    match event {
-                        Event::Text(t) => {
-                            if let Some(language) = &current_language {
-                                render_code(
-                                    &mut text,
-                                    &mut highlights,
-                                    t.as_ref(),
-                                    language,
-                                    style,
-                                );
-                            } else {
-                                text.push_str(t.as_ref());
-
-                                let mut style = HighlightStyle::default();
-                                if bold_depth > 0 {
-                                    style.weight = Some(Weight::BOLD);
-                                }
-                                if italic_depth > 0 {
-                                    style.italic = Some(true);
-                                }
-                                if let Some(link_url) = link_url.clone() {
-                                    region_ranges.push(prev_len..text.len());
-                                    regions.push(RenderedRegion {
-                                        link_url: Some(link_url),
-                                        code: false,
-                                    });
-                                    style.underline = Some(Underline {
-                                        thickness: 1.0.into(),
-                                        ..Default::default()
-                                    });
-                                }
-
-                                if style != HighlightStyle::default() {
-                                    let mut new_highlight = true;
-                                    if let Some((last_range, last_style)) = highlights.last_mut() {
-                                        if last_range.end == prev_len && last_style == &style {
-                                            last_range.end = text.len();
-                                            new_highlight = false;
-                                        }
-                                    }
-                                    if new_highlight {
-                                        highlights.push((prev_len..text.len(), style));
-                                    }
-                                }
-                            }
-                        }
-                        Event::Code(t) => {
-                            text.push_str(t.as_ref());
-                            region_ranges.push(prev_len..text.len());
-                            if link_url.is_some() {
-                                highlights.push((
-                                    prev_len..text.len(),
-                                    HighlightStyle {
-                                        underline: Some(Underline {
-                                            thickness: 1.0.into(),
-                                            ..Default::default()
-                                        }),
-                                        ..Default::default()
-                                    },
-                                ));
-                            }
-                            regions.push(RenderedRegion {
-                                code: true,
-                                link_url: link_url.clone(),
-                            });
-                        }
-                        Event::Start(tag) => match tag {
-                            Tag::Paragraph => new_paragraph(&mut text, &mut list_stack),
-                            Tag::Heading(_, _, _) => {
-                                new_paragraph(&mut text, &mut list_stack);
-                                bold_depth += 1;
-                            }
-                            Tag::CodeBlock(kind) => {
-                                new_paragraph(&mut text, &mut list_stack);
-                                current_language = if let CodeBlockKind::Fenced(language) = kind {
-                                    language_registry
-                                        .language_for_name(language.as_ref())
-                                        .now_or_never()
-                                        .and_then(Result::ok)
-                                } else {
-                                    language.cloned()
-                                }
-                            }
-                            Tag::Emphasis => italic_depth += 1,
-                            Tag::Strong => bold_depth += 1,
-                            Tag::Link(_, url, _) => link_url = Some(url.to_string()),
-                            Tag::List(number) => {
-                                list_stack.push((number, false));
-                            }
-                            Tag::Item => {
-                                let len = list_stack.len();
-                                if let Some((list_number, has_content)) = list_stack.last_mut() {
-                                    *has_content = false;
-                                    if !text.is_empty() && !text.ends_with('\n') {
-                                        text.push('\n');
-                                    }
-                                    for _ in 0..len - 1 {
-                                        text.push_str("  ");
-                                    }
-                                    if let Some(number) = list_number {
-                                        text.push_str(&format!("{}. ", number));
-                                        *number += 1;
-                                        *has_content = false;
-                                    } else {
-                                        text.push_str("- ");
-                                    }
-                                }
-                            }
-                            _ => {}
-                        },
-                        Event::End(tag) => match tag {
-                            Tag::Heading(_, _, _) => bold_depth -= 1,
-                            Tag::CodeBlock(_) => current_language = None,
-                            Tag::Emphasis => italic_depth -= 1,
-                            Tag::Strong => bold_depth -= 1,
-                            Tag::Link(_, _, _) => link_url = None,
-                            Tag::List(_) => drop(list_stack.pop()),
-                            _ => {}
-                        },
-                        Event::HardBreak => text.push('\n'),
-                        Event::SoftBreak => text.push(' '),
-                        _ => {}
-                    }
-                }
+                render_markdown_mut(&block.text, language_registry, language, &mut data)
             }
             HoverBlockKind::Code { language } => {
                 if let Some(language) = language_registry
@@ -520,62 +372,17 @@ fn render_blocks(
                     .now_or_never()
                     .and_then(Result::ok)
                 {
-                    render_code(&mut text, &mut highlights, &block.text, &language, style);
+                    render_code(&mut data.text, &mut data.highlights, &block.text, &language);
                 } else {
-                    text.push_str(&block.text);
+                    data.text.push_str(&block.text);
                 }
             }
         }
     }
 
-    RenderedInfo {
-        theme_id,
-        text: text.trim().to_string(),
-        highlights,
-        region_ranges,
-        regions,
-    }
-}
+    data.text = data.text.trim().to_string();
 
-fn render_code(
-    text: &mut String,
-    highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
-    content: &str,
-    language: &Arc<Language>,
-    style: &EditorStyle,
-) {
-    let prev_len = text.len();
-    text.push_str(content);
-    for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
-        if let Some(style) = highlight_id.style(&style.syntax) {
-            highlights.push((prev_len + range.start..prev_len + range.end, style));
-        }
-    }
-}
-
-fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
-    let mut is_subsequent_paragraph_of_list = false;
-    if let Some((_, has_content)) = list_stack.last_mut() {
-        if *has_content {
-            is_subsequent_paragraph_of_list = true;
-        } else {
-            *has_content = true;
-            return;
-        }
-    }
-
-    if !text.is_empty() {
-        if !text.ends_with('\n') {
-            text.push('\n');
-        }
-        text.push('\n');
-    }
-    for _ in 0..list_stack.len().saturating_sub(1) {
-        text.push_str("  ");
-    }
-    if is_subsequent_paragraph_of_list {
-        text.push_str("  ");
-    }
+    data
 }
 
 #[derive(Default)]
@@ -608,8 +415,8 @@ impl HoverState {
                 self.info_popover
                     .as_ref()
                     .map(|info_popover| match &info_popover.symbol_range {
-                        DocumentRange::Text(range) => &range.start,
-                        DocumentRange::Inlay(range) => &range.inlay_position,
+                        RangeInEditor::Text(range) => &range.start,
+                        RangeInEditor::Inlay(range) => &range.inlay_position,
                     })
             })?;
         let point = anchor.to_display_point(&snapshot.display_snapshot);
@@ -635,25 +442,10 @@ impl HoverState {
 #[derive(Debug, Clone)]
 pub struct InfoPopover {
     pub project: ModelHandle<Project>,
-    symbol_range: DocumentRange,
+    symbol_range: RangeInEditor,
     pub blocks: Vec<HoverBlock>,
     language: Option<Arc<Language>>,
-    rendered_content: Option<RenderedInfo>,
-}
-
-#[derive(Debug, Clone)]
-struct RenderedInfo {
-    theme_id: usize,
-    text: String,
-    highlights: Vec<(Range<usize>, HighlightStyle)>,
-    region_ranges: Vec<Range<usize>>,
-    regions: Vec<RenderedRegion>,
-}
-
-#[derive(Debug, Clone)]
-struct RenderedRegion {
-    code: bool,
-    link_url: Option<String>,
+    rendered_content: Option<RichText>,
 }
 
 impl InfoPopover {
@@ -662,63 +454,24 @@ impl InfoPopover {
         style: &EditorStyle,
         cx: &mut ViewContext<Editor>,
     ) -> AnyElement<Editor> {
-        if let Some(rendered) = &self.rendered_content {
-            if rendered.theme_id != style.theme_id {
-                self.rendered_content = None;
-            }
-        }
-
         let rendered_content = self.rendered_content.get_or_insert_with(|| {
             render_blocks(
-                style.theme_id,
                 &self.blocks,
                 self.project.read(cx).languages(),
                 self.language.as_ref(),
-                style,
             )
         });
 
-        MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
-            let mut region_id = 0;
-            let view_id = cx.view_id();
-
+        MouseEventHandler::new::<InfoPopover, _>(0, cx, move |_, cx| {
             let code_span_background_color = style.document_highlight_read_background;
-            let regions = rendered_content.regions.clone();
             Flex::column()
                 .scrollable::<HoverBlock>(1, None, cx)
-                .with_child(
-                    Text::new(rendered_content.text.clone(), style.text.clone())
-                        .with_highlights(rendered_content.highlights.clone())
-                        .with_custom_runs(
-                            rendered_content.region_ranges.clone(),
-                            move |ix, bounds, cx| {
-                                region_id += 1;
-                                let region = regions[ix].clone();
-                                if let Some(url) = region.link_url {
-                                    cx.scene().push_cursor_region(CursorRegion {
-                                        bounds,
-                                        style: CursorStyle::PointingHand,
-                                    });
-                                    cx.scene().push_mouse_region(
-                                        MouseRegion::new::<Self>(view_id, region_id, bounds)
-                                            .on_click::<Editor, _>(
-                                                MouseButton::Left,
-                                                move |_, _, cx| cx.platform().open_url(&url),
-                                            ),
-                                    );
-                                }
-                                if region.code {
-                                    cx.scene().push_quad(gpui::Quad {
-                                        bounds,
-                                        background: Some(code_span_background_color),
-                                        border: Default::default(),
-                                        corner_radii: (2.0).into(),
-                                    });
-                                }
-                            },
-                        )
-                        .with_soft_wrap(true),
-                )
+                .with_child(rendered_content.element(
+                    style.syntax.clone(),
+                    style.text.clone(),
+                    code_span_background_color,
+                    cx,
+                ))
                 .contained()
                 .with_style(style.hover_popover.container)
         })
@@ -811,13 +564,15 @@ mod tests {
         inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
         link_go_to_definition::update_inlay_link_and_hover_points,
         test::editor_lsp_test_context::EditorLspTestContext,
+        InlayId,
     };
     use collections::BTreeSet;
-    use gpui::fonts::Weight;
+    use gpui::fonts::{HighlightStyle, Underline, Weight};
     use indoc::indoc;
     use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
     use lsp::LanguageServerId;
     use project::{HoverBlock, HoverBlockKind};
+    use rich_text::Highlight;
     use smol::stream::StreamExt;
     use unindent::Unindent;
     use util::test::marked_text_ranges;
@@ -1028,7 +783,7 @@ mod tests {
         .await;
 
         cx.condition(|editor, _| editor.hover_state.visible()).await;
-        cx.editor(|editor, cx| {
+        cx.editor(|editor, _| {
             let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
             assert_eq!(
                 blocks,
@@ -1038,8 +793,7 @@ mod tests {
                 }],
             );
 
-            let style = editor.style(cx);
-            let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
+            let rendered = render_blocks(&blocks, &Default::default(), None);
             assert_eq!(
                 rendered.text,
                 code_str.trim(),
@@ -1231,7 +985,7 @@ mod tests {
                 expected_styles,
             } in &rows[0..]
             {
-                let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
+                let rendered = render_blocks(&blocks, &Default::default(), None);
 
                 let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
                 let expected_highlights = ranges
@@ -1242,8 +996,21 @@ mod tests {
                     rendered.text, expected_text,
                     "wrong text for input {blocks:?}"
                 );
+
+                let rendered_highlights: Vec<_> = rendered
+                    .highlights
+                    .iter()
+                    .filter_map(|(range, highlight)| {
+                        let style = match highlight {
+                            Highlight::Id(id) => id.style(&style.syntax)?,
+                            Highlight::Highlight(style) => style.clone(),
+                        };
+                        Some((range.clone(), style))
+                    })
+                    .collect();
+
                 assert_eq!(
-                    rendered.highlights, expected_highlights,
+                    rendered_highlights, expected_highlights,
                     "wrong highlights for input {blocks:?}"
                 );
             }
@@ -1477,25 +1244,16 @@ mod tests {
             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
         cx.foreground().run_until_parked();
         cx.update_editor(|editor, cx| {
-            let snapshot = editor.snapshot(cx);
             let hover_state = &editor.hover_state;
             assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
             let popover = hover_state.info_popover.as_ref().unwrap();
             let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-            let entire_inlay_start = snapshot.display_point_to_inlay_offset(
-                inlay_range.start.to_display_point(&snapshot),
-                Bias::Left,
-            );
-
-            let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len());
             assert_eq!(
                 popover.symbol_range,
-                DocumentRange::Inlay(InlayRange {
+                RangeInEditor::Inlay(InlayHighlight {
+                    inlay: InlayId::Hint(0),
                     inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-                    highlight_start: expected_new_type_label_start,
-                    highlight_end: InlayOffset(
-                        expected_new_type_label_start.0 + new_type_label.len()
-                    ),
+                    range: ": ".len()..": ".len() + new_type_label.len(),
                 }),
                 "Popover range should match the new type label part"
             );
@@ -1543,23 +1301,17 @@ mod tests {
             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
         cx.foreground().run_until_parked();
         cx.update_editor(|editor, cx| {
-            let snapshot = editor.snapshot(cx);
             let hover_state = &editor.hover_state;
             assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
             let popover = hover_state.info_popover.as_ref().unwrap();
             let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-            let entire_inlay_start = snapshot.display_point_to_inlay_offset(
-                inlay_range.start.to_display_point(&snapshot),
-                Bias::Left,
-            );
-            let expected_struct_label_start =
-                InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len());
             assert_eq!(
                 popover.symbol_range,
-                DocumentRange::Inlay(InlayRange {
+                RangeInEditor::Inlay(InlayHighlight {
+                    inlay: InlayId::Hint(0),
                     inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-                    highlight_start: expected_struct_label_start,
-                    highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()),
+                    range: ": ".len() + new_type_label.len() + "<".len()
+                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
                 }),
                 "Popover range should match the struct label part"
             );

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

@@ -43,7 +43,8 @@ pub struct CachedExcerptHints {
     version: usize,
     buffer_version: Global,
     buffer_id: u64,
-    hints: Vec<(InlayId, InlayHint)>,
+    ordered_hints: Vec<InlayId>,
+    hints_by_id: HashMap<InlayId, InlayHint>,
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -316,7 +317,7 @@ impl InlayHintCache {
             self.hints.retain(|cached_excerpt, cached_hints| {
                 let retain = excerpts_to_query.contains_key(cached_excerpt);
                 if !retain {
-                    invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id));
+                    invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied());
                 }
                 retain
             });
@@ -384,7 +385,7 @@ impl InlayHintCache {
             let shown_excerpt_hints_to_remove =
                 shown_hints_to_remove.entry(*excerpt_id).or_default();
             let excerpt_cached_hints = excerpt_cached_hints.read();
-            let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable();
+            let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
             shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
                 let Some(buffer) = shown_anchor
                     .buffer_id
@@ -395,7 +396,8 @@ impl InlayHintCache {
                 let buffer_snapshot = buffer.read(cx).snapshot();
                 loop {
                     match excerpt_cache.peek() {
-                        Some((cached_hint_id, cached_hint)) => {
+                        Some(&cached_hint_id) => {
+                            let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
                             if cached_hint_id == shown_hint_id {
                                 excerpt_cache.next();
                                 return !new_kinds.contains(&cached_hint.kind);
@@ -428,7 +430,8 @@ impl InlayHintCache {
                 }
             });
 
-            for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
+            for cached_hint_id in excerpt_cache {
+                let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
                 let cached_hint_kind = maybe_missed_cached_hint.kind;
                 if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
                     to_insert.push(Inlay::hint(
@@ -463,7 +466,7 @@ impl InlayHintCache {
             self.update_tasks.remove(&excerpt_to_remove);
             if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) {
                 let cached_hints = cached_hints.read();
-                to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id));
+                to_remove.extend(cached_hints.ordered_hints.iter().copied());
             }
         }
         if to_remove.is_empty() {
@@ -489,10 +492,8 @@ impl InlayHintCache {
         self.hints
             .get(&excerpt_id)?
             .read()
-            .hints
-            .iter()
-            .find(|&(id, _)| id == &hint_id)
-            .map(|(_, hint)| hint)
+            .hints_by_id
+            .get(&hint_id)
             .cloned()
     }
 
@@ -500,7 +501,13 @@ impl InlayHintCache {
         let mut hints = Vec::new();
         for excerpt_hints in self.hints.values() {
             let excerpt_hints = excerpt_hints.read();
-            hints.extend(excerpt_hints.hints.iter().map(|(_, hint)| hint).cloned());
+            hints.extend(
+                excerpt_hints
+                    .ordered_hints
+                    .iter()
+                    .map(|id| &excerpt_hints.hints_by_id[id])
+                    .cloned(),
+            );
         }
         hints
     }
@@ -518,12 +525,7 @@ impl InlayHintCache {
     ) {
         if let Some(excerpt_hints) = self.hints.get(&excerpt_id) {
             let mut guard = excerpt_hints.write();
-            if let Some(cached_hint) = guard
-                .hints
-                .iter_mut()
-                .find(|(hint_id, _)| hint_id == &id)
-                .map(|(_, hint)| hint)
-            {
+            if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
                 if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state {
                     let hint_to_resolve = cached_hint.clone();
                     let server_id = *server_id;
@@ -555,12 +557,7 @@ impl InlayHintCache {
                                     editor.inlay_hint_cache.hints.get(&excerpt_id)
                                 {
                                     let mut guard = excerpt_hints.write();
-                                    if let Some(cached_hint) = guard
-                                        .hints
-                                        .iter_mut()
-                                        .find(|(hint_id, _)| hint_id == &id)
-                                        .map(|(_, hint)| hint)
-                                    {
+                                    if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
                                         if cached_hint.resolve_state == ResolveState::Resolving {
                                             resolved_hint.resolve_state = ResolveState::Resolved;
                                             *cached_hint = resolved_hint;
@@ -944,7 +941,7 @@ async fn fetch_and_update_hints(
         })
         .await;
     if let Some(new_update) = new_update {
-        log::info!(
+        log::debug!(
             "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
             new_update.remove_from_visible.len(),
             new_update.remove_from_cache.len(),
@@ -986,12 +983,17 @@ fn calculate_hint_updates(
         let missing_from_cache = match &cached_excerpt_hints {
             Some(cached_excerpt_hints) => {
                 let cached_excerpt_hints = cached_excerpt_hints.read();
-                match cached_excerpt_hints.hints.binary_search_by(|probe| {
-                    probe.1.position.cmp(&new_hint.position, buffer_snapshot)
-                }) {
+                match cached_excerpt_hints
+                    .ordered_hints
+                    .binary_search_by(|probe| {
+                        cached_excerpt_hints.hints_by_id[probe]
+                            .position
+                            .cmp(&new_hint.position, buffer_snapshot)
+                    }) {
                     Ok(ix) => {
                         let mut missing_from_cache = true;
-                        for (cached_inlay_id, cached_hint) in &cached_excerpt_hints.hints[ix..] {
+                        for id in &cached_excerpt_hints.ordered_hints[ix..] {
+                            let cached_hint = &cached_excerpt_hints.hints_by_id[id];
                             if new_hint
                                 .position
                                 .cmp(&cached_hint.position, buffer_snapshot)
@@ -1000,7 +1002,7 @@ fn calculate_hint_updates(
                                 break;
                             }
                             if cached_hint == &new_hint {
-                                excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
+                                excerpt_hints_to_persist.insert(*id, cached_hint.kind);
                                 missing_from_cache = false;
                             }
                         }
@@ -1031,12 +1033,12 @@ fn calculate_hint_updates(
             let cached_excerpt_hints = cached_excerpt_hints.read();
             remove_from_cache.extend(
                 cached_excerpt_hints
-                    .hints
+                    .ordered_hints
                     .iter()
-                    .filter(|(cached_inlay_id, _)| {
+                    .filter(|cached_inlay_id| {
                         !excerpt_hints_to_persist.contains_key(cached_inlay_id)
                     })
-                    .map(|(cached_inlay_id, _)| *cached_inlay_id),
+                    .copied(),
             );
         }
     }
@@ -1080,7 +1082,8 @@ fn apply_hint_update(
                 version: query.cache_version,
                 buffer_version: buffer_snapshot.version().clone(),
                 buffer_id: query.buffer_id,
-                hints: Vec::new(),
+                ordered_hints: Vec::new(),
+                hints_by_id: HashMap::default(),
             }))
         });
     let mut cached_excerpt_hints = cached_excerpt_hints.write();
@@ -1093,20 +1096,27 @@ fn apply_hint_update(
 
     let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
     cached_excerpt_hints
-        .hints
-        .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
+        .ordered_hints
+        .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id));
+    cached_excerpt_hints
+        .hints_by_id
+        .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
     let mut splice = InlaySplice {
         to_remove: new_update.remove_from_visible,
         to_insert: Vec::new(),
     };
     for new_hint in new_update.add_to_cache {
-        let cached_hints = &mut cached_excerpt_hints.hints;
-        let insert_position = match cached_hints
-            .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot))
-        {
+        let insert_position = match cached_excerpt_hints
+            .ordered_hints
+            .binary_search_by(|probe| {
+                cached_excerpt_hints.hints_by_id[probe]
+                    .position
+                    .cmp(&new_hint.position, &buffer_snapshot)
+            }) {
             Ok(i) => {
                 let mut insert_position = Some(i);
-                for (_, cached_hint) in &cached_hints[i..] {
+                for id in &cached_excerpt_hints.ordered_hints[i..] {
+                    let cached_hint = &cached_excerpt_hints.hints_by_id[id];
                     if new_hint
                         .position
                         .cmp(&cached_hint.position, &buffer_snapshot)
@@ -1137,7 +1147,11 @@ fn apply_hint_update(
                     .to_insert
                     .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
             }
-            cached_hints.insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint));
+            let new_id = InlayId::Hint(new_inlay_id);
+            cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
+            cached_excerpt_hints
+                .ordered_hints
+                .insert(insert_position, new_id);
             cached_inlays_changed = true;
         }
     }
@@ -1157,7 +1171,7 @@ fn apply_hint_update(
                 outdated_excerpt_caches.insert(*excerpt_id);
                 splice
                     .to_remove
-                    .extend(excerpt_hints.hints.iter().map(|(id, _)| id));
+                    .extend(excerpt_hints.ordered_hints.iter().copied());
             }
         }
         cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
@@ -3311,8 +3325,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
     pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
         let mut labels = Vec::new();
         for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
-            for (_, inlay) in &excerpt_hints.read().hints {
-                labels.push(inlay.text());
+            let excerpt_hints = excerpt_hints.read();
+            for id in &excerpt_hints.ordered_hints {
+                labels.push(excerpt_hints.hints_by_id[id].text());
             }
         }
 

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

@@ -16,8 +16,8 @@ use language::{
     proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
     SelectionGoal,
 };
-use project::{FormatTrigger, Item as _, Project, ProjectPath};
-use rpc::proto::{self, update_view};
+use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
+use rpc::proto::{self, update_view, PeerId};
 use smallvec::SmallVec;
 use std::{
     borrow::Cow,
@@ -26,6 +26,7 @@ use std::{
     iter,
     ops::Range,
     path::{Path, PathBuf},
+    sync::Arc,
 };
 use text::Selection;
 use util::{
@@ -155,13 +156,9 @@ impl FollowableItem for Editor {
         }))
     }
 
-    fn set_leader_replica_id(
-        &mut self,
-        leader_replica_id: Option<u16>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.leader_replica_id = leader_replica_id;
-        if self.leader_replica_id.is_some() {
+    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
+        self.leader_peer_id = leader_peer_id;
+        if self.leader_peer_id.is_some() {
             self.buffer.update(cx, |buffer, cx| {
                 buffer.remove_active_selections(cx);
             });
@@ -308,6 +305,10 @@ impl FollowableItem for Editor {
             _ => false,
         }
     }
+
+    fn is_project_item(&self, _cx: &AppContext) -> bool {
+        true
+    }
 }
 
 async fn update_editor_from_message(
@@ -978,7 +979,28 @@ impl SearchableItem for Editor {
         }
         self.change_selections(None, cx, |s| s.select_ranges(ranges));
     }
+    fn replace(
+        &mut self,
+        identifier: &Self::Match,
+        query: &SearchQuery,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let text = self.buffer.read(cx);
+        let text = text.snapshot(cx);
+        let text = text.text_for_range(identifier.clone()).collect::<Vec<_>>();
+        let text: Cow<_> = if text.len() == 1 {
+            text.first().cloned().unwrap().into()
+        } else {
+            let joined_chunks = text.join("");
+            joined_chunks.into()
+        };
 
+        if let Some(replacement) = query.replacement_for(&text) {
+            self.transact(cx, |this, cx| {
+                this.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
+            });
+        }
+    }
     fn match_index_for_direction(
         &mut self,
         matches: &Vec<Range<Anchor>>,
@@ -1030,7 +1052,7 @@ impl SearchableItem for Editor {
 
     fn find_matches(
         &mut self,
-        query: project::search::SearchQuery,
+        query: Arc<project::search::SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Range<Anchor>>> {
         let buffer = self.buffer().read(cx).snapshot(cx);
@@ -1,8 +1,8 @@
 use crate::{
-    display_map::{DisplaySnapshot, InlayOffset},
+    display_map::DisplaySnapshot,
     element::PointForPosition,
     hover_popover::{self, InlayHover},
-    Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase,
+    Anchor, DisplayPoint, Editor, EditorSnapshot, InlayId, SelectPhase,
 };
 use gpui::{Task, ViewContext};
 use language::{Bias, ToOffset};
@@ -17,44 +17,19 @@ use util::TryFutureExt;
 #[derive(Debug, Default)]
 pub struct LinkGoToDefinitionState {
     pub last_trigger_point: Option<TriggerPoint>,
-    pub symbol_range: Option<DocumentRange>,
+    pub symbol_range: Option<RangeInEditor>,
     pub kind: Option<LinkDefinitionKind>,
     pub definitions: Vec<GoToDefinitionLink>,
     pub task: Option<Task<Option<()>>>,
 }
 
-#[derive(Debug)]
-pub enum GoToDefinitionTrigger {
-    Text(DisplayPoint),
-    InlayHint(InlayRange, lsp::Location, LanguageServerId),
-}
-
-#[derive(Debug, Clone)]
-pub enum GoToDefinitionLink {
-    Text(LocationLink),
-    InlayHint(lsp::Location, LanguageServerId),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct InlayRange {
-    pub inlay_position: Anchor,
-    pub highlight_start: InlayOffset,
-    pub highlight_end: InlayOffset,
-}
-
-#[derive(Debug, Clone)]
-pub enum TriggerPoint {
-    Text(Anchor),
-    InlayHint(InlayRange, lsp::Location, LanguageServerId),
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum DocumentRange {
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub enum RangeInEditor {
     Text(Range<Anchor>),
-    Inlay(InlayRange),
+    Inlay(InlayHighlight),
 }
 
-impl DocumentRange {
+impl RangeInEditor {
     pub fn as_text_range(&self) -> Option<Range<Anchor>> {
         match self {
             Self::Text(range) => Some(range.clone()),
@@ -64,28 +39,47 @@ impl DocumentRange {
 
     fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
         match (self, trigger_point) {
-            (DocumentRange::Text(range), TriggerPoint::Text(point)) => {
+            (Self::Text(range), TriggerPoint::Text(point)) => {
                 let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
                 point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge()
             }
-            (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _, _)) => {
-                range.highlight_start.cmp(&point.highlight_end).is_le()
-                    && range.highlight_end.cmp(&point.highlight_end).is_ge()
+            (Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => {
+                highlight.inlay == point.inlay
+                    && highlight.range.contains(&point.range.start)
+                    && highlight.range.contains(&point.range.end)
             }
-            (DocumentRange::Inlay(_), TriggerPoint::Text(_))
-            | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
+            (Self::Inlay(_), TriggerPoint::Text(_))
+            | (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
         }
     }
 }
 
-impl TriggerPoint {
-    fn anchor(&self) -> &Anchor {
-        match self {
-            TriggerPoint::Text(anchor) => anchor,
-            TriggerPoint::InlayHint(range, _, _) => &range.inlay_position,
-        }
-    }
+#[derive(Debug)]
+pub enum GoToDefinitionTrigger {
+    Text(DisplayPoint),
+    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
+}
+
+#[derive(Debug, Clone)]
+pub enum GoToDefinitionLink {
+    Text(LocationLink),
+    InlayHint(lsp::Location, LanguageServerId),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct InlayHighlight {
+    pub inlay: InlayId,
+    pub inlay_position: Anchor,
+    pub range: Range<usize>,
+}
+
+#[derive(Debug, Clone)]
+pub enum TriggerPoint {
+    Text(Anchor),
+    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
+}
 
+impl TriggerPoint {
     pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind {
         match self {
             TriggerPoint::Text(_) => {
@@ -98,6 +92,13 @@ impl TriggerPoint {
             TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type,
         }
     }
+
+    fn anchor(&self) -> &Anchor {
+        match self {
+            TriggerPoint::Text(anchor) => anchor,
+            TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position,
+        }
+    }
 }
 
 pub fn update_go_to_definition_link(
@@ -135,11 +136,7 @@ pub fn update_go_to_definition_link(
                 }
             }
             (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => {
-                if range_a
-                    .inlay_position
-                    .cmp(&range_b.inlay_position, &snapshot.buffer_snapshot)
-                    .is_eq()
-                {
+                if range_a == range_b {
                     return;
                 }
             }
@@ -173,10 +170,6 @@ pub fn update_inlay_link_and_hover_points(
     shift_held: bool,
     cx: &mut ViewContext<'_, '_, Editor>,
 ) {
-    let hint_start_offset =
-        snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left);
-    let hint_end_offset =
-        snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right);
     let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
         Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
     } else {
@@ -224,15 +217,14 @@ pub fn update_inlay_link_and_hover_points(
                         }
                     }
                     ResolveState::Resolved => {
-                        let mut actual_hint_start = hint_start_offset;
-                        let mut actual_hint_end = hint_end_offset;
+                        let mut extra_shift_left = 0;
+                        let mut extra_shift_right = 0;
                         if cached_hint.padding_left {
-                            actual_hint_start.0 += 1;
-                            actual_hint_end.0 += 1;
+                            extra_shift_left += 1;
+                            extra_shift_right += 1;
                         }
                         if cached_hint.padding_right {
-                            actual_hint_start.0 += 1;
-                            actual_hint_end.0 += 1;
+                            extra_shift_right += 1;
                         }
                         match cached_hint.label {
                             project::InlayHintLabel::String(_) => {
@@ -253,11 +245,11 @@ pub fn update_inlay_link_and_hover_points(
                                                     }
                                                 }
                                             },
-                                            triggered_from: hovered_offset,
-                                            range: InlayRange {
+                                            range: InlayHighlight {
+                                                inlay: hovered_hint.id,
                                                 inlay_position: hovered_hint.position,
-                                                highlight_start: actual_hint_start,
-                                                highlight_end: actual_hint_end,
+                                                range: extra_shift_left
+                                                    ..hovered_hint.text.len() + extra_shift_right,
                                             },
                                         },
                                         cx,
@@ -266,13 +258,24 @@ pub fn update_inlay_link_and_hover_points(
                                 }
                             }
                             project::InlayHintLabel::LabelParts(label_parts) => {
+                                let hint_start =
+                                    snapshot.anchor_to_inlay_offset(hovered_hint.position);
                                 if let Some((hovered_hint_part, part_range)) =
                                     hover_popover::find_hovered_hint_part(
                                         label_parts,
-                                        actual_hint_start..actual_hint_end,
+                                        hint_start,
                                         hovered_offset,
                                     )
                                 {
+                                    let highlight_start =
+                                        (part_range.start - hint_start).0 + extra_shift_left;
+                                    let highlight_end =
+                                        (part_range.end - hint_start).0 + extra_shift_right;
+                                    let highlight = InlayHighlight {
+                                        inlay: hovered_hint.id,
+                                        inlay_position: hovered_hint.position,
+                                        range: highlight_start..highlight_end,
+                                    };
                                     if let Some(tooltip) = hovered_hint_part.tooltip {
                                         hover_popover::hover_at_inlay(
                                             editor,
@@ -292,12 +295,7 @@ pub fn update_inlay_link_and_hover_points(
                                                         kind: content.kind,
                                                     },
                                                 },
-                                                triggered_from: hovered_offset,
-                                                range: InlayRange {
-                                                    inlay_position: hovered_hint.position,
-                                                    highlight_start: part_range.start,
-                                                    highlight_end: part_range.end,
-                                                },
+                                                range: highlight.clone(),
                                             },
                                             cx,
                                         );
@@ -310,11 +308,7 @@ pub fn update_inlay_link_and_hover_points(
                                         update_go_to_definition_link(
                                             editor,
                                             Some(GoToDefinitionTrigger::InlayHint(
-                                                InlayRange {
-                                                    inlay_position: hovered_hint.position,
-                                                    highlight_start: part_range.start,
-                                                    highlight_end: part_range.end,
-                                                },
+                                                highlight,
                                                 location,
                                                 language_server_id,
                                             )),
@@ -425,7 +419,7 @@ pub fn show_link_definition(
                                     let end = snapshot
                                         .buffer_snapshot
                                         .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
-                                    DocumentRange::Text(start..end)
+                                    RangeInEditor::Text(start..end)
                                 })
                             }),
                             definition_result
@@ -435,8 +429,8 @@ pub fn show_link_definition(
                         )
                     })
                 }
-                TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => Some((
-                    Some(DocumentRange::Inlay(*trigger_source)),
+                TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
+                    Some(RangeInEditor::Inlay(highlight.clone())),
                     vec![GoToDefinitionLink::InlayHint(
                         lsp_location.clone(),
                         *server_id,
@@ -446,7 +440,7 @@ pub fn show_link_definition(
 
             this.update(&mut cx, |this, cx| {
                 // Clear any existing highlights
-                this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
+                this.clear_highlights::<LinkGoToDefinitionState>(cx);
                 this.link_go_to_definition_state.kind = Some(definition_kind);
                 this.link_go_to_definition_state.symbol_range = result
                     .as_ref()
@@ -498,26 +492,26 @@ pub fn show_link_definition(
                                     // If no symbol range returned from language server, use the surrounding word.
                                     let (offset_range, _) =
                                         snapshot.surrounding_word(*trigger_anchor);
-                                    DocumentRange::Text(
+                                    RangeInEditor::Text(
                                         snapshot.anchor_before(offset_range.start)
                                             ..snapshot.anchor_after(offset_range.end),
                                     )
                                 }
-                                TriggerPoint::InlayHint(inlay_coordinates, _, _) => {
-                                    DocumentRange::Inlay(*inlay_coordinates)
+                                TriggerPoint::InlayHint(highlight, _, _) => {
+                                    RangeInEditor::Inlay(highlight.clone())
                                 }
                             });
 
                         match highlight_range {
-                            DocumentRange::Text(text_range) => this
+                            RangeInEditor::Text(text_range) => this
                                 .highlight_text::<LinkGoToDefinitionState>(
                                     vec![text_range],
                                     style,
                                     cx,
                                 ),
-                            DocumentRange::Inlay(inlay_coordinates) => this
+                            RangeInEditor::Inlay(highlight) => this
                                 .highlight_inlays::<LinkGoToDefinitionState>(
-                                    vec![inlay_coordinates],
+                                    vec![highlight],
                                     style,
                                     cx,
                                 ),
@@ -547,7 +541,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
 
     editor.link_go_to_definition_state.task = None;
 
-    editor.clear_text_highlights::<LinkGoToDefinitionState>(cx);
+    editor.clear_highlights::<LinkGoToDefinitionState>(cx);
 }
 
 pub fn go_to_fetched_definition(
@@ -1199,30 +1193,19 @@ mod tests {
         cx.foreground().run_until_parked();
         cx.update_editor(|editor, cx| {
             let snapshot = editor.snapshot(cx);
-            let actual_ranges = snapshot
-                .highlight_ranges::<LinkGoToDefinitionState>()
-                .map(|ranges| ranges.as_ref().clone().1)
-                .unwrap_or_default()
+            let actual_highlights = snapshot
+                .inlay_highlights::<LinkGoToDefinitionState>()
                 .into_iter()
-                .map(|range| match range {
-                    DocumentRange::Text(range) => {
-                        panic!("Unexpected regular text selection range {range:?}")
-                    }
-                    DocumentRange::Inlay(inlay_range) => inlay_range,
-                })
+                .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
                 .collect::<Vec<_>>();
 
             let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-            let expected_highlight_start = snapshot.display_point_to_inlay_offset(
-                inlay_range.start.to_display_point(&snapshot),
-                Bias::Left,
-            );
-            let expected_ranges = vec![InlayRange {
+            let expected_highlight = InlayHighlight {
+                inlay: InlayId::Hint(0),
                 inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-                highlight_start: expected_highlight_start,
-                highlight_end: InlayOffset(expected_highlight_start.0 + hint_label.len()),
-            }];
-            assert_set_eq!(actual_ranges, expected_ranges);
+                range: 0..hint_label.len(),
+            };
+            assert_set_eq!(actual_highlights, vec![&expected_highlight]);
         });
 
         // Unpress cmd causes highlight to go away
@@ -1242,17 +1225,9 @@ mod tests {
         cx.update_editor(|editor, cx| {
             let snapshot = editor.snapshot(cx);
             let actual_ranges = snapshot
-                .highlight_ranges::<LinkGoToDefinitionState>()
+                .text_highlight_ranges::<LinkGoToDefinitionState>()
                 .map(|ranges| ranges.as_ref().clone().1)
-                .unwrap_or_default()
-                .into_iter()
-                .map(|range| match range {
-                    DocumentRange::Text(range) => {
-                        panic!("Unexpected regular text selection range {range:?}")
-                    }
-                    DocumentRange::Inlay(inlay_range) => inlay_range,
-                })
-                .collect::<Vec<_>>();
+                .unwrap_or_default();
 
             assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
         });

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

@@ -70,6 +70,9 @@ pub enum Event {
     Edited {
         sigleton_buffer_edited: bool,
     },
+    TransactionUndone {
+        transaction_id: TransactionId,
+    },
     Reloaded,
     DiffBaseChanged,
     LanguageChanged,
@@ -771,30 +774,36 @@ impl MultiBuffer {
     }
 
     pub fn undo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
+        let mut transaction_id = None;
         if let Some(buffer) = self.as_singleton() {
-            return buffer.update(cx, |buffer, cx| buffer.undo(cx));
-        }
+            transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx));
+        } else {
+            while let Some(transaction) = self.history.pop_undo() {
+                let mut undone = false;
+                for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions {
+                    if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) {
+                        undone |= buffer.update(cx, |buffer, cx| {
+                            let undo_to = *buffer_transaction_id;
+                            if let Some(entry) = buffer.peek_undo_stack() {
+                                *buffer_transaction_id = entry.transaction_id();
+                            }
+                            buffer.undo_to_transaction(undo_to, cx)
+                        });
+                    }
+                }
 
-        while let Some(transaction) = self.history.pop_undo() {
-            let mut undone = false;
-            for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions {
-                if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) {
-                    undone |= buffer.update(cx, |buffer, cx| {
-                        let undo_to = *buffer_transaction_id;
-                        if let Some(entry) = buffer.peek_undo_stack() {
-                            *buffer_transaction_id = entry.transaction_id();
-                        }
-                        buffer.undo_to_transaction(undo_to, cx)
-                    });
+                if undone {
+                    transaction_id = Some(transaction.id);
+                    break;
                 }
             }
+        }
 
-            if undone {
-                return Some(transaction.id);
-            }
+        if let Some(transaction_id) = transaction_id {
+            cx.emit(Event::TransactionUndone { transaction_id });
         }
 
-        None
+        transaction_id
     }
 
     pub fn redo(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {

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

@@ -15,9 +15,13 @@ impl ScrollAmount {
             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 * count).trunc() - count.signum())
+                .map(|mut l| {
+                    // for full pages subtract one to leave an anchor line
+                    if count.abs() == 1.0 {
+                        l -= 1.0
+                    }
+                    (l * count).trunc()
+                })
                 .unwrap_or(0.),
         }
     }

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

@@ -14,7 +14,7 @@ use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQ
 use lsp::{notification, request};
 use project::Project;
 use smol::stream::StreamExt;
-use workspace::{pane, AppState, Workspace, WorkspaceHandle};
+use workspace::{AppState, Workspace, WorkspaceHandle};
 
 use crate::{multi_buffer::ToPointUtf16, Editor, ToPoint};
 
@@ -38,12 +38,10 @@ impl<'a> EditorLspTestContext<'a> {
         let app_state = cx.update(AppState::test);
 
         cx.update(|cx| {
-            theme::init((), cx);
             language::init(cx);
             crate::init(cx);
-            pane::init(cx);
+            workspace::init(app_state.clone(), cx);
             Project::init_settings(cx);
-            workspace::init_settings(cx);
         });
 
         let file_name = format!(

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

@@ -3,8 +3,8 @@ use crate::{
 };
 use futures::Future;
 use gpui::{
-    keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, ModelContext,
-    ViewContext, ViewHandle,
+    executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle,
+    ModelContext, ViewContext, ViewHandle,
 };
 use indoc::indoc;
 use language::{Buffer, BufferSnapshot};
@@ -114,6 +114,7 @@ impl<'a> EditorTestContext<'a> {
         let keystroke = Keystroke::parse(keystroke_text).unwrap();
 
         self.cx.dispatch_keystroke(self.window, keystroke, false);
+
         keystroke_under_test_handle
     }
 
@@ -126,6 +127,16 @@ impl<'a> EditorTestContext<'a> {
         for keystroke_text in keystroke_texts.into_iter() {
             self.simulate_keystroke(keystroke_text);
         }
+        // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
+        // before returning.
+        // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
+        // quickly races with async actions.
+        if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() {
+            executor.run_until_parked();
+        } else {
+            unreachable!();
+        }
+
         keystrokes_under_test_handle
     }
 
@@ -225,7 +236,6 @@ impl<'a> EditorTestContext<'a> {
                 .map(|h| h.1.clone())
                 .unwrap_or_default()
                 .into_iter()
-                .filter_map(|range| range.as_text_range())
                 .map(|range| range.to_offset(&snapshot.buffer_snapshot))
                 .collect()
         });
@@ -237,11 +247,10 @@ impl<'a> EditorTestContext<'a> {
         let expected_ranges = self.ranges(marked_text);
         let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
         let actual_ranges: Vec<Range<usize>> = snapshot
-            .highlight_ranges::<Tag>()
+            .text_highlight_ranges::<Tag>()
             .map(|ranges| ranges.as_ref().clone().1)
             .unwrap_or_default()
             .into_iter()
-            .filter_map(|range| range.as_text_range())
             .map(|range| range.to_offset(&snapshot.buffer_snapshot))
             .collect();
         assert_set_eq!(actual_ranges, expected_ranges);

crates/feedback/Cargo.toml πŸ”—

@@ -33,7 +33,7 @@ lazy_static.workspace = true
 postage.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
-sysinfo = "0.27.1"
+sysinfo.workspace = true
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 urlencoding = "2.1.2"
 

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

@@ -13,7 +13,7 @@ use gpui::{
 use isahc::Request;
 use language::Buffer;
 use postage::prelude::Stream;
-use project::Project;
+use project::{search::SearchQuery, Project};
 use regex::Regex;
 use serde::Serialize;
 use smallvec::SmallVec;
@@ -276,7 +276,7 @@ impl Item for FeedbackEditor {
     ) -> AnyElement<T> {
         Flex::row()
             .with_child(
-                Svg::new("icons/feedback_16.svg")
+                Svg::new("icons/feedback.svg")
                     .with_color(style.label.text.color)
                     .constrained()
                     .with_width(style.type_icon_width)
@@ -418,10 +418,13 @@ impl SearchableItem for FeedbackEditor {
         self.editor
             .update(cx, |e, cx| e.select_matches(matches, cx))
     }
-
+    fn replace(&mut self, matches: &Self::Match, query: &SearchQuery, cx: &mut ViewContext<Self>) {
+        self.editor
+            .update(cx, |e, cx| e.replace(matches, query, cx));
+    }
     fn find_matches(
         &mut self,
-        query: project::search::SearchQuery,
+        query: Arc<project::search::SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Self::Match>> {
         self.editor

crates/file_finder/Cargo.toml πŸ”—

@@ -10,6 +10,7 @@ doctest = false
 
 [dependencies]
 editor = { path = "../editor" }
+collections = { path = "../collections" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 menu = { path = "../menu" }

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

@@ -1,5 +1,6 @@
+use collections::HashMap;
 use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
-use fuzzy::PathMatch;
+use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
     actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
 };
@@ -32,38 +33,114 @@ pub struct FileFinderDelegate {
     history_items: Vec<FoundPath>,
 }
 
-#[derive(Debug)]
-enum Matches {
-    History(Vec<FoundPath>),
-    Search(Vec<PathMatch>),
+#[derive(Debug, Default)]
+struct Matches {
+    history: Vec<(FoundPath, Option<PathMatch>)>,
+    search: Vec<PathMatch>,
 }
 
 #[derive(Debug)]
 enum Match<'a> {
-    History(&'a FoundPath),
+    History(&'a FoundPath, Option<&'a PathMatch>),
     Search(&'a PathMatch),
 }
 
 impl Matches {
     fn len(&self) -> usize {
-        match self {
-            Self::History(items) => items.len(),
-            Self::Search(items) => items.len(),
-        }
+        self.history.len() + self.search.len()
     }
 
     fn get(&self, index: usize) -> Option<Match<'_>> {
-        match self {
-            Self::History(items) => items.get(index).map(Match::History),
-            Self::Search(items) => items.get(index).map(Match::Search),
+        if index < self.history.len() {
+            self.history
+                .get(index)
+                .map(|(path, path_match)| Match::History(path, path_match.as_ref()))
+        } else {
+            self.search
+                .get(index - self.history.len())
+                .map(Match::Search)
+        }
+    }
+
+    fn push_new_matches(
+        &mut self,
+        history_items: &Vec<FoundPath>,
+        query: &PathLikeWithPosition<FileSearchQuery>,
+        mut new_search_matches: Vec<PathMatch>,
+        extend_old_matches: bool,
+    ) {
+        let matching_history_paths = matching_history_item_paths(history_items, query);
+        new_search_matches
+            .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
+        let history_items_to_show = history_items
+            .iter()
+            .filter_map(|history_item| {
+                Some((
+                    history_item.clone(),
+                    Some(
+                        matching_history_paths
+                            .get(&history_item.project.path)?
+                            .clone(),
+                    ),
+                ))
+            })
+            .collect::<Vec<_>>();
+        self.history = history_items_to_show;
+        if extend_old_matches {
+            self.search
+                .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
+            util::extend_sorted(
+                &mut self.search,
+                new_search_matches.into_iter(),
+                100,
+                |a, b| b.cmp(a),
+            )
+        } else {
+            self.search = new_search_matches;
         }
     }
 }
 
-impl Default for Matches {
-    fn default() -> Self {
-        Self::History(Vec::new())
+fn matching_history_item_paths(
+    history_items: &Vec<FoundPath>,
+    query: &PathLikeWithPosition<FileSearchQuery>,
+) -> HashMap<Arc<Path>, PathMatch> {
+    let history_items_by_worktrees = history_items
+        .iter()
+        .map(|found_path| {
+            let path = &found_path.project.path;
+            let candidate = PathMatchCandidate {
+                path,
+                char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()),
+            };
+            (found_path.project.worktree_id, candidate)
+        })
+        .fold(
+            HashMap::default(),
+            |mut candidates, (worktree_id, new_candidate)| {
+                candidates
+                    .entry(worktree_id)
+                    .or_insert_with(Vec::new)
+                    .push(new_candidate);
+                candidates
+            },
+        );
+    let mut matching_history_paths = HashMap::default();
+    for (worktree, candidates) in history_items_by_worktrees {
+        let max_results = candidates.len() + 1;
+        matching_history_paths.extend(
+            fuzzy::match_fixed_path_set(
+                candidates,
+                worktree.to_usize(),
+                query.path_like.path_query(),
+                false,
+                max_results,
+            )
+            .into_iter()
+            .map(|path_match| (Arc::clone(&path_match.path), path_match)),
+        );
     }
+    matching_history_paths
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -81,66 +158,82 @@ impl FoundPath {
 actions!(file_finder, [Toggle]);
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(toggle_file_finder);
+    cx.add_action(toggle_or_cycle_file_finder);
     FileFinder::init(cx);
 }
 
 const MAX_RECENT_SELECTIONS: usize = 20;
 
-fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
-    workspace.toggle_modal(cx, |workspace, cx| {
-        let project = workspace.project().read(cx);
-
-        let currently_opened_path = workspace
-            .active_item(cx)
-            .and_then(|item| item.project_path(cx))
-            .map(|project_path| {
-                let abs_path = project
-                    .worktree_for_id(project_path.worktree_id, cx)
-                    .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
-                FoundPath::new(project_path, abs_path)
-            });
-
-        // if exists, bubble the currently opened path to the top
-        let history_items = currently_opened_path
-            .clone()
-            .into_iter()
-            .chain(
-                workspace
-                    .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
+fn toggle_or_cycle_file_finder(
+    workspace: &mut Workspace,
+    _: &Toggle,
+    cx: &mut ViewContext<Workspace>,
+) {
+    match workspace.modal::<FileFinder>() {
+        Some(file_finder) => file_finder.update(cx, |file_finder, cx| {
+            let current_index = file_finder.delegate().selected_index();
+            file_finder.select_next(&menu::SelectNext, cx);
+            let new_index = file_finder.delegate().selected_index();
+            if current_index == new_index {
+                file_finder.select_first(&menu::SelectFirst, cx);
+            }
+        }),
+        None => {
+            workspace.toggle_modal(cx, |workspace, cx| {
+                let project = workspace.project().read(cx);
+
+                let currently_opened_path = workspace
+                    .active_item(cx)
+                    .and_then(|item| item.project_path(cx))
+                    .map(|project_path| {
+                        let abs_path = project
+                            .worktree_for_id(project_path.worktree_id, cx)
+                            .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
+                        FoundPath::new(project_path, abs_path)
+                    });
+
+                // if exists, bubble the currently opened path to the top
+                let history_items = currently_opened_path
+                    .clone()
                     .into_iter()
-                    .filter(|(history_path, _)| {
-                        Some(history_path)
-                            != currently_opened_path
-                                .as_ref()
-                                .map(|found_path| &found_path.project)
-                    })
-                    .filter(|(_, history_abs_path)| {
-                        history_abs_path.as_ref()
-                            != currently_opened_path
-                                .as_ref()
-                                .and_then(|found_path| found_path.absolute.as_ref())
-                    })
-                    .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
-            )
-            .collect();
-
-        let project = workspace.project().clone();
-        let workspace = cx.handle().downgrade();
-        let finder = cx.add_view(|cx| {
-            Picker::new(
-                FileFinderDelegate::new(
-                    workspace,
-                    project,
-                    currently_opened_path,
-                    history_items,
-                    cx,
-                ),
-                cx,
-            )
-        });
-        finder
-    });
+                    .chain(
+                        workspace
+                            .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
+                            .into_iter()
+                            .filter(|(history_path, _)| {
+                                Some(history_path)
+                                    != currently_opened_path
+                                        .as_ref()
+                                        .map(|found_path| &found_path.project)
+                            })
+                            .filter(|(_, history_abs_path)| {
+                                history_abs_path.as_ref()
+                                    != currently_opened_path
+                                        .as_ref()
+                                        .and_then(|found_path| found_path.absolute.as_ref())
+                            })
+                            .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
+                    )
+                    .collect();
+
+                let project = workspace.project().clone();
+                let workspace = cx.handle().downgrade();
+                let finder = cx.add_view(|cx| {
+                    Picker::new(
+                        FileFinderDelegate::new(
+                            workspace,
+                            project,
+                            currently_opened_path,
+                            history_items,
+                            cx,
+                        ),
+                        cx,
+                    )
+                });
+                finder
+            });
+        }
+    }
 }
 
 pub enum Event {
@@ -255,24 +348,14 @@ impl FileFinderDelegate {
     ) {
         if search_id >= self.latest_search_id {
             self.latest_search_id = search_id;
-            if self.latest_search_did_cancel
+            let extend_old_matches = self.latest_search_did_cancel
                 && Some(query.path_like.path_query())
                     == self
                         .latest_search_query
                         .as_ref()
-                        .map(|query| query.path_like.path_query())
-            {
-                match &mut self.matches {
-                    Matches::History(_) => self.matches = Matches::Search(matches),
-                    Matches::Search(search_matches) => {
-                        util::extend_sorted(search_matches, matches.into_iter(), 100, |a, b| {
-                            b.cmp(a)
-                        })
-                    }
-                }
-            } else {
-                self.matches = Matches::Search(matches);
-            }
+                        .map(|query| query.path_like.path_query());
+            self.matches
+                .push_new_matches(&self.history_items, &query, matches, extend_old_matches);
             self.latest_search_query = Some(query);
             self.latest_search_did_cancel = did_cancel;
             cx.notify();
@@ -286,7 +369,7 @@ impl FileFinderDelegate {
         ix: usize,
     ) -> (String, Vec<usize>, String, Vec<usize>) {
         let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
-            Match::History(found_path) => {
+            Match::History(found_path, found_path_match) => {
                 let worktree_id = found_path.project.worktree_id;
                 let project_relative_path = &found_path.project.path;
                 let has_worktree = self
@@ -318,14 +401,22 @@ impl FileFinderDelegate {
                         path = Arc::from(absolute_path.as_path());
                     }
                 }
-                self.labels_for_path_match(&PathMatch {
+
+                let mut path_match = PathMatch {
                     score: ix as f64,
                     positions: Vec::new(),
                     worktree_id: worktree_id.to_usize(),
                     path,
                     path_prefix: "".into(),
                     distance_to_relative_ancestor: usize::MAX,
-                })
+                };
+                if let Some(found_path_match) = found_path_match {
+                    path_match
+                        .positions
+                        .extend(found_path_match.positions.iter())
+                }
+
+                self.labels_for_path_match(&path_match)
             }
             Match::Search(path_match) => self.labels_for_path_match(path_match),
         };
@@ -406,8 +497,9 @@ impl PickerDelegate for FileFinderDelegate {
         if raw_query.is_empty() {
             let project = self.project.read(cx);
             self.latest_search_id = post_inc(&mut self.search_count);
-            self.matches = Matches::History(
-                self.history_items
+            self.matches = Matches {
+                history: self
+                    .history_items
                     .iter()
                     .filter(|history_item| {
                         project
@@ -421,8 +513,10 @@ impl PickerDelegate for FileFinderDelegate {
                                     .is_some())
                     })
                     .cloned()
+                    .map(|p| (p, None))
                     .collect(),
-            );
+                search: Vec::new(),
+            };
             cx.notify();
             Task::ready(())
         } else {
@@ -454,7 +548,7 @@ impl PickerDelegate for FileFinderDelegate {
                         }
                     };
                     match m {
-                        Match::History(history_match) => {
+                        Match::History(history_match, _) => {
                             let worktree_id = history_match.project.worktree_id;
                             if workspace
                                 .project()
@@ -866,11 +960,11 @@ mod tests {
 
         finder.update(cx, |finder, cx| {
             let delegate = finder.delegate_mut();
-            let matches = match &delegate.matches {
-                Matches::Search(path_matches) => path_matches,
-                _ => panic!("Search matches expected"),
-            }
-            .clone();
+            assert!(
+                delegate.matches.history.is_empty(),
+                "Search matches expected"
+            );
+            let matches = delegate.matches.search.clone();
 
             // Simulate a search being cancelled after the time limit,
             // returning only a subset of the matches that would have been found.
@@ -893,12 +987,11 @@ mod tests {
                 cx,
             );
 
-            match &delegate.matches {
-                Matches::Search(new_matches) => {
-                    assert_eq!(new_matches.as_slice(), &matches[0..4])
-                }
-                _ => panic!("Search matches expected"),
-            };
+            assert!(
+                delegate.matches.history.is_empty(),
+                "Search matches expected"
+            );
+            assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
         });
     }
 
@@ -1006,10 +1099,11 @@ mod tests {
         cx.read(|cx| {
             let finder = finder.read(cx);
             let delegate = finder.delegate();
-            let matches = match &delegate.matches {
-                Matches::Search(path_matches) => path_matches,
-                _ => panic!("Search matches expected"),
-            };
+            assert!(
+                delegate.matches.history.is_empty(),
+                "Search matches expected"
+            );
+            let matches = delegate.matches.search.clone();
             assert_eq!(matches.len(), 1);
 
             let (file_name, file_name_positions, full_path, full_path_positions) =
@@ -1088,10 +1182,11 @@ mod tests {
 
         finder.read_with(cx, |f, _| {
             let delegate = f.delegate();
-            let matches = match &delegate.matches {
-                Matches::Search(path_matches) => path_matches,
-                _ => panic!("Search matches expected"),
-            };
+            assert!(
+                delegate.matches.history.is_empty(),
+                "Search matches expected"
+            );
+            let matches = delegate.matches.search.clone();
             assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
             assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
         });
@@ -1459,6 +1554,255 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_toggle_panel_new_selections(
+        deterministic: Arc<gpui::executor::Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let app_state = init_test(cx);
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/src",
+                json!({
+                    "test": {
+                        "first.rs": "// First Rust file",
+                        "second.rs": "// Second Rust file",
+                        "third.rs": "// Third Rust file",
+                    }
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+
+        // generate some history to select from
+        open_close_queried_buffer(
+            "fir",
+            1,
+            "first.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "sec",
+            1,
+            "second.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "thi",
+            1,
+            "third.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        let current_history = open_close_queried_buffer(
+            "sec",
+            1,
+            "second.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+
+        for expected_selected_index in 0..current_history.len() {
+            cx.dispatch_action(window.into(), Toggle);
+            let selected_index = cx.read(|cx| {
+                workspace
+                    .read(cx)
+                    .modal::<FileFinder>()
+                    .unwrap()
+                    .read(cx)
+                    .delegate()
+                    .selected_index()
+            });
+            assert_eq!(
+                selected_index, expected_selected_index,
+                "Should select the next item in the history"
+            );
+        }
+
+        cx.dispatch_action(window.into(), Toggle);
+        let selected_index = cx.read(|cx| {
+            workspace
+                .read(cx)
+                .modal::<FileFinder>()
+                .unwrap()
+                .read(cx)
+                .delegate()
+                .selected_index()
+        });
+        assert_eq!(
+            selected_index, 0,
+            "Should wrap around the history and start all over"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_search_preserves_history_items(
+        deterministic: Arc<gpui::executor::Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let app_state = init_test(cx);
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/src",
+                json!({
+                    "test": {
+                        "first.rs": "// First Rust file",
+                        "second.rs": "// Second Rust file",
+                        "third.rs": "// Third Rust file",
+                        "fourth.rs": "// Fourth Rust file",
+                    }
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        let worktree_id = cx.read(|cx| {
+            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+            assert_eq!(worktrees.len(), 1,);
+
+            WorktreeId::from_usize(worktrees[0].id())
+        });
+
+        // generate some history to select from
+        open_close_queried_buffer(
+            "fir",
+            1,
+            "first.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "sec",
+            1,
+            "second.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "thi",
+            1,
+            "third.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "sec",
+            1,
+            "second.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+
+        cx.dispatch_action(window.into(), Toggle);
+        let first_query = "f";
+        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+        finder
+            .update(cx, |finder, cx| {
+                finder
+                    .delegate_mut()
+                    .update_matches(first_query.to_string(), cx)
+            })
+            .await;
+        finder.read_with(cx, |finder, _| {
+            let delegate = finder.delegate();
+            assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
+            let history_match = delegate.matches.history.first().unwrap();
+            assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
+            assert_eq!(history_match.0, FoundPath::new(
+                ProjectPath {
+                    worktree_id,
+                    path: Arc::from(Path::new("test/first.rs")),
+                },
+                Some(PathBuf::from("/src/test/first.rs"))
+            ));
+            assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
+            assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
+        });
+
+        let second_query = "fsdasdsa";
+        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+        finder
+            .update(cx, |finder, cx| {
+                finder
+                    .delegate_mut()
+                    .update_matches(second_query.to_string(), cx)
+            })
+            .await;
+        finder.read_with(cx, |finder, _| {
+            let delegate = finder.delegate();
+            assert!(
+                delegate.matches.history.is_empty(),
+                "No history entries should match {second_query}"
+            );
+            assert!(
+                delegate.matches.search.is_empty(),
+                "No search entries should match {second_query}"
+            );
+        });
+
+        let first_query_again = first_query;
+        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+        finder
+            .update(cx, |finder, cx| {
+                finder
+                    .delegate_mut()
+                    .update_matches(first_query_again.to_string(), cx)
+            })
+            .await;
+        finder.read_with(cx, |finder, _| {
+            let delegate = finder.delegate();
+            assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
+            let history_match = delegate.matches.history.first().unwrap();
+            assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
+            assert_eq!(history_match.0, FoundPath::new(
+                ProjectPath {
+                    worktree_id,
+                    path: Arc::from(Path::new("test/first.rs")),
+                },
+                Some(PathBuf::from("/src/test/first.rs"))
+            ));
+            assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
+            assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
+        });
+    }
+
     async fn open_close_queried_buffer(
         input: &str,
         expected_matches: usize,
@@ -1528,13 +1872,8 @@ mod tests {
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
         active_pane
             .update(cx, |pane, cx| {
-                pane.close_active_item(
-                    &workspace::CloseActiveItem {
-                        save_behavior: None,
-                    },
-                    cx,
-                )
-                .unwrap()
+                pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx)
+                    .unwrap()
             })
             .await
             .unwrap();

crates/fs/Cargo.toml πŸ”—

@@ -9,8 +9,6 @@ path = "src/fs.rs"
 
 [dependencies]
 collections = { path = "../collections" }
-gpui = { path = "../gpui" }
-lsp = { path = "../lsp" }
 rope = { path = "../rope" }
 text = { path = "../text" }
 util = { path = "../util" }
@@ -26,7 +24,7 @@ lazy_static.workspace = true
 parking_lot.workspace = true
 smol.workspace = true
 regex.workspace = true
-git2 = { version = "0.15", default-features = false }
+git2.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
@@ -34,8 +32,10 @@ log.workspace = true
 libc = "0.2"
 time.workspace = true
 
+gpui = { path = "../gpui", optional = true}
+
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
 
 [features]
-test-support = []
+test-support = ["gpui/test-support"]

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

@@ -93,33 +93,6 @@ pub struct Metadata {
     pub is_dir: bool,
 }
 
-impl From<lsp::CreateFileOptions> for CreateOptions {
-    fn from(options: lsp::CreateFileOptions) -> Self {
-        Self {
-            overwrite: options.overwrite.unwrap_or(false),
-            ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
-        }
-    }
-}
-
-impl From<lsp::RenameFileOptions> for RenameOptions {
-    fn from(options: lsp::RenameFileOptions) -> Self {
-        Self {
-            overwrite: options.overwrite.unwrap_or(false),
-            ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
-        }
-    }
-}
-
-impl From<lsp::DeleteFileOptions> for RemoveOptions {
-    fn from(options: lsp::DeleteFileOptions) -> Self {
-        Self {
-            recursive: options.recursive.unwrap_or(false),
-            ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false),
-        }
-    }
-}
-
 pub struct RealFs;
 
 #[async_trait::async_trait]
@@ -507,7 +480,7 @@ impl FakeFs {
         state.emit_event(&[path]);
     }
 
-    fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
+    pub fn write_file_internal(&self, path: impl AsRef<Path>, content: String) -> Result<()> {
         let mut state = self.state.lock();
         let path = path.as_ref();
         let inode = state.next_inode;

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

@@ -4,5 +4,7 @@ mod paths;
 mod strings;
 
 pub use char_bag::CharBag;
-pub use paths::{match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet};
+pub use paths::{
+    match_fixed_path_set, match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet,
+};
 pub use strings::{match_strings, StringMatch, StringMatchCandidate};

crates/fuzzy/src/paths.rs πŸ”—

@@ -90,6 +90,44 @@ impl Ord for PathMatch {
     }
 }
 
+pub fn match_fixed_path_set(
+    candidates: Vec<PathMatchCandidate>,
+    worktree_id: usize,
+    query: &str,
+    smart_case: bool,
+    max_results: usize,
+) -> Vec<PathMatch> {
+    let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
+    let query = query.chars().collect::<Vec<_>>();
+    let query_char_bag = CharBag::from(&lowercase_query[..]);
+
+    let mut matcher = Matcher::new(
+        &query,
+        &lowercase_query,
+        query_char_bag,
+        smart_case,
+        max_results,
+    );
+
+    let mut results = Vec::new();
+    matcher.match_candidates(
+        &[],
+        &[],
+        candidates.into_iter(),
+        &mut results,
+        &AtomicBool::new(false),
+        |candidate, score| PathMatch {
+            score,
+            worktree_id,
+            positions: Vec::new(),
+            path: candidate.path.clone(),
+            path_prefix: Arc::from(""),
+            distance_to_relative_ancestor: usize::MAX,
+        },
+    );
+    results
+}
+
 pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
     candidate_sets: &'a [Set],
     query: &str,

crates/git/Cargo.toml πŸ”—

@@ -20,7 +20,7 @@ smol.workspace = true
 parking_lot.workspace = true
 async-trait.workspace = true
 futures.workspace = true
-git2 = { version = "0.15", default-features = false }
+git2.workspace = true
 
 [dev-dependencies]
 unindent.workspace = true

crates/gpui/Cargo.toml πŸ”—

@@ -11,7 +11,7 @@ path = "src/gpui.rs"
 doctest = false
 
 [features]
-test-support = ["backtrace", "dhat", "env_logger", "collections/test-support"]
+test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"]
 
 [dependencies]
 collections = { path = "../collections" }
@@ -53,7 +53,7 @@ thiserror.workspace = true
 time.workspace = true
 tiny-skia = "0.5"
 usvg = { version = "0.14", features = [] }
-uuid = { version = "1.1.2", features = ["v4"] }
+uuid.workspace = true
 waker-fn = "1.1.0"
 
 [build-dependencies]
@@ -67,6 +67,7 @@ dhat = "0.3"
 env_logger.workspace = true
 png = "0.16"
 simplelog = "0.9"
+util = { path = "../util", features = ["test-support"] }
 
 [target.'cfg(target_os = "macos")'.dependencies]
 media = { path = "../media" }

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

@@ -684,23 +684,41 @@ impl AppContext {
                 );
             },
         );
+        fn inner(
+            this: &mut AppContext,
+            name: &'static str,
+            deserializer: fn(serde_json::Value) -> anyhow::Result<Box<dyn Action>>,
+            action_id: TypeId,
+            view_id: TypeId,
+            handler: Box<ActionCallback>,
+            capture: bool,
+        ) {
+            this.action_deserializers
+                .entry(name)
+                .or_insert((action_id.clone(), deserializer));
+
+            let actions = if capture {
+                &mut this.capture_actions
+            } else {
+                &mut this.actions
+            };
 
-        self.action_deserializers
-            .entry(A::qualified_name())
-            .or_insert((TypeId::of::<A>(), A::from_json_str));
-
-        let actions = if capture {
-            &mut self.capture_actions
-        } else {
-            &mut self.actions
-        };
-
-        actions
-            .entry(TypeId::of::<V>())
-            .or_default()
-            .entry(TypeId::of::<A>())
-            .or_default()
-            .push(handler);
+            actions
+                .entry(view_id)
+                .or_default()
+                .entry(action_id)
+                .or_default()
+                .push(handler);
+        }
+        inner(
+            self,
+            A::qualified_name(),
+            A::from_json_str,
+            TypeId::of::<A>(),
+            TypeId::of::<V>(),
+            handler,
+            capture,
+        );
     }
 
     pub fn add_async_action<A, V, F>(&mut self, mut handler: F)
@@ -1234,7 +1252,7 @@ impl AppContext {
                 result
             })
         } else {
-            panic!("circular model update");
+            panic!("circular model update for {}", std::any::type_name::<T>());
         }
     }
 

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

@@ -33,6 +33,7 @@ use std::{
     any::{type_name, Any, TypeId},
     mem,
     ops::{Deref, DerefMut, Range, Sub},
+    sync::Arc,
 };
 use taffy::{
     tree::{Measurable, MeasureFunc},
@@ -56,7 +57,7 @@ pub struct Window {
     pub(crate) rendered_views: HashMap<usize, Box<dyn AnyRootElement>>,
     scene: SceneBuilder,
     pub(crate) text_style_stack: Vec<TextStyle>,
-    pub(crate) theme_stack: Vec<Box<dyn Any>>,
+    pub(crate) theme_stack: Vec<Arc<dyn Any + Send + Sync>>,
     pub(crate) new_parents: HashMap<usize, usize>,
     pub(crate) views_to_notify_if_ancestors_change: HashMap<usize, SmallVec<[usize; 2]>>,
     titlebar_height: f32,
@@ -70,7 +71,7 @@ pub struct Window {
     pub(crate) hovered_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
-    text_layout_cache: TextLayoutCache,
+    text_layout_cache: Arc<TextLayoutCache>,
     refreshing: bool,
 }
 
@@ -106,7 +107,7 @@ impl Window {
             cursor_regions: Default::default(),
             mouse_regions: Default::default(),
             event_handlers: Default::default(),
-            text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
+            text_layout_cache: Arc::new(TextLayoutCache::new(cx.font_system.clone())),
             last_mouse_moved_event: None,
             last_mouse_position: Vector2F::zero(),
             pressed_buttons: Default::default(),
@@ -302,7 +303,7 @@ impl<'a> WindowContext<'a> {
         self.window.refreshing
     }
 
-    pub fn text_layout_cache(&self) -> &TextLayoutCache {
+    pub fn text_layout_cache(&self) -> &Arc<TextLayoutCache> {
         &self.window.text_layout_cache
     }
 
@@ -611,9 +612,10 @@ impl<'a> WindowContext<'a> {
             }
 
             Event::MouseUp(e) => {
-                // NOTE: The order of event pushes is important! MouseUp events MUST be fired
-                // before click events, and so the MouseUp events need to be pushed before
-                // MouseClick events.
+                mouse_events.push(MouseEvent::Up(MouseUp {
+                    region: Default::default(),
+                    platform_event: e.clone(),
+                }));
 
                 // Synthesize one last drag event to end the drag
                 mouse_events.push(MouseEvent::Drag(MouseDrag {
@@ -626,10 +628,7 @@ impl<'a> WindowContext<'a> {
                     },
                     end: true,
                 }));
-                mouse_events.push(MouseEvent::Up(MouseUp {
-                    region: Default::default(),
-                    platform_event: e.clone(),
-                }));
+
                 mouse_events.push(MouseEvent::UpOut(MouseUpOut {
                     region: Default::default(),
                     platform_event: e.clone(),
@@ -1338,18 +1337,21 @@ impl<'a> WindowContext<'a> {
         self.window.text_style_stack.pop();
     }
 
-    pub fn theme<T: 'static>(&self) -> &T {
+    pub fn theme<T: 'static + Send + Sync>(&self) -> Arc<T> {
         self.window
             .theme_stack
             .iter()
             .rev()
-            .find_map(|theme| theme.downcast_ref())
+            .find_map(|theme| {
+                let entry = Arc::clone(theme);
+                entry.downcast::<T>().ok()
+            })
             .ok_or_else(|| anyhow!("no theme provided of type {}", type_name::<T>()))
             .unwrap()
     }
 
-    pub fn push_theme<T: 'static>(&mut self, theme: T) {
-        self.window.theme_stack.push(Box::new(theme));
+    pub fn push_theme<T: 'static + Send + Sync>(&mut self, theme: T) {
+        self.window.theme_stack.push(Arc::new(theme));
     }
 
     pub fn pop_theme(&mut self) {

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

@@ -98,7 +98,12 @@ impl FontCache {
         }
 
         Err(anyhow!(
-            "could not find a non-empty font family matching one of the given names"
+            "could not find a non-empty font family matching one of the given names: {}",
+            names
+                .iter()
+                .map(|name| format!("`{name}`"))
+                .collect::<Vec<_>>()
+                .join(", ")
         ))
     }
 

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

@@ -1,12 +1,12 @@
-use std::fmt::Debug;
-
 use super::scene::{Path, PathVertex};
 use crate::{color::Color, json::ToJson};
+use derive_more::Neg;
 pub use pathfinder_geometry::*;
 use rect::RectF;
 use refineable::Refineable;
 use serde::{Deserialize, Deserializer};
 use serde_json::json;
+use std::fmt::Debug;
 use vector::{vec2f, Vector2F};
 
 pub struct PathBuilder {
@@ -167,15 +167,6 @@ pub struct Size<T: Clone + Default + Debug> {
     pub height: T,
 }
 
-impl Size<Length> {
-    pub fn full() -> Self {
-        Self {
-            width: relative(1.),
-            height: relative(1.),
-        }
-    }
-}
-
 impl<S, T: Clone + Default + Debug> From<taffy::geometry::Size<S>> for Size<T>
 where
     S: Into<T>,
@@ -203,8 +194,8 @@ where
 impl Size<DefiniteLength> {
     pub fn zero() -> Self {
         Self {
-            width: px(0.),
-            height: px(0.),
+            width: pixels(0.).into(),
+            height: pixels(0.).into(),
         }
     }
 
@@ -244,6 +235,17 @@ pub struct Edges<T: Clone + Default + Debug> {
     pub left: T,
 }
 
+impl<T: Clone + Default + Debug> Edges<T> {
+    pub fn uniform(value: T) -> Self {
+        Self {
+            top: value.clone(),
+            right: value.clone(),
+            bottom: value.clone(),
+            left: value.clone(),
+        }
+    }
+}
+
 impl Edges<Length> {
     pub fn auto() -> Self {
         Self {
@@ -256,10 +258,10 @@ impl Edges<Length> {
 
     pub fn zero() -> Self {
         Self {
-            top: px(0.),
-            right: px(0.),
-            bottom: px(0.),
-            left: px(0.),
+            top: pixels(0.).into(),
+            right: pixels(0.).into(),
+            bottom: pixels(0.).into(),
+            left: pixels(0.).into(),
         }
     }
 
@@ -279,10 +281,10 @@ impl Edges<Length> {
 impl Edges<DefiniteLength> {
     pub fn zero() -> Self {
         Self {
-            top: px(0.),
-            right: px(0.),
-            bottom: px(0.),
-            left: px(0.),
+            top: pixels(0.).into(),
+            right: pixels(0.).into(),
+            bottom: pixels(0.).into(),
+            left: pixels(0.).into(),
         }
     }
 
@@ -299,10 +301,10 @@ impl Edges<DefiniteLength> {
 impl Edges<AbsoluteLength> {
     pub fn zero() -> Self {
         Self {
-            top: px(0.),
-            right: px(0.),
-            bottom: px(0.),
-            left: px(0.),
+            top: pixels(0.),
+            right: pixels(0.),
+            bottom: pixels(0.),
+            left: pixels(0.),
         }
     }
 
@@ -331,7 +333,7 @@ impl Edges<f32> {
     }
 }
 
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Neg)]
 pub enum AbsoluteLength {
     Pixels(f32),
     Rems(f32),
@@ -369,7 +371,7 @@ impl Default for AbsoluteLength {
 }
 
 /// A non-auto length that can be defined in pixels, rems, or percent of parent.
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Neg)]
 pub enum DefiniteLength {
     Absolute(AbsoluteLength),
     Relative(f32), // 0. to 1.
@@ -413,7 +415,7 @@ impl Default for DefiniteLength {
 }
 
 /// A length that can be defined in pixels, rems, percent of parent, or auto.
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Neg)]
 pub enum Length {
     Definite(DefiniteLength),
     Auto,
@@ -428,20 +430,16 @@ impl std::fmt::Debug for Length {
     }
 }
 
-pub fn relative<T: From<DefiniteLength>>(fraction: f32) -> T {
-    DefiniteLength::Relative(fraction).into()
-}
-
-pub fn rems<T: From<AbsoluteLength>>(rems: f32) -> T {
-    AbsoluteLength::Rems(rems).into()
+pub fn relative(fraction: f32) -> DefiniteLength {
+    DefiniteLength::Relative(fraction)
 }
 
-pub fn px<T: From<AbsoluteLength>>(pixels: f32) -> T {
-    AbsoluteLength::Pixels(pixels).into()
+pub fn rems(rems: f32) -> AbsoluteLength {
+    AbsoluteLength::Rems(rems)
 }
 
-pub fn pixels<T: From<AbsoluteLength>>(pixels: f32) -> T {
-    AbsoluteLength::Pixels(pixels).into()
+pub fn pixels(pixels: f32) -> AbsoluteLength {
+    AbsoluteLength::Pixels(pixels)
 }
 
 pub fn auto() -> Length {

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

@@ -75,7 +75,6 @@ impl KeymapMatcher {
         keystroke: Keystroke,
         mut dispatch_path: Vec<(usize, KeymapContext)>,
     ) -> MatchResult {
-        let mut any_pending = false;
         // Collect matched bindings into an ordered list using the position in the matching binding first,
         // and then the order the binding matched in the view tree second.
         // The key is the reverse position of the binding in the bindings list so that later bindings
@@ -84,7 +83,8 @@ impl KeymapMatcher {
         let no_action_id = (NoAction {}).id();
 
         let first_keystroke = self.pending_keystrokes.is_empty();
-        self.pending_keystrokes.push(keystroke.clone());
+        let mut pending_key = None;
+        let mut previous_keystrokes = self.pending_keystrokes.clone();
 
         self.contexts.clear();
         self.contexts
@@ -106,24 +106,32 @@ impl KeymapMatcher {
             }
 
             for binding in self.keymap.bindings().iter().rev() {
-                match binding.match_keys_and_context(&self.pending_keystrokes, &self.contexts[i..])
-                {
-                    BindingMatchResult::Complete(action) => {
-                        if action.id() != no_action_id {
-                            matched_bindings.push((*view_id, action));
+                for possibility in keystroke.match_possibilities() {
+                    previous_keystrokes.push(possibility.clone());
+                    match binding.match_keys_and_context(&previous_keystrokes, &self.contexts[i..])
+                    {
+                        BindingMatchResult::Complete(action) => {
+                            if action.id() != no_action_id {
+                                matched_bindings.push((*view_id, action));
+                            }
                         }
+                        BindingMatchResult::Partial => {
+                            if pending_key == None || pending_key == Some(possibility.clone()) {
+                                self.pending_views
+                                    .insert(*view_id, self.contexts[i].clone());
+                                pending_key = Some(possibility)
+                            }
+                        }
+                        _ => {}
                     }
-                    BindingMatchResult::Partial => {
-                        self.pending_views
-                            .insert(*view_id, self.contexts[i].clone());
-                        any_pending = true;
-                    }
-                    _ => {}
+                    previous_keystrokes.pop();
                 }
             }
         }
 
-        if !any_pending {
+        if pending_key.is_some() {
+            self.pending_keystrokes.push(pending_key.unwrap());
+        } else {
             self.clear_pending();
         }
 
@@ -131,7 +139,7 @@ impl KeymapMatcher {
             // Collect the sorted matched bindings into the final vec for ease of use
             // Matched bindings are in order by precedence
             MatchResult::Matches(matched_bindings)
-        } else if any_pending {
+        } else if !self.pending_keystrokes.is_empty() {
             MatchResult::Pending
         } else {
             MatchResult::None
@@ -340,6 +348,7 @@ mod tests {
                 shift: false,
                 cmd: false,
                 function: false,
+                ime_key: None,
             }
         );
 
@@ -352,6 +361,7 @@ mod tests {
                 shift: true,
                 cmd: false,
                 function: false,
+                ime_key: None,
             }
         );
 
@@ -364,6 +374,7 @@ mod tests {
                 shift: true,
                 cmd: true,
                 function: false,
+                ime_key: None,
             }
         );
 
@@ -466,7 +477,7 @@ mod tests {
         #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
         pub struct A(pub String);
         impl_actions!(test, [A]);
-        actions!(test, [B, Ab]);
+        actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
 
         #[derive(Clone, Debug, Eq, PartialEq)]
         struct ActionArg {
@@ -477,6 +488,10 @@ mod tests {
             Binding::new("a", A("x".to_string()), Some("a")),
             Binding::new("b", B, Some("a")),
             Binding::new("a b", Ab, Some("a || b")),
+            Binding::new("$", Dollar, Some("a")),
+            Binding::new("\"", Quote, Some("a")),
+            Binding::new("alt-s", Ess, Some("a")),
+            Binding::new("ctrl-`", Backtick, Some("a")),
         ]);
 
         let mut context_a = KeymapContext::default();
@@ -543,6 +558,30 @@ mod tests {
             MatchResult::Matches(vec![(1, Box::new(Ab))])
         );
 
+        // handle Czech $ (option + 4 key)
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("alt-Γ§->$")?, vec![(1, context_a.clone())]),
+            MatchResult::Matches(vec![(1, Box::new(Dollar))])
+        );
+
+        // handle Brazillian quote (quote key then space key)
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("space->\"")?, vec![(1, context_a.clone())]),
+            MatchResult::Matches(vec![(1, Box::new(Quote))])
+        );
+
+        // handle ctrl+` on a brazillian keyboard
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("ctrl-->`")?, vec![(1, context_a.clone())]),
+            MatchResult::Matches(vec![(1, Box::new(Backtick))])
+        );
+
+        // handle alt-s on a US keyboard
+        assert_eq!(
+            matcher.push_keystroke(Keystroke::parse("alt-s->ß")?, vec![(1, context_a.clone())]),
+            MatchResult::Matches(vec![(1, Box::new(Ess))])
+        );
+
         Ok(())
     }
 }

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

@@ -162,7 +162,8 @@ mod tests {
                 shift: false,
                 cmd: false,
                 function: false,
-                key: "q".to_string()
+                key: "q".to_string(),
+                ime_key: None,
             }],
             "{keystroke_duplicate_to_1:?} should have the expected keystroke in the keymap"
         );
@@ -179,7 +180,8 @@ mod tests {
                     shift: false,
                     cmd: false,
                     function: false,
-                    key: "w".to_string()
+                    key: "w".to_string(),
+                    ime_key: None,
                 },
                 &Keystroke {
                     ctrl: true,
@@ -187,7 +189,8 @@ mod tests {
                     shift: false,
                     cmd: false,
                     function: false,
-                    key: "w".to_string()
+                    key: "w".to_string(),
+                    ime_key: None,
                 }
             ],
             "{full_duplicate_to_2:?} should have a duplicated keystroke in the keymap"
@@ -339,7 +342,8 @@ mod tests {
                     shift: false,
                     cmd: false,
                     function: false,
-                    key: expected_key.to_string()
+                    key: expected_key.to_string(),
+                    ime_key: None,
                 }],
                 "{expected:?} should have the expected keystroke with key '{expected_key}' in the keymap for element {i}"
             );

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

@@ -2,6 +2,7 @@ use std::fmt::Write;
 
 use anyhow::anyhow;
 use serde::Deserialize;
+use smallvec::SmallVec;
 
 #[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
 pub struct Keystroke {
@@ -10,10 +11,47 @@ pub struct Keystroke {
     pub shift: bool,
     pub cmd: bool,
     pub function: bool,
+    /// key is the character printed on the key that was pressed
+    /// e.g. for option-s, key is "s"
     pub key: String,
+    /// ime_key is the character inserted by the IME engine when that key was pressed.
+    /// e.g. for option-s, ime_key is "ß"
+    pub ime_key: Option<String>,
 }
 
 impl Keystroke {
+    // When matching a key we cannot know whether the user intended to type
+    // the ime_key or the key. On some non-US keyboards keys we use in our
+    // bindings are behind option (for example `$` is typed `alt-Γ§` on a Czech keyboard),
+    // and on some keyboards the IME handler converts a sequence of keys into a
+    // specific character (for example `"` is typed as `" space` on a brazillian keyboard).
+    pub fn match_possibilities(&self) -> SmallVec<[Keystroke; 2]> {
+        let mut possibilities = SmallVec::new();
+        match self.ime_key.as_ref() {
+            None => possibilities.push(self.clone()),
+            Some(ime_key) => {
+                possibilities.push(Keystroke {
+                    ctrl: self.ctrl,
+                    alt: false,
+                    shift: false,
+                    cmd: false,
+                    function: false,
+                    key: ime_key.to_string(),
+                    ime_key: None,
+                });
+                possibilities.push(Keystroke {
+                    ime_key: None,
+                    ..self.clone()
+                });
+            }
+        }
+        possibilities
+    }
+
+    /// key syntax is:
+    /// [ctrl-][alt-][shift-][cmd-][fn-]key[->ime_key]
+    /// ime_key is only used for generating test events,
+    /// when matching a key with an ime_key set will be matched without it.
     pub fn parse(source: &str) -> anyhow::Result<Self> {
         let mut ctrl = false;
         let mut alt = false;
@@ -21,6 +59,7 @@ impl Keystroke {
         let mut cmd = false;
         let mut function = false;
         let mut key = None;
+        let mut ime_key = None;
 
         let mut components = source.split('-').peekable();
         while let Some(component) = components.next() {
@@ -31,10 +70,14 @@ impl Keystroke {
                 "cmd" => cmd = true,
                 "fn" => function = true,
                 _ => {
-                    if let Some(component) = components.peek() {
-                        if component.is_empty() && source.ends_with('-') {
+                    if let Some(next) = components.peek() {
+                        if next.is_empty() && source.ends_with('-') {
                             key = Some(String::from("-"));
                             break;
+                        } else if next.len() > 1 && next.starts_with('>') {
+                            key = Some(String::from(component));
+                            ime_key = Some(String::from(&next[1..]));
+                            components.next();
                         } else {
                             return Err(anyhow!("Invalid keystroke `{}`", source));
                         }
@@ -54,6 +97,7 @@ impl Keystroke {
             cmd,
             function,
             key,
+            ime_key,
         })
     }
 
@@ -68,7 +112,7 @@ impl std::fmt::Display for Keystroke {
             f.write_char('^')?;
         }
         if self.alt {
-            f.write_char('βŽ‡')?;
+            f.write_char('βŒ₯')?;
         }
         if self.cmd {
             f.write_char('⌘')?;

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

@@ -285,6 +285,7 @@ enum ImeState {
     None,
 }
 
+#[derive(Debug)]
 struct InsertText {
     replacement_range: Option<Range<usize>>,
     text: String,
@@ -1006,40 +1007,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
                         .flatten()
                         .is_some();
                 if !is_composing {
-                    // if the IME has changed the key, we'll first emit an event with the character
-                    // generated by the IME system; then fallback to the keystroke if that is not
-                    // handled.
-                    // cases that we have working:
-                    // - " on a brazillian layout by typing <quote><space>
-                    // - ctrl-` on a brazillian layout by typing <ctrl-`>
-                    // - $ on a czech QWERTY layout by typing <alt-4>
-                    // - 4 on a czech QWERTY layout by typing <shift-4>
-                    // - ctrl-4 on a czech QWERTY layout by typing <ctrl-alt-4> (or <ctrl-shift-4>)
-                    if ime_text.is_some() && ime_text.as_ref() != Some(&event.keystroke.key) {
-                        let event_with_ime_text = KeyDownEvent {
-                            is_held: false,
-                            keystroke: Keystroke {
-                                // we match ctrl because some use-cases need it.
-                                // we don't match alt because it's often used to generate the optional character
-                                // we don't match shift because we're not here with letters (usually)
-                                // we don't match cmd/fn because they don't seem to use IME
-                                ctrl: event.keystroke.ctrl,
-                                alt: false,
-                                shift: false,
-                                cmd: false,
-                                function: false,
-                                key: ime_text.clone().unwrap(),
-                            },
-                        };
-                        handled = callback(Event::KeyDown(event_with_ime_text));
-                    }
-                    if !handled {
-                        // empty key happens when you type a deadkey in input composition.
-                        // (e.g. on a brazillian keyboard typing quote is a deadkey)
-                        if !event.keystroke.key.is_empty() {
-                            handled = callback(Event::KeyDown(event));
-                        }
-                    }
+                    handled = callback(Event::KeyDown(event));
                 }
 
                 if !handled {
@@ -1197,6 +1165,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
         shift: false,
         function: false,
         key: ".".into(),
+        ime_key: None,
     };
     let event = Event::KeyDown(KeyDownEvent {
         keystroke: keystroke.clone(),
@@ -1479,6 +1448,9 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
                 replacement_range,
                 text: text.to_string(),
             });
+            if text.to_string().to_ascii_lowercase() != pending_key_down.0.keystroke.key {
+                pending_key_down.0.keystroke.ime_key = Some(text.to_string());
+            }
             window_state.borrow_mut().pending_key_down = Some(pending_key_down);
         }
     }

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

@@ -103,6 +103,7 @@ pub struct Platform {
     current_clipboard_item: Mutex<Option<ClipboardItem>>,
     cursor: Mutex<CursorStyle>,
     active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+    active_screen: Screen,
 }
 
 impl Platform {
@@ -113,6 +114,7 @@ impl Platform {
             current_clipboard_item: Default::default(),
             cursor: Mutex::new(CursorStyle::Arrow),
             active_window: Default::default(),
+            active_screen: Screen::new(),
         }
     }
 }
@@ -136,12 +138,16 @@ impl super::Platform for Platform {
 
     fn quit(&self) {}
 
-    fn screen_by_id(&self, _id: uuid::Uuid) -> Option<Rc<dyn crate::platform::Screen>> {
-        None
+    fn screen_by_id(&self, uuid: uuid::Uuid) -> Option<Rc<dyn crate::platform::Screen>> {
+        if self.active_screen.uuid == uuid {
+            Some(Rc::new(self.active_screen.clone()))
+        } else {
+            None
+        }
     }
 
     fn screens(&self) -> Vec<Rc<dyn crate::platform::Screen>> {
-        Default::default()
+        vec![Rc::new(self.active_screen.clone())]
     }
 
     fn open_window(
@@ -158,6 +164,7 @@ impl super::Platform for Platform {
                 WindowBounds::Fixed(rect) => rect.size(),
             },
             self.active_window.clone(),
+            Rc::new(self.active_screen.clone()),
         ))
     }
 
@@ -170,6 +177,7 @@ impl super::Platform for Platform {
             handle,
             vec2f(24., 24.),
             self.active_window.clone(),
+            Rc::new(self.active_screen.clone()),
         ))
     }
 
@@ -238,8 +246,18 @@ impl super::Platform for Platform {
     fn restart(&self) {}
 }
 
-#[derive(Debug)]
-pub struct Screen;
+#[derive(Debug, Clone)]
+pub struct Screen {
+    uuid: uuid::Uuid,
+}
+
+impl Screen {
+    fn new() -> Self {
+        Self {
+            uuid: uuid::Uuid::new_v4(),
+        }
+    }
+}
 
 impl super::Screen for Screen {
     fn as_any(&self) -> &dyn Any {
@@ -255,7 +273,7 @@ impl super::Screen for Screen {
     }
 
     fn display_uuid(&self) -> Option<uuid::Uuid> {
-        Some(uuid::Uuid::new_v4())
+        Some(self.uuid)
     }
 }
 
@@ -275,6 +293,7 @@ pub struct Window {
     pub(crate) edited: bool,
     pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
     active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+    screen: Rc<Screen>,
 }
 
 impl Window {
@@ -282,6 +301,7 @@ impl Window {
         handle: AnyWindowHandle,
         size: Vector2F,
         active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+        screen: Rc<Screen>,
     ) -> Self {
         Self {
             handle,
@@ -299,6 +319,7 @@ impl Window {
             edited: false,
             pending_prompts: Default::default(),
             active_window,
+            screen,
         }
     }
 
@@ -329,7 +350,7 @@ impl super::Window for Window {
     }
 
     fn screen(&self) -> Rc<dyn crate::platform::Screen> {
-        Rc::new(Screen)
+        self.screen.clone()
     }
 
     fn mouse_position(&self) -> Vector2F {

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

@@ -2,6 +2,4 @@ mod select;
 
 pub use select::{ItemType, Select, SelectStyle};
 
-pub fn init(cx: &mut super::AppContext) {
-    select::init(cx);
-}
+pub fn init(_: &mut super::AppContext) {}

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

@@ -1,13 +1,12 @@
-use serde::Deserialize;
-
 use crate::{
-    actions, elements::*, impl_actions, platform::MouseButton, AppContext, Entity, View,
-    ViewContext, WeakViewHandle,
+    elements::*,
+    platform::{CursorStyle, MouseButton},
+    AppContext, Entity, View, ViewContext, WeakViewHandle,
 };
 
 pub struct Select {
     handle: WeakViewHandle<Self>,
-    render_item: Box<dyn Fn(usize, ItemType, bool, &AppContext) -> AnyElement<Self>>,
+    render_item: Box<dyn Fn(usize, ItemType, bool, &mut ViewContext<Select>) -> AnyElement<Self>>,
     selected_item_ix: usize,
     item_count: usize,
     is_open: bool,
@@ -27,21 +26,12 @@ pub enum ItemType {
     Unselected,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct SelectItem(pub usize);
-
-actions!(select, [ToggleSelect]);
-impl_actions!(select, [SelectItem]);
-
 pub enum Event {}
 
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(Select::toggle);
-    cx.add_action(Select::select_item);
-}
-
 impl Select {
-    pub fn new<F: 'static + Fn(usize, ItemType, bool, &AppContext) -> AnyElement<Self>>(
+    pub fn new<
+        F: 'static + Fn(usize, ItemType, bool, &mut ViewContext<Self>) -> AnyElement<Self>,
+    >(
         item_count: usize,
         cx: &mut ViewContext<Self>,
         render_item: F,
@@ -63,19 +53,23 @@ impl Select {
     }
 
     pub fn set_item_count(&mut self, count: usize, cx: &mut ViewContext<Self>) {
-        self.item_count = count;
-        cx.notify();
+        if count != self.item_count {
+            self.item_count = count;
+            cx.notify();
+        }
     }
 
-    fn toggle(&mut self, _: &ToggleSelect, cx: &mut ViewContext<Self>) {
+    fn toggle(&mut self, cx: &mut ViewContext<Self>) {
         self.is_open = !self.is_open;
         cx.notify();
     }
 
-    fn select_item(&mut self, action: &SelectItem, cx: &mut ViewContext<Self>) {
-        self.selected_item_ix = action.0;
-        self.is_open = false;
-        cx.notify();
+    pub fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
+        if ix != self.selected_item_ix || self.is_open {
+            self.selected_item_ix = ix;
+            self.is_open = false;
+            cx.notify();
+        }
     }
 
     pub fn selected_index(&self) -> usize {
@@ -116,8 +110,9 @@ impl View for Select {
                 .contained()
                 .with_style(style.header)
             })
+            .with_cursor_style(CursorStyle::PointingHand)
             .on_click(MouseButton::Left, move |_, this, cx| {
-                this.toggle(&Default::default(), cx);
+                this.toggle(cx);
             }),
         );
         if self.is_open {
@@ -142,8 +137,9 @@ impl View for Select {
                                     cx,
                                 )
                             })
+                            .with_cursor_style(CursorStyle::PointingHand)
                             .on_click(MouseButton::Left, move |_, this, cx| {
-                                this.select_item(&SelectItem(ix), cx);
+                                this.set_selected_index(ix, cx);
                             })
                             .into_any()
                         }))

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

@@ -34,6 +34,27 @@ pub trait Element<V: 'static>: 'static + IntoElement<V> {
             phase: ElementPhase::Init,
         }))
     }
+
+    /// Applies a given function `then` to the current element if `condition` is true.
+    /// This function is used to conditionally modify the element based on a given condition.
+    /// If `condition` is false, it just returns the current element as it is.
+    ///
+    /// # Parameters
+    /// - `self`: The current element
+    /// - `condition`: The boolean condition based on which `then` is applied to the element.
+    /// - `then`: A function that takes in the current element and returns a possibly modified element.
+    ///
+    /// # Return
+    /// It returns the potentially modified element.
+    fn when(mut self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
+    where
+        Self: Sized,
+    {
+        if condition {
+            self = then(self);
+        }
+        self
+    }
 }
 
 /// Used to make ElementState<V, E> into a trait object, so we can wrap it in AnyElement<V>.
@@ -177,6 +198,31 @@ pub trait ParentElement<V: 'static> {
         );
         self
     }
+
+    // HACK: This is a temporary hack to get children working for the purposes
+    // of building UI on top of the current version of gpui2.
+    //
+    // We'll (hopefully) be moving away from this in the future.
+    fn children_any<I>(mut self, children: I) -> Self
+    where
+        I: IntoIterator<Item = AnyElement<V>>,
+        Self: Sized,
+    {
+        self.children_mut().extend(children.into_iter());
+        self
+    }
+
+    // HACK: This is a temporary hack to get children working for the purposes
+    // of building UI on top of the current version of gpui2.
+    //
+    // We'll (hopefully) be moving away from this in the future.
+    fn child_any(mut self, children: AnyElement<V>) -> Self
+    where
+        Self: Sized,
+    {
+        self.children_mut().push(children);
+        self
+    }
 }
 
 pub trait IntoElement<V: 'static> {

crates/gpui2/src/elements/pressable.rs πŸ”—

@@ -73,10 +73,15 @@ impl<V: 'static, E: Element<V> + Styleable> Element<V> for Pressable<E> {
                 if bounds.contains_point(event.position) {
                     pressed.set(true);
                     cx.repaint();
+                } else {
+                    cx.bubble_event();
                 }
-            } else if pressed.get() {
-                pressed.set(false);
-                cx.repaint();
+            } else {
+                if pressed.get() {
+                    pressed.set(false);
+                    cx.repaint();
+                }
+                cx.bubble_event();
             }
         });
 

crates/gpui2/src/elements/text.rs πŸ”—

@@ -5,7 +5,7 @@ use crate::{
 use anyhow::Result;
 use gpui::{
     geometry::{vector::Vector2F, Size},
-    text_layout::LineLayout,
+    text_layout::Line,
     LayoutId,
 };
 use parking_lot::Mutex;
@@ -42,7 +42,7 @@ impl<V: 'static> Element<V> for Text {
         _view: &mut V,
         cx: &mut ViewContext<V>,
     ) -> Result<(LayoutId, Self::PaintState)> {
-        let fonts = cx.platform().fonts();
+        let layout_cache = cx.text_layout_cache().clone();
         let text_style = cx.text_style();
         let line_height = cx.font_cache().line_height(text_style.font_size);
         let text = self.text.clone();
@@ -51,14 +51,14 @@ impl<V: 'static> Element<V> for Text {
         let layout_id = cx.add_measured_layout_node(Default::default(), {
             let paint_state = paint_state.clone();
             move |_params| {
-                let line_layout = fonts.layout_line(
+                let line_layout = layout_cache.layout_str(
                     text.as_ref(),
                     text_style.font_size,
                     &[(text.len(), text_style.to_run())],
                 );
 
                 let size = Size {
-                    width: line_layout.width,
+                    width: line_layout.width(),
                     height: line_height,
                 };
 
@@ -95,13 +95,9 @@ impl<V: 'static> Element<V> for Text {
             line_height = paint_state.line_height;
         }
 
-        let text_style = cx.text_style();
-        let line =
-            gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]);
-
         // TODO: We haven't added visible bounds to the new element system yet, so this is a placeholder.
         let visible_bounds = bounds;
-        line.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
+        line_layout.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
     }
 }
 
@@ -114,6 +110,6 @@ impl<V: 'static> IntoElement<V> for Text {
 }
 
 pub struct TextLayout {
-    line_layout: Arc<LineLayout>,
+    line_layout: Arc<Line>,
     line_height: f32,
 }

crates/gpui2/src/style.rs πŸ”—

@@ -316,199 +316,120 @@ pub trait Styleable {
     }
 }
 
+use crate as gpui2;
+
 // Helpers methods that take and return mut self. This includes tailwind style methods for standard sizes etc.
 //
 // Example:
 // // Sets the padding to 0.5rem, just like class="p-2" in Tailwind.
-// fn p_2(mut self) -> Self where Self: Sized;
-pub trait StyleHelpers: Styleable<Style = Style> {
+// fn p_2(mut self) -> Self;
+pub trait StyleHelpers: Sized + Styleable<Style = Style> {
     styleable_helpers!();
 
-    fn h(mut self, height: Length) -> Self
-    where
-        Self: Sized,
-    {
-        self.declared_style().size.height = Some(height);
+    fn full(mut self) -> Self {
+        self.declared_style().size.width = Some(relative(1.).into());
+        self.declared_style().size.height = Some(relative(1.).into());
         self
     }
 
-    /// size_{n}: Sets width & height to {n}
-    ///
-    /// Example:
-    /// size_1: Sets width & height to 1
-    fn size(mut self, size: Length) -> Self
-    where
-        Self: Sized,
-    {
-        self.declared_style().size.height = Some(size);
-        self.declared_style().size.width = Some(size);
-        self
-    }
-
-    fn full(mut self) -> Self
-    where
-        Self: Sized,
-    {
-        self.declared_style().size.width = Some(relative(1.));
-        self.declared_style().size.height = Some(relative(1.));
-        self
-    }
-
-    fn relative(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn relative(mut self) -> Self {
         self.declared_style().position = Some(Position::Relative);
         self
     }
 
-    fn absolute(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn absolute(mut self) -> Self {
         self.declared_style().position = Some(Position::Absolute);
         self
     }
 
-    fn block(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn block(mut self) -> Self {
         self.declared_style().display = Some(Display::Block);
         self
     }
 
-    fn flex(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex(mut self) -> Self {
         self.declared_style().display = Some(Display::Flex);
         self
     }
 
-    fn flex_col(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_col(mut self) -> Self {
         self.declared_style().flex_direction = Some(FlexDirection::Column);
         self
     }
 
-    fn flex_row(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_row(mut self) -> Self {
         self.declared_style().flex_direction = Some(FlexDirection::Row);
         self
     }
 
-    fn flex_1(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_1(mut self) -> Self {
         self.declared_style().flex_grow = Some(1.);
         self.declared_style().flex_shrink = Some(1.);
-        self.declared_style().flex_basis = Some(relative(0.));
+        self.declared_style().flex_basis = Some(relative(0.).into());
         self
     }
 
-    fn flex_auto(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_auto(mut self) -> Self {
         self.declared_style().flex_grow = Some(1.);
         self.declared_style().flex_shrink = Some(1.);
         self.declared_style().flex_basis = Some(Length::Auto);
         self
     }
 
-    fn flex_initial(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_initial(mut self) -> Self {
         self.declared_style().flex_grow = Some(0.);
         self.declared_style().flex_shrink = Some(1.);
         self.declared_style().flex_basis = Some(Length::Auto);
         self
     }
 
-    fn flex_none(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_none(mut self) -> Self {
         self.declared_style().flex_grow = Some(0.);
         self.declared_style().flex_shrink = Some(0.);
         self
     }
 
-    fn grow(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn grow(mut self) -> Self {
         self.declared_style().flex_grow = Some(1.);
         self
     }
 
-    fn items_start(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_start(mut self) -> Self {
         self.declared_style().align_items = Some(AlignItems::FlexStart);
         self
     }
 
-    fn items_end(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_end(mut self) -> Self {
         self.declared_style().align_items = Some(AlignItems::FlexEnd);
         self
     }
 
-    fn items_center(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_center(mut self) -> Self {
         self.declared_style().align_items = Some(AlignItems::Center);
         self
     }
 
-    fn justify_between(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_between(mut self) -> Self {
         self.declared_style().justify_content = Some(JustifyContent::SpaceBetween);
         self
     }
 
-    fn justify_center(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_center(mut self) -> Self {
         self.declared_style().justify_content = Some(JustifyContent::Center);
         self
     }
 
-    fn justify_start(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_start(mut self) -> Self {
         self.declared_style().justify_content = Some(JustifyContent::Start);
         self
     }
 
-    fn justify_end(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_end(mut self) -> Self {
         self.declared_style().justify_content = Some(JustifyContent::End);
         self
     }
 
-    fn justify_around(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_around(mut self) -> Self {
         self.declared_style().justify_content = Some(JustifyContent::SpaceAround);
         self
     }
@@ -516,7 +437,6 @@ pub trait StyleHelpers: Styleable<Style = Style> {
     fn fill<F>(mut self, fill: F) -> Self
     where
         F: Into<Fill>,
-        Self: Sized,
     {
         self.declared_style().fill = Some(fill.into());
         self
@@ -525,7 +445,6 @@ pub trait StyleHelpers: Styleable<Style = Style> {
     fn border_color<C>(mut self, border_color: C) -> Self
     where
         C: Into<Hsla>,
-        Self: Sized,
     {
         self.declared_style().border_color = Some(border_color.into());
         self
@@ -534,72 +453,47 @@ pub trait StyleHelpers: Styleable<Style = Style> {
     fn text_color<C>(mut self, color: C) -> Self
     where
         C: Into<Hsla>,
-        Self: Sized,
     {
         self.declared_style().text_color = Some(color.into());
         self
     }
 
-    fn text_xs(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_xs(mut self) -> Self {
         self.declared_style().font_size = Some(0.75);
         self
     }
 
-    fn text_sm(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_sm(mut self) -> Self {
         self.declared_style().font_size = Some(0.875);
         self
     }
 
-    fn text_base(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_base(mut self) -> Self {
         self.declared_style().font_size = Some(1.0);
         self
     }
 
-    fn text_lg(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_lg(mut self) -> Self {
         self.declared_style().font_size = Some(1.125);
         self
     }
 
-    fn text_xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_xl(mut self) -> Self {
         self.declared_style().font_size = Some(1.25);
         self
     }
 
-    fn text_2xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_2xl(mut self) -> Self {
         self.declared_style().font_size = Some(1.5);
         self
     }
 
-    fn text_3xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_3xl(mut self) -> Self {
         self.declared_style().font_size = Some(1.875);
         self
     }
 
-    fn font(mut self, family_name: impl Into<Arc<str>>) -> Self
-    where
-        Self: Sized,
-    {
+    fn font(mut self, family_name: impl Into<Arc<str>>) -> Self {
         self.declared_style().font_family = Some(family_name.into());
         self
     }

crates/gpui2_macros/src/styleable_helpers.rs πŸ”—

@@ -28,53 +28,106 @@ fn generate_methods() -> Vec<TokenStream2> {
     let mut methods = Vec::new();
 
     for (prefix, auto_allowed, fields) in box_prefixes() {
-        for (suffix, length_tokens) in box_suffixes() {
-            if auto_allowed || suffix != "auto" {
-                let method = generate_method(prefix, suffix, &fields, length_tokens);
-                methods.push(method);
+        methods.push(generate_custom_value_setter(
+            prefix,
+            if auto_allowed {
+                quote! { Length }
+            } else {
+                quote! { DefiniteLength }
+            },
+            &fields,
+        ));
+
+        for (suffix, length_tokens, doc_string) in box_suffixes() {
+            if suffix != "auto" || auto_allowed {
+                methods.push(generate_predefined_setter(
+                    prefix,
+                    suffix,
+                    &fields,
+                    &length_tokens,
+                    false,
+                    doc_string,
+                ));
+            }
+
+            if suffix != "auto" {
+                methods.push(generate_predefined_setter(
+                    prefix,
+                    suffix,
+                    &fields,
+                    &length_tokens,
+                    true,
+                    doc_string,
+                ));
             }
         }
     }
 
     for (prefix, fields) in corner_prefixes() {
-        for (suffix, radius_tokens) in corner_suffixes() {
-            let method = generate_method(prefix, suffix, &fields, radius_tokens);
-            methods.push(method);
+        methods.push(generate_custom_value_setter(
+            prefix,
+            quote! { AbsoluteLength },
+            &fields,
+        ));
+
+        for (suffix, radius_tokens, doc_string) in corner_suffixes() {
+            methods.push(generate_predefined_setter(
+                prefix,
+                suffix,
+                &fields,
+                &radius_tokens,
+                false,
+                doc_string,
+            ));
         }
     }
 
     for (prefix, fields) in border_prefixes() {
-        for (suffix, width_tokens) in border_suffixes() {
-            let method = generate_method(prefix, suffix, &fields, width_tokens);
-            methods.push(method);
+        for (suffix, width_tokens, doc_string) in border_suffixes() {
+            methods.push(generate_predefined_setter(
+                prefix,
+                suffix,
+                &fields,
+                &width_tokens,
+                false,
+                doc_string,
+            ));
         }
     }
-
     methods
 }
 
-fn generate_method(
-    prefix: &'static str,
-    suffix: &'static str,
+fn generate_predefined_setter(
+    name: &'static str,
+    length: &'static str,
     fields: &Vec<TokenStream2>,
-    length_tokens: TokenStream2,
+    length_tokens: &TokenStream2,
+    negate: bool,
+    doc_string: &'static str,
 ) -> TokenStream2 {
-    let method_name = if suffix.is_empty() {
-        format_ident!("{}", prefix)
+    let (negation_prefix, negation_token) = if negate {
+        ("neg_", quote! { - })
     } else {
-        format_ident!("{}_{}", prefix, suffix)
+        ("", quote! {})
+    };
+
+    let method_name = if length.is_empty() {
+        format_ident!("{}{}", negation_prefix, name)
+    } else {
+        format_ident!("{}{}_{}", negation_prefix, name, length)
     };
 
     let field_assignments = fields
         .iter()
         .map(|field_tokens| {
             quote! {
-                style.#field_tokens = Some(gpui::geometry::#length_tokens);
+                style.#field_tokens = Some((#negation_token gpui2::geometry::#length_tokens).into());
             }
         })
         .collect::<Vec<_>>();
 
     let method = quote! {
+        #[doc = #doc_string]
         fn #method_name(mut self) -> Self where Self: std::marker::Sized {
             let mut style = self.declared_style();
             #(#field_assignments)*
@@ -85,6 +138,37 @@ fn generate_method(
     method
 }
 
+fn generate_custom_value_setter(
+    prefix: &'static str,
+    length_type: TokenStream2,
+    fields: &Vec<TokenStream2>,
+) -> TokenStream2 {
+    let method_name = format_ident!("{}", prefix);
+
+    let mut iter = fields.into_iter();
+    let last = iter.next_back().unwrap();
+    let field_assignments = iter
+        .map(|field_tokens| {
+            quote! {
+                style.#field_tokens = Some(length.clone().into());
+            }
+        })
+        .chain(std::iter::once(quote! {
+            style.#last = Some(length.into());
+        }))
+        .collect::<Vec<_>>();
+
+    let method = quote! {
+        fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui2::geometry::#length_type>) -> Self where Self: std::marker::Sized {
+            let mut style = self.declared_style();
+            #(#field_assignments)*
+            self
+        }
+    };
+
+    method
+}
+
 fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
     vec![
         ("w", true, vec![quote! { size.width }]),
@@ -94,10 +178,10 @@ fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
             true,
             vec![quote! {size.width}, quote! {size.height}],
         ),
-        ("min_w", false, vec![quote! { min_size.width }]),
-        ("min_h", false, vec![quote! { min_size.height }]),
-        ("max_w", false, vec![quote! { max_size.width }]),
-        ("max_h", false, vec![quote! { max_size.height }]),
+        ("min_w", true, vec![quote! { min_size.width }]),
+        ("min_h", true, vec![quote! { min_size.height }]),
+        ("max_w", true, vec![quote! { max_size.width }]),
+        ("max_h", true, vec![quote! { max_size.height }]),
         (
             "m",
             true,
@@ -160,55 +244,52 @@ fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>)> {
     ]
 }
 
-fn box_suffixes() -> Vec<(&'static str, TokenStream2)> {
+fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
     vec![
-        ("0", quote! { pixels(0.) }),
-        ("0p5", quote! { rems(0.125) }),
-        ("1", quote! { rems(0.25) }),
-        ("1p5", quote! { rems(0.375) }),
-        ("2", quote! { rems(0.5) }),
-        ("2p5", quote! { rems(0.625) }),
-        ("3", quote! { rems(0.75) }),
-        ("3p5", quote! { rems(0.875) }),
-        ("4", quote! { rems(1.) }),
-        ("5", quote! { rems(1.25) }),
-        ("6", quote! { rems(1.5) }),
-        ("7", quote! { rems(1.75) }),
-        ("8", quote! { rems(2.0) }),
-        ("9", quote! { rems(2.25) }),
-        ("10", quote! { rems(2.5) }),
-        ("11", quote! { rems(2.75) }),
-        ("12", quote! { rems(3.) }),
-        ("16", quote! { rems(4.) }),
-        ("20", quote! { rems(5.) }),
-        ("24", quote! { rems(6.) }),
-        ("32", quote! { rems(8.) }),
-        ("40", quote! { rems(10.) }),
-        ("48", quote! { rems(12.) }),
-        ("56", quote! { rems(14.) }),
-        ("64", quote! { rems(16.) }),
-        ("72", quote! { rems(18.) }),
-        ("80", quote! { rems(20.) }),
-        ("96", quote! { rems(24.) }),
-        ("auto", quote! { auto() }),
-        ("px", quote! { pixels(1.) }),
-        ("full", quote! { relative(1.) }),
-        ("1_2", quote! { relative(0.5) }),
-        ("1_3", quote! { relative(1./3.) }),
-        ("2_3", quote! { relative(2./3.) }),
-        ("1_4", quote! { relative(0.25) }),
-        ("2_4", quote! { relative(0.5) }),
-        ("3_4", quote! { relative(0.75) }),
-        ("1_5", quote! { relative(0.2) }),
-        ("2_5", quote! { relative(0.4) }),
-        ("3_5", quote! { relative(0.6) }),
-        ("4_5", quote! { relative(0.8) }),
-        ("1_6", quote! { relative(1./6.) }),
-        ("5_6", quote! { relative(5./6.) }),
-        ("1_12", quote! { relative(1./12.) }),
-        // ("screen_50", quote! { DefiniteLength::Vh(50.0) }),
-        // ("screen_75", quote! { DefiniteLength::Vh(75.0) }),
-        // ("screen", quote! { DefiniteLength::Vh(100.0) }),
+        ("0", quote! { pixels(0.) }, "0px"),
+        ("0p5", quote! { rems(0.125) }, "2px (0.125rem)"),
+        ("1", quote! { rems(0.25) }, "4px (0.25rem)"),
+        ("1p5", quote! { rems(0.375) }, "6px (0.375rem)"),
+        ("2", quote! { rems(0.5) }, "8px (0.5rem)"),
+        ("2p5", quote! { rems(0.625) }, "10px (0.625rem)"),
+        ("3", quote! { rems(0.75) }, "12px (0.75rem)"),
+        ("3p5", quote! { rems(0.875) }, "14px (0.875rem)"),
+        ("4", quote! { rems(1.) }, "16px (1rem)"),
+        ("5", quote! { rems(1.25) }, "20px (1.25rem)"),
+        ("6", quote! { rems(1.5) }, "24px (1.5rem)"),
+        ("7", quote! { rems(1.75) }, "28px (1.75rem)"),
+        ("8", quote! { rems(2.0) }, "32px (2rem)"),
+        ("9", quote! { rems(2.25) }, "36px (2.25rem)"),
+        ("10", quote! { rems(2.5) }, "40px (2.5rem)"),
+        ("11", quote! { rems(2.75) }, "44px (2.75rem)"),
+        ("12", quote! { rems(3.) }, "48px (3rem)"),
+        ("16", quote! { rems(4.) }, "64px (4rem)"),
+        ("20", quote! { rems(5.) }, "80px (5rem)"),
+        ("24", quote! { rems(6.) }, "96px (6rem)"),
+        ("32", quote! { rems(8.) }, "128px (8rem)"),
+        ("40", quote! { rems(10.) }, "160px (10rem)"),
+        ("48", quote! { rems(12.) }, "192px (12rem)"),
+        ("56", quote! { rems(14.) }, "224px (14rem)"),
+        ("64", quote! { rems(16.) }, "256px (16rem)"),
+        ("72", quote! { rems(18.) }, "288px (18rem)"),
+        ("80", quote! { rems(20.) }, "320px (20rem)"),
+        ("96", quote! { rems(24.) }, "384px (24rem)"),
+        ("auto", quote! { auto() }, "Auto"),
+        ("px", quote! { pixels(1.) }, "1px"),
+        ("full", quote! { relative(1.) }, "100%"),
+        ("1_2", quote! { relative(0.5) }, "50% (1/2)"),
+        ("1_3", quote! { relative(1./3.) }, "33% (1/3)"),
+        ("2_3", quote! { relative(2./3.) }, "66% (2/3)"),
+        ("1_4", quote! { relative(0.25) }, "25% (1/4)"),
+        ("2_4", quote! { relative(0.5) }, "50% (2/4)"),
+        ("3_4", quote! { relative(0.75) }, "75% (3/4)"),
+        ("1_5", quote! { relative(0.2) }, "20% (1/5)"),
+        ("2_5", quote! { relative(0.4) }, "40% (2/5)"),
+        ("3_5", quote! { relative(0.6) }, "60% (3/5)"),
+        ("4_5", quote! { relative(0.8) }, "80% (4/5)"),
+        ("1_6", quote! { relative(1./6.) }, "16% (1/6)"),
+        ("5_6", quote! { relative(5./6.) }, "80% (5/6)"),
+        ("1_12", quote! { relative(1./12.) }, "8% (1/12)"),
     ]
 }
 
@@ -258,16 +339,16 @@ fn corner_prefixes() -> Vec<(&'static str, Vec<TokenStream2>)> {
     ]
 }
 
-fn corner_suffixes() -> Vec<(&'static str, TokenStream2)> {
+fn corner_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
     vec![
-        ("none", quote! { pixels(0.) }),
-        ("sm", quote! { rems(0.125) }),
-        ("md", quote! { rems(0.25) }),
-        ("lg", quote! { rems(0.5) }),
-        ("xl", quote! { rems(0.75) }),
-        ("2xl", quote! { rems(1.) }),
-        ("3xl", quote! { rems(1.5) }),
-        ("full", quote! {  pixels(9999.) }),
+        ("none", quote! { pixels(0.) }, "0px"),
+        ("sm", quote! { rems(0.125) }, "2px (0.125rem)"),
+        ("md", quote! { rems(0.25) }, "4px (0.25rem)"),
+        ("lg", quote! { rems(0.5) }, "8px (0.5rem)"),
+        ("xl", quote! { rems(0.75) }, "12px (0.75rem)"),
+        ("2xl", quote! { rems(1.) }, "16px (1rem)"),
+        ("3xl", quote! { rems(1.5) }, "24px (1.5rem)"),
+        ("full", quote! {  pixels(9999.) }, "9999px"),
     ]
 }
 
@@ -303,25 +384,25 @@ fn border_prefixes() -> Vec<(&'static str, Vec<TokenStream2>)> {
     ]
 }
 
-fn border_suffixes() -> Vec<(&'static str, TokenStream2)> {
+fn border_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
     vec![
-        ("", quote! { pixels(1.) }),
-        ("0", quote! { pixels(0.) }),
-        ("1", quote! { pixels(1.) }),
-        ("2", quote! { pixels(2.) }),
-        ("3", quote! { pixels(3.) }),
-        ("4", quote! { pixels(4.) }),
-        ("5", quote! { pixels(5.) }),
-        ("6", quote! { pixels(6.) }),
-        ("7", quote! { pixels(7.) }),
-        ("8", quote! { pixels(8.) }),
-        ("9", quote! { pixels(9.) }),
-        ("10", quote! { pixels(10.) }),
-        ("11", quote! { pixels(11.) }),
-        ("12", quote! { pixels(12.) }),
-        ("16", quote! { pixels(16.) }),
-        ("20", quote! { pixels(20.) }),
-        ("24", quote! { pixels(24.) }),
-        ("32", quote! { pixels(32.) }),
+        ("", quote! { pixels(1.)}, "1px"),
+        ("0", quote! { pixels(0.)}, "0px"),
+        ("1", quote! { pixels(1.) }, "1px"),
+        ("2", quote! { pixels(2.) }, "2px"),
+        ("3", quote! { pixels(3.) }, "3px"),
+        ("4", quote! { pixels(4.) }, "4px"),
+        ("5", quote! { pixels(5.) }, "5px"),
+        ("6", quote! { pixels(6.) }, "6px"),
+        ("7", quote! { pixels(7.) }, "7px"),
+        ("8", quote! { pixels(8.) }, "8px"),
+        ("9", quote! { pixels(9.) }, "9px"),
+        ("10", quote! { pixels(10.) }, "10px"),
+        ("11", quote! { pixels(11.) }, "11px"),
+        ("12", quote! { pixels(12.) }, "12px"),
+        ("16", quote! { pixels(16.) }, "16px"),
+        ("20", quote! { pixels(20.) }, "20px"),
+        ("24", quote! { pixels(24.) }, "24px"),
+        ("32", quote! { pixels(32.) }, "32px"),
     ]
 }

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

@@ -8,8 +8,8 @@ use crate::{
     language_settings::{language_settings, LanguageSettings},
     outline::OutlineItem,
     syntax_map::{
-        SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot,
-        ToTreeSitterPoint,
+        SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
+        SyntaxSnapshot, ToTreeSitterPoint,
     },
     CodeLabel, LanguageScope, Outline,
 };
@@ -660,12 +660,12 @@ impl Buffer {
             file_changed = true;
         };
 
+        self.file = Some(new_file);
         if file_changed {
             self.file_update_count += 1;
             cx.emit(Event::FileHandleChanged);
             cx.notify();
         }
-        self.file = Some(new_file);
         task
     }
 
@@ -2467,6 +2467,14 @@ impl BufferSnapshot {
         Some(items)
     }
 
+    pub fn matches(
+        &self,
+        range: Range<usize>,
+        query: fn(&Grammar) -> Option<&tree_sitter::Query>,
+    ) -> SyntaxMapMatches {
+        self.syntax.matches(range, self, query)
+    }
+
     /// Returns bracket range pairs overlapping or adjacent to `range`
     pub fn bracket_ranges<'a, T: ToOffset>(
         &'a self,

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

@@ -1427,7 +1427,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex
         // Insert the block at column zero. The entire block is indented
         // so that the first line matches the previous line's indentation.
         buffer.edit(
-            [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
+            [(Point::new(2, 0)..Point::new(2, 0), inserted_text)],
             Some(AutoindentMode::Block {
                 original_indent_columns: original_indent_columns.clone(),
             }),

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

@@ -13,7 +13,7 @@ use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use collections::{HashMap, HashSet};
 use futures::{
-    channel::oneshot,
+    channel::{mpsc, oneshot},
     future::{BoxFuture, Shared},
     FutureExt, TryFutureExt as _,
 };
@@ -48,9 +48,6 @@ use unicase::UniCase;
 use util::{http::HttpClient, paths::PathExt};
 use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
 
-#[cfg(any(test, feature = "test-support"))]
-use futures::channel::mpsc;
-
 pub use buffer::Operation;
 pub use buffer::*;
 pub use diagnostic_set::DiagnosticEntry;
@@ -64,6 +61,27 @@ pub fn init(cx: &mut AppContext) {
     language_settings::init(cx);
 }
 
+#[derive(Clone, Default)]
+struct LspBinaryStatusSender {
+    txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(Arc<Language>, LanguageServerBinaryStatus)>>>>,
+}
+
+impl LspBinaryStatusSender {
+    fn subscribe(&self) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
+        let (tx, rx) = mpsc::unbounded();
+        self.txs.lock().push(tx);
+        rx
+    }
+
+    fn send(&self, language: Arc<Language>, status: LanguageServerBinaryStatus) {
+        let mut txs = self.txs.lock();
+        txs.retain(|tx| {
+            tx.unbounded_send((language.clone(), status.clone()))
+                .is_ok()
+        });
+    }
+}
+
 thread_local! {
     static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
 }
@@ -594,14 +612,13 @@ struct AvailableLanguage {
 pub struct LanguageRegistry {
     state: RwLock<LanguageRegistryState>,
     language_server_download_dir: Option<Arc<Path>>,
-    lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
-    lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
     login_shell_env_loaded: Shared<Task<()>>,
     #[allow(clippy::type_complexity)]
     lsp_binary_paths: Mutex<
         HashMap<LanguageServerName, Shared<Task<Result<LanguageServerBinary, Arc<anyhow::Error>>>>>,
     >,
     executor: Option<Arc<Background>>,
+    lsp_binary_status_tx: LspBinaryStatusSender,
 }
 
 struct LanguageRegistryState {
@@ -624,7 +641,6 @@ pub struct PendingLanguageServer {
 
 impl LanguageRegistry {
     pub fn new(login_shell_env_loaded: Task<()>) -> Self {
-        let (lsp_binary_statuses_tx, lsp_binary_statuses_rx) = async_broadcast::broadcast(16);
         Self {
             state: RwLock::new(LanguageRegistryState {
                 next_language_server_id: 0,
@@ -638,11 +654,10 @@ impl LanguageRegistry {
                 reload_count: 0,
             }),
             language_server_download_dir: None,
-            lsp_binary_statuses_tx,
-            lsp_binary_statuses_rx,
             login_shell_env_loaded: login_shell_env_loaded.shared(),
             lsp_binary_paths: Default::default(),
             executor: None,
+            lsp_binary_status_tx: Default::default(),
         }
     }
 
@@ -918,8 +933,8 @@ impl LanguageRegistry {
         let container_dir: Arc<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 lsp_binary_statuses = self.lsp_binary_status_tx.clone();
 
         let task = {
             let container_dir = container_dir.clone();
@@ -976,8 +991,8 @@ impl LanguageRegistry {
 
     pub fn language_server_binary_statuses(
         &self,
-    ) -> async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)> {
-        self.lsp_binary_statuses_rx.clone()
+    ) -> mpsc::UnboundedReceiver<(Arc<Language>, LanguageServerBinaryStatus)> {
+        self.lsp_binary_status_tx.subscribe()
     }
 
     pub fn delete_server_container(
@@ -1003,6 +1018,10 @@ impl LanguageRegistry {
                 .log_err();
         })
     }
+
+    pub fn next_language_server_id(&self) -> LanguageServerId {
+        self.state.write().next_language_server_id()
+    }
 }
 
 impl LanguageRegistryState {
@@ -1054,7 +1073,7 @@ async fn get_binary(
     language: Arc<Language>,
     delegate: Arc<dyn LspAdapterDelegate>,
     container_dir: Arc<Path>,
-    statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
+    statuses: LspBinaryStatusSender,
     mut cx: AsyncAppContext,
 ) -> Result<LanguageServerBinary> {
     if !container_dir.exists() {
@@ -1081,19 +1100,15 @@ async fn get_binary(
             .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
             .await
         {
-            statuses
-                .broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
-                .await?;
+            statuses.send(language.clone(), LanguageServerBinaryStatus::Cached);
             return Ok(binary);
         } else {
-            statuses
-                .broadcast((
-                    language.clone(),
-                    LanguageServerBinaryStatus::Failed {
-                        error: format!("{:?}", error),
-                    },
-                ))
-                .await?;
+            statuses.send(
+                language.clone(),
+                LanguageServerBinaryStatus::Failed {
+                    error: format!("{:?}", error),
+                },
+            );
         }
     }
 
@@ -1105,27 +1120,21 @@ async fn fetch_latest_binary(
     language: Arc<Language>,
     delegate: &dyn LspAdapterDelegate,
     container_dir: &Path,
-    lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
+    lsp_binary_statuses_tx: LspBinaryStatusSender,
 ) -> Result<LanguageServerBinary> {
     let container_dir: Arc<Path> = container_dir.into();
-    lsp_binary_statuses_tx
-        .broadcast((
-            language.clone(),
-            LanguageServerBinaryStatus::CheckingForUpdate,
-        ))
-        .await?;
+    lsp_binary_statuses_tx.send(
+        language.clone(),
+        LanguageServerBinaryStatus::CheckingForUpdate,
+    );
 
     let version_info = adapter.fetch_latest_server_version(delegate).await?;
-    lsp_binary_statuses_tx
-        .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
-        .await?;
+    lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading);
 
     let binary = adapter
         .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
         .await?;
-    lsp_binary_statuses_tx
-        .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
-        .await?;
+    lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloaded);
 
     Ok(binary)
 }

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

@@ -8,12 +8,12 @@ use gpui::{
         ParentElement, Stack,
     },
     platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
-    ViewHandle, WeakModelHandle,
+    AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View,
+    ViewContext, ViewHandle, WeakModelHandle,
 };
 use language::{Buffer, LanguageServerId, LanguageServerName};
 use lsp::IoKind;
-use project::{Project, Worktree};
+use project::{search::SearchQuery, Project};
 use std::{borrow::Cow, sync::Arc};
 use theme::{ui, Theme};
 use workspace::{
@@ -38,7 +38,8 @@ struct ProjectState {
 struct LanguageServerState {
     log_buffer: ModelHandle<Buffer>,
     rpc_state: Option<LanguageServerRpcState>,
-    _subscription: Option<lsp::Subscription>,
+    _io_logs_subscription: Option<lsp::Subscription>,
+    _lsp_logs_subscription: Option<lsp::Subscription>,
 }
 
 struct LanguageServerRpcState {
@@ -52,10 +53,12 @@ pub struct LspLogView {
     current_server_id: Option<LanguageServerId>,
     is_showing_rpc_trace: bool,
     project: ModelHandle<Project>,
+    _log_store_subscription: Subscription,
 }
 
 pub struct LspLogToolbarItemView {
     log_view: Option<ViewHandle<LspLogView>>,
+    _log_view_subscription: Option<Subscription>,
     menu_open: bool,
 }
 
@@ -69,7 +72,7 @@ enum MessageKind {
 pub(crate) struct LogMenuItem {
     pub server_id: LanguageServerId,
     pub server_name: LanguageServerName,
-    pub worktree: ModelHandle<Worktree>,
+    pub worktree_root_name: String,
     pub rpc_trace_enabled: bool,
     pub rpc_trace_selected: bool,
     pub logs_selected: bool,
@@ -134,8 +137,6 @@ impl LogStore {
     }
 
     pub fn add_project(&mut self, project: &ModelHandle<Project>, cx: &mut ModelContext<Self>) {
-        use project::Event::*;
-
         let weak_project = project.downgrade();
         self.projects.insert(
             weak_project,
@@ -146,13 +147,13 @@ impl LogStore {
                         this.projects.remove(&weak_project);
                     }),
                     cx.subscribe(project, |this, project, event, cx| match event {
-                        LanguageServerAdded(id) => {
+                        project::Event::LanguageServerAdded(id) => {
                             this.add_language_server(&project, *id, cx);
                         }
-                        LanguageServerRemoved(id) => {
+                        project::Event::LanguageServerRemoved(id) => {
                             this.remove_language_server(&project, *id, cx);
                         }
-                        LanguageServerLog(id, message) => {
+                        project::Event::LanguageServerLog(id, message) => {
                             this.add_language_server_log(&project, *id, message, cx);
                         }
                         _ => {}
@@ -176,21 +177,44 @@ impl LogStore {
                 log_buffer: cx
                     .add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""))
                     .clone(),
-                _subscription: None,
+                _io_logs_subscription: None,
+                _lsp_logs_subscription: None,
             }
         });
 
         let server = project.read(cx).language_server_for_id(id);
+        if let Some(server) = server.as_deref() {
+            if server.has_notification_handler::<lsp::notification::LogMessage>() {
+                // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
+                return Some(server_state.log_buffer.clone());
+            }
+        }
+
         let weak_project = project.downgrade();
         let io_tx = self.io_tx.clone();
-        server_state._subscription = server.map(|server| {
+        server_state._io_logs_subscription = server.as_ref().map(|server| {
             server.on_io(move |io_kind, message| {
                 io_tx
                     .unbounded_send((weak_project, id, io_kind, message.to_string()))
                     .ok();
             })
         });
-
+        let this = cx.weak_handle();
+        let weak_project = project.downgrade();
+        server_state._lsp_logs_subscription = server.map(|server| {
+            let server_id = server.server_id();
+            server.on_notification::<lsp::notification::LogMessage, _>({
+                move |params, mut cx| {
+                    if let Some((project, this)) =
+                        weak_project.upgrade(&mut cx).zip(this.upgrade(&mut cx))
+                    {
+                        this.update(&mut cx, |this, cx| {
+                            this.add_language_server_log(&project, server_id, &params.message, cx);
+                        });
+                    }
+                }
+            })
+        });
         Some(server_state.log_buffer.clone())
     }
 
@@ -201,7 +225,16 @@ impl LogStore {
         message: &str,
         cx: &mut ModelContext<Self>,
     ) -> Option<()> {
-        let buffer = self.add_language_server(&project, id, cx)?;
+        let buffer = match self
+            .projects
+            .get_mut(&project.downgrade())?
+            .servers
+            .get(&id)
+            .map(|state| state.log_buffer.clone())
+        {
+            Some(existing_buffer) => existing_buffer,
+            None => self.add_language_server(&project, id, cx)?,
+        };
         buffer.update(cx, |buffer, cx| {
             let len = buffer.len();
             let has_newline = message.ends_with("\n");
@@ -288,19 +321,15 @@ impl LogStore {
         language_server_id: LanguageServerId,
         io_kind: IoKind,
         message: &str,
-        cx: &mut AppContext,
+        cx: &mut ModelContext<Self>,
     ) -> Option<()> {
         let is_received = match io_kind {
             IoKind::StdOut => true,
             IoKind::StdIn => false,
             IoKind::StdErr => {
                 let project = project.upgrade(cx)?;
-                project.update(cx, |_, cx| {
-                    cx.emit(project::Event::LanguageServerLog(
-                        language_server_id,
-                        format!("stderr: {}\n", message.trim()),
-                    ))
-                });
+                let message = format!("stderr: {}\n", message.trim());
+                self.add_language_server_log(&project, language_server_id, &message, cx);
                 return Some(());
             }
         };
@@ -346,12 +375,49 @@ impl LspLogView {
             .get(&project.downgrade())
             .and_then(|project| project.servers.keys().copied().next());
         let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
+        let _log_store_subscription = cx.observe(&log_store, |this, store, cx| {
+            (|| -> Option<()> {
+                let project_state = store.read(cx).projects.get(&this.project.downgrade())?;
+                if let Some(current_lsp) = this.current_server_id {
+                    if !project_state.servers.contains_key(&current_lsp) {
+                        if let Some(server) = project_state.servers.iter().next() {
+                            if this.is_showing_rpc_trace {
+                                this.show_rpc_trace_for_server(*server.0, cx)
+                            } else {
+                                this.show_logs_for_server(*server.0, cx)
+                            }
+                        } else {
+                            this.current_server_id = None;
+                            this.editor.update(cx, |editor, cx| {
+                                editor.set_read_only(false);
+                                editor.clear(cx);
+                                editor.set_read_only(true);
+                            });
+                            cx.notify();
+                        }
+                    }
+                } else {
+                    if let Some(server) = project_state.servers.iter().next() {
+                        if this.is_showing_rpc_trace {
+                            this.show_rpc_trace_for_server(*server.0, cx)
+                        } else {
+                            this.show_logs_for_server(*server.0, cx)
+                        }
+                    }
+                }
+
+                Some(())
+            })();
+
+            cx.notify();
+        });
         let mut this = Self {
             editor: Self::editor_for_buffer(project.clone(), buffer, cx),
             project,
             log_store,
             current_server_id: None,
             is_showing_rpc_trace: false,
+            _log_store_subscription,
         };
         if let Some(server_id) = server_id {
             this.show_logs_for_server(server_id, cx);
@@ -388,7 +454,7 @@ impl LspLogView {
                 Some(LogMenuItem {
                     server_id,
                     server_name: language_server_name,
-                    worktree,
+                    worktree_root_name: worktree.read(cx).root_name().to_string(),
                     rpc_trace_enabled: state.rpc_state.is_some(),
                     rpc_trace_selected: self.is_showing_rpc_trace
                         && self.current_server_id == Some(server_id),
@@ -396,6 +462,24 @@ impl LspLogView {
                         && self.current_server_id == Some(server_id),
                 })
             })
+            .chain(
+                self.project
+                    .read(cx)
+                    .supplementary_language_servers()
+                    .filter_map(|(&server_id, (name, _))| {
+                        let state = state.servers.get(&server_id)?;
+                        Some(LogMenuItem {
+                            server_id,
+                            server_name: name.clone(),
+                            worktree_root_name: "supplementary".to_string(),
+                            rpc_trace_enabled: state.rpc_state.is_some(),
+                            rpc_trace_selected: self.is_showing_rpc_trace
+                                && self.current_server_id == Some(server_id),
+                            logs_selected: !self.is_showing_rpc_trace
+                                && self.current_server_id == Some(server_id),
+                        })
+                    }),
+            )
             .collect::<Vec<_>>();
         rows.sort_by_key(|row| row.server_id);
         rows.dedup_by_key(|row| row.server_id);
@@ -524,12 +608,24 @@ impl SearchableItem for LspLogView {
 
     fn find_matches(
         &mut self,
-        query: project::search::SearchQuery,
+        query: Arc<project::search::SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> gpui::Task<Vec<Self::Match>> {
         self.editor.update(cx, |e, cx| e.find_matches(query, cx))
     }
 
+    fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
+        // Since LSP Log is read-only, it doesn't make sense to support replace operation.
+    }
+    fn supported_options() -> workspace::searchable::SearchOptions {
+        workspace::searchable::SearchOptions {
+            case: true,
+            word: true,
+            regex: true,
+            // LSP log is read-only.
+            replacement: false,
+        }
+    }
     fn active_match_index(
         &mut self,
         matches: Vec<Self::Match>,
@@ -544,18 +640,22 @@ impl ToolbarItemView for LspLogToolbarItemView {
     fn set_active_pane_item(
         &mut self,
         active_pane_item: Option<&dyn ItemHandle>,
-        _: &mut ViewContext<Self>,
+        cx: &mut ViewContext<Self>,
     ) -> workspace::ToolbarItemLocation {
         self.menu_open = false;
         if let Some(item) = active_pane_item {
             if let Some(log_view) = item.downcast::<LspLogView>() {
                 self.log_view = Some(log_view.clone());
+                self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
+                    cx.notify();
+                }));
                 return ToolbarItemLocation::PrimaryLeft {
                     flex: Some((1., false)),
                 };
             }
         }
         self.log_view = None;
+        self._log_view_subscription = None;
         ToolbarItemLocation::Hidden
     }
 }
@@ -597,11 +697,12 @@ impl View for LspLogToolbarItemView {
                     Overlay::new(
                         MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
                             Flex::column()
+                                .scrollable::<Self>(0, None, cx)
                                 .with_children(menu_rows.into_iter().map(|row| {
                                     Self::render_language_server_menu_item(
                                         row.server_id,
                                         row.server_name,
-                                        row.worktree,
+                                        &row.worktree_root_name,
                                         row.rpc_trace_enabled,
                                         row.logs_selected,
                                         row.rpc_trace_selected,
@@ -685,6 +786,7 @@ impl LspLogToolbarItemView {
         Self {
             menu_open: false,
             log_view: None,
+            _log_view_subscription: None,
         }
     }
 
@@ -733,15 +835,14 @@ impl LspLogToolbarItemView {
         cx: &mut ViewContext<Self>,
     ) -> impl Element<Self> {
         enum ToggleMenu {}
-        MouseEventHandler::new::<ToggleMenu, _>(0, cx, move |state, cx| {
+        MouseEventHandler::new::<ToggleMenu, _>(0, cx, move |state, _| {
             let label: Cow<str> = current_server
                 .and_then(|row| {
-                    let worktree = row.worktree.read(cx);
                     Some(
                         format!(
                             "{} ({}) - {}",
                             row.server_name.0,
-                            worktree.root_name(),
+                            row.worktree_root_name,
                             if row.rpc_trace_selected {
                                 RPC_MESSAGES
                             } else {
@@ -766,7 +867,7 @@ impl LspLogToolbarItemView {
     fn render_language_server_menu_item(
         id: LanguageServerId,
         name: LanguageServerName,
-        worktree: ModelHandle<Worktree>,
+        worktree_root_name: &str,
         rpc_trace_enabled: bool,
         logs_selected: bool,
         rpc_trace_selected: bool,
@@ -780,7 +881,7 @@ impl LspLogToolbarItemView {
             .with_child({
                 let style = &theme.toolbar_dropdown_menu.section_header;
                 Label::new(
-                    format!("{} ({})", name.0, worktree.read(cx).root_name()),
+                    format!("{} ({})", name.0, worktree_root_name),
                     style.text.clone(),
                 )
                 .contained()

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

@@ -77,7 +77,14 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
             &[LogMenuItem {
                 server_id: language_server.server.server_id(),
                 server_name: LanguageServerName("the-rust-language-server".into()),
-                worktree: project.read(cx).worktrees(cx).next().unwrap(),
+                worktree_root_name: project
+                    .read(cx)
+                    .worktrees(cx)
+                    .next()
+                    .unwrap()
+                    .read(cx)
+                    .root_name()
+                    .to_string(),
                 rpc_trace_enabled: false,
                 rpc_trace_selected: false,
                 logs_selected: true,

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

@@ -605,6 +605,10 @@ impl LanguageServer {
         self.notification_handlers.lock().remove(T::METHOD);
     }
 
+    pub fn has_notification_handler<T: notification::Notification>(&self) -> bool {
+        self.notification_handlers.lock().contains_key(T::METHOD)
+    }
+
     #[must_use]
     pub fn on_custom_notification<Params, F>(&self, method: &'static str, mut f: F) -> Subscription
     where
@@ -712,11 +716,11 @@ impl LanguageServer {
         }
     }
 
-    pub fn name<'a>(self: &'a Arc<Self>) -> &'a str {
+    pub fn name(&self) -> &str {
         &self.name
     }
 
-    pub fn capabilities<'a>(self: &'a Arc<Self>) -> &'a ServerCapabilities {
+    pub fn capabilities(&self) -> &ServerCapabilities {
         &self.capabilities
     }
 

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

@@ -25,7 +25,8 @@ pub struct Picker<D: PickerDelegate> {
     max_size: Vector2F,
     theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
     confirmed: bool,
-    pending_update_matches: Task<Option<()>>,
+    pending_update_matches: Option<Task<Option<()>>>,
+    confirm_on_update: Option<bool>,
     has_focus: bool,
 }
 
@@ -208,7 +209,8 @@ impl<D: PickerDelegate> Picker<D> {
             max_size: vec2f(540., 420.),
             theme,
             confirmed: false,
-            pending_update_matches: Task::ready(None),
+            pending_update_matches: None,
+            confirm_on_update: None,
             has_focus: false,
         };
         this.update_matches(String::new(), cx);
@@ -263,11 +265,13 @@ impl<D: PickerDelegate> Picker<D> {
     pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
         let update = self.delegate.update_matches(query, cx);
         self.matches_updated(cx);
-        self.pending_update_matches = cx.spawn(|this, mut cx| async move {
+        self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move {
             update.await;
-            this.update(&mut cx, |this, cx| this.matches_updated(cx))
-                .log_err()
-        });
+            this.update(&mut cx, |this, cx| {
+                this.matches_updated(cx);
+            })
+            .log_err()
+        }));
     }
 
     fn matches_updated(&mut self, cx: &mut ViewContext<Self>) {
@@ -278,6 +282,11 @@ impl<D: PickerDelegate> Picker<D> {
             ScrollTarget::Show(index)
         };
         self.list_state.scroll_to(target);
+        self.pending_update_matches = None;
+        if let Some(secondary) = self.confirm_on_update.take() {
+            self.confirmed = true;
+            self.delegate.confirm(secondary, cx)
+        }
         cx.notify();
     }
 
@@ -331,13 +340,21 @@ impl<D: PickerDelegate> Picker<D> {
     }
 
     pub fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        self.confirmed = true;
-        self.delegate.confirm(false, cx);
+        if self.pending_update_matches.is_some() {
+            self.confirm_on_update = Some(false)
+        } else {
+            self.confirmed = true;
+            self.delegate.confirm(false, cx);
+        }
     }
 
     pub fn secondary_confirm(&mut self, _: &SecondaryConfirm, cx: &mut ViewContext<Self>) {
-        self.confirmed = true;
-        self.delegate.confirm(true, cx);
+        if self.pending_update_matches.is_some() {
+            self.confirm_on_update = Some(true)
+        } else {
+            self.confirmed = true;
+            self.delegate.confirm(true, cx);
+        }
     }
 
     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {

crates/project/Cargo.toml πŸ”—

@@ -37,7 +37,7 @@ sum_tree = { path = "../sum_tree" }
 terminal = { path = "../terminal" }
 util = { path = "../util" }
 
-aho-corasick = "0.7"
+aho-corasick = "1.1"
 anyhow.workspace = true
 async-trait.workspace = true
 backtrace = "0.3"
@@ -75,6 +75,6 @@ lsp = { path = "../lsp", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
-git2 = { version = "0.15", default-features = false }
+git2.workspace = true
 tempdir.workspace = true
 unindent.workspace = true

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

@@ -11,7 +11,7 @@ mod project_tests;
 mod worktree_tests;
 
 use anyhow::{anyhow, Context, Result};
-use client::{proto, Client, TypedEnvelope, UserId, UserStore};
+use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet};
 use copilot::Copilot;
@@ -108,6 +108,8 @@ pub struct Project {
     active_entry: Option<ProjectEntryId>,
     buffer_ordered_messages_tx: mpsc::UnboundedSender<BufferOrderedMessage>,
     languages: Arc<LanguageRegistry>,
+    supplementary_language_servers:
+        HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
     language_servers: HashMap<LanguageServerId, LanguageServerState>,
     language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>,
     language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
@@ -147,7 +149,8 @@ pub struct Project {
     _maintain_buffer_languages: Task<()>,
     _maintain_workspace_config: Task<()>,
     terminals: Terminals,
-    copilot_enabled: bool,
+    copilot_lsp_subscription: Option<gpui::Subscription>,
+    copilot_log_subscription: Option<lsp::Subscription>,
     current_lsp_settings: HashMap<Arc<str>, LspSettings>,
 }
 
@@ -250,13 +253,6 @@ enum ProjectClientState {
     },
 }
 
-#[derive(Clone, Debug)]
-pub struct Collaborator {
-    pub peer_id: proto::PeerId,
-    pub replica_id: ReplicaId,
-    pub user_id: UserId,
-}
-
 #[derive(Clone, Debug, PartialEq)]
 pub enum Event {
     LanguageServerAdded(LanguageServerId),
@@ -618,6 +614,8 @@ impl Project {
             let (tx, rx) = mpsc::unbounded();
             cx.spawn_weak(|this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
                 .detach();
+            let copilot_lsp_subscription =
+                Copilot::global(cx).map(|copilot| subscribe_for_copilot_events(&copilot, cx));
             Self {
                 worktrees: Default::default(),
                 buffer_ordered_messages_tx: tx,
@@ -647,6 +645,7 @@ impl Project {
                 fs,
                 next_entry_id: Default::default(),
                 next_diagnostic_group_id: Default::default(),
+                supplementary_language_servers: HashMap::default(),
                 language_servers: Default::default(),
                 language_server_ids: Default::default(),
                 language_server_statuses: Default::default(),
@@ -658,7 +657,8 @@ impl Project {
                 terminals: Terminals {
                     local_handles: Vec::new(),
                 },
-                copilot_enabled: Copilot::global(cx).is_some(),
+                copilot_lsp_subscription,
+                copilot_log_subscription: None,
                 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
             }
         })
@@ -694,6 +694,8 @@ impl Project {
             let (tx, rx) = mpsc::unbounded();
             cx.spawn_weak(|this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
                 .detach();
+            let copilot_lsp_subscription =
+                Copilot::global(cx).map(|copilot| subscribe_for_copilot_events(&copilot, cx));
             let mut this = Self {
                 worktrees: Vec::new(),
                 buffer_ordered_messages_tx: tx,
@@ -723,6 +725,7 @@ impl Project {
                     remote_id,
                     replica_id,
                 }),
+                supplementary_language_servers: HashMap::default(),
                 language_servers: Default::default(),
                 language_server_ids: Default::default(),
                 language_server_statuses: response
@@ -751,7 +754,8 @@ impl Project {
                 terminals: Terminals {
                     local_handles: Vec::new(),
                 },
-                copilot_enabled: Copilot::global(cx).is_some(),
+                copilot_lsp_subscription,
+                copilot_log_subscription: None,
                 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
             };
             for worktree in worktrees {
@@ -882,12 +886,14 @@ impl Project {
             self.restart_language_servers(worktree, language, cx);
         }
 
-        if !self.copilot_enabled && Copilot::global(cx).is_some() {
-            self.copilot_enabled = true;
-            for buffer in self.opened_buffers.values() {
-                if let Some(buffer) = buffer.upgrade(cx) {
-                    self.register_buffer_with_copilot(&buffer, cx);
+        if self.copilot_lsp_subscription.is_none() {
+            if let Some(copilot) = Copilot::global(cx) {
+                for buffer in self.opened_buffers.values() {
+                    if let Some(buffer) = buffer.upgrade(cx) {
+                        self.register_buffer_with_copilot(&buffer, cx);
+                    }
                 }
+                self.copilot_lsp_subscription = Some(subscribe_for_copilot_events(&copilot, cx));
             }
         }
 
@@ -912,7 +918,6 @@ impl Project {
         self.user_store.clone()
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn opened_buffers(&self, cx: &AppContext) -> Vec<ModelHandle<Buffer>> {
         self.opened_buffers
             .values()
@@ -970,6 +975,10 @@ impl Project {
         &self.collaborators
     }
 
+    pub fn host(&self) -> Option<&Collaborator> {
+        self.collaborators.values().find(|c| c.replica_id == 0)
+    }
+
     /// Collect all worktrees, including ones that don't appear in the project panel
     pub fn worktrees<'a>(
         &'a self,
@@ -1832,6 +1841,7 @@ impl Project {
                     Worktree::Remote(_) => panic!("cannot remote buffers as new files"),
                 })
                 .await?;
+
             this.update(&mut cx, |this, cx| {
                 this.detect_language_for_buffer(&buffer, cx);
                 this.register_buffer_with_language_servers(&buffer, cx);
@@ -2219,26 +2229,62 @@ impl Project {
                         .get_mut(&buffer.remote_id())
                         .and_then(|m| m.get_mut(&language_server.server_id()))?;
                     let previous_snapshot = buffer_snapshots.last()?;
-                    let next_version = previous_snapshot.version + 1;
 
-                    let content_changes = buffer
-                        .edits_since::<(PointUtf16, usize)>(previous_snapshot.snapshot.version())
-                        .map(|edit| {
-                            let edit_start = edit.new.start.0;
-                            let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
-                            let new_text = next_snapshot
-                                .text_for_range(edit.new.start.1..edit.new.end.1)
-                                .collect();
-                            lsp::TextDocumentContentChangeEvent {
-                                range: Some(lsp::Range::new(
-                                    point_to_lsp(edit_start),
-                                    point_to_lsp(edit_end),
-                                )),
+                    let build_incremental_change = || {
+                        buffer
+                            .edits_since::<(PointUtf16, usize)>(
+                                previous_snapshot.snapshot.version(),
+                            )
+                            .map(|edit| {
+                                let edit_start = edit.new.start.0;
+                                let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
+                                let new_text = next_snapshot
+                                    .text_for_range(edit.new.start.1..edit.new.end.1)
+                                    .collect();
+                                lsp::TextDocumentContentChangeEvent {
+                                    range: Some(lsp::Range::new(
+                                        point_to_lsp(edit_start),
+                                        point_to_lsp(edit_end),
+                                    )),
+                                    range_length: None,
+                                    text: new_text,
+                                }
+                            })
+                            .collect()
+                    };
+
+                    let document_sync_kind = language_server
+                        .capabilities()
+                        .text_document_sync
+                        .as_ref()
+                        .and_then(|sync| match sync {
+                            lsp::TextDocumentSyncCapability::Kind(kind) => Some(*kind),
+                            lsp::TextDocumentSyncCapability::Options(options) => options.change,
+                        });
+
+                    let content_changes: Vec<_> = match document_sync_kind {
+                        Some(lsp::TextDocumentSyncKind::FULL) => {
+                            vec![lsp::TextDocumentContentChangeEvent {
+                                range: None,
                                 range_length: None,
-                                text: new_text,
+                                text: next_snapshot.text(),
+                            }]
+                        }
+                        Some(lsp::TextDocumentSyncKind::INCREMENTAL) => build_incremental_change(),
+                        _ => {
+                            #[cfg(any(test, feature = "test-support"))]
+                            {
+                                build_incremental_change()
                             }
-                        })
-                        .collect();
+
+                            #[cfg(not(any(test, feature = "test-support")))]
+                            {
+                                continue;
+                            }
+                        }
+                    };
+
+                    let next_version = previous_snapshot.version + 1;
 
                     buffer_snapshots.push(LspBufferSnapshot {
                         version: next_version,
@@ -2268,11 +2314,13 @@ impl Project {
                 };
 
                 for (_, _, server) in self.language_servers_for_worktree(worktree_id) {
+                    let text = include_text(server.as_ref()).then(|| buffer.read(cx).text());
+
                     server
                         .notify::<lsp::notification::DidSaveTextDocument>(
                             lsp::DidSaveTextDocumentParams {
                                 text_document: text_document.clone(),
-                                text: None,
+                                text,
                             },
                         )
                         .log_err();
@@ -2321,7 +2369,30 @@ impl Project {
                     }
                 }
             }
+            BufferEvent::FileHandleChanged => {
+                let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
+                    return None;
+                };
+
+                match self.local_buffer_ids_by_entry_id.get(&file.entry_id) {
+                    Some(_) => {
+                        return None;
+                    }
+                    None => {
+                        let remote_id = buffer.read(cx).remote_id();
+                        self.local_buffer_ids_by_entry_id
+                            .insert(file.entry_id, remote_id);
 
+                        self.local_buffer_ids_by_path.insert(
+                            ProjectPath {
+                                worktree_id: file.worktree_id(cx),
+                                path: file.path.clone(),
+                            },
+                            remote_id,
+                        );
+                    }
+                }
+            }
             _ => {}
         }
 
@@ -2790,18 +2861,6 @@ impl Project {
             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();
@@ -4926,8 +4985,16 @@ impl Project {
                     if abs_path.ends_with("/") {
                         fs.create_dir(&abs_path).await?;
                     } else {
-                        fs.create_file(&abs_path, op.options.map(Into::into).unwrap_or_default())
-                            .await?;
+                        fs.create_file(
+                            &abs_path,
+                            op.options
+                                .map(|options| fs::CreateOptions {
+                                    overwrite: options.overwrite.unwrap_or(false),
+                                    ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
+                                })
+                                .unwrap_or_default(),
+                        )
+                        .await?;
                     }
                 }
 
@@ -4943,7 +5010,12 @@ impl Project {
                     fs.rename(
                         &source_abs_path,
                         &target_abs_path,
-                        op.options.map(Into::into).unwrap_or_default(),
+                        op.options
+                            .map(|options| fs::RenameOptions {
+                                overwrite: options.overwrite.unwrap_or(false),
+                                ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
+                            })
+                            .unwrap_or_default(),
                     )
                     .await?;
                 }
@@ -4953,7 +5025,13 @@ impl Project {
                         .uri
                         .to_file_path()
                         .map_err(|_| anyhow!("can't convert URI to path"))?;
-                    let options = op.options.map(Into::into).unwrap_or_default();
+                    let options = op
+                        .options
+                        .map(|options| fs::RemoveOptions {
+                            recursive: options.recursive.unwrap_or(false),
+                            ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false),
+                        })
+                        .unwrap_or_default();
                     if abs_path.ends_with("/") {
                         fs.remove_dir(&abs_path, options).await?;
                     } else {
@@ -5852,7 +5930,9 @@ impl Project {
                 Some(&buffer_id) => buffer_id,
                 None => match self.local_buffer_ids_by_path.get(&project_path) {
                     Some(&buffer_id) => buffer_id,
-                    None => continue,
+                    None => {
+                        continue;
+                    }
                 },
             };
 
@@ -7955,9 +8035,23 @@ impl Project {
             })
     }
 
+    pub fn supplementary_language_servers(
+        &self,
+    ) -> impl '_
+           + Iterator<
+        Item = (
+            &LanguageServerId,
+            &(LanguageServerName, Arc<LanguageServer>),
+        ),
+    > {
+        self.supplementary_language_servers.iter()
+    }
+
     pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
-        if let LanguageServerState::Running { server, .. } = self.language_servers.get(&id)? {
+        if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) {
             Some(server.clone())
+        } else if let Some((_, server)) = self.supplementary_language_servers.get(&id) {
+            Some(Arc::clone(server))
         } else {
             None
         }
@@ -8017,6 +8111,45 @@ impl Project {
     }
 }
 
+fn subscribe_for_copilot_events(
+    copilot: &ModelHandle<Copilot>,
+    cx: &mut ModelContext<'_, Project>,
+) -> gpui::Subscription {
+    cx.subscribe(
+        copilot,
+        |project, copilot, copilot_event, cx| match copilot_event {
+            copilot::Event::CopilotLanguageServerStarted => {
+                match copilot.read(cx).language_server() {
+                    Some((name, copilot_server)) => {
+                        // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
+                        if !copilot_server.has_notification_handler::<copilot::request::LogMessage>() {
+                            let new_server_id = copilot_server.server_id();
+                            let weak_project = cx.weak_handle();
+                            let copilot_log_subscription = copilot_server
+                                .on_notification::<copilot::request::LogMessage, _>(
+                                    move |params, mut cx| {
+                                        if let Some(project) = weak_project.upgrade(&mut cx) {
+                                            project.update(&mut cx, |_, cx| {
+                                                cx.emit(Event::LanguageServerLog(
+                                                    new_server_id,
+                                                    params.message,
+                                                ));
+                                            })
+                                        }
+                                    },
+                                );
+                            project.supplementary_language_servers.insert(new_server_id, (name.clone(), Arc::clone(copilot_server)));
+                            project.copilot_log_subscription = Some(copilot_log_subscription);
+                            cx.emit(Event::LanguageServerAdded(new_server_id));
+                        }
+                    }
+                    None => debug_panic!("Received Copilot language server started event, but no language server is running"),
+                }
+            }
+        },
+    )
+}
+
 fn glob_literal_prefix<'a>(glob: &'a str) -> &'a str {
     let mut literal_end = 0;
     for (i, part) in glob.split(path::MAIN_SEPARATOR).enumerate() {
@@ -8161,16 +8294,6 @@ impl Entity for Project {
     }
 }
 
-impl Collaborator {
-    fn from_proto(message: proto::Collaborator) -> Result<Self> {
-        Ok(Self {
-            peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
-            replica_id: message.replica_id as ReplicaId,
-            user_id: message.user_id as UserId,
-        })
-    }
-}
-
 impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
     fn from((worktree_id, path): (WorktreeId, P)) -> Self {
         Self {
@@ -8275,3 +8398,19 @@ async fn wait_for_loading_buffer(
         receiver.next().await;
     }
 }
+
+fn include_text(server: &lsp::LanguageServer) -> bool {
+    server
+        .capabilities()
+        .text_document_sync
+        .as_ref()
+        .and_then(|sync| match sync {
+            lsp::TextDocumentSyncCapability::Kind(_) => None,
+            lsp::TextDocumentSyncCapability::Options(options) => options.save.as_ref(),
+        })
+        .and_then(|save_options| match save_options {
+            lsp::TextDocumentSyncSaveOptions::Supported(_) => None,
+            lsp::TextDocumentSyncSaveOptions::SaveOptions(options) => options.include_text,
+        })
+        .unwrap_or(false)
+}

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

@@ -3598,7 +3598,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         search(
             &project,
-            SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
+            SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
             cx
         )
         .await
@@ -3623,7 +3623,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
     assert_eq!(
         search(
             &project,
-            SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
+            SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
             cx
         )
         .await
@@ -3664,7 +3664,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 vec![PathMatcher::new("*.odd").unwrap()],
                 Vec::new()
-            ),
+            )
+            .unwrap(),
             cx
         )
         .await
@@ -3682,7 +3683,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 vec![PathMatcher::new("*.rs").unwrap()],
                 Vec::new()
-            ),
+            )
+            .unwrap(),
             cx
         )
         .await
@@ -3706,7 +3708,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                     PathMatcher::new("*.odd").unwrap(),
                 ],
                 Vec::new()
-            ),
+            ).unwrap(),
             cx
         )
         .await
@@ -3731,7 +3733,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
                     PathMatcher::new("*.odd").unwrap(),
                 ],
                 Vec::new()
-            ),
+            ).unwrap(),
             cx
         )
         .await
@@ -3774,7 +3776,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 Vec::new(),
                 vec![PathMatcher::new("*.odd").unwrap()],
-            ),
+            )
+            .unwrap(),
             cx
         )
         .await
@@ -3797,7 +3800,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                 true,
                 Vec::new(),
                 vec![PathMatcher::new("*.rs").unwrap()],
-            ),
+            )
+            .unwrap(),
             cx
         )
         .await
@@ -3821,7 +3825,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                     PathMatcher::new("*.ts").unwrap(),
                     PathMatcher::new("*.odd").unwrap(),
                 ],
-            ),
+            ).unwrap(),
             cx
         )
         .await
@@ -3846,7 +3850,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
                     PathMatcher::new("*.ts").unwrap(),
                     PathMatcher::new("*.odd").unwrap(),
                 ],
-            ),
+            ).unwrap(),
             cx
         )
         .await
@@ -3883,7 +3887,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                 true,
                 vec![PathMatcher::new("*.odd").unwrap()],
                 vec![PathMatcher::new("*.odd").unwrap()],
-            ),
+            )
+            .unwrap(),
             cx
         )
         .await
@@ -3901,7 +3906,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                 true,
                 vec![PathMatcher::new("*.ts").unwrap()],
                 vec![PathMatcher::new("*.ts").unwrap()],
-            ),
+            ).unwrap(),
             cx
         )
         .await
@@ -3925,7 +3930,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                     PathMatcher::new("*.ts").unwrap(),
                     PathMatcher::new("*.odd").unwrap()
                 ],
-            ),
+            )
+            .unwrap(),
             cx
         )
         .await
@@ -3949,7 +3955,8 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
                     PathMatcher::new("*.rs").unwrap(),
                     PathMatcher::new("*.odd").unwrap()
                 ],
-            ),
+            )
+            .unwrap(),
             cx
         )
         .await

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

@@ -7,6 +7,7 @@ use language::{char_kind, BufferSnapshot};
 use regex::{Regex, RegexBuilder};
 use smol::future::yield_now;
 use std::{
+    borrow::Cow,
     io::{BufRead, BufReader, Read},
     ops::Range,
     path::{Path, PathBuf},
@@ -34,7 +35,8 @@ impl SearchInputs {
 #[derive(Clone, Debug)]
 pub enum SearchQuery {
     Text {
-        search: Arc<AhoCorasick<usize>>,
+        search: Arc<AhoCorasick>,
+        replacement: Option<String>,
         whole_word: bool,
         case_sensitive: bool,
         inner: SearchInputs,
@@ -42,7 +44,7 @@ pub enum SearchQuery {
 
     Regex {
         regex: Regex,
-
+        replacement: Option<String>,
         multiline: bool,
         whole_word: bool,
         case_sensitive: bool,
@@ -82,23 +84,23 @@ impl SearchQuery {
         case_sensitive: bool,
         files_to_include: Vec<PathMatcher>,
         files_to_exclude: Vec<PathMatcher>,
-    ) -> Self {
+    ) -> Result<Self> {
         let query = query.to_string();
         let search = AhoCorasickBuilder::new()
-            .auto_configure(&[&query])
             .ascii_case_insensitive(!case_sensitive)
-            .build(&[&query]);
+            .build(&[&query])?;
         let inner = SearchInputs {
             query: query.into(),
             files_to_exclude,
             files_to_include,
         };
-        Self::Text {
+        Ok(Self::Text {
             search: Arc::new(search),
+            replacement: None,
             whole_word,
             case_sensitive,
             inner,
-        }
+        })
     }
 
     pub fn regex(
@@ -130,6 +132,7 @@ impl SearchQuery {
         };
         Ok(Self::Regex {
             regex,
+            replacement: None,
             multiline,
             whole_word,
             case_sensitive,
@@ -147,16 +150,30 @@ impl SearchQuery {
                 deserialize_path_matches(&message.files_to_exclude)?,
             )
         } else {
-            Ok(Self::text(
+            Self::text(
                 message.query,
                 message.whole_word,
                 message.case_sensitive,
                 deserialize_path_matches(&message.files_to_include)?,
                 deserialize_path_matches(&message.files_to_exclude)?,
-            ))
+            )
+        }
+    }
+    pub fn with_replacement(mut self, new_replacement: String) -> Self {
+        match self {
+            Self::Text {
+                ref mut replacement,
+                ..
+            }
+            | Self::Regex {
+                ref mut replacement,
+                ..
+            } => {
+                *replacement = Some(new_replacement);
+                self
+            }
         }
     }
-
     pub fn to_proto(&self, project_id: u64) -> proto::SearchProject {
         proto::SearchProject {
             project_id,
@@ -214,7 +231,29 @@ impl SearchQuery {
             }
         }
     }
-
+    /// Returns the replacement text for this `SearchQuery`.
+    pub fn replacement(&self) -> Option<&str> {
+        match self {
+            SearchQuery::Text { replacement, .. } | SearchQuery::Regex { replacement, .. } => {
+                replacement.as_deref()
+            }
+        }
+    }
+    /// Replaces search hits if replacement is set. `text` is assumed to be a string that matches this `SearchQuery` exactly, without any leftovers on either side.
+    pub fn replacement_for<'a>(&self, text: &'a str) -> Option<Cow<'a, str>> {
+        match self {
+            SearchQuery::Text { replacement, .. } => replacement.clone().map(Cow::from),
+            SearchQuery::Regex {
+                regex, replacement, ..
+            } => {
+                if let Some(replacement) = replacement {
+                    Some(regex.replace(text, replacement))
+                } else {
+                    None
+                }
+            }
+        }
+    }
     pub async fn search(
         &self,
         buffer: &BufferSnapshot,

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

@@ -84,6 +84,7 @@ impl Project {
             terminal_settings::ActivateScript::Default => "activate",
             terminal_settings::ActivateScript::Csh => "activate.csh",
             terminal_settings::ActivateScript::Fish => "activate.fish",
+            terminal_settings::ActivateScript::Nushell => "activate.nu",
         };
 
         for virtual_environment_name in settings.directories {

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

@@ -122,6 +122,7 @@ actions!(
         CopyPath,
         CopyRelativePath,
         RevealInFinder,
+        OpenInTerminal,
         Cut,
         Paste,
         Delete,
@@ -156,6 +157,7 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
     cx.add_action(ProjectPanel::copy_path);
     cx.add_action(ProjectPanel::copy_relative_path);
     cx.add_action(ProjectPanel::reveal_in_finder);
+    cx.add_action(ProjectPanel::open_in_terminal);
     cx.add_action(ProjectPanel::new_search_in_directory);
     cx.add_action(
         |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext<ProjectPanel>| {
@@ -423,24 +425,30 @@ impl ProjectPanel {
             menu_entries.push(ContextMenuItem::Separator);
             menu_entries.push(ContextMenuItem::action("Cut", Cut));
             menu_entries.push(ContextMenuItem::action("Copy", Copy));
+            if let Some(clipboard_entry) = self.clipboard_entry {
+                if clipboard_entry.worktree_id() == worktree.id() {
+                    menu_entries.push(ContextMenuItem::action("Paste", Paste));
+                }
+            }
             menu_entries.push(ContextMenuItem::Separator);
             menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
             menu_entries.push(ContextMenuItem::action(
                 "Copy Relative Path",
                 CopyRelativePath,
             ));
+
+            if entry.is_dir() {
+                menu_entries.push(ContextMenuItem::Separator);
+            }
             menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
             if entry.is_dir() {
+                menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal));
                 menu_entries.push(ContextMenuItem::action(
                     "Search Inside",
                     NewSearchInDirectory,
                 ));
             }
-            if let Some(clipboard_entry) = self.clipboard_entry {
-                if clipboard_entry.worktree_id() == worktree.id() {
-                    menu_entries.push(ContextMenuItem::action("Paste", Paste));
-                }
-            }
+
             menu_entries.push(ContextMenuItem::Separator);
             menu_entries.push(ContextMenuItem::action("Rename", Rename));
             if !is_root {
@@ -965,6 +973,26 @@ impl ProjectPanel {
         }
     }
 
+    fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
+        if let Some((worktree, entry)) = self.selected_entry(cx) {
+            let window = cx.window();
+            let view_id = cx.view_id();
+            let path = worktree.abs_path().join(&entry.path);
+
+            cx.app_context()
+                .spawn(|mut cx| async move {
+                    window.dispatch_action(
+                        view_id,
+                        &workspace::OpenTerminal {
+                            working_directory: path,
+                        },
+                        &mut cx,
+                    );
+                })
+                .detach();
+        }
+    }
+
     pub fn new_search_in_directory(
         &mut self,
         _: &NewSearchInDirectory,
@@ -1485,7 +1513,7 @@ impl ProjectPanel {
         .as_draggable(entry_id, {
             let row_container_style = theme.dragged_entry.container;
 
-            move |_, cx: &mut ViewContext<Workspace>| {
+            move |_, _, cx: &mut ViewContext<Workspace>| {
                 let theme = theme::current(cx).clone();
                 Self::render_entry_visual_element(
                     &details,
@@ -1709,7 +1737,7 @@ mod tests {
     use settings::SettingsStore;
     use std::{
         collections::HashSet,
-        path::Path,
+        path::{Path, PathBuf},
         sync::atomic::{self, AtomicUsize},
     };
     use workspace::{pane, AppState};
@@ -2731,6 +2759,71 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.background());
+        fs.as_fake().insert_tree("/root", json!({})).await;
+        let project = Project::test(fs, ["/root".as_ref()], cx).await;
+        let workspace = cx
+            .add_window(|cx| Workspace::test_new(project.clone(), cx))
+            .root(cx);
+        let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
+
+        // Make a new buffer with no backing file
+        workspace.update(cx, |workspace, cx| {
+            Editor::new_file(workspace, &Default::default(), cx)
+        });
+
+        // "Save as"" the buffer, creating a new backing file for it
+        let task = workspace.update(cx, |workspace, cx| {
+            workspace.save_active_item(workspace::SaveIntent::Save, cx)
+        });
+
+        cx.foreground().run_until_parked();
+        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
+        task.await.unwrap();
+
+        // Rename the file
+        select_path(&panel, "root/new", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v root", "      new  <== selected"]
+        );
+        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+        panel.update(cx, |panel, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("newer", cx));
+        });
+        panel
+            .update(cx, |panel, cx| panel.confirm(&Confirm, cx))
+            .unwrap()
+            .await
+            .unwrap();
+
+        cx.foreground().run_until_parked();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v root", "      newer  <== selected"]
+        );
+
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.save_active_item(workspace::SaveIntent::Save, cx)
+            })
+            .await
+            .unwrap();
+
+        cx.foreground().run_until_parked();
+        // assert that saving the file doesn't restore "new"
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..10, cx),
+            &["v root", "      newer  <== selected"]
+        );
+    }
+
     fn toggle_expand_dir(
         panel: &ViewHandle<ProjectPanel>,
         path: impl AsRef<Path>,
@@ -2834,6 +2927,7 @@ mod tests {
             editor::init_settings(cx);
             crate::init((), cx);
             workspace::init_settings(cx);
+            client::init_settings(cx);
             Project::init_settings(cx);
         });
     }

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

@@ -69,7 +69,7 @@ impl ProjectSymbolsDelegate {
             &self.external_match_candidates,
             query,
             false,
-            MAX_MATCHES - visible_matches.len(),
+            MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
             &Default::default(),
             cx.background().clone(),
         ));

crates/quick_action_bar/Cargo.toml πŸ”—

@@ -9,7 +9,7 @@ path = "src/quick_action_bar.rs"
 doctest = false
 
 [dependencies]
-ai = { path = "../ai" }
+assistant = { path = "../assistant" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 search = { path = "../search" }

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

@@ -1,4 +1,4 @@
-use ai::{assistant::InlineAssist, AssistantPanel};
+use assistant::{assistant_panel::InlineAssist, AssistantPanel};
 use editor::Editor;
 use gpui::{
     elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
@@ -48,24 +48,26 @@ impl View for QuickActionBar {
             return Empty::new().into_any();
         };
 
-        let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
-        let mut bar = Flex::row().with_child(render_quick_action_bar_button(
-            0,
-            "icons/inlay_hint.svg",
-            inlay_hints_enabled,
-            (
-                "Toggle Inlay Hints".to_string(),
-                Some(Box::new(editor::ToggleInlayHints)),
-            ),
-            cx,
-            |this, cx| {
-                if let Some(editor) = this.active_editor() {
-                    editor.update(cx, |editor, cx| {
-                        editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
-                    });
-                }
-            },
-        ));
+        let mut bar = Flex::row();
+        if editor.read(cx).supports_inlay_hints(cx) {
+            bar = bar.with_child(render_quick_action_bar_button(
+                0,
+                "icons/inlay_hint.svg",
+                editor.read(cx).inlay_hints_enabled(),
+                (
+                    "Toggle Inlay Hints".to_string(),
+                    Some(Box::new(editor::ToggleInlayHints)),
+                ),
+                cx,
+                |this, cx| {
+                    if let Some(editor) = this.active_editor() {
+                        editor.update(cx, |editor, cx| {
+                            editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
+                        });
+                    }
+                },
+            ));
+        }
 
         if editor.read(cx).buffer().read(cx).is_singleton() {
             let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
@@ -94,7 +96,7 @@ impl View for QuickActionBar {
 
         bar.add_child(render_quick_action_bar_button(
             2,
-            "icons/radix/magic-wand.svg",
+            "icons/magic-wand.svg",
             false,
             ("Inline Assist".into(), Some(Box::new(InlineAssist))),
             cx,
@@ -163,12 +165,18 @@ impl ToolbarItemView for QuickActionBar {
 
                 if let Some(editor) = active_item.downcast::<Editor>() {
                     let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
+                    let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
                     self._inlay_hints_enabled_subscription =
                         Some(cx.observe(&editor, move |_, editor, cx| {
-                            let new_inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
-                            if inlay_hints_enabled != new_inlay_hints_enabled {
-                                inlay_hints_enabled = new_inlay_hints_enabled;
-                                cx.notify();
+                            let editor = editor.read(cx);
+                            let new_inlay_hints_enabled = editor.inlay_hints_enabled();
+                            let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
+                            let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
+                                || supports_inlay_hints != new_supports_inlay_hints;
+                            inlay_hints_enabled = new_inlay_hints_enabled;
+                            supports_inlay_hints = new_supports_inlay_hints;
+                            if should_notify {
+                                cx.notify()
                             }
                         }));
                     ToolbarItemLocation::PrimaryRight { flex: None }

crates/rich_text/Cargo.toml πŸ”—

@@ -0,0 +1,30 @@
+[package]
+name = "rich_text"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/rich_text.rs"
+doctest = false
+
+[features]
+test-support = [
+    "gpui/test-support",
+    "util/test-support",
+]
+
+
+[dependencies]
+collections = { path = "../collections" }
+gpui = { path = "../gpui" }
+sum_tree = { path = "../sum_tree" }
+theme = { path = "../theme" }
+language = { path = "../language" }
+util = { path = "../util" }
+anyhow.workspace = true
+futures.workspace = true
+lazy_static.workspace = true
+pulldown-cmark = { version = "0.9.2", default-features = false }
+smallvec.workspace = true
+smol.workspace = true

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

@@ -0,0 +1,287 @@
+use std::{ops::Range, sync::Arc};
+
+use futures::FutureExt;
+use gpui::{
+    color::Color,
+    elements::Text,
+    fonts::{HighlightStyle, TextStyle, Underline, Weight},
+    platform::{CursorStyle, MouseButton},
+    AnyElement, CursorRegion, Element, MouseRegion, ViewContext,
+};
+use language::{HighlightId, Language, LanguageRegistry};
+use theme::SyntaxTheme;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum Highlight {
+    Id(HighlightId),
+    Highlight(HighlightStyle),
+}
+
+#[derive(Debug, Clone)]
+pub struct RichText {
+    pub text: String,
+    pub highlights: Vec<(Range<usize>, Highlight)>,
+    pub region_ranges: Vec<Range<usize>>,
+    pub regions: Vec<RenderedRegion>,
+}
+
+#[derive(Debug, Clone)]
+pub struct RenderedRegion {
+    code: bool,
+    link_url: Option<String>,
+}
+
+impl RichText {
+    pub fn element<V: 'static>(
+        &self,
+        syntax: Arc<SyntaxTheme>,
+        style: TextStyle,
+        code_span_background_color: Color,
+        cx: &mut ViewContext<V>,
+    ) -> AnyElement<V> {
+        let mut region_id = 0;
+        let view_id = cx.view_id();
+
+        let regions = self.regions.clone();
+
+        enum Markdown {}
+        Text::new(self.text.clone(), style.clone())
+            .with_highlights(
+                self.highlights
+                    .iter()
+                    .filter_map(|(range, highlight)| {
+                        let style = match highlight {
+                            Highlight::Id(id) => id.style(&syntax)?,
+                            Highlight::Highlight(style) => style.clone(),
+                        };
+                        Some((range.clone(), style))
+                    })
+                    .collect::<Vec<_>>(),
+            )
+            .with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| {
+                region_id += 1;
+                let region = regions[ix].clone();
+                if let Some(url) = region.link_url {
+                    cx.scene().push_cursor_region(CursorRegion {
+                        bounds,
+                        style: CursorStyle::PointingHand,
+                    });
+                    cx.scene().push_mouse_region(
+                        MouseRegion::new::<Markdown>(view_id, region_id, bounds)
+                            .on_click::<V, _>(MouseButton::Left, move |_, _, cx| {
+                                cx.platform().open_url(&url)
+                            }),
+                    );
+                }
+                if region.code {
+                    cx.scene().push_quad(gpui::Quad {
+                        bounds,
+                        background: Some(code_span_background_color),
+                        border: Default::default(),
+                        corner_radii: (2.0).into(),
+                    });
+                }
+            })
+            .with_soft_wrap(true)
+            .into_any()
+    }
+}
+
+pub fn render_markdown_mut(
+    block: &str,
+    language_registry: &Arc<LanguageRegistry>,
+    language: Option<&Arc<Language>>,
+    data: &mut RichText,
+) {
+    use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
+
+    let mut bold_depth = 0;
+    let mut italic_depth = 0;
+    let mut link_url = None;
+    let mut current_language = None;
+    let mut list_stack = Vec::new();
+
+    for event in Parser::new_ext(&block, Options::all()) {
+        let prev_len = data.text.len();
+        match event {
+            Event::Text(t) => {
+                if let Some(language) = &current_language {
+                    render_code(&mut data.text, &mut data.highlights, t.as_ref(), language);
+                } else {
+                    data.text.push_str(t.as_ref());
+
+                    let mut style = HighlightStyle::default();
+                    if bold_depth > 0 {
+                        style.weight = Some(Weight::BOLD);
+                    }
+                    if italic_depth > 0 {
+                        style.italic = Some(true);
+                    }
+                    if let Some(link_url) = link_url.clone() {
+                        data.region_ranges.push(prev_len..data.text.len());
+                        data.regions.push(RenderedRegion {
+                            link_url: Some(link_url),
+                            code: false,
+                        });
+                        style.underline = Some(Underline {
+                            thickness: 1.0.into(),
+                            ..Default::default()
+                        });
+                    }
+
+                    if style != HighlightStyle::default() {
+                        let mut new_highlight = true;
+                        if let Some((last_range, last_style)) = data.highlights.last_mut() {
+                            if last_range.end == prev_len
+                                && last_style == &Highlight::Highlight(style)
+                            {
+                                last_range.end = data.text.len();
+                                new_highlight = false;
+                            }
+                        }
+                        if new_highlight {
+                            data.highlights
+                                .push((prev_len..data.text.len(), Highlight::Highlight(style)));
+                        }
+                    }
+                }
+            }
+            Event::Code(t) => {
+                data.text.push_str(t.as_ref());
+                data.region_ranges.push(prev_len..data.text.len());
+                if link_url.is_some() {
+                    data.highlights.push((
+                        prev_len..data.text.len(),
+                        Highlight::Highlight(HighlightStyle {
+                            underline: Some(Underline {
+                                thickness: 1.0.into(),
+                                ..Default::default()
+                            }),
+                            ..Default::default()
+                        }),
+                    ));
+                }
+                data.regions.push(RenderedRegion {
+                    code: true,
+                    link_url: link_url.clone(),
+                });
+            }
+            Event::Start(tag) => match tag {
+                Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack),
+                Tag::Heading(_, _, _) => {
+                    new_paragraph(&mut data.text, &mut list_stack);
+                    bold_depth += 1;
+                }
+                Tag::CodeBlock(kind) => {
+                    new_paragraph(&mut data.text, &mut list_stack);
+                    current_language = if let CodeBlockKind::Fenced(language) = kind {
+                        language_registry
+                            .language_for_name(language.as_ref())
+                            .now_or_never()
+                            .and_then(Result::ok)
+                    } else {
+                        language.cloned()
+                    }
+                }
+                Tag::Emphasis => italic_depth += 1,
+                Tag::Strong => bold_depth += 1,
+                Tag::Link(_, url, _) => link_url = Some(url.to_string()),
+                Tag::List(number) => {
+                    list_stack.push((number, false));
+                }
+                Tag::Item => {
+                    let len = list_stack.len();
+                    if let Some((list_number, has_content)) = list_stack.last_mut() {
+                        *has_content = false;
+                        if !data.text.is_empty() && !data.text.ends_with('\n') {
+                            data.text.push('\n');
+                        }
+                        for _ in 0..len - 1 {
+                            data.text.push_str("  ");
+                        }
+                        if let Some(number) = list_number {
+                            data.text.push_str(&format!("{}. ", number));
+                            *number += 1;
+                            *has_content = false;
+                        } else {
+                            data.text.push_str("- ");
+                        }
+                    }
+                }
+                _ => {}
+            },
+            Event::End(tag) => match tag {
+                Tag::Heading(_, _, _) => bold_depth -= 1,
+                Tag::CodeBlock(_) => current_language = None,
+                Tag::Emphasis => italic_depth -= 1,
+                Tag::Strong => bold_depth -= 1,
+                Tag::Link(_, _, _) => link_url = None,
+                Tag::List(_) => drop(list_stack.pop()),
+                _ => {}
+            },
+            Event::HardBreak => data.text.push('\n'),
+            Event::SoftBreak => data.text.push(' '),
+            _ => {}
+        }
+    }
+}
+
+pub fn render_markdown(
+    block: String,
+    language_registry: &Arc<LanguageRegistry>,
+    language: Option<&Arc<Language>>,
+) -> RichText {
+    let mut data = RichText {
+        text: Default::default(),
+        highlights: Default::default(),
+        region_ranges: Default::default(),
+        regions: Default::default(),
+    };
+
+    render_markdown_mut(&block, language_registry, language, &mut data);
+
+    data.text = data.text.trim().to_string();
+
+    data
+}
+
+pub fn render_code(
+    text: &mut String,
+    highlights: &mut Vec<(Range<usize>, Highlight)>,
+    content: &str,
+    language: &Arc<Language>,
+) {
+    let prev_len = text.len();
+    text.push_str(content);
+    for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
+        highlights.push((
+            prev_len + range.start..prev_len + range.end,
+            Highlight::Id(highlight_id),
+        ));
+    }
+}
+
+pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
+    let mut is_subsequent_paragraph_of_list = false;
+    if let Some((_, has_content)) = list_stack.last_mut() {
+        if *has_content {
+            is_subsequent_paragraph_of_list = true;
+        } else {
+            *has_content = true;
+            return;
+        }
+    }
+
+    if !text.is_empty() {
+        if !text.ends_with('\n') {
+            text.push('\n');
+        }
+        text.push('\n');
+    }
+    for _ in 0..list_stack.len().saturating_sub(1) {
+        text.push_str("  ");
+    }
+    if is_subsequent_paragraph_of_list {
+        text.push_str("  ");
+    }
+}

crates/rpc/proto/zed.proto πŸ”—

@@ -23,139 +23,154 @@ message Envelope {
         CreateRoomResponse create_room_response = 10;
         JoinRoom join_room = 11;
         JoinRoomResponse join_room_response = 12;
-        RejoinRoom rejoin_room = 108;
-        RejoinRoomResponse rejoin_room_response = 109;
-        LeaveRoom leave_room = 13;
-        Call call = 14;
-        IncomingCall incoming_call = 15;
-        CallCanceled call_canceled = 16;
-        CancelCall cancel_call = 17;
-        DeclineCall decline_call = 18;
-        UpdateParticipantLocation update_participant_location = 19;
-        RoomUpdated room_updated = 20;
-
-        ShareProject share_project = 21;
-        ShareProjectResponse share_project_response = 22;
-        UnshareProject unshare_project = 23;
-        JoinProject join_project = 24;
-        JoinProjectResponse join_project_response = 25;
-        LeaveProject leave_project = 26;
-        AddProjectCollaborator add_project_collaborator = 27;
-        UpdateProjectCollaborator update_project_collaborator = 110;
-        RemoveProjectCollaborator remove_project_collaborator = 28;
-
-        GetDefinition get_definition = 29;
-        GetDefinitionResponse get_definition_response = 30;
-        GetTypeDefinition get_type_definition = 31;
-        GetTypeDefinitionResponse get_type_definition_response = 32;
-        GetReferences get_references = 33;
-        GetReferencesResponse get_references_response = 34;
-        GetDocumentHighlights get_document_highlights = 35;
-        GetDocumentHighlightsResponse get_document_highlights_response = 36;
-        GetProjectSymbols get_project_symbols = 37;
-        GetProjectSymbolsResponse get_project_symbols_response = 38;
-        OpenBufferForSymbol open_buffer_for_symbol = 39;
-        OpenBufferForSymbolResponse open_buffer_for_symbol_response = 40;
-
-        UpdateProject update_project = 41;
-        UpdateWorktree update_worktree = 43;
-
-        CreateProjectEntry create_project_entry = 45;
-        RenameProjectEntry rename_project_entry = 46;
-        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;
-        UpdateLanguageServer update_language_server = 52;
-
-        OpenBufferById open_buffer_by_id = 53;
-        OpenBufferByPath open_buffer_by_path = 54;
-        OpenBufferResponse open_buffer_response = 55;
-        CreateBufferForPeer create_buffer_for_peer = 56;
-        UpdateBuffer update_buffer = 57;
-        UpdateBufferFile update_buffer_file = 58;
-        SaveBuffer save_buffer = 59;
-        BufferSaved buffer_saved = 60;
-        BufferReloaded buffer_reloaded = 61;
-        ReloadBuffers reload_buffers = 62;
-        ReloadBuffersResponse reload_buffers_response = 63;
-        SynchronizeBuffers synchronize_buffers = 200;
-        SynchronizeBuffersResponse synchronize_buffers_response = 201;
-        FormatBuffers format_buffers = 64;
-        FormatBuffersResponse format_buffers_response = 65;
-        GetCompletions get_completions = 66;
-        GetCompletionsResponse get_completions_response = 67;
-        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 68;
-        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 69;
-        GetCodeActions get_code_actions = 70;
-        GetCodeActionsResponse get_code_actions_response = 71;
-        GetHover get_hover = 72;
-        GetHoverResponse get_hover_response = 73;
-        ApplyCodeAction apply_code_action = 74;
-        ApplyCodeActionResponse apply_code_action_response = 75;
-        PrepareRename prepare_rename = 76;
-        PrepareRenameResponse prepare_rename_response = 77;
-        PerformRename perform_rename = 78;
-        PerformRenameResponse perform_rename_response = 79;
-        SearchProject search_project = 80;
-        SearchProjectResponse search_project_response = 81;
-
-        UpdateContacts update_contacts = 92;
-        UpdateInviteInfo update_invite_info = 93;
-        ShowContacts show_contacts = 94;
-
-        GetUsers get_users = 95;
-        FuzzySearchUsers fuzzy_search_users = 96;
-        UsersResponse users_response = 97;
-        RequestContact request_contact = 98;
-        RespondToContactRequest respond_to_contact_request = 99;
-        RemoveContact remove_contact = 100;
-
-        Follow follow = 101;
-        FollowResponse follow_response = 102;
-        UpdateFollowers update_followers = 103;
-        Unfollow unfollow = 104;
-        GetPrivateUserInfo get_private_user_info = 105;
-        GetPrivateUserInfoResponse get_private_user_info_response = 106;
-        UpdateDiffBase update_diff_base = 107;
-
-        OnTypeFormatting on_type_formatting = 111;
-        OnTypeFormattingResponse on_type_formatting_response = 112;
-
-        UpdateWorktreeSettings update_worktree_settings = 113;
-
-        InlayHints inlay_hints = 116;
-        InlayHintsResponse inlay_hints_response = 117;
-        ResolveInlayHint resolve_inlay_hint = 137;
-        ResolveInlayHintResponse resolve_inlay_hint_response = 138;
-        RefreshInlayHints refresh_inlay_hints = 118;
-
-        CreateChannel create_channel = 119;
-        ChannelResponse channel_response = 120;
-        InviteChannelMember invite_channel_member = 121;
-        RemoveChannelMember remove_channel_member = 122;
-        RespondToChannelInvite respond_to_channel_invite = 123;
-        UpdateChannels update_channels = 124;
-        JoinChannel join_channel = 125;
-        RemoveChannel remove_channel = 126;
-        GetChannelMembers get_channel_members = 127;
-        GetChannelMembersResponse get_channel_members_response = 128;
-        SetChannelMemberAdmin set_channel_member_admin = 129;
-        RenameChannel rename_channel = 130;
-
-        JoinChannelBuffer join_channel_buffer = 131;
-        JoinChannelBufferResponse join_channel_buffer_response = 132;
-        UpdateChannelBuffer update_channel_buffer = 133;
-        LeaveChannelBuffer leave_channel_buffer = 134;
-        AddChannelBufferCollaborator add_channel_buffer_collaborator = 135;
-        RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
-        UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139;
-        RejoinChannelBuffers rejoin_channel_buffers = 140;
-        RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141; // Current max
+        RejoinRoom rejoin_room = 13;
+        RejoinRoomResponse rejoin_room_response = 14;
+        LeaveRoom leave_room = 15;
+        Call call = 16;
+        IncomingCall incoming_call = 17;
+        CallCanceled call_canceled = 18;
+        CancelCall cancel_call = 19;
+        DeclineCall decline_call = 20;
+        UpdateParticipantLocation update_participant_location = 21;
+        RoomUpdated room_updated = 22;
+
+        ShareProject share_project = 23;
+        ShareProjectResponse share_project_response = 24;
+        UnshareProject unshare_project = 25;
+        JoinProject join_project = 26;
+        JoinProjectResponse join_project_response = 27;
+        LeaveProject leave_project = 28;
+        AddProjectCollaborator add_project_collaborator = 29;
+        UpdateProjectCollaborator update_project_collaborator = 30;
+        RemoveProjectCollaborator remove_project_collaborator = 31;
+
+        GetDefinition get_definition = 32;
+        GetDefinitionResponse get_definition_response = 33;
+        GetTypeDefinition get_type_definition = 34;
+        GetTypeDefinitionResponse get_type_definition_response = 35;
+        GetReferences get_references = 36;
+        GetReferencesResponse get_references_response = 37;
+        GetDocumentHighlights get_document_highlights = 38;
+        GetDocumentHighlightsResponse get_document_highlights_response = 39;
+        GetProjectSymbols get_project_symbols = 40;
+        GetProjectSymbolsResponse get_project_symbols_response = 41;
+        OpenBufferForSymbol open_buffer_for_symbol = 42;
+        OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43;
+
+        UpdateProject update_project = 44;
+        UpdateWorktree update_worktree = 45;
+
+        CreateProjectEntry create_project_entry = 46;
+        RenameProjectEntry rename_project_entry = 47;
+        CopyProjectEntry copy_project_entry = 48;
+        DeleteProjectEntry delete_project_entry = 49;
+        ProjectEntryResponse project_entry_response = 50;
+        ExpandProjectEntry expand_project_entry = 51;
+        ExpandProjectEntryResponse expand_project_entry_response = 52;
+
+        UpdateDiagnosticSummary update_diagnostic_summary = 53;
+        StartLanguageServer start_language_server = 54;
+        UpdateLanguageServer update_language_server = 55;
+
+        OpenBufferById open_buffer_by_id = 56;
+        OpenBufferByPath open_buffer_by_path = 57;
+        OpenBufferResponse open_buffer_response = 58;
+        CreateBufferForPeer create_buffer_for_peer = 59;
+        UpdateBuffer update_buffer = 60;
+        UpdateBufferFile update_buffer_file = 61;
+        SaveBuffer save_buffer = 62;
+        BufferSaved buffer_saved = 63;
+        BufferReloaded buffer_reloaded = 64;
+        ReloadBuffers reload_buffers = 65;
+        ReloadBuffersResponse reload_buffers_response = 66;
+        SynchronizeBuffers synchronize_buffers = 67;
+        SynchronizeBuffersResponse synchronize_buffers_response = 68;
+        FormatBuffers format_buffers = 69;
+        FormatBuffersResponse format_buffers_response = 70;
+        GetCompletions get_completions = 71;
+        GetCompletionsResponse get_completions_response = 72;
+        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73;
+        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74;
+        GetCodeActions get_code_actions = 75;
+        GetCodeActionsResponse get_code_actions_response = 76;
+        GetHover get_hover = 77;
+        GetHoverResponse get_hover_response = 78;
+        ApplyCodeAction apply_code_action = 79;
+        ApplyCodeActionResponse apply_code_action_response = 80;
+        PrepareRename prepare_rename = 81;
+        PrepareRenameResponse prepare_rename_response = 82;
+        PerformRename perform_rename = 83;
+        PerformRenameResponse perform_rename_response = 84;
+        SearchProject search_project = 85;
+        SearchProjectResponse search_project_response = 86;
+
+        UpdateContacts update_contacts = 87;
+        UpdateInviteInfo update_invite_info = 88;
+        ShowContacts show_contacts = 89;
+
+        GetUsers get_users = 90;
+        FuzzySearchUsers fuzzy_search_users = 91;
+        UsersResponse users_response = 92;
+        RequestContact request_contact = 93;
+        RespondToContactRequest respond_to_contact_request = 94;
+        RemoveContact remove_contact = 95;
+
+        Follow follow = 96;
+        FollowResponse follow_response = 97;
+        UpdateFollowers update_followers = 98;
+        Unfollow unfollow = 99;
+        GetPrivateUserInfo get_private_user_info = 100;
+        GetPrivateUserInfoResponse get_private_user_info_response = 101;
+        UpdateDiffBase update_diff_base = 102;
+
+        OnTypeFormatting on_type_formatting = 103;
+        OnTypeFormattingResponse on_type_formatting_response = 104;
+
+        UpdateWorktreeSettings update_worktree_settings = 105;
+
+        InlayHints inlay_hints = 106;
+        InlayHintsResponse inlay_hints_response = 107;
+        ResolveInlayHint resolve_inlay_hint = 108;
+        ResolveInlayHintResponse resolve_inlay_hint_response = 109;
+        RefreshInlayHints refresh_inlay_hints = 110;
+
+        CreateChannel create_channel = 111;
+        CreateChannelResponse create_channel_response = 112;
+        InviteChannelMember invite_channel_member = 113;
+        RemoveChannelMember remove_channel_member = 114;
+        RespondToChannelInvite respond_to_channel_invite = 115;
+        UpdateChannels update_channels = 116;
+        JoinChannel join_channel = 117;
+        DeleteChannel delete_channel = 118;
+        GetChannelMembers get_channel_members = 119;
+        GetChannelMembersResponse get_channel_members_response = 120;
+        SetChannelMemberAdmin set_channel_member_admin = 121;
+        RenameChannel rename_channel = 122;
+        RenameChannelResponse rename_channel_response = 123;
+
+        JoinChannelBuffer join_channel_buffer = 124;
+        JoinChannelBufferResponse join_channel_buffer_response = 125;
+        UpdateChannelBuffer update_channel_buffer = 126;
+        LeaveChannelBuffer leave_channel_buffer = 127;
+        UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128;
+        RejoinChannelBuffers rejoin_channel_buffers = 129;
+        RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130;
+        AckBufferOperation ack_buffer_operation = 143;
+
+        JoinChannelChat join_channel_chat = 131;
+        JoinChannelChatResponse join_channel_chat_response = 132;
+        LeaveChannelChat leave_channel_chat = 133;
+        SendChannelMessage send_channel_message = 134;
+        SendChannelMessageResponse send_channel_message_response = 135;
+        ChannelMessageSent channel_message_sent = 136;
+        GetChannelMessages get_channel_messages = 137;
+        GetChannelMessagesResponse get_channel_messages_response = 138;
+        RemoveChannelMessage remove_channel_message = 139;
+        AckChannelMessage ack_channel_message = 144;
+
+        LinkChannel link_channel = 140;
+        UnlinkChannel unlink_channel = 141;
+        MoveChannel move_channel = 142; // current max: 144
     }
 }
 
@@ -243,6 +258,7 @@ message Participant {
     PeerId peer_id = 2;
     repeated ParticipantProject projects = 3;
     ParticipantLocation location = 4;
+    uint32 participant_index = 5;
 }
 
 message PendingParticipant {
@@ -425,20 +441,9 @@ message RemoveProjectCollaborator {
     PeerId peer_id = 2;
 }
 
-message AddChannelBufferCollaborator {
+message UpdateChannelBufferCollaborators {
     uint64 channel_id = 1;
-    Collaborator collaborator = 2;
-}
-
-message RemoveChannelBufferCollaborator {
-    uint64 channel_id = 1;
-    PeerId peer_id = 2;
-}
-
-message UpdateChannelBufferCollaborator {
-    uint64 channel_id = 1;
-    PeerId old_peer_id = 2;
-    PeerId new_peer_id = 3;
+    repeated Collaborator collaborators = 2;
 }
 
 message GetDefinition {
@@ -945,11 +950,31 @@ message LspDiskBasedDiagnosticsUpdated {}
 
 message UpdateChannels {
     repeated Channel channels = 1;
-    repeated uint64 remove_channels = 2;
-    repeated Channel channel_invitations = 3;
-    repeated uint64 remove_channel_invitations = 4;
-    repeated ChannelParticipants channel_participants = 5;
-    repeated ChannelPermission channel_permissions = 6;
+    repeated ChannelEdge insert_edge = 2;
+    repeated ChannelEdge delete_edge = 3;
+    repeated uint64 delete_channels = 4;
+    repeated Channel channel_invitations = 5;
+    repeated uint64 remove_channel_invitations = 6;
+    repeated ChannelParticipants channel_participants = 7;
+    repeated ChannelPermission channel_permissions = 8;
+    repeated UnseenChannelMessage unseen_channel_messages = 9;
+    repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10;
+}
+
+message UnseenChannelMessage {
+    uint64 channel_id = 1;
+    uint64 message_id = 2;
+}
+
+message UnseenChannelBufferChange {
+    uint64 channel_id = 1;
+    uint64 epoch = 2;
+    repeated VectorClockEntry version = 3;
+}
+
+message ChannelEdge {
+    uint64 channel_id = 1;
+    uint64 parent_id = 2;
 }
 
 message ChannelPermission {
@@ -966,7 +991,7 @@ message JoinChannel {
     uint64 channel_id = 1;
 }
 
-message RemoveChannel {
+message DeleteChannel {
     uint64 channel_id = 1;
 }
 
@@ -995,8 +1020,9 @@ message CreateChannel {
     optional uint64 parent_id = 2;
 }
 
-message ChannelResponse {
+message CreateChannelResponse {
     Channel channel = 1;
+    optional uint64 parent_id = 2;
 }
 
 message InviteChannelMember {
@@ -1021,10 +1047,86 @@ message RenameChannel {
     string name = 2;
 }
 
+message RenameChannelResponse {
+    Channel channel = 1;
+}
+
+message JoinChannelChat {
+    uint64 channel_id = 1;
+}
+
+message JoinChannelChatResponse {
+    repeated ChannelMessage messages = 1;
+    bool done = 2;
+}
+
+message LeaveChannelChat {
+    uint64 channel_id = 1;
+}
+
+message SendChannelMessage {
+    uint64 channel_id = 1;
+    string body = 2;
+    Nonce nonce = 3;
+}
+
+message RemoveChannelMessage {
+    uint64 channel_id = 1;
+    uint64 message_id = 2;
+}
+
+message AckChannelMessage {
+    uint64 channel_id = 1;
+    uint64 message_id = 2;
+}
+
+message SendChannelMessageResponse {
+    ChannelMessage message = 1;
+}
+
+message ChannelMessageSent {
+    uint64 channel_id = 1;
+    ChannelMessage message = 2;
+}
+
+message GetChannelMessages {
+    uint64 channel_id = 1;
+    uint64 before_message_id = 2;
+}
+
+message GetChannelMessagesResponse {
+    repeated ChannelMessage messages = 1;
+    bool done = 2;
+}
+
+message LinkChannel {
+    uint64 channel_id = 1;
+    uint64 to = 2;
+}
+
+message UnlinkChannel {
+    uint64 channel_id = 1;
+    uint64 from = 2;
+}
+
+message MoveChannel {
+    uint64 channel_id = 1;
+    uint64 from = 2;
+    uint64 to = 3;
+}
+
 message JoinChannelBuffer {
     uint64 channel_id = 1;
 }
 
+message ChannelMessage {
+    uint64 id = 1;
+    string body = 2;
+    uint64 timestamp = 3;
+    uint64 sender_id = 4;
+    Nonce nonce = 5;
+}
+
 message RejoinChannelBuffers {
     repeated ChannelBufferVersion buffers = 1;
 }
@@ -1033,6 +1135,12 @@ message RejoinChannelBuffersResponse {
     repeated RejoinedChannelBuffer buffers = 1;
 }
 
+message AckBufferOperation {
+    uint64 buffer_id = 1;
+    uint64 epoch = 2;
+    repeated VectorClockEntry version = 3;
+}
+
 message JoinChannelBufferResponse {
     uint64 buffer_id = 1;
     uint32 replica_id = 2;
@@ -1119,8 +1227,9 @@ message UpdateDiagnostics {
 }
 
 message Follow {
-    uint64 project_id = 1;
-    PeerId leader_id = 2;
+    uint64 room_id = 1;
+    optional uint64 project_id = 2;
+    PeerId leader_id = 3;
 }
 
 message FollowResponse {
@@ -1129,18 +1238,20 @@ message FollowResponse {
 }
 
 message UpdateFollowers {
-    uint64 project_id = 1;
-    repeated PeerId follower_ids = 2;
+    uint64 room_id = 1;
+    optional uint64 project_id = 2;
+    repeated PeerId follower_ids = 3;
     oneof variant {
-        UpdateActiveView update_active_view = 3;
-        View create_view = 4;
-        UpdateView update_view = 5;
+        UpdateActiveView update_active_view = 4;
+        View create_view = 5;
+        UpdateView update_view = 6;
     }
 }
 
 message Unfollow {
-    uint64 project_id = 1;
-    PeerId leader_id = 2;
+    uint64 room_id = 1;
+    optional uint64 project_id = 2;
+    PeerId leader_id = 3;
 }
 
 message GetPrivateUserInfo {}
@@ -1425,7 +1536,6 @@ message Nonce {
 message Channel {
     uint64 id = 1;
     string name = 2;
-    optional uint64 parent_id = 3;
 }
 
 message Contact {

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

@@ -146,7 +146,8 @@ messages!(
     (CopyProjectEntry, Foreground),
     (CreateBufferForPeer, Foreground),
     (CreateChannel, Foreground),
-    (ChannelResponse, Foreground),
+    (CreateChannelResponse, Foreground),
+    (ChannelMessageSent, Foreground),
     (CreateProjectEntry, Foreground),
     (CreateRoom, Foreground),
     (CreateRoomResponse, Foreground),
@@ -163,6 +164,10 @@ messages!(
     (GetCodeActionsResponse, Background),
     (GetHover, Background),
     (GetHoverResponse, Background),
+    (GetChannelMessages, Background),
+    (GetChannelMessagesResponse, Background),
+    (SendChannelMessage, Background),
+    (SendChannelMessageResponse, Background),
     (GetCompletions, Background),
     (GetCompletionsResponse, Background),
     (GetDefinition, Background),
@@ -184,6 +189,9 @@ messages!(
     (JoinProjectResponse, Foreground),
     (JoinRoom, Foreground),
     (JoinRoomResponse, Foreground),
+    (JoinChannelChat, Foreground),
+    (JoinChannelChatResponse, Foreground),
+    (LeaveChannelChat, Foreground),
     (LeaveProject, Foreground),
     (LeaveRoom, Foreground),
     (OpenBufferById, Background),
@@ -209,6 +217,7 @@ messages!(
     (RejoinRoomResponse, Foreground),
     (RemoveContact, Foreground),
     (RemoveChannelMember, Foreground),
+    (RemoveChannelMessage, Foreground),
     (ReloadBuffers, Foreground),
     (ReloadBuffersResponse, Foreground),
     (RemoveProjectCollaborator, Foreground),
@@ -220,6 +229,7 @@ messages!(
     (RoomUpdated, Foreground),
     (SaveBuffer, Foreground),
     (RenameChannel, Foreground),
+    (RenameChannelResponse, Foreground),
     (SetChannelMemberAdmin, Foreground),
     (SearchProject, Background),
     (SearchProjectResponse, Background),
@@ -237,7 +247,10 @@ messages!(
     (UpdateBuffer, Foreground),
     (UpdateBufferFile, Foreground),
     (UpdateContacts, Foreground),
-    (RemoveChannel, Foreground),
+    (DeleteChannel, Foreground),
+    (MoveChannel, Foreground),
+    (LinkChannel, Foreground),
+    (UnlinkChannel, Foreground),
     (UpdateChannels, Foreground),
     (UpdateDiagnosticSummary, Foreground),
     (UpdateFollowers, Foreground),
@@ -257,9 +270,9 @@ messages!(
     (JoinChannelBufferResponse, Foreground),
     (LeaveChannelBuffer, Background),
     (UpdateChannelBuffer, Foreground),
-    (RemoveChannelBufferCollaborator, Foreground),
-    (AddChannelBufferCollaborator, Foreground),
-    (UpdateChannelBufferCollaborator, Foreground),
+    (UpdateChannelBufferCollaborators, Foreground),
+    (AckBufferOperation, Background),
+    (AckChannelMessage, Background),
 );
 
 request_messages!(
@@ -273,7 +286,7 @@ request_messages!(
     (CopyProjectEntry, ProjectEntryResponse),
     (CreateProjectEntry, ProjectEntryResponse),
     (CreateRoom, CreateRoomResponse),
-    (CreateChannel, ChannelResponse),
+    (CreateChannel, CreateChannelResponse),
     (DeclineCall, Ack),
     (DeleteProjectEntry, ProjectEntryResponse),
     (ExpandProjectEntry, ExpandProjectEntryResponse),
@@ -293,6 +306,7 @@ request_messages!(
     (InviteChannelMember, Ack),
     (JoinProject, JoinProjectResponse),
     (JoinRoom, JoinRoomResponse),
+    (JoinChannelChat, JoinChannelChatResponse),
     (LeaveRoom, Ack),
     (RejoinRoom, RejoinRoomResponse),
     (IncomingCall, Ack),
@@ -313,11 +327,17 @@ request_messages!(
     (RespondToContactRequest, Ack),
     (RespondToChannelInvite, Ack),
     (SetChannelMemberAdmin, Ack),
+    (SendChannelMessage, SendChannelMessageResponse),
+    (GetChannelMessages, GetChannelMessagesResponse),
     (GetChannelMembers, GetChannelMembersResponse),
     (JoinChannel, JoinRoomResponse),
-    (RemoveChannel, Ack),
+    (RemoveChannelMessage, Ack),
+    (DeleteChannel, Ack),
     (RenameProjectEntry, ProjectEntryResponse),
-    (RenameChannel, ChannelResponse),
+    (RenameChannel, RenameChannelResponse),
+    (LinkChannel, Ack),
+    (UnlinkChannel, Ack),
+    (MoveChannel, Ack),
     (SaveBuffer, BufferSaved),
     (SearchProject, SearchProjectResponse),
     (ShareProject, ShareProjectResponse),
@@ -344,7 +364,6 @@ entity_messages!(
     CreateProjectEntry,
     DeleteProjectEntry,
     ExpandProjectEntry,
-    Follow,
     FormatBuffers,
     GetCodeActions,
     GetCompletions,
@@ -372,12 +391,10 @@ entity_messages!(
     SearchProject,
     StartLanguageServer,
     SynchronizeBuffers,
-    Unfollow,
     UnshareProject,
     UpdateBuffer,
     UpdateBufferFile,
     UpdateDiagnosticSummary,
-    UpdateFollowers,
     UpdateLanguageServer,
     UpdateProject,
     UpdateProjectCollaborator,
@@ -388,10 +405,10 @@ entity_messages!(
 
 entity_messages!(
     channel_id,
+    ChannelMessageSent,
     UpdateChannelBuffer,
-    RemoveChannelBufferCollaborator,
-    AddChannelBufferCollaborator,
-    UpdateChannelBufferCollaborator
+    RemoveChannelMessage,
+    UpdateChannelBufferCollaborators,
 );
 
 const KIB: usize = 1024;

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

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 62;
+pub const PROTOCOL_VERSION: u32 = 64;

crates/search/src/buffer_search.rs πŸ”—

@@ -2,19 +2,16 @@ use crate::{
     history::SearchHistory,
     mode::{next_mode, SearchMode, Side},
     search_bar::{render_nav_button, render_search_mode_button},
-    CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches,
-    SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
+    CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
+    SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace,
+    ToggleWholeWord,
 };
 use collections::HashMap;
 use editor::Editor;
 use futures::channel::oneshot;
 use gpui::{
-    actions,
-    elements::*,
-    impl_actions,
-    platform::{CursorStyle, MouseButton},
-    Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
-    WindowContext,
+    actions, elements::*, impl_actions, Action, AnyViewHandle, AppContext, Entity, Subscription,
+    Task, View, ViewContext, ViewHandle, WindowContext,
 };
 use project::search::SearchQuery;
 use serde::Deserialize;
@@ -54,6 +51,12 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(BufferSearchBar::previous_history_query);
     cx.add_action(BufferSearchBar::cycle_mode);
     cx.add_action(BufferSearchBar::cycle_mode_on_pane);
+    cx.add_action(BufferSearchBar::replace_all);
+    cx.add_action(BufferSearchBar::replace_next);
+    cx.add_action(BufferSearchBar::replace_all_on_pane);
+    cx.add_action(BufferSearchBar::replace_next_on_pane);
+    cx.add_action(BufferSearchBar::toggle_replace);
+    cx.add_action(BufferSearchBar::toggle_replace_on_a_pane);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
     add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
 }
@@ -73,9 +76,11 @@ fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContex
 
 pub struct BufferSearchBar {
     query_editor: ViewHandle<Editor>,
+    replacement_editor: ViewHandle<Editor>,
     active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
     active_match_index: Option<usize>,
     active_searchable_item_subscription: Option<Subscription>,
+    active_search: Option<Arc<SearchQuery>>,
     searchable_items_with_matches:
         HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
     pending_search: Option<Task<()>>,
@@ -85,6 +90,7 @@ pub struct BufferSearchBar {
     dismissed: bool,
     search_history: SearchHistory,
     current_mode: SearchMode,
+    replace_enabled: bool,
 }
 
 impl Entity for BufferSearchBar {
@@ -96,6 +102,21 @@ impl View for BufferSearchBar {
         "BufferSearchBar"
     }
 
+    fn update_keymap_context(
+        &self,
+        keymap: &mut gpui::keymap_matcher::KeymapContext,
+        cx: &AppContext,
+    ) {
+        Self::reset_to_default_keymap_context(keymap);
+        let in_replace = self
+            .replacement_editor
+            .read_with(cx, |_, cx| cx.is_self_focused())
+            .unwrap_or(false);
+        if in_replace {
+            keymap.add_identifier("in_replace");
+        }
+    }
+
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
         if cx.is_self_focused() {
             cx.focus(&self.query_editor);
@@ -156,6 +177,9 @@ impl View for BufferSearchBar {
         self.query_editor.update(cx, |editor, cx| {
             editor.set_placeholder_text(new_placeholder_text, cx);
         });
+        self.replacement_editor.update(cx, |editor, cx| {
+            editor.set_placeholder_text("Replace with...", cx);
+        });
         let search_button_for_mode = |mode, side, cx: &mut ViewContext<BufferSearchBar>| {
             let is_active = self.current_mode == mode;
 
@@ -212,7 +236,6 @@ impl View for BufferSearchBar {
                 cx,
             )
         };
-
         let query_column = Flex::row()
             .with_child(
                 Svg::for_style(theme.search.editor_icon.clone().icon)
@@ -243,7 +266,57 @@ impl View for BufferSearchBar {
             .with_max_width(theme.search.editor.max_width)
             .with_height(theme.search.search_bar_row_height)
             .flex(1., false);
+        let should_show_replace_input = self.replace_enabled && supported_options.replacement;
 
+        let replacement = should_show_replace_input.then(|| {
+            Flex::row()
+                .with_child(
+                    Svg::for_style(theme.search.replace_icon.clone().icon)
+                        .contained()
+                        .with_style(theme.search.replace_icon.clone().container),
+                )
+                .with_child(ChildView::new(&self.replacement_editor, cx).flex(1., true))
+                .align_children_center()
+                .flex(1., true)
+                .contained()
+                .with_style(query_container_style)
+                .constrained()
+                .with_min_width(theme.search.editor.min_width)
+                .with_max_width(theme.search.editor.max_width)
+                .with_height(theme.search.search_bar_row_height)
+                .flex(1., false)
+        });
+        let replace_all = should_show_replace_input.then(|| {
+            super::replace_action(
+                ReplaceAll,
+                "Replace all",
+                "icons/replace_all.svg",
+                theme.tooltip.clone(),
+                theme.search.action_button.clone(),
+            )
+        });
+        let replace_next = should_show_replace_input.then(|| {
+            super::replace_action(
+                ReplaceNext,
+                "Replace next",
+                "icons/replace_next.svg",
+                theme.tooltip.clone(),
+                theme.search.action_button.clone(),
+            )
+        });
+        let switches_column = supported_options.replacement.then(|| {
+            Flex::row()
+                .align_children_center()
+                .with_child(super::toggle_replace_button(
+                    self.replace_enabled,
+                    theme.tooltip.clone(),
+                    theme.search.option_button_component.clone(),
+                ))
+                .constrained()
+                .with_height(theme.search.search_bar_row_height)
+                .contained()
+                .with_style(theme.search.option_button_group)
+        });
         let mode_column = Flex::row()
             .with_child(search_button_for_mode(
                 SearchMode::Text,
@@ -261,7 +334,10 @@ impl View for BufferSearchBar {
             .with_height(theme.search.search_bar_row_height);
 
         let nav_column = Flex::row()
-            .with_child(self.render_action_button("all", cx))
+            .align_children_center()
+            .with_children(replace_next)
+            .with_children(replace_all)
+            .with_child(self.render_action_button("icons/select-all.svg", cx))
             .with_child(Flex::row().with_children(match_count))
             .with_child(nav_button_for_direction("<", Direction::Prev, cx))
             .with_child(nav_button_for_direction(">", Direction::Next, cx))
@@ -272,6 +348,8 @@ impl View for BufferSearchBar {
         Flex::row()
             .with_child(query_column)
             .with_child(mode_column)
+            .with_children(switches_column)
+            .with_children(replacement)
             .with_child(nav_column)
             .contained()
             .with_style(theme.search.container)
@@ -345,9 +423,18 @@ impl BufferSearchBar {
         });
         cx.subscribe(&query_editor, Self::on_query_editor_event)
             .detach();
-
+        let replacement_editor = cx.add_view(|cx| {
+            Editor::auto_height(
+                2,
+                Some(Arc::new(|theme| theme.search.editor.input.clone())),
+                cx,
+            )
+        });
+        // cx.subscribe(&replacement_editor, Self::on_query_editor_event)
+        //     .detach();
         Self {
             query_editor,
+            replacement_editor,
             active_searchable_item: None,
             active_searchable_item_subscription: None,
             active_match_index: None,
@@ -359,6 +446,8 @@ impl BufferSearchBar {
             dismissed: true,
             search_history: SearchHistory::default(),
             current_mode: SearchMode::default(),
+            active_search: None,
+            replace_enabled: false,
         }
     }
 
@@ -441,13 +530,32 @@ impl BufferSearchBar {
     pub fn query(&self, cx: &WindowContext) -> String {
         self.query_editor.read(cx).text(cx)
     }
-
+    pub fn replacement(&self, cx: &WindowContext) -> String {
+        self.replacement_editor.read(cx).text(cx)
+    }
     pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
         self.active_searchable_item
             .as_ref()
             .map(|searchable_item| searchable_item.query_suggestion(cx))
     }
 
+    pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
+        if replacement.is_none() {
+            self.replace_enabled = false;
+            return;
+        }
+        self.replace_enabled = true;
+        self.replacement_editor
+            .update(cx, |replacement_editor, cx| {
+                replacement_editor
+                    .buffer()
+                    .update(cx, |replacement_buffer, cx| {
+                        let len = replacement_buffer.len(cx);
+                        replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
+                    });
+            });
+    }
+
     pub fn search(
         &mut self,
         query: &str,
@@ -477,37 +585,16 @@ impl BufferSearchBar {
     ) -> AnyElement<Self> {
         let tooltip = "Select All Matches";
         let tooltip_style = theme::current(cx).tooltip.clone();
-        let action_type_id = 0_usize;
-        let has_matches = self.active_match_index.is_some();
-        let cursor_style = if has_matches {
-            CursorStyle::PointingHand
-        } else {
-            CursorStyle::default()
-        };
-        enum ActionButton {}
-        MouseEventHandler::new::<ActionButton, _>(action_type_id, cx, |state, cx| {
-            let theme = theme::current(cx);
-            let style = theme
-                .search
-                .action_button
-                .in_state(has_matches)
-                .style_for(state);
-            Label::new(icon, style.text.clone())
-                .aligned()
-                .contained()
-                .with_style(style.container)
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            this.select_all_matches(&SelectAllMatches, cx)
-        })
-        .with_cursor_style(cursor_style)
-        .with_tooltip::<ActionButton>(
-            action_type_id,
-            tooltip.to_string(),
-            Some(Box::new(SelectAllMatches)),
-            tooltip_style,
-            cx,
-        )
+
+        let theme = theme::current(cx);
+        let style = theme.search.action_button.clone();
+
+        gpui::elements::Component::element(SafeStylable::with_style(
+            theme::components::action_button::Button::action(SelectAllMatches)
+                .with_tooltip(tooltip, tooltip_style)
+                .with_contents(theme::components::svg::Svg::new(icon)),
+            style,
+        ))
         .into_any()
     }
 
@@ -609,6 +696,22 @@ impl BufferSearchBar {
         }
     }
 
+    pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+            if let Some(matches) = self
+                .searchable_items_with_matches
+                .get(&searchable_item.downgrade())
+            {
+                if matches.len() == 0 {
+                    return;
+                }
+                let new_match_index = matches.len() - 1;
+                searchable_item.update_matches(matches, cx);
+                searchable_item.activate_match(new_match_index, matches, cx);
+            }
+        }
+    }
+
     fn select_next_match_on_pane(
         pane: &mut Pane,
         action: &SelectNextMatch,
@@ -688,6 +791,7 @@ impl BufferSearchBar {
         let (done_tx, done_rx) = oneshot::channel();
         let query = self.query(cx);
         self.pending_search.take();
+
         if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
             if query.is_empty() {
                 self.active_match_index.take();
@@ -695,7 +799,7 @@ impl BufferSearchBar {
                 let _ = done_tx.send(());
                 cx.notify();
             } else {
-                let query = if self.current_mode == SearchMode::Regex {
+                let query: Arc<_> = if self.current_mode == SearchMode::Regex {
                     match SearchQuery::regex(
                         query,
                         self.search_options.contains(SearchOptions::WHOLE_WORD),
@@ -703,7 +807,7 @@ impl BufferSearchBar {
                         Vec::new(),
                         Vec::new(),
                     ) {
-                        Ok(query) => query,
+                        Ok(query) => query.with_replacement(self.replacement(cx)),
                         Err(_) => {
                             self.query_contains_error = true;
                             cx.notify();
@@ -711,15 +815,23 @@ impl BufferSearchBar {
                         }
                     }
                 } else {
-                    SearchQuery::text(
+                    match SearchQuery::text(
                         query,
                         self.search_options.contains(SearchOptions::WHOLE_WORD),
                         self.search_options.contains(SearchOptions::CASE_SENSITIVE),
                         Vec::new(),
                         Vec::new(),
-                    )
-                };
-
+                    ) {
+                        Ok(query) => query.with_replacement(self.replacement(cx)),
+                        Err(_) => {
+                            self.query_contains_error = true;
+                            cx.notify();
+                            return done_rx;
+                        }
+                    }
+                }
+                .into();
+                self.active_search = Some(query.clone());
                 let query_text = query.as_str().to_string();
                 let matches = active_searchable_item.find_matches(query, cx);
 
@@ -810,6 +922,97 @@ impl BufferSearchBar {
             cx.propagate_action();
         }
     }
+    fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
+        if let Some(_) = &self.active_searchable_item {
+            self.replace_enabled = !self.replace_enabled;
+            if !self.replace_enabled {
+                cx.focus(&self.query_editor);
+            }
+            cx.notify();
+        }
+    }
+    fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext<Pane>) {
+        let mut should_propagate = true;
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            search_bar.update(cx, |bar, cx| {
+                if let Some(_) = &bar.active_searchable_item {
+                    should_propagate = false;
+                    bar.replace_enabled = !bar.replace_enabled;
+                    if bar.dismissed {
+                        bar.show(cx);
+                    }
+                    if !bar.replace_enabled {
+                        cx.focus(&bar.query_editor);
+                    }
+                    cx.notify();
+                }
+            });
+        }
+        if should_propagate {
+            cx.propagate_action();
+        }
+    }
+    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
+        let mut should_propagate = true;
+        if !self.dismissed && self.active_search.is_some() {
+            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+                if let Some(query) = self.active_search.as_ref() {
+                    if let Some(matches) = self
+                        .searchable_items_with_matches
+                        .get(&searchable_item.downgrade())
+                    {
+                        if let Some(active_index) = self.active_match_index {
+                            let query = query
+                                .as_ref()
+                                .clone()
+                                .with_replacement(self.replacement(cx));
+                            searchable_item.replace(&matches[active_index], &query, cx);
+                            self.select_next_match(&SelectNextMatch, cx);
+                        }
+                        should_propagate = false;
+                        self.focus_editor(&FocusEditor, cx);
+                    }
+                }
+            }
+        }
+        if should_propagate {
+            cx.propagate_action();
+        }
+    }
+    pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
+        if !self.dismissed && self.active_search.is_some() {
+            if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+                if let Some(query) = self.active_search.as_ref() {
+                    if let Some(matches) = self
+                        .searchable_items_with_matches
+                        .get(&searchable_item.downgrade())
+                    {
+                        let query = query
+                            .as_ref()
+                            .clone()
+                            .with_replacement(self.replacement(cx));
+                        for m in matches {
+                            searchable_item.replace(m, &query, cx);
+                        }
+                    }
+                }
+            }
+        }
+    }
+    fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext<Pane>) {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            search_bar.update(cx, |bar, cx| bar.replace_next(action, cx));
+            return;
+        }
+        cx.propagate_action();
+    }
+    fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext<Pane>) {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            search_bar.update(cx, |bar, cx| bar.replace_all(action, cx));
+            return;
+        }
+        cx.propagate_action();
+    }
 }
 
 #[cfg(test)]
@@ -861,7 +1064,7 @@ mod tests {
             .unwrap();
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[
                     (
                         DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
@@ -882,7 +1085,7 @@ mod tests {
         editor.next_notification(cx).await;
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[(
                     DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
                     Color::red(),
@@ -898,7 +1101,7 @@ mod tests {
             .unwrap();
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[
                     (
                         DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
@@ -939,7 +1142,7 @@ mod tests {
         editor.next_notification(cx).await;
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[
                     (
                         DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
@@ -1150,7 +1353,7 @@ mod tests {
             .unwrap();
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[(
                     DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
                     Color::red(),
@@ -1177,7 +1380,7 @@ mod tests {
         editor.next_notification(cx).await;
         editor.update(cx, |editor, cx| {
             assert_eq!(
-                editor.all_background_highlights(cx),
+                editor.all_text_background_highlights(cx),
                 &[(
                     DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
                     Color::red(),
@@ -1539,4 +1742,109 @@ mod tests {
             assert_eq!(search_bar.search_options, SearchOptions::NONE);
         });
     }
+    #[gpui::test]
+    async fn test_replace_simple(cx: &mut TestAppContext) {
+        let (editor, search_bar) = init_test(cx);
+
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.search("expression", None, cx)
+            })
+            .await
+            .unwrap();
+
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.replacement_editor.update(cx, |editor, cx| {
+                // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
+                editor.set_text("expr$1", cx);
+            });
+            search_bar.replace_all(&ReplaceAll, cx)
+        });
+        assert_eq!(
+            editor.read_with(cx, |this, cx| { this.text(cx) }),
+            r#"
+        A regular expr$1 (shortened as regex or regexp;[1] also referred to as
+        rational expr$1[2][3]) is a sequence of characters that specifies a search
+        pattern in text. Usually such patterns are used by string-searching algorithms
+        for "find" or "find and replace" operations on strings, or for input validation.
+        "#
+            .unindent()
+        );
+
+        // Search for word boundaries and replace just a single one.
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
+            })
+            .await
+            .unwrap();
+
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.replacement_editor.update(cx, |editor, cx| {
+                editor.set_text("banana", cx);
+            });
+            search_bar.replace_next(&ReplaceNext, cx)
+        });
+        // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
+        assert_eq!(
+            editor.read_with(cx, |this, cx| { this.text(cx) }),
+            r#"
+        A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
+        rational expr$1[2][3]) is a sequence of characters that specifies a search
+        pattern in text. Usually such patterns are used by string-searching algorithms
+        for "find" or "find and replace" operations on strings, or for input validation.
+        "#
+            .unindent()
+        );
+        // Let's turn on regex mode.
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.activate_search_mode(SearchMode::Regex, cx);
+                search_bar.search("\\[([^\\]]+)\\]", None, cx)
+            })
+            .await
+            .unwrap();
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.replacement_editor.update(cx, |editor, cx| {
+                editor.set_text("${1}number", cx);
+            });
+            search_bar.replace_all(&ReplaceAll, cx)
+        });
+        assert_eq!(
+            editor.read_with(cx, |this, cx| { this.text(cx) }),
+            r#"
+        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
+        rational expr$12number3number) is a sequence of characters that specifies a search
+        pattern in text. Usually such patterns are used by string-searching algorithms
+        for "find" or "find and replace" operations on strings, or for input validation.
+        "#
+            .unindent()
+        );
+        // Now with a whole-word twist.
+        search_bar
+            .update(cx, |search_bar, cx| {
+                search_bar.activate_search_mode(SearchMode::Regex, cx);
+                search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
+            })
+            .await
+            .unwrap();
+        search_bar.update(cx, |search_bar, cx| {
+            search_bar.replacement_editor.update(cx, |editor, cx| {
+                editor.set_text("things", cx);
+            });
+            search_bar.replace_all(&ReplaceAll, cx)
+        });
+        // The only word affected by this edit should be `algorithms`, even though there's a bunch
+        // of words in this text that would match this regex if not for WHOLE_WORD.
+        assert_eq!(
+            editor.read_with(cx, |this, cx| { this.text(cx) }),
+            r#"
+        A regular expr$1 (shortened as regex banana regexp;1number also referred to as
+        rational expr$12number3number) is a sequence of characters that specifies a search
+        pattern in text. Usually such patterns are used by string-searching things
+        for "find" or "find and replace" operations on strings, or for input validation.
+        "#
+            .unindent()
+        );
+    }
 }

crates/search/src/project_search.rs πŸ”—

@@ -2,8 +2,9 @@ use crate::{
     history::SearchHistory,
     mode::{SearchMode, Side},
     search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
-    ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions,
-    SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord,
+    ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery,
+    PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch,
+    ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
 };
 use anyhow::{Context, Result};
 use collections::HashMap;
@@ -51,20 +52,37 @@ actions!(
 #[derive(Default)]
 struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
 
+#[derive(Default)]
+struct ActiveSettings(HashMap<WeakModelHandle<Project>, ProjectSearchSettings>);
+
 pub fn init(cx: &mut AppContext) {
     cx.set_global(ActiveSearches::default());
+    cx.set_global(ActiveSettings::default());
     cx.add_action(ProjectSearchView::deploy);
     cx.add_action(ProjectSearchView::move_focus_to_results);
-    cx.add_action(ProjectSearchBar::search);
+    cx.add_action(ProjectSearchBar::confirm);
     cx.add_action(ProjectSearchBar::search_in_new);
     cx.add_action(ProjectSearchBar::select_next_match);
     cx.add_action(ProjectSearchBar::select_prev_match);
+    cx.add_action(ProjectSearchBar::replace_next);
+    cx.add_action(ProjectSearchBar::replace_all);
     cx.add_action(ProjectSearchBar::cycle_mode);
     cx.add_action(ProjectSearchBar::next_history_query);
     cx.add_action(ProjectSearchBar::previous_history_query);
     cx.add_action(ProjectSearchBar::activate_regex_mode);
+    cx.add_action(ProjectSearchBar::toggle_replace);
+    cx.add_action(ProjectSearchBar::toggle_replace_on_a_pane);
+    cx.add_action(ProjectSearchBar::activate_text_mode);
+
+    // This action should only be registered if the semantic index is enabled
+    // We are registering it all the time, as I dont want to introduce a dependency
+    // for Semantic Index Settings globally whenever search is tested.
+    cx.add_action(ProjectSearchBar::activate_semantic_mode);
+
     cx.capture_action(ProjectSearchBar::tab);
     cx.capture_action(ProjectSearchBar::tab_previous);
+    cx.capture_action(ProjectSearchView::replace_all);
+    cx.capture_action(ProjectSearchView::replace_next);
     add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
     add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
     add_toggle_filters_action::<ToggleFilters>(cx);
@@ -115,6 +133,7 @@ enum InputPanel {
 pub struct ProjectSearchView {
     model: ModelHandle<ProjectSearch>,
     query_editor: ViewHandle<Editor>,
+    replacement_editor: ViewHandle<Editor>,
     results_editor: ViewHandle<Editor>,
     semantic_state: Option<SemanticState>,
     semantic_permissioned: Option<bool>,
@@ -126,6 +145,7 @@ pub struct ProjectSearchView {
     included_files_editor: ViewHandle<Editor>,
     excluded_files_editor: ViewHandle<Editor>,
     filters_enabled: bool,
+    replace_enabled: bool,
     current_mode: SearchMode,
 }
 
@@ -135,6 +155,13 @@ struct SemanticState {
     _subscription: Subscription,
 }
 
+#[derive(Debug, Clone)]
+struct ProjectSearchSettings {
+    search_options: SearchOptions,
+    filters_enabled: bool,
+    current_mode: SearchMode,
+}
+
 pub struct ProjectSearchBar {
     active_project_search: Option<ViewHandle<ProjectSearchView>>,
     subscription: Option<Subscription>,
@@ -303,7 +330,7 @@ impl View for ProjectSearchView {
             // If Text -> Major: "Text search all files and folders", Minor: {...}
 
             let current_mode = self.current_mode;
-            let major_text = if model.pending_search.is_some() {
+            let mut major_text = if model.pending_search.is_some() {
                 Cow::Borrowed("Searching...")
             } else if model.no_results.is_some_and(|v| v) {
                 Cow::Borrowed("No Results")
@@ -317,9 +344,18 @@ impl View for ProjectSearchView {
                 }
             };
 
+            let mut show_minor_text = true;
             let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
                 let status = semantic.index_status;
                 match status {
+                    SemanticIndexStatus::NotAuthenticated => {
+                        major_text = Cow::Borrowed("Not Authenticated");
+                        show_minor_text = false;
+                        Some(
+                            "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables"
+                                .to_string(),
+                        )
+                    }
                     SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
                     SemanticIndexStatus::Indexing {
                         remaining_files,
@@ -361,10 +397,13 @@ impl View for ProjectSearchView {
                         let mut minor_text = Vec::new();
                         minor_text.push("".into());
                         minor_text.extend(semantic_status);
-                        minor_text.push("Simply explain the code you are looking to find.".into());
-                        minor_text.push(
-                            "ex. 'prompt user for permissions to index their project'".into(),
-                        );
+                        if show_minor_text {
+                            minor_text
+                                .push("Simply explain the code you are looking to find.".into());
+                            minor_text.push(
+                                "ex. 'prompt user for permissions to index their project'".into(),
+                            );
+                        }
                         minor_text
                     }
                     _ => vec![
@@ -460,6 +499,13 @@ impl View for ProjectSearchView {
                 .insert(self.model.read(cx).project.downgrade(), handle)
         });
 
+        cx.update_global(|state: &mut ActiveSettings, cx| {
+            state.0.insert(
+                self.model.read(cx).project.downgrade(),
+                self.current_settings(),
+            );
+        });
+
         if cx.is_self_focused() {
             if self.query_editor_was_focused {
                 cx.focus(&self.query_editor);
@@ -483,6 +529,7 @@ impl Item for ProjectSearchView {
     fn should_close_item_on_event(event: &Self::Event) -> bool {
         event == &Self::Event::Dismiss
     }
+
     fn act_as_type<'a>(
         &'a self,
         type_id: TypeId,
@@ -511,7 +558,7 @@ impl Item for ProjectSearchView {
     ) -> AnyElement<T> {
         Flex::row()
             .with_child(
-                Svg::new("icons/magnifying_glass_12.svg")
+                Svg::new("icons/magnifying_glass.svg")
                     .with_color(tab_theme.label.text.color)
                     .constrained()
                     .with_width(tab_theme.type_icon_width)
@@ -593,7 +640,7 @@ impl Item for ProjectSearchView {
         Self: Sized,
     {
         let model = self.model.update(cx, |model, cx| model.clone(cx));
-        Some(Self::new(model, cx))
+        Some(Self::new(model, cx, None))
     }
 
     fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
@@ -651,8 +698,31 @@ impl Item for ProjectSearchView {
 }
 
 impl ProjectSearchView {
-    fn toggle_search_option(&mut self, option: SearchOptions) {
+    fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) {
+        self.filters_enabled = !self.filters_enabled;
+        cx.update_global(|state: &mut ActiveSettings, cx| {
+            state.0.insert(
+                self.model.read(cx).project.downgrade(),
+                self.current_settings(),
+            );
+        });
+    }
+
+    fn current_settings(&self) -> ProjectSearchSettings {
+        ProjectSearchSettings {
+            search_options: self.search_options,
+            filters_enabled: self.filters_enabled,
+            current_mode: self.current_mode,
+        }
+    }
+    fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) {
         self.search_options.toggle(option);
+        cx.update_global(|state: &mut ActiveSettings, cx| {
+            state.0.insert(
+                self.model.read(cx).project.downgrade(),
+                self.current_settings(),
+            );
+        });
     }
 
     fn index_project(&mut self, cx: &mut ViewContext<Self>) {
@@ -701,8 +771,9 @@ impl ProjectSearchView {
                         }));
                     return;
                 }
+            } else {
+                semantic_state.maintain_rate_limit = None;
             }
-            semantic_state.maintain_rate_limit = None;
         }
     }
 
@@ -784,14 +855,75 @@ impl ProjectSearchView {
             }
         }
 
+        cx.update_global(|state: &mut ActiveSettings, cx| {
+            state.0.insert(
+                self.model.read(cx).project.downgrade(),
+                self.current_settings(),
+            );
+        });
+
         cx.notify();
     }
+    fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
+        let model = self.model.read(cx);
+        if let Some(query) = model.active_query.as_ref() {
+            if model.match_ranges.is_empty() {
+                return;
+            }
+            if let Some(active_index) = self.active_match_index {
+                let query = query.clone().with_replacement(self.replacement(cx));
+                self.results_editor.replace(
+                    &(Box::new(model.match_ranges[active_index].clone()) as _),
+                    &query,
+                    cx,
+                );
+                self.select_match(Direction::Next, cx)
+            }
+        }
+    }
+    pub fn replacement(&self, cx: &AppContext) -> String {
+        self.replacement_editor.read(cx).text(cx)
+    }
+    fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
+        let model = self.model.read(cx);
+        if let Some(query) = model.active_query.as_ref() {
+            if model.match_ranges.is_empty() {
+                return;
+            }
+            if self.active_match_index.is_some() {
+                let query = query.clone().with_replacement(self.replacement(cx));
+                let matches = model
+                    .match_ranges
+                    .iter()
+                    .map(|item| Box::new(item.clone()) as _)
+                    .collect::<Vec<_>>();
+                for item in matches {
+                    self.results_editor.replace(&item, &query, cx);
+                }
+            }
+        }
+    }
 
-    fn new(model: ModelHandle<ProjectSearch>, cx: &mut ViewContext<Self>) -> Self {
+    fn new(
+        model: ModelHandle<ProjectSearch>,
+        cx: &mut ViewContext<Self>,
+        settings: Option<ProjectSearchSettings>,
+    ) -> Self {
         let project;
         let excerpts;
+        let mut replacement_text = None;
         let mut query_text = String::new();
-        let mut options = SearchOptions::NONE;
+
+        // Read in settings if available
+        let (mut options, current_mode, filters_enabled) = if let Some(settings) = settings {
+            (
+                settings.search_options,
+                settings.current_mode,
+                settings.filters_enabled,
+            )
+        } else {
+            (SearchOptions::NONE, Default::default(), false)
+        };
 
         {
             let model = model.read(cx);
@@ -799,6 +931,7 @@ impl ProjectSearchView {
             excerpts = model.excerpts.clone();
             if let Some(active_query) = model.active_query.as_ref() {
                 query_text = active_query.as_str().to_string();
+                replacement_text = active_query.replacement().map(ToOwned::to_owned);
                 options = SearchOptions::from_query(active_query);
             }
         }
@@ -819,7 +952,17 @@ impl ProjectSearchView {
             cx.emit(ViewEvent::EditorEvent(event.clone()))
         })
         .detach();
-
+        let replacement_editor = cx.add_view(|cx| {
+            let mut editor = Editor::single_line(
+                Some(Arc::new(|theme| theme.search.editor.input.clone())),
+                cx,
+            );
+            editor.set_placeholder_text("Replace in project..", cx);
+            if let Some(text) = replacement_text {
+                editor.set_text(text, cx);
+            }
+            editor
+        });
         let results_editor = cx.add_view(|cx| {
             let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx);
             editor.set_searchable(false);
@@ -870,10 +1013,10 @@ impl ProjectSearchView {
             cx.emit(ViewEvent::EditorEvent(event.clone()))
         })
         .detach();
-        let filters_enabled = false;
 
         // Check if Worktrees have all been previously indexed
         let mut this = ProjectSearchView {
+            replacement_editor,
             search_id: model.read(cx).search_id,
             model,
             query_editor,
@@ -887,7 +1030,8 @@ impl ProjectSearchView {
             included_files_editor,
             excluded_files_editor,
             filters_enabled,
-            current_mode: Default::default(),
+            current_mode,
+            replace_enabled: false,
         };
         this.model_changed(cx);
         this
@@ -918,7 +1062,7 @@ impl ProjectSearchView {
         };
 
         let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
-        let search = cx.add_view(|cx| ProjectSearchView::new(model, cx));
+        let search = cx.add_view(|cx| ProjectSearchView::new(model, cx, None));
         workspace.add_item(Box::new(search.clone()), cx);
         search.update(cx, |search, cx| {
             search
@@ -968,8 +1112,20 @@ impl ProjectSearchView {
             workspace.activate_item(&existing, cx);
             existing
         } else {
+            let settings = cx
+                .global::<ActiveSettings>()
+                .0
+                .get(&workspace.project().downgrade());
+
+            let settings = if let Some(settings) = settings {
+                Some(settings.clone())
+            } else {
+                None
+            };
+
             let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
-            let view = cx.add_view(|cx| ProjectSearchView::new(model, cx));
+            let view = cx.add_view(|cx| ProjectSearchView::new(model, cx, settings));
+
             workspace.add_item(Box::new(view.clone()), cx);
             view
         };
@@ -1049,13 +1205,23 @@ impl ProjectSearchView {
                     }
                 }
             }
-            _ => Some(SearchQuery::text(
+            _ => match SearchQuery::text(
                 text,
                 self.search_options.contains(SearchOptions::WHOLE_WORD),
                 self.search_options.contains(SearchOptions::CASE_SENSITIVE),
                 included_files,
                 excluded_files,
-            )),
+            ) {
+                Ok(query) => {
+                    self.panels_with_errors.remove(&InputPanel::Query);
+                    Some(query)
+                }
+                Err(_e) => {
+                    self.panels_with_errors.insert(InputPanel::Query);
+                    cx.notify();
+                    None
+                }
+            },
         }
     }
 
@@ -1205,9 +1371,18 @@ impl ProjectSearchBar {
             })
         }
     }
-    fn search(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+        let mut should_propagate = true;
         if let Some(search_view) = self.active_project_search.as_ref() {
-            search_view.update(cx, |search_view, cx| search_view.search(cx));
+            search_view.update(cx, |search_view, cx| {
+                if !search_view.replacement_editor.is_focused(cx) {
+                    should_propagate = false;
+                    search_view.search(cx);
+                }
+            });
+        }
+        if should_propagate {
+            cx.propagate_action();
         }
     }
 
@@ -1235,7 +1410,7 @@ impl ProjectSearchBar {
                     model
                 });
                 workspace.add_item(
-                    Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
+                    Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx, None))),
                     cx,
                 );
             }
@@ -1253,6 +1428,26 @@ impl ProjectSearchBar {
         }
     }
 
+    fn replace_next(pane: &mut Pane, _: &ReplaceNext, cx: &mut ViewContext<Pane>) {
+        if let Some(search_view) = pane
+            .active_item()
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            search_view.update(cx, |view, cx| view.replace_next(&ReplaceNext, cx));
+        } else {
+            cx.propagate_action();
+        }
+    }
+    fn replace_all(pane: &mut Pane, _: &ReplaceAll, cx: &mut ViewContext<Pane>) {
+        if let Some(search_view) = pane
+            .active_item()
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            search_view.update(cx, |view, cx| view.replace_all(&ReplaceAll, cx));
+        } else {
+            cx.propagate_action();
+        }
+    }
     fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext<Pane>) {
         if let Some(search_view) = pane
             .active_item()
@@ -1283,12 +1478,16 @@ impl ProjectSearchBar {
         };
 
         active_project_search.update(cx, |project_view, cx| {
-            let views = &[
-                &project_view.query_editor,
-                &project_view.included_files_editor,
-                &project_view.excluded_files_editor,
-            ];
-
+            let mut views = vec![&project_view.query_editor];
+            if project_view.filters_enabled {
+                views.extend([
+                    &project_view.included_files_editor,
+                    &project_view.excluded_files_editor,
+                ]);
+            }
+            if project_view.replace_enabled {
+                views.push(&project_view.replacement_editor);
+            }
             let current_index = match views
                 .iter()
                 .enumerate()
@@ -1314,15 +1513,58 @@ impl ProjectSearchBar {
     fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) -> bool {
         if let Some(search_view) = self.active_project_search.as_ref() {
             search_view.update(cx, |search_view, cx| {
-                search_view.toggle_search_option(option);
+                search_view.toggle_search_option(option, cx);
                 search_view.search(cx);
             });
+
             cx.notify();
             true
         } else {
             false
         }
     }
+    fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
+        if let Some(search) = &self.active_project_search {
+            search.update(cx, |this, cx| {
+                this.replace_enabled = !this.replace_enabled;
+                if !this.replace_enabled {
+                    cx.focus(&this.query_editor);
+                }
+                cx.notify();
+            });
+        }
+    }
+    fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext<Pane>) {
+        let mut should_propagate = true;
+        if let Some(search_view) = pane
+            .active_item()
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            search_view.update(cx, |this, cx| {
+                should_propagate = false;
+                this.replace_enabled = !this.replace_enabled;
+                if !this.replace_enabled {
+                    cx.focus(&this.query_editor);
+                }
+                cx.notify();
+            });
+        }
+        if should_propagate {
+            cx.propagate_action();
+        }
+    }
+    fn activate_text_mode(pane: &mut Pane, _: &ActivateTextMode, cx: &mut ViewContext<Pane>) {
+        if let Some(search_view) = pane
+            .active_item()
+            .and_then(|item| item.downcast::<ProjectSearchView>())
+        {
+            search_view.update(cx, |view, cx| {
+                view.activate_search_mode(SearchMode::Text, cx)
+            });
+        } else {
+            cx.propagate_action();
+        }
+    }
 
     fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext<Pane>) {
         if let Some(search_view) = pane
@@ -1337,10 +1579,29 @@ impl ProjectSearchBar {
         }
     }
 
+    fn activate_semantic_mode(
+        pane: &mut Pane,
+        _: &ActivateSemanticMode,
+        cx: &mut ViewContext<Pane>,
+    ) {
+        if SemanticIndex::enabled(cx) {
+            if let Some(search_view) = pane
+                .active_item()
+                .and_then(|item| item.downcast::<ProjectSearchView>())
+            {
+                search_view.update(cx, |view, cx| {
+                    view.activate_search_mode(SearchMode::Semantic, cx)
+                });
+            } else {
+                cx.propagate_action();
+            }
+        }
+    }
+
     fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) -> bool {
         if let Some(search_view) = self.active_project_search.as_ref() {
             search_view.update(cx, |search_view, cx| {
-                search_view.filters_enabled = !search_view.filters_enabled;
+                search_view.toggle_filters(cx);
                 search_view
                     .included_files_editor
                     .update(cx, |_, cx| cx.notify());
@@ -1426,6 +1687,28 @@ impl View for ProjectSearchBar {
         "ProjectSearchBar"
     }
 
+    fn update_keymap_context(
+        &self,
+        keymap: &mut gpui::keymap_matcher::KeymapContext,
+        cx: &AppContext,
+    ) {
+        Self::reset_to_default_keymap_context(keymap);
+        let in_replace = self
+            .active_project_search
+            .as_ref()
+            .map(|search| {
+                search
+                    .read(cx)
+                    .replacement_editor
+                    .read_with(cx, |_, cx| cx.is_self_focused())
+            })
+            .flatten()
+            .unwrap_or(false);
+        if in_replace {
+            keymap.add_identifier("in_replace");
+        }
+    }
+
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         if let Some(_search) = self.active_project_search.as_ref() {
             let search = _search.read(cx);
@@ -1439,7 +1722,7 @@ impl View for ProjectSearchBar {
             let search = _search.read(cx);
             let filter_button = render_option_button_icon(
                 search.filters_enabled,
-                "icons/filter_12.svg",
+                "icons/filter.svg",
                 0,
                 "Toggle filters",
                 Box::new(ToggleFilters),
@@ -1470,14 +1753,14 @@ impl View for ProjectSearchBar {
             };
             let case_sensitive = is_semantic_disabled.then(|| {
                 render_option_button_icon(
-                    "icons/case_insensitive_12.svg",
+                    "icons/case_insensitive.svg",
                     SearchOptions::CASE_SENSITIVE,
                     cx,
                 )
             });
 
             let whole_word = is_semantic_disabled.then(|| {
-                render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx)
+                render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
             });
 
             let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
@@ -1527,7 +1810,43 @@ impl View for ProjectSearchBar {
                 .with_style(theme.search.match_index.container)
                 .aligned()
             });
-
+            let should_show_replace_input = search.replace_enabled;
+            let replacement = should_show_replace_input.then(|| {
+                Flex::row()
+                    .with_child(
+                        Svg::for_style(theme.search.replace_icon.clone().icon)
+                            .contained()
+                            .with_style(theme.search.replace_icon.clone().container),
+                    )
+                    .with_child(ChildView::new(&search.replacement_editor, cx).flex(1., true))
+                    .align_children_center()
+                    .flex(1., true)
+                    .contained()
+                    .with_style(query_container_style)
+                    .constrained()
+                    .with_min_width(theme.search.editor.min_width)
+                    .with_max_width(theme.search.editor.max_width)
+                    .with_height(theme.search.search_bar_row_height)
+                    .flex(1., false)
+            });
+            let replace_all = should_show_replace_input.then(|| {
+                super::replace_action(
+                    ReplaceAll,
+                    "Replace all",
+                    "icons/replace_all.svg",
+                    theme.tooltip.clone(),
+                    theme.search.action_button.clone(),
+                )
+            });
+            let replace_next = should_show_replace_input.then(|| {
+                super::replace_action(
+                    ReplaceNext,
+                    "Replace next",
+                    "icons/replace_next.svg",
+                    theme.tooltip.clone(),
+                    theme.search.action_button.clone(),
+                )
+            });
             let query_column = Flex::column()
                 .with_spacing(theme.search.search_row_spacing)
                 .with_child(
@@ -1580,7 +1899,17 @@ impl View for ProjectSearchBar {
                         .flex(1., false)
                 }))
                 .flex(1., false);
-
+            let switches_column = Flex::row()
+                .align_children_center()
+                .with_child(super::toggle_replace_button(
+                    search.replace_enabled,
+                    theme.tooltip.clone(),
+                    theme.search.option_button_component.clone(),
+                ))
+                .constrained()
+                .with_height(theme.search.search_bar_row_height)
+                .contained()
+                .with_style(theme.search.option_button_group);
             let mode_column =
                 Flex::row()
                     .with_child(search_button_for_mode(
@@ -1618,6 +1947,8 @@ impl View for ProjectSearchBar {
             };
 
             let nav_column = Flex::row()
+                .with_children(replace_next)
+                .with_children(replace_all)
                 .with_child(Flex::row().with_children(matches))
                 .with_child(nav_button_for_direction("<", Direction::Prev, cx))
                 .with_child(nav_button_for_direction(">", Direction::Next, cx))
@@ -1628,6 +1959,8 @@ impl View for ProjectSearchBar {
             Flex::row()
                 .with_child(query_column)
                 .with_child(mode_column)
+                .with_child(switches_column)
+                .with_children(replacement)
                 .with_child(nav_column)
                 .contained()
                 .with_style(theme.search.container)
@@ -1704,7 +2037,7 @@ pub mod tests {
         let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
         let search_view = cx
-            .add_window(|cx| ProjectSearchView::new(search.clone(), cx))
+            .add_window(|cx| ProjectSearchView::new(search.clone(), cx, None))
             .root(cx);
 
         search_view.update(cx, |search_view, cx| {
@@ -1724,7 +2057,7 @@ pub mod tests {
             assert_eq!(
                 search_view
                     .results_editor
-                    .update(cx, |editor, cx| editor.all_background_highlights(cx)),
+                    .update(cx, |editor, cx| editor.all_text_background_highlights(cx)),
                 &[
                     (
                         DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35),

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

@@ -8,7 +8,9 @@ use gpui::{
 pub use mode::SearchMode;
 use project::search::SearchQuery;
 pub use project_search::{ProjectSearchBar, ProjectSearchView};
-use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle};
+use theme::components::{
+    action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle,
+};
 
 pub mod buffer_search;
 mod history;
@@ -27,6 +29,7 @@ actions!(
         CycleMode,
         ToggleWholeWord,
         ToggleCaseSensitive,
+        ToggleReplace,
         SelectNextMatch,
         SelectPrevMatch,
         SelectAllMatches,
@@ -34,7 +37,9 @@ actions!(
         PreviousHistoryQuery,
         ActivateTextMode,
         ActivateSemanticMode,
-        ActivateRegexMode
+        ActivateRegexMode,
+        ReplaceAll,
+        ReplaceNext,
     ]
 );
 
@@ -58,8 +63,8 @@ impl SearchOptions {
 
     pub fn icon(&self) -> &'static str {
         match *self {
-            SearchOptions::WHOLE_WORD => "icons/word_search_12.svg",
-            SearchOptions::CASE_SENSITIVE => "icons/case_insensitive_12.svg",
+            SearchOptions::WHOLE_WORD => "icons/word_search.svg",
+            SearchOptions::CASE_SENSITIVE => "icons/case_insensitive.svg",
             _ => panic!("{:?} is not a named SearchOption", self),
         }
     }
@@ -98,3 +103,32 @@ impl SearchOptions {
             .into_any()
     }
 }
+
+fn toggle_replace_button<V: View>(
+    active: bool,
+    tooltip_style: TooltipStyle,
+    button_style: ToggleIconButtonStyle,
+) -> AnyElement<V> {
+    Button::dynamic_action(Box::new(ToggleReplace))
+        .with_tooltip("Toggle Replace", tooltip_style)
+        .with_contents(theme::components::svg::Svg::new("icons/replace.svg"))
+        .toggleable(active)
+        .with_style(button_style)
+        .element()
+        .into_any()
+}
+
+fn replace_action<V: View>(
+    action: impl Action,
+    name: &'static str,
+    icon_path: &'static str,
+    tooltip_style: TooltipStyle,
+    button_style: IconButtonStyle,
+) -> AnyElement<V> {
+    Button::dynamic_action(Box::new(action))
+        .with_tooltip(name, tooltip_style)
+        .with_contents(theme::components::svg::Svg::new(icon_path))
+        .with_style(button_style)
+        .element()
+        .into_any()
+}

crates/semantic_index/Cargo.toml πŸ”—

@@ -9,6 +9,7 @@ path = "src/semantic_index.rs"
 doctest = false
 
 [dependencies]
+ai = { path = "../ai" }
 collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
@@ -23,24 +24,22 @@ settings = { path = "../settings" }
 anyhow.workspace = true
 postage.workspace = true
 futures.workspace = true
+ordered-float.workspace = true
 smol.workspace = true
-rusqlite = { version = "0.27.0", features = ["blob", "array", "modern_sqlite"] }
-isahc.workspace = true
+rusqlite.workspace = true
 log.workspace = true
 tree-sitter.workspace = true
 lazy_static.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 async-trait.workspace = true
-bincode = "1.3.3"
-matrixmultiply = "0.3.7"
 tiktoken-rs = "0.5.0"
 parking_lot.workspace = true
 rand.workspace = true
 schemars.workspace = true
 globset.workspace = true
 sha1 = "0.10.5"
-parse_duration = "2.1.1"
+ndarray = { version = "0.15.0" }
 
 [dev-dependencies]
 collections = { path = "../collections", features = ["test-support"] }
@@ -50,6 +49,10 @@ project = { path = "../project", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 settings = { path = "../settings", features = ["test-support"]}
+rust-embed = { version = "8.0", features = ["include-exclude"] }
+client = { path = "../client" }
+zed = { path = "../zed"}
+node_runtime = { path = "../node_runtime"}
 
 pretty_assertions.workspace = true
 rand.workspace = true
@@ -67,3 +70,6 @@ tree-sitter-elixir.workspace = true
 tree-sitter-lua.workspace = true
 tree-sitter-ruby.workspace = true
 tree-sitter-php.workspace = true
+
+[[example]]
+name = "eval"

crates/semantic_index/README.md πŸ”—

@@ -1,31 +1,20 @@
 
-WIP: Sample SQL Queries
-/*
+# Semantic Index
 
-create table "files" (
-"id" INTEGER PRIMARY KEY,
-"path" VARCHAR,
-"sha1" VARCHAR,
-);
+## Evaluation
 
-create table symbols (
-"file_id" INTEGER REFERENCES("files", "id") ON CASCADE DELETE,
-"offset" INTEGER,
-"embedding" VECTOR,
-);
+### Metrics
 
-insert into "files" ("path", "sha1") values ("src/main.rs", "sha1") return id;
-insert into symbols (
-"file_id",
-"start",
-"end",
-"embedding"
-) values (
-(id,),
-(id,),
-(id,),
-(id,),
-)
+nDCG@k:
+- "The value of NDCG is determined by comparing the relevance of the items returned by the search engine to the relevance of the item that a hypothetical "ideal" search engine would return.
+- "The relevance of result is represented by a score (also known as a 'grade') that is assigned to the search query. The scores of these results are then discounted based on their position in the search results -- did they get recommended first or last?"
 
+MRR@k:
+- "Mean reciprocal rank quantifies the rank of the first relevant item found in teh recommendation list."
 
-*/
+MAP@k:
+- "Mean average precision averages the precision@k metric at each relevant item position in the recommendation list.
+
+Resources:
+- [Evaluating recommendation metrics](https://www.shaped.ai/blog/evaluating-recommendation-systems-map-mmr-ndcg)
+- [Math Walkthrough](https://towardsdatascience.com/demystifying-ndcg-bee3be58cfe0)

crates/semantic_index/eval/gpt-engineer.json πŸ”—

@@ -0,0 +1,114 @@
+{
+  "repo": "https://github.com/AntonOsika/gpt-engineer.git",
+  "commit": "7735a6445bae3611c62f521e6464c67c957f87c2",
+  "assertions": [
+    {
+      "query": "How do I contribute to this project?",
+      "matches": [
+        ".github/CONTRIBUTING.md:1",
+        "ROADMAP.md:48"
+      ]
+    },
+    {
+      "query": "What version of the openai package is active?",
+      "matches": [
+        "pyproject.toml:14"
+      ]
+    },
+    {
+      "query": "Ask user for clarification",
+      "matches": [
+        "gpt_engineer/steps.py:69"
+      ]
+    },
+    {
+      "query": "generate tests for python code",
+      "matches": [
+        "gpt_engineer/steps.py:153"
+      ]
+    },
+    {
+      "query": "get item from database based on key",
+      "matches": [
+        "gpt_engineer/db.py:42",
+        "gpt_engineer/db.py:68"
+      ]
+    },
+    {
+      "query": "prompt user to select files",
+      "matches": [
+        "gpt_engineer/file_selector.py:171",
+        "gpt_engineer/file_selector.py:306",
+        "gpt_engineer/file_selector.py:289",
+        "gpt_engineer/file_selector.py:234"
+      ]
+    },
+    {
+      "query": "send to rudderstack",
+      "matches": [
+        "gpt_engineer/collect.py:11",
+        "gpt_engineer/collect.py:38"
+      ]
+    },
+    {
+      "query": "parse code blocks from chat messages",
+      "matches": [
+        "gpt_engineer/chat_to_files.py:10",
+        "docs/intro/chat_parsing.md:1"
+      ]
+    },
+    {
+      "query": "how do I use the docker cli?",
+      "matches": [
+        "docker/README.md:1"
+      ]
+    },
+    {
+      "query": "ask the user if the code ran successfully?",
+      "matches": [
+        "gpt_engineer/learning.py:54"
+      ]
+    },
+    {
+      "query": "how is consent granted by the user?",
+      "matches": [
+        "gpt_engineer/learning.py:107",
+        "gpt_engineer/learning.py:130",
+        "gpt_engineer/learning.py:152"
+      ]
+    },
+    {
+      "query": "what are all the different steps the agent can take?",
+      "matches": [
+        "docs/intro/steps_module.md:1",
+        "gpt_engineer/steps.py:391"
+      ]
+    },
+    {
+      "query": "ask the user for clarification?",
+      "matches": [
+        "gpt_engineer/steps.py:69"
+      ]
+    },
+    {
+      "query": "what models are available?",
+      "matches": [
+        "gpt_engineer/ai.py:315",
+        "gpt_engineer/ai.py:341",
+        "docs/open-models.md:1"
+      ]
+    },
+    {
+      "query": "what is the current focus of the project?",
+      "matches": [
+        "ROADMAP.md:11"
+      ]
+    },
+    {
+      "query": "does the agent know how to fix code?",
+      "matches": [
+        "gpt_engineer/steps.py:367"
+      ]
+    }
+  ]
+}

crates/semantic_index/eval/tree-sitter.json πŸ”—

@@ -0,0 +1,104 @@
+{
+  "repo": "https://github.com/tree-sitter/tree-sitter.git",
+  "commit": "46af27796a76c72d8466627d499f2bca4af958ee",
+  "assertions": [
+    {
+      "query": "What attributes are available for the tags configuration struct?",
+      "matches": [
+        "tags/src/lib.rs:24"
+      ]
+    },
+    {
+      "query": "create a new tag configuration",
+      "matches": [
+        "tags/src/lib.rs:119"
+      ]
+    },
+    {
+      "query": "generate tags based on config",
+      "matches": [
+        "tags/src/lib.rs:261"
+      ]
+    },
+    {
+      "query": "match on ts quantifier in rust",
+      "matches": [
+        "lib/binding_rust/lib.rs:139"
+      ]
+    },
+    {
+      "query": "cli command to generate tags",
+      "matches": [
+        "cli/src/tags.rs:10"
+      ]
+    },
+    {
+      "query": "what version of the tree-sitter-tags package is active?",
+      "matches": [
+        "tags/Cargo.toml:4"
+      ]
+    },
+    {
+      "query": "Insert a new parse state",
+      "matches": [
+        "cli/src/generate/build_tables/build_parse_table.rs:153"
+      ]
+    },
+    {
+      "query": "Handle conflict when numerous actions occur on the same symbol",
+      "matches": [
+        "cli/src/generate/build_tables/build_parse_table.rs:363",
+        "cli/src/generate/build_tables/build_parse_table.rs:442"
+      ]
+    },
+    {
+      "query": "Match based on associativity of actions",
+      "matches": [
+        "cri/src/generate/build_tables/build_parse_table.rs:542"
+      ]
+    },
+    {
+      "query": "Format token set display",
+      "matches": [
+        "cli/src/generate/build_tables/item.rs:246"
+      ]
+    },
+    {
+      "query": "extract choices from rule",
+      "matches": [
+        "cli/src/generate/prepare_grammar/flatten_grammar.rs:124"
+      ]
+    },
+    {
+      "query": "How do we identify if a symbol is being used?",
+      "matches": [
+        "cli/src/generate/prepare_grammar/flatten_grammar.rs:175"
+      ]
+    },
+    {
+      "query": "How do we launch the playground?",
+      "matches": [
+        "cli/src/playground.rs:46"
+      ]
+    },
+    {
+      "query": "How do we test treesitter query matches in rust?",
+      "matches": [
+        "cli/src/query_testing.rs:152",
+        "cli/src/tests/query_test.rs:781",
+        "cli/src/tests/query_test.rs:2163",
+        "cli/src/tests/query_test.rs:3781",
+        "cli/src/tests/query_test.rs:887"
+      ]
+    },
+    {
+      "query": "What does the CLI do?",
+      "matches": [
+        "cli/README.md:10",
+        "cli/loader/README.md:3",
+        "docs/section-5-implementation.md:14",
+        "docs/section-5-implementation.md:18"
+      ]
+    }
+  ]
+}

crates/semantic_index/examples/eval.rs πŸ”—

@@ -0,0 +1,531 @@
+use ai::embedding::OpenAIEmbeddings;
+use anyhow::{anyhow, Result};
+use client::{self, UserStore};
+use gpui::{AsyncAppContext, ModelHandle, Task};
+use language::LanguageRegistry;
+use node_runtime::RealNodeRuntime;
+use project::{Project, RealFs};
+use semantic_index::semantic_index_settings::SemanticIndexSettings;
+use semantic_index::{SearchResult, SemanticIndex};
+use serde::{Deserialize, Serialize};
+use settings::{default_settings, SettingsStore};
+use std::path::{Path, PathBuf};
+use std::process::Command;
+use std::sync::Arc;
+use std::time::{Duration, Instant};
+use std::{cmp, env, fs};
+use util::channel::{RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
+use util::http::{self};
+use util::paths::EMBEDDINGS_DIR;
+use zed::languages;
+
+#[derive(Deserialize, Clone, Serialize)]
+struct EvaluationQuery {
+    query: String,
+    matches: Vec<String>,
+}
+
+impl EvaluationQuery {
+    fn match_pairs(&self) -> Vec<(PathBuf, u32)> {
+        let mut pairs = Vec::new();
+        for match_identifier in self.matches.iter() {
+            let mut match_parts = match_identifier.split(":");
+
+            if let Some(file_path) = match_parts.next() {
+                if let Some(row_number) = match_parts.next() {
+                    pairs.push((PathBuf::from(file_path), row_number.parse::<u32>().unwrap()));
+                }
+            }
+        }
+        pairs
+    }
+}
+
+#[derive(Deserialize, Clone)]
+struct RepoEval {
+    repo: String,
+    commit: String,
+    assertions: Vec<EvaluationQuery>,
+}
+
+const TMP_REPO_PATH: &str = "eval_repos";
+
+fn parse_eval() -> anyhow::Result<Vec<RepoEval>> {
+    let eval_folder = env::current_dir()?
+        .as_path()
+        .parent()
+        .unwrap()
+        .join("crates/semantic_index/eval");
+
+    let mut repo_evals: Vec<RepoEval> = Vec::new();
+    for entry in fs::read_dir(eval_folder)? {
+        let file_path = entry.unwrap().path();
+        if let Some(extension) = file_path.extension() {
+            if extension == "json" {
+                if let Ok(file) = fs::read_to_string(file_path) {
+                    let repo_eval = serde_json::from_str(file.as_str());
+
+                    match repo_eval {
+                        Ok(repo_eval) => {
+                            repo_evals.push(repo_eval);
+                        }
+                        Err(err) => {
+                            println!("Err: {:?}", err);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    Ok(repo_evals)
+}
+
+fn clone_repo(repo_eval: RepoEval) -> anyhow::Result<(String, PathBuf)> {
+    let repo_name = Path::new(repo_eval.repo.as_str())
+        .file_name()
+        .unwrap()
+        .to_str()
+        .unwrap()
+        .to_owned()
+        .replace(".git", "");
+
+    let clone_path = fs::canonicalize(env::current_dir()?)?
+        .parent()
+        .ok_or(anyhow!("path canonicalization failed"))?
+        .parent()
+        .unwrap()
+        .join(TMP_REPO_PATH);
+
+    // Delete Clone Path if already exists
+    let _ = fs::remove_dir_all(&clone_path);
+    let _ = fs::create_dir(&clone_path);
+
+    let _ = Command::new("git")
+        .args(["clone", repo_eval.repo.as_str()])
+        .current_dir(clone_path.clone())
+        .output()?;
+    // Update clone path to be new directory housing the repo.
+    let clone_path = clone_path.join(repo_name.clone());
+    let _ = Command::new("git")
+        .args(["checkout", repo_eval.commit.as_str()])
+        .current_dir(clone_path.clone())
+        .output()?;
+
+    Ok((repo_name, clone_path))
+}
+
+fn dcg(hits: Vec<usize>) -> f32 {
+    let mut result = 0.0;
+    for (idx, hit) in hits.iter().enumerate() {
+        result += *hit as f32 / (2.0 + idx as f32).log2();
+    }
+
+    result
+}
+
+fn get_hits(
+    eval_query: EvaluationQuery,
+    search_results: Vec<SearchResult>,
+    k: usize,
+    cx: &AsyncAppContext,
+) -> (Vec<usize>, Vec<usize>) {
+    let ideal = vec![1; cmp::min(eval_query.matches.len(), k)];
+
+    let mut hits = Vec::new();
+    for result in search_results {
+        let (path, start_row, end_row) = result.buffer.read_with(cx, |buffer, _cx| {
+            let path = buffer.file().unwrap().path().to_path_buf();
+            let start_row = buffer.offset_to_point(result.range.start.offset).row;
+            let end_row = buffer.offset_to_point(result.range.end.offset).row;
+            (path, start_row, end_row)
+        });
+
+        let match_pairs = eval_query.match_pairs();
+        let mut found = 0;
+        for (match_path, match_row) in match_pairs {
+            if match_path == path {
+                if match_row >= start_row && match_row <= end_row {
+                    found = 1;
+                    break;
+                }
+            }
+        }
+
+        hits.push(found);
+    }
+
+    // For now, we are calculating ideal_hits a bit different, as technically
+    // with overlapping ranges, one match can result in more than result.
+    let mut ideal_hits = hits.clone();
+    ideal_hits.retain(|x| x == &1);
+
+    let ideal = if ideal.len() > ideal_hits.len() {
+        ideal
+    } else {
+        ideal_hits
+    };
+
+    // Fill ideal to 10 length
+    let mut filled_ideal = [0; 10];
+    for (idx, i) in ideal.to_vec().into_iter().enumerate() {
+        filled_ideal[idx] = i;
+    }
+
+    (filled_ideal.to_vec(), hits)
+}
+
+fn evaluate_ndcg(hits: Vec<usize>, ideal: Vec<usize>) -> Vec<f32> {
+    // NDCG or Normalized Discounted Cumulative Gain, is determined by comparing the relevance of
+    // items returned by the search engine relative to the hypothetical ideal.
+    // Relevance is represented as a series of booleans, in which each search result returned
+    // is identified as being inside the test set of matches (1) or not (0).
+
+    // For example, if result 1, 3 and 5 match the 3 relevant results provided
+    // actual dcg is calculated against a vector of [1, 0, 1, 0, 1]
+    // whereas ideal dcg is calculated against a vector of [1, 1, 1, 0, 0]
+    // as this ideal vector assumes the 3 relevant results provided were returned first
+    // normalized dcg is then calculated as actual dcg / ideal dcg.
+
+    // NDCG ranges from 0 to 1, which higher values indicating better performance
+    // Commonly NDCG is expressed as NDCG@k, in which k represents the metric calculated
+    // including only the top k values returned.
+    // The @k metrics can help you identify, at what point does the relevant results start to fall off.
+    // Ie. a NDCG@1 of 0.9 and a NDCG@3 of 0.5 may indicate that the first result returned in usually
+    // very high quality, whereas rank results quickly drop off after the first result.
+
+    let mut ndcg = Vec::new();
+    for idx in 1..(hits.len() + 1) {
+        let hits_at_k = hits[0..idx].to_vec();
+        let ideal_at_k = ideal[0..idx].to_vec();
+
+        let at_k = dcg(hits_at_k.clone()) / dcg(ideal_at_k.clone());
+
+        ndcg.push(at_k);
+    }
+
+    ndcg
+}
+
+fn evaluate_map(hits: Vec<usize>) -> Vec<f32> {
+    let mut map_at_k = Vec::new();
+
+    let non_zero = hits.iter().sum::<usize>() as f32;
+    if non_zero == 0.0 {
+        return vec![0.0; hits.len()];
+    }
+
+    let mut rolling_non_zero = 0.0;
+    let mut rolling_map = 0.0;
+    for (idx, h) in hits.into_iter().enumerate() {
+        rolling_non_zero += h as f32;
+        if h == 1 {
+            rolling_map += rolling_non_zero / (idx + 1) as f32;
+        }
+        map_at_k.push(rolling_map / non_zero);
+    }
+
+    map_at_k
+}
+
+fn evaluate_mrr(hits: Vec<usize>) -> f32 {
+    for (idx, h) in hits.into_iter().enumerate() {
+        if h == 1 {
+            return 1.0 / (idx + 1) as f32;
+        }
+    }
+
+    return 0.0;
+}
+
+fn init_logger() {
+    env_logger::init();
+}
+
+#[derive(Serialize)]
+struct QueryMetrics {
+    query: EvaluationQuery,
+    millis_to_search: Duration,
+    ndcg: Vec<f32>,
+    map: Vec<f32>,
+    mrr: f32,
+    hits: Vec<usize>,
+    precision: Vec<f32>,
+    recall: Vec<f32>,
+}
+
+#[derive(Serialize)]
+struct SummaryMetrics {
+    millis_to_search: f32,
+    ndcg: Vec<f32>,
+    map: Vec<f32>,
+    mrr: f32,
+    precision: Vec<f32>,
+    recall: Vec<f32>,
+}
+
+#[derive(Serialize)]
+struct RepoEvaluationMetrics {
+    millis_to_index: Duration,
+    query_metrics: Vec<QueryMetrics>,
+    repo_metrics: Option<SummaryMetrics>,
+}
+
+impl RepoEvaluationMetrics {
+    fn new(millis_to_index: Duration) -> Self {
+        RepoEvaluationMetrics {
+            millis_to_index,
+            query_metrics: Vec::new(),
+            repo_metrics: None,
+        }
+    }
+
+    fn save(&self, repo_name: String) -> Result<()> {
+        let results_string = serde_json::to_string(&self)?;
+        fs::write(format!("./{}_evaluation.json", repo_name), results_string)
+            .expect("Unable to write file");
+        Ok(())
+    }
+
+    fn summarize(&mut self) {
+        let l = self.query_metrics.len() as f32;
+        let millis_to_search: f32 = self
+            .query_metrics
+            .iter()
+            .map(|metrics| metrics.millis_to_search.as_millis())
+            .sum::<u128>() as f32
+            / l;
+
+        let mut ndcg_sum = vec![0.0; 10];
+        let mut map_sum = vec![0.0; 10];
+        let mut precision_sum = vec![0.0; 10];
+        let mut recall_sum = vec![0.0; 10];
+        let mut mmr_sum = 0.0;
+
+        for query_metric in self.query_metrics.iter() {
+            for (ndcg, query_ndcg) in ndcg_sum.iter_mut().zip(query_metric.ndcg.clone()) {
+                *ndcg += query_ndcg;
+            }
+
+            for (mapp, query_map) in map_sum.iter_mut().zip(query_metric.map.clone()) {
+                *mapp += query_map;
+            }
+
+            for (pre, query_pre) in precision_sum.iter_mut().zip(query_metric.precision.clone()) {
+                *pre += query_pre;
+            }
+
+            for (rec, query_rec) in recall_sum.iter_mut().zip(query_metric.recall.clone()) {
+                *rec += query_rec;
+            }
+
+            mmr_sum += query_metric.mrr;
+        }
+
+        let ndcg = ndcg_sum.iter().map(|val| val / l).collect::<Vec<f32>>();
+        let map = map_sum.iter().map(|val| val / l).collect::<Vec<f32>>();
+        let precision = precision_sum
+            .iter()
+            .map(|val| val / l)
+            .collect::<Vec<f32>>();
+        let recall = recall_sum.iter().map(|val| val / l).collect::<Vec<f32>>();
+        let mrr = mmr_sum / l;
+
+        self.repo_metrics = Some(SummaryMetrics {
+            millis_to_search,
+            ndcg,
+            map,
+            mrr,
+            precision,
+            recall,
+        })
+    }
+}
+
+fn evaluate_precision(hits: Vec<usize>) -> Vec<f32> {
+    let mut rolling_hit: f32 = 0.0;
+    let mut precision = Vec::new();
+    for (idx, hit) in hits.into_iter().enumerate() {
+        rolling_hit += hit as f32;
+        precision.push(rolling_hit / ((idx as f32) + 1.0));
+    }
+
+    precision
+}
+
+fn evaluate_recall(hits: Vec<usize>, ideal: Vec<usize>) -> Vec<f32> {
+    let total_relevant = ideal.iter().sum::<usize>() as f32;
+    let mut recall = Vec::new();
+    let mut rolling_hit: f32 = 0.0;
+    for hit in hits {
+        rolling_hit += hit as f32;
+        recall.push(rolling_hit / total_relevant);
+    }
+
+    recall
+}
+
+async fn evaluate_repo(
+    repo_name: String,
+    index: ModelHandle<SemanticIndex>,
+    project: ModelHandle<Project>,
+    query_matches: Vec<EvaluationQuery>,
+    cx: &mut AsyncAppContext,
+) -> Result<RepoEvaluationMetrics> {
+    // Index Project
+    let index_t0 = Instant::now();
+    index
+        .update(cx, |index, cx| index.index_project(project.clone(), cx))
+        .await?;
+    let mut repo_metrics = RepoEvaluationMetrics::new(index_t0.elapsed());
+
+    for query in query_matches {
+        // Query each match in order
+        let search_t0 = Instant::now();
+        let search_results = index
+            .update(cx, |index, cx| {
+                index.search_project(project.clone(), query.clone().query, 10, vec![], vec![], cx)
+            })
+            .await?;
+        let millis_to_search = search_t0.elapsed();
+
+        // Get Hits/Ideal
+        let k = 10;
+        let (ideal, hits) = self::get_hits(query.clone(), search_results, k, cx);
+
+        // Evaluate ndcg@k, for k = 1, 3, 5, 10
+        let ndcg = evaluate_ndcg(hits.clone(), ideal.clone());
+
+        // Evaluate map@k, for k = 1, 3, 5, 10
+        let map = evaluate_map(hits.clone());
+
+        // Evaluate mrr
+        let mrr = evaluate_mrr(hits.clone());
+
+        // Evaluate precision
+        let precision = evaluate_precision(hits.clone());
+
+        // Evaluate Recall
+        let recall = evaluate_recall(hits.clone(), ideal);
+
+        let query_metrics = QueryMetrics {
+            query,
+            millis_to_search,
+            ndcg,
+            map,
+            mrr,
+            hits,
+            precision,
+            recall,
+        };
+
+        repo_metrics.query_metrics.push(query_metrics);
+    }
+
+    repo_metrics.summarize();
+    let _ = repo_metrics.save(repo_name);
+
+    anyhow::Ok(repo_metrics)
+}
+
+fn main() {
+    // Launch new repo as a new Zed workspace/project
+    let app = gpui::App::new(()).unwrap();
+    let fs = Arc::new(RealFs);
+    let http = http::client();
+    let http_client = http::client();
+    init_logger();
+
+    app.run(move |cx| {
+        cx.set_global(*RELEASE_CHANNEL);
+
+        let client = client::Client::new(http.clone(), cx);
+        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client.clone(), cx));
+
+        // Initialize Settings
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(default_settings().as_ref(), cx)
+            .unwrap();
+        cx.set_global(store);
+
+        // Initialize Languages
+        let login_shell_env_loaded = Task::ready(());
+        let mut languages = LanguageRegistry::new(login_shell_env_loaded);
+        languages.set_executor(cx.background().clone());
+        let languages = Arc::new(languages);
+
+        let node_runtime = RealNodeRuntime::new(http.clone());
+        languages::init(languages.clone(), node_runtime.clone(), cx);
+        language::init(cx);
+
+        project::Project::init(&client, cx);
+        semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
+
+        settings::register::<SemanticIndexSettings>(cx);
+
+        let db_file_path = EMBEDDINGS_DIR
+            .join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
+            .join("embeddings_db");
+
+        let languages = languages.clone();
+        let fs = fs.clone();
+        cx.spawn(|mut cx| async move {
+            let semantic_index = SemanticIndex::new(
+                fs.clone(),
+                db_file_path,
+                Arc::new(OpenAIEmbeddings::new(http_client, cx.background())),
+                languages.clone(),
+                cx.clone(),
+            )
+            .await?;
+
+            if let Ok(repo_evals) = parse_eval() {
+                for repo in repo_evals {
+                    let cloned = clone_repo(repo.clone());
+                    match cloned {
+                        Ok((repo_name, clone_path)) => {
+                            println!(
+                                "Cloned {:?} @ {:?} into {:?}",
+                                repo.repo, repo.commit, &clone_path
+                            );
+
+                            // Create Project
+                            let project = cx.update(|cx| {
+                                Project::local(
+                                    client.clone(),
+                                    user_store.clone(),
+                                    languages.clone(),
+                                    fs.clone(),
+                                    cx,
+                                )
+                            });
+
+                            // Register Worktree
+                            let _ = project
+                                .update(&mut cx, |project, cx| {
+                                    project.find_or_create_local_worktree(clone_path, true, cx)
+                                })
+                                .await;
+
+                            let _ = evaluate_repo(
+                                repo_name,
+                                semantic_index.clone(),
+                                project,
+                                repo.assertions,
+                                &mut cx,
+                            )
+                            .await?;
+                        }
+                        Err(err) => {
+                            println!("Error cloning: {:?}", err);
+                        }
+                    }
+                }
+            }
+
+            anyhow::Ok(())
+        })
+        .detach();
+    });
+}

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

@@ -1,18 +1,19 @@
 use crate::{
-    embedding::Embedding,
     parsing::{Span, SpanDigest},
     SEMANTIC_INDEX_VERSION,
 };
+use ai::embedding::Embedding;
 use anyhow::{anyhow, Context, Result};
 use collections::HashMap;
 use futures::channel::oneshot;
 use gpui::executor;
+use ndarray::{Array1, Array2};
+use ordered_float::OrderedFloat;
 use project::{search::PathMatcher, Fs};
 use rpc::proto::Timestamp;
 use rusqlite::params;
 use rusqlite::types::Value;
 use std::{
-    cmp::Ordering,
     future::Future,
     ops::Range,
     path::{Path, PathBuf},
@@ -22,6 +23,13 @@ use std::{
 };
 use util::TryFutureExt;
 
+pub fn argsort<T: Ord>(data: &[T]) -> Vec<usize> {
+    let mut indices = (0..data.len()).collect::<Vec<_>>();
+    indices.sort_by_key(|&i| &data[i]);
+    indices.reverse();
+    indices
+}
+
 #[derive(Debug)]
 pub struct FileRecord {
     pub id: usize,
@@ -190,6 +198,10 @@ impl VectorDatabase {
                 )",
                 [],
             )?;
+            db.execute(
+                "CREATE INDEX spans_digest ON spans (digest)",
+                [],
+            )?;
 
             log::trace!("vector database initialized with updated schema.");
             Ok(())
@@ -274,6 +286,39 @@ impl VectorDatabase {
         })
     }
 
+    pub fn embeddings_for_digests(
+        &self,
+        digests: Vec<SpanDigest>,
+    ) -> impl Future<Output = Result<HashMap<SpanDigest, Embedding>>> {
+        self.transact(move |db| {
+            let mut query = db.prepare(
+                "
+                SELECT digest, embedding
+                FROM spans
+                WHERE digest IN rarray(?)
+                ",
+            )?;
+            let mut embeddings_by_digest = HashMap::default();
+            let digests = Rc::new(
+                digests
+                    .into_iter()
+                    .map(|p| Value::Blob(p.0.to_vec()))
+                    .collect::<Vec<_>>(),
+            );
+            let rows = query.query_map(params![digests], |row| {
+                Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?))
+            })?;
+
+            for row in rows {
+                if let Ok(row) = row {
+                    embeddings_by_digest.insert(row.0, row.1);
+                }
+            }
+
+            Ok(embeddings_by_digest)
+        })
+    }
+
     pub fn embeddings_for_files(
         &self,
         worktree_id_file_paths: HashMap<i64, Vec<Arc<Path>>>,
@@ -370,24 +415,92 @@ impl VectorDatabase {
         query_embedding: &Embedding,
         limit: usize,
         file_ids: &[i64],
-    ) -> impl Future<Output = Result<Vec<(i64, f32)>>> {
-        let query_embedding = query_embedding.clone();
+    ) -> impl Future<Output = Result<Vec<(i64, OrderedFloat<f32>)>>> {
         let file_ids = file_ids.to_vec();
+        let query = query_embedding.clone().0;
+        let query = Array1::from_vec(query);
         self.transact(move |db| {
-            let mut results = Vec::<(i64, f32)>::with_capacity(limit + 1);
-            Self::for_each_span(db, &file_ids, |id, embedding| {
-                let similarity = embedding.similarity(&query_embedding);
-                let ix = match results.binary_search_by(|(_, s)| {
-                    similarity.partial_cmp(&s).unwrap_or(Ordering::Equal)
-                }) {
-                    Ok(ix) => ix,
-                    Err(ix) => ix,
-                };
-                results.insert(ix, (id, similarity));
-                results.truncate(limit);
-            })?;
+            let mut query_statement = db.prepare(
+                "
+                    SELECT
+                        id, embedding
+                    FROM
+                        spans
+                    WHERE
+                        file_id IN rarray(?)
+                    ",
+            )?;
+
+            let deserialized_rows = query_statement
+                .query_map(params![ids_to_sql(&file_ids)], |row| {
+                    Ok((row.get::<_, usize>(0)?, row.get::<_, Embedding>(1)?))
+                })?
+                .filter_map(|row| row.ok())
+                .collect::<Vec<(usize, Embedding)>>();
+
+            if deserialized_rows.len() == 0 {
+                return Ok(Vec::new());
+            }
+
+            // Get Length of Embeddings Returned
+            let embedding_len = deserialized_rows[0].1 .0.len();
+
+            let batch_n = 1000;
+            let mut batches = Vec::new();
+            let mut batch_ids = Vec::new();
+            let mut batch_embeddings: Vec<f32> = Vec::new();
+            deserialized_rows.iter().for_each(|(id, embedding)| {
+                batch_ids.push(id);
+                batch_embeddings.extend(&embedding.0);
+
+                if batch_ids.len() == batch_n {
+                    let embeddings = std::mem::take(&mut batch_embeddings);
+                    let ids = std::mem::take(&mut batch_ids);
+                    let array =
+                        Array2::from_shape_vec((ids.len(), embedding_len.clone()), embeddings);
+                    match array {
+                        Ok(array) => {
+                            batches.push((ids, array));
+                        }
+                        Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err),
+                    }
+                }
+            });
+
+            if batch_ids.len() > 0 {
+                let array = Array2::from_shape_vec(
+                    (batch_ids.len(), embedding_len),
+                    batch_embeddings.clone(),
+                );
+                match array {
+                    Ok(array) => {
+                        batches.push((batch_ids.clone(), array));
+                    }
+                    Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err),
+                }
+            }
+
+            let mut ids: Vec<usize> = Vec::new();
+            let mut results = Vec::new();
+            for (batch_ids, array) in batches {
+                let scores = array
+                    .dot(&query.t())
+                    .to_vec()
+                    .iter()
+                    .map(|score| OrderedFloat(*score))
+                    .collect::<Vec<OrderedFloat<f32>>>();
+                results.extend(scores);
+                ids.extend(batch_ids);
+            }
 
-            anyhow::Ok(results)
+            let sorted_idx = argsort(&results);
+            let mut sorted_results = Vec::new();
+            let last_idx = limit.min(sorted_idx.len());
+            for idx in &sorted_idx[0..last_idx] {
+                sorted_results.push((ids[*idx] as i64, results[*idx]))
+            }
+
+            Ok(sorted_results)
         })
     }
 
@@ -430,31 +543,6 @@ impl VectorDatabase {
         })
     }
 
-    fn for_each_span(
-        db: &rusqlite::Connection,
-        file_ids: &[i64],
-        mut f: impl FnMut(i64, Embedding),
-    ) -> Result<()> {
-        let mut query_statement = db.prepare(
-            "
-            SELECT
-                id, embedding
-            FROM
-                spans
-            WHERE
-                file_id IN rarray(?)
-            ",
-        )?;
-
-        query_statement
-            .query_map(params![ids_to_sql(&file_ids)], |row| {
-                Ok((row.get(0)?, row.get::<_, Embedding>(1)?))
-            })?
-            .filter_map(|row| row.ok())
-            .for_each(|(id, embedding)| f(id, embedding));
-        Ok(())
-    }
-
     pub fn spans_for_ids(
         &self,
         ids: &[i64],

crates/semantic_index/src/embedding_queue.rs πŸ”—

@@ -1,4 +1,5 @@
-use crate::{embedding::EmbeddingProvider, parsing::Span, JobHandle};
+use crate::{parsing::Span, JobHandle};
+use ai::embedding::EmbeddingProvider;
 use gpui::executor::Background;
 use parking_lot::Mutex;
 use smol::channel;

crates/semantic_index/src/parsing.rs πŸ”—

@@ -1,4 +1,4 @@
-use crate::embedding::{Embedding, EmbeddingProvider};
+use ai::embedding::{Embedding, EmbeddingProvider};
 use anyhow::{anyhow, Result};
 use language::{Grammar, Language};
 use rusqlite::{
@@ -7,6 +7,7 @@ use rusqlite::{
 };
 use sha1::{Digest, Sha1};
 use std::{
+    borrow::Cow,
     cmp::{self, Reverse},
     collections::HashSet,
     ops::Range,
@@ -16,7 +17,7 @@ use std::{
 use tree_sitter::{Parser, QueryCursor};
 
 #[derive(Debug, PartialEq, Eq, Clone, Hash)]
-pub struct SpanDigest([u8; 20]);
+pub struct SpanDigest(pub [u8; 20]);
 
 impl FromSql for SpanDigest {
     fn column_result(value: ValueRef) -> FromSqlResult<Self> {
@@ -60,8 +61,9 @@ const CODE_CONTEXT_TEMPLATE: &str =
 const ENTIRE_FILE_TEMPLATE: &str =
     "The below snippet is from file '<path>'\n\n```<language>\n<item>\n```";
 const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file '<path>'\n\n<item>";
-pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] =
-    &["TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML"];
+pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &[
+    "TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML", "Scheme",
+];
 
 pub struct CodeContextRetriever {
     pub parser: Parser,
@@ -94,12 +96,15 @@ impl CodeContextRetriever {
 
     fn parse_entire_file(
         &self,
-        relative_path: &Path,
+        relative_path: Option<&Path>,
         language_name: Arc<str>,
         content: &str,
     ) -> Result<Vec<Span>> {
         let document_span = ENTIRE_FILE_TEMPLATE
-            .replace("<path>", relative_path.to_string_lossy().as_ref())
+            .replace(
+                "<path>",
+                &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+            )
             .replace("<language>", language_name.as_ref())
             .replace("<item>", &content);
         let digest = SpanDigest::from(document_span.as_str());
@@ -114,9 +119,16 @@ impl CodeContextRetriever {
         }])
     }
 
-    fn parse_markdown_file(&self, relative_path: &Path, content: &str) -> Result<Vec<Span>> {
+    fn parse_markdown_file(
+        &self,
+        relative_path: Option<&Path>,
+        content: &str,
+    ) -> Result<Vec<Span>> {
         let document_span = MARKDOWN_CONTEXT_TEMPLATE
-            .replace("<path>", relative_path.to_string_lossy().as_ref())
+            .replace(
+                "<path>",
+                &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+            )
             .replace("<item>", &content);
         let digest = SpanDigest::from(document_span.as_str());
         let (document_span, token_count) = self.embedding_provider.truncate(&document_span);
@@ -188,7 +200,7 @@ impl CodeContextRetriever {
 
     pub fn parse_file_with_template(
         &mut self,
-        relative_path: &Path,
+        relative_path: Option<&Path>,
         content: &str,
         language: Arc<Language>,
     ) -> Result<Vec<Span>> {
@@ -196,14 +208,17 @@ impl CodeContextRetriever {
 
         if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) {
             return self.parse_entire_file(relative_path, language_name, &content);
-        } else if language_name.as_ref() == "Markdown" {
+        } else if ["Markdown", "Plain Text"].contains(&language_name.as_ref()) {
             return self.parse_markdown_file(relative_path, &content);
         }
 
         let mut spans = self.parse_file(content, language)?;
         for span in &mut spans {
             let document_content = CODE_CONTEXT_TEMPLATE
-                .replace("<path>", relative_path.to_string_lossy().as_ref())
+                .replace(
+                    "<path>",
+                    &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+                )
                 .replace("<language>", language_name.as_ref())
                 .replace("item", &span.content);
 

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

@@ -1,5 +1,4 @@
 mod db;
-mod embedding;
 mod embedding_queue;
 mod parsing;
 pub mod semantic_index_settings;
@@ -8,39 +7,42 @@ pub mod semantic_index_settings;
 mod semantic_index_tests;
 
 use crate::semantic_index_settings::SemanticIndexSettings;
+use ai::embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings};
 use anyhow::{anyhow, Result};
 use collections::{BTreeMap, HashMap, HashSet};
 use db::VectorDatabase;
-use embedding::{Embedding, EmbeddingProvider, OpenAIEmbeddings};
 use embedding_queue::{EmbeddingQueue, FileToEmbed};
 use futures::{future, FutureExt, StreamExt};
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
 use language::{Anchor, Bias, Buffer, Language, LanguageRegistry};
+use lazy_static::lazy_static;
+use ordered_float::OrderedFloat;
 use parking_lot::Mutex;
-use parsing::{CodeContextRetriever, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES};
+use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES};
 use postage::watch;
 use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, Worktree, WorktreeId};
 use smol::channel;
 use std::{
-    cmp::Ordering,
+    cmp::Reverse,
+    env,
     future::Future,
+    mem,
     ops::Range,
     path::{Path, PathBuf},
     sync::{Arc, Weak},
     time::{Duration, Instant, SystemTime},
 };
-use util::{
-    channel::{ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME},
-    http::HttpClient,
-    paths::EMBEDDINGS_DIR,
-    ResultExt,
-};
+use util::{channel::RELEASE_CHANNEL_NAME, http::HttpClient, paths::EMBEDDINGS_DIR, ResultExt};
 use workspace::WorkspaceCreated;
 
-const SEMANTIC_INDEX_VERSION: usize = 10;
+const SEMANTIC_INDEX_VERSION: usize = 11;
 const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60);
 const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250);
 
+lazy_static! {
+    static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
+}
+
 pub fn init(
     fs: Arc<dyn Fs>,
     http_client: Arc<dyn HttpClient>,
@@ -53,11 +55,6 @@ pub fn init(
         .join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
         .join("embeddings_db");
 
-    // This needs to be removed at some point before stable.
-    if *RELEASE_CHANNEL == ReleaseChannel::Stable {
-        return;
-    }
-
     cx.subscribe_global::<WorkspaceCreated, _>({
         move |event, cx| {
             let Some(semantic_index) = SemanticIndex::global(cx) else {
@@ -108,6 +105,7 @@ pub fn init(
 
 #[derive(Copy, Clone, Debug)]
 pub enum SemanticIndexStatus {
+    NotAuthenticated,
     NotIndexed,
     Indexed,
     Indexing {
@@ -262,9 +260,11 @@ pub struct PendingFile {
     job_handle: JobHandle,
 }
 
+#[derive(Clone)]
 pub struct SearchResult {
     pub buffer: ModelHandle<Buffer>,
     pub range: Range<Anchor>,
+    pub similarity: OrderedFloat<f32>,
 }
 
 impl SemanticIndex {
@@ -278,10 +278,13 @@ impl SemanticIndex {
 
     pub fn enabled(cx: &AppContext) -> bool {
         settings::get::<SemanticIndexSettings>(cx).enabled
-            && *RELEASE_CHANNEL != ReleaseChannel::Stable
     }
 
     pub fn status(&self, project: &ModelHandle<Project>) -> SemanticIndexStatus {
+        if !self.embedding_provider.is_authenticated() {
+            return SemanticIndexStatus::NotAuthenticated;
+        }
+
         if let Some(project_state) = self.projects.get(&project.downgrade()) {
             if project_state
                 .worktrees
@@ -301,7 +304,7 @@ impl SemanticIndex {
         }
     }
 
-    async fn new(
+    pub async fn new(
         fs: Arc<dyn Fs>,
         database_path: PathBuf,
         embedding_provider: Arc<dyn EmbeddingProvider>,
@@ -402,7 +405,7 @@ impl SemanticIndex {
 
         if let Some(content) = fs.load(&pending_file.absolute_path).await.log_err() {
             if let Some(mut spans) = retriever
-                .parse_file_with_template(&pending_file.relative_path, &content, language)
+                .parse_file_with_template(Some(&pending_file.relative_path), &content, language)
                 .log_err()
             {
                 log::trace!(
@@ -422,7 +425,7 @@ impl SemanticIndex {
                     path: pending_file.relative_path,
                     mtime: pending_file.modified_time,
                     job_handle: pending_file.job_handle,
-                    spans: spans,
+                    spans,
                 });
             }
         }
@@ -687,38 +690,79 @@ impl SemanticIndex {
     pub fn search_project(
         &mut self,
         project: ModelHandle<Project>,
-        phrase: String,
+        query: String,
         limit: usize,
         includes: Vec<PathMatcher>,
         excludes: Vec<PathMatcher>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Vec<SearchResult>>> {
+        if query.is_empty() {
+            return Task::ready(Ok(Vec::new()));
+        }
+
         let index = self.index_project(project.clone(), cx);
         let embedding_provider = self.embedding_provider.clone();
-        let db_path = self.db.path().clone();
-        let fs = self.fs.clone();
+
         cx.spawn(|this, mut cx| async move {
             index.await?;
-
             let t0 = Instant::now();
-            let database =
-                VectorDatabase::new(fs.clone(), db_path.clone(), cx.background()).await?;
+            let query = embedding_provider
+                .embed_batch(vec![query])
+                .await?
+                .pop()
+                .ok_or_else(|| anyhow!("could not embed query"))?;
+            log::trace!("Embedding Search Query: {:?}ms", t0.elapsed().as_millis());
+
+            let search_start = Instant::now();
+            let modified_buffer_results = this.update(&mut cx, |this, cx| {
+                this.search_modified_buffers(
+                    &project,
+                    query.clone(),
+                    limit,
+                    &includes,
+                    &excludes,
+                    cx,
+                )
+            });
+            let file_results = this.update(&mut cx, |this, cx| {
+                this.search_files(project, query, limit, includes, excludes, cx)
+            });
+            let (modified_buffer_results, file_results) =
+                futures::join!(modified_buffer_results, file_results);
 
-            if phrase.len() == 0 {
-                return Ok(Vec::new());
+            // Weave together the results from modified buffers and files.
+            let mut results = Vec::new();
+            let mut modified_buffers = HashSet::default();
+            for result in modified_buffer_results.log_err().unwrap_or_default() {
+                modified_buffers.insert(result.buffer.clone());
+                results.push(result);
             }
+            for result in file_results.log_err().unwrap_or_default() {
+                if !modified_buffers.contains(&result.buffer) {
+                    results.push(result);
+                }
+            }
+            results.sort_by_key(|result| Reverse(result.similarity));
+            results.truncate(limit);
+            log::trace!("Semantic search took {:?}", search_start.elapsed());
+            Ok(results)
+        })
+    }
 
-            let phrase_embedding = embedding_provider
-                .embed_batch(vec![phrase])
-                .await?
-                .into_iter()
-                .next()
-                .unwrap();
-
-            log::trace!(
-                "Embedding search phrase took: {:?} milliseconds",
-                t0.elapsed().as_millis()
-            );
+    pub fn search_files(
+        &mut self,
+        project: ModelHandle<Project>,
+        query: Embedding,
+        limit: usize,
+        includes: Vec<PathMatcher>,
+        excludes: Vec<PathMatcher>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<SearchResult>>> {
+        let db_path = self.db.path().clone();
+        let fs = self.fs.clone();
+        cx.spawn(|this, mut cx| async move {
+            let database =
+                VectorDatabase::new(fs.clone(), db_path.clone(), cx.background()).await?;
 
             let worktree_db_ids = this.read_with(&cx, |this, _| {
                 let project_state = this
@@ -738,16 +782,22 @@ impl SemanticIndex {
                     .collect::<Vec<i64>>();
                 anyhow::Ok(worktree_db_ids)
             })?;
+
             let file_ids = database
                 .retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes)
                 .await?;
 
             let batch_n = cx.background().num_cpus();
             let ids_len = file_ids.clone().len();
-            let batch_size = if ids_len <= batch_n {
-                ids_len
-            } else {
-                ids_len / batch_n
+            let minimum_batch_size = 50;
+
+            let batch_size = {
+                let size = ids_len / batch_n;
+                if size < minimum_batch_size {
+                    minimum_batch_size
+                } else {
+                    size
+                }
             };
 
             let mut batch_results = Vec::new();
@@ -756,36 +806,41 @@ impl SemanticIndex {
                 let limit = limit.clone();
                 let fs = fs.clone();
                 let db_path = db_path.clone();
-                let phrase_embedding = phrase_embedding.clone();
+                let query = query.clone();
                 if let Some(db) = VectorDatabase::new(fs, db_path.clone(), cx.background())
                     .await
                     .log_err()
                 {
                     batch_results.push(async move {
-                        db.top_k_search(&phrase_embedding, limit, batch.as_slice())
-                            .await
+                        db.top_k_search(&query, limit, batch.as_slice()).await
                     });
                 }
             }
+
             let batch_results = futures::future::join_all(batch_results).await;
 
             let mut results = Vec::new();
             for batch_result in batch_results {
                 if batch_result.is_ok() {
                     for (id, similarity) in batch_result.unwrap() {
-                        let ix = match results.binary_search_by(|(_, s)| {
-                            similarity.partial_cmp(&s).unwrap_or(Ordering::Equal)
-                        }) {
+                        let ix = match results
+                            .binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s))
+                        {
                             Ok(ix) => ix,
                             Err(ix) => ix,
                         };
+
                         results.insert(ix, (id, similarity));
                         results.truncate(limit);
                     }
                 }
             }
 
-            let ids = results.into_iter().map(|(id, _)| id).collect::<Vec<i64>>();
+            let ids = results.iter().map(|(id, _)| *id).collect::<Vec<i64>>();
+            let scores = results
+                .into_iter()
+                .map(|(_, score)| score)
+                .collect::<Vec<_>>();
             let spans = database.spans_for_ids(ids.as_slice()).await?;
 
             let mut tasks = Vec::new();
@@ -809,25 +864,116 @@ impl SemanticIndex {
             })?;
 
             let buffers = futures::future::join_all(tasks).await;
-
-            log::trace!(
-                "Semantic Searching took: {:?} milliseconds in total",
-                t0.elapsed().as_millis()
-            );
-
             Ok(buffers
                 .into_iter()
                 .zip(ranges)
-                .filter_map(|(buffer, range)| {
+                .zip(scores)
+                .filter_map(|((buffer, range), similarity)| {
                     let buffer = buffer.log_err()?;
                     let range = buffer.read_with(&cx, |buffer, _| {
                         let start = buffer.clip_offset(range.start, Bias::Left);
                         let end = buffer.clip_offset(range.end, Bias::Right);
                         buffer.anchor_before(start)..buffer.anchor_after(end)
                     });
-                    Some(SearchResult { buffer, range })
+                    Some(SearchResult {
+                        buffer,
+                        range,
+                        similarity,
+                    })
                 })
-                .collect::<Vec<_>>())
+                .collect())
+        })
+    }
+
+    fn search_modified_buffers(
+        &self,
+        project: &ModelHandle<Project>,
+        query: Embedding,
+        limit: usize,
+        includes: &[PathMatcher],
+        excludes: &[PathMatcher],
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<SearchResult>>> {
+        let modified_buffers = project
+            .read(cx)
+            .opened_buffers(cx)
+            .into_iter()
+            .filter_map(|buffer_handle| {
+                let buffer = buffer_handle.read(cx);
+                let snapshot = buffer.snapshot();
+                let excluded = snapshot.resolve_file_path(cx, false).map_or(false, |path| {
+                    excludes.iter().any(|matcher| matcher.is_match(&path))
+                });
+
+                let included = if includes.len() == 0 {
+                    true
+                } else {
+                    snapshot.resolve_file_path(cx, false).map_or(false, |path| {
+                        includes.iter().any(|matcher| matcher.is_match(&path))
+                    })
+                };
+
+                if buffer.is_dirty() && !excluded && included {
+                    Some((buffer_handle, snapshot))
+                } else {
+                    None
+                }
+            })
+            .collect::<HashMap<_, _>>();
+
+        let embedding_provider = self.embedding_provider.clone();
+        let fs = self.fs.clone();
+        let db_path = self.db.path().clone();
+        let background = cx.background().clone();
+        cx.background().spawn(async move {
+            let db = VectorDatabase::new(fs, db_path.clone(), background).await?;
+            let mut results = Vec::<SearchResult>::new();
+
+            let mut retriever = CodeContextRetriever::new(embedding_provider.clone());
+            for (buffer, snapshot) in modified_buffers {
+                let language = snapshot
+                    .language_at(0)
+                    .cloned()
+                    .unwrap_or_else(|| language::PLAIN_TEXT.clone());
+                let mut spans = retriever
+                    .parse_file_with_template(None, &snapshot.text(), language)
+                    .log_err()
+                    .unwrap_or_default();
+                if Self::embed_spans(&mut spans, embedding_provider.as_ref(), &db)
+                    .await
+                    .log_err()
+                    .is_some()
+                {
+                    for span in spans {
+                        let similarity = span.embedding.unwrap().similarity(&query);
+                        let ix = match results
+                            .binary_search_by_key(&Reverse(similarity), |result| {
+                                Reverse(result.similarity)
+                            }) {
+                            Ok(ix) => ix,
+                            Err(ix) => ix,
+                        };
+
+                        let range = {
+                            let start = snapshot.clip_offset(span.range.start, Bias::Left);
+                            let end = snapshot.clip_offset(span.range.end, Bias::Right);
+                            snapshot.anchor_before(start)..snapshot.anchor_after(end)
+                        };
+
+                        results.insert(
+                            ix,
+                            SearchResult {
+                                buffer: buffer.clone(),
+                                range,
+                                similarity,
+                            },
+                        );
+                        results.truncate(limit);
+                    }
+                }
+            }
+
+            Ok(results)
         })
     }
 
@@ -836,9 +982,11 @@ impl SemanticIndex {
         project: ModelHandle<Project>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
-        if !self.projects.contains_key(&project.downgrade()) {
-            log::trace!("Registering Project for Semantic Index");
+        if !self.embedding_provider.is_authenticated() {
+            return Task::ready(Err(anyhow!("user is not authenticated")));
+        }
 
+        if !self.projects.contains_key(&project.downgrade()) {
             let subscription = cx.subscribe(&project, |this, project, event, cx| match event {
                 project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => {
                     this.project_worktrees_changed(project.clone(), cx);
@@ -1011,6 +1159,63 @@ impl SemanticIndex {
             Ok(())
         })
     }
+
+    async fn embed_spans(
+        spans: &mut [Span],
+        embedding_provider: &dyn EmbeddingProvider,
+        db: &VectorDatabase,
+    ) -> Result<()> {
+        let mut batch = Vec::new();
+        let mut batch_tokens = 0;
+        let mut embeddings = Vec::new();
+
+        let digests = spans
+            .iter()
+            .map(|span| span.digest.clone())
+            .collect::<Vec<_>>();
+        let embeddings_for_digests = db
+            .embeddings_for_digests(digests)
+            .await
+            .log_err()
+            .unwrap_or_default();
+
+        for span in &*spans {
+            if embeddings_for_digests.contains_key(&span.digest) {
+                continue;
+            };
+
+            if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() {
+                let batch_embeddings = embedding_provider
+                    .embed_batch(mem::take(&mut batch))
+                    .await?;
+                embeddings.extend(batch_embeddings);
+                batch_tokens = 0;
+            }
+
+            batch_tokens += span.token_count;
+            batch.push(span.content.clone());
+        }
+
+        if !batch.is_empty() {
+            let batch_embeddings = embedding_provider
+                .embed_batch(mem::take(&mut batch))
+                .await?;
+
+            embeddings.extend(batch_embeddings);
+        }
+
+        let mut embeddings = embeddings.into_iter();
+        for span in spans {
+            let embedding = if let Some(embedding) = embeddings_for_digests.get(&span.digest) {
+                Some(embedding.clone())
+            } else {
+                embeddings.next()
+            };
+            let embedding = embedding.ok_or_else(|| anyhow!("failed to embed spans"))?;
+            span.embedding = Some(embedding);
+        }
+        Ok(())
+    }
 }
 
 impl Entity for SemanticIndex {

crates/semantic_index/src/semantic_index_settings.rs πŸ”—

@@ -6,13 +6,11 @@ use settings::Setting;
 #[derive(Deserialize, Debug)]
 pub struct SemanticIndexSettings {
     pub enabled: bool,
-    pub reindexing_delay_seconds: usize,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct SemanticIndexSettingsContent {
     pub enabled: Option<bool>,
-    pub reindexing_delay_seconds: Option<usize>,
 }
 
 impl Setting for SemanticIndexSettings {

crates/semantic_index/src/semantic_index_tests.rs πŸ”—

@@ -1,10 +1,10 @@
 use crate::{
-    embedding::{DummyEmbeddings, Embedding, EmbeddingProvider},
     embedding_queue::EmbeddingQueue,
     parsing::{subtract_ranges, CodeContextRetriever, Span, SpanDigest},
     semantic_index_settings::SemanticIndexSettings,
     FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT,
 };
+use ai::embedding::{DummyEmbeddings, Embedding, EmbeddingProvider};
 use anyhow::Result;
 use async_trait::async_trait;
 use gpui::{executor::Deterministic, Task, TestAppContext};
@@ -305,6 +305,11 @@ async fn test_code_context_retrieval_rust() {
                 todo!();
             }
         }
+
+        #[derive(Clone)]
+        struct D {
+            name: String
+        }
     "
     .unindent();
 
@@ -361,6 +366,15 @@ async fn test_code_context_retrieval_rust() {
                 .unindent(),
                 text.find("fn function_2").unwrap(),
             ),
+            (
+                "
+                #[derive(Clone)]
+                struct D {
+                    name: String
+                }"
+                .unindent(),
+                text.find("struct D").unwrap(),
+            ),
         ],
     );
 }
@@ -1267,6 +1281,9 @@ impl FakeEmbeddingProvider {
 
 #[async_trait]
 impl EmbeddingProvider for FakeEmbeddingProvider {
+    fn is_authenticated(&self) -> bool {
+        true
+    }
     fn truncate(&self, span: &str) -> (String, usize) {
         (span.to_string(), 1)
     }
@@ -1419,6 +1436,9 @@ fn rust_lang() -> Arc<Language> {
                         name: (_) @name)
                 ] @item
             )
+
+            (attribute_item) @collapse
+            (use_declaration) @collapse
             "#,
         )
         .unwrap(),

crates/sqlez/Cargo.toml πŸ”—

@@ -7,10 +7,10 @@ publish = false
 [dependencies]
 anyhow.workspace = true
 indoc.workspace = true
-libsqlite3-sys = { version = "0.24", features = ["bundled"] }
+libsqlite3-sys = { version = "0.26", features = ["bundled"] }
 smol.workspace = true
 thread_local = "1.1.4"
 lazy_static.workspace = true
 parking_lot.workspace = true
 futures.workspace = true
-uuid = { version = "1.1.2", features = ["v4"] }
+uuid.workspace = true

crates/storybook/Cargo.toml πŸ”—

@@ -8,20 +8,22 @@ publish = false
 name = "storybook"
 path = "src/storybook.rs"
 
-[features]
-test-support = []
-
 [dependencies]
 anyhow.workspace = true
-derive_more.workspace = true
+clap = { version = "4.4", features = ["derive", "string"] }
+chrono = "0.4"
+fs = { path = "../fs" }
+futures.workspace = true
 gpui2 = { path = "../gpui2" }
+itertools = "0.11.0"
 log.workspace = true
-refineable = { path = "../refineable" }
 rust-embed.workspace = true
 serde.workspace = true
 settings = { path = "../settings" }
 simplelog = "0.9"
+strum = { version = "0.25.0", features = ["derive"] }
 theme = { path = "../theme" }
+ui = { path = "../ui" }
 util = { path = "../util" }
 
 [dev-dependencies]

crates/storybook/src/collab_panel.rs πŸ”—

@@ -1,177 +0,0 @@
-use crate::theme::{theme, Theme};
-use gpui2::{
-    elements::{div, div::ScrollState, img, svg},
-    style::{StyleHelpers, Styleable},
-    ArcCow, Element, IntoElement, ParentElement, ViewContext,
-};
-use std::marker::PhantomData;
-
-#[derive(Element)]
-pub struct CollabPanelElement<V: 'static> {
-    view_type: PhantomData<V>,
-    scroll_state: ScrollState,
-}
-
-// When I improve child view rendering, I'd like to have V implement a trait  that
-// provides the scroll state, among other things.
-pub fn collab_panel<V: 'static>(scroll_state: ScrollState) -> CollabPanelElement<V> {
-    CollabPanelElement {
-        view_type: PhantomData,
-        scroll_state,
-    }
-}
-
-impl<V: 'static> CollabPanelElement<V> {
-    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-
-        // Panel
-        div()
-            .w_64()
-            .h_full()
-            .flex()
-            .flex_col()
-            .font("Zed Sans Extended")
-            .text_color(theme.middle.base.default.foreground)
-            .border_color(theme.middle.base.default.border)
-            .border()
-            .fill(theme.middle.base.default.background)
-            .child(
-                div()
-                    .w_full()
-                    .flex()
-                    .flex_col()
-                    .overflow_y_scroll(self.scroll_state.clone())
-                    // List Container
-                    .child(
-                        div()
-                            .fill(theme.lowest.base.default.background)
-                            .pb_1()
-                            .border_color(theme.lowest.base.default.border)
-                            .border_b()
-                            //:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
-                            // .group()
-                            // List Section Header
-                            .child(self.list_section_header("#CRDB", true, theme))
-                            // List Item Large
-                            .child(self.list_item(
-                                "http://github.com/maxbrunsfeld.png?s=50",
-                                "maxbrunsfeld",
-                                theme,
-                            )),
-                    )
-                    .child(
-                        div()
-                            .py_2()
-                            .flex()
-                            .flex_col()
-                            .child(self.list_section_header("CHANNELS", true, theme)),
-                    )
-                    .child(
-                        div()
-                            .py_2()
-                            .flex()
-                            .flex_col()
-                            .child(self.list_section_header("CONTACTS", true, theme))
-                            .children(
-                                std::iter::repeat_with(|| {
-                                    vec![
-                                        self.list_item(
-                                            "http://github.com/as-cii.png?s=50",
-                                            "as-cii",
-                                            theme,
-                                        ),
-                                        self.list_item(
-                                            "http://github.com/nathansobo.png?s=50",
-                                            "nathansobo",
-                                            theme,
-                                        ),
-                                        self.list_item(
-                                            "http://github.com/maxbrunsfeld.png?s=50",
-                                            "maxbrunsfeld",
-                                            theme,
-                                        ),
-                                    ]
-                                })
-                                .take(10)
-                                .flatten(),
-                            ),
-                    ),
-            )
-            .child(
-                div()
-                    .h_7()
-                    .px_2()
-                    .border_t()
-                    .border_color(theme.middle.variant.default.border)
-                    .flex()
-                    .items_center()
-                    .child(
-                        div()
-                            .text_sm()
-                            .text_color(theme.middle.variant.default.foreground)
-                            .child("Find..."),
-                    ),
-            )
-    }
-
-    fn list_section_header(
-        &self,
-        label: impl IntoElement<V>,
-        expanded: bool,
-        theme: &Theme,
-    ) -> impl Element<V> {
-        div()
-            .h_7()
-            .px_2()
-            .flex()
-            .justify_between()
-            .items_center()
-            .child(div().flex().gap_1().text_sm().child(label))
-            .child(
-                div().flex().h_full().gap_1().items_center().child(
-                    svg()
-                        .path(if expanded {
-                            "icons/radix/caret-down.svg"
-                        } else {
-                            "icons/radix/caret-up.svg"
-                        })
-                        .w_3p5()
-                        .h_3p5()
-                        .fill(theme.middle.variant.default.foreground),
-                ),
-            )
-    }
-
-    fn list_item(
-        &self,
-        avatar_uri: impl Into<ArcCow<'static, str>>,
-        label: impl IntoElement<V>,
-        theme: &Theme,
-    ) -> impl Element<V> {
-        div()
-            .h_7()
-            .px_2()
-            .flex()
-            .items_center()
-            .hover()
-            .fill(theme.lowest.variant.hovered.background)
-            .active()
-            .fill(theme.lowest.variant.pressed.background)
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_1()
-                    .text_sm()
-                    .child(
-                        img()
-                            .uri(avatar_uri)
-                            .size_3p5()
-                            .rounded_full()
-                            .fill(theme.middle.positive.default.foreground),
-                    )
-                    .child(label),
-            )
-    }
-}

crates/storybook/src/components.rs πŸ”—

@@ -1,97 +0,0 @@
-use gpui2::{
-    elements::div, interactive::Interactive, platform::MouseButton, style::StyleHelpers, ArcCow,
-    Element, EventContext, IntoElement, ParentElement, ViewContext,
-};
-use std::{marker::PhantomData, rc::Rc};
-
-struct ButtonHandlers<V, D> {
-    click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
-}
-
-impl<V, D> Default for ButtonHandlers<V, D> {
-    fn default() -> Self {
-        Self { click: None }
-    }
-}
-
-#[derive(Element)]
-pub struct Button<V: 'static, D: 'static> {
-    handlers: ButtonHandlers<V, D>,
-    label: Option<ArcCow<'static, str>>,
-    icon: Option<ArcCow<'static, str>>,
-    data: Rc<D>,
-    view_type: PhantomData<V>,
-}
-
-// Impl block for buttons without data.
-// See below for an impl block for any button.
-impl<V: 'static> Button<V, ()> {
-    fn new() -> Self {
-        Self {
-            handlers: ButtonHandlers::default(),
-            label: None,
-            icon: None,
-            data: Rc::new(()),
-            view_type: PhantomData,
-        }
-    }
-
-    pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
-        Button {
-            handlers: ButtonHandlers::default(),
-            label: self.label,
-            icon: self.icon,
-            data: Rc::new(data),
-            view_type: PhantomData,
-        }
-    }
-}
-
-// Impl block for button regardless of its data type.
-impl<V: 'static, D: 'static> Button<V, D> {
-    pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
-        self.label = Some(label.into());
-        self
-    }
-
-    pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
-        self.icon = Some(icon.into());
-        self
-    }
-
-    pub fn on_click(
-        mut self,
-        handler: impl Fn(&mut V, &D, &mut EventContext<V>) + 'static,
-    ) -> Self {
-        self.handlers.click = Some(Rc::new(handler));
-        self
-    }
-}
-
-pub fn button<V>() -> Button<V, ()> {
-    Button::new()
-}
-
-impl<V: 'static, D: 'static> Button<V, D> {
-    fn render(
-        &mut self,
-        view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) -> impl IntoElement<V> + Interactive<V> {
-        // let colors = &cx.theme::<Theme>().colors;
-
-        let button = div()
-            // .fill(colors.error(0.5))
-            .h_4()
-            .children(self.label.clone());
-
-        if let Some(handler) = self.handlers.click.clone() {
-            let data = self.data.clone();
-            button.on_mouse_down(MouseButton::Left, move |view, event, cx| {
-                handler(view, data.as_ref(), cx)
-            })
-        } else {
-            button
-        }
-    }
-}

crates/storybook/src/stories/components.rs πŸ”—

@@ -0,0 +1,22 @@
+pub mod assistant_panel;
+pub mod breadcrumb;
+pub mod buffer;
+pub mod chat_panel;
+pub mod collab_panel;
+pub mod context_menu;
+pub mod facepile;
+pub mod keybinding;
+pub mod language_selector;
+pub mod multi_buffer;
+pub mod palette;
+pub mod panel;
+pub mod project_panel;
+pub mod recent_projects;
+pub mod status_bar;
+pub mod tab;
+pub mod tab_bar;
+pub mod terminal;
+pub mod theme_selector;
+pub mod title_bar;
+pub mod toolbar;
+pub mod traffic_lights;

crates/storybook/src/stories/components/assistant_panel.rs πŸ”—

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::AssistantPanel;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct AssistantPanelStory {}
+
+impl AssistantPanelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, AssistantPanel<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(AssistantPanel::new())
+    }
+}

crates/storybook/src/stories/components/breadcrumb.rs πŸ”—

@@ -0,0 +1,45 @@
+use std::path::PathBuf;
+use std::str::FromStr;
+
+use ui::prelude::*;
+use ui::{Breadcrumb, HighlightedText, Symbol};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct BreadcrumbStory {}
+
+impl BreadcrumbStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        Story::container(cx)
+            .child(Story::title_for::<_, Breadcrumb>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Breadcrumb::new(
+                PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
+                vec![
+                    Symbol(vec![
+                        HighlightedText {
+                            text: "impl ".to_string(),
+                            color: HighlightColor::Keyword.hsla(&theme),
+                        },
+                        HighlightedText {
+                            text: "BreadcrumbStory".to_string(),
+                            color: HighlightColor::Function.hsla(&theme),
+                        },
+                    ]),
+                    Symbol(vec![
+                        HighlightedText {
+                            text: "fn ".to_string(),
+                            color: HighlightColor::Keyword.hsla(&theme),
+                        },
+                        HighlightedText {
+                            text: "render".to_string(),
+                            color: HighlightColor::Function.hsla(&theme),
+                        },
+                    ]),
+                ],
+            ))
+    }
+}

crates/storybook/src/stories/components/buffer.rs πŸ”—

@@ -0,0 +1,36 @@
+use gpui2::geometry::rems;
+use ui::prelude::*;
+use ui::{
+    empty_buffer_example, hello_world_rust_buffer_example,
+    hello_world_rust_buffer_with_status_example, Buffer,
+};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct BufferStory {}
+
+impl BufferStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        Story::container(cx)
+            .child(Story::title_for::<_, Buffer>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
+            .child(Story::label(cx, "Hello World (Rust)"))
+            .child(
+                div()
+                    .w(rems(64.))
+                    .h_96()
+                    .child(hello_world_rust_buffer_example(&theme)),
+            )
+            .child(Story::label(cx, "Hello World (Rust) with Status"))
+            .child(
+                div()
+                    .w(rems(64.))
+                    .h_96()
+                    .child(hello_world_rust_buffer_with_status_example(&theme)),
+            )
+    }
+}

crates/storybook/src/stories/components/chat_panel.rs πŸ”—

@@ -0,0 +1,46 @@
+use chrono::DateTime;
+use ui::prelude::*;
+use ui::{ChatMessage, ChatPanel, Panel};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ChatPanelStory {}
+
+impl ChatPanelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, ChatPanel<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Panel::new(
+                ScrollState::default(),
+                |_, _| vec![ChatPanel::new(ScrollState::default()).into_any()],
+                Box::new(()),
+            ))
+            .child(Story::label(cx, "With Mesages"))
+            .child(Panel::new(
+                ScrollState::default(),
+                |_, _| {
+                    vec![ChatPanel::new(ScrollState::default())
+                        .with_messages(vec![
+                            ChatMessage::new(
+                                "osiewicz".to_string(),
+                                "is this thing on?".to_string(),
+                                DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
+                                    .unwrap()
+                                    .naive_local(),
+                            ),
+                            ChatMessage::new(
+                                "maxdeviant".to_string(),
+                                "Reading you loud and clear!".to_string(),
+                                DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
+                                    .unwrap()
+                                    .naive_local(),
+                            ),
+                        ])
+                        .into_any()]
+                },
+                Box::new(()),
+            ))
+    }
+}

crates/storybook/src/stories/components/collab_panel.rs πŸ”—

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::CollabPanel;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct CollabPanelStory {}
+
+impl CollabPanelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, CollabPanel<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(CollabPanel::new(ScrollState::default()))
+    }
+}

crates/storybook/src/stories/components/context_menu.rs πŸ”—

@@ -0,0 +1,21 @@
+use ui::prelude::*;
+use ui::{ContextMenu, ContextMenuItem, Label};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ContextMenuStory {}
+
+impl ContextMenuStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            //.fill(theme.middle.base.default.background)
+            .child(Story::title_for::<_, ContextMenu>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(ContextMenu::new([
+                ContextMenuItem::header("Section header"),
+                ContextMenuItem::Separator,
+                ContextMenuItem::entry(Label::new("Some entry")),
+            ]))
+    }
+}

crates/storybook/src/stories/components/facepile.rs πŸ”—

@@ -0,0 +1,25 @@
+use ui::prelude::*;
+use ui::{static_players, Facepile};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct FacepileStory {}
+
+impl FacepileStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let players = static_players();
+
+        Story::container(cx)
+            .child(Story::title_for::<_, Facepile>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(
+                div()
+                    .flex()
+                    .gap_3()
+                    .child(Facepile::new(players.clone().into_iter().take(1)))
+                    .child(Facepile::new(players.clone().into_iter().take(2)))
+                    .child(Facepile::new(players.clone().into_iter().take(3))),
+            )
+    }
+}

crates/storybook/src/stories/components/keybinding.rs πŸ”—

@@ -0,0 +1,64 @@
+use itertools::Itertools;
+use strum::IntoEnumIterator;
+use ui::prelude::*;
+use ui::{Keybinding, ModifierKey, ModifierKeys};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct KeybindingStory {}
+
+impl KeybindingStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let all_modifier_permutations = ModifierKey::iter().permutations(2);
+
+        Story::container(cx)
+            .child(Story::title_for::<_, Keybinding>(cx))
+            .child(Story::label(cx, "Single Key"))
+            .child(Keybinding::new("Z".to_string(), ModifierKeys::new()))
+            .child(Story::label(cx, "Single Key with Modifier"))
+            .child(
+                div()
+                    .flex()
+                    .gap_3()
+                    .children(ModifierKey::iter().map(|modifier| {
+                        Keybinding::new("C".to_string(), ModifierKeys::new().add(modifier))
+                    })),
+            )
+            .child(Story::label(cx, "Single Key with Modifier (Permuted)"))
+            .child(
+                div().flex().flex_col().children(
+                    all_modifier_permutations
+                        .chunks(4)
+                        .into_iter()
+                        .map(|chunk| {
+                            div()
+                                .flex()
+                                .gap_4()
+                                .py_3()
+                                .children(chunk.map(|permutation| {
+                                    let mut modifiers = ModifierKeys::new();
+
+                                    for modifier in permutation {
+                                        modifiers = modifiers.add(modifier);
+                                    }
+
+                                    Keybinding::new("X".to_string(), modifiers)
+                                }))
+                        }),
+                ),
+            )
+            .child(Story::label(cx, "Single Key with All Modifiers"))
+            .child(Keybinding::new("Z".to_string(), ModifierKeys::all()))
+            .child(Story::label(cx, "Chord"))
+            .child(Keybinding::new_chord(
+                ("A".to_string(), ModifierKeys::new()),
+                ("Z".to_string(), ModifierKeys::new()),
+            ))
+            .child(Story::label(cx, "Chord with Modifier"))
+            .child(Keybinding::new_chord(
+                ("A".to_string(), ModifierKeys::new().control(true)),
+                ("Z".to_string(), ModifierKeys::new().shift(true)),
+            ))
+    }
+}

crates/storybook/src/stories/components/language_selector.rs πŸ”—

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::LanguageSelector;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct LanguageSelectorStory {}
+
+impl LanguageSelectorStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, LanguageSelector>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(LanguageSelector::new())
+    }
+}

crates/storybook/src/stories/components/multi_buffer.rs πŸ”—

@@ -0,0 +1,24 @@
+use ui::prelude::*;
+use ui::{hello_world_rust_buffer_example, MultiBuffer};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct MultiBufferStory {}
+
+impl MultiBufferStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        Story::container(cx)
+            .child(Story::title_for::<_, MultiBuffer<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(MultiBuffer::new(vec![
+                hello_world_rust_buffer_example(&theme),
+                hello_world_rust_buffer_example(&theme),
+                hello_world_rust_buffer_example(&theme),
+                hello_world_rust_buffer_example(&theme),
+                hello_world_rust_buffer_example(&theme),
+            ]))
+    }
+}

crates/storybook/src/stories/components/palette.rs πŸ”—

@@ -0,0 +1,53 @@
+use ui::prelude::*;
+use ui::{Keybinding, ModifierKeys, Palette, PaletteItem};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct PaletteStory {}
+
+impl PaletteStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Palette<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Palette::new(ScrollState::default()))
+            .child(Story::label(cx, "With Items"))
+            .child(
+                Palette::new(ScrollState::default())
+                    .placeholder("Execute a command...")
+                    .items(vec![
+                        PaletteItem::new("theme selector: toggle").keybinding(
+                            Keybinding::new_chord(
+                                ("k".to_string(), ModifierKeys::new().command(true)),
+                                ("t".to_string(), ModifierKeys::new().command(true)),
+                            ),
+                        ),
+                        PaletteItem::new("assistant: inline assist").keybinding(Keybinding::new(
+                            "enter".to_string(),
+                            ModifierKeys::new().command(true),
+                        )),
+                        PaletteItem::new("assistant: quote selection").keybinding(Keybinding::new(
+                            ">".to_string(),
+                            ModifierKeys::new().command(true),
+                        )),
+                        PaletteItem::new("assistant: toggle focus").keybinding(Keybinding::new(
+                            "?".to_string(),
+                            ModifierKeys::new().command(true),
+                        )),
+                        PaletteItem::new("auto update: check"),
+                        PaletteItem::new("auto update: view release notes"),
+                        PaletteItem::new("branches: open recent").keybinding(Keybinding::new(
+                            "b".to_string(),
+                            ModifierKeys::new().command(true).alt(true),
+                        )),
+                        PaletteItem::new("chat panel: toggle focus"),
+                        PaletteItem::new("cli: install"),
+                        PaletteItem::new("client: sign in"),
+                        PaletteItem::new("client: sign out"),
+                        PaletteItem::new("editor: cancel")
+                            .keybinding(Keybinding::new("escape".to_string(), ModifierKeys::new())),
+                    ]),
+            )
+    }
+}

crates/storybook/src/stories/components/panel.rs πŸ”—

@@ -0,0 +1,25 @@
+use ui::prelude::*;
+use ui::{Label, Panel};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct PanelStory {}
+
+impl PanelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Panel<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Panel::new(
+                ScrollState::default(),
+                |_, _| {
+                    vec![div()
+                        .overflow_y_scroll(ScrollState::default())
+                        .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1))))
+                        .into_any()]
+                },
+                Box::new(()),
+            ))
+    }
+}

crates/storybook/src/stories/components/project_panel.rs πŸ”—

@@ -0,0 +1,20 @@
+use ui::prelude::*;
+use ui::{Panel, ProjectPanel};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ProjectPanelStory {}
+
+impl ProjectPanelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, ProjectPanel<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Panel::new(
+                ScrollState::default(),
+                |_, _| vec![ProjectPanel::new(ScrollState::default()).into_any()],
+                Box::new(()),
+            ))
+    }
+}

crates/storybook/src/stories/components/recent_projects.rs πŸ”—

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::RecentProjects;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct RecentProjectsStory {}
+
+impl RecentProjectsStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, RecentProjects>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(RecentProjects::new())
+    }
+}

crates/storybook/src/stories/components/status_bar.rs πŸ”—

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::StatusBar;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct StatusBarStory {}
+
+impl StatusBarStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, StatusBar<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(StatusBar::new())
+    }
+}

crates/storybook/src/stories/components/tab.rs πŸ”—

@@ -0,0 +1,91 @@
+use strum::IntoEnumIterator;
+use ui::prelude::*;
+use ui::{h_stack, v_stack, Tab};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct TabStory {}
+
+impl TabStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let git_statuses = GitStatus::iter();
+        let fs_statuses = FileSystemStatus::iter();
+
+        Story::container(cx)
+            .child(Story::title_for::<_, Tab>(cx))
+            .child(
+                h_stack().child(
+                    v_stack()
+                        .gap_2()
+                        .child(Story::label(cx, "Default"))
+                        .child(Tab::new()),
+                ),
+            )
+            .child(
+                h_stack().child(
+                    v_stack().gap_2().child(Story::label(cx, "Current")).child(
+                        h_stack()
+                            .gap_4()
+                            .child(Tab::new().title("Current".to_string()).current(true))
+                            .child(Tab::new().title("Not Current".to_string()).current(false)),
+                    ),
+                ),
+            )
+            .child(
+                h_stack().child(
+                    v_stack()
+                        .gap_2()
+                        .child(Story::label(cx, "Titled"))
+                        .child(Tab::new().title("label".to_string())),
+                ),
+            )
+            .child(
+                h_stack().child(
+                    v_stack()
+                        .gap_2()
+                        .child(Story::label(cx, "With Icon"))
+                        .child(
+                            Tab::new()
+                                .title("label".to_string())
+                                .icon(Some(ui::Icon::Envelope)),
+                        ),
+                ),
+            )
+            .child(
+                h_stack().child(
+                    v_stack()
+                        .gap_2()
+                        .child(Story::label(cx, "Close Side"))
+                        .child(
+                            h_stack()
+                                .gap_4()
+                                .child(
+                                    Tab::new()
+                                        .title("Left".to_string())
+                                        .close_side(IconSide::Left),
+                                )
+                                .child(Tab::new().title("Right".to_string())),
+                        ),
+                ),
+            )
+            .child(
+                v_stack()
+                    .gap_2()
+                    .child(Story::label(cx, "Git Status"))
+                    .child(h_stack().gap_4().children(git_statuses.map(|git_status| {
+                        Tab::new()
+                            .title(git_status.to_string())
+                            .git_status(git_status)
+                    }))),
+            )
+            .child(
+                v_stack()
+                    .gap_2()
+                    .child(Story::label(cx, "File System Status"))
+                    .child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
+                        Tab::new().title(fs_status.to_string()).fs_status(fs_status)
+                    }))),
+            )
+    }
+}

crates/storybook/src/stories/components/tab_bar.rs πŸ”—

@@ -0,0 +1,46 @@
+use ui::prelude::*;
+use ui::{Tab, TabBar};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct TabBarStory {}
+
+impl TabBarStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, TabBar<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(TabBar::new(vec![
+                Tab::new()
+                    .title("Cargo.toml".to_string())
+                    .current(false)
+                    .git_status(GitStatus::Modified),
+                Tab::new()
+                    .title("Channels Panel".to_string())
+                    .current(false),
+                Tab::new()
+                    .title("channels_panel.rs".to_string())
+                    .current(true)
+                    .git_status(GitStatus::Modified),
+                Tab::new()
+                    .title("workspace.rs".to_string())
+                    .current(false)
+                    .git_status(GitStatus::Modified),
+                Tab::new()
+                    .title("icon_button.rs".to_string())
+                    .current(false),
+                Tab::new()
+                    .title("storybook.rs".to_string())
+                    .current(false)
+                    .git_status(GitStatus::Created),
+                Tab::new().title("theme.rs".to_string()).current(false),
+                Tab::new()
+                    .title("theme_registry.rs".to_string())
+                    .current(false),
+                Tab::new()
+                    .title("styleable_helpers.rs".to_string())
+                    .current(false),
+            ]))
+    }
+}

crates/storybook/src/stories/components/terminal.rs πŸ”—

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::Terminal;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct TerminalStory {}
+
+impl TerminalStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Terminal>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Terminal::new())
+    }
+}

crates/storybook/src/stories/components/theme_selector.rs πŸ”—

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::ThemeSelector;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ThemeSelectorStory {}
+
+impl ThemeSelectorStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, ThemeSelector>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(ThemeSelector::new())
+    }
+}

crates/storybook/src/stories/components/title_bar.rs πŸ”—

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::TitleBar;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct TitleBarStory {}
+
+impl TitleBarStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, TitleBar<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(TitleBar::new(cx))
+    }
+}

crates/storybook/src/stories/components/toolbar.rs πŸ”—

@@ -0,0 +1,70 @@
+use std::path::PathBuf;
+use std::str::FromStr;
+use std::sync::Arc;
+
+use ui::prelude::*;
+use ui::{theme, Breadcrumb, HighlightColor, HighlightedText, Icon, IconButton, Symbol, Toolbar};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ToolbarStory {}
+
+impl ToolbarStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        struct LeftItemsPayload {
+            pub theme: Arc<Theme>,
+        }
+
+        Story::container(cx)
+            .child(Story::title_for::<_, Toolbar<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Toolbar::new(
+                |_, payload| {
+                    let payload = payload.downcast_ref::<LeftItemsPayload>().unwrap();
+
+                    let theme = payload.theme.clone();
+
+                    vec![Breadcrumb::new(
+                        PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
+                        vec![
+                            Symbol(vec![
+                                HighlightedText {
+                                    text: "impl ".to_string(),
+                                    color: HighlightColor::Keyword.hsla(&theme),
+                                },
+                                HighlightedText {
+                                    text: "ToolbarStory".to_string(),
+                                    color: HighlightColor::Function.hsla(&theme),
+                                },
+                            ]),
+                            Symbol(vec![
+                                HighlightedText {
+                                    text: "fn ".to_string(),
+                                    color: HighlightColor::Keyword.hsla(&theme),
+                                },
+                                HighlightedText {
+                                    text: "render".to_string(),
+                                    color: HighlightColor::Function.hsla(&theme),
+                                },
+                            ]),
+                        ],
+                    )
+                    .into_any()]
+                },
+                Box::new(LeftItemsPayload {
+                    theme: theme.clone(),
+                }),
+                |_, _| {
+                    vec![
+                        IconButton::new(Icon::InlayHint).into_any(),
+                        IconButton::new(Icon::MagnifyingGlass).into_any(),
+                        IconButton::new(Icon::MagicWand).into_any(),
+                    ]
+                },
+                Box::new(()),
+            ))
+    }
+}

crates/storybook/src/stories/components/traffic_lights.rs πŸ”—

@@ -0,0 +1,18 @@
+use ui::prelude::*;
+use ui::TrafficLights;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct TrafficLightsStory {}
+
+impl TrafficLightsStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, TrafficLights>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(TrafficLights::new())
+            .child(Story::label(cx, "Unfocused"))
+            .child(TrafficLights::new().window_has_focus(false))
+    }
+}

crates/storybook/src/stories/elements/avatar.rs πŸ”—

@@ -0,0 +1,23 @@
+use ui::prelude::*;
+use ui::Avatar;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct AvatarStory {}
+
+impl AvatarStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Avatar>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Avatar::new(
+                "https://avatars.githubusercontent.com/u/1714999?v=4",
+            ))
+            .child(Story::label(cx, "Rounded rectangle"))
+            .child(
+                Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
+                    .shape(Shape::RoundedRectangle),
+            )
+    }
+}

crates/storybook/src/stories/elements/button.rs πŸ”—

@@ -0,0 +1,192 @@
+use gpui2::elements::div;
+use gpui2::geometry::rems;
+use gpui2::{Element, IntoElement, ViewContext};
+use strum::IntoEnumIterator;
+use ui::prelude::*;
+use ui::{h_stack, v_stack, Button, Icon, IconPosition, Label};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ButtonStory {}
+
+impl ButtonStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let states = InteractionState::iter();
+
+        Story::container(cx)
+            .child(Story::title_for::<_, Button<V>>(cx))
+            .child(
+                div()
+                    .flex()
+                    .gap_8()
+                    .child(
+                        div()
+                            .child(Story::label(cx, "Ghost (Default)"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Ghost)
+                                            .state(state),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Ghost – Left Icon"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Ghost)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Left)
+                                            .state(state),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Ghost – Right Icon"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Ghost)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Right)
+                                            .state(state),
+                                    )
+                            }))),
+                    )
+                    .child(
+                        div()
+                            .child(Story::label(cx, "Filled"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .state(state),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Filled – Left Button"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Left)
+                                            .state(state),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Filled – Right Button"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Right)
+                                            .state(state),
+                                    )
+                            }))),
+                    )
+                    .child(
+                        div()
+                            .child(Story::label(cx, "Fixed With"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .state(state)
+                                            .width(Some(rems(6.).into())),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Fixed With – Left Icon"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .state(state)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Left)
+                                            .width(Some(rems(6.).into())),
+                                    )
+                            })))
+                            .child(Story::label(cx, "Fixed With – Right Icon"))
+                            .child(h_stack().gap_2().children(states.clone().map(|state| {
+                                v_stack()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(state.to_string())
+                                            .color(ui::LabelColor::Muted)
+                                            .size(ui::LabelSize::Small),
+                                    )
+                                    .child(
+                                        Button::new("Label")
+                                            .variant(ButtonVariant::Filled)
+                                            .state(state)
+                                            .icon(Icon::Plus)
+                                            .icon_position(IconPosition::Right)
+                                            .width(Some(rems(6.).into())),
+                                    )
+                            }))),
+                    ),
+            )
+            .child(Story::label(cx, "Button with `on_click`"))
+            .child(
+                Button::new("Label")
+                    .variant(ButtonVariant::Ghost)
+                    // NOTE: There currently appears to be a bug in GPUI2 where only the last event handler will fire.
+                    // So adding additional buttons with `on_click`s after this one will cause this `on_click` to not fire.
+                    .on_click(|_view, _cx| println!("Button clicked.")),
+            )
+    }
+}

crates/storybook/src/stories/elements/icon.rs πŸ”—

@@ -0,0 +1,19 @@
+use strum::IntoEnumIterator;
+use ui::prelude::*;
+use ui::{Icon, IconElement};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct IconStory {}
+
+impl IconStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let icons = Icon::iter();
+
+        Story::container(cx)
+            .child(Story::title_for::<_, IconElement>(cx))
+            .child(Story::label(cx, "All Icons"))
+            .child(div().flex().gap_3().children(icons.map(IconElement::new)))
+    }
+}

crates/storybook/src/stories/elements/input.rs πŸ”—

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::Input;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct InputStory {}
+
+impl InputStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Input>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(div().flex().child(Input::new("Search")))
+    }
+}

crates/storybook/src/stories/elements/label.rs πŸ”—

@@ -0,0 +1,18 @@
+use ui::prelude::*;
+use ui::Label;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct LabelStory {}
+
+impl LabelStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Label>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Label::new("Hello, world!"))
+            .child(Story::label(cx, "Highlighted"))
+            .child(Label::new("Hello, world!").with_highlights(vec![0, 1, 2, 7, 8, 12]))
+    }
+}

crates/storybook/src/stories/kitchen_sink.rs πŸ”—

@@ -0,0 +1,26 @@
+use strum::IntoEnumIterator;
+use ui::prelude::*;
+
+use crate::story::Story;
+use crate::story_selector::{ComponentStory, ElementStory};
+
+#[derive(Element, Default)]
+pub struct KitchenSinkStory {}
+
+impl KitchenSinkStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let element_stories = ElementStory::iter().map(|selector| selector.story());
+        let component_stories = ComponentStory::iter().map(|selector| selector.story());
+
+        Story::container(cx)
+            .overflow_y_scroll(ScrollState::default())
+            .child(Story::title(cx, "Kitchen Sink"))
+            .child(Story::label(cx, "Elements"))
+            .child(div().flex().flex_col().children_any(element_stories))
+            .child(Story::label(cx, "Components"))
+            .child(div().flex().flex_col().children_any(component_stories))
+            // Add a bit of space at the bottom of the kitchen sink so elements
+            // don't end up squished right up against the bottom of the screen.
+            .child(div().p_4())
+    }
+}

crates/storybook/src/story.rs πŸ”—

@@ -0,0 +1,44 @@
+use gpui2::elements::div::Div;
+use ui::prelude::*;
+use ui::theme;
+
+pub struct Story {}
+
+impl Story {
+    pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
+        let theme = theme(cx);
+
+        div()
+            .size_full()
+            .flex()
+            .flex_col()
+            .pt_2()
+            .px_4()
+            .font("Zed Mono Extended")
+            .fill(theme.lowest.base.default.background)
+    }
+
+    pub fn title<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Element<V> {
+        let theme = theme(cx);
+
+        div()
+            .text_xl()
+            .text_color(theme.lowest.base.default.foreground)
+            .child(title.to_owned())
+    }
+
+    pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Element<V> {
+        Self::title(cx, std::any::type_name::<T>())
+    }
+
+    pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Element<V> {
+        let theme = theme(cx);
+
+        div()
+            .mt_4()
+            .mb_2()
+            .text_xs()
+            .text_color(theme.lowest.base.default.foreground)
+            .child(label.to_owned())
+    }
+}

crates/storybook/src/story_selector.rs πŸ”—

@@ -0,0 +1,178 @@
+use std::str::FromStr;
+use std::sync::OnceLock;
+
+use anyhow::{anyhow, Context};
+use clap::builder::PossibleValue;
+use clap::ValueEnum;
+use gpui2::{AnyElement, Element};
+use strum::{EnumIter, EnumString, IntoEnumIterator};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
+#[strum(serialize_all = "snake_case")]
+pub enum ElementStory {
+    Avatar,
+    Button,
+    Icon,
+    Input,
+    Label,
+}
+
+impl ElementStory {
+    pub fn story<V: 'static>(&self) -> AnyElement<V> {
+        use crate::stories::elements;
+
+        match self {
+            Self::Avatar => elements::avatar::AvatarStory::default().into_any(),
+            Self::Button => elements::button::ButtonStory::default().into_any(),
+            Self::Icon => elements::icon::IconStory::default().into_any(),
+            Self::Input => elements::input::InputStory::default().into_any(),
+            Self::Label => elements::label::LabelStory::default().into_any(),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
+#[strum(serialize_all = "snake_case")]
+pub enum ComponentStory {
+    AssistantPanel,
+    Breadcrumb,
+    Buffer,
+    ContextMenu,
+    ChatPanel,
+    CollabPanel,
+    Facepile,
+    Keybinding,
+    LanguageSelector,
+    MultiBuffer,
+    Palette,
+    Panel,
+    ProjectPanel,
+    RecentProjects,
+    StatusBar,
+    Tab,
+    TabBar,
+    Terminal,
+    ThemeSelector,
+    TitleBar,
+    Toolbar,
+    TrafficLights,
+}
+
+impl ComponentStory {
+    pub fn story<V: 'static>(&self) -> AnyElement<V> {
+        use crate::stories::components;
+
+        match self {
+            Self::AssistantPanel => {
+                components::assistant_panel::AssistantPanelStory::default().into_any()
+            }
+            Self::Breadcrumb => components::breadcrumb::BreadcrumbStory::default().into_any(),
+            Self::Buffer => components::buffer::BufferStory::default().into_any(),
+            Self::ContextMenu => components::context_menu::ContextMenuStory::default().into_any(),
+            Self::ChatPanel => components::chat_panel::ChatPanelStory::default().into_any(),
+            Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(),
+            Self::Facepile => components::facepile::FacepileStory::default().into_any(),
+            Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(),
+            Self::LanguageSelector => {
+                components::language_selector::LanguageSelectorStory::default().into_any()
+            }
+            Self::MultiBuffer => components::multi_buffer::MultiBufferStory::default().into_any(),
+            Self::Palette => components::palette::PaletteStory::default().into_any(),
+            Self::Panel => components::panel::PanelStory::default().into_any(),
+            Self::ProjectPanel => {
+                components::project_panel::ProjectPanelStory::default().into_any()
+            }
+            Self::RecentProjects => {
+                components::recent_projects::RecentProjectsStory::default().into_any()
+            }
+            Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(),
+            Self::Tab => components::tab::TabStory::default().into_any(),
+            Self::TabBar => components::tab_bar::TabBarStory::default().into_any(),
+            Self::Terminal => components::terminal::TerminalStory::default().into_any(),
+            Self::ThemeSelector => {
+                components::theme_selector::ThemeSelectorStory::default().into_any()
+            }
+            Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(),
+            Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(),
+            Self::TrafficLights => {
+                components::traffic_lights::TrafficLightsStory::default().into_any()
+            }
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum StorySelector {
+    Element(ElementStory),
+    Component(ComponentStory),
+    KitchenSink,
+}
+
+impl FromStr for StorySelector {
+    type Err = anyhow::Error;
+
+    fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
+        let story = raw_story_name.to_ascii_lowercase();
+
+        if story == "kitchen_sink" {
+            return Ok(Self::KitchenSink);
+        }
+
+        if let Some((_, story)) = story.split_once("elements/") {
+            let element_story = ElementStory::from_str(story)
+                .with_context(|| format!("story not found for element '{story}'"))?;
+
+            return Ok(Self::Element(element_story));
+        }
+
+        if let Some((_, story)) = story.split_once("components/") {
+            let component_story = ComponentStory::from_str(story)
+                .with_context(|| format!("story not found for component '{story}'"))?;
+
+            return Ok(Self::Component(component_story));
+        }
+
+        Err(anyhow!("story not found for '{raw_story_name}'"))
+    }
+}
+
+impl StorySelector {
+    pub fn story<V: 'static>(&self) -> AnyElement<V> {
+        match self {
+            Self::Element(element_story) => element_story.story(),
+            Self::Component(component_story) => component_story.story(),
+            Self::KitchenSink => {
+                crate::stories::kitchen_sink::KitchenSinkStory::default().into_any()
+            }
+        }
+    }
+}
+
+/// The list of all stories available in the storybook.
+static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new();
+
+impl ValueEnum for StorySelector {
+    fn value_variants<'a>() -> &'a [Self] {
+        let stories = ALL_STORY_SELECTORS.get_or_init(|| {
+            let element_stories = ElementStory::iter().map(StorySelector::Element);
+            let component_stories = ComponentStory::iter().map(StorySelector::Component);
+
+            element_stories
+                .chain(component_stories)
+                .chain(std::iter::once(StorySelector::KitchenSink))
+                .collect::<Vec<_>>()
+        });
+
+        stories
+    }
+
+    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
+        let value = match self {
+            Self::Element(story) => format!("elements/{story}"),
+            Self::Component(story) => format!("components/{story}"),
+            Self::KitchenSink => "kitchen_sink".to_string(),
+        };
+
+        Some(PossibleValue::new(value))
+    }
+}

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

@@ -1,29 +1,82 @@
 #![allow(dead_code, unused_variables)]
 
-use crate::theme::Theme;
+mod stories;
+mod story;
+mod story_selector;
+
+use std::{process::Command, sync::Arc};
+
 use ::theme as legacy_theme;
-use element_ext::ElementExt;
-use gpui2::{serde_json, vec2f, view, Element, RectF, ViewContext, WindowBounds};
-use legacy_theme::ThemeSettings;
+use clap::Parser;
+use gpui2::{
+    serde_json, vec2f, view, Element, IntoElement, ParentElement, RectF, ViewContext, WindowBounds,
+};
+use legacy_theme::{ThemeRegistry, ThemeSettings};
 use log::LevelFilter;
 use settings::{default_settings, SettingsStore};
 use simplelog::SimpleLogger;
+use ui::prelude::*;
+use ui::{ElementExt, Theme, WorkspaceElement};
 
-mod collab_panel;
-mod components;
-mod element_ext;
-mod theme;
-mod workspace;
+use crate::story_selector::StorySelector;
 
 gpui2::actions! {
     storybook,
     [ToggleInspector]
 }
 
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+    #[arg(value_enum)]
+    story: Option<StorySelector>,
+
+    /// The name of the theme to use in the storybook.
+    ///
+    /// If not provided, the default theme will be used.
+    #[arg(long)]
+    theme: Option<String>,
+}
+
+async fn watch_zed_changes(fs: Arc<dyn fs::Fs>) -> Option<()> {
+    if std::env::var("ZED_HOT_RELOAD").is_err() {
+        return None;
+    }
+    use futures::StreamExt;
+    let mut events = fs
+        .watch(".".as_ref(), std::time::Duration::from_millis(100))
+        .await;
+    let mut current_child: Option<std::process::Child> = None;
+    while let Some(events) = events.next().await {
+        if !events.iter().any(|event| {
+            event
+                .path
+                .to_str()
+                .map(|path| path.contains("/crates/"))
+                .unwrap_or_default()
+        }) {
+            continue;
+        }
+        let child = current_child.take().map(|mut child| child.kill());
+        log::info!("Storybook changed, rebuilding...");
+        current_child = Some(
+            Command::new("cargo")
+                .args(["run", "-p", "storybook"])
+                .spawn()
+                .ok()?,
+        );
+    }
+    Some(())
+}
+
 fn main() {
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 
-    gpui2::App::new(Assets).unwrap().run(|cx| {
+    let args = Args::parse();
+
+    let fs = Arc::new(fs::RealFs);
+
+    gpui2::App::new(Assets).unwrap().run(move |cx| {
         let mut store = SettingsStore::default();
         store
             .set_default_settings(default_settings().as_ref(), cx)
@@ -32,39 +85,76 @@ fn main() {
         legacy_theme::init(Assets, cx);
         // load_embedded_fonts(cx.platform().as_ref());
 
+        let theme_registry = cx.global::<Arc<ThemeRegistry>>();
+
+        let theme_override = args
+            .theme
+            .and_then(|theme| {
+                theme_registry
+                    .list_names(true)
+                    .find(|known_theme| theme == *known_theme)
+            })
+            .and_then(|theme_name| theme_registry.get(&theme_name).ok());
+
+        cx.spawn(|_| async move {
+            watch_zed_changes(fs).await;
+        })
+        .detach();
         cx.add_window(
             gpui2::WindowOptions {
-                bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1400., 900.))),
+                bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1700., 980.))),
                 center: true,
                 ..Default::default()
             },
-            |cx| {
-                view(|cx| {
-                    cx.enable_inspector();
-                    storybook(&mut ViewContext::new(cx))
-                })
+            |cx| match args.story {
+                Some(selector) => view(move |cx| {
+                    render_story(
+                        &mut ViewContext::new(cx),
+                        theme_override.clone(),
+                        div().flex().flex_col().h_full().child_any(selector.story()),
+                    )
+                }),
+                None => view(move |cx| {
+                    render_story(
+                        &mut ViewContext::new(cx),
+                        theme_override.clone(),
+                        WorkspaceElement::default(),
+                    )
+                }),
             },
         );
         cx.platform().activate(true);
     });
 }
 
-fn storybook<V: 'static>(cx: &mut ViewContext<V>) -> impl Element<V> {
-    workspace().themed(current_theme(cx))
+fn render_story<V: 'static, S: IntoElement<V>>(
+    cx: &mut ViewContext<V>,
+    theme_override: Option<Arc<legacy_theme::Theme>>,
+    story: S,
+) -> impl Element<V> {
+    let theme = current_theme(cx, theme_override);
+
+    story.into_element().themed(theme)
+}
+
+fn current_theme<V: 'static>(
+    cx: &mut ViewContext<V>,
+    theme_override: Option<Arc<legacy_theme::Theme>>,
+) -> Theme {
+    let legacy_theme =
+        theme_override.unwrap_or_else(|| settings::get::<ThemeSettings>(cx).theme.clone());
+
+    let new_theme: Theme = serde_json::from_value(legacy_theme.base_theme.clone()).unwrap();
+
+    add_base_theme_to_legacy_theme(&legacy_theme, new_theme)
 }
 
 // Nathan: During the transition to gpui2, we will include the base theme on the legacy Theme struct.
-fn current_theme<V: 'static>(cx: &mut ViewContext<V>) -> Theme {
-    settings::get::<ThemeSettings>(cx)
-        .theme
+fn add_base_theme_to_legacy_theme(legacy_theme: &legacy_theme::Theme, new_theme: Theme) -> Theme {
+    legacy_theme
         .deserialized_base_theme
         .lock()
-        .get_or_insert_with(|| {
-            let theme: Theme =
-                serde_json::from_value(settings::get::<ThemeSettings>(cx).theme.base_theme.clone())
-                    .unwrap();
-            Box::new(theme)
-        })
+        .get_or_insert_with(|| Box::new(new_theme))
         .downcast_ref::<Theme>()
         .unwrap()
         .clone()
@@ -73,7 +163,6 @@ fn current_theme<V: 'static>(cx: &mut ViewContext<V>) -> Theme {
 use anyhow::{anyhow, Result};
 use gpui2::AssetSource;
 use rust_embed::RustEmbed;
-use workspace::workspace;
 
 #[derive(RustEmbed)]
 #[folder = "../../assets"]

crates/storybook/src/workspace.rs πŸ”—

@@ -1,444 +0,0 @@
-use crate::theme::theme;
-use gpui2::{
-    black,
-    elements::{div, div::ScrollState, img, svg},
-    style::{StyleHelpers, Styleable},
-    white, Element, IntoElement, ParentElement, ViewContext,
-};
-
-#[derive(Element, Default)]
-struct WorkspaceElement {
-    left_scroll_state: ScrollState,
-    right_scroll_state: ScrollState,
-}
-
-pub fn workspace<V: 'static>() -> impl Element<V> {
-    WorkspaceElement::default()
-}
-
-impl WorkspaceElement {
-    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-
-        return div()
-            .size_full()
-            .fill(white())
-            .font("Helvetica")
-            .text_base()
-            .text_color(black())
-            .child("The quick brown fox ran over the lazy dog.");
-
-        // div()
-        //     .size_full()
-        //     .flex()
-        //     .flex_col()
-        //     .font("Zed Sans Extended")
-        //     .gap_0()
-        //     .justify_start()
-        //     .items_start()
-        //     .text_color(theme.lowest.base.default.foreground)
-        //     .fill(theme.middle.base.default.background)
-        //     .child(titlebar())
-        //     .child(
-        //         div()
-        //             .flex_1()
-        //             .w_full()
-        //             .flex()
-        //             .flex_row()
-        //             .overflow_hidden()
-        //             .child(collab_panel(self.left_scroll_state.clone()))
-        //             .child(div().h_full().flex_1())
-        //             .child(collab_panel(self.right_scroll_state.clone())),
-        //     )
-        //     .child(statusbar())
-    }
-}
-
-#[derive(Element)]
-struct TitleBar;
-
-pub fn titlebar<V: 'static>() -> impl Element<V> {
-    TitleBar
-}
-
-impl TitleBar {
-    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .justify_between()
-            .w_full()
-            .h_8()
-            .fill(theme.lowest.base.default.background)
-            .child(self.left_group(cx))
-            .child(self.right_group(cx))
-    }
-
-    fn left_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .h_full()
-            .gap_4()
-            .px_2()
-            // === Traffic Lights === //
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_2()
-                    .child(
-                        div()
-                            .w_3()
-                            .h_3()
-                            .rounded_full()
-                            .fill(theme.lowest.positive.default.foreground),
-                    )
-                    .child(
-                        div()
-                            .w_3()
-                            .h_3()
-                            .rounded_full()
-                            .fill(theme.lowest.warning.default.foreground),
-                    )
-                    .child(
-                        div()
-                            .w_3()
-                            .h_3()
-                            .rounded_full()
-                            .fill(theme.lowest.negative.default.foreground),
-                    ),
-            )
-            // === Project Info === //
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_1()
-                    .child(
-                        div()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .px_2()
-                            .rounded_md()
-                            .hover()
-                            .fill(theme.lowest.base.hovered.background)
-                            .active()
-                            .fill(theme.lowest.base.pressed.background)
-                            .child(div().text_sm().child("project")),
-                    )
-                    .child(
-                        div()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .px_2()
-                            .rounded_md()
-                            .text_color(theme.lowest.variant.default.foreground)
-                            .hover()
-                            .fill(theme.lowest.base.hovered.background)
-                            .active()
-                            .fill(theme.lowest.base.pressed.background)
-                            .child(div().text_sm().child("branch")),
-                    ),
-            )
-    }
-
-    fn right_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .h_full()
-            .gap_3()
-            .px_2()
-            // === Actions === //
-            .child(
-                div().child(
-                    div().flex().items_center().gap_1().child(
-                        div().size_4().flex().items_center().justify_center().child(
-                            svg()
-                                .path("icons/exit.svg")
-                                .size_4()
-                                .fill(theme.lowest.base.default.foreground),
-                        ),
-                    ),
-                ),
-            )
-            .child(div().w_px().h_3().fill(theme.lowest.base.default.border))
-            // === Comms === //
-            .child(
-                div().child(
-                    div()
-                        .flex()
-                        .items_center()
-                        .gap_px()
-                        .child(
-                            div()
-                                .px_2()
-                                .py_1()
-                                .rounded_md()
-                                .h_full()
-                                .flex()
-                                .items_center()
-                                .justify_center()
-                                .hover()
-                                .fill(theme.lowest.base.hovered.background)
-                                .active()
-                                .fill(theme.lowest.base.pressed.background)
-                                .child(
-                                    svg()
-                                        .path("icons/microphone.svg")
-                                        .size_3p5()
-                                        .fill(theme.lowest.base.default.foreground),
-                                ),
-                        )
-                        .child(
-                            div()
-                                .px_2()
-                                .py_1()
-                                .rounded_md()
-                                .h_full()
-                                .flex()
-                                .items_center()
-                                .justify_center()
-                                .hover()
-                                .fill(theme.lowest.base.hovered.background)
-                                .active()
-                                .fill(theme.lowest.base.pressed.background)
-                                .child(
-                                    svg()
-                                        .path("icons/radix/speaker-loud.svg")
-                                        .size_3p5()
-                                        .fill(theme.lowest.base.default.foreground),
-                                ),
-                        )
-                        .child(
-                            div()
-                                .px_2()
-                                .py_1()
-                                .rounded_md()
-                                .h_full()
-                                .flex()
-                                .items_center()
-                                .justify_center()
-                                .hover()
-                                .fill(theme.lowest.base.hovered.background)
-                                .active()
-                                .fill(theme.lowest.base.pressed.background)
-                                .child(
-                                    svg()
-                                        .path("icons/radix/desktop.svg")
-                                        .size_3p5()
-                                        .fill(theme.lowest.base.default.foreground),
-                                ),
-                        ),
-                ),
-            )
-            .child(div().w_px().h_3().fill(theme.lowest.base.default.border))
-            // User Group
-            .child(
-                div().child(
-                    div()
-                        .px_1()
-                        .py_1()
-                        .flex()
-                        .items_center()
-                        .justify_center()
-                        .rounded_md()
-                        .gap_0p5()
-                        .hover()
-                        .fill(theme.lowest.base.hovered.background)
-                        .active()
-                        .fill(theme.lowest.base.pressed.background)
-                        .child(
-                            img()
-                                .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
-                                .size_4()
-                                .rounded_md()
-                                .fill(theme.middle.on.default.foreground),
-                        )
-                        .child(
-                            svg()
-                                .path("icons/caret_down_8.svg")
-                                .w_2()
-                                .h_2()
-                                .fill(theme.lowest.variant.default.foreground),
-                        ),
-                ),
-            )
-    }
-}
-
-// ================================================================================ //
-
-#[derive(Element)]
-struct StatusBar;
-
-pub fn statusbar<V: 'static>() -> impl Element<V> {
-    StatusBar
-}
-
-impl StatusBar {
-    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .justify_between()
-            .w_full()
-            .h_8()
-            .fill(theme.lowest.base.default.background)
-            .child(self.left_group(cx))
-            .child(self.right_group(cx))
-    }
-
-    fn left_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .h_full()
-            .gap_4()
-            .px_2()
-            // === Tools === //
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_1()
-                    .child(
-                        div()
-                            .w_6()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .child(
-                                svg()
-                                    .path("icons/project.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.base.default.foreground),
-                            ),
-                    )
-                    .child(
-                        div()
-                            .w_6()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .child(
-                                svg()
-                                    .path("icons/conversations.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.base.default.foreground),
-                            ),
-                    )
-                    .child(
-                        div()
-                            .w_6()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .child(
-                                svg()
-                                    .path("icons/file_icons/notebook.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.accent.default.foreground),
-                            ),
-                    ),
-            )
-            // === Diagnostics === //
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_2()
-                    .child(
-                        div()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .gap_0p5()
-                            .px_1()
-                            .text_color(theme.lowest.variant.default.foreground)
-                            .hover()
-                            .fill(theme.lowest.base.hovered.background)
-                            .active()
-                            .fill(theme.lowest.base.pressed.background)
-                            .child(
-                                svg()
-                                    .path("icons/error.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.negative.default.foreground),
-                            )
-                            .child(div().text_sm().child("2")),
-                    )
-                    .child(
-                        div()
-                            .text_sm()
-                            .text_color(theme.lowest.variant.default.foreground)
-                            .child("Something is wrong"),
-                    ),
-            )
-    }
-
-    fn right_group<V: 'static>(&mut self, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        let theme = theme(cx);
-        div()
-            .flex()
-            .items_center()
-            .h_full()
-            .gap_4()
-            .px_2()
-            // === Tools === //
-            .child(
-                div()
-                    .flex()
-                    .items_center()
-                    .gap_1()
-                    .child(
-                        div()
-                            .w_6()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .child(
-                                svg()
-                                    .path("icons/check_circle.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.base.default.foreground),
-                            ),
-                    )
-                    .child(
-                        div()
-                            .w_6()
-                            .h_full()
-                            .flex()
-                            .items_center()
-                            .justify_center()
-                            .child(
-                                svg()
-                                    .path("icons/copilot.svg")
-                                    .w_4()
-                                    .h_4()
-                                    .fill(theme.lowest.accent.default.foreground),
-                            ),
-                    ),
-            )
-    }
-}

crates/terminal/src/mappings/keys.rs πŸ”—

@@ -333,6 +333,7 @@ mod test {
             cmd: false,
             function: false,
             key: "πŸ––πŸ»".to_string(), //2 char string
+            ime_key: None,
         };
         assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None);
     }

crates/terminal_view/src/terminal_panel.rs πŸ”—

@@ -1,4 +1,4 @@
-use std::sync::Arc;
+use std::{path::PathBuf, sync::Arc};
 
 use crate::TerminalView;
 use db::kvp::KEY_VALUE_STORE;
@@ -23,6 +23,7 @@ actions!(terminal_panel, [ToggleFocus]);
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(TerminalPanel::new_terminal);
+    cx.add_action(TerminalPanel::open_terminal);
 }
 
 #[derive(Debug)]
@@ -70,7 +71,7 @@ impl TerminalPanel {
                 Flex::row()
                     .with_child(Pane::render_tab_bar_button(
                         0,
-                        "icons/plus_12.svg",
+                        "icons/plus.svg",
                         false,
                         Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))),
                         cx,
@@ -79,7 +80,7 @@ impl TerminalPanel {
                             cx.window_context().defer(move |cx| {
                                 if let Some(this) = this.upgrade(cx) {
                                     this.update(cx, |this, cx| {
-                                        this.add_terminal(cx);
+                                        this.add_terminal(None, cx);
                                     });
                                 }
                             })
@@ -90,9 +91,9 @@ impl TerminalPanel {
                     .with_child(Pane::render_tab_bar_button(
                         1,
                         if pane.is_zoomed() {
-                            "icons/minimize_8.svg"
+                            "icons/minimize.svg"
                         } else {
-                            "icons/maximize_8.svg"
+                            "icons/maximize.svg"
                         },
                         pane.is_zoomed(),
                         Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
@@ -230,6 +231,21 @@ impl TerminalPanel {
         }
     }
 
+    pub fn open_terminal(
+        workspace: &mut Workspace,
+        action: &workspace::OpenTerminal,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let Some(this) = workspace.focus_panel::<Self>(cx) else {
+            return;
+        };
+
+        this.update(cx, |this, cx| {
+            this.add_terminal(Some(action.working_directory.clone()), cx)
+        })
+    }
+
+    ///Create a new Terminal in the current working directory or the user's home directory
     fn new_terminal(
         workspace: &mut Workspace,
         _: &workspace::NewTerminal,
@@ -239,19 +255,23 @@ impl TerminalPanel {
             return;
         };
 
-        this.update(cx, |this, cx| this.add_terminal(cx))
+        this.update(cx, |this, cx| this.add_terminal(None, cx))
     }
 
-    fn add_terminal(&mut self, cx: &mut ViewContext<Self>) {
+    fn add_terminal(&mut self, working_directory: Option<PathBuf>, cx: &mut ViewContext<Self>) {
         let workspace = self.workspace.clone();
         cx.spawn(|this, mut cx| async move {
             let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
             workspace.update(&mut cx, |workspace, cx| {
-                let working_directory_strategy = settings::get::<TerminalSettings>(cx)
-                    .working_directory
-                    .clone();
-                let working_directory =
-                    crate::get_working_directory(workspace, cx, working_directory_strategy);
+                let working_directory = if let Some(working_directory) = working_directory {
+                    Some(working_directory)
+                } else {
+                    let working_directory_strategy = settings::get::<TerminalSettings>(cx)
+                        .working_directory
+                        .clone();
+                    crate::get_working_directory(workspace, cx, working_directory_strategy)
+                };
+
                 let window = cx.window();
                 if let Some(terminal) = workspace.project().update(cx, |project, cx| {
                     project
@@ -389,7 +409,7 @@ impl Panel for TerminalPanel {
 
     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         if active && self.pane.read(cx).items_len() == 0 {
-            self.add_terminal(cx)
+            self.add_terminal(None, cx)
         }
     }
 

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

@@ -18,7 +18,7 @@ use gpui::{
     ViewHandle, WeakViewHandle,
 };
 use language::Bias;
-use project::{LocalWorktree, Project};
+use project::{search::SearchQuery, LocalWorktree, Project};
 use serde::Deserialize;
 use smallvec::{smallvec, SmallVec};
 use smol::Timer;
@@ -26,6 +26,7 @@ use std::{
     borrow::Cow,
     ops::RangeInclusive,
     path::{Path, PathBuf},
+    sync::Arc,
     time::Duration,
 };
 use terminal::{
@@ -283,12 +284,7 @@ impl TerminalView {
     pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext<Self>) {
         let menu_entries = vec![
             ContextMenuItem::action("Clear", Clear),
-            ContextMenuItem::action(
-                "Close",
-                pane::CloseActiveItem {
-                    save_behavior: None,
-                },
-            ),
+            ContextMenuItem::action("Close", pane::CloseActiveItem { save_intent: None }),
         ];
 
         self.context_menu.update(cx, |menu, cx| {
@@ -380,10 +376,10 @@ impl TerminalView {
 
     pub fn find_matches(
         &mut self,
-        query: project::search::SearchQuery,
+        query: Arc<project::search::SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<RangeInclusive<Point>>> {
-        let searcher = regex_search_for_query(query);
+        let searcher = regex_search_for_query(&query);
 
         if let Some(searcher) = searcher {
             self.terminal
@@ -486,7 +482,7 @@ fn possible_open_targets(
         .collect()
 }
 
-pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
+pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
     let query = query.as_str();
     let searcher = RegexSearch::new(&query);
     searcher.ok()
@@ -798,6 +794,7 @@ impl SearchableItem for TerminalView {
             case: false,
             word: false,
             regex: false,
+            replacement: false,
         }
     }
 
@@ -851,10 +848,10 @@ impl SearchableItem for TerminalView {
     /// Get all of the matches for this query, should be done on the background
     fn find_matches(
         &mut self,
-        query: project::search::SearchQuery,
+        query: Arc<project::search::SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Self::Match>> {
-        if let Some(searcher) = regex_search_for_query(query) {
+        if let Some(searcher) = regex_search_for_query(&query) {
             self.terminal()
                 .update(cx, |term, cx| term.find_matches(searcher, cx))
         } else {
@@ -898,6 +895,9 @@ impl SearchableItem for TerminalView {
 
         res
     }
+    fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
+        // Replacement is not supported in terminal view, so this is a no-op.
+    }
 }
 
 ///Get's the working directory for the given workspace, respecting the user's settings.

crates/text/src/selection.rs πŸ”—

@@ -100,6 +100,10 @@ impl Selection<usize> {
             reversed: false,
         }
     }
+
+    pub fn equals(&self, offset_range: &Range<usize>) -> bool {
+        self.start == offset_range.start && self.end == offset_range.end
+    }
 }
 
 impl Selection<Anchor> {

crates/theme/src/components.rs πŸ”—

@@ -26,7 +26,6 @@ impl<C: SafeStylable> ComponentExt<C> for C {
 }
 
 pub mod disclosure {
-
     use gpui::{
         elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable},
         Action, Element,

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

@@ -3,7 +3,9 @@ mod theme_registry;
 mod theme_settings;
 pub mod ui;
 
-use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle};
+use components::{
+    action_button::ButtonStyle, disclosure::DisclosureStyle, IconButtonStyle, ToggleIconButtonStyle,
+};
 use gpui::{
     color::Color,
     elements::{Border, ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
@@ -50,6 +52,7 @@ pub struct Theme {
     pub copilot: Copilot,
     pub collab_panel: CollabPanel,
     pub project_panel: ProjectPanel,
+    pub chat_panel: ChatPanel,
     pub command_palette: CommandPalette,
     pub picker: Picker,
     pub editor: Editor,
@@ -128,6 +131,7 @@ pub struct Titlebar {
     pub menu: TitlebarMenu,
     pub project_menu_button: Toggleable<Interactive<ContainedText>>,
     pub git_menu_button: Toggleable<Interactive<ContainedText>>,
+    pub project_host: Interactive<ContainedText>,
     pub item_spacing: f32,
     pub face_pile_spacing: f32,
     pub avatar_ribbon: AvatarRibbon,
@@ -235,6 +239,7 @@ pub struct CollabPanel {
     pub log_in_button: Interactive<ContainedText>,
     pub channel_editor: ContainerStyle,
     pub channel_hash: Icon,
+    pub channel_note_active_color: Color,
     pub tabbed_modal: TabbedModal,
     pub contact_finder: ContactFinder,
     pub channel_modal: ChannelModal,
@@ -248,7 +253,7 @@ pub struct CollabPanel {
     pub leave_call: Interactive<ContainedText>,
     pub contact_row: Toggleable<Interactive<ContainerStyle>>,
     pub channel_row: Toggleable<Interactive<ContainerStyle>>,
-    pub channel_name: ContainedText,
+    pub channel_name: Toggleable<ContainedText>,
     pub row_height: f32,
     pub project_row: Toggleable<Interactive<ProjectRow>>,
     pub tree_branch: Toggleable<Interactive<TreeBranch>>,
@@ -439,9 +444,7 @@ pub struct Search {
     pub include_exclude_editor: FindEditor,
     pub invalid_include_exclude_editor: ContainerStyle,
     pub include_exclude_inputs: ContainedText,
-    pub option_button: Toggleable<Interactive<IconButton>>,
     pub option_button_component: ToggleIconButtonStyle,
-    pub action_button: Toggleable<Interactive<ContainedText>>,
     pub match_background: Color,
     pub match_index: ContainedText,
     pub major_results_status: TextStyle,
@@ -453,6 +456,10 @@ pub struct Search {
     pub search_row_spacing: f32,
     pub option_button_height: f32,
     pub modes_container: ContainerStyle,
+    pub replace_icon: IconStyle,
+    // Used for filters and replace
+    pub option_button: Toggleable<Interactive<IconButton>>,
+    pub action_button: IconButtonStyle,
 }
 
 #[derive(Clone, Deserialize, Default, JsonSchema)]
@@ -621,9 +628,26 @@ pub struct IconButton {
 }
 
 #[derive(Deserialize, Default, JsonSchema)]
-pub struct ChatMessage {
+pub struct ChatPanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
+    pub list: ContainerStyle,
+    pub channel_select: ChannelSelect,
+    pub input_editor: FieldEditor,
+    pub avatar: AvatarStyle,
+    pub avatar_container: ContainerStyle,
+    pub message: ChatMessage,
+    pub continuation_message: ChatMessage,
+    pub last_message_bottom_spacing: f32,
+    pub pending_message: ChatMessage,
+    pub sign_in_prompt: Interactive<TextStyle>,
+    pub icon_button: Interactive<IconButton>,
+}
+
+#[derive(Deserialize, Default, JsonSchema)]
+pub struct ChatMessage {
+    #[serde(flatten)]
+    pub container: Interactive<ContainerStyle>,
     pub body: TextStyle,
     pub sender: ContainedText,
     pub timestamp: ContainedText,
@@ -637,7 +661,6 @@ pub struct ChannelSelect {
     pub item: ChannelName,
     pub active_item: ChannelName,
     pub hovered_item: ChannelName,
-    pub hovered_active_item: ChannelName,
     pub menu: ContainerStyle,
 }
 
@@ -1048,13 +1071,12 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
 }
 
 impl Editor {
-    pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle {
-        let style_ix = replica_id as usize % (self.guest_selections.len() + 1);
-        if style_ix == 0 {
-            &self.selection
-        } else {
-            &self.guest_selections[style_ix - 1]
+    pub fn selection_style_for_room_participant(&self, participant_index: u32) -> SelectionStyle {
+        if self.guest_selections.is_empty() {
+            return SelectionStyle::default();
         }
+        let style_ix = participant_index as usize % self.guest_selections.len();
+        self.guest_selections[style_ix]
     }
 }
 

crates/ui/Cargo.toml πŸ”—

@@ -0,0 +1,16 @@
+[package]
+name = "ui"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[dependencies]
+anyhow.workspace = true
+chrono = "0.4"
+gpui2 = { path = "../gpui2" }
+serde.workspace = true
+settings = { path = "../settings" }
+smallvec.workspace = true
+strum = { version = "0.25.0", features = ["derive"] }
+theme = { path = "../theme" }
+rand = "0.8"

crates/ui/docs/_project.md πŸ”—

@@ -0,0 +1,13 @@
+## Project Plan
+
+- Port existing UI to GPUI2
+- Update UI in places that GPUI1 was limiting us*
+- Understand the needs &/|| struggles the engineers have been having with building UI in the past and address as many of those as possible as we go
+- Ship a simple, straightforward system with documentation that is easy to use to build UI
+
+## Component Classification
+
+To simplify the understanding of components and minimize unnecessary cognitive load, let's categorize components into two types:
+
+- An element refers to a standalone component that doesn't import any other 'ui' components.
+- A component indicates a component that utilizes or imports other 'ui' components.

crates/ui/docs/elevation.md πŸ”—

@@ -0,0 +1,57 @@
+# Elevation
+
+Elevation in Zed applies to all surfaces and components. Elevation is categorized into levels.
+
+Elevation accomplishes the following:
+- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars.
+- Reflects spatial relationships, for instance, how a floating action button’s shadow intimates its disconnection from a collection of cards.
+- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces.
+
+Elevations are the initial elevation values assigned to components by default.
+
+Components may transition to a higher elevation in some cases, like user interations.
+
+On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest.
+
+## Understanding Elevation
+
+Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations.
+
+Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview)
+
+## Elevation Levels
+
+Zed integrates six unique elevation levels in its design system. The elevation of a surface is expressed as a whole number ranging from 0 to 5, both numbers inclusive. A component’s elevation is ascertained by combining the component’s resting elevation with any dynamic elevation offsets.
+
+The levels are detailed as follows:
+
+0. App Background
+1. UI Surface
+2. Elevated Elements
+3. Wash
+4. Focused Element
+5. Dragged Element
+
+### 0. App Background
+
+The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app.
+
+### 1. UI Surface
+
+The UI Surface is the standard elevation for components and is placed above the app background. It is generally used for the background color of the app bar, card, and sheet.
+
+### 2. Elevated Elements
+
+Elevated elements appear above the UI surface layer surfaces and components. Elevated elements are predominantly used for creating popovers, context menus, and tooltips.
+
+### 3. Wash
+
+Wash denotes a distinct elevation reserved to isolate app UI layers from high elevation components such as modals, notifications, and overlaid panels. The wash may not consistently be visible when these components are active. This layer is often referred to as a scrim or overlay and the background color of the wash is typically deployed in its design.
+
+### 4. Focused Element
+
+Focused elements obtain a higher elevation above surfaces and components at wash elevation. They are often used for modals, notifications, and overlaid panels and indicate that they are the sole element the user is interacting with at the moment.
+
+### 5. Dragged Element
+
+Dragged elements gain the highest elevation, thus appearing above surfaces and components at the elevation of focused elements. These are typically used for elements that are being dragged, following the cursor

crates/ui/src/children.rs πŸ”—

@@ -0,0 +1,7 @@
+use std::any::Any;
+
+use gpui2::{AnyElement, ViewContext};
+
+pub type HackyChildren<V> = fn(&mut ViewContext<V>, &dyn Any) -> Vec<AnyElement<V>>;
+
+pub type HackyChildrenPayload = Box<dyn Any>;

crates/ui/src/components.rs πŸ”—

@@ -0,0 +1,163 @@
+mod assistant_panel;
+mod breadcrumb;
+mod buffer;
+mod chat_panel;
+mod collab_panel;
+mod command_palette;
+mod context_menu;
+mod editor_pane;
+mod facepile;
+mod icon_button;
+mod keybinding;
+mod language_selector;
+mod list;
+mod multi_buffer;
+mod palette;
+mod panel;
+mod panes;
+mod player_stack;
+mod project_panel;
+mod recent_projects;
+mod status_bar;
+mod tab;
+mod tab_bar;
+mod terminal;
+mod theme_selector;
+mod title_bar;
+mod toast;
+mod toolbar;
+mod traffic_lights;
+mod workspace;
+
+pub use assistant_panel::*;
+pub use breadcrumb::*;
+pub use buffer::*;
+pub use chat_panel::*;
+pub use collab_panel::*;
+pub use command_palette::*;
+pub use context_menu::*;
+pub use editor_pane::*;
+pub use facepile::*;
+pub use icon_button::*;
+pub use keybinding::*;
+pub use language_selector::*;
+pub use list::*;
+pub use multi_buffer::*;
+pub use palette::*;
+pub use panel::*;
+pub use panes::*;
+pub use player_stack::*;
+pub use project_panel::*;
+pub use recent_projects::*;
+pub use status_bar::*;
+pub use tab::*;
+pub use tab_bar::*;
+pub use terminal::*;
+pub use theme_selector::*;
+pub use title_bar::*;
+pub use toast::*;
+pub use toolbar::*;
+pub use traffic_lights::*;
+pub use workspace::*;
+
+// Nate: Commenting this out for now, unsure if we need it.
+
+// use std::marker::PhantomData;
+// use std::rc::Rc;
+
+// use gpui2::elements::div;
+// use gpui2::interactive::Interactive;
+// use gpui2::platform::MouseButton;
+// use gpui2::{ArcCow, Element, EventContext, IntoElement, ParentElement, ViewContext};
+
+// struct ButtonHandlers<V, D> {
+//     click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
+// }
+
+// impl<V, D> Default for ButtonHandlers<V, D> {
+//     fn default() -> Self {
+//         Self { click: None }
+//     }
+// }
+
+// #[derive(Element)]
+// pub struct Button<V: 'static, D: 'static> {
+//     handlers: ButtonHandlers<V, D>,
+//     label: Option<ArcCow<'static, str>>,
+//     icon: Option<ArcCow<'static, str>>,
+//     data: Rc<D>,
+//     view_type: PhantomData<V>,
+// }
+
+// // Impl block for buttons without data.
+// // See below for an impl block for any button.
+// impl<V: 'static> Button<V, ()> {
+//     fn new() -> Self {
+//         Self {
+//             handlers: ButtonHandlers::default(),
+//             label: None,
+//             icon: None,
+//             data: Rc::new(()),
+//             view_type: PhantomData,
+//         }
+//     }
+
+//     pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
+//         Button {
+//             handlers: ButtonHandlers::default(),
+//             label: self.label,
+//             icon: self.icon,
+//             data: Rc::new(data),
+//             view_type: PhantomData,
+//         }
+//     }
+// }
+
+// // Impl block for button regardless of its data type.
+// impl<V: 'static, D: 'static> Button<V, D> {
+//     pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
+//         self.label = Some(label.into());
+//         self
+//     }
+
+//     pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
+//         self.icon = Some(icon.into());
+//         self
+//     }
+
+//     pub fn on_click(
+//         mut self,
+//         handler: impl Fn(&mut V, &D, &mut EventContext<V>) + 'static,
+//     ) -> Self {
+//         self.handlers.click = Some(Rc::new(handler));
+//         self
+//     }
+// }
+
+// pub fn button<V>() -> Button<V, ()> {
+//     Button::new()
+// }
+
+// impl<V: 'static, D: 'static> Button<V, D> {
+//     fn render(
+//         &mut self,
+//         view: &mut V,
+//         cx: &mut ViewContext<V>,
+//     ) -> impl IntoElement<V> + Interactive<V> {
+//         // let colors = &cx.theme::<Theme>().colors;
+
+//         let button = div()
+//             // .fill(colors.error(0.5))
+//             .h_4()
+//             .children(self.label.clone());
+
+//         if let Some(handler) = self.handlers.click.clone() {
+//             let data = self.data.clone();
+//             button.on_mouse_down(MouseButton::Left, move |view, event, cx| {
+//                 handler(view, data.as_ref(), cx)
+//             })
+//         } else {
+//             button
+//         }
+//     }
+// }

crates/ui/src/components/assistant_panel.rs πŸ”—

@@ -0,0 +1,91 @@
+use std::marker::PhantomData;
+
+use gpui2::geometry::rems;
+
+use crate::prelude::*;
+use crate::theme::theme;
+use crate::{Icon, IconButton, Label, Panel, PanelSide};
+
+#[derive(Element)]
+pub struct AssistantPanel<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+    current_side: PanelSide,
+}
+
+impl<V: 'static> AssistantPanel<V> {
+    pub fn new() -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state: ScrollState::default(),
+            current_side: PanelSide::default(),
+        }
+    }
+
+    pub fn side(mut self, side: PanelSide) -> Self {
+        self.current_side = side;
+        self
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        struct PanelPayload {
+            pub scroll_state: ScrollState,
+        }
+
+        Panel::new(
+            self.scroll_state.clone(),
+            |_, payload| {
+                let payload = payload.downcast_ref::<PanelPayload>().unwrap();
+
+                vec![div()
+                    .flex()
+                    .flex_col()
+                    .h_full()
+                    .px_2()
+                    .gap_2()
+                    // Header
+                    .child(
+                        div()
+                            .flex()
+                            .justify_between()
+                            .gap_2()
+                            .child(
+                                div()
+                                    .flex()
+                                    .child(IconButton::new(Icon::Menu))
+                                    .child(Label::new("New Conversation")),
+                            )
+                            .child(
+                                div()
+                                    .flex()
+                                    .items_center()
+                                    .gap_px()
+                                    .child(IconButton::new(Icon::SplitMessage))
+                                    .child(IconButton::new(Icon::Quote))
+                                    .child(IconButton::new(Icon::MagicWand))
+                                    .child(IconButton::new(Icon::Plus))
+                                    .child(IconButton::new(Icon::Maximize)),
+                            ),
+                    )
+                    // Chat Body
+                    .child(
+                        div()
+                            .w_full()
+                            .flex()
+                            .flex_col()
+                            .gap_3()
+                            .overflow_y_scroll(payload.scroll_state.clone())
+                            .child(Label::new("Is this thing on?")),
+                    )
+                    .into_any()]
+            },
+            Box::new(PanelPayload {
+                scroll_state: self.scroll_state.clone(),
+            }),
+        )
+        .side(self.current_side)
+        .width(rems(32.))
+    }
+}

crates/ui/src/components/breadcrumb.rs πŸ”—

@@ -0,0 +1,71 @@
+use std::path::PathBuf;
+
+use gpui2::elements::div::Div;
+
+use crate::{h_stack, theme};
+use crate::{prelude::*, HighlightedText};
+
+#[derive(Clone)]
+pub struct Symbol(pub Vec<HighlightedText>);
+
+#[derive(Element)]
+pub struct Breadcrumb {
+    path: PathBuf,
+    symbols: Vec<Symbol>,
+}
+
+impl Breadcrumb {
+    pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
+        Self { path, symbols }
+    }
+
+    fn render_separator<V: 'static>(&self, theme: &Theme) -> Div<V> {
+        div()
+            .child(" β€Ί ")
+            .text_color(HighlightColor::Default.hsla(theme))
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let symbols_len = self.symbols.len();
+
+        h_stack()
+            .px_1()
+            // TODO: Read font from theme (or settings?).
+            .font("Zed Mono Extended")
+            .text_sm()
+            .text_color(theme.middle.base.default.foreground)
+            .rounded_md()
+            .hover()
+            .fill(theme.highest.base.hovered.background)
+            .child(self.path.clone().to_str().unwrap().to_string())
+            .child(if !self.symbols.is_empty() {
+                self.render_separator(&theme)
+            } else {
+                div()
+            })
+            .child(
+                div().flex().children(
+                    self.symbols
+                        .iter()
+                        .enumerate()
+                        // TODO: Could use something like `intersperse` here instead.
+                        .flat_map(|(ix, symbol)| {
+                            let mut items =
+                                vec![div().flex().children(symbol.0.iter().map(|segment| {
+                                    div().child(segment.text.clone()).text_color(segment.color)
+                                }))];
+
+                            let is_last_segment = ix == symbols_len - 1;
+                            if !is_last_segment {
+                                items.push(self.render_separator(&theme));
+                            }
+
+                            items
+                        })
+                        .collect::<Vec<_>>(),
+                ),
+            )
+    }
+}

crates/ui/src/components/buffer.rs πŸ”—

@@ -0,0 +1,233 @@
+use gpui2::{Hsla, WindowContext};
+
+use crate::prelude::*;
+use crate::{h_stack, theme, v_stack, Icon, IconElement};
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub struct PlayerCursor {
+    color: Hsla,
+    index: usize,
+}
+
+#[derive(Default, PartialEq, Clone)]
+pub struct HighlightedText {
+    pub text: String,
+    pub color: Hsla,
+}
+
+#[derive(Default, PartialEq, Clone)]
+pub struct HighlightedLine {
+    pub highlighted_texts: Vec<HighlightedText>,
+}
+
+#[derive(Default, PartialEq, Clone)]
+pub struct BufferRow {
+    pub line_number: usize,
+    pub code_action: bool,
+    pub current: bool,
+    pub line: Option<HighlightedLine>,
+    pub cursors: Option<Vec<PlayerCursor>>,
+    pub status: GitStatus,
+    pub show_line_number: bool,
+}
+
+#[derive(Clone)]
+pub struct BufferRows {
+    pub show_line_numbers: bool,
+    pub rows: Vec<BufferRow>,
+}
+
+impl Default for BufferRows {
+    fn default() -> Self {
+        Self {
+            show_line_numbers: true,
+            rows: vec![BufferRow {
+                line_number: 1,
+                code_action: false,
+                current: true,
+                line: None,
+                cursors: None,
+                status: GitStatus::None,
+                show_line_number: true,
+            }],
+        }
+    }
+}
+
+impl BufferRow {
+    pub fn new(line_number: usize) -> Self {
+        Self {
+            line_number,
+            code_action: false,
+            current: false,
+            line: None,
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number: true,
+        }
+    }
+
+    pub fn set_line(mut self, line: Option<HighlightedLine>) -> Self {
+        self.line = line;
+        self
+    }
+
+    pub fn set_cursors(mut self, cursors: Option<Vec<PlayerCursor>>) -> Self {
+        self.cursors = cursors;
+        self
+    }
+
+    pub fn add_cursor(mut self, cursor: PlayerCursor) -> Self {
+        if let Some(cursors) = &mut self.cursors {
+            cursors.push(cursor);
+        } else {
+            self.cursors = Some(vec![cursor]);
+        }
+        self
+    }
+
+    pub fn set_status(mut self, status: GitStatus) -> Self {
+        self.status = status;
+        self
+    }
+
+    pub fn set_show_line_number(mut self, show_line_number: bool) -> Self {
+        self.show_line_number = show_line_number;
+        self
+    }
+
+    pub fn set_code_action(mut self, code_action: bool) -> Self {
+        self.code_action = code_action;
+        self
+    }
+
+    pub fn set_current(mut self, current: bool) -> Self {
+        self.current = current;
+        self
+    }
+}
+
+#[derive(Element, Clone)]
+pub struct Buffer {
+    scroll_state: ScrollState,
+    rows: Option<BufferRows>,
+    readonly: bool,
+    language: Option<String>,
+    title: Option<String>,
+    path: Option<String>,
+}
+
+impl Buffer {
+    pub fn new() -> Self {
+        Self {
+            scroll_state: ScrollState::default(),
+            rows: Some(BufferRows::default()),
+            readonly: false,
+            language: None,
+            title: Some("untitled".to_string()),
+            path: None,
+        }
+    }
+
+    pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) {
+        self.scroll_state = scroll_state;
+    }
+
+    pub fn set_title<T: Into<Option<String>>>(mut self, title: T) -> Self {
+        self.title = title.into();
+        self
+    }
+
+    pub fn set_path<P: Into<Option<String>>>(mut self, path: P) -> Self {
+        self.path = path.into();
+        self
+    }
+
+    pub fn set_readonly(mut self, readonly: bool) -> Self {
+        self.readonly = readonly;
+        self
+    }
+
+    pub fn set_rows<R: Into<Option<BufferRows>>>(mut self, rows: R) -> Self {
+        self.rows = rows.into();
+        self
+    }
+
+    pub fn set_language<L: Into<Option<String>>>(mut self, language: L) -> Self {
+        self.language = language.into();
+        self
+    }
+
+    fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        let line_background = if row.current {
+            theme.middle.base.default.background
+        } else {
+            system_color.transparent
+        };
+
+        let line_number_color = if row.current {
+            HighlightColor::Default.hsla(&theme)
+        } else {
+            HighlightColor::Comment.hsla(&theme)
+        };
+
+        h_stack()
+            .fill(line_background)
+            .w_full()
+            .gap_2()
+            .px_1()
+            .child(
+                h_stack()
+                    .w_4()
+                    .h_full()
+                    .px_0p5()
+                    .when(row.code_action, |c| {
+                        div().child(IconElement::new(Icon::Bolt))
+                    }),
+            )
+            .when(row.show_line_number, |this| {
+                this.child(
+                    h_stack().justify_end().px_0p5().w_3().child(
+                        div()
+                            .text_color(line_number_color)
+                            .child(row.line_number.to_string()),
+                    ),
+                )
+            })
+            .child(div().mx_0p5().w_1().h_full().fill(row.status.hsla(cx)))
+            .children(row.line.map(|line| {
+                div()
+                    .flex()
+                    .children(line.highlighted_texts.iter().map(|highlighted_text| {
+                        div()
+                            .text_color(highlighted_text.color)
+                            .child(highlighted_text.text.clone())
+                    }))
+            }))
+    }
+
+    fn render_rows<V: 'static>(&self, cx: &WindowContext) -> Vec<impl IntoElement<V>> {
+        match &self.rows {
+            Some(rows) => rows
+                .rows
+                .iter()
+                .map(|row| Self::render_row(row.clone(), cx))
+                .collect(),
+            None => vec![],
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let rows = self.render_rows(cx);
+        v_stack()
+            .flex_1()
+            .w_full()
+            .h_full()
+            .fill(theme.highest.base.default.background)
+            .children(rows)
+    }
+}

crates/ui/src/components/chat_panel.rs πŸ”—

@@ -0,0 +1,108 @@
+use std::marker::PhantomData;
+
+use chrono::NaiveDateTime;
+
+use crate::prelude::*;
+use crate::theme::theme;
+use crate::{Icon, IconButton, Input, Label, LabelColor};
+
+#[derive(Element)]
+pub struct ChatPanel<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+    messages: Vec<ChatMessage>,
+}
+
+impl<V: 'static> ChatPanel<V> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+            messages: Vec::new(),
+        }
+    }
+
+    pub fn with_messages(mut self, messages: Vec<ChatMessage>) -> Self {
+        self.messages = messages;
+        self
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .flex_col()
+            .justify_between()
+            .h_full()
+            .px_2()
+            .gap_2()
+            // Header
+            .child(
+                div()
+                    .flex()
+                    .justify_between()
+                    .py_2()
+                    .child(div().flex().child(Label::new("#design")))
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(IconButton::new(Icon::File))
+                            .child(IconButton::new(Icon::AudioOn)),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    // Chat Body
+                    .child(
+                        div()
+                            .w_full()
+                            .flex()
+                            .flex_col()
+                            .gap_3()
+                            .overflow_y_scroll(self.scroll_state.clone())
+                            .children(self.messages.clone()),
+                    )
+                    // Composer
+                    .child(div().flex().my_2().child(Input::new("Message #design"))),
+            )
+    }
+}
+
+#[derive(Element, Clone)]
+pub struct ChatMessage {
+    author: String,
+    text: String,
+    sent_at: NaiveDateTime,
+}
+
+impl ChatMessage {
+    pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self {
+        Self {
+            author,
+            text,
+            sent_at,
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div()
+            .flex()
+            .flex_col()
+            .child(
+                div()
+                    .flex()
+                    .gap_2()
+                    .child(Label::new(self.author.clone()))
+                    .child(
+                        Label::new(self.sent_at.format("%m/%d/%Y").to_string())
+                            .color(LabelColor::Muted),
+                    ),
+            )
+            .child(div().child(Label::new(self.text.clone())))
+    }
+}

crates/ui/src/components/collab_panel.rs πŸ”—

@@ -0,0 +1,161 @@
+use std::marker::PhantomData;
+
+use gpui2::elements::{img, svg};
+use gpui2::ArcCow;
+
+use crate::prelude::*;
+use crate::theme::{theme, Theme};
+use crate::{
+    static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List,
+    ListHeader, ToggleState,
+};
+
+#[derive(Element)]
+pub struct CollabPanel<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+}
+
+impl<V: 'static> CollabPanel<V> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        v_stack()
+            .w_64()
+            .h_full()
+            .fill(theme.middle.base.default.background)
+            .child(
+                v_stack()
+                    .w_full()
+                    .overflow_y_scroll(self.scroll_state.clone())
+                    .child(
+                        div()
+                            .fill(theme.lowest.base.default.background)
+                            .pb_1()
+                            .border_color(theme.lowest.base.default.border)
+                            .border_b()
+                            .child(
+                                List::new(static_collab_panel_current_call())
+                                    .header(
+                                        ListHeader::new("CRDB")
+                                            .left_icon(Icon::Hash.into())
+                                            .set_toggle(ToggleState::Toggled),
+                                    )
+                                    .set_toggle(ToggleState::Toggled),
+                            ),
+                    )
+                    .child(
+                        v_stack().py_1().child(
+                            List::new(static_collab_panel_channels())
+                                .header(
+                                    ListHeader::new("CHANNELS").set_toggle(ToggleState::Toggled),
+                                )
+                                .empty_message("No channels yet. Add a channel to get started.")
+                                .set_toggle(ToggleState::Toggled),
+                        ),
+                    )
+                    .child(
+                        v_stack().py_1().child(
+                            List::new(static_collab_panel_current_call())
+                                .header(
+                                    ListHeader::new("CONTACTS – ONLINE")
+                                        .set_toggle(ToggleState::Toggled),
+                                )
+                                .set_toggle(ToggleState::Toggled),
+                        ),
+                    )
+                    .child(
+                        v_stack().py_1().child(
+                            List::new(static_collab_panel_current_call())
+                                .header(
+                                    ListHeader::new("CONTACTS – OFFLINE")
+                                        .set_toggle(ToggleState::NotToggled),
+                                )
+                                .set_toggle(ToggleState::NotToggled),
+                        ),
+                    ),
+            )
+            .child(
+                div()
+                    .h_7()
+                    .px_2()
+                    .border_t()
+                    .border_color(theme.middle.variant.default.border)
+                    .flex()
+                    .items_center()
+                    .child(
+                        div()
+                            .text_sm()
+                            .text_color(theme.middle.variant.default.foreground)
+                            .child("Find..."),
+                    ),
+            )
+    }
+
+    fn list_section_header(
+        &self,
+        label: impl Into<ArcCow<'static, str>>,
+        expanded: bool,
+        theme: &Theme,
+    ) -> impl Element<V> {
+        div()
+            .h_7()
+            .px_2()
+            .flex()
+            .justify_between()
+            .items_center()
+            .child(div().flex().gap_1().text_sm().child(label))
+            .child(
+                div().flex().h_full().gap_1().items_center().child(
+                    svg()
+                        .path(if expanded {
+                            "icons/caret_down.svg"
+                        } else {
+                            "icons/caret_up.svg"
+                        })
+                        .w_3p5()
+                        .h_3p5()
+                        .fill(theme.middle.variant.default.foreground),
+                ),
+            )
+    }
+
+    fn list_item(
+        &self,
+        avatar_uri: impl Into<ArcCow<'static, str>>,
+        label: impl Into<ArcCow<'static, str>>,
+        theme: &Theme,
+    ) -> impl Element<V> {
+        div()
+            .h_7()
+            .px_2()
+            .flex()
+            .items_center()
+            .hover()
+            .fill(theme.lowest.variant.hovered.background)
+            .active()
+            .fill(theme.lowest.variant.pressed.background)
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .text_sm()
+                    .child(
+                        img()
+                            .uri(avatar_uri)
+                            .size_3p5()
+                            .rounded_full()
+                            .fill(theme.middle.positive.default.foreground),
+                    )
+                    .child(label),
+            )
+    }
+}

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

@@ -0,0 +1,29 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::{example_editor_actions, OrderMethod, Palette};
+
+#[derive(Element)]
+pub struct CommandPalette<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+}
+
+impl<V: 'static> CommandPalette<V> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div().child(
+            Palette::new(self.scroll_state.clone())
+                .items(example_editor_actions())
+                .placeholder("Execute a command...")
+                .empty_string("No items found.")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}

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

@@ -0,0 +1,65 @@
+use crate::prelude::*;
+use crate::theme::theme;
+use crate::{
+    v_stack, Label, List, ListEntry, ListItem, ListItemVariant, ListSeparator, ListSubHeader,
+};
+
+#[derive(Clone)]
+pub enum ContextMenuItem {
+    Header(&'static str),
+    Entry(Label),
+    Separator,
+}
+
+impl ContextMenuItem {
+    fn to_list_item(self) -> ListItem {
+        match self {
+            ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
+            ContextMenuItem::Entry(label) => {
+                ListEntry::new(label).variant(ListItemVariant::Inset).into()
+            }
+            ContextMenuItem::Separator => ListSeparator::new().into(),
+        }
+    }
+    pub fn header(label: &'static str) -> Self {
+        Self::Header(label)
+    }
+    pub fn separator() -> Self {
+        Self::Separator
+    }
+    pub fn entry(label: Label) -> Self {
+        Self::Entry(label)
+    }
+}
+
+#[derive(Element)]
+pub struct ContextMenu {
+    items: Vec<ContextMenuItem>,
+}
+
+impl ContextMenu {
+    pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
+        Self {
+            items: items.into_iter().collect(),
+        }
+    }
+    fn render<V: 'static>(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        v_stack()
+            .flex()
+            .fill(theme.lowest.base.default.background)
+            .border()
+            .border_color(theme.lowest.base.default.border)
+            .child(
+                List::new(
+                    self.items
+                        .clone()
+                        .into_iter()
+                        .map(ContextMenuItem::to_list_item)
+                        .collect(),
+                )
+                .set_toggle(ToggleState::Toggled),
+            )
+        //div().p_1().children(self.items.clone())
+    }
+}

crates/ui/src/components/editor_pane.rs πŸ”—

@@ -0,0 +1,60 @@
+use std::marker::PhantomData;
+use std::path::PathBuf;
+
+use crate::prelude::*;
+use crate::{v_stack, Breadcrumb, Buffer, Icon, IconButton, Symbol, Tab, TabBar, Toolbar};
+
+pub struct Editor {
+    pub tabs: Vec<Tab>,
+    pub path: PathBuf,
+    pub symbols: Vec<Symbol>,
+    pub buffer: Buffer,
+}
+
+#[derive(Element)]
+pub struct EditorPane<V: 'static> {
+    view_type: PhantomData<V>,
+    editor: Editor,
+}
+
+impl<V: 'static> EditorPane<V> {
+    pub fn new(editor: Editor) -> Self {
+        Self {
+            view_type: PhantomData,
+            editor,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        struct LeftItemsPayload {
+            path: PathBuf,
+            symbols: Vec<Symbol>,
+        }
+
+        v_stack()
+            .w_full()
+            .h_full()
+            .flex_1()
+            .child(TabBar::new(self.editor.tabs.clone()))
+            .child(Toolbar::new(
+                |_, payload| {
+                    let payload = payload.downcast_ref::<LeftItemsPayload>().unwrap();
+
+                    vec![Breadcrumb::new(payload.path.clone(), payload.symbols.clone()).into_any()]
+                },
+                Box::new(LeftItemsPayload {
+                    path: self.editor.path.clone(),
+                    symbols: self.editor.symbols.clone(),
+                }),
+                |_, _| {
+                    vec![
+                        IconButton::new(Icon::InlayHint).into_any(),
+                        IconButton::new(Icon::MagnifyingGlass).into_any(),
+                        IconButton::new(Icon::MagicWand).into_any(),
+                    ]
+                },
+                Box::new(()),
+            ))
+            .child(self.editor.buffer.clone())
+    }
+}

crates/ui/src/components/facepile.rs πŸ”—

@@ -0,0 +1,28 @@
+use crate::prelude::*;
+use crate::{theme, Avatar, Player};
+
+#[derive(Element)]
+pub struct Facepile {
+    players: Vec<Player>,
+}
+
+impl Facepile {
+    pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
+        Self {
+            players: players.collect(),
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let player_count = self.players.len();
+        let player_list = self.players.iter().enumerate().map(|(ix, player)| {
+            let isnt_last = ix < player_count - 1;
+
+            div()
+                .when(isnt_last, |div| div.neg_mr_1())
+                .child(Avatar::new(player.avatar_src().to_string()))
+        });
+        div().p_1().flex().items_center().children(player_list)
+    }
+}

crates/ui/src/components/icon_button.rs πŸ”—

@@ -0,0 +1,67 @@
+use crate::prelude::*;
+use crate::{theme, Icon, IconColor, IconElement};
+
+#[derive(Element)]
+pub struct IconButton {
+    icon: Icon,
+    color: IconColor,
+    variant: ButtonVariant,
+    state: InteractionState,
+}
+
+impl IconButton {
+    pub fn new(icon: Icon) -> Self {
+        Self {
+            icon,
+            color: IconColor::default(),
+            variant: ButtonVariant::default(),
+            state: InteractionState::default(),
+        }
+    }
+
+    pub fn icon(mut self, icon: Icon) -> Self {
+        self.icon = icon;
+        self
+    }
+
+    pub fn color(mut self, color: IconColor) -> Self {
+        self.color = color;
+        self
+    }
+
+    pub fn variant(mut self, variant: ButtonVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let icon_color = match (self.state, self.color) {
+            (InteractionState::Disabled, _) => IconColor::Disabled,
+            _ => self.color,
+        };
+
+        let mut div = div();
+        if self.variant == ButtonVariant::Filled {
+            div = div.fill(theme.highest.on.default.background);
+        }
+
+        div.w_7()
+            .h_6()
+            .flex()
+            .items_center()
+            .justify_center()
+            .rounded_md()
+            .hover()
+            .fill(theme.highest.base.hovered.background)
+            .active()
+            .fill(theme.highest.base.pressed.background)
+            .child(IconElement::new(self.icon).color(icon_color))
+    }
+}

crates/ui/src/components/keybinding.rs πŸ”—

@@ -0,0 +1,158 @@
+use std::collections::HashSet;
+
+use strum::{EnumIter, IntoEnumIterator};
+
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Element, Clone)]
+pub struct Keybinding {
+    /// A keybinding consists of a key and a set of modifier keys.
+    /// More then one keybinding produces a chord.
+    ///
+    /// This should always contain at least one element.
+    keybinding: Vec<(String, ModifierKeys)>,
+}
+
+impl Keybinding {
+    pub fn new(key: String, modifiers: ModifierKeys) -> Self {
+        Self {
+            keybinding: vec![(key, modifiers)],
+        }
+    }
+
+    pub fn new_chord(
+        first_note: (String, ModifierKeys),
+        second_note: (String, ModifierKeys),
+    ) -> Self {
+        Self {
+            keybinding: vec![first_note, second_note],
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div()
+            .flex()
+            .gap_2()
+            .children(self.keybinding.iter().map(|(key, modifiers)| {
+                div()
+                    .flex()
+                    .gap_1()
+                    .children(ModifierKey::iter().filter_map(|modifier| {
+                        if modifiers.0.contains(&modifier) {
+                            Some(Key::new(modifier.glyph()))
+                        } else {
+                            None
+                        }
+                    }))
+                    .child(Key::new(key.clone()))
+            }))
+    }
+}
+
+#[derive(Element)]
+pub struct Key {
+    key: String,
+}
+
+impl Key {
+    pub fn new<K>(key: K) -> Self
+    where
+        K: Into<String>,
+    {
+        Self { key: key.into() }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .px_2()
+            .py_0()
+            .rounded_md()
+            .text_sm()
+            .text_color(theme.lowest.on.default.foreground)
+            .fill(theme.lowest.on.default.background)
+            .child(self.key.clone())
+    }
+}
+
+// NOTE: The order the modifier keys appear in this enum impacts the order in
+// which they are rendered in the UI.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum ModifierKey {
+    Control,
+    Alt,
+    Command,
+    Shift,
+}
+
+impl ModifierKey {
+    /// Returns the glyph for the [`ModifierKey`].
+    pub fn glyph(&self) -> char {
+        match self {
+            Self::Control => '^',
+            Self::Alt => 'βŒ₯',
+            Self::Command => '⌘',
+            Self::Shift => '⇧',
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct ModifierKeys(HashSet<ModifierKey>);
+
+impl ModifierKeys {
+    pub fn new() -> Self {
+        Self(HashSet::new())
+    }
+
+    pub fn all() -> Self {
+        Self(HashSet::from_iter(ModifierKey::iter()))
+    }
+
+    pub fn add(mut self, modifier: ModifierKey) -> Self {
+        self.0.insert(modifier);
+        self
+    }
+
+    pub fn control(mut self, control: bool) -> Self {
+        if control {
+            self.0.insert(ModifierKey::Control);
+        } else {
+            self.0.remove(&ModifierKey::Control);
+        }
+
+        self
+    }
+
+    pub fn alt(mut self, alt: bool) -> Self {
+        if alt {
+            self.0.insert(ModifierKey::Alt);
+        } else {
+            self.0.remove(&ModifierKey::Alt);
+        }
+
+        self
+    }
+
+    pub fn command(mut self, command: bool) -> Self {
+        if command {
+            self.0.insert(ModifierKey::Command);
+        } else {
+            self.0.remove(&ModifierKey::Command);
+        }
+
+        self
+    }
+
+    pub fn shift(mut self, shift: bool) -> Self {
+        if shift {
+            self.0.insert(ModifierKey::Shift);
+        } else {
+            self.0.remove(&ModifierKey::Shift);
+        }
+
+        self
+    }
+}

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

@@ -0,0 +1,36 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Element)]
+pub struct LanguageSelector {
+    scroll_state: ScrollState,
+}
+
+impl LanguageSelector {
+    pub fn new() -> Self {
+        Self {
+            scroll_state: ScrollState::default(),
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div().child(
+            Palette::new(self.scroll_state.clone())
+                .items(vec![
+                    PaletteItem::new("C"),
+                    PaletteItem::new("C++"),
+                    PaletteItem::new("CSS"),
+                    PaletteItem::new("Elixir"),
+                    PaletteItem::new("Elm"),
+                    PaletteItem::new("ERB"),
+                    PaletteItem::new("Rust (current)"),
+                    PaletteItem::new("Scheme"),
+                    PaletteItem::new("TOML"),
+                    PaletteItem::new("TypeScript"),
+                ])
+                .placeholder("Select a language...")
+                .empty_string("No matches")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}

crates/ui/src/components/list.rs πŸ”—

@@ -0,0 +1,512 @@
+use gpui2::elements::div::Div;
+use gpui2::{Hsla, WindowContext};
+
+use crate::prelude::*;
+use crate::{
+    h_stack, theme, token, v_stack, Avatar, DisclosureControlVisibility, Icon, IconColor,
+    IconElement, IconSize, InteractionState, Label, LabelColor, LabelSize, SystemColor,
+    ToggleState,
+};
+
+#[derive(Clone, Copy, Default, Debug, PartialEq)]
+pub enum ListItemVariant {
+    /// The list item extends to the far left and right of the list.
+    #[default]
+    FullWidth,
+    Inset,
+}
+
+#[derive(Element, Clone, Copy)]
+pub struct ListHeader {
+    label: &'static str,
+    left_icon: Option<Icon>,
+    variant: ListItemVariant,
+    state: InteractionState,
+    toggleable: Toggleable,
+}
+
+impl ListHeader {
+    pub fn new(label: &'static str) -> Self {
+        Self {
+            label,
+            left_icon: None,
+            variant: ListItemVariant::default(),
+            state: InteractionState::default(),
+            toggleable: Toggleable::default(),
+        }
+    }
+
+    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
+        self.toggleable = toggle.into();
+        self
+    }
+
+    pub fn set_toggleable(mut self, toggleable: Toggleable) -> Self {
+        self.toggleable = toggleable;
+        self
+    }
+
+    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+        self.left_icon = left_icon;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    fn disclosure_control<V: 'static>(&self) -> Div<V> {
+        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+        let is_toggled = Toggleable::is_toggled(&self.toggleable);
+
+        match (is_toggleable, is_toggled) {
+            (false, _) => div(),
+            (_, true) => div().child(IconElement::new(Icon::ChevronRight).color(IconColor::Muted)),
+            (_, false) => div().child(IconElement::new(Icon::ChevronDown).size(IconSize::Small)),
+        }
+    }
+
+    fn background_color(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        match self.state {
+            InteractionState::Hovered => theme.lowest.base.hovered.background,
+            InteractionState::Active => theme.lowest.base.pressed.background,
+            InteractionState::Enabled => theme.lowest.on.default.background,
+            _ => system_color.transparent,
+        }
+    }
+
+    fn label_color(&self) -> LabelColor {
+        match self.state {
+            InteractionState::Disabled => LabelColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn icon_color(&self) -> IconColor {
+        match self.state {
+            InteractionState::Disabled => IconColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let token = token();
+        let system_color = SystemColor::new();
+        let background_color = self.background_color(cx);
+
+        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+        let is_toggled = Toggleable::is_toggled(&self.toggleable);
+
+        let disclosure_control = self.disclosure_control();
+
+        h_stack()
+            .flex_1()
+            .w_full()
+            .fill(background_color)
+            .when(self.state == InteractionState::Focused, |this| {
+                this.border()
+                    .border_color(theme.lowest.accent.default.border)
+            })
+            .relative()
+            .py_1()
+            .child(
+                div()
+                    .h_6()
+                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+                    .flex()
+                    .flex_1()
+                    .w_full()
+                    .gap_1()
+                    .items_center()
+                    .justify_between()
+                    .child(
+                        div()
+                            .flex()
+                            .gap_1()
+                            .items_center()
+                            .children(self.left_icon.map(|i| {
+                                IconElement::new(i)
+                                    .color(IconColor::Muted)
+                                    .size(IconSize::Small)
+                            }))
+                            .child(
+                                Label::new(self.label)
+                                    .color(LabelColor::Muted)
+                                    .size(LabelSize::Small),
+                            ),
+                    )
+                    .child(disclosure_control),
+            )
+    }
+}
+
+#[derive(Element, Clone, Copy)]
+pub struct ListSubHeader {
+    label: &'static str,
+    left_icon: Option<Icon>,
+    variant: ListItemVariant,
+}
+
+impl ListSubHeader {
+    pub fn new(label: &'static str) -> Self {
+        Self {
+            label,
+            left_icon: None,
+            variant: ListItemVariant::default(),
+        }
+    }
+
+    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+        self.left_icon = left_icon;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let token = token();
+
+        h_stack().flex_1().w_full().relative().py_1().child(
+            div()
+                .h_6()
+                .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+                .flex()
+                .flex_1()
+                .w_full()
+                .gap_1()
+                .items_center()
+                .justify_between()
+                .child(
+                    div()
+                        .flex()
+                        .gap_1()
+                        .items_center()
+                        .children(self.left_icon.map(|i| {
+                            IconElement::new(i)
+                                .color(IconColor::Muted)
+                                .size(IconSize::Small)
+                        }))
+                        .child(
+                            Label::new(self.label)
+                                .color(LabelColor::Muted)
+                                .size(LabelSize::Small),
+                        ),
+                ),
+        )
+    }
+}
+
+#[derive(Clone)]
+pub enum LeftContent {
+    Icon(Icon),
+    Avatar(&'static str),
+}
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum ListEntrySize {
+    #[default]
+    Small,
+    Medium,
+}
+
+#[derive(Clone, Element)]
+pub enum ListItem {
+    Entry(ListEntry),
+    Separator(ListSeparator),
+    Header(ListSubHeader),
+}
+
+impl From<ListEntry> for ListItem {
+    fn from(entry: ListEntry) -> Self {
+        Self::Entry(entry)
+    }
+}
+
+impl From<ListSeparator> for ListItem {
+    fn from(entry: ListSeparator) -> Self {
+        Self::Separator(entry)
+    }
+}
+
+impl From<ListSubHeader> for ListItem {
+    fn from(entry: ListSubHeader) -> Self {
+        Self::Header(entry)
+    }
+}
+
+impl ListItem {
+    fn render<V: 'static>(&mut self, v: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        match self {
+            ListItem::Entry(entry) => div().child(entry.render(v, cx)),
+            ListItem::Separator(separator) => div().child(separator.render(v, cx)),
+            ListItem::Header(header) => div().child(header.render(v, cx)),
+        }
+    }
+    pub fn new(label: Label) -> Self {
+        Self::Entry(ListEntry::new(label))
+    }
+    pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
+        if let Self::Entry(entry) = self {
+            Some(entry)
+        } else {
+            None
+        }
+    }
+}
+
+#[derive(Element, Clone)]
+pub struct ListEntry {
+    disclosure_control_style: DisclosureControlVisibility,
+    indent_level: u32,
+    label: Label,
+    left_content: Option<LeftContent>,
+    variant: ListItemVariant,
+    size: ListEntrySize,
+    state: InteractionState,
+    toggle: Option<ToggleState>,
+}
+
+impl ListEntry {
+    pub fn new(label: Label) -> Self {
+        Self {
+            disclosure_control_style: DisclosureControlVisibility::default(),
+            indent_level: 0,
+            label,
+            variant: ListItemVariant::default(),
+            left_content: None,
+            size: ListEntrySize::default(),
+            state: InteractionState::default(),
+            toggle: None,
+        }
+    }
+    pub fn variant(mut self, variant: ListItemVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+    pub fn indent_level(mut self, indent_level: u32) -> Self {
+        self.indent_level = indent_level;
+        self
+    }
+
+    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
+        self.toggle = Some(toggle);
+        self
+    }
+
+    pub fn left_content(mut self, left_content: LeftContent) -> Self {
+        self.left_content = Some(left_content);
+        self
+    }
+
+    pub fn left_icon(mut self, left_icon: Icon) -> Self {
+        self.left_content = Some(LeftContent::Icon(left_icon));
+        self
+    }
+
+    pub fn left_avatar(mut self, left_avatar: &'static str) -> Self {
+        self.left_content = Some(LeftContent::Avatar(left_avatar));
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    pub fn size(mut self, size: ListEntrySize) -> Self {
+        self.size = size;
+        self
+    }
+
+    pub fn disclosure_control_style(
+        mut self,
+        disclosure_control_style: DisclosureControlVisibility,
+    ) -> Self {
+        self.disclosure_control_style = disclosure_control_style;
+        self
+    }
+
+    fn background_color(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        match self.state {
+            InteractionState::Hovered => theme.lowest.base.hovered.background,
+            InteractionState::Active => theme.lowest.base.pressed.background,
+            InteractionState::Enabled => theme.lowest.on.default.background,
+            _ => system_color.transparent,
+        }
+    }
+
+    fn label_color(&self) -> LabelColor {
+        match self.state {
+            InteractionState::Disabled => LabelColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn icon_color(&self) -> IconColor {
+        match self.state {
+            InteractionState::Disabled => IconColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn disclosure_control<V: 'static>(
+        &mut self,
+        cx: &mut ViewContext<V>,
+    ) -> Option<impl IntoElement<V>> {
+        let theme = theme(cx);
+        let token = token();
+
+        let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
+            IconElement::new(Icon::ChevronDown)
+        } else {
+            IconElement::new(Icon::ChevronRight)
+        }
+        .color(IconColor::Muted)
+        .size(IconSize::Small);
+
+        match (self.toggle, self.disclosure_control_style) {
+            (Some(_), DisclosureControlVisibility::OnHover) => {
+                Some(div().absolute().neg_left_5().child(disclosure_control_icon))
+            }
+            (Some(_), DisclosureControlVisibility::Always) => {
+                Some(div().child(disclosure_control_icon))
+            }
+            (None, _) => None,
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let token = token();
+        let system_color = SystemColor::new();
+        let background_color = self.background_color(cx);
+
+        let left_content = match self.left_content {
+            Some(LeftContent::Icon(i)) => {
+                Some(h_stack().child(IconElement::new(i).size(IconSize::Small)))
+            }
+            Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
+            None => None,
+        };
+
+        let sized_item = match self.size {
+            ListEntrySize::Small => div().h_6(),
+            ListEntrySize::Medium => div().h_7(),
+        };
+
+        div()
+            .fill(background_color)
+            .when(self.state == InteractionState::Focused, |this| {
+                this.border()
+                    .border_color(theme.lowest.accent.default.border)
+            })
+            .relative()
+            .py_1()
+            .child(
+                sized_item
+                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+                    // .ml(rems(0.75 * self.indent_level as f32))
+                    .children((0..self.indent_level).map(|_| {
+                        div()
+                            .w(token.list_indent_depth)
+                            .h_full()
+                            .flex()
+                            .justify_center()
+                            .child(h_stack().child(div().w_px().h_full()).child(
+                                div().w_px().h_full().fill(theme.middle.base.default.border),
+                            ))
+                    }))
+                    .flex()
+                    .gap_1()
+                    .items_center()
+                    .relative()
+                    .children(self.disclosure_control(cx))
+                    .children(left_content)
+                    .child(self.label.clone()),
+            )
+    }
+}
+
+#[derive(Clone, Default, Element)]
+pub struct ListSeparator;
+
+impl ListSeparator {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div().h_px().w_full().fill(theme.lowest.base.default.border)
+    }
+}
+
+#[derive(Element)]
+pub struct List {
+    items: Vec<ListItem>,
+    empty_message: &'static str,
+    header: Option<ListHeader>,
+    toggleable: Toggleable,
+}
+
+impl List {
+    pub fn new(items: Vec<ListItem>) -> Self {
+        Self {
+            items,
+            empty_message: "No items",
+            header: None,
+            toggleable: Toggleable::default(),
+        }
+    }
+
+    pub fn empty_message(mut self, empty_message: &'static str) -> Self {
+        self.empty_message = empty_message;
+        self
+    }
+
+    pub fn header(mut self, header: ListHeader) -> Self {
+        self.header = Some(header);
+        self
+    }
+
+    pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
+        self.toggleable = toggle.into();
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let token = token();
+        let is_toggleable = self.toggleable != Toggleable::NotToggleable;
+        let is_toggled = Toggleable::is_toggled(&self.toggleable);
+
+        let disclosure_control = if is_toggleable {
+            IconElement::new(Icon::ChevronRight)
+        } else {
+            IconElement::new(Icon::ChevronDown)
+        };
+
+        let list_content = match (self.items.is_empty(), is_toggled) {
+            (_, false) => div(),
+            (false, _) => div().children(self.items.iter().cloned()),
+            (true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)),
+        };
+
+        v_stack()
+            .py_1()
+            .children(
+                self.header
+                    .clone()
+                    .map(|header| header.set_toggleable(self.toggleable)),
+            )
+            .child(list_content)
+    }
+}

crates/ui/src/components/multi_buffer.rs πŸ”—

@@ -0,0 +1,42 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::{v_stack, Buffer, Icon, IconButton, Label, LabelSize};
+
+#[derive(Element)]
+pub struct MultiBuffer<V: 'static> {
+    view_type: PhantomData<V>,
+    buffers: Vec<Buffer>,
+}
+
+impl<V: 'static> MultiBuffer<V> {
+    pub fn new(buffers: Vec<Buffer>) -> Self {
+        Self {
+            view_type: PhantomData,
+            buffers,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        v_stack()
+            .w_full()
+            .h_full()
+            .flex_1()
+            .children(self.buffers.clone().into_iter().map(|buffer| {
+                v_stack()
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .justify_between()
+                            .p_4()
+                            .fill(theme.lowest.base.default.background)
+                            .child(Label::new("main.rs").size(LabelSize::Small))
+                            .child(IconButton::new(Icon::ArrowUpRight)),
+                    )
+                    .child(buffer)
+            }))
+    }
+}

crates/ui/src/components/palette.rs πŸ”—

@@ -0,0 +1,152 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::theme::theme;
+use crate::{h_stack, v_stack, Keybinding, Label, LabelColor};
+
+#[derive(Element)]
+pub struct Palette<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+    input_placeholder: &'static str,
+    empty_string: &'static str,
+    items: Vec<PaletteItem>,
+    default_order: OrderMethod,
+}
+
+impl<V: 'static> Palette<V> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+            input_placeholder: "Find something...",
+            empty_string: "No items found.",
+            items: vec![],
+            default_order: OrderMethod::default(),
+        }
+    }
+
+    pub fn items(mut self, items: Vec<PaletteItem>) -> Self {
+        self.items = items;
+        self
+    }
+
+    pub fn placeholder(mut self, input_placeholder: &'static str) -> Self {
+        self.input_placeholder = input_placeholder;
+        self
+    }
+
+    pub fn empty_string(mut self, empty_string: &'static str) -> Self {
+        self.empty_string = empty_string;
+        self
+    }
+
+    // TODO: Hook up sort order
+    pub fn default_order(mut self, default_order: OrderMethod) -> Self {
+        self.default_order = default_order;
+        self
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        v_stack()
+            .w_96()
+            .rounded_lg()
+            .fill(theme.lowest.base.default.background)
+            .border()
+            .border_color(theme.lowest.base.default.border)
+            .child(
+                v_stack()
+                    .gap_px()
+                    .child(v_stack().py_0p5().px_1().child(
+                        div().px_2().py_0p5().child(
+                            Label::new(self.input_placeholder).color(LabelColor::Placeholder),
+                        ),
+                    ))
+                    .child(div().h_px().w_full().fill(theme.lowest.base.default.border))
+                    .child(
+                        v_stack()
+                            .py_0p5()
+                            .px_1()
+                            .grow()
+                            .max_h_96()
+                            .overflow_y_scroll(self.scroll_state.clone())
+                            .children(
+                                vec![if self.items.is_empty() {
+                                    Some(h_stack().justify_between().px_2().py_1().child(
+                                        Label::new(self.empty_string).color(LabelColor::Muted),
+                                    ))
+                                } else {
+                                    None
+                                }]
+                                .into_iter()
+                                .flatten(),
+                            )
+                            .children(self.items.iter().map(|item| {
+                                h_stack()
+                                    .justify_between()
+                                    .px_2()
+                                    .py_0p5()
+                                    .rounded_lg()
+                                    .hover()
+                                    .fill(theme.lowest.base.hovered.background)
+                                    .active()
+                                    .fill(theme.lowest.base.pressed.background)
+                                    .child(item.clone())
+                            })),
+                    ),
+            )
+    }
+}
+
+#[derive(Element, Clone)]
+pub struct PaletteItem {
+    pub label: &'static str,
+    pub sublabel: Option<&'static str>,
+    pub keybinding: Option<Keybinding>,
+}
+
+impl PaletteItem {
+    pub fn new(label: &'static str) -> Self {
+        Self {
+            label,
+            sublabel: None,
+            keybinding: None,
+        }
+    }
+
+    pub fn label(mut self, label: &'static str) -> Self {
+        self.label = label;
+        self
+    }
+
+    pub fn sublabel<L: Into<Option<&'static str>>>(mut self, sublabel: L) -> Self {
+        self.sublabel = sublabel.into();
+        self
+    }
+
+    pub fn keybinding<K>(mut self, keybinding: K) -> Self
+    where
+        K: Into<Option<Keybinding>>,
+    {
+        self.keybinding = keybinding.into();
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .flex_row()
+            .grow()
+            .justify_between()
+            .child(
+                v_stack()
+                    .child(Label::new(self.label))
+                    .children(self.sublabel.map(|sublabel| Label::new(sublabel))),
+            )
+            .children(self.keybinding.clone())
+    }
+}

crates/ui/src/components/panel.rs πŸ”—

@@ -0,0 +1,142 @@
+use std::marker::PhantomData;
+
+use gpui2::geometry::AbsoluteLength;
+
+use crate::prelude::*;
+use crate::{theme, token, v_stack};
+
+#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
+pub enum PanelAllowedSides {
+    LeftOnly,
+    RightOnly,
+    BottomOnly,
+    #[default]
+    LeftAndRight,
+    All,
+}
+
+impl PanelAllowedSides {
+    /// Return a `HashSet` that contains the allowable `PanelSide`s.
+    pub fn allowed_sides(&self) -> HashSet<PanelSide> {
+        match self {
+            Self::LeftOnly => HashSet::from_iter([PanelSide::Left]),
+            Self::RightOnly => HashSet::from_iter([PanelSide::Right]),
+            Self::BottomOnly => HashSet::from_iter([PanelSide::Bottom]),
+            Self::LeftAndRight => HashSet::from_iter([PanelSide::Left, PanelSide::Right]),
+            Self::All => HashSet::from_iter([PanelSide::Left, PanelSide::Right, PanelSide::Bottom]),
+        }
+    }
+}
+
+#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
+pub enum PanelSide {
+    #[default]
+    Left,
+    Right,
+    Bottom,
+}
+
+use std::collections::HashSet;
+
+#[derive(Element)]
+pub struct Panel<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+    current_side: PanelSide,
+    /// Defaults to PanelAllowedSides::LeftAndRight
+    allowed_sides: PanelAllowedSides,
+    initial_width: AbsoluteLength,
+    width: Option<AbsoluteLength>,
+    children: HackyChildren<V>,
+    payload: HackyChildrenPayload,
+}
+
+impl<V: 'static> Panel<V> {
+    pub fn new(
+        scroll_state: ScrollState,
+        children: HackyChildren<V>,
+        payload: HackyChildrenPayload,
+    ) -> Self {
+        let token = token();
+
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+            current_side: PanelSide::default(),
+            allowed_sides: PanelAllowedSides::default(),
+            initial_width: token.default_panel_size,
+            width: None,
+            children,
+            payload,
+        }
+    }
+
+    pub fn initial_width(mut self, initial_width: AbsoluteLength) -> Self {
+        self.initial_width = initial_width;
+        self
+    }
+
+    pub fn width(mut self, width: AbsoluteLength) -> Self {
+        self.width = Some(width);
+        self
+    }
+
+    pub fn allowed_sides(mut self, allowed_sides: PanelAllowedSides) -> Self {
+        self.allowed_sides = allowed_sides;
+        self
+    }
+
+    pub fn side(mut self, side: PanelSide) -> Self {
+        let allowed_sides = self.allowed_sides.allowed_sides();
+
+        if allowed_sides.contains(&side) {
+            self.current_side = side;
+        } else {
+            panic!(
+                "The panel side {:?} was not added as allowed before it was set.",
+                side
+            );
+        }
+        self
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let token = token();
+        let theme = theme(cx);
+
+        let panel_base;
+        let current_width = self.width.unwrap_or(self.initial_width);
+
+        match self.current_side {
+            PanelSide::Left => {
+                panel_base = v_stack()
+                    .flex_initial()
+                    .h_full()
+                    .w(current_width)
+                    .fill(theme.middle.base.default.background)
+                    .border_r()
+                    .border_color(theme.middle.base.default.border);
+            }
+            PanelSide::Right => {
+                panel_base = v_stack()
+                    .flex_initial()
+                    .h_full()
+                    .w(current_width)
+                    .fill(theme.middle.base.default.background)
+                    .border_l()
+                    .border_color(theme.middle.base.default.border);
+            }
+            PanelSide::Bottom => {
+                panel_base = v_stack()
+                    .flex_initial()
+                    .w_full()
+                    .h(current_width)
+                    .fill(theme.middle.base.default.background)
+                    .border_t()
+                    .border_color(theme.middle.base.default.border);
+            }
+        }
+
+        panel_base.children_any((self.children)(cx, self.payload.as_ref()))
+    }
+}

crates/ui/src/components/panes.rs πŸ”—

@@ -0,0 +1,132 @@
+use std::marker::PhantomData;
+
+use gpui2::geometry::{Length, Size};
+use gpui2::{hsla, Hsla};
+
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Default, PartialEq)]
+pub enum SplitDirection {
+    #[default]
+    Horizontal,
+    Vertical,
+}
+
+#[derive(Element)]
+pub struct Pane<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+    size: Size<Length>,
+    fill: Hsla,
+    children: HackyChildren<V>,
+    payload: HackyChildrenPayload,
+}
+
+impl<V: 'static> Pane<V> {
+    pub fn new(
+        scroll_state: ScrollState,
+        size: Size<Length>,
+        children: HackyChildren<V>,
+        payload: HackyChildrenPayload,
+    ) -> Self {
+        // Fill is only here for debugging purposes, remove before release
+        let system_color = SystemColor::new();
+
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+            size,
+            fill: hsla(0.3, 0.3, 0.3, 1.),
+            // fill: system_color.transparent,
+            children,
+            payload,
+        }
+    }
+
+    pub fn fill(mut self, fill: Hsla) -> Self {
+        self.fill = fill;
+        self
+    }
+
+    fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .flex_initial()
+            .fill(self.fill)
+            .w(self.size.width)
+            .h(self.size.height)
+            .overflow_y_scroll(self.scroll_state.clone())
+            .children_any((self.children)(cx, self.payload.as_ref()))
+    }
+}
+
+#[derive(Element)]
+pub struct PaneGroup<V: 'static> {
+    view_type: PhantomData<V>,
+    groups: Vec<PaneGroup<V>>,
+    panes: Vec<Pane<V>>,
+    split_direction: SplitDirection,
+}
+
+impl<V: 'static> PaneGroup<V> {
+    pub fn new_groups(groups: Vec<PaneGroup<V>>, split_direction: SplitDirection) -> Self {
+        Self {
+            view_type: PhantomData,
+            groups,
+            panes: Vec::new(),
+            split_direction,
+        }
+    }
+
+    pub fn new_panes(panes: Vec<Pane<V>>, split_direction: SplitDirection) -> Self {
+        Self {
+            view_type: PhantomData,
+            groups: Vec::new(),
+            panes,
+            split_direction,
+        }
+    }
+
+    fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        if !self.panes.is_empty() {
+            let el = div()
+                .flex()
+                .flex_1()
+                .gap_px()
+                .w_full()
+                .h_full()
+                .fill(theme.lowest.base.default.background)
+                .children(self.panes.iter_mut().map(|pane| pane.render(view, cx)));
+
+            if self.split_direction == SplitDirection::Horizontal {
+                return el;
+            } else {
+                return el.flex_col();
+            }
+        }
+
+        if !self.groups.is_empty() {
+            let el = div()
+                .flex()
+                .flex_1()
+                .gap_px()
+                .w_full()
+                .h_full()
+                .fill(theme.lowest.base.default.background)
+                .children(self.groups.iter_mut().map(|group| group.render(view, cx)));
+
+            if self.split_direction == SplitDirection::Horizontal {
+                return el;
+            } else {
+                return el.flex_col();
+            }
+        }
+
+        unreachable!()
+    }
+}

crates/ui/src/components/player_stack.rs πŸ”—

@@ -0,0 +1,65 @@
+use crate::prelude::*;
+use crate::{Avatar, Facepile, PlayerWithCallStatus};
+
+#[derive(Element)]
+pub struct PlayerStack {
+    player_with_call_status: PlayerWithCallStatus,
+}
+
+impl PlayerStack {
+    pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self {
+        Self {
+            player_with_call_status,
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let system_color = SystemColor::new();
+        let player = self.player_with_call_status.get_player();
+        self.player_with_call_status.get_call_status();
+
+        let followers = self
+            .player_with_call_status
+            .get_call_status()
+            .followers
+            .as_ref()
+            .map(|followers| followers.clone());
+
+        // if we have no followers return a slightly different element
+        // if mic_status == muted add a red ring to avatar
+
+        div()
+            .h_full()
+            .flex()
+            .flex_col()
+            .gap_px()
+            .justify_center()
+            .child(
+                div().flex().justify_center().w_full().child(
+                    div()
+                        .w_4()
+                        .h_0p5()
+                        .rounded_sm()
+                        .fill(player.cursor_color(cx)),
+                ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .justify_center()
+                    .h_6()
+                    .pl_1()
+                    .rounded_lg()
+                    .fill(if followers.is_none() {
+                        system_color.transparent
+                    } else {
+                        player.selection_color(cx)
+                    })
+                    .child(Avatar::new(player.avatar_src().to_string()))
+                    .children(followers.map(|followers| {
+                        div().neg_ml_2().child(Facepile::new(followers.into_iter()))
+                    })),
+            )
+    }
+}

crates/ui/src/components/project_panel.rs πŸ”—

@@ -0,0 +1,58 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::{
+    static_project_panel_project_items, static_project_panel_single_items, theme, Input, List,
+    ListHeader,
+};
+
+#[derive(Element)]
+pub struct ProjectPanel<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+}
+
+impl<V: 'static> ProjectPanel<V> {
+    pub fn new(scroll_state: ScrollState) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .flex_col()
+            .w_full()
+            .h_full()
+            .px_2()
+            .fill(theme.middle.base.default.background)
+            .child(
+                div()
+                    .w_56()
+                    .flex()
+                    .flex_col()
+                    .overflow_y_scroll(ScrollState::default())
+                    .child(
+                        List::new(static_project_panel_single_items())
+                            .header(ListHeader::new("FILES").set_toggle(ToggleState::Toggled))
+                            .empty_message("No files in directory")
+                            .set_toggle(ToggleState::Toggled),
+                    )
+                    .child(
+                        List::new(static_project_panel_project_items())
+                            .header(ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled))
+                            .empty_message("No folders in directory")
+                            .set_toggle(ToggleState::Toggled),
+                    ),
+            )
+            .child(
+                Input::new("Find something...")
+                    .value("buffe".to_string())
+                    .state(InteractionState::Focused),
+            )
+    }
+}

crates/ui/src/components/recent_projects.rs πŸ”—

@@ -0,0 +1,32 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Element)]
+pub struct RecentProjects {
+    scroll_state: ScrollState,
+}
+
+impl RecentProjects {
+    pub fn new() -> Self {
+        Self {
+            scroll_state: ScrollState::default(),
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div().child(
+            Palette::new(self.scroll_state.clone())
+                .items(vec![
+                    PaletteItem::new("zed").sublabel("~/projects/zed"),
+                    PaletteItem::new("saga").sublabel("~/projects/saga"),
+                    PaletteItem::new("journal").sublabel("~/journal"),
+                    PaletteItem::new("dotfiles").sublabel("~/dotfiles"),
+                    PaletteItem::new("zed.dev").sublabel("~/projects/zed.dev"),
+                    PaletteItem::new("laminar").sublabel("~/projects/laminar"),
+                ])
+                .placeholder("Recent Projects...")
+                .empty_string("No matches")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}

crates/ui/src/components/status_bar.rs πŸ”—

@@ -0,0 +1,144 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::theme::{theme, Theme};
+use crate::{Button, Icon, IconButton, IconColor, ToolDivider};
+
+#[derive(Default, PartialEq)]
+pub enum Tool {
+    #[default]
+    ProjectPanel,
+    CollaborationPanel,
+    Terminal,
+    Assistant,
+    Feedback,
+    Diagnostics,
+}
+
+struct ToolGroup {
+    active_index: Option<usize>,
+    tools: Vec<Tool>,
+}
+
+impl Default for ToolGroup {
+    fn default() -> Self {
+        ToolGroup {
+            active_index: None,
+            tools: vec![],
+        }
+    }
+}
+
+#[derive(Element)]
+pub struct StatusBar<V: 'static> {
+    view_type: PhantomData<V>,
+    left_tools: Option<ToolGroup>,
+    right_tools: Option<ToolGroup>,
+    bottom_tools: Option<ToolGroup>,
+}
+
+impl<V: 'static> StatusBar<V> {
+    pub fn new() -> Self {
+        Self {
+            view_type: PhantomData,
+            left_tools: None,
+            right_tools: None,
+            bottom_tools: None,
+        }
+    }
+
+    pub fn left_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.left_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    pub fn right_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.right_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    pub fn bottom_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
+        self.bottom_tools = {
+            let mut tools = vec![tool];
+            tools.extend(self.left_tools.take().unwrap_or_default().tools);
+            Some(ToolGroup {
+                active_index,
+                tools,
+            })
+        };
+        self
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .py_0p5()
+            .px_1()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .fill(theme.lowest.base.default.background)
+            .child(self.left_tools(&theme))
+            .child(self.right_tools(&theme))
+    }
+
+    fn left_tools(&self, theme: &Theme) -> impl Element<V> {
+        div()
+            .flex()
+            .items_center()
+            .gap_1()
+            .child(IconButton::new(Icon::FileTree).color(IconColor::Accent))
+            .child(IconButton::new(Icon::Hash))
+            .child(ToolDivider::new())
+            .child(IconButton::new(Icon::XCircle))
+    }
+    fn right_tools(&self, theme: &Theme) -> impl Element<V> {
+        div()
+            .flex()
+            .items_center()
+            .gap_2()
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(Button::new("116:25"))
+                    .child(Button::new("Rust")),
+            )
+            .child(ToolDivider::new())
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(IconButton::new(Icon::Copilot))
+                    .child(IconButton::new(Icon::Envelope)),
+            )
+            .child(ToolDivider::new())
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .child(IconButton::new(Icon::Terminal))
+                    .child(IconButton::new(Icon::MessageBubbles))
+                    .child(IconButton::new(Icon::Ai)),
+            )
+    }
+}

crates/ui/src/components/tab.rs πŸ”—

@@ -0,0 +1,131 @@
+use crate::prelude::*;
+use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor};
+
+#[derive(Element, Clone)]
+pub struct Tab {
+    title: String,
+    icon: Option<Icon>,
+    current: bool,
+    dirty: bool,
+    fs_status: FileSystemStatus,
+    git_status: GitStatus,
+    diagnostic_status: DiagnosticStatus,
+    close_side: IconSide,
+}
+
+impl Tab {
+    pub fn new() -> Self {
+        Self {
+            title: "untitled".to_string(),
+            icon: None,
+            current: false,
+            dirty: false,
+            fs_status: FileSystemStatus::None,
+            git_status: GitStatus::None,
+            diagnostic_status: DiagnosticStatus::None,
+            close_side: IconSide::Right,
+        }
+    }
+
+    pub fn current(mut self, current: bool) -> Self {
+        self.current = current;
+        self
+    }
+
+    pub fn title(mut self, title: String) -> Self {
+        self.title = title;
+        self
+    }
+
+    pub fn icon<I>(mut self, icon: I) -> Self
+    where
+        I: Into<Option<Icon>>,
+    {
+        self.icon = icon.into();
+        self
+    }
+
+    pub fn dirty(mut self, dirty: bool) -> Self {
+        self.dirty = dirty;
+        self
+    }
+
+    pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
+        self.fs_status = fs_status;
+        self
+    }
+
+    pub fn git_status(mut self, git_status: GitStatus) -> Self {
+        self.git_status = git_status;
+        self
+    }
+
+    pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
+        self.diagnostic_status = diagnostic_status;
+        self
+    }
+
+    pub fn close_side(mut self, close_side: IconSide) -> Self {
+        self.close_side = close_side;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
+        let is_deleted = self.fs_status == FileSystemStatus::Deleted;
+
+        let label = match (self.git_status, is_deleted) {
+            (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
+                .color(LabelColor::Hidden)
+                .set_strikethrough(true),
+            (GitStatus::None, false) => Label::new(self.title.clone()),
+            (GitStatus::Created, false) => {
+                Label::new(self.title.clone()).color(LabelColor::Created)
+            }
+            (GitStatus::Modified, false) => {
+                Label::new(self.title.clone()).color(LabelColor::Modified)
+            }
+            (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent),
+            (GitStatus::Conflict, false) => Label::new(self.title.clone()),
+        };
+
+        let close_icon = IconElement::new(Icon::Close).color(IconColor::Muted);
+
+        div()
+            .px_2()
+            .py_0p5()
+            .flex()
+            .items_center()
+            .justify_center()
+            .fill(if self.current {
+                theme.highest.base.default.background
+            } else {
+                theme.middle.base.default.background
+            })
+            .child(
+                div()
+                    .px_1()
+                    .flex()
+                    .items_center()
+                    .gap_1()
+                    .children(has_fs_conflict.then(|| {
+                        IconElement::new(Icon::ExclamationTriangle)
+                            .size(crate::IconSize::Small)
+                            .color(IconColor::Warning)
+                    }))
+                    .children(self.icon.map(IconElement::new))
+                    .children(if self.close_side == IconSide::Left {
+                        Some(close_icon.clone())
+                    } else {
+                        None
+                    })
+                    .child(label)
+                    .children(if self.close_side == IconSide::Right {
+                        Some(close_icon)
+                    } else {
+                        None
+                    }),
+            )
+    }
+}

crates/ui/src/components/tab_bar.rs πŸ”—

@@ -0,0 +1,85 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::{theme, Icon, IconButton, Tab};
+
+#[derive(Element)]
+pub struct TabBar<V: 'static> {
+    view_type: PhantomData<V>,
+    scroll_state: ScrollState,
+    tabs: Vec<Tab>,
+}
+
+impl<V: 'static> TabBar<V> {
+    pub fn new(tabs: Vec<Tab>) -> Self {
+        Self {
+            view_type: PhantomData,
+            scroll_state: ScrollState::default(),
+            tabs,
+        }
+    }
+
+    pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) {
+        self.scroll_state = scroll_state;
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let can_navigate_back = true;
+        let can_navigate_forward = false;
+
+        div()
+            .w_full()
+            .flex()
+            .fill(theme.middle.base.default.background)
+            // Left Side
+            .child(
+                div()
+                    .px_1()
+                    .flex()
+                    .flex_none()
+                    .gap_2()
+                    // Nav Buttons
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(
+                                IconButton::new(Icon::ArrowLeft)
+                                    .state(InteractionState::Enabled.if_enabled(can_navigate_back)),
+                            )
+                            .child(
+                                IconButton::new(Icon::ArrowRight).state(
+                                    InteractionState::Enabled.if_enabled(can_navigate_forward),
+                                ),
+                            ),
+                    ),
+            )
+            .child(
+                div().w_0().flex_1().h_full().child(
+                    div()
+                        .flex()
+                        .overflow_x_scroll(self.scroll_state.clone())
+                        .children(self.tabs.clone()),
+                ),
+            )
+            // Right Side
+            .child(
+                div()
+                    .px_1()
+                    .flex()
+                    .flex_none()
+                    .gap_2()
+                    // Nav Buttons
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_px()
+                            .child(IconButton::new(Icon::Plus))
+                            .child(IconButton::new(Icon::Split)),
+                    ),
+            )
+    }
+}

crates/ui/src/components/terminal.rs πŸ”—

@@ -0,0 +1,84 @@
+use std::sync::Arc;
+
+use gpui2::geometry::{relative, rems, Size};
+
+use crate::prelude::*;
+use crate::{theme, Icon, IconButton, Pane, Tab};
+
+#[derive(Element)]
+pub struct Terminal {}
+
+impl Terminal {
+    pub fn new() -> Self {
+        Self {}
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let can_navigate_back = true;
+        let can_navigate_forward = false;
+
+        div()
+            .flex()
+            .flex_col()
+            .w_full()
+            .child(
+                // Terminal Tabs.
+                div()
+                    .w_full()
+                    .flex()
+                    .fill(theme.middle.base.default.background)
+                    .child(
+                        div().px_1().flex().flex_none().gap_2().child(
+                            div()
+                                .flex()
+                                .items_center()
+                                .gap_px()
+                                .child(
+                                    IconButton::new(Icon::ArrowLeft).state(
+                                        InteractionState::Enabled.if_enabled(can_navigate_back),
+                                    ),
+                                )
+                                .child(IconButton::new(Icon::ArrowRight).state(
+                                    InteractionState::Enabled.if_enabled(can_navigate_forward),
+                                )),
+                        ),
+                    )
+                    .child(
+                        div().w_0().flex_1().h_full().child(
+                            div()
+                                .flex()
+                                .child(
+                                    Tab::new()
+                                        .title("zed β€” fish".to_string())
+                                        .icon(Icon::Terminal)
+                                        .close_side(IconSide::Right)
+                                        .current(true),
+                                )
+                                .child(
+                                    Tab::new()
+                                        .title("zed β€” fish".to_string())
+                                        .icon(Icon::Terminal)
+                                        .close_side(IconSide::Right)
+                                        .current(false),
+                                ),
+                        ),
+                    ),
+            )
+            // Terminal Pane.
+            .child(Pane::new(
+                ScrollState::default(),
+                Size {
+                    width: relative(1.).into(),
+                    height: rems(36.).into(),
+                },
+                |_, payload| {
+                    let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+                    vec![crate::static_data::terminal_buffer(&theme).into_any()]
+                },
+                Box::new(theme),
+            ))
+    }
+}

crates/ui/src/components/theme_selector.rs πŸ”—

@@ -0,0 +1,37 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Element)]
+pub struct ThemeSelector {
+    scroll_state: ScrollState,
+}
+
+impl ThemeSelector {
+    pub fn new() -> Self {
+        Self {
+            scroll_state: ScrollState::default(),
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div().child(
+            Palette::new(self.scroll_state.clone())
+                .items(vec![
+                    PaletteItem::new("One Dark"),
+                    PaletteItem::new("RosΓ© Pine"),
+                    PaletteItem::new("RosΓ© Pine Moon"),
+                    PaletteItem::new("Sandcastle"),
+                    PaletteItem::new("Solarized Dark"),
+                    PaletteItem::new("Summercamp"),
+                    PaletteItem::new("Atelier Cave Light"),
+                    PaletteItem::new("Atelier Dune Light"),
+                    PaletteItem::new("Atelier Estuary Light"),
+                    PaletteItem::new("Atelier Forest Light"),
+                    PaletteItem::new("Atelier Heath Light"),
+                ])
+                .placeholder("Select Theme...")
+                .empty_string("No matches")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}

crates/ui/src/components/title_bar.rs πŸ”—

@@ -0,0 +1,117 @@
+use std::marker::PhantomData;
+use std::sync::atomic::AtomicBool;
+use std::sync::Arc;
+
+use crate::{prelude::*, PlayerWithCallStatus};
+use crate::{
+    theme, Avatar, Button, Icon, IconButton, IconColor, PlayerStack, ToolDivider, TrafficLights,
+};
+
+#[derive(Clone)]
+pub struct Livestream {
+    pub players: Vec<PlayerWithCallStatus>,
+    pub channel: Option<String>, // projects
+                                 // windows
+}
+
+#[derive(Element)]
+pub struct TitleBar<V: 'static> {
+    view_type: PhantomData<V>,
+    /// If the window is active from the OS's perspective.
+    is_active: Arc<AtomicBool>,
+    livestream: Option<Livestream>,
+}
+
+impl<V: 'static> TitleBar<V> {
+    pub fn new(cx: &mut ViewContext<V>) -> Self {
+        let is_active = Arc::new(AtomicBool::new(true));
+        let active = is_active.clone();
+
+        cx.observe_window_activation(move |_, is_active, cx| {
+            active.store(is_active, std::sync::atomic::Ordering::SeqCst);
+            cx.notify();
+        })
+        .detach();
+
+        Self {
+            view_type: PhantomData,
+            is_active,
+            livestream: None,
+        }
+    }
+
+    pub fn set_livestream(mut self, livestream: Option<Livestream>) -> Self {
+        self.livestream = livestream;
+        self
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let has_focus = cx.window_is_active();
+
+        let player_list = if let Some(livestream) = &self.livestream {
+            livestream.players.clone().into_iter()
+        } else {
+            vec![].into_iter()
+        };
+
+        div()
+            .flex()
+            .items_center()
+            .justify_between()
+            .w_full()
+            .h_8()
+            .fill(theme.lowest.base.default.background)
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .h_full()
+                    .gap_4()
+                    .px_2()
+                    .child(TrafficLights::new().window_has_focus(has_focus))
+                    // === Project Info === //
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(Button::new("zed"))
+                            .child(Button::new("nate/gpui2-ui-components")),
+                    )
+                    .children(player_list.map(|p| PlayerStack::new(p)))
+                    .child(IconButton::new(Icon::Plus)),
+            )
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(IconButton::new(Icon::FolderX))
+                            .child(IconButton::new(Icon::Close)),
+                    )
+                    .child(ToolDivider::new())
+                    .child(
+                        div()
+                            .px_2()
+                            .flex()
+                            .items_center()
+                            .gap_1()
+                            .child(IconButton::new(Icon::Mic))
+                            .child(IconButton::new(Icon::AudioOn))
+                            .child(IconButton::new(Icon::Screen).color(IconColor::Accent)),
+                    )
+                    .child(
+                        div().px_2().flex().items_center().child(
+                            Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                .shape(Shape::RoundedRectangle),
+                        ),
+                    ),
+            )
+    }
+}

crates/ui/src/components/toast.rs πŸ”—

@@ -0,0 +1,66 @@
+use crate::prelude::*;
+
+#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
+pub enum ToastOrigin {
+    #[default]
+    Bottom,
+    BottomRight,
+}
+
+#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
+pub enum ToastVariant {
+    #[default]
+    Toast,
+    Status,
+}
+
+/// A toast is a small, temporary window that appears to show a message to the user
+/// or indicate a required action.
+///
+/// Toasts should not persist on the screen for more than a few seconds unless
+/// they are actively showing the a process in progress.
+///
+/// Only one toast may be visible at a time.
+#[derive(Element)]
+pub struct Toast<V: 'static> {
+    origin: ToastOrigin,
+    children: HackyChildren<V>,
+    payload: HackyChildrenPayload,
+}
+
+impl<V: 'static> Toast<V> {
+    pub fn new(
+        origin: ToastOrigin,
+        children: HackyChildren<V>,
+        payload: HackyChildrenPayload,
+    ) -> Self {
+        Self {
+            origin,
+            children,
+            payload,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let color = ThemeColor::new(cx);
+
+        let mut div = div();
+
+        if self.origin == ToastOrigin::Bottom {
+            div = div.right_1_2();
+        } else {
+            div = div.right_4();
+        }
+
+        div.absolute()
+            .bottom_4()
+            .flex()
+            .py_2()
+            .px_1p5()
+            .min_w_40()
+            .rounded_md()
+            .fill(color.elevated_surface)
+            .max_w_64()
+            .children_any((self.children)(cx, self.payload.as_ref()))
+    }
+}

crates/ui/src/components/toolbar.rs πŸ”—

@@ -0,0 +1,49 @@
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Clone)]
+pub struct ToolbarItem {}
+
+#[derive(Element)]
+pub struct Toolbar<V: 'static> {
+    left_items: HackyChildren<V>,
+    left_items_payload: HackyChildrenPayload,
+    right_items: HackyChildren<V>,
+    right_items_payload: HackyChildrenPayload,
+}
+
+impl<V: 'static> Toolbar<V> {
+    pub fn new(
+        left_items: HackyChildren<V>,
+        left_items_payload: HackyChildrenPayload,
+        right_items: HackyChildren<V>,
+        right_items_payload: HackyChildrenPayload,
+    ) -> Self {
+        Self {
+            left_items,
+            left_items_payload,
+            right_items,
+            right_items_payload,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            .fill(theme.highest.base.default.background)
+            .p_2()
+            .flex()
+            .justify_between()
+            .child(
+                div()
+                    .flex()
+                    .children_any((self.left_items)(cx, self.left_items_payload.as_ref())),
+            )
+            .child(
+                div()
+                    .flex()
+                    .children_any((self.right_items)(cx, self.right_items_payload.as_ref())),
+            )
+    }
+}

crates/ui/src/components/traffic_lights.rs πŸ”—

@@ -0,0 +1,78 @@
+use crate::prelude::*;
+use crate::{theme, token, SystemColor};
+
+#[derive(Clone, Copy)]
+enum TrafficLightColor {
+    Red,
+    Yellow,
+    Green,
+}
+
+#[derive(Element)]
+struct TrafficLight {
+    color: TrafficLightColor,
+    window_has_focus: bool,
+}
+
+impl TrafficLight {
+    fn new(color: TrafficLightColor, window_has_focus: bool) -> Self {
+        Self {
+            color,
+            window_has_focus,
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        let fill = match (self.window_has_focus, self.color) {
+            (true, TrafficLightColor::Red) => system_color.mac_os_traffic_light_red,
+            (true, TrafficLightColor::Yellow) => system_color.mac_os_traffic_light_yellow,
+            (true, TrafficLightColor::Green) => system_color.mac_os_traffic_light_green,
+            (false, _) => theme.lowest.base.active.background,
+        };
+
+        div().w_3().h_3().rounded_full().fill(fill)
+    }
+}
+
+#[derive(Element)]
+pub struct TrafficLights {
+    window_has_focus: bool,
+}
+
+impl TrafficLights {
+    pub fn new() -> Self {
+        Self {
+            window_has_focus: true,
+        }
+    }
+
+    pub fn window_has_focus(mut self, window_has_focus: bool) -> Self {
+        self.window_has_focus = window_has_focus;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let token = token();
+
+        div()
+            .flex()
+            .items_center()
+            .gap_2()
+            .child(TrafficLight::new(
+                TrafficLightColor::Red,
+                self.window_has_focus,
+            ))
+            .child(TrafficLight::new(
+                TrafficLightColor::Yellow,
+                self.window_has_focus,
+            ))
+            .child(TrafficLight::new(
+                TrafficLightColor::Green,
+                self.window_has_focus,
+            ))
+    }
+}

crates/ui/src/components/workspace.rs πŸ”—

@@ -0,0 +1,186 @@
+use std::sync::Arc;
+
+use chrono::DateTime;
+use gpui2::geometry::{relative, rems, Size};
+
+use crate::{
+    hello_world_rust_editor_with_status_example, prelude::*, random_players_with_call_status,
+    Livestream,
+};
+use crate::{
+    theme, v_stack, ChatMessage, ChatPanel, EditorPane, Pane, PaneGroup, Panel, PanelAllowedSides,
+    PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
+};
+
+#[derive(Element, Default)]
+pub struct WorkspaceElement {
+    left_panel_scroll_state: ScrollState,
+    right_panel_scroll_state: ScrollState,
+    tab_bar_scroll_state: ScrollState,
+    bottom_panel_scroll_state: ScrollState,
+}
+
+impl WorkspaceElement {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx).clone();
+
+        let temp_size = rems(36.).into();
+
+        let root_group = PaneGroup::new_groups(
+            vec![
+                PaneGroup::new_panes(
+                    vec![
+                        Pane::new(
+                            ScrollState::default(),
+                            Size {
+                                width: relative(1.).into(),
+                                height: temp_size,
+                            },
+                            |_, payload| {
+                                let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+                                vec![EditorPane::new(hello_world_rust_editor_with_status_example(
+                                    &theme,
+                                ))
+                                .into_any()]
+                            },
+                            Box::new(theme.clone()),
+                        ),
+                        Pane::new(
+                            ScrollState::default(),
+                            Size {
+                                width: relative(1.).into(),
+                                height: temp_size,
+                            },
+                            |_, _| vec![Terminal::new().into_any()],
+                            Box::new(()),
+                        ),
+                    ],
+                    SplitDirection::Vertical,
+                ),
+                PaneGroup::new_panes(
+                    vec![Pane::new(
+                        ScrollState::default(),
+                        Size {
+                            width: relative(1.).into(),
+                            height: relative(1.).into(),
+                        },
+                        |_, payload| {
+                            let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+                            vec![EditorPane::new(hello_world_rust_editor_with_status_example(
+                                &theme,
+                            ))
+                            .into_any()]
+                        },
+                        Box::new(theme.clone()),
+                    )],
+                    SplitDirection::Vertical,
+                ),
+            ],
+            SplitDirection::Horizontal,
+        );
+
+        div()
+            .relative()
+            .size_full()
+            .flex()
+            .flex_col()
+            .font("Zed Sans Extended")
+            .gap_0()
+            .justify_start()
+            .items_start()
+            .text_color(theme.lowest.base.default.foreground)
+            .fill(theme.lowest.base.default.background)
+            .child(TitleBar::new(cx).set_livestream(Some(Livestream {
+                players: random_players_with_call_status(7),
+                channel: Some("gpui2-ui".to_string()),
+            })))
+            .child(
+                div()
+                    .flex_1()
+                    .w_full()
+                    .flex()
+                    .flex_row()
+                    .overflow_hidden()
+                    .border_t()
+                    .border_b()
+                    .border_color(theme.lowest.base.default.border)
+                    .child(
+                        Panel::new(
+                            self.left_panel_scroll_state.clone(),
+                            |_, payload| vec![ProjectPanel::new(ScrollState::default()).into_any()],
+                            Box::new(()),
+                        )
+                        .side(PanelSide::Left),
+                    )
+                    .child(
+                        v_stack()
+                            .flex_1()
+                            .h_full()
+                            .child(
+                                div()
+                                    .flex()
+                                    .flex_1()
+                                    // CSS Hack: Flex 1 has to have a set height to properly fill the space
+                                    // Or it will give you a height of 0
+                                    .h_px()
+                                    .child(root_group),
+                            )
+                            .child(
+                                Panel::new(
+                                    self.bottom_panel_scroll_state.clone(),
+                                    |_, _| vec![Terminal::new().into_any()],
+                                    Box::new(()),
+                                )
+                                .allowed_sides(PanelAllowedSides::BottomOnly)
+                                .side(PanelSide::Bottom),
+                            ),
+                    )
+                    .child(
+                        Panel::new(
+                            self.right_panel_scroll_state.clone(),
+                            |_, payload| {
+                                vec![ChatPanel::new(ScrollState::default())
+                                    .with_messages(vec![
+                                        ChatMessage::new(
+                                            "osiewicz".to_string(),
+                                            "is this thing on?".to_string(),
+                                            DateTime::parse_from_rfc3339(
+                                                "2023-09-27T15:40:52.707Z",
+                                            )
+                                            .unwrap()
+                                            .naive_local(),
+                                        ),
+                                        ChatMessage::new(
+                                            "maxdeviant".to_string(),
+                                            "Reading you loud and clear!".to_string(),
+                                            DateTime::parse_from_rfc3339(
+                                                "2023-09-28T15:40:52.707Z",
+                                            )
+                                            .unwrap()
+                                            .naive_local(),
+                                        ),
+                                    ])
+                                    .into_any()]
+                            },
+                            Box::new(()),
+                        )
+                        .side(PanelSide::Right),
+                    ),
+            )
+            .child(StatusBar::new())
+        // An example of a toast is below
+        // Currently because of stacking order this gets obscured by other elements
+
+        // .child(Toast::new(
+        //     ToastOrigin::Bottom,
+        //     |_, payload| {
+        //         let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+        //         vec![Label::new("label").into_any()]
+        //     },
+        //     Box::new(theme.clone()),
+        // ))
+    }
+}

crates/storybook/src/element_ext.rs β†’ crates/ui/src/element_ext.rs πŸ”—

@@ -1,7 +1,9 @@
-use crate::theme::{Theme, Themed};
-use gpui2::Element;
 use std::marker::PhantomData;
 
+use gpui2::Element;
+
+use crate::theme::{Theme, Themed};
+
 pub trait ElementExt<V: 'static>: Element<V> {
     fn themed(self, theme: Theme) -> Themed<V, Self>
     where

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

@@ -0,0 +1,19 @@
+mod avatar;
+mod button;
+mod details;
+mod icon;
+mod input;
+mod label;
+mod player;
+mod stack;
+mod tool_divider;
+
+pub use avatar::*;
+pub use button::*;
+pub use details::*;
+pub use icon::*;
+pub use input::*;
+pub use label::*;
+pub use player::*;
+pub use stack::*;
+pub use tool_divider::*;

crates/ui/src/elements/avatar.rs πŸ”—

@@ -0,0 +1,41 @@
+use gpui2::elements::img;
+use gpui2::ArcCow;
+
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Element, Clone)]
+pub struct Avatar {
+    src: ArcCow<'static, str>,
+    shape: Shape,
+}
+
+impl Avatar {
+    pub fn new(src: impl Into<ArcCow<'static, str>>) -> Self {
+        Self {
+            src: src.into(),
+            shape: Shape::Circle,
+        }
+    }
+
+    pub fn shape(mut self, shape: Shape) -> Self {
+        self.shape = shape;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let mut img = img();
+
+        if self.shape == Shape::Circle {
+            img = img.rounded_full();
+        } else {
+            img = img.rounded_md();
+        }
+
+        img.uri(self.src.clone())
+            .size_4()
+            .fill(theme.middle.warning.default.foreground)
+    }
+}

crates/ui/src/elements/button.rs πŸ”—

@@ -0,0 +1,203 @@
+use std::rc::Rc;
+
+use gpui2::geometry::DefiniteLength;
+use gpui2::platform::MouseButton;
+use gpui2::{EventContext, Hsla, Interactive, WindowContext};
+
+use crate::prelude::*;
+use crate::{h_stack, theme, Icon, IconColor, IconElement, Label, LabelColor, LabelSize};
+
+#[derive(Default, PartialEq, Clone, Copy)]
+pub enum IconPosition {
+    #[default]
+    Left,
+    Right,
+}
+
+#[derive(Default, Copy, Clone, PartialEq)]
+pub enum ButtonVariant {
+    #[default]
+    Ghost,
+    Filled,
+}
+
+struct ButtonHandlers<V> {
+    click: Option<Rc<dyn Fn(&mut V, &mut EventContext<V>)>>,
+}
+
+impl<V> Default for ButtonHandlers<V> {
+    fn default() -> Self {
+        Self { click: None }
+    }
+}
+
+#[derive(Element)]
+pub struct Button<V: 'static> {
+    label: String,
+    variant: ButtonVariant,
+    state: InteractionState,
+    icon: Option<Icon>,
+    icon_position: Option<IconPosition>,
+    width: Option<DefiniteLength>,
+    handlers: ButtonHandlers<V>,
+}
+
+impl<V: 'static> Button<V> {
+    pub fn new<L>(label: L) -> Self
+    where
+        L: Into<String>,
+    {
+        Self {
+            label: label.into(),
+            variant: Default::default(),
+            state: Default::default(),
+            icon: None,
+            icon_position: None,
+            width: Default::default(),
+            handlers: ButtonHandlers::default(),
+        }
+    }
+
+    pub fn ghost<L>(label: L) -> Self
+    where
+        L: Into<String>,
+    {
+        Self::new(label).variant(ButtonVariant::Ghost)
+    }
+
+    pub fn variant(mut self, variant: ButtonVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    pub fn icon(mut self, icon: Icon) -> Self {
+        self.icon = Some(icon);
+        self
+    }
+
+    pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
+        if self.icon.is_none() {
+            panic!("An icon must be present if an icon_position is provided.");
+        }
+        self.icon_position = Some(icon_position);
+        self
+    }
+
+    pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
+        self.width = width;
+        self
+    }
+
+    pub fn on_click(mut self, handler: impl Fn(&mut V, &mut EventContext<V>) + 'static) -> Self {
+        self.handlers.click = Some(Rc::new(handler));
+        self
+    }
+
+    fn background_color(&self, cx: &mut ViewContext<V>) -> Hsla {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        match (self.variant, self.state) {
+            (ButtonVariant::Ghost, InteractionState::Hovered) => {
+                theme.lowest.base.hovered.background
+            }
+            (ButtonVariant::Ghost, InteractionState::Active) => {
+                theme.lowest.base.pressed.background
+            }
+            (ButtonVariant::Filled, InteractionState::Enabled) => {
+                theme.lowest.on.default.background
+            }
+            (ButtonVariant::Filled, InteractionState::Hovered) => {
+                theme.lowest.on.hovered.background
+            }
+            (ButtonVariant::Filled, InteractionState::Active) => theme.lowest.on.pressed.background,
+            (ButtonVariant::Filled, InteractionState::Disabled) => {
+                theme.lowest.on.disabled.background
+            }
+            _ => system_color.transparent,
+        }
+    }
+
+    fn label_color(&self) -> LabelColor {
+        match self.state {
+            InteractionState::Disabled => LabelColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn icon_color(&self) -> IconColor {
+        match self.state {
+            InteractionState::Disabled => IconColor::Disabled,
+            _ => Default::default(),
+        }
+    }
+
+    fn border_color(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        match self.state {
+            InteractionState::Focused => theme.lowest.accent.default.border,
+            _ => system_color.transparent,
+        }
+    }
+
+    fn render_label(&self) -> Label {
+        Label::new(self.label.clone())
+            .size(LabelSize::Small)
+            .color(self.label_color())
+    }
+
+    fn render_icon(&self, icon_color: IconColor) -> Option<IconElement> {
+        self.icon.map(|i| IconElement::new(i).color(icon_color))
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let icon_color = self.icon_color();
+        let system_color = SystemColor::new();
+        let border_color = self.border_color(cx);
+
+        let mut el = h_stack()
+            .h_6()
+            .px_1()
+            .items_center()
+            .rounded_md()
+            .border()
+            .border_color(border_color)
+            .fill(self.background_color(cx));
+
+        match (self.icon, self.icon_position) {
+            (Some(_), Some(IconPosition::Left)) => {
+                el = el
+                    .gap_1()
+                    .child(self.render_label())
+                    .children(self.render_icon(icon_color))
+            }
+            (Some(_), Some(IconPosition::Right)) => {
+                el = el
+                    .gap_1()
+                    .children(self.render_icon(icon_color))
+                    .child(self.render_label())
+            }
+            (_, _) => el = el.child(self.render_label()),
+        }
+
+        if let Some(width) = self.width {
+            el = el.w(width).justify_center();
+        }
+
+        if let Some(click_handler) = self.handlers.click.clone() {
+            el = el.on_mouse_down(MouseButton::Left, move |view, event, cx| {
+                click_handler(view, cx);
+            });
+        }
+
+        el
+    }
+}

crates/ui/src/elements/details.rs πŸ”—

@@ -0,0 +1,33 @@
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Element, Clone)]
+pub struct Details {
+    text: &'static str,
+    meta: Option<&'static str>,
+}
+
+impl Details {
+    pub fn new(text: &'static str) -> Self {
+        Self { text, meta: None }
+    }
+
+    pub fn meta_text(mut self, meta: &'static str) -> Self {
+        self.meta = Some(meta);
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div()
+            // .flex()
+            // .w_full()
+            .p_1()
+            .gap_0p5()
+            .text_xs()
+            .text_color(theme.lowest.base.default.foreground)
+            .child(self.text)
+            .children(self.meta.map(|m| m))
+    }
+}

crates/ui/src/elements/icon.rs πŸ”—

@@ -0,0 +1,185 @@
+use std::sync::Arc;
+
+use gpui2::elements::svg;
+use gpui2::Hsla;
+use strum::EnumIter;
+
+use crate::prelude::*;
+use crate::theme::theme;
+use crate::Theme;
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum IconSize {
+    Small,
+    #[default]
+    Large,
+}
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum IconColor {
+    #[default]
+    Default,
+    Muted,
+    Disabled,
+    Placeholder,
+    Accent,
+    Error,
+    Warning,
+    Success,
+    Info,
+}
+
+impl IconColor {
+    pub fn color(self, theme: Arc<Theme>) -> Hsla {
+        match self {
+            IconColor::Default => theme.lowest.base.default.foreground,
+            IconColor::Muted => theme.lowest.variant.default.foreground,
+            IconColor::Disabled => theme.lowest.base.disabled.foreground,
+            IconColor::Placeholder => theme.lowest.base.disabled.foreground,
+            IconColor::Accent => theme.lowest.accent.default.foreground,
+            IconColor::Error => theme.lowest.negative.default.foreground,
+            IconColor::Warning => theme.lowest.warning.default.foreground,
+            IconColor::Success => theme.lowest.positive.default.foreground,
+            IconColor::Info => theme.lowest.accent.default.foreground,
+        }
+    }
+}
+
+#[derive(Default, PartialEq, Copy, Clone, EnumIter)]
+pub enum Icon {
+    Ai,
+    ArrowLeft,
+    ArrowRight,
+    ArrowUpRight,
+    AudioOff,
+    AudioOn,
+    Bolt,
+    ChevronDown,
+    ChevronLeft,
+    ChevronRight,
+    ChevronUp,
+    Close,
+    ExclamationTriangle,
+    ExternalLink,
+    File,
+    FileGeneric,
+    FileDoc,
+    FileGit,
+    FileLock,
+    FileRust,
+    FileToml,
+    FileTree,
+    Folder,
+    FolderOpen,
+    FolderX,
+    #[default]
+    Hash,
+    InlayHint,
+    MagicWand,
+    MagnifyingGlass,
+    Maximize,
+    Menu,
+    MessageBubbles,
+    Mic,
+    MicMute,
+    Plus,
+    Quote,
+    Screen,
+    SelectAll,
+    Split,
+    SplitMessage,
+    Terminal,
+    XCircle,
+    Copilot,
+    Envelope,
+}
+
+impl Icon {
+    pub fn path(self) -> &'static str {
+        match self {
+            Icon::Ai => "icons/ai.svg",
+            Icon::ArrowLeft => "icons/arrow_left.svg",
+            Icon::ArrowRight => "icons/arrow_right.svg",
+            Icon::ArrowUpRight => "icons/arrow_up_right.svg",
+            Icon::AudioOff => "icons/speaker-off.svg",
+            Icon::AudioOn => "icons/speaker-loud.svg",
+            Icon::Bolt => "icons/bolt.svg",
+            Icon::ChevronDown => "icons/chevron_down.svg",
+            Icon::ChevronLeft => "icons/chevron_left.svg",
+            Icon::ChevronRight => "icons/chevron_right.svg",
+            Icon::ChevronUp => "icons/chevron_up.svg",
+            Icon::Close => "icons/x.svg",
+            Icon::ExclamationTriangle => "icons/warning.svg",
+            Icon::ExternalLink => "icons/external_link.svg",
+            Icon::File => "icons/file.svg",
+            Icon::FileGeneric => "icons/file_icons/file.svg",
+            Icon::FileDoc => "icons/file_icons/book.svg",
+            Icon::FileGit => "icons/file_icons/git.svg",
+            Icon::FileLock => "icons/file_icons/lock.svg",
+            Icon::FileRust => "icons/file_icons/rust.svg",
+            Icon::FileToml => "icons/file_icons/toml.svg",
+            Icon::FileTree => "icons/project.svg",
+            Icon::Folder => "icons/file_icons/folder.svg",
+            Icon::FolderOpen => "icons/file_icons/folder_open.svg",
+            Icon::FolderX => "icons/stop_sharing.svg",
+            Icon::Hash => "icons/hash.svg",
+            Icon::InlayHint => "icons/inlay_hint.svg",
+            Icon::MagicWand => "icons/magic-wand.svg",
+            Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
+            Icon::Maximize => "icons/maximize.svg",
+            Icon::Menu => "icons/menu.svg",
+            Icon::MessageBubbles => "icons/conversations.svg",
+            Icon::Mic => "icons/mic.svg",
+            Icon::MicMute => "icons/mic-mute.svg",
+            Icon::Plus => "icons/plus.svg",
+            Icon::Quote => "icons/quote.svg",
+            Icon::Screen => "icons/desktop.svg",
+            Icon::SelectAll => "icons/select-all.svg",
+            Icon::Split => "icons/split.svg",
+            Icon::SplitMessage => "icons/split_message.svg",
+            Icon::Terminal => "icons/terminal.svg",
+            Icon::XCircle => "icons/error.svg",
+            Icon::Copilot => "icons/copilot.svg",
+            Icon::Envelope => "icons/feedback.svg",
+        }
+    }
+}
+
+#[derive(Element, Clone)]
+pub struct IconElement {
+    icon: Icon,
+    color: IconColor,
+    size: IconSize,
+}
+
+impl IconElement {
+    pub fn new(icon: Icon) -> Self {
+        Self {
+            icon,
+            color: IconColor::default(),
+            size: IconSize::default(),
+        }
+    }
+
+    pub fn color(mut self, color: IconColor) -> Self {
+        self.color = color;
+        self
+    }
+
+    pub fn size(mut self, size: IconSize) -> Self {
+        self.size = size;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+        let fill = self.color.color(theme);
+
+        let sized_svg = match self.size {
+            IconSize::Small => svg().size_3p5(),
+            IconSize::Large => svg().size_4(),
+        };
+
+        sized_svg.flex_none().path(self.icon.path()).fill(fill)
+    }
+}

crates/ui/src/elements/input.rs πŸ”—

@@ -0,0 +1,106 @@
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Default, PartialEq)]
+pub enum InputVariant {
+    #[default]
+    Ghost,
+    Filled,
+}
+
+#[derive(Element)]
+pub struct Input {
+    placeholder: &'static str,
+    value: String,
+    state: InteractionState,
+    variant: InputVariant,
+}
+
+impl Input {
+    pub fn new(placeholder: &'static str) -> Self {
+        Self {
+            placeholder,
+            value: "".to_string(),
+            state: InteractionState::default(),
+            variant: InputVariant::default(),
+        }
+    }
+
+    pub fn value(mut self, value: String) -> Self {
+        self.value = value;
+        self
+    }
+
+    pub fn state(mut self, state: InteractionState) -> Self {
+        self.state = state;
+        self
+    }
+
+    pub fn variant(mut self, variant: InputVariant) -> Self {
+        self.variant = variant;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let text_el;
+        let text_color;
+        let background_color_default;
+        let background_color_active;
+
+        let mut border_color_default = theme.middle.base.default.border;
+        let mut border_color_hover = theme.middle.base.hovered.border;
+        let mut border_color_active = theme.middle.base.pressed.border;
+        let border_color_focus = theme.middle.base.pressed.background;
+
+        match self.variant {
+            InputVariant::Ghost => {
+                background_color_default = theme.middle.base.default.background;
+                background_color_active = theme.middle.base.active.background;
+            }
+            InputVariant::Filled => {
+                background_color_default = theme.middle.on.default.background;
+                background_color_active = theme.middle.on.active.background;
+            }
+        };
+
+        if self.state == InteractionState::Focused {
+            border_color_default = theme.players[0].cursor;
+            border_color_hover = theme.players[0].cursor;
+            border_color_active = theme.players[0].cursor;
+        }
+
+        if self.state == InteractionState::Focused || self.state == InteractionState::Active {
+            text_el = self.value.clone();
+            text_color = theme.lowest.base.default.foreground;
+        } else {
+            text_el = self.placeholder.to_string().clone();
+            text_color = theme.lowest.base.disabled.foreground;
+        }
+
+        div()
+            .h_7()
+            .w_full()
+            .px_2()
+            .border()
+            .border_color(border_color_default)
+            .fill(background_color_default)
+            .hover()
+            .border_color(border_color_hover)
+            .active()
+            .border_color(border_color_active)
+            .fill(background_color_active)
+            .flex()
+            .items_center()
+            .child(
+                div()
+                    .flex()
+                    .items_center()
+                    .text_sm()
+                    .text_color(text_color)
+                    .child(text_el)
+                    .child(div().text_color(theme.players[0].cursor).child("|")),
+            )
+    }
+}

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

@@ -0,0 +1,161 @@
+use gpui2::{Hsla, WindowContext};
+use smallvec::SmallVec;
+
+use crate::prelude::*;
+use crate::theme::theme;
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum LabelColor {
+    #[default]
+    Default,
+    Muted,
+    Created,
+    Modified,
+    Deleted,
+    Disabled,
+    Hidden,
+    Placeholder,
+    Accent,
+}
+
+impl LabelColor {
+    pub fn hsla(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+
+        match self {
+            Self::Default => theme.middle.base.default.foreground,
+            Self::Muted => theme.middle.variant.default.foreground,
+            Self::Created => theme.middle.positive.default.foreground,
+            Self::Modified => theme.middle.warning.default.foreground,
+            Self::Deleted => theme.middle.negative.default.foreground,
+            Self::Disabled => theme.middle.base.disabled.foreground,
+            Self::Hidden => theme.middle.variant.default.foreground,
+            Self::Placeholder => theme.middle.base.disabled.foreground,
+            Self::Accent => theme.middle.accent.default.foreground,
+        }
+    }
+}
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum LabelSize {
+    #[default]
+    Default,
+    Small,
+}
+
+#[derive(Element, Clone)]
+pub struct Label {
+    label: String,
+    color: LabelColor,
+    size: LabelSize,
+    highlight_indices: Vec<usize>,
+    strikethrough: bool,
+}
+
+impl Label {
+    pub fn new<L>(label: L) -> Self
+    where
+        L: Into<String>,
+    {
+        Self {
+            label: label.into(),
+            color: LabelColor::Default,
+            size: LabelSize::Default,
+            highlight_indices: Vec::new(),
+            strikethrough: false,
+        }
+    }
+
+    pub fn color(mut self, color: LabelColor) -> Self {
+        self.color = color;
+        self
+    }
+
+    pub fn size(mut self, size: LabelSize) -> Self {
+        self.size = size;
+        self
+    }
+
+    pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
+        self.highlight_indices = indices;
+        self
+    }
+
+    pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
+        self.strikethrough = strikethrough;
+        self
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        let highlight_color = theme.lowest.accent.default.foreground;
+
+        let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
+
+        let mut runs: SmallVec<[Run; 8]> = SmallVec::new();
+
+        for (char_ix, char) in self.label.char_indices() {
+            let mut color = self.color.hsla(cx);
+
+            if let Some(highlight_ix) = highlight_indices.peek() {
+                if char_ix == *highlight_ix {
+                    color = highlight_color;
+
+                    highlight_indices.next();
+                }
+            }
+
+            let last_run = runs.last_mut();
+
+            let start_new_run = if let Some(last_run) = last_run {
+                if color == last_run.color {
+                    last_run.text.push(char);
+                    false
+                } else {
+                    true
+                }
+            } else {
+                true
+            };
+
+            if start_new_run {
+                runs.push(Run {
+                    text: char.to_string(),
+                    color,
+                });
+            }
+        }
+
+        div()
+            .flex()
+            .when(self.strikethrough, |this| {
+                this.relative().child(
+                    div()
+                        .absolute()
+                        .top_px()
+                        .my_auto()
+                        .w_full()
+                        .h_px()
+                        .fill(LabelColor::Hidden.hsla(cx)),
+                )
+            })
+            .children(runs.into_iter().map(|run| {
+                let mut div = div();
+
+                if self.size == LabelSize::Small {
+                    div = div.text_xs();
+                } else {
+                    div = div.text_sm();
+                }
+
+                div.text_color(run.color).child(run.text)
+            }))
+    }
+}
+
+/// A run of text that receives the same style.
+struct Run {
+    pub text: String,
+    pub color: Hsla,
+}

crates/ui/src/elements/player.rs πŸ”—

@@ -0,0 +1,133 @@
+use gpui2::{Hsla, ViewContext};
+
+use crate::theme;
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum PlayerStatus {
+    #[default]
+    Offline,
+    Online,
+    InCall,
+    Away,
+    DoNotDisturb,
+    Invisible,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum MicStatus {
+    Muted,
+    #[default]
+    Unmuted,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum VideoStatus {
+    On,
+    #[default]
+    Off,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum ScreenShareStatus {
+    Shared,
+    #[default]
+    NotShared,
+}
+
+#[derive(Clone)]
+pub struct PlayerCallStatus {
+    pub mic_status: MicStatus,
+    /// Indicates if the player is currently speaking
+    /// And the intensity of the volume coming through
+    ///
+    /// 0.0 - 1.0
+    pub voice_activity: f32,
+    pub video_status: VideoStatus,
+    pub screen_share_status: ScreenShareStatus,
+    pub in_current_project: bool,
+    pub disconnected: bool,
+    pub following: Option<Vec<Player>>,
+    pub followers: Option<Vec<Player>>,
+}
+
+impl PlayerCallStatus {
+    pub fn new() -> Self {
+        Self {
+            mic_status: MicStatus::default(),
+            voice_activity: 0.,
+            video_status: VideoStatus::default(),
+            screen_share_status: ScreenShareStatus::default(),
+            in_current_project: true,
+            disconnected: false,
+            following: None,
+            followers: None,
+        }
+    }
+}
+
+#[derive(PartialEq, Clone)]
+pub struct Player {
+    index: usize,
+    avatar_src: String,
+    username: String,
+    status: PlayerStatus,
+}
+
+#[derive(Clone)]
+pub struct PlayerWithCallStatus {
+    player: Player,
+    call_status: PlayerCallStatus,
+}
+
+impl PlayerWithCallStatus {
+    pub fn new(player: Player, call_status: PlayerCallStatus) -> Self {
+        Self {
+            player,
+            call_status,
+        }
+    }
+
+    pub fn get_player(&self) -> &Player {
+        &self.player
+    }
+
+    pub fn get_call_status(&self) -> &PlayerCallStatus {
+        &self.call_status
+    }
+}
+
+impl Player {
+    pub fn new(index: usize, avatar_src: String, username: String) -> Self {
+        Self {
+            index,
+            avatar_src,
+            username,
+            status: Default::default(),
+        }
+    }
+
+    pub fn set_status(mut self, status: PlayerStatus) -> Self {
+        self.status = status;
+        self
+    }
+
+    pub fn cursor_color<V>(&self, cx: &mut ViewContext<V>) -> Hsla {
+        let theme = theme(cx);
+        let index = self.index % 8;
+        theme.players[self.index].cursor
+    }
+
+    pub fn selection_color<V>(&self, cx: &mut ViewContext<V>) -> Hsla {
+        let theme = theme(cx);
+        let index = self.index % 8;
+        theme.players[self.index].selection
+    }
+
+    pub fn avatar_src(&self) -> &str {
+        &self.avatar_src
+    }
+
+    pub fn index(&self) -> usize {
+        self.index
+    }
+}

crates/ui/src/elements/stack.rs πŸ”—

@@ -0,0 +1,31 @@
+use gpui2::elements::div::Div;
+
+use crate::prelude::*;
+
+pub trait Stack: StyleHelpers {
+    /// Horizontally stacks elements.
+    fn h_stack(self) -> Self {
+        self.flex().flex_row().items_center()
+    }
+
+    /// Vertically stacks elements.
+    fn v_stack(self) -> Self {
+        self.flex().flex_col()
+    }
+}
+
+impl<V> Stack for Div<V> {}
+
+/// Horizontally stacks elements.
+///
+/// Sets `flex()`, `flex_row()`, `items_center()`
+pub fn h_stack<V: 'static>() -> Div<V> {
+    div().h_stack()
+}
+
+/// Vertically stacks elements.
+///
+/// Sets `flex()`, `flex_col()`
+pub fn v_stack<V: 'static>() -> Div<V> {
+    div().v_stack()
+}

crates/ui/src/elements/tool_divider.rs πŸ”—

@@ -0,0 +1,17 @@
+use crate::prelude::*;
+use crate::theme;
+
+#[derive(Element)]
+pub struct ToolDivider {}
+
+impl ToolDivider {
+    pub fn new() -> Self {
+        Self {}
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        div().w_px().h_3().fill(theme.lowest.base.default.border)
+    }
+}

crates/ui/src/lib.rs πŸ”—

@@ -0,0 +1,20 @@
+#![allow(dead_code, unused_variables)]
+
+mod children;
+mod components;
+mod element_ext;
+mod elements;
+pub mod prelude;
+mod static_data;
+mod theme;
+mod tokens;
+
+pub use children::*;
+pub use components::*;
+pub use element_ext::*;
+pub use elements::*;
+pub use prelude::*;
+pub use static_data::*;
+pub use tokens::*;
+
+pub use crate::theme::*;

crates/ui/src/prelude.rs πŸ”—

@@ -0,0 +1,274 @@
+pub use gpui2::elements::div::{div, ScrollState};
+pub use gpui2::style::{StyleHelpers, Styleable};
+pub use gpui2::{Element, IntoElement, ParentElement, ViewContext};
+
+pub use crate::{theme, ButtonVariant, HackyChildren, HackyChildrenPayload, InputVariant, Theme};
+
+use gpui2::{hsla, rgb, Hsla, WindowContext};
+use strum::EnumIter;
+
+#[derive(Default)]
+pub struct SystemColor {
+    pub transparent: Hsla,
+    pub mac_os_traffic_light_red: Hsla,
+    pub mac_os_traffic_light_yellow: Hsla,
+    pub mac_os_traffic_light_green: Hsla,
+}
+
+impl SystemColor {
+    pub fn new() -> SystemColor {
+        SystemColor {
+            transparent: hsla(0.0, 0.0, 0.0, 0.0),
+            mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
+            mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
+            mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
+        }
+    }
+    pub fn color(&self) -> Hsla {
+        self.transparent
+    }
+}
+
+#[derive(Clone, Copy)]
+pub struct ThemeColor {
+    pub border: Hsla,
+    pub border_variant: Hsla,
+    /// The background color of an elevated surface, like a modal, tooltip or toast.
+    pub elevated_surface: Hsla,
+}
+
+impl ThemeColor {
+    pub fn new(cx: &WindowContext) -> Self {
+        let theme = theme(cx);
+
+        Self {
+            border: theme.lowest.base.default.border,
+            border_variant: theme.lowest.variant.default.border,
+            elevated_surface: theme.middle.base.default.background,
+        }
+    }
+}
+
+#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
+pub enum HighlightColor {
+    #[default]
+    Default,
+    Comment,
+    String,
+    Function,
+    Keyword,
+}
+
+impl HighlightColor {
+    pub fn hsla(&self, theme: &Theme) -> Hsla {
+        let system_color = SystemColor::new();
+
+        match self {
+            Self::Default => theme
+                .syntax
+                .get("primary")
+                .expect("no theme.syntax.primary")
+                .clone(),
+            Self::Comment => theme
+                .syntax
+                .get("comment")
+                .expect("no theme.syntax.comment")
+                .clone(),
+            Self::String => theme
+                .syntax
+                .get("string")
+                .expect("no theme.syntax.string")
+                .clone(),
+            Self::Function => theme
+                .syntax
+                .get("function")
+                .expect("no theme.syntax.function")
+                .clone(),
+            Self::Keyword => theme
+                .syntax
+                .get("keyword")
+                .expect("no theme.syntax.keyword")
+                .clone(),
+        }
+    }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum FileSystemStatus {
+    #[default]
+    None,
+    Conflict,
+    Deleted,
+}
+
+impl FileSystemStatus {
+    pub fn to_string(&self) -> String {
+        match self {
+            Self::None => "None".to_string(),
+            Self::Conflict => "Conflict".to_string(),
+            Self::Deleted => "Deleted".to_string(),
+        }
+    }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum GitStatus {
+    #[default]
+    None,
+    Created,
+    Modified,
+    Deleted,
+    Conflict,
+    Renamed,
+}
+
+impl GitStatus {
+    pub fn to_string(&self) -> String {
+        match self {
+            Self::None => "None".to_string(),
+            Self::Created => "Created".to_string(),
+            Self::Modified => "Modified".to_string(),
+            Self::Deleted => "Deleted".to_string(),
+            Self::Conflict => "Conflict".to_string(),
+            Self::Renamed => "Renamed".to_string(),
+        }
+    }
+
+    pub fn hsla(&self, cx: &WindowContext) -> Hsla {
+        let theme = theme(cx);
+        let system_color = SystemColor::new();
+
+        match self {
+            Self::None => system_color.transparent,
+            Self::Created => theme.lowest.positive.default.foreground,
+            Self::Modified => theme.lowest.warning.default.foreground,
+            Self::Deleted => theme.lowest.negative.default.foreground,
+            Self::Conflict => theme.lowest.warning.default.foreground,
+            Self::Renamed => theme.lowest.accent.default.foreground,
+        }
+    }
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum DiagnosticStatus {
+    #[default]
+    None,
+    Error,
+    Warning,
+    Info,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum IconSide {
+    #[default]
+    Left,
+    Right,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum OrderMethod {
+    #[default]
+    Ascending,
+    Descending,
+    MostRecent,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum Shape {
+    #[default]
+    Circle,
+    RoundedRectangle,
+}
+
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
+pub enum DisclosureControlVisibility {
+    #[default]
+    OnHover,
+    Always,
+}
+
+#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
+pub enum InteractionState {
+    #[default]
+    Enabled,
+    Hovered,
+    Active,
+    Focused,
+    Disabled,
+}
+
+impl InteractionState {
+    pub fn if_enabled(&self, enabled: bool) -> Self {
+        if enabled {
+            *self
+        } else {
+            InteractionState::Disabled
+        }
+    }
+}
+
+#[derive(Default, PartialEq)]
+pub enum SelectedState {
+    #[default]
+    Unselected,
+    PartiallySelected,
+    Selected,
+}
+
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
+pub enum Toggleable {
+    Toggleable(ToggleState),
+    #[default]
+    NotToggleable,
+}
+
+impl Toggleable {
+    pub fn is_toggled(&self) -> bool {
+        match self {
+            Self::Toggleable(ToggleState::Toggled) => true,
+            _ => false,
+        }
+    }
+}
+
+impl From<ToggleState> for Toggleable {
+    fn from(state: ToggleState) -> Self {
+        Self::Toggleable(state)
+    }
+}
+
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
+pub enum ToggleState {
+    /// The "on" state of a toggleable element.
+    ///
+    /// Example:
+    ///     - A collasable list that is currently expanded
+    ///     - A toggle button that is currently on.
+    Toggled,
+    /// The "off" state of a toggleable element.
+    ///
+    /// Example:
+    ///     - A collasable list that is currently collapsed
+    ///     - A toggle button that is currently off.
+    #[default]
+    NotToggled,
+}
+
+impl From<Toggleable> for ToggleState {
+    fn from(toggleable: Toggleable) -> Self {
+        match toggleable {
+            Toggleable::Toggleable(state) => state,
+            Toggleable::NotToggleable => ToggleState::NotToggled,
+        }
+    }
+}
+
+impl From<bool> for ToggleState {
+    fn from(toggled: bool) -> Self {
+        if toggled {
+            ToggleState::Toggled
+        } else {
+            ToggleState::NotToggled
+        }
+    }
+}

crates/ui/src/static_data.rs πŸ”—

@@ -0,0 +1,966 @@
+use std::path::PathBuf;
+use std::str::FromStr;
+
+use rand::Rng;
+
+use crate::{
+    Buffer, BufferRow, BufferRows, Editor, FileSystemStatus, GitStatus, HighlightColor,
+    HighlightedLine, HighlightedText, Icon, Keybinding, Label, LabelColor, ListEntry,
+    ListEntrySize, ListItem, Livestream, MicStatus, ModifierKeys, PaletteItem, Player,
+    PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, Theme, ToggleState,
+    VideoStatus,
+};
+
+pub fn static_tabs_example() -> Vec<Tab> {
+    vec![
+        Tab::new()
+            .title("wip.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .fs_status(FileSystemStatus::Deleted),
+        Tab::new()
+            .title("Cargo.toml".to_string())
+            .icon(Icon::FileToml)
+            .current(false)
+            .git_status(GitStatus::Modified),
+        Tab::new()
+            .title("Channels Panel".to_string())
+            .icon(Icon::Hash)
+            .current(false),
+        Tab::new()
+            .title("channels_panel.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(true)
+            .git_status(GitStatus::Modified),
+        Tab::new()
+            .title("workspace.rs".to_string())
+            .current(false)
+            .icon(Icon::FileRust)
+            .git_status(GitStatus::Modified),
+        Tab::new()
+            .title("icon_button.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new()
+            .title("storybook.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .git_status(GitStatus::Created),
+        Tab::new()
+            .title("theme.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new()
+            .title("theme_registry.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new()
+            .title("styleable_helpers.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+    ]
+}
+
+pub fn static_tabs_1() -> Vec<Tab> {
+    vec![
+        Tab::new()
+            .title("project_panel.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .fs_status(FileSystemStatus::Deleted),
+        Tab::new()
+            .title("tab_bar.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .git_status(GitStatus::Modified),
+        Tab::new()
+            .title("workspace.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new()
+            .title("tab.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(true)
+            .git_status(GitStatus::Modified),
+    ]
+}
+
+pub fn static_tabs_2() -> Vec<Tab> {
+    vec![
+        Tab::new()
+            .title("tab_bar.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .fs_status(FileSystemStatus::Deleted),
+        Tab::new()
+            .title("static_data.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(true)
+            .git_status(GitStatus::Modified),
+    ]
+}
+
+pub fn static_tabs_3() -> Vec<Tab> {
+    vec![Tab::new().git_status(GitStatus::Created).current(true)]
+}
+
+pub fn static_players() -> Vec<Player> {
+    vec![
+        Player::new(
+            0,
+            "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
+            "nathansobo".into(),
+        ),
+        Player::new(
+            1,
+            "https://avatars.githubusercontent.com/u/326587?v=4".into(),
+            "maxbrunsfeld".into(),
+        ),
+        Player::new(
+            2,
+            "https://avatars.githubusercontent.com/u/482957?v=4".into(),
+            "as-cii".into(),
+        ),
+        Player::new(
+            3,
+            "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
+            "iamnbutler".into(),
+        ),
+        Player::new(
+            4,
+            "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+            "maxdeviant".into(),
+        ),
+    ]
+}
+
+#[derive(Debug)]
+pub struct PlayerData {
+    pub url: String,
+    pub name: String,
+}
+pub fn static_player_data() -> Vec<PlayerData> {
+    vec![
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
+            name: "iamnbutler".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/326587?v=4".into(),
+            name: "maxbrunsfeld".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/482957?v=4".into(),
+            name: "as-cii".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1789?v=4".into(),
+            name: "nathansobo".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+            name: "ForLoveOfCats".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/2690773?v=4".into(),
+            name: "SomeoneToIgnore".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/19867440?v=4".into(),
+            name: "JosephTLyons".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/24362066?v=4".into(),
+            name: "osiewicz".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/22121886?v=4".into(),
+            name: "KCaverly".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+            name: "maxdeviant".into(),
+        },
+    ]
+}
+pub fn create_static_players(player_data: Vec<PlayerData>) -> Vec<Player> {
+    let mut players = Vec::new();
+    for data in player_data {
+        players.push(Player::new(players.len(), data.url, data.name));
+    }
+    players
+}
+pub fn static_player_1(data: &Vec<PlayerData>) -> Player {
+    Player::new(1, data[0].url.clone(), data[0].name.clone())
+}
+pub fn static_player_2(data: &Vec<PlayerData>) -> Player {
+    Player::new(2, data[1].url.clone(), data[1].name.clone())
+}
+pub fn static_player_3(data: &Vec<PlayerData>) -> Player {
+    Player::new(3, data[2].url.clone(), data[2].name.clone())
+}
+pub fn static_player_4(data: &Vec<PlayerData>) -> Player {
+    Player::new(4, data[3].url.clone(), data[3].name.clone())
+}
+pub fn static_player_5(data: &Vec<PlayerData>) -> Player {
+    Player::new(5, data[4].url.clone(), data[4].name.clone())
+}
+pub fn static_player_6(data: &Vec<PlayerData>) -> Player {
+    Player::new(6, data[5].url.clone(), data[5].name.clone())
+}
+pub fn static_player_7(data: &Vec<PlayerData>) -> Player {
+    Player::new(7, data[6].url.clone(), data[6].name.clone())
+}
+pub fn static_player_8(data: &Vec<PlayerData>) -> Player {
+    Player::new(8, data[7].url.clone(), data[7].name.clone())
+}
+pub fn static_player_9(data: &Vec<PlayerData>) -> Player {
+    Player::new(9, data[8].url.clone(), data[8].name.clone())
+}
+pub fn static_player_10(data: &Vec<PlayerData>) -> Player {
+    Player::new(10, data[9].url.clone(), data[9].name.clone())
+}
+pub fn static_livestream() -> Livestream {
+    Livestream {
+        players: random_players_with_call_status(7),
+        channel: Some("gpui2-ui".to_string()),
+    }
+}
+pub fn populate_player_call_status(
+    player: Player,
+    followers: Option<Vec<Player>>,
+) -> PlayerCallStatus {
+    let mut rng = rand::thread_rng();
+    let in_current_project: bool = rng.gen();
+    let disconnected: bool = rng.gen();
+    let voice_activity: f32 = rng.gen();
+    let mic_status = if rng.gen_bool(0.5) {
+        MicStatus::Muted
+    } else {
+        MicStatus::Unmuted
+    };
+    let video_status = if rng.gen_bool(0.5) {
+        VideoStatus::On
+    } else {
+        VideoStatus::Off
+    };
+    let screen_share_status = if rng.gen_bool(0.5) {
+        ScreenShareStatus::Shared
+    } else {
+        ScreenShareStatus::NotShared
+    };
+    PlayerCallStatus {
+        mic_status,
+        voice_activity,
+        video_status,
+        screen_share_status,
+        in_current_project,
+        disconnected,
+        following: None,
+        followers,
+    }
+}
+pub fn random_players_with_call_status(number_of_players: usize) -> Vec<PlayerWithCallStatus> {
+    let players = create_static_players(static_player_data());
+    let mut player_status = vec![];
+    for i in 0..number_of_players {
+        let followers = if i == 0 {
+            Some(vec![
+                players[1].clone(),
+                players[3].clone(),
+                players[5].clone(),
+                players[6].clone(),
+            ])
+        } else if i == 1 {
+            Some(vec![players[2].clone(), players[6].clone()])
+        } else {
+            None
+        };
+        let call_status = populate_player_call_status(players[i].clone(), followers);
+        player_status.push(PlayerWithCallStatus::new(players[i].clone(), call_status));
+    }
+    player_status
+}
+
+pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
+    let players = static_players();
+    let mut player_0_status = PlayerCallStatus::new();
+    let player_1_status = PlayerCallStatus::new();
+    let player_2_status = PlayerCallStatus::new();
+    let mut player_3_status = PlayerCallStatus::new();
+    let mut player_4_status = PlayerCallStatus::new();
+
+    player_0_status.screen_share_status = ScreenShareStatus::Shared;
+    player_0_status.followers = Some(vec![players[1].clone(), players[3].clone()]);
+
+    player_3_status.voice_activity = 0.5;
+    player_4_status.mic_status = MicStatus::Muted;
+    player_4_status.in_current_project = false;
+
+    vec![
+        PlayerWithCallStatus::new(players[0].clone(), player_0_status),
+        PlayerWithCallStatus::new(players[1].clone(), player_1_status),
+        PlayerWithCallStatus::new(players[2].clone(), player_2_status),
+        PlayerWithCallStatus::new(players[3].clone(), player_3_status),
+        PlayerWithCallStatus::new(players[4].clone(), player_4_status),
+    ]
+}
+
+pub fn static_project_panel_project_items() -> Vec<ListItem> {
+    vec![
+        ListEntry::new(Label::new("zed"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(0)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new(".cargo"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".config"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".git").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".cargo"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".idea").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("assets"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("crates"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(1)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("activity_indicator"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("ai"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("audio"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("auto_update"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("breadcrumbs"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("call"))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2),
+        ListEntry::new(Label::new("sqlez").color(LabelColor::Modified))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2)
+            .set_toggle(ToggleState::NotToggled),
+        ListEntry::new(Label::new("gpui2"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(2)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("src"))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(3)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("derive_element.rs"))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(4),
+        ListEntry::new(Label::new("storybook").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(1)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("docs").color(LabelColor::Default))
+            .left_icon(Icon::Folder.into())
+            .indent_level(2)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("src").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(3)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("ui").color(LabelColor::Modified))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(4)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("component").color(LabelColor::Created))
+            .left_icon(Icon::FolderOpen.into())
+            .indent_level(5)
+            .set_toggle(ToggleState::Toggled),
+        ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("tab.rs").color(LabelColor::Default))
+            .left_icon(Icon::FileRust.into())
+            .indent_level(6),
+        ListEntry::new(Label::new("target").color(LabelColor::Hidden))
+            .left_icon(Icon::Folder.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".dockerignore"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Cargo.lock"))
+            .left_icon(Icon::FileLock.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Cargo.toml"))
+            .left_icon(Icon::FileToml.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Dockerfile"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("Procfile"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(1),
+        ListEntry::new(Label::new("README.md"))
+            .left_icon(Icon::FileDoc.into())
+            .indent_level(1),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_project_panel_single_items() -> Vec<ListItem> {
+    vec![
+        ListEntry::new(Label::new("todo.md"))
+            .left_icon(Icon::FileDoc.into())
+            .indent_level(0),
+        ListEntry::new(Label::new("README.md"))
+            .left_icon(Icon::FileDoc.into())
+            .indent_level(0),
+        ListEntry::new(Label::new("config.json"))
+            .left_icon(Icon::FileGeneric.into())
+            .indent_level(0),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_collab_panel_current_call() -> Vec<ListItem> {
+    vec![
+        ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
+        ListEntry::new(Label::new("nathansobo"))
+            .left_avatar("http://github.com/nathansobo.png?s=50"),
+        ListEntry::new(Label::new("maxbrunsfeld"))
+            .left_avatar("http://github.com/maxbrunsfeld.png?s=50"),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn static_collab_panel_channels() -> Vec<ListItem> {
+    vec![
+        ListEntry::new(Label::new("zed"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(0),
+        ListEntry::new(Label::new("community"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(1),
+        ListEntry::new(Label::new("dashboards"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("feedback"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("teams-in-channels-alpha"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("current-projects"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(1),
+        ListEntry::new(Label::new("codegen"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("gpui2"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("livestreaming"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("open-source"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("replace"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("semantic-index"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("vim"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+        ListEntry::new(Label::new("web-tech"))
+            .left_icon(Icon::Hash.into())
+            .size(ListEntrySize::Medium)
+            .indent_level(2),
+    ]
+    .into_iter()
+    .map(From::from)
+    .collect()
+}
+
+pub fn example_editor_actions() -> Vec<PaletteItem> {
+    vec![
+        PaletteItem::new("New File").keybinding(Keybinding::new(
+            "N".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Open File").keybinding(Keybinding::new(
+            "O".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Save File").keybinding(Keybinding::new(
+            "S".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Cut").keybinding(Keybinding::new(
+            "X".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Copy").keybinding(Keybinding::new(
+            "C".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Paste").keybinding(Keybinding::new(
+            "V".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Undo").keybinding(Keybinding::new(
+            "Z".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Redo").keybinding(Keybinding::new(
+            "Z".to_string(),
+            ModifierKeys::new().control(true).shift(true),
+        )),
+        PaletteItem::new("Find").keybinding(Keybinding::new(
+            "F".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Replace").keybinding(Keybinding::new(
+            "R".to_string(),
+            ModifierKeys::new().control(true),
+        )),
+        PaletteItem::new("Jump to Line"),
+        PaletteItem::new("Select All"),
+        PaletteItem::new("Deselect All"),
+        PaletteItem::new("Switch Document"),
+        PaletteItem::new("Insert Line Below"),
+        PaletteItem::new("Insert Line Above"),
+        PaletteItem::new("Move Line Up"),
+        PaletteItem::new("Move Line Down"),
+        PaletteItem::new("Toggle Comment"),
+        PaletteItem::new("Delete Line"),
+    ]
+}
+
+pub fn empty_editor_example() -> Editor {
+    Editor {
+        tabs: static_tabs_example(),
+        path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+        symbols: vec![],
+        buffer: empty_buffer_example(),
+    }
+}
+
+pub fn empty_buffer_example() -> Buffer {
+    Buffer::new().set_rows(Some(BufferRows::default()))
+}
+
+pub fn hello_world_rust_editor_example(theme: &Theme) -> Editor {
+    Editor {
+        tabs: static_tabs_example(),
+        path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+        symbols: vec![Symbol(vec![
+            HighlightedText {
+                text: "fn ".to_string(),
+                color: HighlightColor::Keyword.hsla(&theme),
+            },
+            HighlightedText {
+                text: "main".to_string(),
+                color: HighlightColor::Function.hsla(&theme),
+            },
+        ])],
+        buffer: hello_world_rust_buffer_example(theme),
+    }
+}
+
+pub fn hello_world_rust_buffer_example(theme: &Theme) -> Buffer {
+    Buffer::new()
+        .set_title("hello_world.rs".to_string())
+        .set_path("src/hello_world.rs".to_string())
+        .set_language("rust".to_string())
+        .set_rows(Some(BufferRows {
+            show_line_numbers: true,
+            rows: hello_world_rust_buffer_rows(theme),
+        }))
+}
+
+pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
+    let show_line_number = true;
+
+    vec![
+        BufferRow {
+            line_number: 1,
+            code_action: false,
+            current: true,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "fn ".to_string(),
+                        color: HighlightColor::Keyword.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "main".to_string(),
+                        color: HighlightColor::Function.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "() {".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 2,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "    // Statements here are executed when the compiled binary is called."
+                        .to_string(),
+                    color: HighlightColor::Comment.hsla(&theme),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 3,
+            code_action: false,
+            current: false,
+            line: None,
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 4,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "    // Print text to the console.".to_string(),
+                    color: HighlightColor::Comment.hsla(&theme),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 5,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "    println!(".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "\"Hello, world!\"".to_string(),
+                        color: HighlightColor::String.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: ");".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 6,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "}".to_string(),
+                    color: HighlightColor::Default.hsla(&theme),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+    ]
+}
+
+pub fn hello_world_rust_editor_with_status_example(theme: &Theme) -> Editor {
+    Editor {
+        tabs: static_tabs_example(),
+        path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+        symbols: vec![Symbol(vec![
+            HighlightedText {
+                text: "fn ".to_string(),
+                color: HighlightColor::Keyword.hsla(&theme),
+            },
+            HighlightedText {
+                text: "main".to_string(),
+                color: HighlightColor::Function.hsla(&theme),
+            },
+        ])],
+        buffer: hello_world_rust_buffer_with_status_example(theme),
+    }
+}
+
+pub fn hello_world_rust_buffer_with_status_example(theme: &Theme) -> Buffer {
+    Buffer::new()
+        .set_title("hello_world.rs".to_string())
+        .set_path("src/hello_world.rs".to_string())
+        .set_language("rust".to_string())
+        .set_rows(Some(BufferRows {
+            show_line_numbers: true,
+            rows: hello_world_rust_with_status_buffer_rows(theme),
+        }))
+}
+
+pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
+    let show_line_number = true;
+
+    vec![
+        BufferRow {
+            line_number: 1,
+            code_action: false,
+            current: true,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "fn ".to_string(),
+                        color: HighlightColor::Keyword.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "main".to_string(),
+                        color: HighlightColor::Function.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "() {".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 2,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "// Statements here are executed when the compiled binary is called."
+                        .to_string(),
+                    color: HighlightColor::Comment.hsla(&theme),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::Modified,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 3,
+            code_action: false,
+            current: false,
+            line: None,
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 4,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "    // Print text to the console.".to_string(),
+                    color: HighlightColor::Comment.hsla(&theme),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 5,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "    println!(".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "\"Hello, world!\"".to_string(),
+                        color: HighlightColor::String.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: ");".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 6,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "}".to_string(),
+                    color: HighlightColor::Default.hsla(&theme),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 7,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "".to_string(),
+                    color: HighlightColor::Default.hsla(&theme),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::Created,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 8,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "// Marshall and Nate were here".to_string(),
+                    color: HighlightColor::Comment.hsla(&theme),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::Created,
+            show_line_number,
+        },
+    ]
+}
+
+pub fn terminal_buffer(theme: &Theme) -> Buffer {
+    Buffer::new()
+        .set_title("zed β€” fish".to_string())
+        .set_rows(Some(BufferRows {
+            show_line_numbers: false,
+            rows: terminal_buffer_rows(theme),
+        }))
+}
+
+pub fn terminal_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
+    let show_line_number = false;
+
+    vec![
+        BufferRow {
+            line_number: 1,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "maxdeviant ".to_string(),
+                        color: HighlightColor::Keyword.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "in ".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "profaned-capital ".to_string(),
+                        color: HighlightColor::Function.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "in ".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "~/p/zed ".to_string(),
+                        color: HighlightColor::Function.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "on ".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "ξ‚  gpui2-ui ".to_string(),
+                        color: HighlightColor::Keyword.hsla(&theme),
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 2,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "Ξ» ".to_string(),
+                    color: HighlightColor::String.hsla(&theme),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+    ]
+}

crates/storybook/src/theme.rs β†’ crates/ui/src/theme.rs πŸ”—

@@ -1,9 +1,13 @@
-use gpui2::{
-    color::Hsla, element::Element, serde_json, AppContext, IntoElement, Vector2F, ViewContext,
-    WindowContext,
-};
-use serde::{de::Visitor, Deserialize, Deserializer};
-use std::{collections::HashMap, fmt, marker::PhantomData};
+use std::collections::HashMap;
+use std::fmt;
+use std::marker::PhantomData;
+use std::sync::Arc;
+
+use gpui2::color::Hsla;
+use gpui2::element::Element;
+use gpui2::{serde_json, AppContext, IntoElement, Vector2F, ViewContext, WindowContext};
+use serde::de::Visitor;
+use serde::{Deserialize, Deserializer};
 use theme::ThemeSettings;
 
 #[derive(Deserialize, Clone, Default, Debug)]
@@ -187,6 +191,6 @@ fn preferred_theme<V: 'static>(cx: &AppContext) -> Theme {
         .clone()
 }
 
-pub fn theme<'a>(cx: &'a WindowContext) -> &'a Theme {
+pub fn theme(cx: &WindowContext) -> Arc<Theme> {
     cx.theme::<Theme>()
 }

crates/ui/src/tokens.rs πŸ”—

@@ -0,0 +1,25 @@
+use gpui2::geometry::AbsoluteLength;
+use gpui2::{hsla, Hsla};
+
+#[derive(Clone, Copy)]
+pub struct Token {
+    pub list_indent_depth: AbsoluteLength,
+    pub default_panel_size: AbsoluteLength,
+    pub state_hover_background: Hsla,
+    pub state_active_background: Hsla,
+}
+
+impl Default for Token {
+    fn default() -> Self {
+        Self {
+            list_indent_depth: AbsoluteLength::Rems(0.5),
+            default_panel_size: AbsoluteLength::Rems(16.),
+            state_hover_background: hsla(0.0, 0.0, 0.0, 0.08),
+            state_active_background: hsla(0.0, 0.0, 0.0, 0.16),
+        }
+    }
+}
+
+pub fn token() -> Token {
+    Token::default()
+}

crates/ui/tracker.md πŸ”—

@@ -0,0 +1,133 @@
+* = Not in the app today
+
+## Template
+- [ ] Workspace
+- [ ] Title Bar
+- [ ] Project Panel
+- [ ] Collab Panel
+- [ ] Project Diagnosics
+- [ ] Project Search
+- [ ] Feedback Editor
+- [ ] Terminal
+- [ ] Assistant
+- [ ] Chat*
+- [ ] Notifications*
+- [ ] Status Bar
+- [ ] Panes
+- [ ] Pane
+- [ ] Editor
+- [ ] Tab Bar
+- [ ] Tool Bar
+- [ ] Buffer
+- [ ] Zoomed Editor (Modal)
+
+### Palettes
+- [ ] Project Files Palette (⌘-P)
+- [ ] Command Palette (⌘-SHIFT-P)
+- [ ] Recent Projects Palette (⌘-OPT-O)
+- [ ] Recent Branches Palette (⌘-OPT-B)
+- [ ] Project Symbols (⌘-T)
+- [ ] Theme Palette (⌘-K, ⌘-T)
+- [ ] Outline View (⌘-SHIFT-O)
+
+### Debug Views
+- [ ] LSP Tool
+- [ ] Syntax Tree
+
+## Modules
+
+### Title Bar
+- [ ] Traffic Lights
+- [ ] Host Menu
+- [ ] Project Menu
+- [ ] Branch Menu
+- [ ] Collaborators
+- [ ] Add Collaborator*
+- [ ] Project Controls
+- [ ] Call Controls
+- [ ] User Menu
+
+### Project Panel
+- [ ] Open Editors*
+- [ ] Open Files (Non-project files)
+- [ ] Project Files
+- [ ] Root Folder - Context Menu
+- [ ] Folder - Context Menu
+- [ ] File - Context Menu
+- [ ] Project Filter*
+
+### Collab Panel
+- [ ] Current Call
+- [ ] Channels
+- [ ] Channel - Context Menu
+- [ ] Contacts
+- [ ] Collab Filter
+
+### Project Diagnosics
+WIP
+
+### Feedback Editor
+- [ ] Feedback Header
+- [ ] Editor
+- [ ] Feedback Actions
+
+### Terminal
+- [ ] Terminal Toolbar*
+- [ ] Terminal Line
+- [ ] Terminal Input
+
+### Assistant
+- [ ] Toolbar
+- [ ] History / Past Conversations
+- [ ] Model Controls / Token Counter
+- [ ] Chat Editor
+
+### Chat
+WIP
+
+### Notifications
+WIP
+
+### Status Bar
+- [ ] Status Bar Tool (Icon)
+- [ ] Status Bar Tool (Text)
+- [ ] Status Bar Tool - Context Menu
+- [ ] Status Bar Tool - Popover Palette
+- [ ] Status Bar Tool - Popover Menu
+- [ ] Diagnostic Message
+- [ ] LSP Message
+- [ ] Update message (New version available, downloading, etc)
+
+### Panes/Pane
+
+- [ ] Editor
+- [ ] Split Divider/Control
+
+### Editor
+- [ ] Editor
+- [ ] Read-only Editor
+- [ ] Rendered Markdown View*
+
+### Tab Bar
+- [ ] Navigation History / Control
+- [ ] Tabs
+- [ ] Editor Controls (New, Split, Zoom)
+
+### Tool Bar
+- [ ] Breadcrumb
+- [ ] Editor Tool (Togglable)
+- [ ] Buffer Search
+
+### Buffer
+
+### Zoomed Editor (Modal)
+- [ ] Modal View
+
+### Palette
+- [ ] Input
+- [ ] Section Title
+- [ ] List
+
+## Components
+
+- [ ] Context Menu

crates/util/Cargo.toml πŸ”—

@@ -25,10 +25,10 @@ rust-embed.workspace = true
 tempdir = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
-git2 = { version = "0.15", default-features = false, optional = true }
+git2 = { workspace = true, optional = true }
 dirs = "3.0"
 take-until = "0.2.0"
 
 [dev-dependencies]
 tempdir.workspace = true
-git2 = { version = "0.15", default-features = false }
+git2.workspace = true

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

@@ -41,6 +41,8 @@ pub fn truncate(s: &str, max_chars: usize) -> &str {
     }
 }
 
+/// Removes characters from the end of the string if it's length is greater than `max_chars` and
+/// appends "..." to the string. Returns string unchanged if it's length is smaller than max_chars.
 pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {
     debug_assert!(max_chars >= 5);
 
@@ -51,6 +53,18 @@ pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {
     }
 }
 
+/// Removes characters from the front of the string if it's length is greater than `max_chars` and
+/// prepends the string with "...". Returns string unchanged if it's length is smaller than max_chars.
+pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String {
+    debug_assert!(max_chars >= 5);
+
+    let truncation_ix = s.char_indices().map(|(i, _)| i).nth_back(max_chars);
+    match truncation_ix {
+        Some(length) => "…".to_string() + &s[length..],
+        None => s.to_string(),
+    }
+}
+
 pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
     let prev = *value;
     *value += T::from(1);

crates/vim/Cargo.toml πŸ”—

@@ -34,6 +34,8 @@ settings = { path = "../settings" }
 workspace = { path = "../workspace" }
 theme = { path = "../theme" }
 language_selector = { path = "../language_selector"}
+diagnostics = { path = "../diagnostics" }
+zed-actions = { path = "../zed-actions" }
 
 [dev-dependencies]
 indoc.workspace = true

crates/vim/src/command.rs πŸ”—

@@ -0,0 +1,438 @@
+use command_palette::CommandInterceptResult;
+use editor::{SortLinesCaseInsensitive, SortLinesCaseSensitive};
+use gpui::{impl_actions, Action, AppContext};
+use serde_derive::Deserialize;
+use workspace::{SaveIntent, Workspace};
+
+use crate::{
+    motion::{EndOfDocument, Motion},
+    normal::{
+        move_cursor,
+        search::{FindCommand, ReplaceCommand},
+        JoinLines,
+    },
+    state::Mode,
+    Vim,
+};
+
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+pub struct GoToLine {
+    pub line: u32,
+}
+
+impl_actions!(vim, [GoToLine]);
+
+pub fn init(cx: &mut AppContext) {
+    cx.add_action(|_: &mut Workspace, action: &GoToLine, cx| {
+        Vim::update(cx, |vim, cx| {
+            vim.switch_mode(Mode::Normal, false, cx);
+            move_cursor(vim, Motion::StartOfDocument, Some(action.line as usize), cx);
+        });
+    });
+}
+
+pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option<CommandInterceptResult> {
+    // Note: this is a very poor simulation of vim's command palette.
+    // In the future we should adjust it to handle parsing range syntax,
+    // and then calling the appropriate commands with/without ranges.
+    //
+    // We also need to support passing arguments to commands like :w
+    // (ideally with filename autocompletion).
+    //
+    // For now, you can only do a replace on the % range, and you can
+    // only use a specific line number range to "go to line"
+    while query.starts_with(":") {
+        query = &query[1..];
+    }
+
+    let (name, action) = match query {
+        // save and quit
+        "w" | "wr" | "wri" | "writ" | "write" => (
+            "write",
+            workspace::Save {
+                save_intent: Some(SaveIntent::Save),
+            }
+            .boxed_clone(),
+        ),
+        "w!" | "wr!" | "wri!" | "writ!" | "write!" => (
+            "write!",
+            workspace::Save {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "q" | "qu" | "qui" | "quit" => (
+            "quit",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Close),
+            }
+            .boxed_clone(),
+        ),
+        "q!" | "qu!" | "qui!" | "quit!" => (
+            "quit!",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Skip),
+            }
+            .boxed_clone(),
+        ),
+        "wq" => (
+            "wq",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Save),
+            }
+            .boxed_clone(),
+        ),
+        "wq!" => (
+            "wq!",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "x" | "xi" | "xit" | "exi" | "exit" => (
+            "exit",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::SaveAll),
+            }
+            .boxed_clone(),
+        ),
+        "x!" | "xi!" | "xit!" | "exi!" | "exit!" => (
+            "exit!",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "up" | "upd" | "upda" | "updat" | "update" => (
+            "update",
+            workspace::Save {
+                save_intent: Some(SaveIntent::SaveAll),
+            }
+            .boxed_clone(),
+        ),
+        "wa" | "wal" | "wall" => (
+            "wall",
+            workspace::SaveAll {
+                save_intent: Some(SaveIntent::SaveAll),
+            }
+            .boxed_clone(),
+        ),
+        "wa!" | "wal!" | "wall!" => (
+            "wall!",
+            workspace::SaveAll {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "qa" | "qal" | "qall" | "quita" | "quital" | "quitall" => (
+            "quitall",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::Close),
+            }
+            .boxed_clone(),
+        ),
+        "qa!" | "qal!" | "qall!" | "quita!" | "quital!" | "quitall!" => (
+            "quitall!",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::Skip),
+            }
+            .boxed_clone(),
+        ),
+        "xa" | "xal" | "xall" => (
+            "xall",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::SaveAll),
+            }
+            .boxed_clone(),
+        ),
+        "xa!" | "xal!" | "xall!" => (
+            "xall!",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "wqa" | "wqal" | "wqall" => (
+            "wqall",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::SaveAll),
+            }
+            .boxed_clone(),
+        ),
+        "wqa!" | "wqal!" | "wqall!" => (
+            "wqall!",
+            workspace::CloseAllItemsAndPanes {
+                save_intent: Some(SaveIntent::Overwrite),
+            }
+            .boxed_clone(),
+        ),
+        "cq" | "cqu" | "cqui" | "cquit" | "cq!" | "cqu!" | "cqui!" | "cquit!" => {
+            ("cquit!", zed_actions::Quit.boxed_clone())
+        }
+
+        // pane management
+        "sp" | "spl" | "spli" | "split" => ("split", workspace::SplitUp.boxed_clone()),
+        "vs" | "vsp" | "vspl" | "vspli" | "vsplit" => {
+            ("vsplit", workspace::SplitLeft.boxed_clone())
+        }
+        "new" => (
+            "new",
+            workspace::NewFileInDirection(workspace::SplitDirection::Up).boxed_clone(),
+        ),
+        "vne" | "vnew" => (
+            "vnew",
+            workspace::NewFileInDirection(workspace::SplitDirection::Left).boxed_clone(),
+        ),
+        "tabe" | "tabed" | "tabedi" | "tabedit" => ("tabedit", workspace::NewFile.boxed_clone()),
+        "tabnew" => ("tabnew", workspace::NewFile.boxed_clone()),
+
+        "tabn" | "tabne" | "tabnex" | "tabnext" => {
+            ("tabnext", workspace::ActivateNextItem.boxed_clone())
+        }
+        "tabp" | "tabpr" | "tabpre" | "tabprev" | "tabprevi" | "tabprevio" | "tabpreviou"
+        | "tabprevious" => ("tabprevious", workspace::ActivatePrevItem.boxed_clone()),
+        "tabN" | "tabNe" | "tabNex" | "tabNext" => {
+            ("tabNext", workspace::ActivatePrevItem.boxed_clone())
+        }
+        "tabc" | "tabcl" | "tabclo" | "tabclos" | "tabclose" => (
+            "tabclose",
+            workspace::CloseActiveItem {
+                save_intent: Some(SaveIntent::Close),
+            }
+            .boxed_clone(),
+        ),
+
+        // quickfix / loclist (merged together for now)
+        "cl" | "cli" | "clis" | "clist" => ("clist", diagnostics::Deploy.boxed_clone()),
+        "cc" => ("cc", editor::Hover.boxed_clone()),
+        "ll" => ("ll", editor::Hover.boxed_clone()),
+        "cn" | "cne" | "cnex" | "cnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()),
+        "lne" | "lnex" | "lnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()),
+
+        "cpr" | "cpre" | "cprev" | "cprevi" | "cprevio" | "cpreviou" | "cprevious" => {
+            ("cprevious", editor::GoToPrevDiagnostic.boxed_clone())
+        }
+        "cN" | "cNe" | "cNex" | "cNext" => ("cNext", editor::GoToPrevDiagnostic.boxed_clone()),
+        "lp" | "lpr" | "lpre" | "lprev" | "lprevi" | "lprevio" | "lpreviou" | "lprevious" => {
+            ("lprevious", editor::GoToPrevDiagnostic.boxed_clone())
+        }
+        "lN" | "lNe" | "lNex" | "lNext" => ("lNext", editor::GoToPrevDiagnostic.boxed_clone()),
+
+        // modify the buffer (should accept [range])
+        "j" | "jo" | "joi" | "join" => ("join", JoinLines.boxed_clone()),
+        "d" | "de" | "del" | "dele" | "delet" | "delete" | "dl" | "dell" | "delel" | "deletl"
+        | "deletel" | "dp" | "dep" | "delp" | "delep" | "deletp" | "deletep" => {
+            ("delete", editor::DeleteLine.boxed_clone())
+        }
+        "sor" | "sor " | "sort" | "sort " => ("sort", SortLinesCaseSensitive.boxed_clone()),
+        "sor i" | "sort i" => ("sort i", SortLinesCaseInsensitive.boxed_clone()),
+
+        // goto (other ranges handled under _ => )
+        "$" => ("$", EndOfDocument.boxed_clone()),
+
+        _ => {
+            if query.starts_with("/") || query.starts_with("?") {
+                (
+                    query,
+                    FindCommand {
+                        query: query[1..].to_string(),
+                        backwards: query.starts_with("?"),
+                    }
+                    .boxed_clone(),
+                )
+            } else if query.starts_with("%") {
+                (
+                    query,
+                    ReplaceCommand {
+                        query: query.to_string(),
+                    }
+                    .boxed_clone(),
+                )
+            } else if let Ok(line) = query.parse::<u32>() {
+                (query, GoToLine { line }.boxed_clone())
+            } else {
+                return None;
+            }
+        }
+    };
+
+    let string = ":".to_owned() + name;
+    let positions = generate_positions(&string, query);
+
+    Some(CommandInterceptResult {
+        action,
+        string,
+        positions,
+    })
+}
+
+fn generate_positions(string: &str, query: &str) -> Vec<usize> {
+    let mut positions = Vec::new();
+    let mut chars = query.chars().into_iter();
+
+    let Some(mut current) = chars.next() else {
+        return positions;
+    };
+
+    for (i, c) in string.chars().enumerate() {
+        if c == current {
+            positions.push(i);
+            if let Some(c) = chars.next() {
+                current = c;
+            } else {
+                break;
+            }
+        }
+    }
+
+    positions
+}
+
+#[cfg(test)]
+mod test {
+    use std::path::Path;
+
+    use crate::test::{NeovimBackedTestContext, VimTestContext};
+    use gpui::{executor::Foreground, TestAppContext};
+    use indoc::indoc;
+
+    #[gpui::test]
+    async fn test_command_basics(cx: &mut TestAppContext) {
+        if let Foreground::Deterministic { cx_id: _, executor } = cx.foreground().as_ref() {
+            executor.run_until_parked();
+        }
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+            Λ‡a
+            b
+            c"})
+            .await;
+
+        cx.simulate_shared_keystrokes([":", "j", "enter"]).await;
+
+        // hack: our cursor positionining after a join command is wrong
+        cx.simulate_shared_keystrokes(["^"]).await;
+        cx.assert_shared_state(indoc! {
+            "Λ‡a b
+            c"
+        })
+        .await;
+    }
+
+    #[gpui::test]
+    async fn test_command_goto(cx: &mut TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+            Λ‡a
+            b
+            c"})
+            .await;
+        cx.simulate_shared_keystrokes([":", "3", "enter"]).await;
+        cx.assert_shared_state(indoc! {"
+            a
+            b
+            Λ‡c"})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_command_replace(cx: &mut TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+            Λ‡a
+            b
+            c"})
+            .await;
+        cx.simulate_shared_keystrokes([":", "%", "s", "/", "b", "/", "d", "enter"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            a
+            Λ‡d
+            c"})
+            .await;
+        cx.simulate_shared_keystrokes([
+            ":", "%", "s", ":", ".", ":", "\\", "0", "\\", "0", "enter",
+        ])
+        .await;
+        cx.assert_shared_state(indoc! {"
+            aa
+            dd
+            Λ‡cc"})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_command_search(cx: &mut TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+                Λ‡a
+                b
+                a
+                c"})
+            .await;
+        cx.simulate_shared_keystrokes([":", "/", "b", "enter"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+                a
+                Λ‡b
+                a
+                c"})
+            .await;
+        cx.simulate_shared_keystrokes([":", "?", "a", "enter"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+                Λ‡a
+                b
+                a
+                c"})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_command_write(cx: &mut TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+        let path = Path::new("/root/dir/file.rs");
+        let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
+
+        cx.simulate_keystrokes(["i", "@", "escape"]);
+        cx.simulate_keystrokes([":", "w", "enter"]);
+
+        assert_eq!(fs.load(&path).await.unwrap(), "@\n");
+
+        fs.as_fake()
+            .write_file_internal(path, "oops\n".to_string())
+            .unwrap();
+
+        // conflict!
+        cx.simulate_keystrokes(["i", "@", "escape"]);
+        cx.simulate_keystrokes([":", "w", "enter"]);
+        let window = cx.window;
+        assert!(window.has_pending_prompt(cx.cx));
+        // "Cancel"
+        window.simulate_prompt_answer(0, cx.cx);
+        assert_eq!(fs.load(&path).await.unwrap(), "oops\n");
+        assert!(!window.has_pending_prompt(cx.cx));
+        // force overwrite
+        cx.simulate_keystrokes([":", "w", "!", "enter"]);
+        assert!(!window.has_pending_prompt(cx.cx));
+        assert_eq!(fs.load(&path).await.unwrap(), "@@\n");
+    }
+
+    #[gpui::test]
+    async fn test_command_quit(cx: &mut TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.simulate_keystrokes([":", "n", "e", "w", "enter"]);
+        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
+        cx.simulate_keystrokes([":", "q", "enter"]);
+        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1));
+        cx.simulate_keystrokes([":", "n", "e", "w", "enter"]);
+        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
+        cx.simulate_keystrokes([":", "q", "a", "enter"]);
+        cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 0));
+    }
+}

crates/vim/src/editor_events.rs πŸ”—

@@ -35,9 +35,12 @@ fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
     editor.window().update(cx, |cx| {
         Vim::update(cx, |vim, cx| {
             vim.workspace_state.recording = false;
+            vim.workspace_state.recorded_actions.clear();
             if let Some(previous_editor) = vim.active_editor.clone() {
                 if previous_editor == editor.clone() {
+                    vim.clear_operator(cx);
                     vim.active_editor = None;
+                    vim.editor_subscription = None;
                 }
             }
 
@@ -48,13 +51,42 @@ fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
 
 fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) {
     editor.window().update(cx, |cx| {
-        cx.update_default_global(|vim: &mut Vim, _| {
+        Vim::update(cx, |vim, _| {
             if let Some(previous_editor) = vim.active_editor.clone() {
                 if previous_editor == editor.clone() {
                     vim.active_editor = None;
+                    vim.editor_subscription = None;
                 }
             }
             vim.editor_states.remove(&editor.id())
         });
     });
 }
+
+#[cfg(test)]
+mod test {
+    use crate::{test::VimTestContext, Vim};
+    use editor::Editor;
+    use gpui::View;
+    use language::Buffer;
+
+    // regression test for blur called with a different active editor
+    #[gpui::test]
+    async fn test_blur_focus(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        let buffer = cx.add_model(|_| Buffer::new(0, 0, "a = 1\nb = 2\n"));
+        let window2 = cx.add_window(|cx| Editor::for_buffer(buffer, None, cx));
+        let editor2 = cx.read(|cx| window2.root(cx)).unwrap();
+
+        cx.update(|cx| {
+            let vim = Vim::read(cx);
+            assert_eq!(vim.active_editor.unwrap().id(), editor2.id())
+        });
+
+        // no panic when blurring an editor in a different window.
+        cx.update_editor(|editor1, cx| {
+            editor1.focus_out(cx.handle().into_any(), cx);
+        });
+    }
+}

crates/vim/src/insert.rs πŸ”—

@@ -1,6 +1,6 @@
-use crate::{state::Mode, Vim};
+use crate::{normal::repeat, state::Mode, Vim};
 use editor::{scroll::autoscroll::Autoscroll, Bias};
-use gpui::{actions, AppContext, ViewContext};
+use gpui::{actions, Action, AppContext, ViewContext};
 use language::SelectionGoal;
 use workspace::Workspace;
 
@@ -10,24 +10,42 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(normal_before);
 }
 
-fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
-    Vim::update(cx, |vim, cx| {
-        vim.stop_recording();
-        vim.update_active_editor(cx, |editor, cx| {
-            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                s.move_cursors_with(|map, mut cursor, _| {
-                    *cursor.column_mut() = cursor.column().saturating_sub(1);
-                    (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<Workspace>) {
+    let should_repeat = Vim::update(cx, |vim, cx| {
+        let count = vim.take_count(cx).unwrap_or(1);
+        vim.stop_recording_immediately(action.boxed_clone());
+        if count <= 1 || vim.workspace_state.replaying {
+            vim.update_active_editor(cx, |editor, cx| {
+                editor.cancel(&Default::default(), cx);
+                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                    s.move_cursors_with(|map, mut cursor, _| {
+                        *cursor.column_mut() = cursor.column().saturating_sub(1);
+                        (map.clip_point(cursor, Bias::Left), SelectionGoal::None)
+                    });
                 });
             });
-        });
-        vim.switch_mode(Mode::Normal, false, cx);
-    })
+            vim.switch_mode(Mode::Normal, false, cx);
+            false
+        } else {
+            true
+        }
+    });
+
+    if should_repeat {
+        repeat::repeat(cx, true)
+    }
 }
 
 #[cfg(test)]
 mod test {
-    use crate::{state::Mode, test::VimTestContext};
+    use std::sync::Arc;
+
+    use gpui::executor::Deterministic;
+
+    use crate::{
+        state::Mode,
+        test::{NeovimBackedTestContext, VimTestContext},
+    };
 
     #[gpui::test]
     async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
@@ -40,4 +58,78 @@ mod test {
         assert_eq!(cx.mode(), Mode::Normal);
         cx.assert_editor_state("Tesˇt");
     }
+
+    #[gpui::test]
+    async fn test_insert_with_counts(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["5", "i", "-", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("----Λ‡-hello\n").await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["5", "a", "-", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("h----Λ‡-ello\n").await;
+
+        cx.simulate_shared_keystrokes(["4", "shift-i", "-", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("---Λ‡-h-----ello\n").await;
+
+        cx.simulate_shared_keystrokes(["3", "shift-a", "-", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("----h-----ello--Λ‡-\n").await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["3", "o", "o", "i", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("hello\noi\noi\noˇi\n").await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["3", "shift-o", "o", "i", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("oi\noi\noˇi\nhello\n").await;
+    }
+
+    #[gpui::test]
+    async fn test_insert_with_repeat(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["3", "i", "-", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("--Λ‡-hello\n").await;
+        cx.simulate_shared_keystrokes(["."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("----Λ‡--hello\n").await;
+        cx.simulate_shared_keystrokes(["2", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("-----Λ‡---hello\n").await;
+
+        cx.set_shared_state("Λ‡hello\n").await;
+        cx.simulate_shared_keystrokes(["2", "o", "k", "k", "escape"])
+            .await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("hello\nkk\nkˇk\n").await;
+        cx.simulate_shared_keystrokes(["."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("hello\nkk\nkk\nkk\nkˇk\n").await;
+        cx.simulate_shared_keystrokes(["1", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state("hello\nkk\nkk\nkk\nkk\nkˇk\n").await;
+    }
 }

crates/vim/src/motion.rs πŸ”—

@@ -40,6 +40,8 @@ pub enum Motion {
     FindForward { before: bool, char: char },
     FindBackward { after: bool, char: char },
     NextLineStart,
+    StartOfLineDownward,
+    EndOfLineDownward,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -117,6 +119,8 @@ actions!(
         EndOfDocument,
         Matching,
         NextLineStart,
+        StartOfLineDownward,
+        EndOfLineDownward,
     ]
 );
 impl_actions!(
@@ -207,6 +211,12 @@ pub fn init(cx: &mut AppContext) {
          cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
     );
     cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
+    cx.add_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
+        motion(Motion::StartOfLineDownward, cx)
+    });
+    cx.add_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
+        motion(Motion::EndOfLineDownward, cx)
+    });
     cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
         repeat_motion(action.backwards, cx)
     })
@@ -219,11 +229,11 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
         Vim::update(cx, |vim, cx| vim.pop_operator(cx));
     }
 
-    let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
+    let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
     let operator = Vim::read(cx).active_operator();
     match Vim::read(cx).state().mode {
-        Mode::Normal => normal_motion(motion, operator, times, cx),
-        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx),
+        Mode::Normal => normal_motion(motion, operator, count, cx),
+        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
         Mode::Insert => {
             // Shouldn't execute a motion in insert mode. Ignoring
         }
@@ -272,6 +282,7 @@ impl Motion {
             | EndOfDocument
             | CurrentLine
             | NextLineStart
+            | StartOfLineDownward
             | StartOfParagraph
             | EndOfParagraph => true,
             EndOfLine { .. }
@@ -282,6 +293,7 @@ impl Motion {
             | Backspace
             | Right
             | StartOfLine { .. }
+            | EndOfLineDownward
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace { .. }
@@ -305,6 +317,8 @@ impl Motion {
             | StartOfLine { .. }
             | StartOfParagraph
             | EndOfParagraph
+            | StartOfLineDownward
+            | EndOfLineDownward
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace { .. }
@@ -322,6 +336,7 @@ impl Motion {
             | EndOfDocument
             | CurrentLine
             | EndOfLine { .. }
+            | EndOfLineDownward
             | NextWordEnd { .. }
             | Matching
             | FindForward { .. }
@@ -330,6 +345,7 @@ impl Motion {
             | Backspace
             | Right
             | StartOfLine { .. }
+            | StartOfLineDownward
             | StartOfParagraph
             | EndOfParagraph
             | NextWordStart { .. }
@@ -396,7 +412,7 @@ impl Motion {
                 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
                 SelectionGoal::None,
             ),
-            CurrentLine => (end_of_line(map, false, point), SelectionGoal::None),
+            CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
             StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
             EndOfDocument => (
                 end_of_document(map, point, maybe_times),
@@ -412,6 +428,8 @@ impl Motion {
                 SelectionGoal::None,
             ),
             NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
+            StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
+            EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
         };
 
         (new_point != point || infallible).then_some((new_point, goal))
@@ -449,6 +467,22 @@ impl Motion {
 
                 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
             } else {
+                // Another special case: When using the "w" motion in combination with an
+                // operator and the last word moved over is at the end of a line, the end of
+                // that word becomes the end of the operated text, not the first word in the
+                // next line.
+                if let Motion::NextWordStart {
+                    ignore_punctuation: _,
+                } = self
+                {
+                    let start_row = selection.start.to_point(&map).row;
+                    if selection.end.to_point(&map).row > start_row {
+                        selection.end =
+                            Point::new(start_row, map.buffer_snapshot.line_len(start_row))
+                                .to_display_point(&map)
+                    }
+                }
+
                 // If the motion is exclusive and the end of the motion is in column 1, the
                 // end of the motion is moved to the end of the previous line and the motion
                 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
@@ -515,11 +549,15 @@ fn down(
 
     let new_row = cmp::min(
         start.row() + times as u32,
-        map.buffer_snapshot.max_point().row,
+        map.fold_snapshot.max_point().row(),
     );
     let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
-    let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col));
+    let point = map.fold_point_to_display_point(
+        map.fold_snapshot
+            .clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
+    );
 
+    // clip twice to "clip at end of line"
     (map.clip_point(point, Bias::Left), goal)
 }
 
@@ -555,7 +593,10 @@ pub(crate) fn up(
 
     let new_row = start.row().saturating_sub(times as u32);
     let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row));
-    let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col));
+    let point = map.fold_point_to_display_point(
+        map.fold_snapshot
+            .clip_point(FoldPoint::new(new_row, new_col), Bias::Left),
+    );
 
     (map.clip_point(point, Bias::Left), goal)
 }
@@ -849,6 +890,13 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
     first_non_whitespace(map, false, correct_line)
 }
 
+fn next_line_end(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
+    if times > 1 {
+        point = down(map, point, SelectionGoal::None, times - 1).0;
+    }
+    end_of_line(map, false, point)
+}
+
 #[cfg(test)]
 
 mod test {

crates/vim/src/normal.rs πŸ”—

@@ -1,10 +1,11 @@
 mod case;
 mod change;
 mod delete;
+mod increment;
 mod paste;
-mod repeat;
+pub(crate) mod repeat;
 mod scroll;
-mod search;
+pub(crate) mod search;
 pub mod substitute;
 mod yank;
 
@@ -56,6 +57,7 @@ pub fn init(cx: &mut AppContext) {
     scroll::init(cx);
     search::init(cx);
     substitute::init(cx);
+    increment::init(cx);
 
     cx.add_action(insert_after);
     cx.add_action(insert_before);
@@ -68,21 +70,21 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
         Vim::update(cx, |vim, cx| {
             vim.record_current_action(cx);
-            let times = vim.pop_number_operator(cx);
+            let times = vim.take_count(cx);
             delete_motion(vim, Motion::Left, times, cx);
         })
     });
     cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
         Vim::update(cx, |vim, cx| {
             vim.record_current_action(cx);
-            let times = vim.pop_number_operator(cx);
+            let times = vim.take_count(cx);
             delete_motion(vim, Motion::Right, times, cx);
         })
     });
     cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
         Vim::update(cx, |vim, cx| {
             vim.start_recording(cx);
-            let times = vim.pop_number_operator(cx);
+            let times = vim.take_count(cx);
             change_motion(
                 vim,
                 Motion::EndOfLine {
@@ -96,7 +98,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
         Vim::update(cx, |vim, cx| {
             vim.record_current_action(cx);
-            let times = vim.pop_number_operator(cx);
+            let times = vim.take_count(cx);
             delete_motion(
                 vim,
                 Motion::EndOfLine {
@@ -110,7 +112,7 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| {
         Vim::update(cx, |vim, cx| {
             vim.record_current_action(cx);
-            let mut times = vim.pop_number_operator(cx).unwrap_or(1);
+            let mut times = vim.take_count(cx).unwrap_or(1);
             if vim.state().mode.is_visual() {
                 times = 1;
             } else if times > 1 {
@@ -168,7 +170,12 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) {
     })
 }
 
-fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
+pub(crate) 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| {
@@ -356,7 +363,7 @@ mod test {
 
     use crate::{
         state::Mode::{self},
-        test::{ExemptionFeatures, NeovimBackedTestContext},
+        test::NeovimBackedTestContext,
     };
 
     #[gpui::test]
@@ -762,20 +769,22 @@ mod test {
 
     #[gpui::test]
     async fn test_dd(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "d"]);
-        cx.assert("Λ‡").await;
-        cx.assert("The Λ‡quick").await;
-        cx.assert_all(indoc! {"
-                The qˇuick
-                brown Λ‡fox
-                jumps Λ‡over"})
-            .await;
-        cx.assert_exempted(
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.assert_neovim_compatible("Λ‡", ["d", "d"]).await;
+        cx.assert_neovim_compatible("The Λ‡quick", ["d", "d"]).await;
+        for marked_text in cx.each_marked_position(indoc! {"
+            The qˇuick
+            brown Λ‡fox
+            jumps Λ‡over"})
+        {
+            cx.assert_neovim_compatible(&marked_text, ["d", "d"]).await;
+        }
+        cx.assert_neovim_compatible(
             indoc! {"
                 The quick
                 Λ‡
                 brown fox"},
-            ExemptionFeatures::DeletionOnEmptyLine,
+            ["d", "d"],
         )
         .await;
     }

crates/vim/src/normal/case.rs πŸ”—

@@ -8,7 +8,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim};
 pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
     Vim::update(cx, |vim, cx| {
         vim.record_current_action(cx);
-        let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
+        let count = vim.take_count(cx).unwrap_or(1) as u32;
         vim.update_active_editor(cx, |editor, cx| {
             let mut ranges = Vec::new();
             let mut cursor_positions = Vec::new();

crates/vim/src/normal/change.rs πŸ”—

@@ -76,12 +76,6 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
 // word does not include the following white space.  {Vi: "cw" when on a blank
 //     followed by other blanks changes only the first blank; this is probably a
 //     bug, because "dw" deletes all the blanks}
-//
-// NOT HANDLED YET
-// Another special case: When using the "w" motion in combination with an
-// operator and the last word moved over is at the end of a line, the end of
-// that word becomes the end of the operated text, not the first word in the
-// next line.
 fn expand_changed_word_selection(
     map: &DisplaySnapshot,
     selection: &mut Selection<DisplayPoint>,
@@ -121,7 +115,7 @@ fn expand_changed_word_selection(
 mod test {
     use indoc::indoc;
 
-    use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
+    use crate::test::NeovimBackedTestContext;
 
     #[gpui::test]
     async fn test_change_h(cx: &mut gpui::TestAppContext) {
@@ -239,150 +233,178 @@ mod test {
 
     #[gpui::test]
     async fn test_change_0(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "0"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.assert_neovim_compatible(
+            indoc! {"
             The qˇuick
-            brown fox"})
-            .await;
-        cx.assert(indoc! {"
+            brown fox"},
+            ["c", "0"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             Λ‡
-            brown fox"})
-            .await;
+            brown fox"},
+            ["c", "0"],
+        )
+        .await;
     }
 
     #[gpui::test]
     async fn test_change_k(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "k"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brown Λ‡fox
-            jumps over"})
-            .await;
-        cx.assert(indoc! {"
+            jumps over"},
+            ["c", "k"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brown fox
-            jumps Λ‡over"})
-            .await;
-        cx.assert_exempted(
+            jumps Λ‡over"},
+            ["c", "k"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The qˇuick
             brown fox
             jumps over"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "k"],
         )
         .await;
-        cx.assert_exempted(
+        cx.assert_neovim_compatible(
             indoc! {"
             Λ‡
             brown fox
             jumps over"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "k"],
         )
         .await;
     }
 
     #[gpui::test]
     async fn test_change_j(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "j"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brown Λ‡fox
-            jumps over"})
-            .await;
-        cx.assert_exempted(
+            jumps over"},
+            ["c", "j"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             jumps Λ‡over"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "j"],
         )
         .await;
-        cx.assert(indoc! {"
+        cx.assert_neovim_compatible(
+            indoc! {"
             The qˇuick
             brown fox
-            jumps over"})
-            .await;
-        cx.assert_exempted(
+            jumps over"},
+            ["c", "j"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             Λ‡"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "j"],
         )
         .await;
     }
 
     #[gpui::test]
     async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx)
-            .await
-            .binding(["c", "shift-g"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert(indoc! {"
+            the lazy"},
+            ["c", "shift-g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert_exempted(
+            the lazy"},
+            ["c", "shift-g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             jumps over
             the lˇazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "shift-g"],
         )
         .await;
-        cx.assert_exempted(
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             jumps over
             Λ‡"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "shift-g"],
         )
         .await;
     }
 
     #[gpui::test]
     async fn test_change_gg(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx)
-            .await
-            .binding(["c", "g", "g"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert(indoc! {"
+            the lazy"},
+            ["c", "g", "g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brown fox
             jumps over
-            the lˇazy"})
-            .await;
-        cx.assert_exempted(
+            the lˇazy"},
+            ["c", "g", "g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The qˇuick
             brown fox
             jumps over
             the lazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "g", "g"],
         )
         .await;
-        cx.assert_exempted(
+        cx.assert_neovim_compatible(
             indoc! {"
             Λ‡
             brown fox
             jumps over
             the lazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["c", "g", "g"],
         )
         .await;
     }
@@ -427,27 +449,17 @@ mod test {
     async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
-        cx.add_initial_state_exemptions(
-            indoc! {"
-            Λ‡The quick brown
-
-            fox jumps-over
-            the lazy dog
-            "},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
-        );
-
         for count in 1..=5 {
-            cx.assert_binding_matches_all(
-                ["c", &count.to_string(), "b"],
-                indoc! {"
-                    ˇThe quˇickˇ browˇn
-                    Λ‡
-                    ˇfox ˇjumpsˇ-ˇoˇver
-                    Λ‡the lazy dog
-                    "},
-            )
-            .await;
+            for marked_text in cx.each_marked_position(indoc! {"
+                ˇThe quˇickˇ browˇn
+                Λ‡
+                ˇfox ˇjumpsˇ-ˇoˇver
+                Λ‡the lazy dog
+                "})
+            {
+                cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"])
+                    .await;
+            }
         }
     }
 

crates/vim/src/normal/delete.rs πŸ”—

@@ -2,6 +2,7 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}
 use collections::{HashMap, HashSet};
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
 use gpui::WindowContext;
+use language::Point;
 
 pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
     vim.stop_recording();
@@ -14,6 +15,27 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
                     let original_head = selection.head();
                     original_columns.insert(selection.id, original_head.column());
                     motion.expand_selection(map, selection, times, true);
+
+                    // Motion::NextWordStart on an empty line should delete it.
+                    if let Motion::NextWordStart {
+                        ignore_punctuation: _,
+                    } = motion
+                    {
+                        if selection.is_empty()
+                            && map
+                                .buffer_snapshot
+                                .line_len(selection.start.to_point(&map).row)
+                                == 0
+                        {
+                            selection.end = map
+                                .buffer_snapshot
+                                .clip_point(
+                                    Point::new(selection.start.to_point(&map).row + 1, 0),
+                                    Bias::Left,
+                                )
+                                .to_display_point(map)
+                        }
+                    }
                 });
             });
             copy_selections_content(editor, motion.linewise(), cx);
@@ -129,28 +151,44 @@ mod test {
 
     #[gpui::test]
     async fn test_delete_w(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "w"]);
-        cx.assert("Teˇst").await;
-        cx.assert("Tˇest test").await;
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.assert_neovim_compatible(
+            indoc! {"
+            Test tesˇt
+                test"},
+            ["d", "w"],
+        )
+        .await;
+
+        cx.assert_neovim_compatible("Teˇst", ["d", "w"]).await;
+        cx.assert_neovim_compatible("Tˇest test", ["d", "w"]).await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             Test teˇst
-            test"})
-            .await;
-        cx.assert(indoc! {"
+            test"},
+            ["d", "w"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             Test tesˇt
-            test"})
-            .await;
-        cx.assert_exempted(
+            test"},
+            ["d", "w"],
+        )
+        .await;
+
+        cx.assert_neovim_compatible(
             indoc! {"
             Test test
             Λ‡
             test"},
-            ExemptionFeatures::DeleteWordOnEmptyLine,
+            ["d", "w"],
         )
         .await;
 
         let mut cx = cx.binding(["d", "shift-w"]);
-        cx.assert("Test teˇst-test test").await;
+        cx.assert_neovim_compatible("Test teˇst-test test", ["d", "shift-w"])
+            .await;
     }
 
     #[gpui::test]
@@ -278,37 +316,41 @@ mod test {
 
     #[gpui::test]
     async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx)
-            .await
-            .binding(["d", "shift-g"]);
-        cx.assert(indoc! {"
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert(indoc! {"
+            the lazy"},
+            ["d", "shift-g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert_exempted(
+            the lazy"},
+            ["d", "shift-g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             jumps over
             the lˇazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["d", "shift-g"],
         )
         .await;
-        cx.assert_exempted(
+        cx.assert_neovim_compatible(
             indoc! {"
             The quick
             brown fox
             jumps over
             Λ‡"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["d", "shift-g"],
         )
         .await;
     }
@@ -318,34 +360,40 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx)
             .await
             .binding(["d", "g", "g"]);
-        cx.assert(indoc! {"
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brownˇ fox
             jumps over
-            the lazy"})
-            .await;
-        cx.assert(indoc! {"
+            the lazy"},
+            ["d", "g", "g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
+            indoc! {"
             The quick
             brown fox
             jumps over
-            the lˇazy"})
-            .await;
-        cx.assert_exempted(
+            the lˇazy"},
+            ["d", "g", "g"],
+        )
+        .await;
+        cx.assert_neovim_compatible(
             indoc! {"
             The qˇuick
             brown fox
             jumps over
             the lazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["d", "g", "g"],
         )
         .await;
-        cx.assert_exempted(
+        cx.assert_neovim_compatible(
             indoc! {"
             Λ‡
             brown fox
             jumps over
             the lazy"},
-            ExemptionFeatures::OperatorAbortsOnFailedMotion,
+            ["d", "g", "g"],
         )
         .await;
     }
@@ -387,4 +435,40 @@ mod test {
         assert_eq!(cx.active_operator(), None);
         assert_eq!(cx.mode(), Mode::Normal);
     }
+
+    #[gpui::test]
+    async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state(indoc! {"
+                The Λ‡quick brown
+                fox jumps over
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["d", "2", "d"]).await;
+        cx.assert_shared_state(indoc! {"
+        the Λ‡lazy dog"})
+            .await;
+
+        cx.set_shared_state(indoc! {"
+                The Λ‡quick brown
+                fox jumps over
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["2", "d", "d"]).await;
+        cx.assert_shared_state(indoc! {"
+        the Λ‡lazy dog"})
+            .await;
+
+        cx.set_shared_state(indoc! {"
+                The Λ‡quick brown
+                fox jumps over
+                the moon,
+                a star, and
+                the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["2", "d", "2", "d"]).await;
+        cx.assert_shared_state(indoc! {"
+        the Λ‡lazy dog"})
+            .await;
+    }
 }

crates/vim/src/normal/increment.rs πŸ”—

@@ -0,0 +1,268 @@
+use std::ops::Range;
+
+use editor::{scroll::autoscroll::Autoscroll, MultiBufferSnapshot, ToOffset, ToPoint};
+use gpui::{impl_actions, AppContext, WindowContext};
+use language::{Bias, Point};
+use serde::Deserialize;
+use workspace::Workspace;
+
+use crate::{state::Mode, Vim};
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct Increment {
+    #[serde(default)]
+    step: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+struct Decrement {
+    #[serde(default)]
+    step: bool,
+}
+
+impl_actions!(vim, [Increment, Decrement]);
+
+pub fn init(cx: &mut AppContext) {
+    cx.add_action(|_: &mut Workspace, action: &Increment, cx| {
+        Vim::update(cx, |vim, cx| {
+            vim.record_current_action(cx);
+            let count = vim.take_count(cx).unwrap_or(1);
+            let step = if action.step { 1 } else { 0 };
+            increment(vim, count as i32, step, cx)
+        })
+    });
+    cx.add_action(|_: &mut Workspace, action: &Decrement, cx| {
+        Vim::update(cx, |vim, cx| {
+            vim.record_current_action(cx);
+            let count = vim.take_count(cx).unwrap_or(1);
+            let step = if action.step { -1 } else { 0 };
+            increment(vim, count as i32 * -1, step, cx)
+        })
+    });
+}
+
+fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
+    vim.update_active_editor(cx, |editor, cx| {
+        let mut edits = Vec::new();
+        let mut new_anchors = Vec::new();
+
+        let snapshot = editor.buffer().read(cx).snapshot(cx);
+        for selection in editor.selections.all_adjusted(cx) {
+            if !selection.is_empty() {
+                if vim.state().mode != Mode::VisualBlock || new_anchors.is_empty() {
+                    new_anchors.push((true, snapshot.anchor_before(selection.start)))
+                }
+            }
+            for row in selection.start.row..=selection.end.row {
+                let start = if row == selection.start.row {
+                    selection.start
+                } else {
+                    Point::new(row, 0)
+                };
+
+                if let Some((range, num, radix)) = find_number(&snapshot, start) {
+                    if let Ok(val) = i32::from_str_radix(&num, radix) {
+                        let result = val + delta;
+                        delta += step;
+                        let replace = match radix {
+                            10 => format!("{}", result),
+                            16 => {
+                                if num.to_ascii_lowercase() == num {
+                                    format!("{:x}", result)
+                                } else {
+                                    format!("{:X}", result)
+                                }
+                            }
+                            2 => format!("{:b}", result),
+                            _ => unreachable!(),
+                        };
+                        edits.push((range.clone(), replace));
+                    }
+                    if selection.is_empty() {
+                        new_anchors.push((false, snapshot.anchor_after(range.end)))
+                    }
+                } else {
+                    if selection.is_empty() {
+                        new_anchors.push((true, snapshot.anchor_after(start)))
+                    }
+                }
+            }
+        }
+        editor.transact(cx, |editor, cx| {
+            editor.edit(edits, cx);
+
+            let snapshot = editor.buffer().read(cx).snapshot(cx);
+            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+                let mut new_ranges = Vec::new();
+                for (visual, anchor) in new_anchors.iter() {
+                    let mut point = anchor.to_point(&snapshot);
+                    if !*visual && point.column > 0 {
+                        point.column -= 1;
+                        point = snapshot.clip_point(point, Bias::Left)
+                    }
+                    new_ranges.push(point..point);
+                }
+                s.select_ranges(new_ranges)
+            })
+        });
+    });
+    vim.switch_mode(Mode::Normal, true, cx)
+}
+
+fn find_number(
+    snapshot: &MultiBufferSnapshot,
+    start: Point,
+) -> Option<(Range<Point>, String, u32)> {
+    let mut offset = start.to_offset(snapshot);
+
+    // go backwards to the start of any number the selection is within
+    for ch in snapshot.reversed_chars_at(offset) {
+        if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' {
+            offset -= ch.len_utf8();
+            continue;
+        }
+        break;
+    }
+
+    let mut begin = None;
+    let mut end = None;
+    let mut num = String::new();
+    let mut radix = 10;
+
+    let mut chars = snapshot.chars_at(offset).peekable();
+    // find the next number on the line (may start after the original cursor position)
+    while let Some(ch) = chars.next() {
+        if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) {
+            radix = 2;
+            begin = None;
+            num = String::new();
+        }
+        if num == "0" && ch == 'x' && chars.peek().is_some() && chars.peek().unwrap().is_digit(16) {
+            radix = 16;
+            begin = None;
+            num = String::new();
+        }
+
+        if ch.is_digit(radix)
+            || (begin.is_none()
+                && ch == '-'
+                && chars.peek().is_some()
+                && chars.peek().unwrap().is_digit(radix))
+        {
+            if begin.is_none() {
+                begin = Some(offset);
+            }
+            num.push(ch);
+        } else {
+            if begin.is_some() {
+                end = Some(offset);
+                break;
+            } else if ch == '\n' {
+                break;
+            }
+        }
+        offset += ch.len_utf8();
+    }
+    if let Some(begin) = begin {
+        let end = end.unwrap_or(offset);
+        Some((begin.to_point(snapshot)..end.to_point(snapshot), num, radix))
+    } else {
+        None
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use indoc::indoc;
+
+    use crate::test::NeovimBackedTestContext;
+
+    #[gpui::test]
+    async fn test_increment(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+            1Λ‡2
+            "})
+            .await;
+
+        cx.simulate_shared_keystrokes(["ctrl-a"]).await;
+        cx.assert_shared_state(indoc! {"
+            1Λ‡3
+            "})
+            .await;
+        cx.simulate_shared_keystrokes(["ctrl-x"]).await;
+        cx.assert_shared_state(indoc! {"
+            1Λ‡2
+            "})
+            .await;
+
+        cx.simulate_shared_keystrokes(["9", "9", "ctrl-a"]).await;
+        cx.assert_shared_state(indoc! {"
+            11Λ‡1
+            "})
+            .await;
+        cx.simulate_shared_keystrokes(["1", "1", "1", "ctrl-x"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            Λ‡0
+            "})
+            .await;
+        cx.simulate_shared_keystrokes(["."]).await;
+        cx.assert_shared_state(indoc! {"
+            -11Λ‡1
+            "})
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.assert_matches_neovim("Λ‡ total: 0xff", ["ctrl-a"], " total: 0x10Λ‡0")
+            .await;
+        cx.assert_matches_neovim("ˇ total: 0xff", ["ctrl-x"], " total: 0xfˇe")
+            .await;
+        cx.assert_matches_neovim("ˇ total: 0xFF", ["ctrl-x"], " total: 0xFˇE")
+            .await;
+        cx.assert_matches_neovim("(Λ‡0b10f)", ["ctrl-a"], "(0b1Λ‡1f)")
+            .await;
+        cx.assert_matches_neovim("Λ‡-1", ["ctrl-a"], "Λ‡0").await;
+        cx.assert_matches_neovim("banˇana", ["ctrl-a"], "banˇana")
+            .await;
+    }
+
+    #[gpui::test]
+    async fn test_increment_steps(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {"
+            Λ‡1
+            1
+            1  2
+            1
+            1"})
+            .await;
+
+        cx.simulate_shared_keystrokes(["j", "v", "shift-g", "g", "ctrl-a"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            1
+            Λ‡2
+            3  2
+            4
+            5"})
+            .await;
+        cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g", "g", "ctrl-x"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+            Λ‡0
+            0
+            0  2
+            0
+            0"})
+            .await;
+    }
+}

crates/vim/src/normal/repeat.rs πŸ”—

@@ -1,10 +1,11 @@
 use crate::{
+    insert::NormalBefore,
     motion::Motion,
     state::{Mode, RecordedSelection, ReplayableAction},
     visual::visual_motion,
     Vim,
 };
-use gpui::{actions, Action, AppContext};
+use gpui::{actions, Action, AppContext, WindowContext};
 use workspace::Workspace;
 
 actions!(vim, [Repeat, EndRepeat,]);
@@ -17,138 +18,187 @@ fn should_replay(action: &Box<dyn Action>) -> bool {
     true
 }
 
+fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
+    match action {
+        ReplayableAction::Action(action) => {
+            if super::InsertBefore.id() == action.id()
+                || super::InsertAfter.id() == action.id()
+                || super::InsertFirstNonWhitespace.id() == action.id()
+                || super::InsertEndOfLine.id() == action.id()
+            {
+                Some(super::InsertBefore.boxed_clone())
+            } else if super::InsertLineAbove.id() == action.id()
+                || super::InsertLineBelow.id() == action.id()
+            {
+                Some(super::InsertLineBelow.boxed_clone())
+            } else {
+                None
+            }
+        }
+        ReplayableAction::Insertion { .. } => None,
+    }
+}
+
 pub(crate) fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
         Vim::update(cx, |vim, cx| {
             vim.workspace_state.replaying = false;
-            vim.update_active_editor(cx, |editor, _| {
-                editor.show_local_selections = true;
-            });
             vim.switch_mode(Mode::Normal, false, cx)
         });
     });
 
-    cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
-        let Some((actions, editor, selection)) = Vim::update(cx, |vim, cx| {
-            let actions = vim.workspace_state.recorded_actions.clone();
-            let Some(editor) = vim.active_editor.clone() else {
-                return None;
-            };
-            let count = vim.pop_number_operator(cx);
-
-            vim.workspace_state.replaying = true;
-
-            let selection = vim.workspace_state.recorded_selection.clone();
-            match selection {
-                RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
-                    vim.workspace_state.recorded_count = None;
-                    vim.switch_mode(Mode::Visual, false, cx)
-                }
-                RecordedSelection::VisualLine { .. } => {
-                    vim.workspace_state.recorded_count = None;
-                    vim.switch_mode(Mode::VisualLine, false, cx)
-                }
-                RecordedSelection::VisualBlock { .. } => {
-                    vim.workspace_state.recorded_count = None;
-                    vim.switch_mode(Mode::VisualBlock, false, cx)
-                }
-                RecordedSelection::None => {
-                    if let Some(count) = count {
-                        vim.workspace_state.recorded_count = Some(count);
-                    }
-                }
-            }
+    cx.add_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
+}
 
-            if let Some(editor) = editor.upgrade(cx) {
-                editor.update(cx, |editor, _| {
-                    editor.show_local_selections = false;
-                })
-            } else {
-                return None;
-            }
+pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
+    let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| {
+        let actions = vim.workspace_state.recorded_actions.clone();
+        if actions.is_empty() {
+            return None;
+        }
 
-            Some((actions, editor, selection))
-        }) else {
-            return;
+        let Some(editor) = vim.active_editor.clone() else {
+            return None;
         };
+        let count = vim.take_count(cx);
 
+        let selection = vim.workspace_state.recorded_selection.clone();
         match selection {
-            RecordedSelection::SingleLine { cols } => {
-                if cols > 1 {
-                    visual_motion(Motion::Right, Some(cols as usize - 1), cx)
-                }
+            RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
+                vim.workspace_state.recorded_count = None;
+                vim.switch_mode(Mode::Visual, false, cx)
             }
-            RecordedSelection::Visual { rows, cols } => {
-                visual_motion(
-                    Motion::Down {
-                        display_lines: false,
-                    },
-                    Some(rows as usize),
-                    cx,
-                );
-                visual_motion(
-                    Motion::StartOfLine {
-                        display_lines: false,
-                    },
-                    None,
-                    cx,
-                );
-                if cols > 1 {
-                    visual_motion(Motion::Right, Some(cols as usize - 1), cx)
-                }
+            RecordedSelection::VisualLine { .. } => {
+                vim.workspace_state.recorded_count = None;
+                vim.switch_mode(Mode::VisualLine, false, cx)
             }
-            RecordedSelection::VisualBlock { rows, cols } => {
-                visual_motion(
-                    Motion::Down {
-                        display_lines: false,
-                    },
-                    Some(rows as usize),
-                    cx,
-                );
-                if cols > 1 {
-                    visual_motion(Motion::Right, Some(cols as usize - 1), cx);
+            RecordedSelection::VisualBlock { .. } => {
+                vim.workspace_state.recorded_count = None;
+                vim.switch_mode(Mode::VisualBlock, false, cx)
+            }
+            RecordedSelection::None => {
+                if let Some(count) = count {
+                    vim.workspace_state.recorded_count = Some(count);
                 }
             }
-            RecordedSelection::VisualLine { rows } => {
-                visual_motion(
-                    Motion::Down {
-                        display_lines: false,
-                    },
-                    Some(rows as usize),
-                    cx,
-                );
+        }
+
+        Some((actions, editor, selection))
+    }) else {
+        return;
+    };
+
+    match selection {
+        RecordedSelection::SingleLine { cols } => {
+            if cols > 1 {
+                visual_motion(Motion::Right, Some(cols as usize - 1), cx)
+            }
+        }
+        RecordedSelection::Visual { rows, cols } => {
+            visual_motion(
+                Motion::Down {
+                    display_lines: false,
+                },
+                Some(rows as usize),
+                cx,
+            );
+            visual_motion(
+                Motion::StartOfLine {
+                    display_lines: false,
+                },
+                None,
+                cx,
+            );
+            if cols > 1 {
+                visual_motion(Motion::Right, Some(cols as usize - 1), cx)
+            }
+        }
+        RecordedSelection::VisualBlock { rows, cols } => {
+            visual_motion(
+                Motion::Down {
+                    display_lines: false,
+                },
+                Some(rows as usize),
+                cx,
+            );
+            if cols > 1 {
+                visual_motion(Motion::Right, Some(cols as usize - 1), cx);
+            }
+        }
+        RecordedSelection::VisualLine { rows } => {
+            visual_motion(
+                Motion::Down {
+                    display_lines: false,
+                },
+                Some(rows as usize),
+                cx,
+            );
+        }
+        RecordedSelection::None => {}
+    }
+
+    // insert internally uses repeat to handle counts
+    // vim doesn't treat 3a1 as though you literally repeated a1
+    // 3 times, instead it inserts the content thrice at the insert position.
+    if let Some(to_repeat) = repeatable_insert(&actions[0]) {
+        if let Some(ReplayableAction::Action(action)) = actions.last() {
+            if action.id() == NormalBefore.id() {
+                actions.pop();
             }
-            RecordedSelection::None => {}
         }
 
-        let window = cx.window();
-        cx.app_context()
-            .spawn(move |mut cx| async move {
-                for action in actions {
-                    match action {
-                        ReplayableAction::Action(action) => {
-                            if should_replay(&action) {
-                                window
-                                    .dispatch_action(editor.id(), action.as_ref(), &mut cx)
-                                    .ok_or_else(|| anyhow::anyhow!("window was closed"))
-                            } else {
-                                Ok(())
-                            }
+        let mut new_actions = actions.clone();
+        actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
+
+        let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1);
+
+        // if we came from insert mode we're just doing repititions 2 onwards.
+        if from_insert_mode {
+            count -= 1;
+            new_actions[0] = actions[0].clone();
+        }
+
+        for _ in 1..count {
+            new_actions.append(actions.clone().as_mut());
+        }
+        new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
+        actions = new_actions;
+    }
+
+    Vim::update(cx, |vim, _| vim.workspace_state.replaying = true);
+    let window = cx.window();
+    cx.app_context()
+        .spawn(move |mut cx| async move {
+            editor.update(&mut cx, |editor, _| {
+                editor.show_local_selections = false;
+            })?;
+            for action in actions {
+                match action {
+                    ReplayableAction::Action(action) => {
+                        if should_replay(&action) {
+                            window
+                                .dispatch_action(editor.id(), action.as_ref(), &mut cx)
+                                .ok_or_else(|| anyhow::anyhow!("window was closed"))
+                        } else {
+                            Ok(())
                         }
-                        ReplayableAction::Insertion {
-                            text,
-                            utf16_range_to_replace,
-                        } => editor.update(&mut cx, |editor, cx| {
-                            editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
-                        }),
-                    }?
-                }
-                window
-                    .dispatch_action(editor.id(), &EndRepeat, &mut cx)
-                    .ok_or_else(|| anyhow::anyhow!("window was closed"))
-            })
-            .detach_and_log_err(cx);
-    });
+                    }
+                    ReplayableAction::Insertion {
+                        text,
+                        utf16_range_to_replace,
+                    } => editor.update(&mut cx, |editor, cx| {
+                        editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
+                    }),
+                }?
+            }
+            editor.update(&mut cx, |editor, _| {
+                editor.show_local_selections = true;
+            })?;
+            window
+                .dispatch_action(editor.id(), &EndRepeat, &mut cx)
+                .ok_or_else(|| anyhow::anyhow!("window was closed"))
+        })
+        .detach_and_log_err(cx);
 }
 
 #[cfg(test)]
@@ -203,7 +253,7 @@ mod test {
         deterministic.run_until_parked();
         cx.simulate_shared_keystrokes(["."]).await;
         deterministic.run_until_parked();
-        cx.set_shared_state("THE QUICK Λ‡brown fox").await;
+        cx.assert_shared_state("THE QUICK Λ‡brown fox").await;
     }
 
     #[gpui::test]
@@ -424,4 +474,55 @@ mod test {
         })
         .await;
     }
+
+    #[gpui::test]
+    async fn test_repeat_motion_counts(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state(indoc! {
+            "Λ‡the quick brown
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["3", "d", "3", "l"]).await;
+        cx.assert_shared_state(indoc! {
+            "Λ‡ brown
+            fox jumps over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["j", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state(indoc! {
+            " brown
+            Λ‡ over
+            the lazy dog"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["j", "2", "."]).await;
+        deterministic.run_until_parked();
+        cx.assert_shared_state(indoc! {
+            " brown
+             over
+            Λ‡e lazy dog"
+        })
+        .await;
+    }
+
+    #[gpui::test]
+    async fn test_record_interrupted(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        cx.set_state("Λ‡hello\n", Mode::Normal);
+        cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape", "escape"]);
+        deterministic.run_until_parked();
+        cx.assert_state("Λ‡jhello\n", Mode::Normal);
+    }
 }

crates/vim/src/normal/scroll.rs πŸ”—

@@ -15,19 +15,19 @@ actions!(
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &LineDown, cx| {
-        scroll(cx, |c| ScrollAmount::Line(c.unwrap_or(1.)))
+        scroll(cx, false, |c| ScrollAmount::Line(c.unwrap_or(1.)))
     });
     cx.add_action(|_: &mut Workspace, _: &LineUp, cx| {
-        scroll(cx, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
+        scroll(cx, false, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
     });
     cx.add_action(|_: &mut Workspace, _: &PageDown, cx| {
-        scroll(cx, |c| ScrollAmount::Page(c.unwrap_or(1.)))
+        scroll(cx, false, |c| ScrollAmount::Page(c.unwrap_or(1.)))
     });
     cx.add_action(|_: &mut Workspace, _: &PageUp, cx| {
-        scroll(cx, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
+        scroll(cx, false, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
     });
     cx.add_action(|_: &mut Workspace, _: &ScrollDown, cx| {
-        scroll(cx, |c| {
+        scroll(cx, true, |c| {
             if let Some(c) = c {
                 ScrollAmount::Line(c)
             } else {
@@ -36,7 +36,7 @@ pub fn init(cx: &mut AppContext) {
         })
     });
     cx.add_action(|_: &mut Workspace, _: &ScrollUp, cx| {
-        scroll(cx, |c| {
+        scroll(cx, true, |c| {
             if let Some(c) = c {
                 ScrollAmount::Line(-c)
             } else {
@@ -46,15 +46,27 @@ pub fn init(cx: &mut AppContext) {
     });
 }
 
-fn scroll(cx: &mut ViewContext<Workspace>, by: fn(c: Option<f32>) -> ScrollAmount) {
+fn scroll(
+    cx: &mut ViewContext<Workspace>,
+    move_cursor: bool,
+    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));
+        let amount = by(vim.take_count(cx).map(|c| c as f32));
+        vim.update_active_editor(cx, |editor, cx| {
+            scroll_editor(editor, move_cursor, &amount, cx)
+        });
     })
 }
 
-fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
+fn scroll_editor(
+    editor: &mut Editor,
+    preserve_cursor_position: bool,
+    amount: &ScrollAmount,
+    cx: &mut ViewContext<Editor>,
+) {
     let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
+    let old_top_anchor = editor.scroll_manager.anchor().anchor;
 
     editor.scroll_screen(amount, cx);
     if should_move_cursor {
@@ -68,8 +80,14 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
 
         editor.change_selections(None, cx, |s| {
             s.move_with(|map, selection| {
-                let head = selection.head();
+                let mut head = selection.head();
                 let top = top_anchor.to_display_point(map);
+
+                if preserve_cursor_position {
+                    let old_top = old_top_anchor.to_display_point(map);
+                    let new_row = top.row() + selection.head().row() - old_top.row();
+                    head = map.clip_point(DisplayPoint::new(new_row, head.column()), Bias::Left)
+                }
                 let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32;
                 let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1;
 
@@ -92,7 +110,10 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
 
 #[cfg(test)]
 mod test {
-    use crate::{state::Mode, test::VimTestContext};
+    use crate::{
+        state::Mode,
+        test::{NeovimBackedTestContext, VimTestContext},
+    };
     use gpui::geometry::vector::vec2f;
     use indoc::indoc;
     use language::Point;
@@ -148,10 +169,10 @@ mod test {
         });
         cx.simulate_keystrokes(["ctrl-d"]);
         cx.update_editor(|editor, cx| {
-            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0));
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
             assert_eq!(
                 editor.selections.newest(cx).range(),
-                Point::new(5, 0)..Point::new(5, 0)
+                Point::new(6, 0)..Point::new(6, 0)
             )
         });
 
@@ -162,11 +183,48 @@ mod test {
         });
         cx.simulate_keystrokes(["v", "ctrl-d"]);
         cx.update_editor(|editor, cx| {
-            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0));
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
             assert_eq!(
                 editor.selections.newest(cx).range(),
-                Point::new(0, 0)..Point::new(5, 1)
+                Point::new(0, 0)..Point::new(6, 1)
             )
         });
     }
+    #[gpui::test]
+    async fn test_ctrl_d_u(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_scroll_height(10).await;
+
+        pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
+            let mut text = String::new();
+            for row in 0..rows {
+                let c: char = (start_char as u32 + row as u32) as u8 as char;
+                let mut line = c.to_string().repeat(cols);
+                if row < rows - 1 {
+                    line.push('\n');
+                }
+                text += &line;
+            }
+            text
+        }
+        let content = "Λ‡".to_owned() + &sample_text(26, 2, 'a');
+        cx.set_shared_state(&content).await;
+
+        // skip over the scrolloff at the top
+        // test ctrl-d
+        cx.simulate_shared_keystrokes(["4", "j", "ctrl-d"]).await;
+        cx.assert_state_matches().await;
+        cx.simulate_shared_keystrokes(["ctrl-d"]).await;
+        cx.assert_state_matches().await;
+        cx.simulate_shared_keystrokes(["g", "g", "ctrl-d"]).await;
+        cx.assert_state_matches().await;
+
+        // test ctrl-u
+        cx.simulate_shared_keystrokes(["ctrl-u"]).await;
+        cx.assert_state_matches().await;
+        cx.simulate_shared_keystrokes(["ctrl-d", "ctrl-d", "4", "j", "ctrl-u", "ctrl-u"])
+            .await;
+        cx.assert_state_matches().await;
+    }
 }

crates/vim/src/normal/search.rs πŸ”—

@@ -3,7 +3,7 @@ use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
 use serde_derive::Deserialize;
 use workspace::{searchable::Direction, Pane, Workspace};
 
-use crate::{state::SearchState, Vim};
+use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim};
 
 #[derive(Clone, Deserialize, PartialEq)]
 #[serde(rename_all = "camelCase")]
@@ -25,7 +25,29 @@ pub(crate) struct Search {
     backwards: bool,
 }
 
-impl_actions!(vim, [MoveToNext, MoveToPrev, Search]);
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+pub struct FindCommand {
+    pub query: String,
+    pub backwards: bool,
+}
+
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+pub struct ReplaceCommand {
+    pub query: String,
+}
+
+#[derive(Debug, Default)]
+struct Replacement {
+    search: String,
+    replacement: String,
+    should_replace_all: bool,
+    is_case_sensitive: bool,
+}
+
+impl_actions!(
+    vim,
+    [MoveToNext, MoveToPrev, Search, FindCommand, ReplaceCommand]
+);
 actions!(vim, [SearchSubmit]);
 
 pub(crate) fn init(cx: &mut AppContext) {
@@ -34,6 +56,9 @@ pub(crate) fn init(cx: &mut AppContext) {
     cx.add_action(search);
     cx.add_action(search_submit);
     cx.add_action(search_deploy);
+
+    cx.add_action(find_command);
+    cx.add_action(replace_command);
 }
 
 fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
@@ -52,7 +77,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
         Direction::Next
     };
     Vim::update(cx, |vim, cx| {
-        let count = vim.pop_number_operator(cx).unwrap_or(1);
+        let count = vim.take_count(cx).unwrap_or(1);
         pane.update(cx, |pane, cx| {
             if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                 search_bar.update(cx, |search_bar, cx| {
@@ -65,6 +90,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
                     cx.focus_self();
 
                     if query.is_empty() {
+                        search_bar.set_replacement(None, cx);
                         search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
                         search_bar.activate_search_mode(SearchMode::Regex, cx);
                     }
@@ -119,7 +145,7 @@ pub fn move_to_internal(
 ) {
     Vim::update(cx, |vim, cx| {
         let pane = workspace.active_pane().clone();
-        let count = vim.pop_number_operator(cx).unwrap_or(1);
+        let count = vim.take_count(cx).unwrap_or(1);
         pane.update(cx, |pane, cx| {
             if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                 let search = search_bar.update(cx, |search_bar, cx| {
@@ -151,6 +177,174 @@ pub fn move_to_internal(
     });
 }
 
+fn find_command(workspace: &mut Workspace, action: &FindCommand, cx: &mut ViewContext<Workspace>) {
+    let pane = workspace.active_pane().clone();
+    pane.update(cx, |pane, cx| {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            let search = search_bar.update(cx, |search_bar, cx| {
+                if !search_bar.show(cx) {
+                    return None;
+                }
+                let mut query = action.query.clone();
+                if query == "" {
+                    query = search_bar.query(cx);
+                };
+
+                search_bar.activate_search_mode(SearchMode::Regex, cx);
+                Some(search_bar.search(&query, Some(SearchOptions::CASE_SENSITIVE), cx))
+            });
+            let Some(search) = search else { return };
+            let search_bar = search_bar.downgrade();
+            let direction = if action.backwards {
+                Direction::Prev
+            } else {
+                Direction::Next
+            };
+            cx.spawn(|_, mut cx| async move {
+                search.await?;
+                search_bar.update(&mut cx, |search_bar, cx| {
+                    search_bar.select_match(direction, 1, cx)
+                })?;
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+    })
+}
+
+fn replace_command(
+    workspace: &mut Workspace,
+    action: &ReplaceCommand,
+    cx: &mut ViewContext<Workspace>,
+) {
+    let replacement = parse_replace_all(&action.query);
+    let pane = workspace.active_pane().clone();
+    pane.update(cx, |pane, cx| {
+        let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
+            return;
+        };
+        let search = search_bar.update(cx, |search_bar, cx| {
+            if !search_bar.show(cx) {
+                return None;
+            }
+
+            let mut options = SearchOptions::default();
+            if replacement.is_case_sensitive {
+                options.set(SearchOptions::CASE_SENSITIVE, true)
+            }
+            let search = if replacement.search == "" {
+                search_bar.query(cx)
+            } else {
+                replacement.search
+            };
+
+            search_bar.set_replacement(Some(&replacement.replacement), cx);
+            search_bar.activate_search_mode(SearchMode::Regex, cx);
+            Some(search_bar.search(&search, Some(options), cx))
+        });
+        let Some(search) = search else { return };
+        let search_bar = search_bar.downgrade();
+        cx.spawn(|_, mut cx| async move {
+            search.await?;
+            search_bar.update(&mut cx, |search_bar, cx| {
+                if replacement.should_replace_all {
+                    search_bar.select_last_match(cx);
+                    search_bar.replace_all(&Default::default(), cx);
+                    Vim::update(cx, |vim, cx| {
+                        move_cursor(
+                            vim,
+                            Motion::StartOfLine {
+                                display_lines: false,
+                            },
+                            None,
+                            cx,
+                        )
+                    })
+                }
+            })?;
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    })
+}
+
+// convert a vim query into something more usable by zed.
+// we don't attempt to fully convert between the two regex syntaxes,
+// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
+// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
+fn parse_replace_all(query: &str) -> Replacement {
+    let mut chars = query.chars();
+    if Some('%') != chars.next() || Some('s') != chars.next() {
+        return Replacement::default();
+    }
+
+    let Some(delimeter) = chars.next() else {
+        return Replacement::default();
+    };
+
+    let mut search = String::new();
+    let mut replacement = String::new();
+    let mut flags = String::new();
+
+    let mut buffer = &mut search;
+
+    let mut escaped = false;
+    // 0 - parsing search
+    // 1 - parsing replacement
+    // 2 - parsing flags
+    let mut phase = 0;
+
+    for c in chars {
+        if escaped {
+            escaped = false;
+            if phase == 1 && c.is_digit(10) {
+                buffer.push('$')
+            // unescape escaped parens
+            } else if phase == 0 && c == '(' || c == ')' {
+            } else if c != delimeter {
+                buffer.push('\\')
+            }
+            buffer.push(c)
+        } else if c == '\\' {
+            escaped = true;
+        } else if c == delimeter {
+            if phase == 0 {
+                buffer = &mut replacement;
+                phase = 1;
+            } else if phase == 1 {
+                buffer = &mut flags;
+                phase = 2;
+            } else {
+                break;
+            }
+        } else {
+            // escape unescaped parens
+            if phase == 0 && c == '(' || c == ')' {
+                buffer.push('\\')
+            }
+            buffer.push(c)
+        }
+    }
+
+    let mut replacement = Replacement {
+        search,
+        replacement,
+        should_replace_all: true,
+        is_case_sensitive: true,
+    };
+
+    for c in flags.chars() {
+        match c {
+            'g' | 'I' => {}
+            'c' | 'n' => replacement.should_replace_all = false,
+            'i' => replacement.is_case_sensitive = false,
+            _ => {}
+        }
+    }
+
+    replacement
+}
+
 #[cfg(test)]
 mod test {
     use std::sync::Arc;
@@ -227,7 +421,7 @@ mod test {
         deterministic.run_until_parked();
 
         cx.update_editor(|editor, cx| {
-            let highlights = editor.all_background_highlights(cx);
+            let highlights = editor.all_text_background_highlights(cx);
             assert_eq!(3, highlights.len());
             assert_eq!(
                 DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),

crates/vim/src/normal/substitute.rs πŸ”—

@@ -11,7 +11,7 @@ pub(crate) fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
         Vim::update(cx, |vim, cx| {
             vim.start_recording(cx);
-            let count = vim.pop_number_operator(cx);
+            let count = vim.take_count(cx);
             substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
         })
     });
@@ -22,7 +22,7 @@ pub(crate) fn init(cx: &mut AppContext) {
             if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
                 vim.switch_mode(Mode::VisualLine, false, cx)
             }
-            let count = vim.pop_number_operator(cx);
+            let count = vim.take_count(cx);
             substitute(vim, count, true, cx)
         })
     });

crates/vim/src/state.rs πŸ”—

@@ -33,7 +33,6 @@ impl Default for Mode {
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
 pub enum Operator {
-    Number(usize),
     Change,
     Delete,
     Yank,
@@ -47,6 +46,12 @@ pub enum Operator {
 pub struct EditorState {
     pub mode: Mode,
     pub last_mode: Mode,
+
+    /// pre_count is the number before an operator is specified (3 in 3d2d)
+    pub pre_count: Option<usize>,
+    /// post_count is the number after an operator is specified (2 in 3d2d)
+    pub post_count: Option<usize>,
+
     pub operator_stack: Vec<Operator>,
 }
 
@@ -158,6 +163,10 @@ impl EditorState {
         }
     }
 
+    pub fn active_operator(&self) -> Option<Operator> {
+        self.operator_stack.last().copied()
+    }
+
     pub fn keymap_context_layer(&self) -> KeymapContext {
         let mut context = KeymapContext::default();
         context.add_identifier("VimEnabled");
@@ -174,7 +183,13 @@ impl EditorState {
             context.add_identifier("VimControl");
         }
 
-        let active_operator = self.operator_stack.last();
+        if self.active_operator().is_none() && self.pre_count.is_some()
+            || self.active_operator().is_some() && self.post_count.is_some()
+        {
+            context.add_identifier("VimCount");
+        }
+
+        let active_operator = self.active_operator();
 
         if let Some(active_operator) = active_operator {
             for context_flag in active_operator.context_flags().into_iter() {
@@ -194,7 +209,6 @@ impl EditorState {
 impl Operator {
     pub fn id(&self) -> &'static str {
         match self {
-            Operator::Number(_) => "n",
             Operator::Object { around: false } => "i",
             Operator::Object { around: true } => "a",
             Operator::Change => "c",

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

@@ -186,11 +186,8 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
         assert_eq!(bar.query(cx), "cc");
     });
 
-    // wait for the query editor change event to fire.
-    search_bar.next_notification(&cx).await;
-
     cx.update_editor(|editor, cx| {
-        let highlights = editor.all_background_highlights(cx);
+        let highlights = editor.all_text_background_highlights(cx);
         assert_eq!(3, highlights.len());
         assert_eq!(
             DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
@@ -574,3 +571,84 @@ async fn test_folds(cx: &mut gpui::TestAppContext) {
     "})
         .await;
 }
+
+#[gpui::test]
+async fn test_folds_panic(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+    cx.set_neovim_option("foldmethod=manual").await;
+
+    cx.set_shared_state(indoc! { "
+        fn boop() {
+          Λ‡barp()
+          bazp()
+        }
+    "})
+        .await;
+    cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"])
+        .await;
+    cx.simulate_shared_keystrokes(["escape"]).await;
+    cx.simulate_shared_keystrokes(["g", "g"]).await;
+    cx.simulate_shared_keystrokes(["5", "d", "j"]).await;
+    cx.assert_shared_state(indoc! { "Λ‡"}).await;
+}
+
+#[gpui::test]
+async fn test_clear_counts(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_state(indoc! {"
+        The quick brown
+        fox juˇmps over
+        the lazy dog"})
+        .await;
+
+    cx.simulate_shared_keystrokes(["4", "escape", "3", "d", "l"])
+        .await;
+    cx.assert_shared_state(indoc! {"
+        The quick brown
+        fox juˇ over
+        the lazy dog"})
+        .await;
+}
+
+#[gpui::test]
+async fn test_zero(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_state(indoc! {"
+        The quˇick brown
+        fox jumps over
+        the lazy dog"})
+        .await;
+
+    cx.simulate_shared_keystrokes(["0"]).await;
+    cx.assert_shared_state(indoc! {"
+        Λ‡The quick brown
+        fox jumps over
+        the lazy dog"})
+        .await;
+
+    cx.simulate_shared_keystrokes(["1", "0", "l"]).await;
+    cx.assert_shared_state(indoc! {"
+        The quick Λ‡brown
+        fox jumps over
+        the lazy dog"})
+        .await;
+}
+
+#[gpui::test]
+async fn test_selection_goal(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_state(indoc! {"
+        ;;Λ‡;
+        Lorem Ipsum"})
+        .await;
+
+    cx.simulate_shared_keystrokes(["a", "down", "up", ";", "down", "up"])
+        .await;
+    cx.assert_shared_state(indoc! {"
+        ;;;;Λ‡
+        Lorem Ipsum"})
+        .await;
+}

crates/vim/src/test/neovim_backed_binding_test_context.rs πŸ”—

@@ -1,7 +1,5 @@
 use std::ops::{Deref, DerefMut};
 
-use gpui::ContextHandle;
-
 use crate::state::Mode;
 
 use super::{ExemptionFeatures, NeovimBackedTestContext, SUPPORTED_FEATURES};
@@ -33,26 +31,17 @@ impl<'a, const COUNT: usize> NeovimBackedBindingTestContext<'a, COUNT> {
         self.consume().binding(keystrokes)
     }
 
-    pub async fn assert(
-        &mut self,
-        marked_positions: &str,
-    ) -> Option<(ContextHandle, ContextHandle)> {
+    pub async fn assert(&mut self, marked_positions: &str) {
         self.cx
             .assert_binding_matches(self.keystrokes_under_test, marked_positions)
-            .await
+            .await;
     }
 
-    pub async fn assert_exempted(
-        &mut self,
-        marked_positions: &str,
-        feature: ExemptionFeatures,
-    ) -> Option<(ContextHandle, ContextHandle)> {
+    pub async fn assert_exempted(&mut self, marked_positions: &str, feature: ExemptionFeatures) {
         if SUPPORTED_FEATURES.contains(&feature) {
             self.cx
                 .assert_binding_matches(self.keystrokes_under_test, marked_positions)
                 .await
-        } else {
-            None
         }
     }
 

crates/vim/src/test/neovim_backed_test_context.rs πŸ”—

@@ -1,9 +1,10 @@
+use editor::scroll::VERTICAL_SCROLL_MARGIN;
 use indoc::indoc;
 use settings::SettingsStore;
 use std::ops::{Deref, DerefMut, Range};
 
 use collections::{HashMap, HashSet};
-use gpui::ContextHandle;
+use gpui::{geometry::vector::vec2f, ContextHandle};
 use language::{
     language_settings::{AllLanguageSettings, SoftWrap},
     OffsetRangeExt,
@@ -13,24 +14,15 @@ use util::test::{generate_marked_text, marked_text_offsets};
 use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
 use crate::state::Mode;
 
-pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[
-    ExemptionFeatures::DeletionOnEmptyLine,
-    ExemptionFeatures::OperatorAbortsOnFailedMotion,
-];
+pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
 
 /// Enum representing features we have tests for but which don't work, yet. Used
 /// to add exemptions and automatically
 #[derive(PartialEq, Eq)]
 pub enum ExemptionFeatures {
     // MOTIONS
-    // Deletions on empty lines miss some newlines
-    DeletionOnEmptyLine,
-    // When a motion fails, it should should not apply linewise operations
-    OperatorAbortsOnFailedMotion,
     // When an operator completes at the end of the file, an extra newline is left
     OperatorLastNewlineRemains,
-    // Deleting a word on an empty line doesn't remove the newline
-    DeleteWordOnEmptyLine,
 
     // OBJECTS
     // Resulting position after the operation is slightly incorrect for unintuitive reasons.
@@ -68,6 +60,8 @@ pub struct NeovimBackedTestContext<'a> {
 
     last_set_state: Option<String>,
     recent_keystrokes: Vec<String>,
+
+    is_dirty: bool,
 }
 
 impl<'a> NeovimBackedTestContext<'a> {
@@ -81,6 +75,7 @@ impl<'a> NeovimBackedTestContext<'a> {
 
             last_set_state: None,
             recent_keystrokes: Default::default(),
+            is_dirty: false,
         }
     }
 
@@ -110,25 +105,25 @@ impl<'a> NeovimBackedTestContext<'a> {
     pub async fn simulate_shared_keystrokes<const COUNT: usize>(
         &mut self,
         keystroke_texts: [&str; COUNT],
-    ) -> ContextHandle {
+    ) {
         for keystroke_text in keystroke_texts.into_iter() {
             self.recent_keystrokes.push(keystroke_text.to_string());
             self.neovim.send_keystroke(keystroke_text).await;
         }
-        self.simulate_keystrokes(keystroke_texts)
+        self.simulate_keystrokes(keystroke_texts);
     }
 
-    pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle {
+    pub async fn set_shared_state(&mut self, marked_text: &str) {
         let mode = if marked_text.contains("Β»") {
             Mode::Visual
         } else {
             Mode::Normal
         };
-        let context_handle = self.set_state(marked_text, mode);
+        self.set_state(marked_text, mode);
         self.last_set_state = Some(marked_text.to_string());
         self.recent_keystrokes = Vec::new();
         self.neovim.set_state(marked_text).await;
-        context_handle
+        self.is_dirty = true;
     }
 
     pub async fn set_shared_wrap(&mut self, columns: u32) {
@@ -136,7 +131,9 @@ impl<'a> NeovimBackedTestContext<'a> {
             panic!("nvim doesn't support columns < 12")
         }
         self.neovim.set_option("wrap").await;
-        self.neovim.set_option("columns=12").await;
+        self.neovim
+            .set_option(&format!("columns={}", columns))
+            .await;
 
         self.update(|cx| {
             cx.update_global(|settings: &mut SettingsStore, cx| {
@@ -148,11 +145,26 @@ impl<'a> NeovimBackedTestContext<'a> {
         })
     }
 
+    pub async fn set_scroll_height(&mut self, rows: u32) {
+        // match Zed's scrolling behavior
+        self.neovim
+            .set_option(&format!("scrolloff={}", VERTICAL_SCROLL_MARGIN))
+            .await;
+        // +2 to account for the vim command UI at the bottom.
+        self.neovim.set_option(&format!("lines={}", rows + 2)).await;
+        let window = self.window;
+        let line_height =
+            self.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+
+        window.simulate_resize(vec2f(1000., (rows as f32) * line_height), &mut self.cx);
+    }
+
     pub async fn set_neovim_option(&mut self, option: &str) {
         self.neovim.set_option(option).await;
     }
 
     pub async fn assert_shared_state(&mut self, marked_text: &str) {
+        self.is_dirty = false;
         let marked_text = marked_text.replace("β€’", " ");
         let neovim = self.neovim_state().await;
         let editor = self.editor_state();
@@ -258,6 +270,7 @@ impl<'a> NeovimBackedTestContext<'a> {
     }
 
     pub async fn assert_state_matches(&mut self) {
+        self.is_dirty = false;
         let neovim = self.neovim_state().await;
         let editor = self.editor_state();
         let initial_state = self
@@ -289,18 +302,18 @@ impl<'a> NeovimBackedTestContext<'a> {
         &mut self,
         keystrokes: [&str; COUNT],
         initial_state: &str,
-    ) -> Option<(ContextHandle, ContextHandle)> {
+    ) {
         if let Some(possible_exempted_keystrokes) = self.exemptions.get(initial_state) {
             match possible_exempted_keystrokes {
                 Some(exempted_keystrokes) => {
                     if exempted_keystrokes.contains(&format!("{keystrokes:?}")) {
                         // This keystroke was exempted for this insertion text
-                        return None;
+                        return;
                     }
                 }
                 None => {
                     // All keystrokes for this insertion text are exempted
-                    return None;
+                    return;
                 }
             }
         }
@@ -308,7 +321,6 @@ impl<'a> NeovimBackedTestContext<'a> {
         let _state_context = self.set_shared_state(initial_state).await;
         let _keystroke_context = self.simulate_shared_keystrokes(keystrokes).await;
         self.assert_state_matches().await;
-        Some((_state_context, _keystroke_context))
     }
 
     pub async fn assert_binding_matches_all<const COUNT: usize>(
@@ -349,6 +361,17 @@ impl<'a> NeovimBackedTestContext<'a> {
         self.assert_state_matches().await;
     }
 
+    pub async fn assert_matches_neovim<const COUNT: usize>(
+        &mut self,
+        marked_positions: &str,
+        keystrokes: [&str; COUNT],
+        result: &str,
+    ) {
+        self.set_shared_state(marked_positions).await;
+        self.simulate_shared_keystrokes(keystrokes).await;
+        self.assert_shared_state(result).await;
+    }
+
     pub async fn assert_binding_matches_all_exempted<const COUNT: usize>(
         &mut self,
         keystrokes: [&str; COUNT],
@@ -383,6 +406,17 @@ impl<'a> DerefMut for NeovimBackedTestContext<'a> {
     }
 }
 
+// a common mistake in tests is to call set_shared_state when
+// you mean asswert_shared_state. This notices that and lets
+// you know.
+impl<'a> Drop for NeovimBackedTestContext<'a> {
+    fn drop(&mut self) {
+        if self.is_dirty {
+            panic!("Test context was dropped after set_shared_state before assert_shared_state")
+        }
+    }
+}
+
 #[cfg(test)]
 mod test {
     use gpui::TestAppContext;

crates/vim/src/test/neovim_connection.rs πŸ”—

@@ -65,7 +65,13 @@ impl NeovimConnection {
             // Ensure we don't create neovim connections in parallel
             let _lock = NEOVIM_LOCK.lock();
             let (nvim, join_handle, child) = new_child_cmd(
-                &mut Command::new("nvim").arg("--embed").arg("--clean"),
+                &mut Command::new("nvim")
+                    .arg("--embed")
+                    .arg("--clean")
+                    // disable swap (otherwise after about 1000 test runs you run out of swap file names)
+                    .arg("-n")
+                    // disable writing files (just in case)
+                    .arg("-m"),
                 handler,
             )
             .await

crates/vim/src/utils.rs πŸ”—

@@ -26,10 +26,11 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App
             let is_last_line = linewise
                 && end.row == buffer.max_buffer_row()
                 && buffer.max_point().column > 0
+                && start.row < buffer.max_buffer_row()
                 && start == Point::new(start.row, buffer.line_len(start.row));
 
             if is_last_line {
-                start = Point::new(buffer.max_buffer_row(), 0);
+                start = Point::new(start.row + 1, 0);
             }
             for chunk in buffer.text_for_range(start..end) {
                 text.push_str(chunk);

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

@@ -1,6 +1,7 @@
 #[cfg(test)]
 mod test;
 
+mod command;
 mod editor_events;
 mod insert;
 mod mode_indicator;
@@ -13,10 +14,11 @@ mod visual;
 
 use anyhow::Result;
 use collections::{CommandPaletteFilter, HashMap};
+use command_palette::CommandPaletteInterceptor;
 use editor::{movement, Editor, EditorMode, Event};
 use gpui::{
-    actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
-    Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, Action,
+    AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use language::{CursorShape, Point, Selection, SelectionGoal};
 pub use mode_indicator::ModeIndicator;
@@ -40,9 +42,12 @@ pub struct SwitchMode(pub Mode);
 pub struct PushOperator(pub Operator);
 
 #[derive(Clone, Deserialize, PartialEq)]
-struct Number(u8);
+struct Number(usize);
 
-actions!(vim, [Tab, Enter]);
+actions!(
+    vim,
+    [Tab, Enter, Object, InnerObject, FindForward, FindBackward]
+);
 impl_actions!(vim, [Number, SwitchMode, PushOperator]);
 
 #[derive(Copy, Clone, Debug)]
@@ -51,6 +56,7 @@ enum VimEvent {
 }
 
 pub fn init(cx: &mut AppContext) {
+    cx.set_global(Vim::default());
     settings::register::<VimModeSetting>(cx);
 
     editor_events::init(cx);
@@ -59,6 +65,7 @@ pub fn init(cx: &mut AppContext) {
     insert::init(cx);
     object::init(cx);
     motion::init(cx);
+    command::init(cx);
 
     // Vim Actions
     cx.add_action(|_: &mut Workspace, &SwitchMode(mode): &SwitchMode, cx| {
@@ -70,7 +77,7 @@ pub fn init(cx: &mut AppContext) {
         },
     );
     cx.add_action(|_: &mut Workspace, n: &Number, cx: _| {
-        Vim::update(cx, |vim, cx| vim.push_number(n, cx));
+        Vim::update(cx, |vim, cx| vim.push_count_digit(n.0, cx));
     });
 
     cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
@@ -87,11 +94,11 @@ pub fn init(cx: &mut AppContext) {
     cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
         filter.filtered_namespaces.insert("vim");
     });
-    cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| {
+    cx.update_global(|vim: &mut Vim, cx: &mut AppContext| {
         vim.set_enabled(settings::get::<VimModeSetting>(cx).0, cx)
     });
     cx.observe_global::<SettingsStore, _>(|cx| {
-        cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| {
+        cx.update_global(|vim: &mut Vim, cx: &mut AppContext| {
             vim.set_enabled(settings::get::<VimModeSetting>(cx).0, cx)
         });
     })
@@ -156,7 +163,7 @@ impl Vim {
     where
         F: FnOnce(&mut Self, &mut WindowContext) -> S,
     {
-        cx.update_default_global(update)
+        cx.update_global(update)
     }
 
     fn set_active_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut WindowContext) {
@@ -164,7 +171,7 @@ impl Vim {
         self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
             Event::SelectionsChanged { local: true } => {
                 let editor = editor.read(cx);
-                if editor.leader_replica_id().is_none() {
+                if editor.leader_peer_id().is_none() {
                     let newest = editor.selections.newest::<usize>(cx);
                     local_selections_changed(newest, cx);
                 }
@@ -188,6 +195,8 @@ impl Vim {
             if editor_mode == EditorMode::Full
                 && !newest_selection_empty
                 && self.state().mode == Mode::Normal
+                // When following someone, don't switch vim mode.
+                && editor.leader_peer_id().is_none()
             {
                 self.switch_mode(Mode::Visual, true, cx);
             }
@@ -225,23 +234,12 @@ impl Vim {
         let editor = self.active_editor.clone()?.upgrade(cx)?;
         Some(editor.update(cx, update))
     }
-    // ~, shift-j, x, shift-x, p
-    // shift-c, shift-d, shift-i, i, a, o, shift-o, s
-    // c, d
-    // r
 
-    // TODO: shift-j?
-    //
     pub fn start_recording(&mut self, cx: &mut WindowContext) {
         if !self.workspace_state.replaying {
             self.workspace_state.recording = true;
             self.workspace_state.recorded_actions = Default::default();
-            self.workspace_state.recorded_count =
-                if let Some(Operator::Number(number)) = self.active_operator() {
-                    Some(number)
-                } else {
-                    None
-                };
+            self.workspace_state.recorded_count = None;
 
             let selections = self
                 .active_editor
@@ -286,6 +284,16 @@ impl Vim {
         }
     }
 
+    pub fn stop_recording_immediately(&mut self, action: Box<dyn Action>) {
+        if self.workspace_state.recording {
+            self.workspace_state
+                .recorded_actions
+                .push(ReplayableAction::Action(action.boxed_clone()));
+            self.workspace_state.recording = false;
+            self.workspace_state.stop_recording_after_next_action = false;
+        }
+    }
+
     pub fn record_current_action(&mut self, cx: &mut WindowContext) {
         self.start_recording(cx);
         self.stop_recording();
@@ -300,6 +308,9 @@ impl Vim {
             state.mode = mode;
             state.operator_stack.clear();
         });
+        if mode != Mode::Insert {
+            self.take_count(cx);
+        }
 
         cx.emit_global(VimEvent::ModeChanged { mode });
 
@@ -352,6 +363,39 @@ impl Vim {
         });
     }
 
+    fn push_count_digit(&mut self, number: usize, cx: &mut WindowContext) {
+        if self.active_operator().is_some() {
+            self.update_state(|state| {
+                state.post_count = Some(state.post_count.unwrap_or(0) * 10 + number)
+            })
+        } else {
+            self.update_state(|state| {
+                state.pre_count = Some(state.pre_count.unwrap_or(0) * 10 + number)
+            })
+        }
+        // update the keymap so that 0 works
+        self.sync_vim_settings(cx)
+    }
+
+    fn take_count(&mut self, cx: &mut WindowContext) -> Option<usize> {
+        if self.workspace_state.replaying {
+            return self.workspace_state.recorded_count;
+        }
+
+        let count = if self.state().post_count == None && self.state().pre_count == None {
+            return None;
+        } else {
+            Some(self.update_state(|state| {
+                state.post_count.take().unwrap_or(1) * state.pre_count.take().unwrap_or(1)
+            }))
+        };
+        if self.workspace_state.recording {
+            self.workspace_state.recorded_count = count;
+        }
+        self.sync_vim_settings(cx);
+        count
+    }
+
     fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
         if matches!(
             operator,
@@ -363,15 +407,6 @@ impl Vim {
         self.sync_vim_settings(cx);
     }
 
-    fn push_number(&mut self, Number(number): &Number, cx: &mut WindowContext) {
-        if let Some(Operator::Number(current_number)) = self.active_operator() {
-            self.pop_operator(cx);
-            self.push_operator(Operator::Number(current_number * 10 + *number as usize), cx);
-        } else {
-            self.push_operator(Operator::Number(*number as usize), cx);
-        }
-    }
-
     fn maybe_pop_operator(&mut self) -> Option<Operator> {
         self.update_state(|state| state.operator_stack.pop())
     }
@@ -382,22 +417,8 @@ impl Vim {
         self.sync_vim_settings(cx);
         popped_operator
     }
-
-    fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
-        if self.workspace_state.replaying {
-            if let Some(number) = self.workspace_state.recorded_count {
-                return Some(number);
-            }
-        }
-
-        if let Some(Operator::Number(number)) = self.active_operator() {
-            self.pop_operator(cx);
-            return Some(number);
-        }
-        None
-    }
-
     fn clear_operator(&mut self, cx: &mut WindowContext) {
+        self.take_count(cx);
         self.update_state(|state| state.operator_stack.clear());
         self.sync_vim_settings(cx);
     }
@@ -453,6 +474,12 @@ impl Vim {
                 }
             });
 
+            if self.enabled {
+                cx.set_global::<CommandPaletteInterceptor>(Box::new(command::command_interceptor));
+            } else if cx.has_global::<CommandPaletteInterceptor>() {
+                let _ = cx.remove_global::<CommandPaletteInterceptor>();
+            }
+
             cx.update_active_window(|cx| {
                 if self.enabled {
                     let active_editor = cx

crates/vim/src/visual.rs πŸ”—

@@ -1,3 +1,4 @@
+use anyhow::Result;
 use std::{cmp, sync::Arc};
 
 use collections::HashMap;
@@ -12,7 +13,7 @@ use language::{Selection, SelectionGoal};
 use workspace::Workspace;
 
 use crate::{
-    motion::Motion,
+    motion::{start_of_line, Motion},
     object::Object,
     state::{Mode, Operator},
     utils::copy_selections_content,
@@ -28,6 +29,8 @@ actions!(
         VisualDelete,
         VisualYank,
         OtherEnd,
+        SelectNext,
+        SelectPrevious,
     ]
 );
 
@@ -46,6 +49,9 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(other_end);
     cx.add_action(delete);
     cx.add_action(yank);
+
+    cx.add_action(select_next);
+    cx.add_action(select_previous);
 }
 
 pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
@@ -326,7 +332,10 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
             let line_mode = editor.selections.line_mode;
             copy_selections_content(editor, line_mode, cx);
             editor.change_selections(None, cx, |s| {
-                s.move_with(|_, selection| {
+                s.move_with(|map, selection| {
+                    if line_mode {
+                        selection.start = start_of_line(map, false, selection.start);
+                    };
                     selection.collapse_to(selection.start, SelectionGoal::None)
                 });
                 if vim.state().mode == Mode::VisualBlock {
@@ -381,6 +390,50 @@ pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
     });
 }
 
+pub fn select_next(
+    _: &mut Workspace,
+    _: &SelectNext,
+    cx: &mut ViewContext<Workspace>,
+) -> Result<()> {
+    Vim::update(cx, |vim, cx| {
+        let count =
+            vim.take_count(cx)
+                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
+        vim.update_active_editor(cx, |editor, cx| {
+            for _ in 0..count {
+                match editor.select_next(&Default::default(), cx) {
+                    Err(a) => return Err(a),
+                    _ => {}
+                }
+            }
+            Ok(())
+        })
+    })
+    .unwrap_or(Ok(()))
+}
+
+pub fn select_previous(
+    _: &mut Workspace,
+    _: &SelectPrevious,
+    cx: &mut ViewContext<Workspace>,
+) -> Result<()> {
+    Vim::update(cx, |vim, cx| {
+        let count =
+            vim.take_count(cx)
+                .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
+        vim.update_active_editor(cx, |editor, cx| {
+            for _ in 0..count {
+                match editor.select_previous(&Default::default(), cx) {
+                    Err(a) => return Err(a),
+                    _ => {}
+                }
+            }
+            Ok(())
+        })
+    })
+    .unwrap_or(Ok(()))
+}
+
 #[cfg(test)]
 mod test {
     use indoc::indoc;
@@ -672,6 +725,21 @@ mod test {
                     the lazy dog"})
             .await;
         cx.assert_clipboard_content(Some("The q"));
+
+        cx.set_shared_state(indoc! {"
+                    The quick brown
+                    fox Λ‡jumps over
+                    the lazy dog"})
+            .await;
+        cx.simulate_shared_keystrokes(["shift-v", "shift-g", "shift-y"])
+            .await;
+        cx.assert_shared_state(indoc! {"
+                    The quick brown
+                    Λ‡fox jumps over
+                    the lazy dog"})
+            .await;
+        cx.assert_shared_clipboard("fox jumps over\nthe lazy dog\n")
+            .await;
     }
 
     #[gpui::test]

crates/vim/test_data/test_clear_counts.json πŸ”—

@@ -0,0 +1,7 @@
+{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}}
+{"Key":"4"}
+{"Key":"escape"}
+{"Key":"3"}
+{"Key":"d"}
+{"Key":"l"}
+{"Get":{"state":"The quick brown\nfox juˇ over\nthe lazy dog","mode":"Normal"}}

crates/vim/test_data/test_command_replace.json πŸ”—

@@ -0,0 +1,22 @@
+{"Put":{"state":"Λ‡a\nb\nc"}}
+{"Key":":"}
+{"Key":"%"}
+{"Key":"s"}
+{"Key":"/"}
+{"Key":"b"}
+{"Key":"/"}
+{"Key":"d"}
+{"Key":"enter"}
+{"Get":{"state":"a\nˇd\nc","mode":"Normal"}}
+{"Key":":"}
+{"Key":"%"}
+{"Key":"s"}
+{"Key":":"}
+{"Key":"."}
+{"Key":":"}
+{"Key":"\\"}
+{"Key":"0"}
+{"Key":"\\"}
+{"Key":"0"}
+{"Key":"enter"}
+{"Get":{"state":"aa\ndd\nˇcc","mode":"Normal"}}

crates/vim/test_data/test_command_search.json πŸ”—

@@ -0,0 +1,11 @@
+{"Put":{"state":"Λ‡a\nb\na\nc"}}
+{"Key":":"}
+{"Key":"/"}
+{"Key":"b"}
+{"Key":"enter"}
+{"Get":{"state":"a\nˇb\na\nc","mode":"Normal"}}
+{"Key":":"}
+{"Key":"?"}
+{"Key":"a"}
+{"Key":"enter"}
+{"Get":{"state":"Λ‡a\nb\na\nc","mode":"Normal"}}

crates/vim/test_data/test_ctrl_d_u.json πŸ”—

@@ -0,0 +1,22 @@
+{"SetOption":{"value":"scrolloff=3"}}
+{"SetOption":{"value":"lines=12"}}
+{"Put":{"state":"Λ‡aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}}
+{"Key":"4"}
+{"Key":"j"}
+{"Key":"ctrl-d"}
+{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
+{"Key":"ctrl-d"}
+{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\nˇoo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
+{"Key":"g"}
+{"Key":"g"}
+{"Key":"ctrl-d"}
+{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
+{"Key":"ctrl-u"}
+{"Get":{"state":"aa\nbb\ncc\nˇdd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}
+{"Key":"ctrl-d"}
+{"Key":"ctrl-d"}
+{"Key":"4"}
+{"Key":"j"}
+{"Key":"ctrl-u"}
+{"Key":"ctrl-u"}
+{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nˇhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}}

crates/vim/test_data/test_delete_w.json πŸ”—

@@ -1,3 +1,7 @@
+{"Put":{"state":"Test tesˇt\n    test"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Test teˇs\n    test","mode":"Normal"}}
 {"Put":{"state":"Teˇst"}}
 {"Key":"d"}
 {"Key":"w"}
@@ -14,6 +18,10 @@
 {"Key":"d"}
 {"Key":"w"}
 {"Get":{"state":"Test teˇs\ntest","mode":"Normal"}}
+{"Put":{"state":"Test test\nˇ\ntest"}}
+{"Key":"d"}
+{"Key":"w"}
+{"Get":{"state":"Test test\nˇtest","mode":"Normal"}}
 {"Put":{"state":"Test teˇst-test test"}}
 {"Key":"d"}
 {"Key":"shift-w"}

crates/vim/test_data/test_delete_with_counts.json πŸ”—

@@ -0,0 +1,16 @@
+{"Put":{"state":"The Λ‡quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"d"}
+{"Key":"2"}
+{"Key":"d"}
+{"Get":{"state":"the Λ‡lazy dog","mode":"Normal"}}
+{"Put":{"state":"The Λ‡quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"2"}
+{"Key":"d"}
+{"Key":"d"}
+{"Get":{"state":"the Λ‡lazy dog","mode":"Normal"}}
+{"Put":{"state":"The Λ‡quick brown\nfox jumps over\nthe moon,\na star, and\nthe lazy dog"}}
+{"Key":"2"}
+{"Key":"d"}
+{"Key":"2"}
+{"Key":"d"}
+{"Get":{"state":"the Λ‡lazy dog","mode":"Normal"}}

crates/vim/test_data/test_folds_panic.json πŸ”—

@@ -0,0 +1,13 @@
+{"SetOption":{"value":"foldmethod=manual"}}
+{"Put":{"state":"fn boop() {\n  Λ‡barp()\n  bazp()\n}\n"}}
+{"Key":"shift-v"}
+{"Key":"j"}
+{"Key":"z"}
+{"Key":"f"}
+{"Key":"escape"}
+{"Key":"g"}
+{"Key":"g"}
+{"Key":"5"}
+{"Key":"d"}
+{"Key":"j"}
+{"Get":{"state":"Λ‡","mode":"Normal"}}

crates/vim/test_data/test_increment.json πŸ”—

@@ -0,0 +1,16 @@
+{"Put":{"state":"1Λ‡2\n"}}
+{"Key":"ctrl-a"}
+{"Get":{"state":"1Λ‡3\n","mode":"Normal"}}
+{"Key":"ctrl-x"}
+{"Get":{"state":"1Λ‡2\n","mode":"Normal"}}
+{"Key":"9"}
+{"Key":"9"}
+{"Key":"ctrl-a"}
+{"Get":{"state":"11Λ‡1\n","mode":"Normal"}}
+{"Key":"1"}
+{"Key":"1"}
+{"Key":"1"}
+{"Key":"ctrl-x"}
+{"Get":{"state":"Λ‡0\n","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"-11Λ‡1\n","mode":"Normal"}}

crates/vim/test_data/test_increment_radix.json πŸ”—

@@ -0,0 +1,18 @@
+{"Put":{"state":"Λ‡ total: 0xff"}}
+{"Key":"ctrl-a"}
+{"Get":{"state":" total: 0x10Λ‡0","mode":"Normal"}}
+{"Put":{"state":"Λ‡ total: 0xff"}}
+{"Key":"ctrl-x"}
+{"Get":{"state":" total: 0xfˇe","mode":"Normal"}}
+{"Put":{"state":"Λ‡ total: 0xFF"}}
+{"Key":"ctrl-x"}
+{"Get":{"state":" total: 0xFˇE","mode":"Normal"}}
+{"Put":{"state":"(Λ‡0b10f)"}}
+{"Key":"ctrl-a"}
+{"Get":{"state":"(0b1Λ‡1f)","mode":"Normal"}}
+{"Put":{"state":"Λ‡-1"}}
+{"Key":"ctrl-a"}
+{"Get":{"state":"Λ‡0","mode":"Normal"}}
+{"Put":{"state":"banˇana"}}
+{"Key":"ctrl-a"}
+{"Get":{"state":"banˇana","mode":"Normal"}}

crates/vim/test_data/test_increment_steps.json πŸ”—

@@ -0,0 +1,14 @@
+{"Put":{"state":"Λ‡1\n1\n1  2\n1\n1"}}
+{"Key":"j"}
+{"Key":"v"}
+{"Key":"shift-g"}
+{"Key":"g"}
+{"Key":"ctrl-a"}
+{"Get":{"state":"1\nˇ2\n3  2\n4\n5","mode":"Normal"}}
+{"Key":"shift-g"}
+{"Key":"ctrl-v"}
+{"Key":"g"}
+{"Key":"g"}
+{"Key":"g"}
+{"Key":"ctrl-x"}
+{"Get":{"state":"Λ‡0\n0\n0  2\n0\n0","mode":"Normal"}}

crates/vim/test_data/test_insert_with_counts.json πŸ”—

@@ -0,0 +1,36 @@
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"5"}
+{"Key":"i"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"----Λ‡-hello\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"5"}
+{"Key":"a"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"h----Λ‡-ello\n","mode":"Normal"}}
+{"Key":"4"}
+{"Key":"shift-i"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"---Λ‡-h-----ello\n","mode":"Normal"}}
+{"Key":"3"}
+{"Key":"shift-a"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"----h-----ello--Λ‡-\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"3"}
+{"Key":"o"}
+{"Key":"o"}
+{"Key":"i"}
+{"Key":"escape"}
+{"Get":{"state":"hello\noi\noi\noˇi\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"3"}
+{"Key":"shift-o"}
+{"Key":"o"}
+{"Key":"i"}
+{"Key":"escape"}
+{"Get":{"state":"oi\noi\noˇi\nhello\n","mode":"Normal"}}

crates/vim/test_data/test_insert_with_repeat.json πŸ”—

@@ -0,0 +1,23 @@
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"3"}
+{"Key":"i"}
+{"Key":"-"}
+{"Key":"escape"}
+{"Get":{"state":"--Λ‡-hello\n","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"----Λ‡--hello\n","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"."}
+{"Get":{"state":"-----Λ‡---hello\n","mode":"Normal"}}
+{"Put":{"state":"Λ‡hello\n"}}
+{"Key":"2"}
+{"Key":"o"}
+{"Key":"k"}
+{"Key":"k"}
+{"Key":"escape"}
+{"Get":{"state":"hello\nkk\nkˇk\n","mode":"Normal"}}
+{"Key":"."}
+{"Get":{"state":"hello\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}}
+{"Key":"1"}
+{"Key":"."}
+{"Get":{"state":"hello\nkk\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}}

crates/vim/test_data/test_repeat_motion_counts.json πŸ”—

@@ -0,0 +1,13 @@
+{"Put":{"state":"Λ‡the quick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"3"}
+{"Key":"d"}
+{"Key":"3"}
+{"Key":"l"}
+{"Get":{"state":"Λ‡ brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"."}
+{"Get":{"state":" brown\nˇ over\nthe lazy dog","mode":"Normal"}}
+{"Key":"j"}
+{"Key":"2"}
+{"Key":"."}
+{"Get":{"state":" brown\n over\nˇe lazy dog","mode":"Normal"}}

crates/vim/test_data/test_visual_yank.json πŸ”—

@@ -27,3 +27,9 @@
 {"Key":"k"}
 {"Key":"y"}
 {"Get":{"state":"Λ‡The quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Put":{"state":"The quick brown\nfox Λ‡jumps over\nthe lazy dog"}}
+{"Key":"shift-v"}
+{"Key":"shift-g"}
+{"Key":"shift-y"}
+{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"fox jumps over\nthe lazy dog\n"}}

crates/vim/test_data/test_zero.json πŸ”—

@@ -0,0 +1,7 @@
+{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}}
+{"Key":"0"}
+{"Get":{"state":"Λ‡The quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
+{"Key":"1"}
+{"Key":"0"}
+{"Key":"l"}
+{"Get":{"state":"The quick Λ‡brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}

crates/workspace/Cargo.toml πŸ”—

@@ -51,7 +51,7 @@ serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
 smallvec.workspace = true
-uuid = { version = "1.1.2", features = ["v4"] }
+uuid.workspace = true
 
 [dev-dependencies]
 call = { path = "../call", features = ["test-support"] }

crates/workspace/src/dock.rs πŸ”—

@@ -4,7 +4,8 @@ use gpui::{
     elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext,
     Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
-use serde::Deserialize;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
 use std::rc::Rc;
 use theme::ThemeSettings;
 
@@ -132,7 +133,8 @@ pub struct Dock {
     active_panel_index: usize,
 }
 
-#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[serde(rename_all = "lowercase")]
 pub enum DockPosition {
     Left,
     Bottom,

crates/workspace/src/item.rs πŸ”—

@@ -4,7 +4,10 @@ use crate::{
 };
 use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
 use anyhow::Result;
-use client::{proto, Client};
+use client::{
+    proto::{self, PeerId},
+    Client,
+};
 use gpui::geometry::vector::Vector2F;
 use gpui::AnyWindowHandle;
 use gpui::{
@@ -401,6 +404,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         if let Some(followed_item) = self.to_followable_item_handle(cx) {
             if let Some(message) = followed_item.to_state_proto(cx) {
                 workspace.update_followers(
+                    followed_item.is_project_item(cx),
                     proto::update_followers::Variant::CreateView(proto::View {
                         id: followed_item
                             .remote_id(&workspace.app_state.client, cx)
@@ -436,6 +440,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                     };
 
                     if let Some(item) = item.to_followable_item_handle(cx) {
+                        let is_project_item = item.is_project_item(cx);
                         let leader_id = workspace.leader_for_pane(&pane);
 
                         if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
@@ -455,6 +460,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                                 move |this, cx| {
                                     pending_update_scheduled.store(false, Ordering::SeqCst);
                                     this.update_followers(
+                                        is_project_item,
                                         proto::update_followers::Variant::UpdateView(
                                             proto::UpdateView {
                                                 id: item
@@ -475,11 +481,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                         match item_event {
                             ItemEvent::CloseItem => {
                                 pane.update(cx, |pane, cx| {
-                                    pane.close_item_by_id(
-                                        item.id(),
-                                        crate::SaveBehavior::PromptOnWrite,
-                                        cx,
-                                    )
+                                    pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx)
                                 })
                                 .detach_and_log_err(cx);
                                 return;
@@ -696,14 +698,15 @@ pub trait FollowableItem: Item {
         message: proto::update_view::Variant,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>>;
+    fn is_project_item(&self, cx: &AppContext) -> bool;
 
-    fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
+    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
     fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
 }
 
 pub trait FollowableItemHandle: ItemHandle {
     fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
-    fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut WindowContext);
+    fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
     fn add_event_to_update_proto(
         &self,
@@ -718,6 +721,7 @@ pub trait FollowableItemHandle: ItemHandle {
         cx: &mut WindowContext,
     ) -> Task<Result<()>>;
     fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
+    fn is_project_item(&self, cx: &AppContext) -> bool;
 }
 
 impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
@@ -730,10 +734,8 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
         })
     }
 
-    fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut WindowContext) {
-        self.update(cx, |this, cx| {
-            this.set_leader_replica_id(leader_replica_id, cx)
-        })
+    fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext) {
+        self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
     }
 
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
@@ -769,6 +771,10 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
             false
         }
     }
+
+    fn is_project_item(&self, cx: &AppContext) -> bool {
+        self.read(cx).is_project_item(cx)
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]

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

@@ -292,7 +292,7 @@ pub mod simple_message_notification {
                         .with_child(
                             MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
                                 let style = theme.dismiss_button.style_for(state);
-                                Svg::new("icons/x_mark_8.svg")
+                                Svg::new("icons/x.svg")
                                     .with_color(style.color)
                                     .constrained()
                                     .with_width(style.icon_width)

crates/workspace/src/pane.rs πŸ”—

@@ -42,18 +42,25 @@ use std::{
     },
 };
 use theme::{Theme, ThemeSettings};
+use util::truncate_and_remove_front;
 
 #[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
-pub enum SaveBehavior {
-    /// ask before overwriting conflicting files (used by default with %s)
-    PromptOnConflict,
-    /// ask before writing any file that wouldn't be auto-saved (used by default with %w)
-    PromptOnWrite,
-    /// never prompt, write on conflict (used with vim's :w!)
-    SilentlyOverwrite,
-    /// skip all save-related behaviour (used with vim's :cq)
-    DontSave,
+pub enum SaveIntent {
+    /// write all files (even if unchanged)
+    /// prompt before overwriting on-disk changes
+    Save,
+    /// write any files that have local changes
+    /// prompt before overwriting on-disk changes
+    SaveAll,
+    /// always prompt for a new path
+    SaveAs,
+    /// prompt "you have unsaved changes" before writing
+    Close,
+    /// write all dirty files, don't prompt on conflict
+    Overwrite,
+    /// skip all save-related behavior
+    Skip,
 }
 
 #[derive(Clone, Deserialize, PartialEq)]
@@ -78,8 +85,15 @@ pub struct CloseItemsToTheRightById {
 }
 
 #[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
 pub struct CloseActiveItem {
-    pub save_behavior: Option<SaveBehavior>,
+    pub save_intent: Option<SaveIntent>,
+}
+
+#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseAllItems {
+    pub save_intent: Option<SaveIntent>,
 }
 
 actions!(
@@ -92,7 +106,6 @@ actions!(
         CloseCleanItems,
         CloseItemsToTheLeft,
         CloseItemsToTheRight,
-        CloseAllItems,
         GoBack,
         GoForward,
         ReopenClosedItem,
@@ -103,7 +116,7 @@ actions!(
     ]
 );
 
-impl_actions!(pane, [ActivateItem, CloseActiveItem]);
+impl_actions!(pane, [ActivateItem, CloseActiveItem, CloseAllItems]);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -337,7 +350,7 @@ impl Pane {
                     // New menu
                     .with_child(Self::render_tab_bar_button(
                         0,
-                        "icons/plus_12.svg",
+                        "icons/plus.svg",
                         false,
                         Some(("New...".into(), None)),
                         cx,
@@ -352,7 +365,7 @@ impl Pane {
                     ))
                     .with_child(Self::render_tab_bar_button(
                         1,
-                        "icons/split_12.svg",
+                        "icons/split.svg",
                         false,
                         Some(("Split Pane".into(), None)),
                         cx,
@@ -369,10 +382,10 @@ impl Pane {
                         let icon_path;
                         let tooltip_label;
                         if pane.is_zoomed() {
-                            icon_path = "icons/minimize_8.svg";
+                            icon_path = "icons/minimize.svg";
                             tooltip_label = "Zoom In";
                         } else {
-                            icon_path = "icons/maximize_8.svg";
+                            icon_path = "icons/maximize.svg";
                             tooltip_label = "Zoom In";
                         }
 
@@ -722,7 +735,7 @@ impl Pane {
         let active_item_id = self.items[self.active_item_index].id();
         Some(self.close_item_by_id(
             active_item_id,
-            action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite),
+            action.save_intent.unwrap_or(SaveIntent::Close),
             cx,
         ))
     }
@@ -730,12 +743,10 @@ impl Pane {
     pub fn close_item_by_id(
         &mut self,
         item_id_to_close: usize,
-        save_behavior: SaveBehavior,
+        save_intent: SaveIntent,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
-        self.close_items(cx, save_behavior, move |view_id| {
-            view_id == item_id_to_close
-        })
+        self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
     }
 
     pub fn close_inactive_items(
@@ -748,11 +759,9 @@ impl Pane {
         }
 
         let active_item_id = self.items[self.active_item_index].id();
-        Some(
-            self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
-                item_id != active_item_id
-            }),
-        )
+        Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
+            item_id != active_item_id
+        }))
     }
 
     pub fn close_clean_items(
@@ -765,11 +774,9 @@ impl Pane {
             .filter(|item| !item.is_dirty(cx))
             .map(|item| item.id())
             .collect();
-        Some(
-            self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
-                item_ids.contains(&item_id)
-            }),
-        )
+        Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
+            item_ids.contains(&item_id)
+        }))
     }
 
     pub fn close_items_to_the_left(
@@ -794,7 +801,7 @@ impl Pane {
             .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
-        self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+        self.close_items(cx, SaveIntent::Close, move |item_id| {
             item_ids.contains(&item_id)
         })
     }
@@ -822,34 +829,77 @@ impl Pane {
             .take_while(|item| item.id() != item_id)
             .map(|item| item.id())
             .collect();
-        self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
+        self.close_items(cx, SaveIntent::Close, move |item_id| {
             item_ids.contains(&item_id)
         })
     }
 
     pub fn close_all_items(
         &mut self,
-        _: &CloseAllItems,
+        action: &CloseAllItems,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
         if self.items.is_empty() {
             return None;
         }
 
-        Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true))
+        Some(
+            self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
+                true
+            }),
+        )
+    }
+
+    pub(super) fn file_names_for_prompt(
+        items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
+        all_dirty_items: usize,
+        cx: &AppContext,
+    ) -> String {
+        /// Quantity of item paths displayed in prompt prior to cutoff..
+        const FILE_NAMES_CUTOFF_POINT: usize = 10;
+        let mut file_names: Vec<_> = items
+            .filter_map(|item| {
+                item.project_path(cx).and_then(|project_path| {
+                    project_path
+                        .path
+                        .file_name()
+                        .and_then(|name| name.to_str().map(ToOwned::to_owned))
+                })
+            })
+            .take(FILE_NAMES_CUTOFF_POINT)
+            .collect();
+        let should_display_followup_text =
+            all_dirty_items > FILE_NAMES_CUTOFF_POINT || file_names.len() != all_dirty_items;
+        if should_display_followup_text {
+            let not_shown_files = all_dirty_items - file_names.len();
+            if not_shown_files == 1 {
+                file_names.push(".. 1 file not shown".into());
+            } else {
+                file_names.push(format!(".. {} files not shown", not_shown_files).into());
+            }
+        }
+        let file_names = file_names.join("\n");
+        format!(
+            "Do you want to save changes to the following {} files?\n{file_names}",
+            all_dirty_items
+        )
     }
 
     pub fn close_items(
         &mut self,
         cx: &mut ViewContext<Pane>,
-        save_behavior: SaveBehavior,
+        mut save_intent: SaveIntent,
         should_close: impl 'static + Fn(usize) -> bool,
     ) -> Task<Result<()>> {
         // Find the items to close.
         let mut items_to_close = Vec::new();
+        let mut dirty_items = Vec::new();
         for item in &self.items {
             if should_close(item.id()) {
                 items_to_close.push(item.boxed_clone());
+                if item.is_dirty(cx) {
+                    dirty_items.push(item.boxed_clone());
+                }
             }
         }
 
@@ -861,6 +911,22 @@ impl Pane {
 
         let workspace = self.workspace.clone();
         cx.spawn(|pane, mut cx| async move {
+            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
+                let mut answer = pane.update(&mut cx, |_, cx| {
+                    let prompt =
+                        Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
+                    cx.prompt(
+                        PromptLevel::Warning,
+                        &prompt,
+                        &["Save all", "Discard all", "Cancel"],
+                    )
+                })?;
+                match answer.next().await {
+                    Some(0) => save_intent = SaveIntent::SaveAll,
+                    Some(1) => save_intent = SaveIntent::Skip,
+                    _ => {}
+                }
+            }
             let mut saved_project_items_ids = HashSet::default();
             for item in items_to_close.clone() {
                 // Find the item's current index and its set of project item models. Avoid
@@ -900,7 +966,7 @@ impl Pane {
                         &pane,
                         item_ix,
                         &*item,
-                        save_behavior,
+                        save_intent,
                         &mut cx,
                     )
                     .await?
@@ -998,18 +1064,17 @@ impl Pane {
         pane: &WeakViewHandle<Pane>,
         item_ix: usize,
         item: &dyn ItemHandle,
-        save_behavior: SaveBehavior,
+        save_intent: SaveIntent,
         cx: &mut AsyncAppContext,
     ) -> Result<bool> {
         const CONFLICT_MESSAGE: &str =
             "This file has changed on disk since you started editing it. Do you want to overwrite it?";
-        const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
 
-        if save_behavior == SaveBehavior::DontSave {
+        if save_intent == SaveIntent::Skip {
             return Ok(true);
         }
 
-        let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
+        let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.read(|cx| {
             (
                 item.has_conflict(cx),
                 item.is_dirty(cx),
@@ -1018,67 +1083,77 @@ impl Pane {
             )
         });
 
+        // when saving a single buffer, we ignore whether or not it's dirty.
+        if save_intent == SaveIntent::Save {
+            is_dirty = true;
+        }
+
+        if save_intent == SaveIntent::SaveAs {
+            is_dirty = true;
+            has_conflict = false;
+            can_save = false;
+        }
+
+        if save_intent == SaveIntent::Overwrite {
+            has_conflict = false;
+        }
+
         if has_conflict && can_save {
-            if save_behavior == SaveBehavior::SilentlyOverwrite {
-                pane.update(cx, |_, cx| item.save(project, cx))?.await?;
-            } else {
-                let mut answer = pane.update(cx, |pane, cx| {
-                    pane.activate_item(item_ix, true, true, cx);
-                    cx.prompt(
-                        PromptLevel::Warning,
-                        CONFLICT_MESSAGE,
-                        &["Overwrite", "Discard", "Cancel"],
-                    )
-                })?;
-                match answer.next().await {
-                    Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
-                    Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
-                    _ => return Ok(false),
-                }
+            let mut answer = pane.update(cx, |pane, cx| {
+                pane.activate_item(item_ix, true, true, cx);
+                cx.prompt(
+                    PromptLevel::Warning,
+                    CONFLICT_MESSAGE,
+                    &["Overwrite", "Discard", "Cancel"],
+                )
+            })?;
+            match answer.next().await {
+                Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
+                Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
+                _ => return Ok(false),
             }
-        } else if is_dirty && (can_save || is_singleton) {
-            let will_autosave = cx.read(|cx| {
-                matches!(
-                    settings::get::<WorkspaceSettings>(cx).autosave,
-                    AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
-                ) && Self::can_autosave_item(&*item, cx)
-            });
-            let should_save = if save_behavior == SaveBehavior::PromptOnWrite && !will_autosave {
-                let mut answer = pane.update(cx, |pane, cx| {
-                    pane.activate_item(item_ix, true, true, cx);
-                    cx.prompt(
-                        PromptLevel::Warning,
-                        DIRTY_MESSAGE,
-                        &["Save", "Don't Save", "Cancel"],
-                    )
-                })?;
-                match answer.next().await {
-                    Some(0) => true,
-                    Some(1) => false,
-                    _ => return Ok(false),
+        } else if is_dirty && (can_save || can_save_as) {
+            if save_intent == SaveIntent::Close {
+                let will_autosave = cx.read(|cx| {
+                    matches!(
+                        settings::get::<WorkspaceSettings>(cx).autosave,
+                        AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
+                    ) && Self::can_autosave_item(&*item, cx)
+                });
+                if !will_autosave {
+                    let mut answer = pane.update(cx, |pane, cx| {
+                        pane.activate_item(item_ix, true, true, cx);
+                        let prompt = dirty_message_for(item.project_path(cx));
+                        cx.prompt(
+                            PromptLevel::Warning,
+                            &prompt,
+                            &["Save", "Don't Save", "Cancel"],
+                        )
+                    })?;
+                    match answer.next().await {
+                        Some(0) => {}
+                        Some(1) => return Ok(true), // Don't save his file
+                        _ => return Ok(false),      // Cancel
+                    }
                 }
-            } else {
-                true
-            };
+            }
 
-            if should_save {
-                if can_save {
-                    pane.update(cx, |_, cx| item.save(project, cx))?.await?;
-                } else if is_singleton {
-                    let start_abs_path = project
-                        .read_with(cx, |project, cx| {
-                            let worktree = project.visible_worktrees(cx).next()?;
-                            Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
-                        })
-                        .unwrap_or_else(|| Path::new("").into());
+            if can_save {
+                pane.update(cx, |_, cx| item.save(project, cx))?.await?;
+            } else if can_save_as {
+                let start_abs_path = project
+                    .read_with(cx, |project, cx| {
+                        let worktree = project.visible_worktrees(cx).next()?;
+                        Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
+                    })
+                    .unwrap_or_else(|| Path::new("").into());
 
-                    let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
-                    if let Some(abs_path) = abs_path.next().await.flatten() {
-                        pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
-                            .await?;
-                    } else {
-                        return Ok(false);
-                    }
+                let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
+                if let Some(abs_path) = abs_path.next().await.flatten() {
+                    pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
+                        .await?;
+                } else {
+                    return Ok(false);
                 }
             }
         }
@@ -1167,15 +1242,16 @@ impl Pane {
                     vec![
                         ContextMenuItem::action(
                             "Close Active Item",
-                            CloseActiveItem {
-                                save_behavior: None,
-                            },
+                            CloseActiveItem { save_intent: None },
                         ),
                         ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
                         ContextMenuItem::action("Close Clean Items", CloseCleanItems),
                         ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
                         ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
-                        ContextMenuItem::action("Close All Items", CloseAllItems),
+                        ContextMenuItem::action(
+                            "Close All Items",
+                            CloseAllItems { save_intent: None },
+                        ),
                     ]
                 } else {
                     // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
@@ -1187,7 +1263,7 @@ impl Pane {
                                     pane.update(cx, |pane, cx| {
                                         pane.close_item_by_id(
                                             target_item_id,
-                                            SaveBehavior::PromptOnWrite,
+                                            SaveIntent::Close,
                                             cx,
                                         )
                                         .detach_and_log_err(cx);
@@ -1219,7 +1295,10 @@ impl Pane {
                                 }
                             }
                         }),
-                        ContextMenuItem::action("Close All Items", CloseAllItems),
+                        ContextMenuItem::action(
+                            "Close All Items",
+                            CloseAllItems { save_intent: None },
+                        ),
                     ]
                 },
                 cx,
@@ -1339,12 +1418,8 @@ impl Pane {
                                 .on_click(MouseButton::Middle, {
                                     let item_id = item.id();
                                     move |_, pane, cx| {
-                                        pane.close_item_by_id(
-                                            item_id,
-                                            SaveBehavior::PromptOnWrite,
-                                            cx,
-                                        )
-                                        .detach_and_log_err(cx);
+                                        pane.close_item_by_id(item_id, SaveIntent::Close, cx)
+                                            .detach_and_log_err(cx);
                                     }
                                 })
                                 .on_down(
@@ -1383,7 +1458,7 @@ impl Pane {
                         let theme = theme::current(cx).clone();
 
                         let detail = detail.clone();
-                        move |dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
+                        move |_, dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
                             let tab_style = &theme.workspace.tab_bar.dragged_tab;
                             Self::render_dragged_tab(
                                 &dragged_item.handle,
@@ -1535,7 +1610,7 @@ impl Pane {
         let close_element = if hovered {
             let item_id = item.id();
             enum TabCloseButton {}
-            let icon = Svg::new("icons/x_mark_8.svg");
+            let icon = Svg::new("icons/x.svg");
             MouseEventHandler::new::<TabCloseButton, _>(item_id, cx, |mouse_state, _| {
                 if mouse_state.hovered() {
                     icon.with_color(tab_style.icon_close_active)
@@ -1552,7 +1627,7 @@ impl Pane {
                     cx.window_context().defer(move |cx| {
                         if let Some(pane) = pane.upgrade(cx) {
                             pane.update(cx, |pane, cx| {
-                                pane.close_item_by_id(item_id, SaveBehavior::PromptOnWrite, cx)
+                                pane.close_item_by_id(item_id, SaveIntent::Close, cx)
                                     .detach_and_log_err(cx);
                             });
                         }
@@ -1701,7 +1776,7 @@ impl View for Pane {
 
                         let mut tab_row = Flex::row()
                             .with_child(nav_button(
-                                "icons/arrow_left_16.svg",
+                                "icons/arrow_left.svg",
                                 button_style.clone(),
                                 nav_button_height,
                                 tooltip_style.clone(),
@@ -1726,7 +1801,7 @@ impl View for Pane {
                             ))
                             .with_child(
                                 nav_button(
-                                    "icons/arrow_right_16.svg",
+                                    "icons/arrow_right.svg",
                                     button_style.clone(),
                                     nav_button_height,
                                     tooltip_style,
@@ -2135,6 +2210,15 @@ impl<V: 'static> Element<V> for PaneBackdrop<V> {
     }
 }
 
+fn dirty_message_for(buffer_path: Option<ProjectPath>) -> String {
+    let path = buffer_path
+        .as_ref()
+        .and_then(|p| p.path.to_str())
+        .unwrap_or(&"This buffer");
+    let path = truncate_and_remove_front(path, 80);
+    format!("{path} contains unsaved edits. Do you want to save it?")
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -2155,12 +2239,7 @@ mod tests {
 
         pane.update(cx, |pane, cx| {
             assert!(pane
-                .close_active_item(
-                    &CloseActiveItem {
-                        save_behavior: None
-                    },
-                    cx
-                )
+                .close_active_item(&CloseActiveItem { save_intent: None }, cx)
                 .is_none())
         });
     }
@@ -2412,12 +2491,7 @@ mod tests {
         assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
 
         pane.update(cx, |pane, cx| {
-            pane.close_active_item(
-                &CloseActiveItem {
-                    save_behavior: None,
-                },
-                cx,
-            )
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
         })
         .unwrap()
         .await
@@ -2428,12 +2502,7 @@ mod tests {
         assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
 
         pane.update(cx, |pane, cx| {
-            pane.close_active_item(
-                &CloseActiveItem {
-                    save_behavior: None,
-                },
-                cx,
-            )
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
         })
         .unwrap()
         .await
@@ -2441,12 +2510,7 @@ mod tests {
         assert_item_labels(&pane, ["A", "B*", "C"], cx);
 
         pane.update(cx, |pane, cx| {
-            pane.close_active_item(
-                &CloseActiveItem {
-                    save_behavior: None,
-                },
-                cx,
-            )
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
         })
         .unwrap()
         .await
@@ -2454,12 +2518,7 @@ mod tests {
         assert_item_labels(&pane, ["A", "C*"], cx);
 
         pane.update(cx, |pane, cx| {
-            pane.close_active_item(
-                &CloseActiveItem {
-                    save_behavior: None,
-                },
-                cx,
-            )
+            pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
         })
         .unwrap()
         .await
@@ -2569,10 +2628,28 @@ mod tests {
         add_labeled_item(&pane, "C", false, cx);
         assert_item_labels(&pane, ["A", "B", "C*"], cx);
 
-        pane.update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx))
-            .unwrap()
-            .await
+        pane.update(cx, |pane, cx| {
+            pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
+        })
+        .unwrap()
+        .await
+        .unwrap();
+        assert_item_labels(&pane, [], cx);
+
+        add_labeled_item(&pane, "A", true, cx);
+        add_labeled_item(&pane, "B", true, cx);
+        add_labeled_item(&pane, "C", true, cx);
+        assert_item_labels(&pane, ["A^", "B^", "C*^"], cx);
+
+        let save = pane
+            .update(cx, |pane, cx| {
+                pane.close_all_items(&CloseAllItems { save_intent: None }, cx)
+            })
             .unwrap();
+
+        cx.foreground().run_until_parked();
+        window.simulate_prompt_answer(2, cx);
+        save.await.unwrap();
         assert_item_labels(&pane, [], cx);
     }
 

crates/workspace/src/pane_group.rs πŸ”—

@@ -84,6 +84,13 @@ impl PaneGroup {
         }
     }
 
+    pub fn swap(&mut self, from: &ViewHandle<Pane>, to: &ViewHandle<Pane>) {
+        match &mut self.root {
+            Member::Pane(_) => {}
+            Member::Axis(axis) => axis.swap(from, to),
+        };
+    }
+
     pub(crate) fn render(
         &self,
         project: &ModelHandle<Project>,
@@ -183,25 +190,23 @@ impl Member {
                     })
                     .and_then(|leader_id| {
                         let room = active_call?.read(cx).room()?.read(cx);
-                        let collaborator = project.read(cx).collaborators().get(leader_id)?;
-                        let participant = room.remote_participant_for_peer_id(*leader_id)?;
-                        Some((collaborator.replica_id, participant))
+                        room.remote_participant_for_peer_id(*leader_id)
                     });
 
-                let border = if let Some((replica_id, _)) = leader.as_ref() {
-                    let leader_color = theme.editor.replica_selection_style(*replica_id).cursor;
-                    let mut border = Border::all(theme.workspace.leader_border_width, leader_color);
-                    border
+                let mut leader_border = Border::default();
+                let mut leader_status_box = None;
+                if let Some(leader) = &leader {
+                    let leader_color = theme
+                        .editor
+                        .selection_style_for_room_participant(leader.participant_index.0)
+                        .cursor;
+                    leader_border = Border::all(theme.workspace.leader_border_width, leader_color);
+                    leader_border
                         .color
                         .fade_out(1. - theme.workspace.leader_border_opacity);
-                    border.overlay = true;
-                    border
-                } else {
-                    Border::default()
-                };
+                    leader_border.overlay = true;
 
-                let leader_status_box = if let Some((_, leader)) = leader {
-                    match leader.location {
+                    leader_status_box = match leader.location {
                         ParticipantLocation::SharedProject {
                             project_id: leader_project_id,
                         } => {
@@ -210,7 +215,6 @@ impl Member {
                             } else {
                                 let leader_user = leader.user.clone();
                                 let leader_user_id = leader.user.id;
-                                let app_state = Arc::downgrade(app_state);
                                 Some(
                                     MouseEventHandler::new::<FollowIntoExternalProject, _>(
                                         pane.id(),
@@ -218,7 +222,7 @@ impl Member {
                                         |_, _| {
                                             Label::new(
                                                 format!(
-                                                    "Follow {} on their active project",
+                                                    "Follow {} to their active project",
                                                     leader_user.github_login,
                                                 ),
                                                 theme
@@ -234,16 +238,14 @@ impl Member {
                                         },
                                     )
                                     .with_cursor_style(CursorStyle::PointingHand)
-                                    .on_click(MouseButton::Left, move |_, _, cx| {
-                                        if let Some(app_state) = app_state.upgrade() {
-                                            crate::join_remote_project(
-                                                leader_project_id,
-                                                leader_user_id,
-                                                app_state,
-                                                cx,
-                                            )
-                                            .detach_and_log_err(cx);
-                                        }
+                                    .on_click(MouseButton::Left, move |_, this, cx| {
+                                        crate::join_remote_project(
+                                            leader_project_id,
+                                            leader_user_id,
+                                            this.app_state().clone(),
+                                            cx,
+                                        )
+                                        .detach_and_log_err(cx);
                                     })
                                     .aligned()
                                     .bottom()
@@ -282,13 +284,11 @@ impl Member {
                             .right()
                             .into_any(),
                         ),
-                    }
-                } else {
-                    None
-                };
+                    };
+                }
 
                 Stack::new()
-                    .with_child(pane_element.contained().with_border(border))
+                    .with_child(pane_element.contained().with_border(leader_border))
                     .with_children(leader_status_box)
                     .into_any()
             }
@@ -428,6 +428,21 @@ impl PaneAxis {
         }
     }
 
+    fn swap(&mut self, from: &ViewHandle<Pane>, to: &ViewHandle<Pane>) {
+        for member in self.members.iter_mut() {
+            match member {
+                Member::Axis(axis) => axis.swap(from, to),
+                Member::Pane(pane) => {
+                    if pane == from {
+                        *member = Member::Pane(to.clone());
+                    } else if pane == to {
+                        *member = Member::Pane(from.clone())
+                    }
+                }
+            }
+        }
+    }
+
     fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
         debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
 

crates/workspace/src/searchable.rs πŸ”—

@@ -1,4 +1,4 @@
-use std::any::Any;
+use std::{any::Any, sync::Arc};
 
 use gpui::{
     AnyViewHandle, AnyWeakViewHandle, AppContext, Subscription, Task, ViewContext, ViewHandle,
@@ -25,6 +25,8 @@ pub struct SearchOptions {
     pub case: bool,
     pub word: bool,
     pub regex: bool,
+    /// Specifies whether the item supports search & replace.
+    pub replacement: bool,
 }
 
 pub trait SearchableItem: Item {
@@ -35,6 +37,7 @@ pub trait SearchableItem: Item {
             case: true,
             word: true,
             regex: true,
+            replacement: true,
         }
     }
     fn to_search_event(
@@ -52,6 +55,7 @@ pub trait SearchableItem: Item {
         cx: &mut ViewContext<Self>,
     );
     fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
+    fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>);
     fn match_index_for_direction(
         &mut self,
         matches: &Vec<Self::Match>,
@@ -74,7 +78,7 @@ pub trait SearchableItem: Item {
     }
     fn find_matches(
         &mut self,
-        query: SearchQuery,
+        query: Arc<SearchQuery>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Vec<Self::Match>>;
     fn active_match_index(
@@ -103,6 +107,7 @@ pub trait SearchableItemHandle: ItemHandle {
         cx: &mut WindowContext,
     );
     fn select_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut WindowContext);
+    fn replace(&self, _: &Box<dyn Any + Send>, _: &SearchQuery, _: &mut WindowContext);
     fn match_index_for_direction(
         &self,
         matches: &Vec<Box<dyn Any + Send>>,
@@ -113,7 +118,7 @@ pub trait SearchableItemHandle: ItemHandle {
     ) -> usize;
     fn find_matches(
         &self,
-        query: SearchQuery,
+        query: Arc<SearchQuery>,
         cx: &mut WindowContext,
     ) -> Task<Vec<Box<dyn Any + Send>>>;
     fn active_match_index(
@@ -189,7 +194,7 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
     }
     fn find_matches(
         &self,
-        query: SearchQuery,
+        query: Arc<SearchQuery>,
         cx: &mut WindowContext,
     ) -> Task<Vec<Box<dyn Any + Send>>> {
         let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
@@ -209,6 +214,11 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
         let matches = downcast_matches(matches);
         self.update(cx, |this, cx| this.active_match_index(matches, cx))
     }
+
+    fn replace(&self, matches: &Box<dyn Any + Send>, query: &SearchQuery, cx: &mut WindowContext) {
+        let matches = matches.downcast_ref().unwrap();
+        self.update(cx, |this, cx| this.replace(matches, query, cx))
+    }
 }
 
 fn downcast_matches<T: Any + Clone>(matches: &Vec<Box<dyn Any + Send>>) -> Vec<T> {

crates/workspace/src/shared_screen.rs πŸ”—

@@ -112,7 +112,7 @@ impl Item for SharedScreen {
     ) -> gpui::AnyElement<V> {
         Flex::row()
             .with_child(
-                Svg::new("icons/disable_screen_sharing_12.svg")
+                Svg::new("icons/desktop.svg")
                     .with_color(style.label.text.color)
                     .constrained()
                     .with_width(style.type_icon_width)

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

@@ -33,8 +33,8 @@ use gpui::{
     },
     impl_actions,
     platform::{
-        CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
-        WindowOptions,
+        CursorStyle, ModifiersChangedEvent, MouseButton, PathPromptOptions, Platform, PromptLevel,
+        WindowBounds, WindowOptions,
     },
     AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
     ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
@@ -79,7 +79,7 @@ use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use theme::{Theme, ThemeSettings};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use util::{async_iife, ResultExt};
+use util::ResultExt;
 pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
 
 lazy_static! {
@@ -126,9 +126,8 @@ actions!(
         CloseInactiveTabsAndPanes,
         AddFolderToProject,
         Unfollow,
-        Save,
         SaveAs,
-        SaveAll,
+        ReloadActiveItem,
         ActivatePreviousPane,
         ActivateNextPane,
         FollowNextCollaborator,
@@ -158,6 +157,30 @@ pub struct ActivatePane(pub usize);
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct ActivatePaneInDirection(pub SplitDirection);
 
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct SwapPaneInDirection(pub SplitDirection);
+
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct NewFileInDirection(pub SplitDirection);
+
+#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SaveAll {
+    pub save_intent: Option<SaveIntent>,
+}
+
+#[derive(Clone, PartialEq, Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Save {
+    pub save_intent: Option<SaveIntent>,
+}
+
+#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseAllItemsAndPanes {
+    pub save_intent: Option<SaveIntent>,
+}
+
 #[derive(Deserialize)]
 pub struct Toast {
     id: usize,
@@ -203,7 +226,25 @@ impl Clone for Toast {
     }
 }
 
-impl_actions!(workspace, [ActivatePane, ActivatePaneInDirection, Toast]);
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct OpenTerminal {
+    pub working_directory: PathBuf,
+}
+
+impl_actions!(
+    workspace,
+    [
+        ActivatePane,
+        ActivatePaneInDirection,
+        SwapPaneInDirection,
+        NewFileInDirection,
+        Toast,
+        OpenTerminal,
+        SaveAll,
+        Save,
+        CloseAllItemsAndPanes,
+    ]
+);
 
 pub type WorkspaceId = i64;
 
@@ -243,6 +284,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     cx.add_async_action(Workspace::follow_next_collaborator);
     cx.add_async_action(Workspace::close);
     cx.add_async_action(Workspace::close_inactive_items_and_panes);
+    cx.add_async_action(Workspace::close_all_items_and_panes);
     cx.add_global_action(Workspace::close_global);
     cx.add_global_action(restart);
     cx.add_async_action(Workspace::save_all);
@@ -254,13 +296,17 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         },
     );
     cx.add_action(
-        |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
-            workspace.save_active_item(false, cx).detach_and_log_err(cx);
+        |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
+            workspace
+                .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
+                .detach_and_log_err(cx);
         },
     );
     cx.add_action(
         |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
-            workspace.save_active_item(true, cx).detach_and_log_err(cx);
+            workspace
+                .save_active_item(SaveIntent::SaveAs, cx)
+                .detach_and_log_err(cx);
         },
     );
     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
@@ -276,6 +322,12 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         },
     );
 
+    cx.add_action(
+        |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
+            workspace.swap_pane_in_direction(action.0, cx)
+        },
+    );
+
     cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
         workspace.toggle_dock(DockPosition::Left, cx);
     });
@@ -323,11 +375,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         })
         .detach();
     });
-
-    let client = &app_state.client;
-    client.add_view_request_handler(Workspace::handle_follow);
-    client.add_view_message_handler(Workspace::handle_unfollow);
-    client.add_view_message_handler(Workspace::handle_update_followers);
 }
 
 type ProjectItemBuilders = HashMap<
@@ -404,6 +451,7 @@ pub struct AppState {
     pub client: Arc<Client>,
     pub user_store: ModelHandle<UserStore>,
     pub channel_store: ModelHandle<ChannelStore>,
+    pub workspace_store: ModelHandle<WorkspaceStore>,
     pub fs: Arc<dyn fs::Fs>,
     pub build_window_options:
         fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
@@ -412,6 +460,19 @@ pub struct AppState {
     pub background_actions: BackgroundActions,
 }
 
+pub struct WorkspaceStore {
+    workspaces: HashSet<WeakViewHandle<Workspace>>,
+    followers: Vec<Follower>,
+    client: Arc<Client>,
+    _subscriptions: Vec<client::Subscription>,
+}
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
+struct Follower {
+    project_id: Option<u64>,
+    peer_id: PeerId,
+}
+
 impl AppState {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut AppContext) -> Arc<Self> {
@@ -428,6 +489,7 @@ impl AppState {
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
         let channel_store =
             cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+        let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
         theme::init((), cx);
         client::init(&client, cx);
@@ -439,6 +501,7 @@ impl AppState {
             languages,
             user_store,
             channel_store,
+            workspace_store,
             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
             build_window_options: |_, _, _| Default::default(),
             background_actions: || &[],
@@ -499,7 +562,6 @@ pub enum Event {
 
 pub struct Workspace {
     weak_self: WeakViewHandle<Self>,
-    remote_entity_subscription: Option<client::Subscription>,
     modal: Option<ActiveModal>,
     zoomed: Option<AnyWeakViewHandle>,
     zoomed_position: Option<DockPosition>,
@@ -511,11 +573,11 @@ pub struct Workspace {
     panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
     active_pane: ViewHandle<Pane>,
     last_active_center_pane: Option<WeakViewHandle<Pane>>,
+    last_active_view_id: Option<proto::ViewId>,
     status_bar: ViewHandle<StatusBar>,
     titlebar_item: Option<AnyViewHandle>,
     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
     project: ModelHandle<Project>,
-    leader_state: LeaderState,
     follower_states_by_leader: FollowerStatesByLeader,
     last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
     window_edited: bool,
@@ -541,11 +603,6 @@ pub struct ViewId {
     pub id: u64,
 }
 
-#[derive(Default)]
-struct LeaderState {
-    followers: HashSet<PeerId>,
-}
-
 type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
 
 #[derive(Default)]
@@ -566,9 +623,8 @@ impl Workspace {
         cx.observe(&project, |_, _, cx| cx.notify()).detach();
         cx.subscribe(&project, move |this, _, event, cx| {
             match event {
-                project::Event::RemoteIdChanged(remote_id) => {
+                project::Event::RemoteIdChanged(_) => {
                     this.update_window_title(cx);
-                    this.project_remote_id_changed(*remote_id, cx);
                 }
 
                 project::Event::CollaboratorLeft(peer_id) => {
@@ -623,6 +679,10 @@ impl Workspace {
         cx.focus(&center_pane);
         cx.emit(Event::PaneAdded(center_pane.clone()));
 
+        app_state.workspace_store.update(cx, |store, _| {
+            store.workspaces.insert(weak_handle.clone());
+        });
+
         let mut current_user = app_state.user_store.read(cx).watch_current_user();
         let mut connection_status = app_state.client.status();
         let _observe_current_user = cx.spawn(|this, mut cx| async move {
@@ -716,7 +776,8 @@ impl Workspace {
             }),
         ];
 
-        let mut this = Workspace {
+        cx.defer(|this, cx| this.update_window_title(cx));
+        Workspace {
             weak_self: weak_handle.clone(),
             modal: None,
             zoomed: None,
@@ -726,15 +787,14 @@ impl Workspace {
             panes_by_item: Default::default(),
             active_pane: center_pane.clone(),
             last_active_center_pane: Some(center_pane.downgrade()),
+            last_active_view_id: None,
             status_bar,
             titlebar_item: None,
             notifications: Default::default(),
-            remote_entity_subscription: None,
             left_dock,
             bottom_dock,
             right_dock,
             project: project.clone(),
-            leader_state: Default::default(),
             follower_states_by_leader: Default::default(),
             last_leaders_by_pane: Default::default(),
             window_edited: false,
@@ -747,10 +807,7 @@ impl Workspace {
             leader_updates_tx,
             subscriptions,
             pane_history_timestamp,
-        };
-        this.project_remote_id_changed(project.read(cx).remote_id(), cx);
-        cx.defer(|this, cx| this.update_window_title(cx));
-        this
+        }
     }
 
     fn new_local(
@@ -879,7 +936,8 @@ impl Workspace {
                 app_state,
                 cx,
             )
-            .await;
+            .await
+            .unwrap_or_default();
 
             (workspace, opened_items)
         })
@@ -1309,14 +1367,19 @@ impl Workspace {
 
             Ok(this
                 .update(&mut cx, |this, cx| {
-                    this.save_all_internal(SaveBehavior::PromptOnWrite, cx)
+                    this.save_all_internal(SaveIntent::Close, cx)
                 })?
                 .await?)
         })
     }
 
-    fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
-        let save_all = self.save_all_internal(SaveBehavior::PromptOnConflict, cx);
+    fn save_all(
+        &mut self,
+        action: &SaveAll,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let save_all =
+            self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx);
         Some(cx.foreground().spawn(async move {
             save_all.await?;
             Ok(())
@@ -1325,13 +1388,12 @@ impl Workspace {
 
     fn save_all_internal(
         &mut self,
-        save_behaviour: SaveBehavior,
+        mut save_intent: SaveIntent,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
         if self.project.read(cx).is_read_only() {
             return Task::ready(Ok(true));
         }
-
         let dirty_items = self
             .panes
             .iter()
@@ -1347,7 +1409,27 @@ impl Workspace {
             .collect::<Vec<_>>();
 
         let project = self.project.clone();
-        cx.spawn(|_, mut cx| async move {
+        cx.spawn(|workspace, mut cx| async move {
+            // Override save mode and display "Save all files" prompt
+            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
+                let mut answer = workspace.update(&mut cx, |_, cx| {
+                    let prompt = Pane::file_names_for_prompt(
+                        &mut dirty_items.iter().map(|(_, handle)| handle),
+                        dirty_items.len(),
+                        cx,
+                    );
+                    cx.prompt(
+                        PromptLevel::Warning,
+                        &prompt,
+                        &["Save all", "Discard all", "Cancel"],
+                    )
+                })?;
+                match answer.next().await {
+                    Some(0) => save_intent = SaveIntent::SaveAll,
+                    Some(1) => save_intent = SaveIntent::Skip,
+                    _ => {}
+                }
+            }
             for (pane, item) in dirty_items {
                 let (singleton, project_entry_ids) =
                     cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
@@ -1360,7 +1442,7 @@ impl Workspace {
                             &pane,
                             ix,
                             &*item,
-                            save_behaviour,
+                            save_intent,
                             &mut cx,
                         )
                         .await?
@@ -1632,75 +1714,72 @@ impl Workspace {
 
     pub fn save_active_item(
         &mut self,
-        force_name_change: bool,
+        save_intent: SaveIntent,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         let project = self.project.clone();
-        if let Some(item) = self.active_item(cx) {
-            if !force_name_change && item.can_save(cx) {
-                if item.has_conflict(cx) {
-                    const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
+        let pane = self.active_pane();
+        let item_ix = pane.read(cx).active_item_index();
+        let item = pane.read(cx).active_item();
+        let pane = pane.downgrade();
 
-                    let mut answer = cx.prompt(
-                        PromptLevel::Warning,
-                        CONFLICT_MESSAGE,
-                        &["Overwrite", "Cancel"],
-                    );
-                    cx.spawn(|this, mut cx| async move {
-                        let answer = answer.recv().await;
-                        if answer == Some(0) {
-                            this.update(&mut cx, |this, cx| item.save(this.project.clone(), cx))?
-                                .await?;
-                        }
-                        Ok(())
-                    })
-                } else {
-                    item.save(self.project.clone(), cx)
-                }
-            } else if item.is_singleton(cx) {
-                let worktree = self.worktrees(cx).next();
-                let start_abs_path = worktree
-                    .and_then(|w| w.read(cx).as_local())
-                    .map_or(Path::new(""), |w| w.abs_path())
-                    .to_path_buf();
-                let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
-                cx.spawn(|this, mut cx| async move {
-                    if let Some(abs_path) = abs_path.recv().await.flatten() {
-                        this.update(&mut cx, |_, cx| item.save_as(project, abs_path, cx))?
-                            .await?;
-                    }
-                    Ok(())
-                })
+        cx.spawn(|_, mut cx| async move {
+            if let Some(item) = item {
+                Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
+                    .await
+                    .map(|_| ())
             } else {
-                Task::ready(Ok(()))
+                Ok(())
             }
-        } else {
-            Task::ready(Ok(()))
-        }
+        })
     }
 
     pub fn close_inactive_items_and_panes(
         &mut self,
         _: &CloseInactiveTabsAndPanes,
         cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        self.close_all_internal(true, SaveIntent::Close, cx)
+    }
+
+    pub fn close_all_items_and_panes(
+        &mut self,
+        action: &CloseAllItemsAndPanes,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
+    }
+
+    fn close_all_internal(
+        &mut self,
+        retain_active_pane: bool,
+        save_intent: SaveIntent,
+        cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
         let current_pane = self.active_pane();
 
         let mut tasks = Vec::new();
 
-        if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
-            pane.close_inactive_items(&CloseInactiveItems, cx)
-        }) {
-            tasks.push(current_pane_close);
-        };
+        if retain_active_pane {
+            if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
+                pane.close_inactive_items(&CloseInactiveItems, cx)
+            }) {
+                tasks.push(current_pane_close);
+            };
+        }
 
         for pane in self.panes() {
-            if pane.id() == current_pane.id() {
+            if retain_active_pane && pane.id() == current_pane.id() {
                 continue;
             }
 
             if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
-                pane.close_all_items(&CloseAllItems, cx)
+                pane.close_all_items(
+                    &CloseAllItems {
+                        save_intent: Some(save_intent),
+                    },
+                    cx,
+                )
             }) {
                 tasks.push(close_pane_items)
             }
@@ -1931,8 +2010,13 @@ impl Workspace {
             .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
     }
 
-    pub fn split_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
-        let new_pane = self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
+    pub fn split_item(
+        &mut self,
+        split_direction: SplitDirection,
+        item: Box<dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
         new_pane.update(cx, move |new_pane, cx| {
             new_pane.add_item(item, true, true, None, cx)
         })
@@ -2110,7 +2194,7 @@ impl Workspace {
         }
 
         let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
-        self.split_item(Box::new(item.clone()), cx);
+        self.split_item(SplitDirection::Right, Box::new(item.clone()), cx);
         item
     }
 
@@ -2168,11 +2252,32 @@ impl Workspace {
         direction: SplitDirection,
         cx: &mut ViewContext<Self>,
     ) {
-        let bounding_box = match self.center.bounding_box_for_pane(&self.active_pane) {
-            Some(coordinates) => coordinates,
-            None => {
-                return;
-            }
+        if let Some(pane) = self.find_pane_in_direction(direction, cx) {
+            cx.focus(pane);
+        }
+    }
+
+    pub fn swap_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(to) = self
+            .find_pane_in_direction(direction, cx)
+            .map(|pane| pane.clone())
+        {
+            self.center.swap(&self.active_pane.clone(), &to);
+            cx.notify();
+        }
+    }
+
+    fn find_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<&ViewHandle<Pane>> {
+        let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
+            return None;
         };
         let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
         let center = match cursor {
@@ -2188,10 +2293,7 @@ impl Workspace {
             SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
             SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
         };
-
-        if let Some(pane) = self.center.pane_at_pixel_position(target) {
-            cx.focus(pane);
-        }
+        self.center.pane_at_pixel_position(target)
     }
 
     fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
@@ -2410,43 +2512,24 @@ impl Workspace {
         &self.active_pane
     }
 
-    fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
-        if let Some(remote_id) = remote_id {
-            self.remote_entity_subscription = Some(
-                self.app_state
-                    .client
-                    .add_view_for_remote_entity(remote_id, cx),
-            );
-        } else {
-            self.remote_entity_subscription.take();
-        }
-    }
-
     fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
-        self.leader_state.followers.remove(&peer_id);
         if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
             for state in states_by_pane.into_values() {
                 for item in state.items_by_leader_view_id.into_values() {
-                    item.set_leader_replica_id(None, cx);
+                    item.set_leader_peer_id(None, cx);
                 }
             }
         }
         cx.notify();
     }
 
-    pub fn toggle_follow(
+    fn start_following(
         &mut self,
         leader_id: PeerId,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
         let pane = self.active_pane().clone();
 
-        if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
-            if leader_id == prev_leader_id {
-                return None;
-            }
-        }
-
         self.last_leaders_by_pane
             .insert(pane.downgrade(), leader_id);
         self.follower_states_by_leader
@@ -2455,8 +2538,10 @@ impl Workspace {
             .insert(pane.clone(), Default::default());
         cx.notify();
 
-        let project_id = self.project.read(cx).remote_id()?;
+        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+        let project_id = self.project.read(cx).remote_id();
         let request = self.app_state.client.request(proto::Follow {
+            room_id,
             project_id,
             leader_id: Some(leader_id),
         });
@@ -2515,9 +2600,64 @@ impl Workspace {
             None
         };
 
-        next_leader_id
-            .or_else(|| collaborators.keys().copied().next())
-            .and_then(|leader_id| self.toggle_follow(leader_id, cx))
+        let pane = self.active_pane.clone();
+        let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
+        else {
+            return None;
+        };
+        if Some(leader_id) == self.unfollow(&pane, cx) {
+            return None;
+        }
+        self.follow(leader_id, cx)
+    }
+
+    pub fn follow(
+        &mut self,
+        leader_id: PeerId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
+        let project = self.project.read(cx);
+
+        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
+            return None;
+        };
+
+        let other_project_id = match remote_participant.location {
+            call::ParticipantLocation::External => None,
+            call::ParticipantLocation::UnsharedProject => None,
+            call::ParticipantLocation::SharedProject { project_id } => {
+                if Some(project_id) == project.remote_id() {
+                    None
+                } else {
+                    Some(project_id)
+                }
+            }
+        };
+
+        // if they are active in another project, follow there.
+        if let Some(project_id) = other_project_id {
+            let app_state = self.app_state.clone();
+            return Some(crate::join_remote_project(
+                project_id,
+                remote_participant.user.id,
+                app_state,
+                cx,
+            ));
+        }
+
+        // if you're already following, find the right pane and focus it.
+        for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader {
+            if leader_id == *existing_leader_id {
+                for (pane, _) in states_by_pane {
+                    cx.focus(pane);
+                    return None;
+                }
+            }
+        }
+
+        // Otherwise, follow.
+        self.start_following(leader_id, cx)
     }
 
     pub fn unfollow(
@@ -2529,20 +2669,21 @@ impl Workspace {
             let leader_id = *leader_id;
             if let Some(state) = states_by_pane.remove(pane) {
                 for (_, item) in state.items_by_leader_view_id {
-                    item.set_leader_replica_id(None, cx);
+                    item.set_leader_peer_id(None, cx);
                 }
 
                 if states_by_pane.is_empty() {
                     self.follower_states_by_leader.remove(&leader_id);
-                    if let Some(project_id) = self.project.read(cx).remote_id() {
-                        self.app_state
-                            .client
-                            .send(proto::Unfollow {
-                                project_id,
-                                leader_id: Some(leader_id),
-                            })
-                            .log_err();
-                    }
+                    let project_id = self.project.read(cx).remote_id();
+                    let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+                    self.app_state
+                        .client
+                        .send(proto::Unfollow {
+                            room_id,
+                            project_id,
+                            leader_id: Some(leader_id),
+                        })
+                        .log_err();
                 }
 
                 cx.notify();
@@ -2556,10 +2697,6 @@ impl Workspace {
         self.follower_states_by_leader.contains_key(&peer_id)
     }
 
-    pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
-        self.leader_state.followers.contains(&peer_id)
-    }
-
     fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         // TODO: There should be a better system in place for this
         // (https://github.com/zed-industries/zed/issues/1290)
@@ -2710,81 +2847,65 @@ impl Workspace {
 
     // RPC handlers
 
-    async fn handle_follow(
-        this: WeakViewHandle<Self>,
-        envelope: TypedEnvelope<proto::Follow>,
-        _: Arc<Client>,
-        mut cx: AsyncAppContext,
-    ) -> Result<proto::FollowResponse> {
-        this.update(&mut cx, |this, cx| {
-            let client = &this.app_state.client;
-            this.leader_state
-                .followers
-                .insert(envelope.original_sender_id()?);
+    fn handle_follow(
+        &mut self,
+        follower_project_id: Option<u64>,
+        cx: &mut ViewContext<Self>,
+    ) -> proto::FollowResponse {
+        let client = &self.app_state.client;
+        let project_id = self.project.read(cx).remote_id();
 
-            let active_view_id = this.active_item(cx).and_then(|i| {
-                Some(
-                    i.to_followable_item_handle(cx)?
-                        .remote_id(client, cx)?
-                        .to_proto(),
-                )
-            });
+        let active_view_id = self.active_item(cx).and_then(|i| {
+            Some(
+                i.to_followable_item_handle(cx)?
+                    .remote_id(client, cx)?
+                    .to_proto(),
+            )
+        });
 
-            cx.notify();
+        cx.notify();
 
-            Ok(proto::FollowResponse {
-                active_view_id,
-                views: this
-                    .panes()
-                    .iter()
-                    .flat_map(|pane| {
-                        let leader_id = this.leader_for_pane(pane);
-                        pane.read(cx).items().filter_map({
-                            let cx = &cx;
-                            move |item| {
-                                let item = item.to_followable_item_handle(cx)?;
-                                let id = item.remote_id(client, cx)?.to_proto();
-                                let variant = item.to_state_proto(cx)?;
-                                Some(proto::View {
-                                    id: Some(id),
-                                    leader_id,
-                                    variant: Some(variant),
-                                })
+        self.last_active_view_id = active_view_id.clone();
+        proto::FollowResponse {
+            active_view_id,
+            views: self
+                .panes()
+                .iter()
+                .flat_map(|pane| {
+                    let leader_id = self.leader_for_pane(pane);
+                    pane.read(cx).items().filter_map({
+                        let cx = &cx;
+                        move |item| {
+                            let item = item.to_followable_item_handle(cx)?;
+                            if project_id.is_some()
+                                && project_id != follower_project_id
+                                && item.is_project_item(cx)
+                            {
+                                return None;
                             }
-                        })
+                            let id = item.remote_id(client, cx)?.to_proto();
+                            let variant = item.to_state_proto(cx)?;
+                            Some(proto::View {
+                                id: Some(id),
+                                leader_id,
+                                variant: Some(variant),
+                            })
+                        }
                     })
-                    .collect(),
-            })
-        })?
-    }
-
-    async fn handle_unfollow(
-        this: WeakViewHandle<Self>,
-        envelope: TypedEnvelope<proto::Unfollow>,
-        _: Arc<Client>,
-        mut cx: AsyncAppContext,
-    ) -> Result<()> {
-        this.update(&mut cx, |this, cx| {
-            this.leader_state
-                .followers
-                .remove(&envelope.original_sender_id()?);
-            cx.notify();
-            Ok(())
-        })?
+                })
+                .collect(),
+        }
     }
 
-    async fn handle_update_followers(
-        this: WeakViewHandle<Self>,
-        envelope: TypedEnvelope<proto::UpdateFollowers>,
-        _: Arc<Client>,
-        cx: AsyncAppContext,
-    ) -> Result<()> {
-        let leader_id = envelope.original_sender_id()?;
-        this.read_with(&cx, |this, _| {
-            this.leader_updates_tx
-                .unbounded_send((leader_id, envelope.payload))
-        })??;
-        Ok(())
+    fn handle_update_followers(
+        &mut self,
+        leader_id: PeerId,
+        message: proto::UpdateFollowers,
+        _cx: &mut ViewContext<Self>,
+    ) {
+        self.leader_updates_tx
+            .unbounded_send((leader_id, message))
+            .ok();
     }
 
     async fn process_leader_update(
@@ -2857,18 +2978,6 @@ impl Workspace {
         let this = this
             .upgrade(cx)
             .ok_or_else(|| anyhow!("workspace dropped"))?;
-        let project = this
-            .read_with(cx, |this, _| this.project.clone())
-            .ok_or_else(|| anyhow!("window dropped"))?;
-
-        let replica_id = project
-            .read_with(cx, |project, _| {
-                project
-                    .collaborators()
-                    .get(&leader_id)
-                    .map(|c| c.replica_id)
-            })
-            .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
 
         let item_builders = cx.update(|cx| {
             cx.default_global::<FollowableItemBuilders>()
@@ -2913,7 +3022,7 @@ impl Workspace {
                     .get_mut(&pane)?;
 
                 for (id, item) in leader_view_ids.into_iter().zip(items) {
-                    item.set_leader_replica_id(Some(replica_id), cx);
+                    item.set_leader_peer_id(Some(leader_id), cx);
                     state.items_by_leader_view_id.insert(id, item);
                 }
 
@@ -2923,25 +3032,29 @@ impl Workspace {
         Ok(())
     }
 
-    fn update_active_view_for_followers(&self, cx: &AppContext) {
+    fn update_active_view_for_followers(&mut self, cx: &AppContext) {
+        let mut is_project_item = true;
+        let mut update = proto::UpdateActiveView::default();
         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())
-                    }),
+            let item = self
+                .active_item(cx)
+                .and_then(|item| item.to_followable_item_handle(cx));
+            if let Some(item) = item {
+                is_project_item = item.is_project_item(cx);
+                update = proto::UpdateActiveView {
+                    id: item
+                        .remote_id(&self.app_state.client, cx)
+                        .map(|id| id.to_proto()),
                     leader_id: self.leader_for_pane(&self.active_pane),
-                }),
-                cx,
-            );
-        } else {
+                };
+            }
+        }
+
+        if update.id != self.last_active_view_id {
+            self.last_active_view_id = update.id.clone();
             self.update_followers(
-                proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
-                    id: None,
-                    leader_id: None,
-                }),
+                is_project_item,
+                proto::update_followers::Variant::UpdateActiveView(update),
                 cx,
             );
         }
@@ -2949,21 +3062,18 @@ impl Workspace {
 
     fn update_followers(
         &self,
+        project_only: bool,
         update: proto::update_followers::Variant,
         cx: &AppContext,
     ) -> Option<()> {
-        let project_id = self.project.read(cx).remote_id()?;
-        if !self.leader_state.followers.is_empty() {
-            self.app_state
-                .client
-                .send(proto::UpdateFollowers {
-                    project_id,
-                    follower_ids: self.leader_state.followers.iter().copied().collect(),
-                    variant: Some(update),
-                })
-                .log_err();
-        }
-        None
+        let project_id = if project_only {
+            self.project.read(cx).remote_id()
+        } else {
+            None
+        };
+        self.app_state().workspace_store.read_with(cx, |store, cx| {
+            store.update_followers(project_id, update, cx)
+        })
     }
 
     pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
@@ -2985,31 +3095,39 @@ impl Workspace {
         let room = call.read(cx).room()?.read(cx);
         let participant = room.remote_participant_for_peer_id(leader_id)?;
         let mut items_to_activate = Vec::new();
+
+        let leader_in_this_app;
+        let leader_in_this_project;
         match participant.location {
             call::ParticipantLocation::SharedProject { project_id } => {
-                if Some(project_id) == self.project.read(cx).remote_id() {
-                    for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
-                        if let Some(item) = state
-                            .active_view_id
-                            .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)));
-                        }
-                    }
-                }
+                leader_in_this_app = true;
+                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
+            }
+            call::ParticipantLocation::UnsharedProject => {
+                leader_in_this_app = true;
+                leader_in_this_project = false;
             }
-            call::ParticipantLocation::UnsharedProject => {}
             call::ParticipantLocation::External => {
-                for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
-                    if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
-                        items_to_activate.push((pane.clone(), Box::new(shared_screen)));
+                leader_in_this_app = false;
+                leader_in_this_project = false;
+            }
+        };
+
+        for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
+            if leader_in_this_app {
+                let item = state
+                    .active_view_id
+                    .and_then(|id| state.items_by_leader_view_id.get(&id));
+                if let Some(item) = item {
+                    if leader_in_this_project || !item.is_project_item(cx) {
+                        items_to_activate.push((pane.clone(), item.boxed_clone()));
                     }
+                    continue;
                 }
             }
+            if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
+                items_to_activate.push((pane.clone(), Box::new(shared_screen)));
+            }
         }
 
         for (pane, item) in items_to_activate {

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.104.0"
+version = "0.108.0"
 publish = false
 
 [lib]
@@ -50,7 +50,7 @@ language_selector = { path = "../language_selector" }
 lsp = { path = "../lsp" }
 language_tools = { path = "../language_tools" }
 node_runtime = { path = "../node_runtime" }
-ai = { path = "../ai" }
+assistant = { path = "../assistant" }
 outline = { path = "../outline" }
 plugin_runtime = { path = "../plugin_runtime",optional = true }
 project = { path = "../project" }
@@ -62,6 +62,7 @@ rpc = { path = "../rpc" }
 settings = { path = "../settings" }
 feature_flags = { path = "../feature_flags" }
 sum_tree = { path = "../sum_tree" }
+shellexpand = "2.1.0"
 text = { path = "../text" }
 terminal_view = { path = "../terminal_view" }
 theme = { path = "../theme" }
@@ -99,6 +100,7 @@ rust-embed.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
+schemars.workspace = true
 simplelog = "0.9"
 smallvec.workspace = true
 smol.workspace = true
@@ -136,8 +138,7 @@ tree-sitter-nu.workspace = true
 
 url = "2.2"
 urlencoding = "2.1.2"
-uuid = { version = "1.1.2", features = ["v4"] }
-owning_ref = "0.4.1"
+uuid.workspace = true
 
 [dev-dependencies]
 call = { path = "../call", features = ["test-support"] }

crates/zed/src/languages.rs πŸ”—

@@ -1,10 +1,13 @@
 use anyhow::Context;
+use gpui::AppContext;
 pub use language::*;
 use node_runtime::NodeRuntime;
 use rust_embed::RustEmbed;
 use std::{borrow::Cow, str, sync::Arc};
 use util::asset_str;
 
+use self::elixir::ElixirSettings;
+
 mod c;
 mod css;
 mod elixir;
@@ -37,7 +40,13 @@ mod yaml;
 #[exclude = "*.rs"]
 struct LanguageDir;
 
-pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<dyn NodeRuntime>) {
+pub fn init(
+    languages: Arc<LanguageRegistry>,
+    node_runtime: Arc<dyn NodeRuntime>,
+    cx: &mut AppContext,
+) {
+    settings::register::<elixir::ElixirSettings>(cx);
+
     let language = |name, grammar, adapters| {
         languages.register(name, load_config(name), grammar, adapters, load_queries)
     };
@@ -61,11 +70,28 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<dyn NodeRuntime>
             Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
         ],
     );
-    language(
-        "elixir",
-        tree_sitter_elixir::language(),
-        vec![Arc::new(elixir::ElixirLspAdapter)],
-    );
+
+    match &settings::get::<ElixirSettings>(cx).lsp {
+        elixir::ElixirLspSetting::ElixirLs => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![Arc::new(elixir::ElixirLspAdapter)],
+        ),
+        elixir::ElixirLspSetting::NextLs => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![Arc::new(elixir::NextLspAdapter)],
+        ),
+        elixir::ElixirLspSetting::Local { path, arguments } => language(
+            "elixir",
+            tree_sitter_elixir::language(),
+            vec![Arc::new(elixir::LocalLspAdapter {
+                path: path.clone(),
+                arguments: arguments.clone(),
+            })],
+        ),
+    }
+
     language(
         "go",
         tree_sitter_go::language(),

crates/zed/src/languages/elixir.rs πŸ”—

@@ -1,12 +1,17 @@
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
 use gpui::{AsyncAppContext, Task};
 pub use language::*;
 use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Setting;
 use smol::fs::{self, File};
 use std::{
     any::Any,
+    env::consts,
+    ops::Deref,
     path::PathBuf,
     sync::{
         atomic::{AtomicBool, Ordering::SeqCst},
@@ -14,11 +19,50 @@ use std::{
     },
 };
 use util::{
+    async_iife,
     fs::remove_matching,
     github::{latest_github_release, GitHubLspBinaryVersion},
     ResultExt,
 };
 
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct ElixirSettings {
+    pub lsp: ElixirLspSetting,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ElixirLspSetting {
+    ElixirLs,
+    NextLs,
+    Local {
+        path: String,
+        arguments: Vec<String>,
+    },
+}
+
+#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
+pub struct ElixirSettingsContent {
+    lsp: Option<ElixirLspSetting>,
+}
+
+impl Setting for ElixirSettings {
+    const KEY: Option<&'static str> = Some("elixir");
+
+    type FileContent = ElixirSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> Result<Self>
+    where
+        Self: Sized,
+    {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
 pub struct ElixirLspAdapter;
 
 #[async_trait]
@@ -144,14 +188,14 @@ impl LspAdapter for ElixirLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir).await
+        get_cached_server_binary_elixir_ls(container_dir).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir).await
+        get_cached_server_binary_elixir_ls(container_dir).await
     }
 
     async fn label_for_completion(
@@ -238,7 +282,9 @@ impl LspAdapter for ElixirLspAdapter {
     }
 }
 
-async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+async fn get_cached_server_binary_elixir_ls(
+    container_dir: PathBuf,
+) -> Option<LanguageServerBinary> {
     (|| async move {
         let mut last = None;
         let mut entries = fs::read_dir(&container_dir).await?;
@@ -254,3 +300,247 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
     .await
     .log_err()
 }
+
+pub struct NextLspAdapter;
+
+#[async_trait]
+impl LspAdapter for NextLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("next-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "next-ls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release =
+            latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
+        let version = release.name.clone();
+        let platform = match consts::ARCH {
+            "x86_64" => "darwin_arm64",
+            "aarch64" => "darwin_amd64",
+            other => bail!("Running on unsupported platform: {other}"),
+        };
+        let asset_name = format!("next_ls_{}", platform);
+        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: version,
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+
+        let binary_path = container_dir.join("next-ls");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+
+            let mut file = smol::fs::File::create(&binary_path).await?;
+            if !response.status().is_success() {
+                Err(anyhow!(
+                    "download failed with status {}",
+                    response.status().to_string()
+                ))?;
+            }
+            futures::io::copy(response.body_mut(), &mut file).await?;
+
+            fs::set_permissions(
+                &binary_path,
+                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+            )
+            .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec!["--stdio".into()],
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_next(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--stdio".into()];
+                binary
+            })
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_next(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_completion_elixir(completion, language)
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        symbol_kind: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_symbol_elixir(name, symbol_kind, language)
+    }
+}
+
+async fn get_cached_server_binary_next(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 == "next-ls")
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: Vec::new(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })
+    .await
+    .log_err()
+}
+
+pub struct LocalLspAdapter {
+    pub path: String,
+    pub arguments: Vec<String>,
+}
+
+#[async_trait]
+impl LspAdapter for LocalLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("local-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "local-ls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(()) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _: Box<dyn 'static + Send + Any>,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path)?;
+        Ok(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path).ok()?;
+        Some(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path).ok()?;
+        Some(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_completion_elixir(completion, language)
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        symbol: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_symbol_elixir(name, symbol, language)
+    }
+}
+
+fn label_for_completion_elixir(
+    completion: &lsp::CompletionItem,
+    language: &Arc<Language>,
+) -> Option<CodeLabel> {
+    return Some(CodeLabel {
+        runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
+        text: completion.label.clone(),
+        filter_range: 0..completion.label.len(),
+    });
+}
+
+fn label_for_symbol_elixir(
+    name: &str,
+    _: SymbolKind,
+    language: &Arc<Language>,
+) -> Option<CodeLabel> {
+    Some(CodeLabel {
+        runs: language.highlight_text(&name.into(), 0..name.len()),
+        text: name.to_string(),
+        filter_range: 0..name.len(),
+    })
+}

crates/zed/src/languages/rust.rs πŸ”—

@@ -165,17 +165,25 @@ impl LspAdapter for RustLspAdapter {
                 lazy_static! {
                     static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
                 }
-
                 let detail = completion.detail.as_ref().unwrap();
-                if detail.starts_with("fn(") {
-                    let text = REGEX.replace(&completion.label, &detail[2..]).to_string();
-                    let source = Rope::from(format!("fn {} {{}}", text).as_str());
-                    let runs = language.highlight_text(&source, 3..3 + text.len());
-                    return Some(CodeLabel {
-                        filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
-                        text,
-                        runs,
-                    });
+                const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"];
+                let prefix = FUNCTION_PREFIXES
+                    .iter()
+                    .find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix)));
+                // fn keyword should be followed by opening parenthesis.
+                if let Some((prefix, suffix)) = prefix {
+                    if suffix.starts_with('(') {
+                        let text = REGEX.replace(&completion.label, suffix).to_string();
+                        let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
+                        let run_start = prefix.len() + 1;
+                        let runs =
+                            language.highlight_text(&source, run_start..run_start + text.len());
+                        return Some(CodeLabel {
+                            filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
+                            text,
+                            runs,
+                        });
+                    }
                 }
             }
             Some(kind) => {
@@ -377,7 +385,28 @@ mod tests {
                 ],
             })
         );
-
+        assert_eq!(
+            language
+                .label_for_completion(&lsp::CompletionItem {
+                    kind: Some(lsp::CompletionItemKind::FUNCTION),
+                    label: "hello(…)".to_string(),
+                    detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (7..10, highlight_keyword),
+                    (11..17, highlight_type),
+                    (18..19, highlight_type),
+                    (25..28, highlight_type),
+                    (29..30, highlight_type),
+                ],
+            })
+        );
         assert_eq!(
             language
                 .label_for_completion(&lsp::CompletionItem {

crates/zed/src/main.rs πŸ”—

@@ -54,7 +54,7 @@ use welcome::{show_welcome_experience, FIRST_OPEN};
 
 use fs::RealFs;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
-use workspace::AppState;
+use workspace::{AppState, WorkspaceStore};
 use zed::{
     assets::Assets,
     build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
@@ -119,12 +119,6 @@ fn main() {
     app.run(move |cx| {
         cx.set_global(*RELEASE_CHANNEL);
 
-        #[cfg(debug_assertions)]
-        {
-            use feature_flags::FeatureFlagAppExt;
-            cx.set_staff(true);
-        }
-
         let mut store = SettingsStore::default();
         store
             .set_default_settings(default_settings().as_ref(), cx)
@@ -135,15 +129,17 @@ fn main() {
 
         let client = client::Client::new(http.clone(), cx);
         let mut languages = LanguageRegistry::new(login_shell_env_loaded);
+        let copilot_language_server_id = languages.next_language_server_id();
         languages.set_executor(cx.background().clone());
         languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
         let languages = Arc::new(languages);
         let node_runtime = RealNodeRuntime::new(http.clone());
 
-        languages::init(languages.clone(), node_runtime.clone());
+        languages::init(languages.clone(), node_runtime.clone(), cx);
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
         let channel_store =
             cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+        let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
         cx.set_global(client.clone());
 
@@ -165,8 +161,8 @@ fn main() {
         semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
         vim::init(cx);
         terminal_view::init(cx);
-        copilot::init(http.clone(), node_runtime, cx);
-        ai::init(cx);
+        copilot::init(copilot_language_server_id, http.clone(), node_runtime, cx);
+        assistant::init(cx);
         component_test::init(cx);
 
         cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
@@ -181,7 +177,7 @@ fn main() {
         })
         .detach();
 
-        client.telemetry().start(installation_id);
+        client.telemetry().start(installation_id, cx);
 
         let app_state = Arc::new(AppState {
             languages,
@@ -192,6 +188,7 @@ fn main() {
             build_window_options,
             initialize_workspace,
             background_actions,
+            workspace_store,
         });
         cx.set_global(Arc::downgrade(&app_state));
 

crates/zed/src/menus.rs πŸ”—

@@ -38,14 +38,12 @@ pub fn menus() -> Vec<Menu<'static>> {
                 MenuItem::action("Open Recent...", recent_projects::OpenRecent),
                 MenuItem::separator(),
                 MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject),
-                MenuItem::action("Save", workspace::Save),
+                MenuItem::action("Save", workspace::Save { save_intent: None }),
                 MenuItem::action("Save As…", workspace::SaveAs),
-                MenuItem::action("Save All", workspace::SaveAll),
+                MenuItem::action("Save All", workspace::SaveAll { save_intent: None }),
                 MenuItem::action(
                     "Close Editor",
-                    workspace::CloseActiveItem {
-                        save_behavior: None,
-                    },
+                    workspace::CloseActiveItem { save_intent: None },
                 ),
                 MenuItem::action("Close Window", workspace::CloseWindow),
             ],

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

@@ -5,9 +5,9 @@ pub mod only_instance;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
-use ai::AssistantPanel;
 use anyhow::Context;
 use assets::Assets;
+use assistant::AssistantPanel;
 use breadcrumbs::Breadcrumbs;
 pub use client;
 use collab_ui::CollabTitlebarItem; // TODO: Add back toggle collab ui shortcut
@@ -214,6 +214,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
             workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
         },
     );
+    cx.add_action(
+        |workspace: &mut Workspace,
+         _: &collab_ui::chat_panel::ToggleFocus,
+         cx: &mut ViewContext<Workspace>| {
+            workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(cx);
+        },
+    );
     cx.add_action(
         |workspace: &mut Workspace,
          _: &terminal_panel::ToggleFocus,
@@ -268,6 +275,10 @@ pub fn initialize_workspace(
                                     QuickActionBar::new(buffer_search_bar, workspace)
                                 });
                                 toolbar.add_item(quick_action_bar, cx);
+                                let diagnostic_editor_controls = cx.add_view(|_| {
+                                    diagnostics::ToolbarControls::new()
+                                });
+                                toolbar.add_item(diagnostic_editor_controls, cx);
                                 let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
                                 toolbar.add_item(project_search_bar, cx);
                                 let submit_feedback_button =
@@ -338,11 +349,14 @@ pub fn initialize_workspace(
         let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
         let channels_panel =
             collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
-        let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!(
+        let chat_panel =
+            collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
+        let (project_panel, terminal_panel, assistant_panel, channels_panel, chat_panel) = futures::try_join!(
             project_panel,
             terminal_panel,
             assistant_panel,
-            channels_panel
+            channels_panel,
+            chat_panel,
         )?;
         workspace_handle.update(&mut cx, |workspace, cx| {
             let project_panel_position = project_panel.position(cx);
@@ -362,6 +376,7 @@ pub fn initialize_workspace(
             workspace.add_panel(terminal_panel, cx);
             workspace.add_panel(assistant_panel, cx);
             workspace.add_panel(channels_panel, cx);
+            workspace.add_panel(chat_panel, cx);
 
             if !was_deserialized
                 && workspace
@@ -733,7 +748,7 @@ mod tests {
     use theme::{ThemeRegistry, ThemeSettings};
     use workspace::{
         item::{Item, ItemHandle},
-        open_new, open_paths, pane, NewFile, SaveBehavior, SplitDirection, WorkspaceHandle,
+        open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle,
     };
 
     #[gpui::test]
@@ -934,10 +949,14 @@ mod tests {
 
         editor.update(cx, |editor, cx| {
             assert!(editor.text(cx).is_empty());
+            assert!(!editor.is_dirty(cx));
         });
 
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
+        let save_task = workspace.update(cx, |workspace, cx| {
+            workspace.save_active_item(SaveIntent::Save, cx)
+        });
         app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+        cx.foreground().run_until_parked();
         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
         save_task.await.unwrap();
         editor.read_with(cx, |editor, cx| {
@@ -1300,7 +1319,10 @@ mod tests {
             .await;
         cx.read(|cx| assert!(editor.is_dirty(cx)));
 
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
+        let save_task = workspace.update(cx, |workspace, cx| {
+            workspace.save_active_item(SaveIntent::Save, cx)
+        });
+        cx.foreground().run_until_parked();
         window.simulate_prompt_answer(0, cx);
         save_task.await.unwrap();
         editor.read_with(cx, |editor, cx| {
@@ -1342,7 +1364,10 @@ mod tests {
         });
 
         // Save the buffer. This prompts for a filename.
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
+        let save_task = workspace.update(cx, |workspace, cx| {
+            workspace.save_active_item(SaveIntent::Save, cx)
+        });
+        cx.foreground().run_until_parked();
         cx.simulate_new_path_selection(|parent_dir| {
             assert_eq!(parent_dir, Path::new("/root"));
             Some(parent_dir.join("the-new-name.rs"))
@@ -1366,7 +1391,9 @@ mod tests {
             editor.handle_input(" there", cx);
             assert!(editor.is_dirty(cx));
         });
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
+        let save_task = workspace.update(cx, |workspace, cx| {
+            workspace.save_active_item(SaveIntent::Save, cx)
+        });
         save_task.await.unwrap();
         assert!(!cx.did_prompt_for_new_path());
         editor.read_with(cx, |editor, cx| {
@@ -1433,7 +1460,10 @@ mod tests {
         });
 
         // Save the buffer. This prompts for a filename.
-        let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
+        let save_task = workspace.update(cx, |workspace, cx| {
+            workspace.save_active_item(SaveIntent::Save, cx)
+        });
+        cx.foreground().run_until_parked();
         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
         save_task.await.unwrap();
         // The buffer is not dirty anymore and the language is assigned based on the path.
@@ -1497,9 +1527,7 @@ mod tests {
         });
         cx.dispatch_action(
             window.into(),
-            workspace::CloseActiveItem {
-                save_behavior: None,
-            },
+            workspace::CloseActiveItem { save_intent: None },
         );
 
         cx.foreground().run_until_parked();
@@ -1510,9 +1538,7 @@ mod tests {
 
         cx.dispatch_action(
             window.into(),
-            workspace::CloseActiveItem {
-                save_behavior: None,
-            },
+            workspace::CloseActiveItem { save_intent: None },
         );
         cx.foreground().run_until_parked();
         window.simulate_prompt_answer(1, cx);
@@ -1671,7 +1697,7 @@ mod tests {
         pane.update(cx, |pane, cx| {
             let editor3_id = editor3.id();
             drop(editor3);
-            pane.close_item_by_id(editor3_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
@@ -1706,7 +1732,7 @@ mod tests {
         pane.update(cx, |pane, cx| {
             let editor2_id = editor2.id();
             drop(editor2);
-            pane.close_item_by_id(editor2_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
@@ -1863,28 +1889,28 @@ mod tests {
 
         // Close all the pane items in some arbitrary order.
         pane.update(cx, |pane, cx| {
-            pane.close_item_by_id(file1_item_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
 
         pane.update(cx, |pane, cx| {
-            pane.close_item_by_id(file4_item_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
 
         pane.update(cx, |pane, cx| {
-            pane.close_item_by_id(file2_item_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
         assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
 
         pane.update(cx, |pane, cx| {
-            pane.close_item_by_id(file3_item_id, SaveBehavior::PromptOnWrite, cx)
+            pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
         })
         .await
         .unwrap();
@@ -2377,11 +2403,12 @@ mod tests {
 
     #[gpui::test]
     fn test_bundled_languages(cx: &mut AppContext) {
+        cx.set_global(SettingsStore::test(cx));
         let mut languages = LanguageRegistry::test();
         languages.set_executor(cx.background().clone());
         let languages = Arc::new(languages);
         let node_runtime = node_runtime::FakeNodeRuntime::new();
-        languages::init(languages.clone(), node_runtime);
+        languages::init(languages.clone(), node_runtime, cx);
         for name in languages.language_names() {
             languages.language_for_name(&name);
         }
@@ -2407,7 +2434,7 @@ mod tests {
             pane::init(cx);
             project_panel::init((), cx);
             terminal_view::init(cx);
-            ai::init(cx);
+            assistant::init(cx);
             app_state
         })
     }

docs/tools.md πŸ”—

@@ -32,7 +32,7 @@ Have a team member add you to the [Zed Industries](https://zed-industries.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!**
+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 members!**
 
 Once you have joined the community, let a team member know and we can add your correct role.
 
@@ -56,7 +56,7 @@ We use Vercel for all of our web deployments and some backend things. If you sig
 
 ### 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.
+You can get access to many of our shared enviroment variables through 1Password and Vercel. For 1Password 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).
 

rust-toolchain.toml πŸ”—

@@ -1,4 +1,4 @@
 [toolchain]
-channel = "1.72"
+channel = "1.73"
 components = [ "rustfmt" ]
 targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ]

script/deploy πŸ”—

@@ -13,10 +13,11 @@ version=$2
 export_vars_for_environment ${environment}
 image_id=$(image_id_for_version ${version})
 
+export ZED_DO_CERTIFICATE_ID=$(doctl compute certificate list --format ID --no-header)
 export ZED_KUBE_NAMESPACE=${environment}
 export ZED_IMAGE_ID=${image_id}
 
 target_zed_kube_cluster
 envsubst < crates/collab/k8s/manifest.template.yml | kubectl apply -f -
 
-echo "deployed collab v${version} to ${environment}"
+echo "deployed collab v${version} to ${environment}"

script/start-local-collaboration πŸ”—

@@ -44,6 +44,7 @@ position_2=${half_width},${y}
 
 # Authenticate using the collab server's admin secret.
 export ZED_STATELESS=1
+export ZED_ALWAYS_ACTIVE=1
 export ZED_ADMIN_API_TOKEN=secret
 export ZED_SERVER_URL=http://localhost:8080
 export ZED_WINDOW_SIZE=${half_width},${height}

styles/src/style_tree/app.ts πŸ”—

@@ -12,6 +12,7 @@ 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 chat_panel from "./chat_panel"
 import collab_panel from "./collab_panel"
 import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
 import incoming_call_notification from "./incoming_call_notification"
@@ -55,6 +56,7 @@ export default function app(): any {
         terminal: terminal(),
         assistant: assistant(),
         feedback: feedback(),
+        chat_panel: chat_panel(),
         component_test: component_test(),
     }
 }

styles/src/style_tree/assistant.ts πŸ”—

@@ -141,26 +141,26 @@ export default function assistant(): any {
             background: background(theme.highest),
         },
         hamburger_button: tab_bar_button(theme, {
-            icon: "icons/hamburger_15.svg",
+            icon: "icons/menu.svg",
         }),
 
         split_button: tab_bar_button(theme, {
-            icon: "icons/split_message_15.svg",
+            icon: "icons/split_message.svg",
         }),
         quote_button: tab_bar_button(theme, {
-            icon: "icons/radix/quote.svg",
+            icon: "icons/quote.svg",
         }),
         assist_button: tab_bar_button(theme, {
-            icon: "icons/radix/magic-wand.svg",
+            icon: "icons/magic-wand.svg",
         }),
         zoom_in_button: tab_bar_button(theme, {
-            icon: "icons/radix/maximize.svg",
+            icon: "icons/maximize.svg",
         }),
         zoom_out_button: tab_bar_button(theme, {
-            icon: "icons/radix/minimize.svg",
+            icon: "icons/minimize.svg",
         }),
         plus_button: tab_bar_button(theme, {
-            icon: "icons/radix/plus.svg",
+            icon: "icons/plus.svg",
         }),
         title: {
             ...text(theme.highest, "sans", "default", { size: "xs" }),

styles/src/style_tree/chat_panel.ts πŸ”—

@@ -0,0 +1,175 @@
+import {
+    background,
+    border,
+    text,
+} from "./components"
+import { icon_button } from "../component/icon_button"
+import { useTheme } from "../theme"
+import { interactive } from "../element"
+
+export default function chat_panel(): any {
+    const theme = useTheme()
+    const layer = theme.middle
+
+    const SPACING = 12 as const
+
+    const channel_name = {
+        padding: {
+            left: SPACING,
+            right: SPACING,
+            top: 4,
+            bottom: 4,
+        },
+        hash: {
+            ...text(layer, "sans", "base"),
+        },
+        name: text(layer, "sans", "base"),
+    }
+
+    return {
+        background: background(layer),
+        avatar: {
+            icon_width: 24,
+            icon_height: 24,
+            corner_radius: 4,
+            outer_width: 24,
+            outer_corner_radius: 16,
+        },
+        avatar_container: {
+            padding: {
+                right: 6,
+                left: 2,
+                top: 2,
+                bottom: 2,
+            }
+        },
+        list: {
+
+        },
+        channel_select: {
+            header: {
+                ...channel_name,
+                border: border(layer, { bottom: true })
+            },
+            item: channel_name,
+            active_item: {
+                ...channel_name,
+                background: background(layer, "on", "active"),
+            },
+            hovered_item: {
+                ...channel_name,
+                background: background(layer, "on", "hovered"),
+            },
+            menu: {
+                background: background(layer, "on"),
+                border: border(layer, { bottom: true })
+            }
+        },
+        icon_button: icon_button({
+            variant: "ghost",
+            color: "variant",
+            size: "sm",
+        }),
+        input_editor: {
+            background: background(layer, "on"),
+            corner_radius: 6,
+            text: text(layer, "sans", "base"),
+            placeholder_text: text(layer, "sans", "base", "disabled", {
+                size: "xs",
+            }),
+            selection: theme.players[0],
+            border: border(layer, "on"),
+            margin: {
+                left: SPACING,
+                right: SPACING,
+                bottom: SPACING,
+            },
+            padding: {
+                bottom: 4,
+                left: 8,
+                right: 8,
+                top: 4,
+            },
+        },
+        message: {
+            ...interactive({
+                base: {
+                    margin: { top: SPACING },
+                    padding: {
+                        top: 4,
+                        bottom: 4,
+                        left: SPACING / 2,
+                        right: SPACING / 3,
+                    }
+                },
+                state: {
+                    hovered: {
+                        background: background(layer, "hovered"),
+                    },
+                },
+            }),
+            body: text(layer, "sans", "base"),
+            sender: {
+                margin: {
+                    right: 8,
+                },
+                ...text(layer, "sans", "base", { weight: "bold" }),
+            },
+            timestamp: text(layer, "sans", "base", "disabled"),
+        },
+        last_message_bottom_spacing: SPACING,
+        continuation_message: {
+            body: text(layer, "sans", "base"),
+            sender: {
+                margin: {
+                    right: 8,
+                },
+                ...text(layer, "sans", "base", { weight: "bold" }),
+            },
+            timestamp: text(layer, "sans", "base", "disabled"),
+            ...interactive({
+                base: {
+                    padding: {
+                        top: 4,
+                        bottom: 4,
+                        left: SPACING / 2,
+                        right: SPACING / 3,
+                    }
+                },
+                state: {
+                    hovered: {
+                        background: background(layer, "hovered"),
+                    },
+                },
+            }),
+        },
+        pending_message: {
+            body: text(layer, "sans", "base"),
+            sender: {
+                margin: {
+                    right: 8,
+                },
+                ...text(layer, "sans", "base", "disabled"),
+            },
+            timestamp: text(layer, "sans", "base"),
+            ...interactive({
+                base: {
+                    padding: {
+                        top: 4,
+                        bottom: 4,
+                        left: SPACING / 2,
+                        right: SPACING / 3,
+                    }
+                },
+                state: {
+                    hovered: {
+                        background: background(layer, "hovered"),
+                    },
+                },
+            }),
+        },
+        sign_in_prompt: {
+            default: text(layer, "sans", "base"),
+        }
+    }
+}

styles/src/style_tree/collab_panel.ts πŸ”—

@@ -194,6 +194,7 @@ export default function contacts_panel(): any {
         },
         user_query_editor: filter_input,
         channel_hash: icon_style,
+        channel_note_active_color: foreground(layer, "active"),
         user_query_editor_height: 33,
         add_contact_button: header_icon_button,
         add_channel_button: header_icon_button,
@@ -267,10 +268,18 @@ export default function contacts_panel(): any {
         }),
         channel_row: item_row,
         channel_name: {
-            ...text(layer, "sans", { size: "sm" }),
-            margin: {
-                left: CHANNEL_SPACING,
+            active: {
+                ...text(layer, "sans", { size: "sm", weight: "bold" }),
+                margin: {
+                    left: CHANNEL_SPACING,
+                },
             },
+            inactive: {
+                ...text(layer, "sans", { size: "sm" }),
+                margin: {
+                    left: CHANNEL_SPACING,
+                },
+            }
         },
         list_empty_label_container: {
             margin: {

styles/src/style_tree/contact_notification.ts πŸ”—

@@ -42,10 +42,10 @@ export default function contact_notification(): any {
         dismiss_button: {
             default: {
                 color: foreground(theme.lowest, "variant"),
-                icon_width: 8,
-                icon_height: 8,
-                button_width: 8,
-                button_height: 8,
+                icon_width: 14,
+                icon_height: 14,
+                button_width: 14,
+                button_height: 14,
                 hover: {
                     color: foreground(theme.lowest, "hovered"),
                 },

styles/src/style_tree/copilot.ts πŸ”—

@@ -41,7 +41,7 @@ export default function copilot(): any {
             base: {
                 icon: svg(
                     foreground(theme.middle, "variant"),
-                    "icons/link_out_12.svg",
+                    "icons/external_link.svg",
                     12,
                     12
                 ),
@@ -91,7 +91,7 @@ export default function copilot(): any {
                 base: {
                     icon: svg(
                         foreground(theme.middle, "variant"),
-                        "icons/x_mark_8.svg",
+                        "icons/x.svg",
                         8,
                         8
                     ),
@@ -112,7 +112,7 @@ export default function copilot(): any {
                     hovered: {
                         icon: svg(
                             foreground(theme.middle, "on"),
-                            "icons/x_mark_8.svg",
+                            "icons/x.svg",
                             8,
                             8
                         ),
@@ -120,7 +120,7 @@ export default function copilot(): any {
                     clicked: {
                         icon: svg(
                             foreground(theme.middle, "base"),
-                            "icons/x_mark_8.svg",
+                            "icons/x.svg",
                             8,
                             8
                         ),
@@ -141,7 +141,7 @@ export default function copilot(): any {
             header: {
                 icon: svg(
                     foreground(theme.middle, "default"),
-                    "icons/zed_plus_copilot_32.svg",
+                    "icons/zed_x_copilot.svg",
                     92,
                     32
                 ),

styles/src/style_tree/editor.ts πŸ”—

@@ -91,9 +91,9 @@ export default function editor(): any {
             vertical_scale: 0.55,
         },
         folds: {
-            icon_margin_scale: 2.5,
-            folded_icon: "icons/chevron_right_8.svg",
-            foldable_icon: "icons/chevron_down_8.svg",
+            icon_margin_scale: 4,
+            folded_icon: "icons/chevron_right.svg",
+            foldable_icon: "icons/chevron_down.svg",
             indicator: toggleable({
                 base: interactive({
                     base: {

styles/src/style_tree/search.ts πŸ”—

@@ -30,15 +30,13 @@ export default function search(): any {
         selection: theme.players[0],
         text: text(theme.highest, "mono", "default"),
         border: border(theme.highest),
-        margin: {
-            right: SEARCH_ROW_SPACING,
-        },
         padding: {
             top: 4,
             bottom: 4,
             left: 10,
             right: 4,
         },
+        margin: { right: SEARCH_ROW_SPACING }
     }
 
     const include_exclude_editor = {
@@ -125,7 +123,7 @@ export default function search(): any {
 
                     button_width: 32,
                     background: background(theme.highest, "on"),
-                    corner_radius: 2,
+                    corner_radius: 6,
                     margin: { right: 2 },
                     border: {
                         width: 1,
@@ -185,26 +183,6 @@ export default function search(): any {
                 },
             },
         }),
-        // Search tool buttons
-        // HACK: This is not how disabled elements should be created
-        // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled
-        action_button: toggleable({
-            state: {
-                inactive: text_button({
-                    variant: "ghost",
-                    layer: theme.highest,
-                    disabled: true,
-                    margin: { right: SEARCH_ROW_SPACING },
-                    text_properties: { size: "sm" },
-                }),
-                active: text_button({
-                    variant: "ghost",
-                    layer: theme.highest,
-                    margin: { right: SEARCH_ROW_SPACING },
-                    text_properties: { size: "sm" },
-                }),
-            },
-        }),
         editor,
         invalid_editor: {
             ...editor,
@@ -218,12 +196,12 @@ export default function search(): any {
         match_index: {
             ...text(theme.highest, "mono", { size: "sm" }),
             padding: {
+                left: SEARCH_ROW_SPACING,
                 right: SEARCH_ROW_SPACING,
             },
         },
         option_button_group: {
             padding: {
-                left: SEARCH_ROW_SPACING,
                 right: SEARCH_ROW_SPACING,
             },
         },
@@ -397,7 +375,64 @@ export default function search(): any {
         search_bar_row_height: 34,
         search_row_spacing: 8,
         option_button_height: 22,
-        modes_container: {},
+        modes_container: {
+            padding: {
+                right: SEARCH_ROW_SPACING,
+            }
+        },
+        replace_icon: {
+            icon: {
+                color: foreground(theme.highest, "disabled"),
+                asset: "icons/replace.svg",
+                dimensions: {
+                    width: 14,
+                    height: 14,
+                },
+            },
+            container: {
+                margin: { right: 4 },
+                padding: { left: 1, right: 1 },
+            },
+        },
+        action_button: interactive({
+            base: {
+                icon_size: 14,
+                color: foreground(theme.highest, "variant"),
+
+                button_width: 32,
+                background: background(theme.highest, "on"),
+                corner_radius: 6,
+                margin: { right: 2 },
+                border: {
+                    width: 1,
+                    color: background(theme.highest, "on"),
+                },
+                padding: {
+                    left: 4,
+                    right: 4,
+                    top: 4,
+                    bottom: 4,
+                },
+            },
+            state: {
+                hovered: {
+                    ...text(theme.highest, "mono", "variant", "hovered"),
+                    background: background(theme.highest, "on", "hovered"),
+                    border: {
+                        width: 1,
+                        color: background(theme.highest, "on", "hovered"),
+                    },
+                },
+                clicked: {
+                    ...text(theme.highest, "mono", "variant", "pressed"),
+                    background: background(theme.highest, "on", "pressed"),
+                    border: {
+                        width: 1,
+                        color: background(theme.highest, "on", "pressed"),
+                    },
+                },
+            },
+        }),
         ...search_results(),
     }
 }

styles/src/style_tree/simple_message_notification.ts πŸ”—

@@ -37,10 +37,10 @@ export default function simple_message_notification(): any {
         dismiss_button: interactive({
             base: {
                 color: foreground(theme.middle),
-                icon_width: 8,
-                icon_height: 8,
-                button_width: 8,
-                button_height: 8,
+                icon_width: 14,
+                icon_height: 14,
+                button_width: 14,
+                button_height: 14,
             },
             state: {
                 hovered: {

styles/src/style_tree/tab_bar.ts πŸ”—

@@ -32,7 +32,7 @@ export default function tab_bar(): any {
         type_icon_width: 14,
 
         // Close icons
-        close_icon_width: 8,
+        close_icon_width: 14,
         icon_close: foreground(layer, "variant"),
         icon_close_active: foreground(layer, "hovered"),
 

styles/src/style_tree/titlebar.ts πŸ”—

@@ -1,4 +1,4 @@
-import { icon_button, toggleable_icon_button, toggleable_text_button } from "../component"
+import { icon_button, text_button, toggleable_icon_button, toggleable_text_button } from "../component"
 import { interactive, toggleable } from "../element"
 import { useTheme, with_opacity } from "../theme"
 import { background, border, foreground, text } from "./components"
@@ -191,6 +191,12 @@ export function titlebar(): any {
             color: "variant",
         }),
 
+        project_host: text_button({
+            text_properties: {
+                weight: "bold"
+            }
+        }),
+
         // Collaborators
         leader_avatar: {
             width: avatar_width,

styles/src/style_tree/update_notification.ts πŸ”—

@@ -26,10 +26,10 @@ export default function update_notification(): any {
         dismiss_button: interactive({
             base: {
                 color: foreground(theme.middle),
-                icon_width: 8,
-                icon_height: 8,
-                button_width: 8,
-                button_height: 8,
+                icon_width: 14,
+                icon_height: 14,
+                button_width: 14,
+                button_height: 14,
             },
             state: {
                 hovered: {

styles/src/style_tree/welcome.ts πŸ”—

@@ -128,7 +128,7 @@ export default function welcome(): any {
             },
             icon: svg(
                 foreground(theme.highest, "on"),
-                "icons/check_12.svg",
+                "icons/check.svg",
                 12,
                 12
             ),