Merge branch 'main' into zed2-breadcrumbs

Julia created

Change summary

Cargo.lock                                                        |   88 
Cargo.toml                                                        |    7 
assets/icons/arrow_down.svg                                       |    3 
assets/icons/arrow_left.svg                                       |    4 
assets/icons/arrow_right.svg                                      |    4 
assets/icons/arrow_up.svg                                         |    3 
assets/icons/command.svg                                          |    1 
assets/icons/control.svg                                          |    3 
assets/icons/option.svg                                           |    3 
assets/icons/return.svg                                           |    3 
assets/icons/shift.svg                                            |    3 
crates/activity_indicator2/Cargo.toml                             |   28 
crates/activity_indicator2/src/activity_indicator.rs              |  333 
crates/auto_update2/src/auto_update.rs                            |   11 
crates/auto_update2/src/update_notification.rs                    |  109 
crates/call2/Cargo.toml                                           |    3 
crates/call2/src/call2.rs                                         |  238 
crates/call2/src/participant.rs                                   |    2 
crates/call2/src/room.rs                                          |    3 
crates/call2/src/shared_screen.rs                                 |  111 
crates/client2/src/client2.rs                                     |    9 
crates/collab/src/tests/integration_tests.rs                      |   25 
crates/collab2/src/tests/channel_tests.rs                         |   37 
crates/collab2/src/tests/integration_tests.rs                     |   36 
crates/collab2/src/tests/test_server.rs                           |    2 
crates/collab_ui2/src/collab_panel.rs                             | 1428 
crates/collab_ui2/src/collab_panel/contact_finder.rs              |  265 
crates/collab_ui2/src/collab_titlebar_item.rs                     |  173 
crates/collab_ui2/src/collab_ui.rs                                |   64 
crates/collab_ui2/src/face_pile.rs                                |  105 
crates/collab_ui2/src/notifications.rs                            |   16 
crates/collab_ui2/src/notifications/incoming_call_notification.rs |  357 
crates/command_palette2/src/command_palette.rs                    |   47 
crates/diagnostics/src/diagnostics.rs                             |    8 
crates/diagnostics/src/items.rs                                   |    6 
crates/diagnostics2/src/diagnostics.rs                            |    6 
crates/diagnostics2/src/items.rs                                  |    6 
crates/diagnostics2/src/toolbar_controls.rs                       |    1 
crates/editor2/src/editor.rs                                      |  369 
crates/editor2/src/editor_tests.rs                                |  682 
crates/editor2/src/element.rs                                     |  282 
crates/editor2/src/hover_popover.rs                               | 1872 
crates/editor2/src/link_go_to_definition.rs                       |   76 
crates/editor2/src/selections_collection.rs                       |   51 
crates/editor2/src/test.rs                                        |    2 
crates/feature_flags2/src/feature_flags2.rs                       |    4 
crates/file_finder/src/file_finder.rs                             |   47 
crates/file_finder2/src/file_finder.rs                            |   75 
crates/go_to_line2/src/go_to_line.rs                              |   34 
crates/gpui2/build.rs                                             |    2 
crates/gpui2/src/action.rs                                        |    1 
crates/gpui2/src/app.rs                                           |   10 
crates/gpui2/src/app/async_context.rs                             |   11 
crates/gpui2/src/app/test_context.rs                              |    2 
crates/gpui2/src/element.rs                                       |   44 
crates/gpui2/src/elements/div.rs                                  |  242 
crates/gpui2/src/elements/img.rs                                  |  148 
crates/gpui2/src/elements/overlay.rs                              |    8 
crates/gpui2/src/elements/text.rs                                 |   22 
crates/gpui2/src/elements/uniform_list.rs                         |    8 
crates/gpui2/src/geometry.rs                                      |   22 
crates/gpui2/src/platform/mac/metal_renderer.rs                   |  138 
crates/gpui2/src/platform/mac/shaders.metal                       |   52 
crates/gpui2/src/platform/mac/window.rs                           |    3 
crates/gpui2/src/scene.rs                                         |   66 
crates/gpui2/src/window.rs                                        |  125 
crates/language/src/highlight_map.rs                              |    8 
crates/language/src/language.rs                                   |   14 
crates/language/src/syntax_map/syntax_map_tests.rs                |    2 
crates/language2/src/highlight_map.rs                             |    8 
crates/language2/src/language2.rs                                 |   14 
crates/language2/src/syntax_map/syntax_map_tests.rs               |    2 
crates/lsp/src/lsp.rs                                             |   33 
crates/lsp2/src/lsp2.rs                                           |   33 
crates/node_runtime/src/node_runtime.rs                           |   14 
crates/picker2/src/picker2.rs                                     |   33 
crates/prettier/src/prettier.rs                                   |    4 
crates/prettier/src/prettier_server.js                            |    3 
crates/prettier2/src/prettier2.rs                                 |    4 
crates/prettier2/src/prettier_server.js                           |    3 
crates/project/src/prettier_support.rs                            |  758 
crates/project/src/project.rs                                     |  723 
crates/project/src/project_tests.rs                               |   93 
crates/project2/src/lsp_command.rs                                |    3 
crates/project2/src/prettier_support.rs                           |  772 
crates/project2/src/project2.rs                                   |  784 
crates/project2/src/project_tests.rs                              |   93 
crates/project_panel2/src/file_associations.rs                    |   71 
crates/project_panel2/src/project_panel.rs                        |  244 
crates/search2/src/buffer_search.rs                               |  133 
crates/search2/src/search.rs                                      |   19 
crates/search2/src/search_bar.rs                                  |   19 
crates/semantic_index/src/semantic_index_tests.rs                 |    4 
crates/storybook2/src/stories/focus.rs                            |   11 
crates/storybook2/src/stories/kitchen_sink.rs                     |    2 
crates/storybook2/src/stories/picker.rs                           |   31 
crates/storybook2/src/stories/scroll.rs                           |    4 
crates/storybook2/src/stories/text.rs                             |    4 
crates/storybook2/src/stories/z_index.rs                          |    4 
crates/storybook2/src/story_selector.rs                           |   19 
crates/storybook2/src/storybook2.rs                               |    6 
crates/terminal_view2/src/terminal_view.rs                        |    9 
crates/theme2/src/default_colors.rs                               |   24 
crates/theme2/src/one_themes.rs                                   |    4 
crates/theme2/src/registry.rs                                     |    4 
crates/theme2/src/styles/stories/players.rs                       |   92 
crates/theme_selector2/Cargo.toml                                 |   29 
crates/theme_selector2/src/theme_selector.rs                      |  276 
crates/ui2/Cargo.toml                                             |    4 
crates/ui2/src/clickable.rs                                       |    7 
crates/ui2/src/components.rs                                      |    8 
crates/ui2/src/components/avatar.rs                               |   60 
crates/ui2/src/components/button.rs                               |  233 
crates/ui2/src/components/button/button.rs                        |   91 
crates/ui2/src/components/button/button_like.rs                   |  273 
crates/ui2/src/components/button/icon_button.rs                   |  102 
crates/ui2/src/components/button/mod.rs                           |    7 
crates/ui2/src/components/checkbox.rs                             |    3 
crates/ui2/src/components/context_menu.rs                         |  189 
crates/ui2/src/components/disclosure.rs                           |   59 
crates/ui2/src/components/divider.rs                              |   13 
crates/ui2/src/components/icon.rs                                 |   33 
crates/ui2/src/components/icon_button.rs                          |  129 
crates/ui2/src/components/input.rs                                |  108 
crates/ui2/src/components/keybinding.rs                           |   90 
crates/ui2/src/components/label.rs                                |    8 
crates/ui2/src/components/list.rs                                 |  457 
crates/ui2/src/components/list/list_header.rs                     |  124 
crates/ui2/src/components/list/list_item.rs                       |  166 
crates/ui2/src/components/list/list_separator.rs                  |   14 
crates/ui2/src/components/list/list_sub_header.rs                 |   56 
crates/ui2/src/components/popover.rs                              |   23 
crates/ui2/src/components/stories.rs                              |   10 
crates/ui2/src/components/stories/avatar.rs                       |   14 
crates/ui2/src/components/stories/button.rs                       |  141 
crates/ui2/src/components/stories/context_menu.rs                 |    6 
crates/ui2/src/components/stories/disclosure.rs                   |   20 
crates/ui2/src/components/stories/icon.rs                         |    2 
crates/ui2/src/components/stories/icon_button.rs                  |   47 
crates/ui2/src/components/stories/input.rs                        |   18 
crates/ui2/src/components/stories/keybinding.rs                   |    2 
crates/ui2/src/components/stories/label.rs                        |    2 
crates/ui2/src/components/stories/list.rs                         |   38 
crates/ui2/src/components/stories/list_item.rs                    |   48 
crates/ui2/src/components/toggle.rs                               |   41 
crates/ui2/src/components/tooltip.rs                              |    6 
crates/ui2/src/disableable.rs                                     |    5 
crates/ui2/src/fixed.rs                                           |   10 
crates/ui2/src/prelude.rs                                         |   72 
crates/ui2/src/selectable.rs                                      |   22 
crates/ui2/src/slot.rs                                            |    8 
crates/ui2/src/styled_ext.rs                                      |    6 
crates/ui2/src/styles/color.rs                                    |    4 
crates/ui2/src/ui2.rs                                             |   12 
crates/ui2/src/utils/format_distance.rs                           |   55 
crates/util/src/channel.rs                                        |    2 
crates/welcome2/Cargo.toml                                        |   37 
crates/welcome2/src/base_keymap_picker.rs                         |  208 
crates/welcome2/src/base_keymap_setting.rs                        |   65 
crates/welcome2/src/welcome.rs                                    |  281 
crates/workspace/src/workspace.rs                                 |   10 
crates/workspace2/src/dock.rs                                     |   30 
crates/workspace2/src/modal_layer.rs                              |    4 
crates/workspace2/src/notifications.rs                            |  138 
crates/workspace2/src/pane.rs                                     |   12 
crates/workspace2/src/shared_screen.rs                            |  151 
crates/workspace2/src/status_bar.rs                               |   20 
crates/workspace2/src/toolbar.rs                                  |   19 
crates/workspace2/src/workspace2.rs                               |  334 
crates/zed/Cargo.toml                                             |    3 
crates/zed/src/languages.rs                                       |   13 
crates/zed/src/languages/elixir/embedding.scm                     |    4 
crates/zed/src/languages/json.rs                                  |    1 
crates/zed/src/languages/nu.rs                                    |   81 
crates/zed/src/languages/php.rs                                   |    1 
crates/zed/src/languages/tailwind.rs                              |    6 
crates/zed/src/languages/typescript.rs                            |   13 
crates/zed/src/languages/uiua.rs                                  |   55 
crates/zed/src/languages/uiua/config.toml                         |   10 
crates/zed/src/languages/uiua/highlights.scm                      |   50 
crates/zed/src/languages/uiua/indents.scm                         |    3 
crates/zed/src/languages/yaml.rs                                  |    6 
crates/zed2/Cargo.toml                                            |   11 
crates/zed2/src/languages.rs                                      |   13 
crates/zed2/src/languages/json.rs                                 |    1 
crates/zed2/src/languages/nu.rs                                   |   55 
crates/zed2/src/languages/php.rs                                  |    1 
crates/zed2/src/languages/tailwind.rs                             |    6 
crates/zed2/src/languages/typescript.rs                           |   13 
crates/zed2/src/languages/uiua.rs                                 |   55 
crates/zed2/src/languages/uiua/config.toml                        |   10 
crates/zed2/src/languages/uiua/highlights.scm                     |   50 
crates/zed2/src/languages/uiua/indents.scm                        |    3 
crates/zed2/src/languages/yaml.rs                                 |    6 
crates/zed2/src/main.rs                                           |  236 
crates/zed2/src/zed2.rs                                           |   26 
script/crate-dep-graph                                            |    2 
197 files changed, 10,407 insertions(+), 6,843 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -19,6 +19,25 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "activity_indicator2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "auto_update2",
+ "editor2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "project2",
+ "settings2",
+ "smallvec",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "addr2line"
 version = "0.17.0"
@@ -1210,6 +1229,7 @@ dependencies = [
  "fs2",
  "futures 0.3.28",
  "gpui2",
+ "image",
  "language2",
  "live_kit_client2",
  "log",
@@ -1221,6 +1241,8 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "settings2",
+ "smallvec",
+ "ui2",
  "util",
  "workspace2",
 ]
@@ -9485,6 +9507,27 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "theme_selector2"
+version = "0.1.0"
+dependencies = [
+ "editor2",
+ "feature_flags2",
+ "fs2",
+ "fuzzy2",
+ "gpui2",
+ "log",
+ "parking_lot 0.11.2",
+ "picker2",
+ "postage",
+ "settings2",
+ "smol",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "thiserror"
 version = "1.0.48"
@@ -9944,7 +9987,7 @@ dependencies = [
 [[package]]
 name = "tree-sitter"
 version = "0.20.10"
-source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222"
+source = "git+https://github.com/tree-sitter/tree-sitter?rev=3b0159d25559b603af566ade3c83d930bf466db1#3b0159d25559b603af566ade3c83d930bf466db1"
 dependencies = [
  "cc",
  "regex",
@@ -10192,6 +10235,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-uiua"
+version = "0.3.3"
+source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-vue"
 version = "0.0.1"
@@ -11059,6 +11111,31 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "welcome2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client2",
+ "db2",
+ "editor2",
+ "fs2",
+ "fuzzy2",
+ "gpui2",
+ "install_cli2",
+ "log",
+ "picker2",
+ "project2",
+ "schemars",
+ "serde",
+ "settings2",
+ "theme2",
+ "theme_selector2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "which"
 version = "4.4.2"
@@ -11513,7 +11590,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.115.0"
+version = "0.116.0"
 dependencies = [
  "activity_indicator",
  "ai",
@@ -11630,6 +11707,7 @@ dependencies = [
  "tree-sitter-svelte",
  "tree-sitter-toml",
  "tree-sitter-typescript",
+ "tree-sitter-uiua",
  "tree-sitter-vue",
  "tree-sitter-yaml",
  "unindent",
@@ -11655,16 +11733,19 @@ dependencies = [
 name = "zed2"
 version = "0.109.0"
 dependencies = [
+ "activity_indicator2",
  "ai2",
  "anyhow",
  "async-compression",
  "async-recursion 0.3.2",
  "async-tar",
  "async-trait",
+ "audio2",
  "auto_update2",
  "backtrace",
  "breadcrumbs2",
  "call2",
+ "channel2",
  "chrono",
  "cli",
  "client2",
@@ -11723,6 +11804,7 @@ dependencies = [
  "terminal_view2",
  "text2",
  "theme2",
+ "theme_selector2",
  "thiserror",
  "tiny_http",
  "toml 0.5.11",
@@ -11752,6 +11834,7 @@ dependencies = [
  "tree-sitter-svelte",
  "tree-sitter-toml",
  "tree-sitter-typescript",
+ "tree-sitter-uiua",
  "tree-sitter-vue",
  "tree-sitter-yaml",
  "unindent",
@@ -11759,6 +11842,7 @@ dependencies = [
  "urlencoding",
  "util",
  "uuid 1.4.1",
+ "welcome2",
  "workspace2",
  "zed_actions2",
 ]

Cargo.toml πŸ”—

@@ -1,6 +1,7 @@
 [workspace]
 members = [
     "crates/activity_indicator",
+    "crates/activity_indicator2",
     "crates/ai",
     "crates/assistant",
     "crates/audio",
@@ -108,6 +109,7 @@ members = [
     "crates/theme2",
     "crates/theme_importer",
     "crates/theme_selector",
+    "crates/theme_selector2",
     "crates/ui2",
     "crates/util",
     "crates/semantic_index",
@@ -116,6 +118,7 @@ members = [
     "crates/vcs_menu",
     "crates/workspace2",
     "crates/welcome",
+    "crates/welcome2",
     "crates/xtask",
     "crates/zed",
     "crates/zed2",
@@ -196,8 +199,10 @@ tree-sitter-lua = "0.0.14"
 tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
 tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
 tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
+tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
+
 [patch.crates-io]
-tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }
+tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" }
 async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
 
 # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457

assets/icons/arrow_down.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.00001 12L3.5 7.50001M8.00001 12L12.5 7.50001M8.00001 12L8.00001 3.00001" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/arrow_left.svg πŸ”—

@@ -1,3 +1,3 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.125 6.99344L6.35938 3.63281M3.125 6.99344L6.35938 10.3672M3.125 6.99344H11" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.5 7.50001L8 3M3.5 7.50001L8 12M3.5 7.50001H12.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/arrow_right.svg πŸ”—

@@ -1,3 +1,3 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.8906 7.00125L7.64062 3.64062M10.8906 7.00125L7.64062 10.375M10.8906 7.00125H3" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.5 7.5L8 12M12.5 7.5L8 3M12.5 7.5L3.5 7.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/arrow_up.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.99999 3.00001L12.5 7.50001M7.99999 3.00001L3.49999 7.50001M7.99999 3.00001L7.99999 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/control.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.5 6.12488L7.64656 1.97853C7.84183 1.78328 8.1584 1.78329 8.35366 1.97854L12.5 6.12488" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/option.svg πŸ”—

@@ -0,0 +1,3 @@
+<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="M5.35606 1.005H1.62545C1.28002 1.005 1 1.28502 1 1.63044C1 1.97587 1.28002 2.25589 1.62545 2.25589L5.35606 2.25589C5.62311 2.25589 5.8607 2.42545 5.94752 2.67799L9.75029 13.7387C10.0108 14.4963 10.7235 15.005 11.5247 15.005H14.3746C14.72 15.005 15 14.725 15 14.3796C15 14.0341 14.72 13.7541 14.3746 13.7541H11.5247C11.2576 13.7541 11.02 13.5845 10.9332 13.332L7.13046 2.27128C6.86998 1.51366 6.15721 1.005 5.35606 1.005ZM14.3745 1.005H9.75125C9.40582 1.005 9.1258 1.28502 9.1258 1.63044C9.1258 1.97587 9.40582 2.25589 9.75125 2.25589L14.3745 2.25589C14.72 2.25589 15 1.97587 15 1.63044C15 1.28502 14.72 1.005 14.3745 1.005Z" fill="black"/>
+</svg>

assets/icons/return.svg πŸ”—

@@ -0,0 +1,3 @@
+<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.375 1.63C8.375 1.28482 8.65482 1.005 9 1.005H12.375C13.8247 1.005 15 2.18025 15 3.63V7.625C15 9.07474 13.8247 10.25 12.375 10.25H3.13388L6.07194 13.1881C6.31602 13.4321 6.31602 13.8279 6.07194 14.0719C5.82786 14.316 5.43214 14.316 5.18806 14.0719L1.18306 10.0669C0.938981 9.82286 0.938981 9.42714 1.18306 9.18306L5.18306 5.18306C5.42714 4.93898 5.82286 4.93898 6.06694 5.18306C6.31102 5.42714 6.31102 5.82286 6.06694 6.06694L3.13388 9H12.375C13.1344 9 13.75 8.38439 13.75 7.625V3.63C13.75 2.87061 13.1344 2.255 12.375 2.255H9C8.65482 2.255 8.375 1.97518 8.375 1.63Z" fill="black"/>
+</svg>

assets/icons/shift.svg πŸ”—

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.46475 7.99652L7.85304 2.15921C7.93223 2.07342 8.06777 2.07341 8.14696 2.15921L13.5352 7.99652C13.7126 8.18869 13.5763 8.5 13.3148 8.5H10.5V13.7C10.5 13.8657 10.3657 14 10.2 14H5.8C5.63431 14 5.5 13.8657 5.5 13.7V8.5H2.6852C2.42367 8.5 2.28737 8.18869 2.46475 7.99652Z" stroke="black" stroke-width="1.25"/>
+</svg>

crates/activity_indicator2/Cargo.toml πŸ”—

@@ -0,0 +1,28 @@
+[package]
+name = "activity_indicator2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/activity_indicator.rs"
+doctest = false
+
+[dependencies]
+auto_update = { path = "../auto_update2", package = "auto_update2" }
+editor = { path = "../editor2", package = "editor2" }
+language = { path = "../language2", package = "language2" }
+gpui = { path = "../gpui2", package = "gpui2" }
+project = { path = "../project2", package = "project2" }
+settings = { path = "../settings2", package = "settings2" }
+ui = { path = "../ui2", package = "ui2" }
+util = { path = "../util" }
+theme = { path = "../theme2", package = "theme2" }
+workspace = { path = "../workspace2", package = "workspace2" }
+
+anyhow.workspace = true
+futures.workspace = true
+smallvec.workspace = true
+
+[dev-dependencies]
+editor = { path = "../editor2", package = "editor2", features = ["test-support"] }

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

@@ -0,0 +1,333 @@
+use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
+use editor::Editor;
+use futures::StreamExt;
+use gpui::{
+    actions, svg, AppContext, CursorStyle, Div, EventEmitter, InteractiveElement as _, Model,
+    ParentElement as _, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, View,
+    ViewContext, VisualContext as _,
+};
+use language::{LanguageRegistry, LanguageServerBinaryStatus};
+use project::{LanguageServerProgress, Project};
+use smallvec::SmallVec;
+use std::{cmp::Reverse, fmt::Write, sync::Arc};
+use ui::h_stack;
+use util::ResultExt;
+use workspace::{item::ItemHandle, StatusItemView, Workspace};
+
+actions!(ShowErrorMessage);
+
+const DOWNLOAD_ICON: &str = "icons/download.svg";
+const WARNING_ICON: &str = "icons/warning.svg";
+
+pub enum Event {
+    ShowError { lsp_name: Arc<str>, error: String },
+}
+
+pub struct ActivityIndicator {
+    statuses: Vec<LspStatus>,
+    project: Model<Project>,
+    auto_updater: Option<Model<AutoUpdater>>,
+}
+
+struct LspStatus {
+    name: Arc<str>,
+    status: LanguageServerBinaryStatus,
+}
+
+struct PendingWork<'a> {
+    language_server_name: &'a str,
+    progress_token: &'a str,
+    progress: &'a LanguageServerProgress,
+}
+
+#[derive(Default)]
+struct Content {
+    icon: Option<&'static str>,
+    message: String,
+    on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
+}
+
+impl ActivityIndicator {
+    pub fn new(
+        workspace: &mut Workspace,
+        languages: Arc<LanguageRegistry>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> View<ActivityIndicator> {
+        let project = workspace.project().clone();
+        let auto_updater = AutoUpdater::get(cx);
+        let this = cx.build_view(|cx: &mut ViewContext<Self>| {
+            let mut status_events = languages.language_server_binary_statuses();
+            cx.spawn(|this, mut cx| async move {
+                while let Some((language, event)) = status_events.next().await {
+                    this.update(&mut cx, |this, cx| {
+                        this.statuses.retain(|s| s.name != language.name());
+                        this.statuses.push(LspStatus {
+                            name: language.name(),
+                            status: event,
+                        });
+                        cx.notify();
+                    })?;
+                }
+                anyhow::Ok(())
+            })
+            .detach();
+            cx.observe(&project, |_, _, cx| cx.notify()).detach();
+
+            if let Some(auto_updater) = auto_updater.as_ref() {
+                cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
+            }
+
+            // cx.observe_active_labeled_tasks(|_, cx| cx.notify())
+            //     .detach();
+
+            Self {
+                statuses: Default::default(),
+                project: project.clone(),
+                auto_updater,
+            }
+        });
+
+        cx.subscribe(&this, move |workspace, _, event, cx| match event {
+            Event::ShowError { lsp_name, error } => {
+                if let Some(buffer) = project
+                    .update(cx, |project, cx| project.create_buffer(error, None, cx))
+                    .log_err()
+                {
+                    buffer.update(cx, |buffer, cx| {
+                        buffer.edit(
+                            [(0..0, format!("Language server error: {}\n\n", lsp_name))],
+                            None,
+                            cx,
+                        );
+                    });
+                    workspace.add_item(
+                        Box::new(cx.build_view(|cx| {
+                            Editor::for_buffer(buffer, Some(project.clone()), cx)
+                        })),
+                        cx,
+                    );
+                }
+            }
+        })
+        .detach();
+        this
+    }
+
+    fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
+        self.statuses.retain(|status| {
+            if let LanguageServerBinaryStatus::Failed { error } = &status.status {
+                cx.emit(Event::ShowError {
+                    lsp_name: status.name.clone(),
+                    error: error.clone(),
+                });
+                false
+            } else {
+                true
+            }
+        });
+
+        cx.notify();
+    }
+
+    fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
+        if let Some(updater) = &self.auto_updater {
+            updater.update(cx, |updater, cx| {
+                updater.dismiss_error(cx);
+            });
+        }
+        cx.notify();
+    }
+
+    fn pending_language_server_work<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> impl Iterator<Item = PendingWork<'a>> {
+        self.project
+            .read(cx)
+            .language_server_statuses()
+            .rev()
+            .filter_map(|status| {
+                if status.pending_work.is_empty() {
+                    None
+                } else {
+                    let mut pending_work = status
+                        .pending_work
+                        .iter()
+                        .map(|(token, progress)| PendingWork {
+                            language_server_name: status.name.as_str(),
+                            progress_token: token.as_str(),
+                            progress,
+                        })
+                        .collect::<SmallVec<[_; 4]>>();
+                    pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
+                    Some(pending_work)
+                }
+            })
+            .flatten()
+    }
+
+    fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
+        // Show any language server has pending activity.
+        let mut pending_work = self.pending_language_server_work(cx);
+        if let Some(PendingWork {
+            language_server_name,
+            progress_token,
+            progress,
+        }) = pending_work.next()
+        {
+            let mut message = language_server_name.to_string();
+
+            message.push_str(": ");
+            if let Some(progress_message) = progress.message.as_ref() {
+                message.push_str(progress_message);
+            } else {
+                message.push_str(progress_token);
+            }
+
+            if let Some(percentage) = progress.percentage {
+                write!(&mut message, " ({}%)", percentage).unwrap();
+            }
+
+            let additional_work_count = pending_work.count();
+            if additional_work_count > 0 {
+                write!(&mut message, " + {} more", additional_work_count).unwrap();
+            }
+
+            return Content {
+                icon: None,
+                message,
+                on_click: None,
+            };
+        }
+
+        // Show any language server installation info.
+        let mut downloading = SmallVec::<[_; 3]>::new();
+        let mut checking_for_update = SmallVec::<[_; 3]>::new();
+        let mut failed = SmallVec::<[_; 3]>::new();
+        for status in &self.statuses {
+            let name = status.name.clone();
+            match status.status {
+                LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
+                LanguageServerBinaryStatus::Downloading => downloading.push(name),
+                LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
+                LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
+            }
+        }
+
+        if !downloading.is_empty() {
+            return Content {
+                icon: Some(DOWNLOAD_ICON),
+                message: format!(
+                    "Downloading {} language server{}...",
+                    downloading.join(", "),
+                    if downloading.len() > 1 { "s" } else { "" }
+                ),
+                on_click: None,
+            };
+        } else if !checking_for_update.is_empty() {
+            return Content {
+                icon: Some(DOWNLOAD_ICON),
+                message: format!(
+                    "Checking for updates to {} language server{}...",
+                    checking_for_update.join(", "),
+                    if checking_for_update.len() > 1 {
+                        "s"
+                    } else {
+                        ""
+                    }
+                ),
+                on_click: None,
+            };
+        } else if !failed.is_empty() {
+            return Content {
+                icon: Some(WARNING_ICON),
+                message: format!(
+                    "Failed to download {} language server{}. Click to show error.",
+                    failed.join(", "),
+                    if failed.len() > 1 { "s" } else { "" }
+                ),
+                on_click: Some(Arc::new(|this, cx| {
+                    this.show_error_message(&Default::default(), cx)
+                })),
+            };
+        }
+
+        // Show any application auto-update info.
+        if let Some(updater) = &self.auto_updater {
+            return match &updater.read(cx).status() {
+                AutoUpdateStatus::Checking => Content {
+                    icon: Some(DOWNLOAD_ICON),
+                    message: "Checking for Zed updates…".to_string(),
+                    on_click: None,
+                },
+                AutoUpdateStatus::Downloading => Content {
+                    icon: Some(DOWNLOAD_ICON),
+                    message: "Downloading Zed update…".to_string(),
+                    on_click: None,
+                },
+                AutoUpdateStatus::Installing => Content {
+                    icon: Some(DOWNLOAD_ICON),
+                    message: "Installing Zed update…".to_string(),
+                    on_click: None,
+                },
+                AutoUpdateStatus::Updated => Content {
+                    icon: None,
+                    message: "Click to restart and update Zed".to_string(),
+                    on_click: Some(Arc::new(|_, cx| {
+                        workspace::restart(&Default::default(), cx)
+                    })),
+                },
+                AutoUpdateStatus::Errored => Content {
+                    icon: Some(WARNING_ICON),
+                    message: "Auto update failed".to_string(),
+                    on_click: Some(Arc::new(|this, cx| {
+                        this.dismiss_error_message(&Default::default(), cx)
+                    })),
+                },
+                AutoUpdateStatus::Idle => Default::default(),
+            };
+        }
+
+        // todo!(show active tasks)
+        // if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
+        //     return Content {
+        //         icon: None,
+        //         message: most_recent_active_task.to_string(),
+        //         on_click: None,
+        //     };
+        // }
+
+        Default::default()
+    }
+}
+
+impl EventEmitter<Event> for ActivityIndicator {}
+
+impl Render for ActivityIndicator {
+    type Element = Stateful<Div>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let content = self.content_to_render(cx);
+
+        let mut result = h_stack()
+            .id("activity-indicator")
+            .on_action(cx.listener(Self::show_error_message))
+            .on_action(cx.listener(Self::dismiss_error_message));
+
+        if let Some(on_click) = content.on_click {
+            result = result
+                .cursor(CursorStyle::PointingHand)
+                .on_click(cx.listener(move |this, _, cx| {
+                    on_click(this, cx);
+                }))
+        }
+
+        result
+            .children(content.icon.map(|icon| svg().path(icon)))
+            .child(SharedString::from(content.message))
+    }
+}
+
+impl StatusItemView for ActivityIndicator {
+    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
+}

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

@@ -84,8 +84,8 @@ impl Settings for AutoUpdateSetting {
 pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
     AutoUpdateSetting::register(cx);
 
-    cx.observe_new_views(|wokrspace: &mut Workspace, _cx| {
-        wokrspace
+    cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+        workspace
             .register_action(|_, action: &Check, cx| check(action, cx))
             .register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
                 let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
@@ -94,6 +94,11 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
                 })
                 .detach();
             });
+
+        // @nate - code to trigger update notification on launch
+        // workspace.show_notification(0, _cx, |cx| {
+        //     cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
+        // });
     })
     .detach();
 
@@ -131,7 +136,7 @@ pub fn check(_: &Check, cx: &mut AppContext) {
     }
 }
 
-fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
+pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
     if let Some(auto_updater) = AutoUpdater::get(cx) {
         let auto_updater = auto_updater.read(cx);
         let server_url = &auto_updater.server_url;

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

@@ -1,87 +1,56 @@
-use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext};
-use menu::Cancel;
-use workspace::notifications::NotificationEvent;
+use gpui::{
+    div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
+    SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
+};
+use util::channel::ReleaseChannel;
+use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
 
 pub struct UpdateNotification {
-    _version: SemanticVersion,
+    version: SemanticVersion,
 }
 
-impl EventEmitter<NotificationEvent> for UpdateNotification {}
+impl EventEmitter<DismissEvent> for UpdateNotification {}
 
 impl Render for UpdateNotification {
     type Element = Div;
 
-    fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
-        div().child("Updated zed!")
-        // let theme = theme::current(cx).clone();
-        // let theme = &theme.update_notification;
-
-        // let app_name = cx.global::<ReleaseChannel>().display_name();
-
-        // MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| {
-        //     Flex::column()
-        //         .with_child(
-        //             Flex::row()
-        //                 .with_child(
-        //                     Text::new(
-        //                         format!("Updated to {app_name} {}", self.version),
-        //                         theme.message.text.clone(),
-        //                     )
-        //                     .contained()
-        //                     .with_style(theme.message.container)
-        //                     .aligned()
-        //                     .top()
-        //                     .left()
-        //                     .flex(1., true),
-        //                 )
-        //                 .with_child(
-        //                     MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
-        //                         let style = theme.dismiss_button.style_for(state);
-        //                         Svg::new("icons/x.svg")
-        //                             .with_color(style.color)
-        //                             .constrained()
-        //                             .with_width(style.icon_width)
-        //                             .aligned()
-        //                             .contained()
-        //                             .with_style(style.container)
-        //                             .constrained()
-        //                             .with_width(style.button_width)
-        //                             .with_height(style.button_width)
-        //                     })
-        //                     .with_padding(Padding::uniform(5.))
-        //                     .on_click(MouseButton::Left, move |_, this, cx| {
-        //                         this.dismiss(&Default::default(), cx)
-        //                     })
-        //                     .aligned()
-        //                     .constrained()
-        //                     .with_height(cx.font_cache().line_height(theme.message.text.font_size))
-        //                     .aligned()
-        //                     .top()
-        //                     .flex_float(),
-        //                 ),
-        //         )
-        //         .with_child({
-        //             let style = theme.action_message.style_for(state);
-        //             Text::new("View the release notes", style.text.clone())
-        //                 .contained()
-        //                 .with_style(style.container)
-        //         })
-        //         .contained()
-        // })
-        // .with_cursor_style(CursorStyle::PointingHand)
-        // .on_click(MouseButton::Left, |_, _, cx| {
-        //     crate::view_release_notes(&Default::default(), cx)
-        // })
-        // .into_any_named("update notification")
+    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+        let app_name = cx.global::<ReleaseChannel>().display_name();
+
+        v_stack()
+            .elevation_3(cx)
+            .p_4()
+            .child(
+                h_stack()
+                    .justify_between()
+                    .child(Label::new(format!(
+                        "Updated to {app_name} {}",
+                        self.version
+                    )))
+                    .child(
+                        div()
+                            .id("cancel")
+                            .child(IconElement::new(Icon::Close))
+                            .cursor_pointer()
+                            .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
+                    ),
+            )
+            .child(
+                div()
+                    .id("notes")
+                    .child(Label::new("View the release notes"))
+                    .cursor_pointer()
+                    .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)),
+            )
     }
 }
 
 impl UpdateNotification {
     pub fn new(version: SemanticVersion) -> Self {
-        Self { _version: version }
+        Self { version }
     }
 
-    pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(NotificationEvent::Dismiss);
+    pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(DismissEvent);
     }
 }

crates/call2/Cargo.toml πŸ”—

@@ -31,16 +31,19 @@ media = { path = "../media" }
 project = { package = "project2", path = "../project2" }
 settings = { package = "settings2", path = "../settings2" }
 util = { path = "../util" }
+ui = {package = "ui2", path = "../ui2"}
 workspace = {package = "workspace2", path = "../workspace2"}
 async-trait.workspace = true
 anyhow.workspace = true
 async-broadcast = "0.4"
 futures.workspace = true
+image = "0.23"
 postage.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 serde_derive.workspace = true
+smallvec.workspace = true
 
 [dev-dependencies]
 client = { package = "client2", path = "../client2", features = ["test-support"] }

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

@@ -1,8 +1,9 @@
 pub mod call_settings;
 pub mod participant;
 pub mod room;
+mod shared_screen;
 
-use anyhow::{anyhow, bail, Result};
+use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use audio::Audio;
 use call_settings::CallSettings;
@@ -13,8 +14,8 @@ use client::{
 use collections::HashSet;
 use futures::{channel::oneshot, future::Shared, Future, FutureExt};
 use gpui::{
-    AppContext, AsyncAppContext, AsyncWindowContext, Context, EventEmitter, Model, ModelContext,
-    Subscription, Task, View, ViewContext, WeakModel, WeakView,
+    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
+    Subscription, Task, View, ViewContext, VisualContext, WeakModel, WindowHandle,
 };
 pub use participant::ParticipantLocation;
 use postage::watch;
@@ -22,6 +23,7 @@ use project::Project;
 use room::Event;
 pub use room::Room;
 use settings::Settings;
+use shared_screen::SharedScreen;
 use std::sync::Arc;
 use util::ResultExt;
 use workspace::{item::ItemHandle, CallHandler, Pane, Workspace};
@@ -332,12 +334,55 @@ impl ActiveCall {
     pub fn join_channel(
         &mut self,
         channel_id: u64,
+        requesting_window: Option<WindowHandle<Workspace>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Model<Room>>>> {
         if let Some(room) = self.room().cloned() {
             if room.read(cx).channel_id() == Some(channel_id) {
-                return Task::ready(Ok(Some(room)));
-            } else {
+                return cx.spawn(|_, _| async move {
+                    todo!();
+                    // let future = room.update(&mut cx, |room, cx| {
+                    //     room.most_active_project(cx).map(|(host, project)| {
+                    //         room.join_project(project, host, app_state.clone(), cx)
+                    //     })
+                    // })
+
+                    // if let Some(future) = future {
+                    //     future.await?;
+                    // }
+
+                    // Ok(Some(room))
+                });
+            }
+
+            let should_prompt = room.update(cx, |room, _| {
+                room.channel_id().is_some()
+                    && room.is_sharing_project()
+                    && room.remote_participants().len() > 0
+            });
+            if should_prompt && requesting_window.is_some() {
+                return cx.spawn(|this, mut cx| async move {
+                    let answer = requesting_window.unwrap().update(&mut cx, |_, cx| {
+                        cx.prompt(
+                            PromptLevel::Warning,
+                            "Leaving this call will unshare your current project.\nDo you want to switch channels?",
+                            &["Yes, Join Channel", "Cancel"],
+                        )
+                    })?;
+                    if answer.await? == 1 {
+                        return Ok(None);
+                    }
+
+                    room.update(&mut cx, |room, cx| room.clear_state(cx))?;
+
+                    this.update(&mut cx, |this, cx| {
+                        this.join_channel(channel_id, requesting_window, cx)
+                    })?
+                    .await
+                });
+            }
+
+            if room.read(cx).channel_id().is_some() {
                 room.update(cx, |room, cx| room.clear_state(cx));
             }
         }
@@ -512,24 +557,17 @@ pub fn report_call_event_for_channel(
 
 pub struct Call {
     active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
-    parent_workspace: WeakView<Workspace>,
 }
 
 impl Call {
-    pub fn new(
-        parent_workspace: WeakView<Workspace>,
-        cx: &mut ViewContext<'_, Workspace>,
-    ) -> Box<dyn CallHandler> {
+    pub fn new(cx: &mut ViewContext<'_, Workspace>) -> Box<dyn CallHandler> {
         let mut active_call = None;
         if cx.has_global::<Model<ActiveCall>>() {
             let call = cx.global::<Model<ActiveCall>>().clone();
             let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
             active_call = Some((call, subscriptions));
         }
-        Box::new(Self {
-            active_call,
-            parent_workspace,
-        })
+        Box::new(Self { active_call })
     }
     fn on_active_call_event(
         workspace: &mut Workspace,
@@ -549,45 +587,10 @@ impl Call {
 
 #[async_trait(?Send)]
 impl CallHandler for Call {
-    fn shared_screen_for_peer(
-        &self,
-        peer_id: PeerId,
-        _pane: &View<Pane>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> Option<Box<dyn ItemHandle>> {
-        let (call, _) = self.active_call.as_ref()?;
-        let room = call.read(cx).room()?.read(cx);
-        let participant = room.remote_participant_for_peer_id(peer_id)?;
-        let _track = participant.video_tracks.values().next()?.clone();
-        let _user = participant.user.clone();
-        todo!();
-        // for item in pane.read(cx).items_of_type::<SharedScreen>() {
-        //     if item.read(cx).peer_id == peer_id {
-        //         return Box::new(Some(item));
-        //     }
-        // }
-
-        // Some(Box::new(cx.build_view(|cx| {
-        //     SharedScreen::new(&track, peer_id, user.clone(), cx)
-        // })))
-    }
-
-    fn room_id(&self, cx: &AppContext) -> Option<u64> {
-        Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
-    }
-    fn hang_up(&self, mut cx: AsyncWindowContext) -> Result<Task<Result<()>>> {
-        let Some((call, _)) = self.active_call.as_ref() else {
-            bail!("Cannot exit a call; not in a call");
-        };
-
-        call.update(&mut cx, |this, cx| this.hang_up(cx))
-    }
-    fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
-        ActiveCall::global(cx).read(cx).location().cloned()
-    }
     fn peer_state(
         &mut self,
         leader_id: PeerId,
+        project: &Model<Project>,
         cx: &mut ViewContext<Workspace>,
     ) -> Option<(bool, bool)> {
         let (call, _) = self.active_call.as_ref()?;
@@ -599,12 +602,7 @@ impl CallHandler for Call {
         match participant.location {
             ParticipantLocation::SharedProject { project_id } => {
                 leader_in_this_app = true;
-                leader_in_this_project = Some(project_id)
-                    == self
-                        .parent_workspace
-                        .update(cx, |this, cx| this.project().read(cx).remote_id())
-                        .log_err()
-                        .flatten();
+                leader_in_this_project = Some(project_id) == project.read(cx).remote_id();
             }
             ParticipantLocation::UnsharedProject => {
                 leader_in_this_app = true;
@@ -618,6 +616,134 @@ impl CallHandler for Call {
 
         Some((leader_in_this_project, leader_in_this_app))
     }
+
+    fn shared_screen_for_peer(
+        &self,
+        peer_id: PeerId,
+        pane: &View<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Box<dyn ItemHandle>> {
+        let (call, _) = self.active_call.as_ref()?;
+        let room = call.read(cx).room()?.read(cx);
+        let participant = room.remote_participant_for_peer_id(peer_id)?;
+        let track = participant.video_tracks.values().next()?.clone();
+        let user = participant.user.clone();
+        for item in pane.read(cx).items_of_type::<SharedScreen>() {
+            if item.read(cx).peer_id == peer_id {
+                return Some(Box::new(item));
+            }
+        }
+
+        Some(Box::new(cx.build_view(|cx| {
+            SharedScreen::new(&track, peer_id, user.clone(), cx)
+        })))
+    }
+    fn room_id(&self, cx: &AppContext) -> Option<u64> {
+        Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
+    }
+    fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
+        let Some((call, _)) = self.active_call.as_ref() else {
+            return Task::ready(Err(anyhow!("Cannot exit a call; not in a call")));
+        };
+
+        call.update(cx, |this, cx| this.hang_up(cx))
+    }
+    fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
+        ActiveCall::global(cx).read(cx).location().cloned()
+    }
+    fn invite(
+        &mut self,
+        called_user_id: u64,
+        initial_project: Option<Model<Project>>,
+        cx: &mut AppContext,
+    ) -> Task<Result<()>> {
+        ActiveCall::global(cx).update(cx, |this, cx| {
+            this.invite(called_user_id, initial_project, cx)
+        })
+    }
+    fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
+        self.active_call
+            .as_ref()
+            .map(|call| {
+                call.0.read(cx).room().map(|room| {
+                    room.read(cx)
+                        .remote_participants()
+                        .iter()
+                        .map(|participant| {
+                            (participant.1.user.clone(), participant.1.peer_id.clone())
+                        })
+                        .collect()
+                })
+            })
+            .flatten()
+    }
+    fn is_muted(&self, cx: &AppContext) -> Option<bool> {
+        self.active_call
+            .as_ref()
+            .map(|call| {
+                call.0
+                    .read(cx)
+                    .room()
+                    .map(|room| room.read(cx).is_muted(cx))
+            })
+            .flatten()
+    }
+    fn toggle_mute(&self, cx: &mut AppContext) {
+        self.active_call.as_ref().map(|call| {
+            call.0.update(cx, |this, cx| {
+                this.room().map(|room| {
+                    let room = room.clone();
+                    cx.spawn(|_, mut cx| async move {
+                        room.update(&mut cx, |this, cx| this.toggle_mute(cx))??
+                            .await
+                    })
+                    .detach_and_log_err(cx);
+                })
+            })
+        });
+    }
+    fn toggle_screen_share(&self, cx: &mut AppContext) {
+        self.active_call.as_ref().map(|call| {
+            call.0.update(cx, |this, cx| {
+                this.room().map(|room| {
+                    room.update(cx, |this, cx| {
+                        if this.is_screen_sharing() {
+                            this.unshare_screen(cx).log_err();
+                        } else {
+                            let t = this.share_screen(cx);
+                            cx.spawn(move |_, _| async move {
+                                t.await.log_err();
+                            })
+                            .detach();
+                        }
+                    })
+                })
+            })
+        });
+    }
+    fn toggle_deafen(&self, cx: &mut AppContext) {
+        self.active_call.as_ref().map(|call| {
+            call.0.update(cx, |this, cx| {
+                this.room().map(|room| {
+                    room.update(cx, |this, cx| {
+                        this.toggle_deafen(cx).log_err();
+                    })
+                })
+            })
+        });
+    }
+    fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
+        self.active_call
+            .as_ref()
+            .map(|call| {
+                call.0
+                    .read(cx)
+                    .room()
+                    .map(|room| room.read(cx).is_deafened())
+            })
+            .flatten()
+            .flatten()
+    }
 }
 
 #[cfg(test)]

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

@@ -4,7 +4,7 @@ use client::{proto, User};
 use collections::HashMap;
 use gpui::WeakModel;
 pub use live_kit_client::Frame;
-use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
+pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
 use project::Project;
 use std::sync::Arc;
 

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

@@ -21,7 +21,7 @@ use live_kit_client::{
 };
 use postage::{sink::Sink, stream::Stream, watch};
 use project::Project;
-use settings::Settings;
+use settings::Settings as _;
 use std::{future::Future, mem, sync::Arc, time::Duration};
 use util::{post_inc, ResultExt, TryFutureExt};
 
@@ -1267,7 +1267,6 @@ impl Room {
                     .ok_or_else(|| anyhow!("live-kit was not initialized"))?
                     .await
             };
-
             let publication = publish_track.await;
             this.upgrade()
                 .ok_or_else(|| anyhow!("room was dropped"))?

crates/call2/src/shared_screen.rs πŸ”—

@@ -0,0 +1,111 @@
+use crate::participant::{Frame, RemoteVideoTrack};
+use anyhow::Result;
+use client::{proto::PeerId, User};
+use futures::StreamExt;
+use gpui::{
+    div, img, AppContext, Div, Element, EventEmitter, FocusHandle, Focusable, FocusableView,
+    InteractiveElement, ParentElement, Render, SharedString, Styled, Task, View, ViewContext,
+    VisualContext, WindowContext,
+};
+use std::sync::{Arc, Weak};
+use ui::{h_stack, Icon, IconElement};
+use workspace::{item::Item, ItemNavHistory, WorkspaceId};
+
+pub enum Event {
+    Close,
+}
+
+pub struct SharedScreen {
+    track: Weak<RemoteVideoTrack>,
+    frame: Option<Frame>,
+    pub peer_id: PeerId,
+    user: Arc<User>,
+    nav_history: Option<ItemNavHistory>,
+    _maintain_frame: Task<Result<()>>,
+    focus: FocusHandle,
+}
+
+impl SharedScreen {
+    pub fn new(
+        track: &Arc<RemoteVideoTrack>,
+        peer_id: PeerId,
+        user: Arc<User>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        cx.focus_handle();
+        let mut frames = track.frames();
+        Self {
+            track: Arc::downgrade(track),
+            frame: None,
+            peer_id,
+            user,
+            nav_history: Default::default(),
+            _maintain_frame: cx.spawn(|this, mut cx| async move {
+                while let Some(frame) = frames.next().await {
+                    this.update(&mut cx, |this, cx| {
+                        this.frame = Some(frame);
+                        cx.notify();
+                    })?;
+                }
+                this.update(&mut cx, |_, cx| cx.emit(Event::Close))?;
+                Ok(())
+            }),
+            focus: cx.focus_handle(),
+        }
+    }
+}
+
+impl EventEmitter<Event> for SharedScreen {}
+impl EventEmitter<workspace::item::ItemEvent> for SharedScreen {}
+
+impl FocusableView for SharedScreen {
+    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+        self.focus.clone()
+    }
+}
+impl Render for SharedScreen {
+    type Element = Focusable<Div>;
+
+    fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
+        div().track_focus(&self.focus).size_full().children(
+            self.frame
+                .as_ref()
+                .map(|frame| img(frame.image()).size_full()),
+        )
+    }
+}
+
+impl Item for SharedScreen {
+    fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
+        Some(format!("{}'s screen", self.user.github_login).into())
+    }
+    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(nav_history) = self.nav_history.as_mut() {
+            nav_history.push::<()>(None, cx);
+        }
+    }
+
+    fn tab_content(&self, _: Option<usize>, _: &WindowContext<'_>) -> gpui::AnyElement {
+        h_stack()
+            .gap_1()
+            .child(IconElement::new(Icon::Screen))
+            .child(SharedString::from(format!(
+                "{}'s screen",
+                self.user.github_login
+            )))
+            .into_any()
+    }
+
+    fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
+        self.nav_history = Some(history);
+    }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<View<Self>> {
+        let track = self.track.upgrade()?;
+        Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
+    }
+}

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

@@ -551,7 +551,6 @@ impl Client {
         F: 'static + Future<Output = Result<()>>,
     {
         let message_type_id = TypeId::of::<M>();
-
         let mut state = self.state.write();
         state
             .models_by_message_type
@@ -694,8 +693,8 @@ impl Client {
         }
     }
 
-    pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
-        read_credentials_from_keychain(cx).await.is_some()
+    pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
+        read_credentials_from_keychain(cx).is_some()
     }
 
     #[async_recursion(?Send)]
@@ -726,7 +725,7 @@ impl Client {
         let mut read_from_keychain = false;
         let mut credentials = self.state.read().credentials.clone();
         if credentials.is_none() && try_keychain {
-            credentials = read_credentials_from_keychain(cx).await;
+            credentials = read_credentials_from_keychain(cx);
             read_from_keychain = credentials.is_some();
         }
         if credentials.is_none() {
@@ -1325,7 +1324,7 @@ impl Client {
     }
 }
 
-async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
+fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
     if IMPERSONATE_LOGIN.is_some() {
         return None;
     }

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

@@ -3941,7 +3941,7 @@ async fn test_collaborating_with_diagnostics(
     // Ensure client B observes the new diagnostics.
     project_b.read_with(cx_b, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             &[(
                 ProjectPath {
                     worktree_id,
@@ -3961,14 +3961,14 @@ async fn test_collaborating_with_diagnostics(
     let project_c = client_c.build_remote_project(project_id, cx_c).await;
     let project_c_diagnostic_summaries =
         Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
-            project.diagnostic_summaries(cx).collect::<Vec<_>>()
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
         })));
     project_c.update(cx_c, |_, cx| {
         let summaries = project_c_diagnostic_summaries.clone();
         cx.subscribe(&project_c, {
             move |p, _, event, cx| {
                 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
-                    *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
+                    *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
                 }
             }
         })
@@ -4018,7 +4018,7 @@ async fn test_collaborating_with_diagnostics(
     deterministic.run_until_parked();
     project_b.read_with(cx_b, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             [(
                 ProjectPath {
                     worktree_id,
@@ -4034,7 +4034,7 @@ async fn test_collaborating_with_diagnostics(
     });
     project_c.read_with(cx_c, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             [(
                 ProjectPath {
                     worktree_id,
@@ -4097,13 +4097,22 @@ async fn test_collaborating_with_diagnostics(
     );
     deterministic.run_until_parked();
     project_a.read_with(cx_a, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
     project_b.read_with(cx_b, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
     project_c.read_with(cx_c, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
 }
 

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

@@ -364,7 +364,8 @@ async fn test_joining_channel_ancestor_member(
     let active_call_b = cx_b.read(ActiveCall::global);
 
     assert!(active_call_b
-        .update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
+        .update(cx_b, |active_call, cx| active_call
+            .join_channel(sub_id, None, cx))
         .await
         .is_ok());
 }
@@ -394,7 +395,9 @@ async fn test_channel_room(
     let active_call_b = cx_b.read(ActiveCall::global);
 
     active_call_a
-        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
+        .update(cx_a, |active_call, cx| {
+            active_call.join_channel(zed_id, None, cx)
+        })
         .await
         .unwrap();
 
@@ -442,7 +445,9 @@ async fn test_channel_room(
     });
 
     active_call_b
-        .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
+        .update(cx_b, |active_call, cx| {
+            active_call.join_channel(zed_id, None, cx)
+        })
         .await
         .unwrap();
 
@@ -559,12 +564,16 @@ async fn test_channel_room(
     });
 
     active_call_a
-        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
+        .update(cx_a, |active_call, cx| {
+            active_call.join_channel(zed_id, None, cx)
+        })
         .await
         .unwrap();
 
     active_call_b
-        .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
+        .update(cx_b, |active_call, cx| {
+            active_call.join_channel(zed_id, None, cx)
+        })
         .await
         .unwrap();
 
@@ -608,7 +617,9 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
     let active_call_a = cx_a.read(ActiveCall::global);
 
     active_call_a
-        .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
+        .update(cx_a, |active_call, cx| {
+            active_call.join_channel(zed_id, None, cx)
+        })
         .await
         .unwrap();
 
@@ -627,7 +638,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
 
     active_call_a
         .update(cx_a, |active_call, cx| {
-            active_call.join_channel(rust_id, cx)
+            active_call.join_channel(rust_id, None, cx)
         })
         .await
         .unwrap();
@@ -793,7 +804,7 @@ async fn test_call_from_channel(
     let active_call_b = cx_b.read(ActiveCall::global);
 
     active_call_a
-        .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
+        .update(cx_a, |call, cx| call.join_channel(channel_id, None, cx))
         .await
         .unwrap();
 
@@ -1286,7 +1297,7 @@ async fn test_guest_access(
 
     // Non-members should not be allowed to join
     assert!(active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
         .await
         .is_err());
 
@@ -1308,7 +1319,7 @@ async fn test_guest_access(
 
     // Client B joins channel A as a guest
     active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
         .await
         .unwrap();
 
@@ -1341,7 +1352,7 @@ async fn test_guest_access(
     assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
 
     active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_b, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_b, None, cx))
         .await
         .unwrap();
 
@@ -1372,7 +1383,7 @@ async fn test_invite_access(
 
     // should not be allowed to join
     assert!(active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
         .await
         .is_err());
 
@@ -1390,7 +1401,7 @@ async fn test_invite_access(
         .unwrap();
 
     active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
+        .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
         .await
         .unwrap();
 

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

@@ -510,9 +510,10 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
 
     // Simultaneously join channel 1 and then channel 2
     active_call_a
-        .update(cx_a, |call, cx| call.join_channel(channel_1, cx))
+        .update(cx_a, |call, cx| call.join_channel(channel_1, None, cx))
         .detach();
-    let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
+    let join_channel_2 =
+        active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx));
 
     join_channel_2.await.unwrap();
 
@@ -538,7 +539,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
         call.invite(client_c.user_id().unwrap(), None, cx)
     });
 
-    let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
+    let join_channel =
+        active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
 
     b_invite.await.unwrap();
     c_invite.await.unwrap();
@@ -567,7 +569,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
         .unwrap();
 
     // Simultaneously join channel 1 and call user B and user C from client A.
-    let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
+    let join_channel =
+        active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
 
     let b_invite = active_call_a.update(cx_a, |call, cx| {
         call.invite(client_b.user_id().unwrap(), None, cx)
@@ -3685,7 +3688,7 @@ async fn test_collaborating_with_diagnostics(
 
     project_b.read_with(cx_b, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             &[(
                 ProjectPath {
                     worktree_id,
@@ -3705,14 +3708,14 @@ async fn test_collaborating_with_diagnostics(
     let project_c = client_c.build_remote_project(project_id, cx_c).await;
     let project_c_diagnostic_summaries =
         Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
-            project.diagnostic_summaries(cx).collect::<Vec<_>>()
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
         })));
     project_c.update(cx_c, |_, cx| {
         let summaries = project_c_diagnostic_summaries.clone();
         cx.subscribe(&project_c, {
             move |p, _, event, cx| {
                 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
-                    *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
+                    *summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
                 }
             }
         })
@@ -3763,7 +3766,7 @@ async fn test_collaborating_with_diagnostics(
 
     project_b.read_with(cx_b, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             [(
                 ProjectPath {
                     worktree_id,
@@ -3780,7 +3783,7 @@ async fn test_collaborating_with_diagnostics(
 
     project_c.read_with(cx_c, |project, cx| {
         assert_eq!(
-            project.diagnostic_summaries(cx).collect::<Vec<_>>(),
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
             [(
                 ProjectPath {
                     worktree_id,
@@ -3841,15 +3844,24 @@ async fn test_collaborating_with_diagnostics(
     executor.run_until_parked();
 
     project_a.read_with(cx_a, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
 
     project_b.read_with(cx_b, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
 
     project_c.read_with(cx_c, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
+        assert_eq!(
+            project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
+            []
+        )
     });
 }
 

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

@@ -221,7 +221,7 @@ impl TestServer {
             fs: fs.clone(),
             build_window_options: |_, _, _| Default::default(),
             node_runtime: FakeNodeRuntime::new(),
-            call_factory: |_, _| Box::new(workspace::TestCallHandler),
+            call_factory: |_| Box::new(workspace::TestCallHandler),
         });
 
         cx.update(|cx| {

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

@@ -1,5 +1,6 @@
+#![allow(unused)]
 // mod channel_modal;
-// mod contact_finder;
+mod contact_finder;
 
 // use crate::{
 //     channel_view::{self, ChannelView},
@@ -15,7 +16,10 @@
 //     proto::{self, PeerId},
 //     Client, Contact, User, UserStore,
 // };
-// use contact_finder::ContactFinder;
+use contact_finder::ContactFinder;
+use menu::{Cancel, Confirm, SelectNext, SelectPrev};
+use rpc::proto;
+use theme::{ActiveTheme, ThemeSettings};
 // use context_menu::{ContextMenu, ContextMenuItem};
 // use db::kvp::KEY_VALUE_STORE;
 // use drag_and_drop::{DragAndDrop, Draggable};
@@ -88,10 +92,10 @@
 //     channel_id: ChannelId,
 // }
 
-// #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-// pub struct OpenChannelNotes {
-//     pub channel_id: ChannelId,
-// }
+#[derive(Action, PartialEq, Debug, Clone, Serialize, Deserialize)]
+pub struct OpenChannelNotes {
+    pub channel_id: ChannelId,
+}
 
 // #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 // pub struct JoinChannelCall {
@@ -148,31 +152,45 @@ actions!(
 //     ]
 // );
 
-// #[derive(Debug, Copy, Clone, PartialEq, Eq)]
-// struct ChannelMoveClipboard {
-//     channel_id: ChannelId,
-// }
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+struct ChannelMoveClipboard {
+    channel_id: ChannelId,
+}
 
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 
-use std::sync::Arc;
+use std::{iter::once, mem, sync::Arc};
 
+use call::ActiveCall;
+use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
+use client::{Client, Contact, User, UserStore};
 use db::kvp::KEY_VALUE_STORE;
+use editor::Editor;
+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
+use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
-    Focusable, FocusableView, InteractiveElement, ParentElement, Render, View, ViewContext,
-    VisualContext, WeakView,
+    actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext,
+    AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle,
+    Focusable, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent,
+    ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, ScrollHandle, SharedString,
+    Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
 };
-use project::Fs;
+use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
-use settings::Settings;
-use util::ResultExt;
+use settings::{Settings, SettingsStore};
+use ui::prelude::*;
+use ui::{
+    h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
+    Label, List, ListHeader, ListItem, Tooltip,
+};
+use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
+    notifications::NotifyResultExt,
     Workspace,
 };
 
-use crate::CollaborationPanelSettings;
+use crate::{face_pile::FacePile, CollaborationPanelSettings};
 
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(|workspace: &mut Workspace, _| {
@@ -215,26 +233,6 @@ pub fn init(cx: &mut AppContext) {
     //         },
     //     );
 
-    //     cx.add_action(
-    //         |panel: &mut CollabPanel,
-    //          action: &StartMoveChannelFor,
-    //          _: &mut ViewContext<CollabPanel>| {
-    //             panel.channel_clipboard = Some(ChannelMoveClipboard {
-    //                 channel_id: action.channel_id,
-    //             });
-    //         },
-    //     );
-
-    //     cx.add_action(
-    //         |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
-    //             if let Some(channel) = panel.selected_channel() {
-    //                 panel.channel_clipboard = Some(ChannelMoveClipboard {
-    //                     channel_id: channel.id,
-    //                 })
-    //             }
-    //         },
-    //     );
-
     //     cx.add_action(
     //         |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
     //             let Some(clipboard) = panel.channel_clipboard.take() else {
@@ -266,63 +264,63 @@ pub fn init(cx: &mut AppContext) {
     //     );
 }
 
-// #[derive(Debug)]
-// pub enum ChannelEditingState {
-//     Create {
-//         location: Option<ChannelId>,
-//         pending_name: Option<String>,
-//     },
-//     Rename {
-//         location: ChannelId,
-//         pending_name: Option<String>,
-//     },
-// }
+#[derive(Debug)]
+pub enum ChannelEditingState {
+    Create {
+        location: Option<ChannelId>,
+        pending_name: Option<String>,
+    },
+    Rename {
+        location: ChannelId,
+        pending_name: Option<String>,
+    },
+}
 
-// impl ChannelEditingState {
-//     fn pending_name(&self) -> Option<&str> {
-//         match self {
-//             ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
-//             ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
-//         }
-//     }
-// }
+impl ChannelEditingState {
+    fn pending_name(&self) -> Option<String> {
+        match self {
+            ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
+            ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
+        }
+    }
+}
 
 pub struct CollabPanel {
-    width: Option<f32>,
+    width: Option<Pixels>,
     fs: Arc<dyn Fs>,
     focus_handle: FocusHandle,
-    // channel_clipboard: Option<ChannelMoveClipboard>,
-    // pending_serialization: Task<Option<()>>,
-    // context_menu: ViewHandle<ContextMenu>,
-    // filter_editor: ViewHandle<Editor>,
-    // channel_name_editor: ViewHandle<Editor>,
-    // channel_editing_state: Option<ChannelEditingState>,
-    // entries: Vec<ListEntry>,
-    // selection: Option<usize>,
-    // user_store: ModelHandle<UserStore>,
-    // client: Arc<Client>,
-    // channel_store: ModelHandle<ChannelStore>,
-    // project: ModelHandle<Project>,
-    // match_candidates: Vec<StringMatchCandidate>,
-    // list_state: ListState<Self>,
-    // subscriptions: Vec<Subscription>,
-    // collapsed_sections: Vec<Section>,
-    // collapsed_channels: Vec<ChannelId>,
-    // drag_target_channel: ChannelDragTarget,
-    _workspace: WeakView<Workspace>,
+    channel_clipboard: Option<ChannelMoveClipboard>,
+    pending_serialization: Task<Option<()>>,
+    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
+    filter_editor: View<Editor>,
+    channel_name_editor: View<Editor>,
+    channel_editing_state: Option<ChannelEditingState>,
+    entries: Vec<ListEntry>,
+    selection: Option<usize>,
+    channel_store: Model<ChannelStore>,
+    user_store: Model<UserStore>,
+    client: Arc<Client>,
+    project: Model<Project>,
+    match_candidates: Vec<StringMatchCandidate>,
+    scroll_handle: ScrollHandle,
+    subscriptions: Vec<Subscription>,
+    collapsed_sections: Vec<Section>,
+    collapsed_channels: Vec<ChannelId>,
+    drag_target_channel: ChannelDragTarget,
+    workspace: WeakView<Workspace>,
     // context_menu_on_selected: bool,
 }
 
-// #[derive(PartialEq, Eq)]
-// enum ChannelDragTarget {
-//     None,
-//     Root,
-//     Channel(ChannelId),
-// }
+#[derive(PartialEq, Eq)]
+enum ChannelDragTarget {
+    None,
+    Root,
+    Channel(ChannelId),
+}
 
 #[derive(Serialize, Deserialize)]
 struct SerializedCollabPanel {
-    width: Option<f32>,
+    width: Option<Pixels>,
     collapsed_channels: Option<Vec<u64>>,
 }
 
@@ -333,124 +331,108 @@ struct SerializedCollabPanel {
 //     Dismissed,
 // }
 
-// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
-// enum Section {
-//     ActiveCall,
-//     Channels,
-//     ChannelInvites,
-//     ContactRequests,
-//     Contacts,
-//     Online,
-//     Offline,
-// }
-
-// #[derive(Clone, Debug)]
-// enum ListEntry {
-//     Header(Section),
-//     CallParticipant {
-//         user: Arc<User>,
-//         peer_id: Option<PeerId>,
-//         is_pending: bool,
-//     },
-//     ParticipantProject {
-//         project_id: u64,
-//         worktree_root_names: Vec<String>,
-//         host_user_id: u64,
-//         is_last: bool,
-//     },
-//     ParticipantScreen {
-//         peer_id: Option<PeerId>,
-//         is_last: bool,
-//     },
-//     IncomingRequest(Arc<User>),
-//     OutgoingRequest(Arc<User>),
-//     ChannelInvite(Arc<Channel>),
-//     Channel {
-//         channel: Arc<Channel>,
-//         depth: usize,
-//         has_children: bool,
-//     },
-//     ChannelNotes {
-//         channel_id: ChannelId,
-//     },
-//     ChannelChat {
-//         channel_id: ChannelId,
-//     },
-//     ChannelEditor {
-//         depth: usize,
-//     },
-//     Contact {
-//         contact: Arc<Contact>,
-//         calling: bool,
-//     },
-//     ContactPlaceholder,
-// }
+#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
+enum Section {
+    ActiveCall,
+    Channels,
+    ChannelInvites,
+    ContactRequests,
+    Contacts,
+    Online,
+    Offline,
+}
 
-// impl Entity for CollabPanel {
-//     type Event = Event;
-// }
+#[derive(Clone, Debug)]
+enum ListEntry {
+    Header(Section),
+    //     CallParticipant {
+    //         user: Arc<User>,
+    //         peer_id: Option<PeerId>,
+    //         is_pending: bool,
+    //     },
+    //     ParticipantProject {
+    //         project_id: u64,
+    //         worktree_root_names: Vec<String>,
+    //         host_user_id: u64,
+    //         is_last: bool,
+    //     },
+    //     ParticipantScreen {
+    //         peer_id: Option<PeerId>,
+    //         is_last: bool,
+    //     },
+    IncomingRequest(Arc<User>),
+    OutgoingRequest(Arc<User>),
+    //     ChannelInvite(Arc<Channel>),
+    Channel {
+        channel: Arc<Channel>,
+        depth: usize,
+        has_children: bool,
+    },
+    //     ChannelNotes {
+    //         channel_id: ChannelId,
+    //     },
+    //     ChannelChat {
+    //         channel_id: ChannelId,
+    //     },
+    ChannelEditor {
+        depth: usize,
+    },
+    Contact {
+        contact: Arc<Contact>,
+        calling: bool,
+    },
+    ContactPlaceholder,
+}
 
 impl CollabPanel {
     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
         cx.build_view(|cx| {
             //             let view_id = cx.view_id();
 
-            //             let filter_editor = cx.add_view(|cx| {
-            //                 let mut editor = Editor::single_line(
-            //                     Some(Arc::new(|theme| {
-            //                         theme.collab_panel.user_query_editor.clone()
-            //                     })),
-            //                     cx,
-            //                 );
-            //                 editor.set_placeholder_text("Filter channels, contacts", cx);
-            //                 editor
-            //             });
-
-            //             cx.subscribe(&filter_editor, |this, _, event, cx| {
-            //                 if let editor::Event::BufferEdited = event {
-            //                     let query = this.filter_editor.read(cx).text(cx);
-            //                     if !query.is_empty() {
-            //                         this.selection.take();
-            //                     }
-            //                     this.update_entries(true, cx);
-            //                     if !query.is_empty() {
-            //                         this.selection = this
-            //                             .entries
-            //                             .iter()
-            //                             .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);
-            //                     }
-            //                 }
-            //             })
-            //             .detach();
-
-            //             let channel_name_editor = cx.add_view(|cx| {
-            //                 Editor::single_line(
-            //                     Some(Arc::new(|theme| {
-            //                         theme.collab_panel.user_query_editor.clone()
-            //                     })),
-            //                     cx,
-            //                 )
-            //             });
-
-            //             cx.subscribe(&channel_name_editor, |this, _, event, cx| {
-            //                 if let editor::Event::Blurred = event {
-            //                     if let Some(state) = &this.channel_editing_state {
-            //                         if state.pending_name().is_some() {
-            //                             return;
-            //                         }
-            //                     }
-            //                     this.take_editing_state(cx);
-            //                     this.update_entries(false, cx);
-            //                     cx.notify();
-            //                 }
-            //             })
-            //             .detach();
+            let filter_editor = cx.build_view(|cx| {
+                let mut editor = Editor::single_line(cx);
+                editor.set_placeholder_text("Filter channels, contacts", cx);
+                editor
+            });
+
+            cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
+                if let editor::EditorEvent::BufferEdited = event {
+                    let query = this.filter_editor.read(cx).text(cx);
+                    if !query.is_empty() {
+                        this.selection.take();
+                    }
+                    this.update_entries(true, cx);
+                    if !query.is_empty() {
+                        this.selection = this
+                            .entries
+                            .iter()
+                            .position(|entry| !matches!(entry, ListEntry::Header(_)));
+                    }
+                } else if let editor::EditorEvent::Blurred = event {
+                    let query = this.filter_editor.read(cx).text(cx);
+                    if query.is_empty() {
+                        this.selection.take();
+                        this.update_entries(true, cx);
+                    }
+                }
+            })
+            .detach();
+
+            let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx));
+
+            cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| {
+                if let editor::EditorEvent::Blurred = event {
+                    if let Some(state) = &this.channel_editing_state {
+                        if state.pending_name().is_some() {
+                            return;
+                        }
+                    }
+                    this.take_editing_state(cx);
+                    this.update_entries(false, cx);
+                    cx.notify();
+                }
+            })
+            .detach();
 
             //             let list_state =
             //                 ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
@@ -583,86 +565,88 @@ impl CollabPanel {
             //                     }
             //                 });
 
-            let this = Self {
+            let mut this = Self {
                 width: None,
                 focus_handle: cx.focus_handle(),
-                //                 channel_clipboard: None,
+                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)),
-                //                 channel_name_editor,
-                //                 filter_editor,
-                //                 entries: Vec::default(),
-                //                 channel_editing_state: None,
-                //                 selection: None,
-                //                 user_store: workspace.user_store().clone(),
-                //                 channel_store: ChannelStore::global(cx),
-                //                 project: workspace.project().clone(),
-                //                 subscriptions: Vec::default(),
-                //                 match_candidates: Vec::default(),
-                //                 collapsed_sections: vec![Section::Offline],
-                //                 collapsed_channels: Vec::default(),
-                _workspace: workspace.weak_handle(),
-                //                 client: workspace.app_state().client.clone(),
+                pending_serialization: Task::ready(None),
+                context_menu: None,
+                channel_name_editor,
+                filter_editor,
+                entries: Vec::default(),
+                channel_editing_state: None,
+                selection: None,
+                channel_store: ChannelStore::global(cx),
+                user_store: workspace.user_store().clone(),
+                project: workspace.project().clone(),
+                subscriptions: Vec::default(),
+                match_candidates: Vec::default(),
+                scroll_handle: ScrollHandle::new(),
+                collapsed_sections: vec![Section::Offline],
+                collapsed_channels: Vec::default(),
+                workspace: workspace.weak_handle(),
+                client: workspace.app_state().client.clone(),
                 //                 context_menu_on_selected: true,
-                //                 drag_target_channel: ChannelDragTarget::None,
-                //                 list_state,
+                drag_target_channel: ChannelDragTarget::None,
             };
 
-            //             this.update_entries(false, cx);
-
-            //             // Update the dock position when the setting changes.
-            //             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();
-            //                     }),
-            //                 );
-
-            //             let active_call = ActiveCall::global(cx);
-            //             this.subscriptions
-            //                 .push(cx.observe(&this.user_store, |this, _, cx| {
-            //                     this.update_entries(true, cx)
-            //                 }));
-            //             this.subscriptions
-            //                 .push(cx.observe(&this.channel_store, |this, _, cx| {
-            //                     this.update_entries(true, cx)
-            //                 }));
-            //             this.subscriptions
-            //                 .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
-            //             this.subscriptions
-            //                 .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
-            //                     this.update_entries(true, cx)
-            //                 }));
-            //             this.subscriptions.push(cx.subscribe(
-            //                 &this.channel_store,
-            //                 |this, _channel_store, e, cx| match e {
-            //                     ChannelEvent::ChannelCreated(channel_id)
-            //                     | ChannelEvent::ChannelRenamed(channel_id) => {
-            //                         if this.take_editing_state(cx) {
-            //                             this.update_entries(false, cx);
-            //                             this.selection = this.entries.iter().position(|entry| {
-            //                                 if let ListEntry::Channel { channel, .. } = entry {
-            //                                     channel.id == *channel_id
-            //                                 } else {
-            //                                     false
-            //                                 }
-            //                             });
-            //                         }
-            //                     }
-            //                 },
-            //             ));
+            this.update_entries(false, cx);
+
+            // Update the dock position when the setting changes.
+            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(PanelEvent::ChangePosition);
+                    }
+                    cx.notify();
+                },
+            ));
+
+            let active_call = ActiveCall::global(cx);
+            this.subscriptions
+                .push(cx.observe(&this.user_store, |this, _, cx| {
+                    this.update_entries(true, cx)
+                }));
+            this.subscriptions
+                .push(cx.observe(&this.channel_store, |this, _, cx| {
+                    this.update_entries(true, cx)
+                }));
+            this.subscriptions
+                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
+            this.subscriptions
+                .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
+                    this.update_entries(true, cx)
+                }));
+            this.subscriptions.push(cx.subscribe(
+                &this.channel_store,
+                |this, _channel_store, e, cx| match e {
+                    ChannelEvent::ChannelCreated(channel_id)
+                    | ChannelEvent::ChannelRenamed(channel_id) => {
+                        if this.take_editing_state(cx) {
+                            this.update_entries(false, cx);
+                            this.selection = this.entries.iter().position(|entry| {
+                                if let ListEntry::Channel { channel, .. } = entry {
+                                    channel.id == *channel_id
+                                } else {
+                                    false
+                                }
+                            });
+                        }
+                    }
+                },
+            ));
 
             this
         })
     }
 
+    fn contacts(&self, cx: &AppContext) -> Option<Vec<Arc<Contact>>> {
+        Some(self.user_store.read(cx).contacts().to_owned())
+    }
     pub async fn load(
         workspace: WeakView<Workspace>,
         mut cx: AsyncWindowContext,
@@ -684,10 +668,9 @@ impl CollabPanel {
             if let Some(serialized_panel) = serialized_panel {
                 panel.update(cx, |panel, cx| {
                     panel.width = serialized_panel.width;
-                    //todo!(collapsed_channels)
-                    // panel.collapsed_channels = serialized_panel
-                    //     .collapsed_channels
-                    //     .unwrap_or_else(|| Vec::new());
+                    panel.collapsed_channels = serialized_panel
+                        .collapsed_channels
+                        .unwrap_or_else(|| Vec::new());
                     cx.notify();
                 });
             }
@@ -695,469 +678,460 @@ impl CollabPanel {
         })
     }
 
-    //     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
-    //         let width = self.width;
-    //         let collapsed_channels = self.collapsed_channels.clone();
-    //         self.pending_serialization = cx.background().spawn(
-    //             async move {
-    //                 KEY_VALUE_STORE
-    //                     .write_kvp(
-    //                         COLLABORATION_PANEL_KEY.into(),
-    //                         serde_json::to_string(&SerializedCollabPanel {
-    //                             width,
-    //                             collapsed_channels: Some(collapsed_channels),
-    //                         })?,
-    //                     )
-    //                     .await?;
-    //                 anyhow::Ok(())
-    //             }
-    //             .log_err(),
-    //         );
-    //     }
-
-    //     fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
-    //         let channel_store = self.channel_store.read(cx);
-    //         let user_store = self.user_store.read(cx);
-    //         let query = self.filter_editor.read(cx).text(cx);
-    //         let executor = cx.background().clone();
-
-    //         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));
-    //             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);
-
-    //                 if let Some(channel_id) = room.channel_id() {
-    //                     self.entries.push(ListEntry::ChannelNotes { channel_id });
-    //                     self.entries.push(ListEntry::ChannelChat { channel_id })
-    //                 }
-
-    //                 // Populate the active user.
-    //                 if let Some(user) = user_store.current_user() {
-    //                     self.match_candidates.clear();
-    //                     self.match_candidates.push(StringMatchCandidate {
-    //                         id: 0,
-    //                         string: user.github_login.clone(),
-    //                         char_bag: user.github_login.chars().collect(),
-    //                     });
-    //                     let matches = executor.block(match_strings(
-    //                         &self.match_candidates,
-    //                         &query,
-    //                         true,
-    //                         usize::MAX,
-    //                         &Default::default(),
-    //                         executor.clone(),
-    //                     ));
-    //                     if !matches.is_empty() {
-    //                         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();
-    //                         while let Some(project) = projects.next() {
-    //                             self.entries.push(ListEntry::ParticipantProject {
-    //                                 project_id: project.id,
-    //                                 worktree_root_names: project.worktree_root_names.clone(),
-    //                                 host_user_id: user_id,
-    //                                 is_last: projects.peek().is_none() && !room.is_screen_sharing(),
-    //                             });
-    //                         }
-    //                         if room.is_screen_sharing() {
-    //                             self.entries.push(ListEntry::ParticipantScreen {
-    //                                 peer_id: None,
-    //                                 is_last: true,
-    //                             });
-    //                         }
-    //                     }
-    //                 }
-
-    //                 // Populate remote participants.
-    //                 self.match_candidates.clear();
-    //                 self.match_candidates
-    //                     .extend(room.remote_participants().iter().map(|(_, participant)| {
-    //                         StringMatchCandidate {
-    //                             id: participant.user.id as usize,
-    //                             string: participant.user.github_login.clone(),
-    //                             char_bag: participant.user.github_login.chars().collect(),
-    //                         }
-    //                     }));
-    //                 let matches = executor.block(match_strings(
-    //                     &self.match_candidates,
-    //                     &query,
-    //                     true,
-    //                     usize::MAX,
-    //                     &Default::default(),
-    //                     executor.clone(),
-    //                 ));
-    //                 for mat in matches {
-    //                     let user_id = mat.candidate_id as u64;
-    //                     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();
-    //                     while let Some(project) = projects.next() {
-    //                         self.entries.push(ListEntry::ParticipantProject {
-    //                             project_id: project.id,
-    //                             worktree_root_names: project.worktree_root_names.clone(),
-    //                             host_user_id: participant.user.id,
-    //                             is_last: projects.peek().is_none()
-    //                                 && participant.video_tracks.is_empty(),
-    //                         });
-    //                     }
-    //                     if !participant.video_tracks.is_empty() {
-    //                         self.entries.push(ListEntry::ParticipantScreen {
-    //                             peer_id: Some(participant.peer_id),
-    //                             is_last: true,
-    //                         });
-    //                     }
-    //                 }
-
-    //                 // Populate pending participants.
-    //                 self.match_candidates.clear();
-    //                 self.match_candidates
-    //                     .extend(room.pending_participants().iter().enumerate().map(
-    //                         |(id, participant)| StringMatchCandidate {
-    //                             id,
-    //                             string: participant.github_login.clone(),
-    //                             char_bag: participant.github_login.chars().collect(),
-    //                         },
-    //                     ));
-    //                 let matches = executor.block(match_strings(
-    //                     &self.match_candidates,
-    //                     &query,
-    //                     true,
-    //                     usize::MAX,
-    //                     &Default::default(),
-    //                     executor.clone(),
-    //                 ));
-    //                 self.entries
-    //                     .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
-    //                         user: room.pending_participants()[mat.candidate_id].clone(),
-    //                         peer_id: None,
-    //                         is_pending: true,
-    //                     }));
-    //             }
-    //         }
-
-    //         let mut request_entries = Vec::new();
-
-    //         if cx.has_flag::<ChannelsAlpha>() {
-    //             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.ordered_channels().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,
-    //                     true,
-    //                     usize::MAX,
-    //                     &Default::default(),
-    //                     executor.clone(),
-    //                 ));
-    //                 if let Some(state) = &self.channel_editing_state {
-    //                     if matches!(state, ChannelEditingState::Create { location: None, .. }) {
-    //                         self.entries.push(ListEntry::ChannelEditor { depth: 0 });
-    //                     }
-    //                 }
-    //                 let mut collapse_depth = None;
-    //                 for mat in matches {
-    //                     let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
-    //                     let depth = channel.parent_path.len();
-
-    //                     if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
-    //                         collapse_depth = Some(depth);
-    //                     } else if let Some(collapsed_depth) = collapse_depth {
-    //                         if depth > collapsed_depth {
-    //                             continue;
-    //                         }
-    //                         if self.is_channel_collapsed(channel.id) {
-    //                             collapse_depth = Some(depth);
-    //                         } else {
-    //                             collapse_depth = None;
-    //                         }
-    //                     }
-
-    //                     let has_children = channel_store
-    //                         .channel_at_index(mat.candidate_id + 1)
-    //                         .map_or(false, |next_channel| {
-    //                             next_channel.parent_path.ends_with(&[channel.id])
-    //                         });
-
-    //                     match &self.channel_editing_state {
-    //                         Some(ChannelEditingState::Create {
-    //                             location: parent_id,
-    //                             ..
-    //                         }) if *parent_id == Some(channel.id) => {
-    //                             self.entries.push(ListEntry::Channel {
-    //                                 channel: channel.clone(),
-    //                                 depth,
-    //                                 has_children: false,
-    //                             });
-    //                             self.entries
-    //                                 .push(ListEntry::ChannelEditor { depth: depth + 1 });
-    //                         }
-    //                         Some(ChannelEditingState::Rename {
-    //                             location: parent_id,
-    //                             ..
-    //                         }) if parent_id == &channel.id => {
-    //                             self.entries.push(ListEntry::ChannelEditor { depth });
-    //                         }
-    //                         _ => {
-    //                             self.entries.push(ListEntry::Channel {
-    //                                 channel: channel.clone(),
-    //                                 depth,
-    //                                 has_children,
-    //                             });
-    //                         }
-    //                     }
-    //                 }
-    //             }
-
-    //             let channel_invites = channel_store.channel_invitations();
-    //             if !channel_invites.is_empty() {
-    //                 self.match_candidates.clear();
-    //                 self.match_candidates
-    //                     .extend(channel_invites.iter().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,
-    //                     true,
-    //                     usize::MAX,
-    //                     &Default::default(),
-    //                     executor.clone(),
-    //                 ));
-    //                 request_entries.extend(matches.iter().map(|mat| {
-    //                     ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
-    //                 }));
-
-    //                 if !request_entries.is_empty() {
-    //                     self.entries
-    //                         .push(ListEntry::Header(Section::ChannelInvites));
-    //                     if !self.collapsed_sections.contains(&Section::ChannelInvites) {
-    //                         self.entries.append(&mut request_entries);
-    //                     }
-    //                 }
-    //             }
-    //         }
-
-    //         self.entries.push(ListEntry::Header(Section::Contacts));
-
-    //         request_entries.clear();
-    //         let incoming = user_store.incoming_contact_requests();
-    //         if !incoming.is_empty() {
-    //             self.match_candidates.clear();
-    //             self.match_candidates
-    //                 .extend(
-    //                     incoming
-    //                         .iter()
-    //                         .enumerate()
-    //                         .map(|(ix, user)| StringMatchCandidate {
-    //                             id: ix,
-    //                             string: user.github_login.clone(),
-    //                             char_bag: user.github_login.chars().collect(),
-    //                         }),
-    //                 );
-    //             let matches = executor.block(match_strings(
-    //                 &self.match_candidates,
-    //                 &query,
-    //                 true,
-    //                 usize::MAX,
-    //                 &Default::default(),
-    //                 executor.clone(),
-    //             ));
-    //             request_entries.extend(
-    //                 matches
-    //                     .iter()
-    //                     .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
-    //             );
-    //         }
-
-    //         let outgoing = user_store.outgoing_contact_requests();
-    //         if !outgoing.is_empty() {
-    //             self.match_candidates.clear();
-    //             self.match_candidates
-    //                 .extend(
-    //                     outgoing
-    //                         .iter()
-    //                         .enumerate()
-    //                         .map(|(ix, user)| StringMatchCandidate {
-    //                             id: ix,
-    //                             string: user.github_login.clone(),
-    //                             char_bag: user.github_login.chars().collect(),
-    //                         }),
-    //                 );
-    //             let matches = executor.block(match_strings(
-    //                 &self.match_candidates,
-    //                 &query,
-    //                 true,
-    //                 usize::MAX,
-    //                 &Default::default(),
-    //                 executor.clone(),
-    //             ));
-    //             request_entries.extend(
-    //                 matches
-    //                     .iter()
-    //                     .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
-    //             );
-    //         }
-
-    //         if !request_entries.is_empty() {
-    //             self.entries
-    //                 .push(ListEntry::Header(Section::ContactRequests));
-    //             if !self.collapsed_sections.contains(&Section::ContactRequests) {
-    //                 self.entries.append(&mut request_entries);
-    //             }
-    //         }
-
-    //         let contacts = user_store.contacts();
-    //         if !contacts.is_empty() {
-    //             self.match_candidates.clear();
-    //             self.match_candidates
-    //                 .extend(
-    //                     contacts
-    //                         .iter()
-    //                         .enumerate()
-    //                         .map(|(ix, contact)| StringMatchCandidate {
-    //                             id: ix,
-    //                             string: contact.user.github_login.clone(),
-    //                             char_bag: contact.user.github_login.chars().collect(),
-    //                         }),
-    //                 );
-
-    //             let matches = executor.block(match_strings(
-    //                 &self.match_candidates,
-    //                 &query,
-    //                 true,
-    //                 usize::MAX,
-    //                 &Default::default(),
-    //                 executor.clone(),
-    //             ));
-
-    //             let (online_contacts, offline_contacts) = matches
-    //                 .iter()
-    //                 .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
-
-    //             for (matches, section) in [
-    //                 (online_contacts, Section::Online),
-    //                 (offline_contacts, Section::Offline),
-    //             ] {
-    //                 if !matches.is_empty() {
-    //                     self.entries.push(ListEntry::Header(section));
-    //                     if !self.collapsed_sections.contains(&section) {
-    //                         let active_call = &ActiveCall::global(cx).read(cx);
-    //                         for mat in matches {
-    //                             let contact = &contacts[mat.candidate_id];
-    //                             self.entries.push(ListEntry::Contact {
-    //                                 contact: contact.clone(),
-    //                                 calling: active_call.pending_invites().contains(&contact.user.id),
-    //                             });
-    //                         }
-    //                     }
-    //                 }
-    //             }
-    //         }
-
-    //         if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
-    //             self.entries.push(ListEntry::ContactPlaceholder);
-    //         }
-
-    //         if select_same_item {
-    //             if let Some(prev_selected_entry) = prev_selected_entry {
-    //                 self.selection.take();
-    //                 for (ix, entry) in self.entries.iter().enumerate() {
-    //                     if *entry == prev_selected_entry {
-    //                         self.selection = Some(ix);
-    //                         break;
-    //                     }
-    //                 }
-    //             }
-    //         } else {
-    //             self.selection = self.selection.and_then(|prev_selection| {
-    //                 if self.entries.is_empty() {
-    //                     None
-    //                 } else {
-    //                     Some(prev_selection.min(self.entries.len() - 1))
-    //                 }
-    //             });
-    //         }
-
-    //         let old_scroll_top = self.list_state.logical_scroll_top();
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let width = self.width;
+        let collapsed_channels = self.collapsed_channels.clone();
+        self.pending_serialization = cx.background_executor().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        COLLABORATION_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedCollabPanel {
+                            width,
+                            collapsed_channels: Some(collapsed_channels),
+                        })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
 
-    //         self.list_state.reset(self.entries.len());
+    fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
+        let channel_store = self.channel_store.read(cx);
+        let user_store = self.user_store.read(cx);
+        let query = self.filter_editor.read(cx).text(cx);
+        let executor = cx.background_executor().clone();
+
+        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
+        let old_entries = mem::take(&mut self.entries);
+        let scroll_to_top = false;
+
+        //         if let Some(room) = ActiveCall::global(cx).read(cx).room() {
+        //             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);
+
+        //                 if let Some(channel_id) = room.channel_id() {
+        //                     self.entries.push(ListEntry::ChannelNotes { channel_id });
+        //                     self.entries.push(ListEntry::ChannelChat { channel_id })
+        //                 }
+
+        //                 // Populate the active user.
+        //                 if let Some(user) = user_store.current_user() {
+        //                     self.match_candidates.clear();
+        //                     self.match_candidates.push(StringMatchCandidate {
+        //                         id: 0,
+        //                         string: user.github_login.clone(),
+        //                         char_bag: user.github_login.chars().collect(),
+        //                     });
+        //                     let matches = executor.block(match_strings(
+        //                         &self.match_candidates,
+        //                         &query,
+        //                         true,
+        //                         usize::MAX,
+        //                         &Default::default(),
+        //                         executor.clone(),
+        //                     ));
+        //                     if !matches.is_empty() {
+        //                         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();
+        //                         while let Some(project) = projects.next() {
+        //                             self.entries.push(ListEntry::ParticipantProject {
+        //                                 project_id: project.id,
+        //                                 worktree_root_names: project.worktree_root_names.clone(),
+        //                                 host_user_id: user_id,
+        //                                 is_last: projects.peek().is_none() && !room.is_screen_sharing(),
+        //                             });
+        //                         }
+        //                         if room.is_screen_sharing() {
+        //                             self.entries.push(ListEntry::ParticipantScreen {
+        //                                 peer_id: None,
+        //                                 is_last: true,
+        //                             });
+        //                         }
+        //                     }
+        //                 }
+
+        //                 // Populate remote participants.
+        //                 self.match_candidates.clear();
+        //                 self.match_candidates
+        //                     .extend(room.remote_participants().iter().map(|(_, participant)| {
+        //                         StringMatchCandidate {
+        //                             id: participant.user.id as usize,
+        //                             string: participant.user.github_login.clone(),
+        //                             char_bag: participant.user.github_login.chars().collect(),
+        //                         }
+        //                     }));
+        //                 let matches = executor.block(match_strings(
+        //                     &self.match_candidates,
+        //                     &query,
+        //                     true,
+        //                     usize::MAX,
+        //                     &Default::default(),
+        //                     executor.clone(),
+        //                 ));
+        //                 for mat in matches {
+        //                     let user_id = mat.candidate_id as u64;
+        //                     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();
+        //                     while let Some(project) = projects.next() {
+        //                         self.entries.push(ListEntry::ParticipantProject {
+        //                             project_id: project.id,
+        //                             worktree_root_names: project.worktree_root_names.clone(),
+        //                             host_user_id: participant.user.id,
+        //                             is_last: projects.peek().is_none()
+        //                                 && participant.video_tracks.is_empty(),
+        //                         });
+        //                     }
+        //                     if !participant.video_tracks.is_empty() {
+        //                         self.entries.push(ListEntry::ParticipantScreen {
+        //                             peer_id: Some(participant.peer_id),
+        //                             is_last: true,
+        //                         });
+        //                     }
+        //                 }
+
+        //                 // Populate pending participants.
+        //                 self.match_candidates.clear();
+        //                 self.match_candidates
+        //                     .extend(room.pending_participants().iter().enumerate().map(
+        //                         |(id, participant)| StringMatchCandidate {
+        //                             id,
+        //                             string: participant.github_login.clone(),
+        //                             char_bag: participant.github_login.chars().collect(),
+        //                         },
+        //                     ));
+        //                 let matches = executor.block(match_strings(
+        //                     &self.match_candidates,
+        //                     &query,
+        //                     true,
+        //                     usize::MAX,
+        //                     &Default::default(),
+        //                     executor.clone(),
+        //                 ));
+        //                 self.entries
+        //                     .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
+        //                         user: room.pending_participants()[mat.candidate_id].clone(),
+        //                         peer_id: None,
+        //                         is_pending: true,
+        //                     }));
+        //             }
+        //         }
+
+        let mut request_entries = Vec::new();
+
+        if cx.has_flag::<ChannelsAlpha>() {
+            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.ordered_channels().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,
+                    true,
+                    usize::MAX,
+                    &Default::default(),
+                    executor.clone(),
+                ));
+                if let Some(state) = &self.channel_editing_state {
+                    if matches!(state, ChannelEditingState::Create { location: None, .. }) {
+                        self.entries.push(ListEntry::ChannelEditor { depth: 0 });
+                    }
+                }
+                let mut collapse_depth = None;
+                for mat in matches {
+                    let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
+                    let depth = channel.parent_path.len();
+
+                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+                        collapse_depth = Some(depth);
+                    } else if let Some(collapsed_depth) = collapse_depth {
+                        if depth > collapsed_depth {
+                            continue;
+                        }
+                        if self.is_channel_collapsed(channel.id) {
+                            collapse_depth = Some(depth);
+                        } else {
+                            collapse_depth = None;
+                        }
+                    }
+
+                    let has_children = channel_store
+                        .channel_at_index(mat.candidate_id + 1)
+                        .map_or(false, |next_channel| {
+                            next_channel.parent_path.ends_with(&[channel.id])
+                        });
+
+                    match &self.channel_editing_state {
+                        Some(ChannelEditingState::Create {
+                            location: parent_id,
+                            ..
+                        }) if *parent_id == Some(channel.id) => {
+                            self.entries.push(ListEntry::Channel {
+                                channel: channel.clone(),
+                                depth,
+                                has_children: false,
+                            });
+                            self.entries
+                                .push(ListEntry::ChannelEditor { depth: depth + 1 });
+                        }
+                        Some(ChannelEditingState::Rename {
+                            location: parent_id,
+                            ..
+                        }) if parent_id == &channel.id => {
+                            self.entries.push(ListEntry::ChannelEditor { depth });
+                        }
+                        _ => {
+                            self.entries.push(ListEntry::Channel {
+                                channel: channel.clone(),
+                                depth,
+                                has_children,
+                            });
+                        }
+                    }
+                }
+            }
 
-    //         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: 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 {
-    //                             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.,
-    //                         })
-    //                     });
+            //             let channel_invites = channel_store.channel_invitations();
+            //             if !channel_invites.is_empty() {
+            //                 self.match_candidates.clear();
+            //                 self.match_candidates
+            //                     .extend(channel_invites.iter().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,
+            //                     true,
+            //                     usize::MAX,
+            //                     &Default::default(),
+            //                     executor.clone(),
+            //                 ));
+            //                 request_entries.extend(matches.iter().map(|mat| {
+            //                     ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
+            //                 }));
 
-    //                 self.list_state
-    //                     .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
-    //             }
-    //         }
+            //                 if !request_entries.is_empty() {
+            //                     self.entries
+            //                         .push(ListEntry::Header(Section::ChannelInvites));
+            //                     if !self.collapsed_sections.contains(&Section::ChannelInvites) {
+            //                         self.entries.append(&mut request_entries);
+            //                     }
+            //                 }
+            //             }
+        }
+
+        self.entries.push(ListEntry::Header(Section::Contacts));
+
+        request_entries.clear();
+        let incoming = user_store.incoming_contact_requests();
+        if !incoming.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    incoming
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, user)| StringMatchCandidate {
+                            id: ix,
+                            string: user.github_login.clone(),
+                            char_bag: user.github_login.chars().collect(),
+                        }),
+                );
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+            request_entries.extend(
+                matches
+                    .iter()
+                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
+            );
+        }
+
+        let outgoing = user_store.outgoing_contact_requests();
+        if !outgoing.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    outgoing
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, user)| StringMatchCandidate {
+                            id: ix,
+                            string: user.github_login.clone(),
+                            char_bag: user.github_login.chars().collect(),
+                        }),
+                );
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+            request_entries.extend(
+                matches
+                    .iter()
+                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
+            );
+        }
+
+        if !request_entries.is_empty() {
+            self.entries
+                .push(ListEntry::Header(Section::ContactRequests));
+            if !self.collapsed_sections.contains(&Section::ContactRequests) {
+                self.entries.append(&mut request_entries);
+            }
+        }
+
+        let contacts = user_store.contacts();
+        if !contacts.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    contacts
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, contact)| StringMatchCandidate {
+                            id: ix,
+                            string: contact.user.github_login.clone(),
+                            char_bag: contact.user.github_login.chars().collect(),
+                        }),
+                );
+
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+
+            let (online_contacts, offline_contacts) = matches
+                .iter()
+                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
+
+            for (matches, section) in [
+                (online_contacts, Section::Online),
+                (offline_contacts, Section::Offline),
+            ] {
+                if !matches.is_empty() {
+                    self.entries.push(ListEntry::Header(section));
+                    if !self.collapsed_sections.contains(&section) {
+                        let active_call = &ActiveCall::global(cx).read(cx);
+                        for mat in matches {
+                            let contact = &contacts[mat.candidate_id];
+                            self.entries.push(ListEntry::Contact {
+                                contact: contact.clone(),
+                                calling: active_call.pending_invites().contains(&contact.user.id),
+                            });
+                        }
+                    }
+                }
+            }
+        }
+
+        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
+            self.entries.push(ListEntry::ContactPlaceholder);
+        }
+
+        if select_same_item {
+            if let Some(prev_selected_entry) = prev_selected_entry {
+                self.selection.take();
+                for (ix, entry) in self.entries.iter().enumerate() {
+                    if *entry == prev_selected_entry {
+                        self.selection = Some(ix);
+                        self.scroll_handle.scroll_to_item(ix);
+                        break;
+                    }
+                }
+            }
+        } else {
+            self.selection = self.selection.and_then(|prev_selection| {
+                if self.entries.is_empty() {
+                    None
+                } else {
+                    let ix = prev_selection.min(self.entries.len() - 1);
+                    self.scroll_handle.scroll_to_item(ix);
+                    Some(ix)
+                }
+            });
+        }
+
+        if scroll_to_top {
+            self.scroll_handle.scroll_to_item(0)
+        } else {
+            let (old_index, old_offset) = self.scroll_handle.logical_scroll_top();
+            // Attempt to maintain the same scroll position.
+            if let Some(old_top_entry) = old_entries.get(old_index) {
+                let (new_index, new_offset) = self
+                    .entries
+                    .iter()
+                    .position(|entry| entry == old_top_entry)
+                    .map(|item_ix| (item_ix, old_offset))
+                    .or_else(|| {
+                        let entry_after_old_top = old_entries.get(old_index + 1)?;
+                        let item_ix = self
+                            .entries
+                            .iter()
+                            .position(|entry| entry == entry_after_old_top)?;
+                        Some((item_ix, px(0.)))
+                    })
+                    .or_else(|| {
+                        let entry_before_old_top = old_entries.get(old_index.saturating_sub(1))?;
+                        let item_ix = self
+                            .entries
+                            .iter()
+                            .position(|entry| entry == entry_before_old_top)?;
+                        Some((item_ix, px(0.)))
+                    })
+                    .unwrap_or_else(|| (old_index, old_offset));
+
+                self.scroll_handle
+                    .set_logical_scroll_top(new_index, new_offset);
+            }
+        }
 
-    //         cx.notify();
-    //     }
+        cx.notify();
+    }
 
     //     fn render_call_participant(
     //         user: &User,

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

@@ -1,37 +1,34 @@
 use client::{ContactRequestStatus, User, UserStore};
 use gpui::{
-    elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
+    div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle,
+    FocusableView, Img, IntoElement, Model, ParentElement as _, Render, Styled, Task, View,
+    ViewContext, VisualContext, WeakView,
 };
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
-use util::TryFutureExt;
-use workspace::Modal;
+use theme::ActiveTheme as _;
+use ui::{h_stack, v_stack, Label};
+use util::{ResultExt as _, TryFutureExt};
 
 pub fn init(cx: &mut AppContext) {
-    Picker::<ContactFinderDelegate>::init(cx);
-    cx.add_action(ContactFinder::dismiss)
+    //Picker::<ContactFinderDelegate>::init(cx);
+    //cx.add_action(ContactFinder::dismiss)
 }
 
 pub struct ContactFinder {
-    picker: ViewHandle<Picker<ContactFinderDelegate>>,
+    picker: View<Picker<ContactFinderDelegate>>,
     has_focus: bool,
 }
 
 impl ContactFinder {
-    pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
-        let picker = cx.add_view(|cx| {
-            Picker::new(
-                ContactFinderDelegate {
-                    user_store,
-                    potential_contacts: Arc::from([]),
-                    selected_index: 0,
-                },
-                cx,
-            )
-            .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
-        });
-
-        cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
+    pub fn new(user_store: Model<UserStore>, cx: &mut ViewContext<Self>) -> Self {
+        let delegate = ContactFinderDelegate {
+            parent: cx.view().downgrade(),
+            user_store,
+            potential_contacts: Arc::from([]),
+            selected_index: 0,
+        };
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
 
         Self {
             picker,
@@ -41,105 +38,72 @@ impl ContactFinder {
 
     pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
         self.picker.update(cx, |picker, cx| {
-            picker.set_query(query, cx);
+            // todo!()
+            // picker.set_query(query, cx);
         });
     }
-
-    fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(PickerEvent::Dismiss);
-    }
-}
-
-impl Entity for ContactFinder {
-    type Event = PickerEvent;
 }
 
-impl View for ContactFinder {
-    fn ui_name() -> &'static str {
-        "ContactFinder"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let full_theme = &theme::current(cx);
-        let theme = &full_theme.collab_panel.tabbed_modal;
-
-        fn render_mode_button(
-            text: &'static str,
-            theme: &theme::TabbedModal,
-            _cx: &mut ViewContext<ContactFinder>,
-        ) -> AnyElement<ContactFinder> {
-            let contained_text = &theme.tab_button.active_state().default;
-            Label::new(text, contained_text.text.clone())
-                .contained()
-                .with_style(contained_text.container.clone())
-                .into_any()
+impl Render for ContactFinder {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        fn render_mode_button(text: &'static str) -> AnyElement {
+            Label::new(text).into_any_element()
         }
 
-        Flex::column()
-            .with_child(
-                Flex::column()
-                    .with_child(
-                        Label::new("Contacts", theme.title.text.clone())
-                            .contained()
-                            .with_style(theme.title.container.clone()),
-                    )
-                    .with_child(Flex::row().with_children([render_mode_button(
-                        "Invite new contacts",
-                        &theme,
-                        cx,
-                    )]))
-                    .expanded()
-                    .contained()
-                    .with_style(theme.header),
-            )
-            .with_child(
-                ChildView::new(&self.picker, cx)
-                    .contained()
-                    .with_style(theme.body),
+        v_stack()
+            .child(
+                v_stack()
+                    .child(Label::new("Contacts"))
+                    .child(h_stack().children([render_mode_button("Invite new contacts")]))
+                    .bg(cx.theme().colors().element_background),
             )
-            .constrained()
-            .with_max_height(theme.max_height)
-            .with_max_width(theme.max_width)
-            .contained()
-            .with_style(theme.modal)
-            .into_any()
+            .child(self.picker.clone())
+            .w_96()
     }
 
-    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-        self.has_focus = true;
-        if cx.is_self_focused() {
-            cx.focus(&self.picker)
-        }
-    }
+    // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+    //     self.has_focus = true;
+    //     if cx.is_self_focused() {
+    //         cx.focus(&self.picker)
+    //     }
+    // }
 
-    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = false;
-    }
+    // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+    //     self.has_focus = false;
+    // }
+
+    type Element = Div;
 }
 
-impl Modal for ContactFinder {
-    fn has_focus(&self) -> bool {
-        self.has_focus
-    }
+// impl Modal for ContactFinder {
+//     fn has_focus(&self) -> bool {
+//         self.has_focus
+//     }
 
-    fn dismiss_on_event(event: &Self::Event) -> bool {
-        match event {
-            PickerEvent::Dismiss => true,
-        }
-    }
-}
+//     fn dismiss_on_event(event: &Self::Event) -> bool {
+//         match event {
+//             PickerEvent::Dismiss => true,
+//         }
+//     }
+// }
 
 pub struct ContactFinderDelegate {
+    parent: WeakView<ContactFinder>,
     potential_contacts: Arc<[Arc<User>]>,
-    user_store: ModelHandle<UserStore>,
+    user_store: Model<UserStore>,
     selected_index: usize,
 }
 
-impl PickerDelegate for ContactFinderDelegate {
-    fn placeholder_text(&self) -> Arc<str> {
-        "Search collaborator by username...".into()
+impl EventEmitter<DismissEvent> for ContactFinder {}
+
+impl FocusableView for ContactFinder {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
+}
 
+impl PickerDelegate for ContactFinderDelegate {
+    type ListItem = Div;
     fn match_count(&self) -> usize {
         self.potential_contacts.len()
     }
@@ -152,6 +116,10 @@ impl PickerDelegate for ContactFinderDelegate {
         self.selected_index = ix;
     }
 
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search collaborator by username...".into()
+    }
+
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
         let search_users = self
             .user_store
@@ -161,7 +129,7 @@ impl PickerDelegate for ContactFinderDelegate {
             async {
                 let potential_contacts = search_users.await?;
                 picker.update(&mut cx, |picker, cx| {
-                    picker.delegate_mut().potential_contacts = potential_contacts.into();
+                    picker.delegate.potential_contacts = potential_contacts.into();
                     cx.notify();
                 })?;
                 anyhow::Ok(())
@@ -191,19 +159,18 @@ impl PickerDelegate for ContactFinderDelegate {
     }
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        cx.emit(PickerEvent::Dismiss);
+        //cx.emit(PickerEvent::Dismiss);
+        self.parent
+            .update(cx, |_, cx| cx.emit(DismissEvent))
+            .log_err();
     }
 
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: &mut MouseState,
         selected: bool,
-        cx: &gpui::AppContext,
-    ) -> AnyElement<Picker<Self>> {
-        let full_theme = &theme::current(cx);
-        let theme = &full_theme.collab_panel.contact_finder;
-        let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
         let user = &self.potential_contacts[ix];
         let request_status = self.user_store.read(cx).contact_request_status(user);
 
@@ -214,48 +181,46 @@ impl PickerDelegate for ContactFinderDelegate {
             ContactRequestStatus::RequestSent => Some("icons/x.svg"),
             ContactRequestStatus::RequestAccepted => None,
         };
-        let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
-            &theme.disabled_contact_button
-        } else {
-            &theme.contact_button
-        };
-        let style = tabbed_modal
-            .picker
-            .item
-            .in_state(selected)
-            .style_for(mouse_state);
-        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(), style.label.clone())
-                    .contained()
-                    .with_style(theme.contact_username)
-                    .aligned()
-                    .left(),
-            )
-            .with_children(icon_path.map(|icon_path| {
-                Svg::new(icon_path)
-                    .with_color(button_style.color)
-                    .constrained()
-                    .with_width(button_style.icon_width)
-                    .aligned()
-                    .contained()
-                    .with_style(button_style.container)
-                    .constrained()
-                    .with_width(button_style.button_width)
-                    .with_height(button_style.button_width)
-                    .aligned()
-                    .flex_float()
-            }))
-            .contained()
-            .with_style(style.container)
-            .constrained()
-            .with_height(tabbed_modal.row_height)
-            .into_any()
+        Some(
+            div()
+                .flex_1()
+                .justify_between()
+                .children(user.avatar.clone().map(|avatar| img(avatar)))
+                .child(Label::new(user.github_login.clone()))
+                .children(icon_path.map(|icon_path| svg().path(icon_path))),
+        )
+        // 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(), style.label.clone())
+        //             .contained()
+        //             .with_style(theme.contact_username)
+        //             .aligned()
+        //             .left(),
+        //     )
+        //     .with_children(icon_path.map(|icon_path| {
+        //         Svg::new(icon_path)
+        //             .with_color(button_style.color)
+        //             .constrained()
+        //             .with_width(button_style.icon_width)
+        //             .aligned()
+        //             .contained()
+        //             .with_style(button_style.container)
+        //             .constrained()
+        //             .with_width(button_style.button_width)
+        //             .with_height(button_style.button_width)
+        //             .aligned()
+        //             .flex_float()
+        //     }))
+        //     .contained()
+        //     .with_style(style.container)
+        //     .constrained()
+        //     .with_height(tabbed_modal.row_height)
+        //     .into_any()
     }
 }

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

@@ -31,14 +31,17 @@ use std::sync::Arc;
 use call::ActiveCall;
 use client::{Client, UserStore};
 use gpui::{
-    div, px, rems, AppContext, Div, InteractiveElement, IntoElement, Model, ParentElement, Render,
-    Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext,
-    WeakView, WindowBounds,
+    div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton,
+    ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription,
+    ViewContext, VisualContext, WeakView, WindowBounds,
 };
 use project::Project;
 use theme::ActiveTheme;
-use ui::{h_stack, Button, ButtonVariant, Color, KeyBinding, Label, Tooltip};
-use workspace::Workspace;
+use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle2, IconButton, KeyBinding, Tooltip};
+use util::ResultExt;
+use workspace::{notifications::NotifyResultExt, Workspace};
+
+use crate::face_pile::FacePile;
 
 // const MAX_PROJECT_NAME_LENGTH: usize = 40;
 // const MAX_BRANCH_NAME_LENGTH: usize = 40;
@@ -85,6 +88,41 @@ impl Render for CollabTitlebarItem {
     type Element = Stateful<Div>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let is_in_room = self
+            .workspace
+            .update(cx, |this, cx| this.call_state().is_in_room(cx))
+            .unwrap_or_default();
+        let is_shared = is_in_room && self.project.read(cx).is_shared();
+        let current_user = self.user_store.read(cx).current_user();
+        let client = self.client.clone();
+        let users = self
+            .workspace
+            .update(cx, |this, cx| this.call_state().remote_participants(cx))
+            .log_err()
+            .flatten();
+        let mic_icon = if self
+            .workspace
+            .update(cx, |this, cx| this.call_state().is_muted(cx))
+            .log_err()
+            .flatten()
+            .unwrap_or_default()
+        {
+            ui::Icon::MicMute
+        } else {
+            ui::Icon::Mic
+        };
+        let speakers_icon = if self
+            .workspace
+            .update(cx, |this, cx| this.call_state().is_deafened(cx))
+            .log_err()
+            .flatten()
+            .unwrap_or_default()
+        {
+            ui::Icon::AudioOff
+        } else {
+            ui::Icon::AudioOn
+        };
+        let workspace = self.workspace.clone();
         h_stack()
             .id("titlebar")
             .justify_between()
@@ -115,8 +153,8 @@ impl Render for CollabTitlebarItem {
                             .border_color(gpui::red())
                             .id("project_owner_indicator")
                             .child(
-                                Button::new("player")
-                                    .variant(ButtonVariant::Ghost)
+                                Button::new("player", "player")
+                                    .style(ButtonStyle2::Subtle)
                                     .color(Some(Color::Player(0))),
                             )
                             .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
@@ -127,7 +165,10 @@ impl Render for CollabTitlebarItem {
                             .border()
                             .border_color(gpui::red())
                             .id("titlebar_project_menu_button")
-                            .child(Button::new("project_name").variant(ButtonVariant::Ghost))
+                            .child(
+                                Button::new("project_name", "project_name")
+                                    .style(ButtonStyle2::Subtle),
+                            )
                             .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
                     )
                     // TODO - Add git menu
@@ -137,8 +178,8 @@ impl Render for CollabTitlebarItem {
                             .border_color(gpui::red())
                             .id("titlebar_git_menu_button")
                             .child(
-                                Button::new("branch_name")
-                                    .variant(ButtonVariant::Ghost)
+                                Button::new("branch_name", "branch_name")
+                                    .style(ButtonStyle2::Subtle)
                                     .color(Some(Color::Muted)),
                             )
                             .tooltip(move |cx| {
@@ -155,8 +196,116 @@ impl Render for CollabTitlebarItem {
                                 .into()
                             }),
                     ),
-            ) // self.titlebar_item
-            .child(h_stack().child(Label::new("Right side titlebar item")))
+            )
+            .when_some(
+                users.zip(current_user.clone()),
+                |this, (remote_participants, current_user)| {
+                    let mut pile = FacePile::default();
+                    pile.extend(
+                        current_user
+                            .avatar
+                            .clone()
+                            .map(|avatar| {
+                                div().child(Avatar::data(avatar.clone())).into_any_element()
+                            })
+                            .into_iter()
+                            .chain(remote_participants.into_iter().flat_map(|(user, peer_id)| {
+                                user.avatar.as_ref().map(|avatar| {
+                                    div()
+                                        .child(
+                                            Avatar::data(avatar.clone()).into_element().into_any(),
+                                        )
+                                        .on_mouse_down(MouseButton::Left, {
+                                            let workspace = workspace.clone();
+                                            move |_, cx| {
+                                                workspace
+                                                    .update(cx, |this, cx| {
+                                                        this.open_shared_screen(peer_id, cx);
+                                                    })
+                                                    .log_err();
+                                            }
+                                        })
+                                        .into_any_element()
+                                })
+                            })),
+                    );
+                    this.child(pile.render(cx))
+                },
+            )
+            .child(div().flex_1())
+            .when(is_in_room, |this| {
+                this.child(
+                    h_stack()
+                        .child(
+                            h_stack()
+                                .child(Button::new(
+                                    "toggle_sharing",
+                                    if is_shared { "Unshare" } else { "Share" },
+                                ))
+                                .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({
+                                    let workspace = workspace.clone();
+                                    move |_, cx| {
+                                        workspace
+                                            .update(cx, |this, cx| {
+                                                this.call_state().hang_up(cx).detach();
+                                            })
+                                            .log_err();
+                                    }
+                                })),
+                        )
+                        .child(
+                            h_stack()
+                                .child(IconButton::new("mute-microphone", mic_icon).on_click({
+                                    let workspace = workspace.clone();
+                                    move |_, cx| {
+                                        workspace
+                                            .update(cx, |this, cx| {
+                                                this.call_state().toggle_mute(cx);
+                                            })
+                                            .log_err();
+                                    }
+                                }))
+                                .child(IconButton::new("mute-sound", speakers_icon).on_click({
+                                    let workspace = workspace.clone();
+                                    move |_, cx| {
+                                        workspace
+                                            .update(cx, |this, cx| {
+                                                this.call_state().toggle_deafen(cx);
+                                            })
+                                            .log_err();
+                                    }
+                                }))
+                                .child(IconButton::new("screen-share", ui::Icon::Screen).on_click(
+                                    move |_, cx| {
+                                        workspace
+                                            .update(cx, |this, cx| {
+                                                this.call_state().toggle_screen_share(cx);
+                                            })
+                                            .log_err();
+                                    },
+                                ))
+                                .pl_2(),
+                        ),
+                )
+            })
+            .map(|this| {
+                if let Some(user) = current_user {
+                    this.when_some(user.avatar.clone(), |this, avatar| {
+                        this.child(ui::Avatar::data(avatar))
+                    })
+                } else {
+                    this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| {
+                        let client = client.clone();
+                        cx.spawn(move |mut cx| async move {
+                            client
+                                .authenticate_and_connect(true, &cx)
+                                .await
+                                .notify_async_err(&mut cx);
+                        })
+                        .detach();
+                    }))
+                }
+            })
     }
 }
 

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

@@ -7,11 +7,14 @@ pub mod notification_panel;
 pub mod notifications;
 mod panel_settings;
 
-use std::sync::Arc;
+use std::{rc::Rc, sync::Arc};
 
 pub use collab_panel::CollabPanel;
 pub use collab_titlebar_item::CollabTitlebarItem;
-use gpui::AppContext;
+use gpui::{
+    point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind,
+    WindowOptions,
+};
 pub use panel_settings::{
     ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
 };
@@ -23,7 +26,7 @@ use workspace::AppState;
 //     [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
 // );
 
-pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     CollaborationPanelSettings::register(cx);
     ChatPanelSettings::register(cx);
     NotificationPanelSettings::register(cx);
@@ -32,7 +35,7 @@ pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
     collab_titlebar_item::init(cx);
     collab_panel::init(cx);
     // chat_panel::init(cx);
-    // notifications::init(&app_state, cx);
+    notifications::init(&app_state, cx);
 
     // cx.add_global_action(toggle_screen_sharing);
     // cx.add_global_action(toggle_mute);
@@ -95,31 +98,36 @@ pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
 //     }
 // }
 
-// fn notification_window_options(
-//     screen: Rc<dyn Screen>,
-//     window_size: Vector2F,
-// ) -> WindowOptions<'static> {
-//     const NOTIFICATION_PADDING: f32 = 16.;
+fn notification_window_options(
+    screen: Rc<dyn PlatformDisplay>,
+    window_size: Size<Pixels>,
+) -> WindowOptions {
+    let notification_margin_width = GlobalPixels::from(16.);
+    let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.);
 
-//     let screen_bounds = screen.content_bounds();
-//     WindowOptions {
-//         bounds: WindowBounds::Fixed(RectF::new(
-//             screen_bounds.upper_right()
-//                 + vec2f(
-//                     -NOTIFICATION_PADDING - window_size.x(),
-//                     NOTIFICATION_PADDING,
-//                 ),
-//             window_size,
-//         )),
-//         titlebar: None,
-//         center: false,
-//         focus: false,
-//         show: true,
-//         kind: WindowKind::PopUp,
-//         is_movable: false,
-//         screen: Some(screen),
-//     }
-// }
+    let screen_bounds = screen.bounds();
+    let size: Size<GlobalPixels> = window_size.into();
+
+    // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument.
+    let bounds = gpui::Bounds::<GlobalPixels> {
+        origin: screen_bounds.upper_right()
+            - point(
+                size.width + notification_margin_width,
+                notification_margin_height,
+            ),
+        size: window_size.into(),
+    };
+    WindowOptions {
+        bounds: WindowBounds::Fixed(bounds),
+        titlebar: None,
+        center: false,
+        focus: false,
+        show: true,
+        kind: WindowKind::PopUp,
+        is_movable: false,
+        display_id: Some(screen.id()),
+    }
+}
 
 // fn render_avatar<T: 'static>(
 //     avatar: Option<Arc<ImageData>>,

crates/collab_ui2/src/face_pile.rs πŸ”—

@@ -1,54 +1,48 @@
-// use std::ops::Range;
+use gpui::{
+    div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext,
+};
 
-// use gpui::{
-//     geometry::{
-//         rect::RectF,
-//         vector::{vec2f, Vector2F},
-//     },
-//     json::ToJson,
-//     serde_json::{self, json},
-//     AnyElement, Axis, Element, View, ViewContext,
-// };
+#[derive(Default)]
+pub struct FacePile {
+    pub faces: Vec<AnyElement>,
+}
 
-// pub(crate) struct FacePile<V: View> {
-//     overlap: f32,
-//     faces: Vec<AnyElement<V>>,
-// }
+impl RenderOnce for FacePile {
+    type Rendered = Div;
 
-// impl<V: View> FacePile<V> {
-//     pub fn new(overlap: f32) -> Self {
-//         Self {
-//             overlap,
-//             faces: Vec::new(),
-//         }
-//     }
-// }
+    fn render(self, _: &mut WindowContext) -> Self::Rendered {
+        let player_count = self.faces.len();
+        let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
+            let isnt_last = ix < player_count - 1;
 
-// impl<V: View> Element<V> for FacePile<V> {
-//     type LayoutState = ();
-//     type PaintState = ();
+            div().when(isnt_last, |div| div.neg_mr_1()).child(player)
+        });
+        div().p_1().flex().items_center().children(player_list)
+    }
+}
 
+// impl Element for FacePile {
+//     type State = ();
 //     fn layout(
 //         &mut self,
-//         constraint: gpui::SizeConstraint,
-//         view: &mut V,
-//         cx: &mut ViewContext<V>,
-//     ) -> (Vector2F, Self::LayoutState) {
-//         debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
-
+//         state: Option<Self::State>,
+//         cx: &mut WindowContext,
+//     ) -> (LayoutId, Self::State) {
 //         let mut width = 0.;
 //         let mut max_height = 0.;
+//         let mut faces = Vec::with_capacity(self.faces.len());
 //         for face in &mut self.faces {
-//             let layout = face.layout(constraint, view, cx);
+//             let layout = face.layout(cx);
 //             width += layout.x();
 //             max_height = f32::max(max_height, layout.y());
+//             faces.push(layout);
 //         }
 //         width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
-
-//         (
-//             Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
-//             (),
-//         )
+//         (cx.request_layout(&Style::default(), faces), ())
+//         // (
+//         //     Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
+//         //     (),
+//         // ))
 //     }
 
 //     fn paint(
@@ -77,37 +71,10 @@
 
 //         ()
 //     }
-
-//     fn rect_for_text_range(
-//         &self,
-//         _: Range<usize>,
-//         _: RectF,
-//         _: RectF,
-//         _: &Self::LayoutState,
-//         _: &Self::PaintState,
-//         _: &V,
-//         _: &ViewContext<V>,
-//     ) -> Option<RectF> {
-//         None
-//     }
-
-//     fn debug(
-//         &self,
-//         bounds: RectF,
-//         _: &Self::LayoutState,
-//         _: &Self::PaintState,
-//         _: &V,
-//         _: &ViewContext<V>,
-//     ) -> serde_json::Value {
-//         json!({
-//             "type": "FacePile",
-//             "bounds": bounds.to_json()
-//         })
-//     }
 // }
 
-// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
-//     fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
-//         self.faces.extend(children);
-//     }
-// }
+impl Extend<AnyElement> for FacePile {
+    fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
+        self.faces.extend(children);
+    }
+}

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

@@ -1,11 +1,11 @@
-// use gpui::AppContext;
-// use std::sync::Arc;
-// use workspace::AppState;
+use gpui::AppContext;
+use std::sync::Arc;
+use workspace::AppState;
 
-// pub mod incoming_call_notification;
+pub mod incoming_call_notification;
 // pub mod project_shared_notification;
 
-// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
-//     incoming_call_notification::init(app_state, cx);
-//     project_shared_notification::init(app_state, cx);
-// }
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    incoming_call_notification::init(app_state, cx);
+    //project_shared_notification::init(app_state, cx);
+}

crates/collab_ui2/src/notifications/incoming_call_notification.rs πŸ”—

@@ -1,14 +1,13 @@
 use crate::notification_window_options;
 use call::{ActiveCall, IncomingCall};
-use client::proto;
 use futures::StreamExt;
 use gpui::{
-    elements::*,
-    geometry::vector::vec2f,
-    platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
+    div, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, Styled, ViewContext,
+    VisualContext as _, WindowHandle,
 };
 use std::sync::{Arc, Weak};
+use ui::prelude::*;
+use ui::{h_stack, v_stack, Avatar, Button, Label};
 use util::ResultExt;
 use workspace::AppState;
 
@@ -19,23 +18,44 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
         let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
         while let Some(incoming_call) = incoming_call.next().await {
             for window in notification_windows.drain(..) {
-                window.remove(&mut cx);
+                window
+                    .update(&mut cx, |_, cx| {
+                        // todo!()
+                        cx.remove_window();
+                    })
+                    .log_err();
             }
 
             if let Some(incoming_call) = incoming_call {
-                let window_size = cx.read(|cx| {
-                    let theme = &theme::current(cx).incoming_call_notification;
-                    vec2f(theme.window_width, theme.window_height)
-                });
+                let unique_screens = cx.update(|cx| cx.displays()).unwrap();
+                let window_size = gpui::Size {
+                    width: px(380.),
+                    height: px(64.),
+                };
 
-                for screen in cx.platform().screens() {
+                for window in unique_screens {
+                    let options = notification_window_options(window, window_size);
                     let window = cx
-                        .add_window(notification_window_options(screen, window_size), |_| {
-                            IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
-                        });
-
+                        .open_window(options, |cx| {
+                            cx.build_view(|_| {
+                                IncomingCallNotification::new(
+                                    incoming_call.clone(),
+                                    app_state.clone(),
+                                )
+                            })
+                        })
+                        .unwrap();
                     notification_windows.push(window);
                 }
+
+                // for screen in cx.platform().screens() {
+                //     let window = cx
+                //         .add_window(notification_window_options(screen, window_size), |_| {
+                //             IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
+                //         });
+
+                //     notification_windows.push(window);
+                // }
             }
         }
     })
@@ -47,167 +67,206 @@ struct RespondToCall {
     accept: bool,
 }
 
-pub struct IncomingCallNotification {
+struct IncomingCallNotificationState {
     call: IncomingCall,
     app_state: Weak<AppState>,
 }
 
-impl IncomingCallNotification {
+pub struct IncomingCallNotification {
+    state: Arc<IncomingCallNotificationState>,
+}
+impl IncomingCallNotificationState {
     pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
         Self { call, app_state }
     }
 
-    fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
+    fn respond(&self, accept: bool, cx: &mut AppContext) {
         let active_call = ActiveCall::global(cx);
         if accept {
             let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
-            let caller_user_id = self.call.calling_user.id;
             let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
             let app_state = self.app_state.clone();
-            cx.app_context()
-                .spawn(|mut cx| async move {
-                    join.await?;
-                    if let Some(project_id) = initial_project_id {
-                        cx.update(|cx| {
-                            if let Some(app_state) = app_state.upgrade() {
-                                workspace::join_remote_project(
-                                    project_id,
-                                    caller_user_id,
-                                    app_state,
-                                    cx,
-                                )
-                                .detach_and_log_err(cx);
-                            }
-                        });
-                    }
-                    anyhow::Ok(())
-                })
-                .detach_and_log_err(cx);
+            let cx: &mut AppContext = cx;
+            cx.spawn(|cx| async move {
+                join.await?;
+                if let Some(_project_id) = initial_project_id {
+                    cx.update(|_cx| {
+                        if let Some(_app_state) = app_state.upgrade() {
+                            // workspace::join_remote_project(
+                            //     project_id,
+                            //     caller_user_id,
+                            //     app_state,
+                            //     cx,
+                            // )
+                            // .detach_and_log_err(cx);
+                        }
+                    })
+                    .log_err();
+                }
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
         } else {
             active_call.update(cx, |active_call, cx| {
                 active_call.decline_incoming(cx).log_err();
             });
         }
     }
+}
 
-    fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = &theme::current(cx).incoming_call_notification;
-        let default_project = proto::ParticipantProject::default();
-        let initial_project = self
-            .call
-            .initial_project
-            .as_ref()
-            .unwrap_or(&default_project);
-        Flex::row()
-            .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
-                Image::from_data(avatar)
-                    .with_style(theme.caller_avatar)
-                    .aligned()
-            }))
-            .with_child(
-                Flex::column()
-                    .with_child(
-                        Label::new(
-                            self.call.calling_user.github_login.clone(),
-                            theme.caller_username.text.clone(),
-                        )
-                        .contained()
-                        .with_style(theme.caller_username.container),
-                    )
-                    .with_child(
-                        Label::new(
-                            format!(
-                                "is sharing a project in Zed{}",
-                                if initial_project.worktree_root_names.is_empty() {
-                                    ""
-                                } else {
-                                    ":"
-                                }
-                            ),
-                            theme.caller_message.text.clone(),
-                        )
-                        .contained()
-                        .with_style(theme.caller_message.container),
-                    )
-                    .with_children(if initial_project.worktree_root_names.is_empty() {
-                        None
-                    } else {
-                        Some(
-                            Label::new(
-                                initial_project.worktree_root_names.join(", "),
-                                theme.worktree_roots.text.clone(),
-                            )
-                            .contained()
-                            .with_style(theme.worktree_roots.container),
-                        )
-                    })
-                    .contained()
-                    .with_style(theme.caller_metadata)
-                    .aligned(),
+impl IncomingCallNotification {
+    pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
+        Self {
+            state: Arc::new(IncomingCallNotificationState::new(call, app_state)),
+        }
+    }
+    fn render_caller(&self, cx: &mut ViewContext<Self>) -> impl Element {
+        h_stack()
+            .children(
+                self.state
+                    .call
+                    .calling_user
+                    .avatar
+                    .as_ref()
+                    .map(|avatar| Avatar::data(avatar.clone())),
+            )
+            .child(
+                v_stack()
+                    .child(Label::new(format!(
+                        "{} is sharing a project in Zed",
+                        self.state.call.calling_user.github_login
+                    )))
+                    .child(self.render_buttons(cx)),
             )
-            .contained()
-            .with_style(theme.caller_container)
-            .flex(1., true)
-            .into_any()
+        // let theme = &theme::current(cx).incoming_call_notification;
+        // let default_project = proto::ParticipantProject::default();
+        // let initial_project = self
+        //     .call
+        //     .initial_project
+        //     .as_ref()
+        //     .unwrap_or(&default_project);
+        // Flex::row()
+        //     .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
+        //         Image::from_data(avatar)
+        //             .with_style(theme.caller_avatar)
+        //             .aligned()
+        //     }))
+        //     .with_child(
+        //         Flex::column()
+        //             .with_child(
+        //                 Label::new(
+        //                     self.call.calling_user.github_login.clone(),
+        //                     theme.caller_username.text.clone(),
+        //                 )
+        //                 .contained()
+        //                 .with_style(theme.caller_username.container),
+        //             )
+        //             .with_child(
+        //                 Label::new(
+        //                     format!(
+        //                         "is sharing a project in Zed{}",
+        //                         if initial_project.worktree_root_names.is_empty() {
+        //                             ""
+        //                         } else {
+        //                             ":"
+        //                         }
+        //                     ),
+        //                     theme.caller_message.text.clone(),
+        //                 )
+        //                 .contained()
+        //                 .with_style(theme.caller_message.container),
+        //             )
+        //             .with_children(if initial_project.worktree_root_names.is_empty() {
+        //                 None
+        //             } else {
+        //                 Some(
+        //                     Label::new(
+        //                         initial_project.worktree_root_names.join(", "),
+        //                         theme.worktree_roots.text.clone(),
+        //                     )
+        //                     .contained()
+        //                     .with_style(theme.worktree_roots.container),
+        //                 )
+        //             })
+        //             .contained()
+        //             .with_style(theme.caller_metadata)
+        //             .aligned(),
+        //     )
+        //     .contained()
+        //     .with_style(theme.caller_container)
+        //     .flex(1., true)
+        //     .into_any()
     }
 
-    fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        enum Accept {}
-        enum Decline {}
-
-        let theme = theme::current(cx);
-        Flex::column()
-            .with_child(
-                MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
-                    let theme = &theme.incoming_call_notification;
-                    Label::new("Accept", theme.accept_button.text.clone())
-                        .aligned()
-                        .contained()
-                        .with_style(theme.accept_button.container)
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, this, cx| {
-                    this.respond(true, cx);
-                })
-                .flex(1., true),
+    fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
+        h_stack()
+            .child(
+                Button::new("accept", "Accept")
+                    .render(cx)
+                    // .bg(green())
+                    .on_click({
+                        let state = self.state.clone();
+                        move |_, cx| state.respond(true, cx)
+                    }),
             )
-            .with_child(
-                MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
-                    let theme = &theme.incoming_call_notification;
-                    Label::new("Decline", theme.decline_button.text.clone())
-                        .aligned()
-                        .contained()
-                        .with_style(theme.decline_button.container)
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, this, cx| {
-                    this.respond(false, cx);
-                })
-                .flex(1., true),
+            .child(
+                Button::new("decline", "Decline")
+                    .render(cx)
+                    // .bg(red())
+                    .on_click({
+                        let state = self.state.clone();
+                        move |_, cx| state.respond(false, cx)
+                    }),
             )
-            .constrained()
-            .with_width(theme.incoming_call_notification.button_width)
-            .into_any()
-    }
-}
 
-impl Entity for IncomingCallNotification {
-    type Event = ();
-}
+        // enum Accept {}
+        // enum Decline {}
 
-impl View for IncomingCallNotification {
-    fn ui_name() -> &'static str {
-        "IncomingCallNotification"
+        // let theme = theme::current(cx);
+        // Flex::column()
+        //     .with_child(
+        //         MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
+        //             let theme = &theme.incoming_call_notification;
+        //             Label::new("Accept", theme.accept_button.text.clone())
+        //                 .aligned()
+        //                 .contained()
+        //                 .with_style(theme.accept_button.container)
+        //         })
+        //         .with_cursor_style(CursorStyle::PointingHand)
+        //         .on_click(MouseButton::Left, |_, this, cx| {
+        //             this.respond(true, cx);
+        //         })
+        //         .flex(1., true),
+        //     )
+        //     .with_child(
+        //         MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
+        //             let theme = &theme.incoming_call_notification;
+        //             Label::new("Decline", theme.decline_button.text.clone())
+        //                 .aligned()
+        //                 .contained()
+        //                 .with_style(theme.decline_button.container)
+        //         })
+        //         .with_cursor_style(CursorStyle::PointingHand)
+        //         .on_click(MouseButton::Left, |_, this, cx| {
+        //             this.respond(false, cx);
+        //         })
+        //         .flex(1., true),
+        //     )
+        //     .constrained()
+        //     .with_width(theme.incoming_call_notification.button_width)
+        //     .into_any()
     }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let background = theme::current(cx).incoming_call_notification.background;
-        Flex::row()
-            .with_child(self.render_caller(cx))
-            .with_child(self.render_buttons(cx))
-            .contained()
-            .with_background_color(background)
-            .expanded()
-            .into_any()
+}
+impl Render for IncomingCallNotification {
+    type Element = Div;
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div().bg(red()).flex_none().child(self.render_caller(cx))
+        // Flex::row()
+        //     .with_child()
+        //     .with_child(self.render_buttons(cx))
+        //     .contained()
+        //     .with_background_color(background)
+        //     .expanded()
+        //     .into_any()
     }
 }

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

@@ -1,16 +1,17 @@
+use std::{
+    cmp::{self, Reverse},
+    sync::Arc,
+};
+
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView,
-    Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
+    actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+    Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
-use std::{
-    cmp::{self, Reverse},
-    sync::Arc,
-};
-use theme::ActiveTheme;
-use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt};
+
+use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
 use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
@@ -68,7 +69,7 @@ impl CommandPalette {
     }
 }
 
-impl EventEmitter<Manager> for CommandPalette {}
+impl EventEmitter<DismissEvent> for CommandPalette {}
 
 impl FocusableView for CommandPalette {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
@@ -80,7 +81,7 @@ impl Render for CommandPalette {
     type Element = Div;
 
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        v_stack().w_96().child(self.picker.clone())
+        v_stack().min_w_96().child(self.picker.clone())
     }
 }
 
@@ -140,7 +141,7 @@ impl CommandPaletteDelegate {
 }
 
 impl PickerDelegate for CommandPaletteDelegate {
-    type ListItem = Div;
+    type ListItem = ListItem;
 
     fn placeholder_text(&self) -> Arc<str> {
         "Execute a command...".into()
@@ -268,7 +269,7 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
         self.command_palette
-            .update(cx, |_, cx| cx.emit(Manager::Dismiss))
+            .update(cx, |_, cx| cx.emit(DismissEvent))
             .log_err();
     }
 
@@ -293,32 +294,26 @@ impl PickerDelegate for CommandPaletteDelegate {
         ix: usize,
         selected: bool,
         cx: &mut ViewContext<Picker<Self>>,
-    ) -> Self::ListItem {
-        let colors = cx.theme().colors();
+    ) -> Option<Self::ListItem> {
         let Some(r#match) = self.matches.get(ix) else {
-            return div();
+            return None;
         };
         let Some(command) = self.commands.get(r#match.candidate_id) else {
-            return div();
+            return None;
         };
 
-        div()
-            .px_1()
-            .text_color(colors.text)
-            .text_ui()
-            .bg(colors.ghost_element_background)
-            .rounded_md()
-            .when(selected, |this| this.bg(colors.ghost_element_selected))
-            .hover(|this| this.bg(colors.ghost_element_hover))
-            .child(
+        Some(
+            ListItem::new(ix).inset(true).selected(selected).child(
                 h_stack()
+                    .w_full()
                     .justify_between()
                     .child(HighlightedLabel::new(
                         command.name.clone(),
                         r#match.positions.clone(),
                     ))
                     .children(KeyBinding::for_action(&*command.action, cx)),
-            )
+            ),
+        )
     }
 }
 

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

@@ -126,7 +126,7 @@ impl View for ProjectDiagnosticsEditor {
         json!({
             "project": json!({
                 "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
-                "summary": project.diagnostic_summary(cx),
+                "summary": project.diagnostic_summary(false, cx),
             }),
             "summary": self.summary,
             "paths_to_update": self.paths_to_update.iter().map(|(server_id, paths)|
@@ -195,7 +195,7 @@ impl ProjectDiagnosticsEditor {
         });
 
         let project = project_handle.read(cx);
-        let summary = project.diagnostic_summary(cx);
+        let summary = project.diagnostic_summary(false, cx);
         let mut this = Self {
             project: project_handle,
             summary,
@@ -241,7 +241,7 @@ impl ProjectDiagnosticsEditor {
         let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
             .project
             .read(cx)
-            .diagnostic_summaries(cx)
+            .diagnostic_summaries(false, cx)
             .fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
                 summaries.entry(server_id).or_default().insert(path);
                 summaries
@@ -320,7 +320,7 @@ impl ProjectDiagnosticsEditor {
                 .context("rechecking diagnostics for paths")?;
 
                 this.update(&mut cx, |this, cx| {
-                    this.summary = this.project.read(cx).diagnostic_summary(cx);
+                    this.summary = this.project.read(cx).diagnostic_summary(false, cx);
                     cx.emit(Event::TitleChanged);
                 })?;
                 anyhow::Ok(())

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

@@ -34,19 +34,19 @@ impl DiagnosticIndicator {
             }
             project::Event::DiskBasedDiagnosticsFinished { language_server_id }
             | project::Event::LanguageServerRemoved(language_server_id) => {
-                this.summary = project.read(cx).diagnostic_summary(cx);
+                this.summary = project.read(cx).diagnostic_summary(false, cx);
                 this.in_progress_checks.remove(language_server_id);
                 cx.notify();
             }
             project::Event::DiagnosticsUpdated { .. } => {
-                this.summary = project.read(cx).diagnostic_summary(cx);
+                this.summary = project.read(cx).diagnostic_summary(false, cx);
                 cx.notify();
             }
             _ => {}
         })
         .detach();
         Self {
-            summary: project.read(cx).diagnostic_summary(cx),
+            summary: project.read(cx).diagnostic_summary(false, cx),
             in_progress_checks: project
                 .read(cx)
                 .language_servers_running_disk_based_diagnostics()

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

@@ -165,7 +165,7 @@ impl ProjectDiagnosticsEditor {
             });
 
         let project = project_handle.read(cx);
-        let summary = project.diagnostic_summary(cx);
+        let summary = project.diagnostic_summary(false, cx);
         let mut this = Self {
             project: project_handle,
             summary,
@@ -252,7 +252,7 @@ impl ProjectDiagnosticsEditor {
         let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
             .project
             .read(cx)
-            .diagnostic_summaries(cx)
+            .diagnostic_summaries(false, cx)
             .fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
                 summaries.entry(server_id).or_default().insert(path);
                 summaries
@@ -332,7 +332,7 @@ impl ProjectDiagnosticsEditor {
                 .context("rechecking diagnostics for paths")?;
 
                 this.update(&mut cx, |this, cx| {
-                    this.summary = this.project.read(cx).diagnostic_summary(cx);
+                    this.summary = this.project.read(cx).diagnostic_summary(false, cx);
                     cx.emit(ItemEvent::UpdateTab);
                     cx.emit(ItemEvent::UpdateBreadcrumbs);
                 })?;

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

@@ -77,13 +77,13 @@ impl DiagnosticIndicator {
 
             project::Event::DiskBasedDiagnosticsFinished { language_server_id }
             | project::Event::LanguageServerRemoved(language_server_id) => {
-                this.summary = project.read(cx).diagnostic_summary(cx);
+                this.summary = project.read(cx).diagnostic_summary(false, cx);
                 this.in_progress_checks.remove(language_server_id);
                 cx.notify();
             }
 
             project::Event::DiagnosticsUpdated { .. } => {
-                this.summary = project.read(cx).diagnostic_summary(cx);
+                this.summary = project.read(cx).diagnostic_summary(false, cx);
                 cx.notify();
             }
 
@@ -92,7 +92,7 @@ impl DiagnosticIndicator {
         .detach();
 
         Self {
-            summary: project.read(cx).diagnostic_summary(cx),
+            summary: project.read(cx).diagnostic_summary(false, cx),
             in_progress_checks: project
                 .read(cx)
                 .language_servers_running_disk_based_diagnostics()

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

@@ -1,5 +1,6 @@
 use crate::ProjectDiagnosticsEditor;
 use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
+use ui::prelude::*;
 use ui::{Icon, IconButton, Tooltip};
 use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 

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

@@ -40,11 +40,12 @@ use fuzzy::{StringMatch, StringMatchCandidate};
 use git::diff_hunk_to_display;
 use gpui::{
     actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
-    AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, ElementId,
-    EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
-    Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, ParentElement, Pixels,
-    Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Task, TextRun, TextStyle,
-    UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
+    AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
+    DispatchPhase, Div, ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures,
+    FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model,
+    MouseButton, ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText,
+    Subscription, Task, TextRun, TextStyle, UniformListScrollHandle, View, ViewContext,
+    VisualContext, WeakView, WhiteSpace, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -72,7 +73,7 @@ use ordered_float::OrderedFloat;
 use parking_lot::{Mutex, RwLock};
 use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
 use rand::prelude::*;
-use rpc::proto::*;
+use rpc::proto::{self, *};
 use scroll::{
     autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
 };
@@ -98,12 +99,13 @@ use text::{OffsetUtf16, Rope};
 use theme::{
     ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
 };
-use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, StyledExt, Tooltip};
+use ui::prelude::*;
+use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, Tooltip};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{
     item::{ItemEvent, ItemHandle},
     searchable::SearchEvent,
-    ItemNavHistory, SplitDirection, ViewId, Workspace,
+    ItemNavHistory, Pane, SplitDirection, ViewId, Workspace,
 };
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@@ -152,7 +154,6 @@ pub fn render_parsed_markdown(
             }),
     );
 
-    // todo!("add the ability to change cursor style for link ranges")
     let mut links = Vec::new();
     let mut link_ranges = Vec::new();
     for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) {
@@ -529,8 +530,6 @@ pub fn init(cx: &mut AppContext) {
     // cx.register_action_type(Editor::context_menu_next);
     // cx.register_action_type(Editor::context_menu_last);
 
-    hover_popover::init(cx);
-
     workspace::register_project_item::<Editor>(cx);
     workspace::register_followable_item::<Editor>(cx);
     workspace::register_deserializable_item::<Editor>(cx);
@@ -663,6 +662,7 @@ pub struct Editor {
     pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
     gutter_width: Pixels,
     style: Option<EditorStyle>,
+    editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
 }
 
 pub struct EditorSnapshot {
@@ -970,95 +970,94 @@ impl CompletionsMenu {
 
     fn pre_resolve_completion_documentation(
         &self,
-        _editor: &Editor,
-        _cx: &mut ViewContext<Editor>,
+        editor: &Editor,
+        cx: &mut ViewContext<Editor>,
     ) -> Option<Task<()>> {
-        // todo!("implementation below ");
-        None
-    }
-    // {
-    //     let settings = EditorSettings::get_global(cx);
-    //     if !settings.show_completion_documentation {
-    //         return None;
-    //     }
+        let settings = EditorSettings::get_global(cx);
+        if !settings.show_completion_documentation {
+            return None;
+        }
 
-    //     let Some(project) = editor.project.clone() else {
-    //         return None;
-    //     };
+        let Some(project) = editor.project.clone() else {
+            return None;
+        };
 
-    //     let client = project.read(cx).client();
-    //     let language_registry = project.read(cx).languages().clone();
+        let client = project.read(cx).client();
+        let language_registry = project.read(cx).languages().clone();
 
-    //     let is_remote = project.read(cx).is_remote();
-    //     let project_id = project.read(cx).remote_id();
+        let is_remote = project.read(cx).is_remote();
+        let project_id = project.read(cx).remote_id();
 
-    //     let completions = self.completions.clone();
-    //     let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
+        let completions = self.completions.clone();
+        let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
 
-    //     Some(cx.spawn(move |this, mut cx| async move {
-    //         if is_remote {
-    //             let Some(project_id) = project_id else {
-    //                 log::error!("Remote project without remote_id");
-    //                 return;
-    //             };
+        Some(cx.spawn(move |this, mut cx| async move {
+            if is_remote {
+                let Some(project_id) = project_id else {
+                    log::error!("Remote project without remote_id");
+                    return;
+                };
 
-    //             for completion_index in completion_indices {
-    //                 let completions_guard = completions.read();
-    //                 let completion = &completions_guard[completion_index];
-    //                 if completion.documentation.is_some() {
-    //                     continue;
-    //                 }
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
 
-    //                 let server_id = completion.server_id;
-    //                 let completion = completion.lsp_completion.clone();
-    //                 drop(completions_guard);
-
-    //                 Self::resolve_completion_documentation_remote(
-    //                     project_id,
-    //                     server_id,
-    //                     completions.clone(),
-    //                     completion_index,
-    //                     completion,
-    //                     client.clone(),
-    //                     language_registry.clone(),
-    //                 )
-    //                 .await;
-
-    //                 _ = this.update(&mut cx, |_, cx| cx.notify());
-    //             }
-    //         } else {
-    //             for completion_index in completion_indices {
-    //                 let completions_guard = completions.read();
-    //                 let completion = &completions_guard[completion_index];
-    //                 if completion.documentation.is_some() {
-    //                     continue;
-    //                 }
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
+
+                    Self::resolve_completion_documentation_remote(
+                        project_id,
+                        server_id,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        client.clone(),
+                        language_registry.clone(),
+                    )
+                    .await;
+
+                    _ = this.update(&mut cx, |_, cx| cx.notify());
+                }
+            } else {
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
 
-    //                 let server_id = completion.server_id;
-    //                 let completion = completion.lsp_completion.clone();
-    //                 drop(completions_guard);
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
 
-    //                 let server = project.read_with(&mut cx, |project, _| {
-    //                     project.language_server_for_id(server_id)
-    //                 });
-    //                 let Some(server) = server else {
-    //                     return;
-    //                 };
-
-    //                 Self::resolve_completion_documentation_local(
-    //                     server,
-    //                     completions.clone(),
-    //                     completion_index,
-    //                     completion,
-    //                     language_registry.clone(),
-    //                 )
-    //                 .await;
-
-    //                 _ = this.update(&mut cx, |_, cx| cx.notify());
-    //             }
-    //         }
-    //     }))
-    // }
+                    let server = project
+                        .read_with(&mut cx, |project, _| {
+                            project.language_server_for_id(server_id)
+                        })
+                        .ok()
+                        .flatten();
+                    let Some(server) = server else {
+                        return;
+                    };
+
+                    Self::resolve_completion_documentation_local(
+                        server,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        language_registry.clone(),
+                    )
+                    .await;
+
+                    _ = this.update(&mut cx, |_, cx| cx.notify());
+                }
+            }
+        }))
+    }
 
     fn attempt_resolve_selected_completion_documentation(
         &mut self,
@@ -1079,10 +1078,9 @@ impl CompletionsMenu {
         let completions = self.completions.clone();
         let completions_guard = completions.read();
         let completion = &completions_guard[completion_index];
-        // todo!()
-        // if completion.documentation.is_some() {
-        //     return;
-        // }
+        if completion.documentation.is_some() {
+            return;
+        }
 
         let server_id = completion.server_id;
         let completion = completion.lsp_completion.clone();
@@ -1141,41 +1139,40 @@ impl CompletionsMenu {
         client: Arc<Client>,
         language_registry: Arc<LanguageRegistry>,
     ) {
-        // todo!()
-        // let request = proto::ResolveCompletionDocumentation {
-        //     project_id,
-        //     language_server_id: server_id.0 as u64,
-        //     lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
-        // };
-
-        // let Some(response) = client
-        //     .request(request)
-        //     .await
-        //     .context("completion documentation resolve proto request")
-        //     .log_err()
-        // else {
-        //     return;
-        // };
-
-        // if response.text.is_empty() {
-        //     let mut completions = completions.write();
-        //     let completion = &mut completions[completion_index];
-        //     completion.documentation = Some(Documentation::Undocumented);
-        // }
-
-        // let documentation = if response.is_markdown {
-        //     Documentation::MultiLineMarkdown(
-        //         markdown::parse_markdown(&response.text, &language_registry, None).await,
-        //     )
-        // } else if response.text.lines().count() <= 1 {
-        //     Documentation::SingleLine(response.text)
-        // } else {
-        //     Documentation::MultiLinePlainText(response.text)
-        // };
-
-        // let mut completions = completions.write();
-        // let completion = &mut completions[completion_index];
-        // completion.documentation = Some(documentation);
+        let request = proto::ResolveCompletionDocumentation {
+            project_id,
+            language_server_id: server_id.0 as u64,
+            lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
+        };
+
+        let Some(response) = client
+            .request(request)
+            .await
+            .context("completion documentation resolve proto request")
+            .log_err()
+        else {
+            return;
+        };
+
+        if response.text.is_empty() {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
+        }
+
+        let documentation = if response.is_markdown {
+            Documentation::MultiLineMarkdown(
+                markdown::parse_markdown(&response.text, &language_registry, None).await,
+            )
+        } else if response.text.lines().count() <= 1 {
+            Documentation::SingleLine(response.text)
+        } else {
+            Documentation::MultiLinePlainText(response.text)
+        };
+
+        let mut completions = completions.write();
+        let completion = &mut completions[completion_index];
+        completion.documentation = Some(documentation);
     }
 
     async fn resolve_completion_documentation_local(
@@ -1185,38 +1182,37 @@ impl CompletionsMenu {
         completion: lsp::CompletionItem,
         language_registry: Arc<LanguageRegistry>,
     ) {
-        // todo!()
-        // let can_resolve = server
-        //     .capabilities()
-        //     .completion_provider
-        //     .as_ref()
-        //     .and_then(|options| options.resolve_provider)
-        //     .unwrap_or(false);
-        // if !can_resolve {
-        //     return;
-        // }
-
-        // let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
-        // let Some(completion_item) = request.await.log_err() else {
-        //     return;
-        // };
-
-        // if let Some(lsp_documentation) = completion_item.documentation {
-        //     let documentation = language::prepare_completion_documentation(
-        //         &lsp_documentation,
-        //         &language_registry,
-        //         None, // TODO: Try to reasonably work out which language the completion is for
-        //     )
-        //     .await;
-
-        //     let mut completions = completions.write();
-        //     let completion = &mut completions[completion_index];
-        //     completion.documentation = Some(documentation);
-        // } else {
-        //     let mut completions = completions.write();
-        //     let completion = &mut completions[completion_index];
-        //     completion.documentation = Some(Documentation::Undocumented);
-        // }
+        let can_resolve = server
+            .capabilities()
+            .completion_provider
+            .as_ref()
+            .and_then(|options| options.resolve_provider)
+            .unwrap_or(false);
+        if !can_resolve {
+            return;
+        }
+
+        let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
+        let Some(completion_item) = request.await.log_err() else {
+            return;
+        };
+
+        if let Some(lsp_documentation) = completion_item.documentation {
+            let documentation = language::prepare_completion_documentation(
+                &lsp_documentation,
+                &language_registry,
+                None, // TODO: Try to reasonably work out which language the completion is for
+            )
+            .await;
+
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(documentation);
+        } else {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
+        }
     }
 
     fn visible(&self) -> bool {
@@ -1272,6 +1268,13 @@ impl CompletionsMenu {
             multiline_docs.map(|div| {
                 div.id("multiline_docs")
                     .max_h(max_height)
+                    .flex_1()
+                    .px_1p5()
+                    .py_1()
+                    .min_w(px(260.))
+                    .max_w(px(640.))
+                    .w(px(500.))
+                    .text_ui()
                     .overflow_y_scroll()
                     // Prevent a mouse down on documentation from being propagated to the editor,
                     // because that would move the cursor.
@@ -1322,13 +1325,18 @@ impl CompletionsMenu {
 
                         div()
                             .id(mat.candidate_id)
-                            .min_w(px(300.))
-                            .max_w(px(700.))
+                            .min_w(px(220.))
+                            .max_w(px(540.))
                             .whitespace_nowrap()
                             .overflow_hidden()
-                            .bg(gpui::green())
-                            .hover(|style| style.bg(gpui::blue()))
-                            .when(item_ix == selected_item, |div| div.bg(gpui::red()))
+                            .text_ui()
+                            .px_1()
+                            .rounded(px(4.))
+                            .bg(cx.theme().colors().ghost_element_background)
+                            .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
+                            .when(item_ix == selected_item, |div| {
+                                div.bg(cx.theme().colors().ghost_element_selected)
+                            })
                             .on_mouse_down(
                                 MouseButton::Left,
                                 cx.listener(move |editor, event, cx| {
@@ -1887,6 +1895,7 @@ impl Editor {
             pixel_position_of_newest_cursor: None,
             gutter_width: Default::default(),
             style: None,
+            editor_actions: Default::default(),
             _subscriptions: vec![
                 cx.observe(&buffer, Self::on_buffer_changed),
                 cx.subscribe(&buffer, Self::on_buffer_event),
@@ -2018,10 +2027,14 @@ impl Editor {
         &self.buffer
     }
 
-    fn workspace(&self) -> Option<View<Workspace>> {
+    pub fn workspace(&self) -> Option<View<Workspace>> {
         self.workspace.as_ref()?.0.upgrade()
     }
 
+    pub fn pane(&self, cx: &AppContext) -> Option<View<Pane>> {
+        self.workspace()?.read(cx).pane_for(&self.handle.upgrade()?)
+    }
+
     pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> {
         self.buffer().read(cx).title(cx)
     }
@@ -4369,7 +4382,7 @@ impl Editor {
                                         editor.fold_at(&FoldAt { buffer_row }, cx);
                                     }
                                 }))
-                                .color(ui::Color::Muted)
+                                .icon_color(ui::Color::Muted)
                         })
                     })
                     .flatten()
@@ -9178,6 +9191,26 @@ impl Editor {
         cx.emit(EditorEvent::Blurred);
         cx.notify();
     }
+
+    pub fn register_action<A: Action>(
+        &mut self,
+        listener: impl Fn(&A, &mut WindowContext) + 'static,
+    ) -> &mut Self {
+        let listener = Arc::new(listener);
+
+        self.editor_actions.push(Box::new(move |cx| {
+            let view = cx.view().clone();
+            let cx = cx.window_context();
+            let listener = listener.clone();
+            cx.on_action(TypeId::of::<A>(), move |action, phase, cx| {
+                let action = action.downcast_ref().unwrap();
+                if phase == DispatchPhase::Bubble {
+                    listener(action, cx)
+                }
+            })
+        }));
+        self
+    }
 }
 
 pub trait CollaborationHub {

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

@@ -5427,178 +5427,177 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
     );
 }
 
-//todo!(completion)
-// #[gpui::test]
-// async fn test_completion(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+async fn test_completion(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
 
-//     let mut cx = EditorLspTestContext::new_rust(
-//         lsp::ServerCapabilities {
-//             completion_provider: Some(lsp::CompletionOptions {
-//                 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
-//                 resolve_provider: Some(true),
-//                 ..Default::default()
-//             }),
-//             ..Default::default()
-//         },
-//         cx,
-//     )
-//     .await;
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+                resolve_provider: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
 
-//     cx.set_state(indoc! {"
-//         oneˇ
-//         two
-//         three
-//     "});
-//     cx.simulate_keystroke(".");
-//     handle_completion_request(
-//         &mut cx,
-//         indoc! {"
-//             one.|<>
-//             two
-//             three
-//         "},
-//         vec!["first_completion", "second_completion"],
-//     )
-//     .await;
-//     cx.condition(|editor, _| editor.context_menu_visible())
-//         .await;
-//     let apply_additional_edits = cx.update_editor(|editor, cx| {
-//         editor.context_menu_next(&Default::default(), cx);
-//         editor
-//             .confirm_completion(&ConfirmCompletion::default(), cx)
-//             .unwrap()
-//     });
-//     cx.assert_editor_state(indoc! {"
-//         one.second_completionˇ
-//         two
-//         three
-//     "});
+    cx.set_state(indoc! {"
+        oneˇ
+        two
+        three
+    "});
+    cx.simulate_keystroke(".");
+    handle_completion_request(
+        &mut cx,
+        indoc! {"
+            one.|<>
+            two
+            three
+        "},
+        vec!["first_completion", "second_completion"],
+    )
+    .await;
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
+    let apply_additional_edits = cx.update_editor(|editor, cx| {
+        editor.context_menu_next(&Default::default(), cx);
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), cx)
+            .unwrap()
+    });
+    cx.assert_editor_state(indoc! {"
+        one.second_completionˇ
+        two
+        three
+    "});
 
-//     handle_resolve_completion_request(
-//         &mut cx,
-//         Some(vec![
-//             (
-//                 //This overlaps with the primary completion edit which is
-//                 //misbehavior from the LSP spec, test that we filter it out
-//                 indoc! {"
-//                     one.second_Λ‡completion
-//                     two
-//                     threeˇ
-//                 "},
-//                 "overlapping additional edit",
-//             ),
-//             (
-//                 indoc! {"
-//                     one.second_completion
-//                     two
-//                     threeˇ
-//                 "},
-//                 "\nadditional edit",
-//             ),
-//         ]),
-//     )
-//     .await;
-//     apply_additional_edits.await.unwrap();
-//     cx.assert_editor_state(indoc! {"
-//         one.second_completionˇ
-//         two
-//         three
-//         additional edit
-//     "});
+    handle_resolve_completion_request(
+        &mut cx,
+        Some(vec![
+            (
+                //This overlaps with the primary completion edit which is
+                //misbehavior from the LSP spec, test that we filter it out
+                indoc! {"
+                    one.second_Λ‡completion
+                    two
+                    threeˇ
+                "},
+                "overlapping additional edit",
+            ),
+            (
+                indoc! {"
+                    one.second_completion
+                    two
+                    threeˇ
+                "},
+                "\nadditional edit",
+            ),
+        ]),
+    )
+    .await;
+    apply_additional_edits.await.unwrap();
+    cx.assert_editor_state(indoc! {"
+        one.second_completionˇ
+        two
+        three
+        additional edit
+    "});
 
-//     cx.set_state(indoc! {"
-//         one.second_completion
-//         twoˇ
-//         threeˇ
-//         additional edit
-//     "});
-//     cx.simulate_keystroke(" ");
-//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
-//     cx.simulate_keystroke("s");
-//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+    cx.set_state(indoc! {"
+        one.second_completion
+        twoˇ
+        threeˇ
+        additional edit
+    "});
+    cx.simulate_keystroke(" ");
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+    cx.simulate_keystroke("s");
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
 
-//     cx.assert_editor_state(indoc! {"
-//         one.second_completion
-//         two sˇ
-//         three sˇ
-//         additional edit
-//     "});
-//     handle_completion_request(
-//         &mut cx,
-//         indoc! {"
-//             one.second_completion
-//             two s
-//             three <s|>
-//             additional edit
-//         "},
-//         vec!["fourth_completion", "fifth_completion", "sixth_completion"],
-//     )
-//     .await;
-//     cx.condition(|editor, _| editor.context_menu_visible())
-//         .await;
+    cx.assert_editor_state(indoc! {"
+        one.second_completion
+        two sˇ
+        three sˇ
+        additional edit
+    "});
+    handle_completion_request(
+        &mut cx,
+        indoc! {"
+            one.second_completion
+            two s
+            three <s|>
+            additional edit
+        "},
+        vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+    )
+    .await;
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
 
-//     cx.simulate_keystroke("i");
+    cx.simulate_keystroke("i");
 
-//     handle_completion_request(
-//         &mut cx,
-//         indoc! {"
-//             one.second_completion
-//             two si
-//             three <si|>
-//             additional edit
-//         "},
-//         vec!["fourth_completion", "fifth_completion", "sixth_completion"],
-//     )
-//     .await;
-//     cx.condition(|editor, _| editor.context_menu_visible())
-//         .await;
+    handle_completion_request(
+        &mut cx,
+        indoc! {"
+            one.second_completion
+            two si
+            three <si|>
+            additional edit
+        "},
+        vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+    )
+    .await;
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
 
-//     let apply_additional_edits = cx.update_editor(|editor, cx| {
-//         editor
-//             .confirm_completion(&ConfirmCompletion::default(), cx)
-//             .unwrap()
-//     });
-//     cx.assert_editor_state(indoc! {"
-//         one.second_completion
-//         two sixth_completionˇ
-//         three sixth_completionˇ
-//         additional edit
-//     "});
+    let apply_additional_edits = cx.update_editor(|editor, cx| {
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), cx)
+            .unwrap()
+    });
+    cx.assert_editor_state(indoc! {"
+        one.second_completion
+        two sixth_completionˇ
+        three sixth_completionˇ
+        additional edit
+    "});
 
-//     handle_resolve_completion_request(&mut cx, None).await;
-//     apply_additional_edits.await.unwrap();
+    handle_resolve_completion_request(&mut cx, None).await;
+    apply_additional_edits.await.unwrap();
 
-//     cx.update(|cx| {
-//         cx.update_global::<SettingsStore, _, _>(|settings, cx| {
-//             settings.update_user_settings::<EditorSettings>(cx, |settings| {
-//                 settings.show_completions_on_input = Some(false);
-//             });
-//         })
-//     });
-//     cx.set_state("editorˇ");
-//     cx.simulate_keystroke(".");
-//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
-//     cx.simulate_keystroke("c");
-//     cx.simulate_keystroke("l");
-//     cx.simulate_keystroke("o");
-//     cx.assert_editor_state("editor.cloˇ");
-//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
-//     cx.update_editor(|editor, cx| {
-//         editor.show_completions(&ShowCompletions, cx);
-//     });
-//     handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
-//     cx.condition(|editor, _| editor.context_menu_visible())
-//         .await;
-//     let apply_additional_edits = cx.update_editor(|editor, cx| {
-//         editor
-//             .confirm_completion(&ConfirmCompletion::default(), cx)
-//             .unwrap()
-//     });
-//     cx.assert_editor_state("editor.closeˇ");
-//     handle_resolve_completion_request(&mut cx, None).await;
-//     apply_additional_edits.await.unwrap();
-// }
+    cx.update(|cx| {
+        cx.update_global::<SettingsStore, _>(|settings, cx| {
+            settings.update_user_settings::<EditorSettings>(cx, |settings| {
+                settings.show_completions_on_input = Some(false);
+            });
+        })
+    });
+    cx.set_state("editorˇ");
+    cx.simulate_keystroke(".");
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+    cx.simulate_keystroke("c");
+    cx.simulate_keystroke("l");
+    cx.simulate_keystroke("o");
+    cx.assert_editor_state("editor.cloˇ");
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+    cx.update_editor(|editor, cx| {
+        editor.show_completions(&ShowCompletions, cx);
+    });
+    handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
+    let apply_additional_edits = cx.update_editor(|editor, cx| {
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), cx)
+            .unwrap()
+    });
+    cx.assert_editor_state("editor.closeˇ");
+    handle_resolve_completion_request(&mut cx, None).await;
+    apply_additional_edits.await.unwrap();
+}
 
 #[gpui::test]
 async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
@@ -7803,197 +7802,196 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
     );
 }
 
-//todo!(completions)
-// #[gpui::test]
-// async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
 
-//     let mut cx = EditorLspTestContext::new_rust(
-//         lsp::ServerCapabilities {
-//             completion_provider: Some(lsp::CompletionOptions {
-//                 trigger_characters: Some(vec![".".to_string()]),
-//                 resolve_provider: Some(true),
-//                 ..Default::default()
-//             }),
-//             ..Default::default()
-//         },
-//         cx,
-//     )
-//     .await;
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string()]),
+                resolve_provider: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
 
-//     cx.set_state(indoc! {"fn main() { let a = 2Λ‡; }"});
-//     cx.simulate_keystroke(".");
-//     let completion_item = lsp::CompletionItem {
-//         label: "some".into(),
-//         kind: Some(lsp::CompletionItemKind::SNIPPET),
-//         detail: Some("Wrap the expression in an `Option::Some`".to_string()),
-//         documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
-//             kind: lsp::MarkupKind::Markdown,
-//             value: "```rust\nSome(2)\n```".to_string(),
-//         })),
-//         deprecated: Some(false),
-//         sort_text: Some("fffffff2".to_string()),
-//         filter_text: Some("some".to_string()),
-//         insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
-//         text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-//             range: lsp::Range {
-//                 start: lsp::Position {
-//                     line: 0,
-//                     character: 22,
-//                 },
-//                 end: lsp::Position {
-//                     line: 0,
-//                     character: 22,
-//                 },
-//             },
-//             new_text: "Some(2)".to_string(),
-//         })),
-//         additional_text_edits: Some(vec![lsp::TextEdit {
-//             range: lsp::Range {
-//                 start: lsp::Position {
-//                     line: 0,
-//                     character: 20,
-//                 },
-//                 end: lsp::Position {
-//                     line: 0,
-//                     character: 22,
-//                 },
-//             },
-//             new_text: "".to_string(),
-//         }]),
-//         ..Default::default()
-//     };
-
-//     let closure_completion_item = completion_item.clone();
-//     let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
-//         let task_completion_item = closure_completion_item.clone();
-//         async move {
-//             Ok(Some(lsp::CompletionResponse::Array(vec![
-//                 task_completion_item,
-//             ])))
-//         }
-//     });
+    cx.set_state(indoc! {"fn main() { let a = 2Λ‡; }"});
+    cx.simulate_keystroke(".");
+    let completion_item = lsp::CompletionItem {
+        label: "some".into(),
+        kind: Some(lsp::CompletionItemKind::SNIPPET),
+        detail: Some("Wrap the expression in an `Option::Some`".to_string()),
+        documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
+            kind: lsp::MarkupKind::Markdown,
+            value: "```rust\nSome(2)\n```".to_string(),
+        })),
+        deprecated: Some(false),
+        sort_text: Some("fffffff2".to_string()),
+        filter_text: Some("some".to_string()),
+        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+            range: lsp::Range {
+                start: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+                end: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+            },
+            new_text: "Some(2)".to_string(),
+        })),
+        additional_text_edits: Some(vec![lsp::TextEdit {
+            range: lsp::Range {
+                start: lsp::Position {
+                    line: 0,
+                    character: 20,
+                },
+                end: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+            },
+            new_text: "".to_string(),
+        }]),
+        ..Default::default()
+    };
 
-//     request.next().await;
+    let closure_completion_item = completion_item.clone();
+    let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
+        let task_completion_item = closure_completion_item.clone();
+        async move {
+            Ok(Some(lsp::CompletionResponse::Array(vec![
+                task_completion_item,
+            ])))
+        }
+    });
 
-//     cx.condition(|editor, _| editor.context_menu_visible())
-//         .await;
-//     let apply_additional_edits = cx.update_editor(|editor, cx| {
-//         editor
-//             .confirm_completion(&ConfirmCompletion::default(), cx)
-//             .unwrap()
-//     });
-//     cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)Λ‡; }"});
-
-//     cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
-//         let task_completion_item = completion_item.clone();
-//         async move { Ok(task_completion_item) }
-//     })
-//     .next()
-//     .await
-//     .unwrap();
-//     apply_additional_edits.await.unwrap();
-//     cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)Λ‡; }"});
-// }
+    request.next().await;
 
-// #[gpui::test]
-// async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
+    let apply_additional_edits = cx.update_editor(|editor, cx| {
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), cx)
+            .unwrap()
+    });
+    cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)Λ‡; }"});
+
+    cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
+        let task_completion_item = completion_item.clone();
+        async move { Ok(task_completion_item) }
+    })
+    .next()
+    .await
+    .unwrap();
+    apply_additional_edits.await.unwrap();
+    cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)Λ‡; }"});
+}
 
-//     let mut cx = EditorLspTestContext::new(
-//         Language::new(
-//             LanguageConfig {
-//                 path_suffixes: vec!["jsx".into()],
-//                 overrides: [(
-//                     "element".into(),
-//                     LanguageConfigOverride {
-//                         word_characters: Override::Set(['-'].into_iter().collect()),
-//                         ..Default::default()
-//                     },
-//                 )]
-//                 .into_iter()
-//                 .collect(),
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_typescript::language_tsx()),
-//         )
-//         .with_override_query("(jsx_self_closing_element) @element")
-//         .unwrap(),
-//         lsp::ServerCapabilities {
-//             completion_provider: Some(lsp::CompletionOptions {
-//                 trigger_characters: Some(vec![":".to_string()]),
-//                 ..Default::default()
-//             }),
-//             ..Default::default()
-//         },
-//         cx,
-//     )
-//     .await;
+#[gpui::test]
+async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
 
-//     cx.lsp
-//         .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
-//             Ok(Some(lsp::CompletionResponse::Array(vec![
-//                 lsp::CompletionItem {
-//                     label: "bg-blue".into(),
-//                     ..Default::default()
-//                 },
-//                 lsp::CompletionItem {
-//                     label: "bg-red".into(),
-//                     ..Default::default()
-//                 },
-//                 lsp::CompletionItem {
-//                     label: "bg-yellow".into(),
-//                     ..Default::default()
-//                 },
-//             ])))
-//         });
+    let mut cx = EditorLspTestContext::new(
+        Language::new(
+            LanguageConfig {
+                path_suffixes: vec!["jsx".into()],
+                overrides: [(
+                    "element".into(),
+                    LanguageConfigOverride {
+                        word_characters: Override::Set(['-'].into_iter().collect()),
+                        ..Default::default()
+                    },
+                )]
+                .into_iter()
+                .collect(),
+                ..Default::default()
+            },
+            Some(tree_sitter_typescript::language_tsx()),
+        )
+        .with_override_query("(jsx_self_closing_element) @element")
+        .unwrap(),
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![":".to_string()]),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
 
-//     cx.set_state(r#"<p class="bgˇ" />"#);
+    cx.lsp
+        .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
+            Ok(Some(lsp::CompletionResponse::Array(vec![
+                lsp::CompletionItem {
+                    label: "bg-blue".into(),
+                    ..Default::default()
+                },
+                lsp::CompletionItem {
+                    label: "bg-red".into(),
+                    ..Default::default()
+                },
+                lsp::CompletionItem {
+                    label: "bg-yellow".into(),
+                    ..Default::default()
+                },
+            ])))
+        });
 
-//     // Trigger completion when typing a dash, because the dash is an extra
-//     // word character in the 'element' scope, which contains the cursor.
-//     cx.simulate_keystroke("-");
-//     cx.executor().run_until_parked();
-//     cx.update_editor(|editor, _| {
-//         if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
-//             assert_eq!(
-//                 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
-//                 &["bg-red", "bg-blue", "bg-yellow"]
-//             );
-//         } else {
-//             panic!("expected completion menu to be open");
-//         }
-//     });
+    cx.set_state(r#"<p class="bgˇ" />"#);
 
-//     cx.simulate_keystroke("l");
-//     cx.executor().run_until_parked();
-//     cx.update_editor(|editor, _| {
-//         if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
-//             assert_eq!(
-//                 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
-//                 &["bg-blue", "bg-yellow"]
-//             );
-//         } else {
-//             panic!("expected completion menu to be open");
-//         }
-//     });
+    // Trigger completion when typing a dash, because the dash is an extra
+    // word character in the 'element' scope, which contains the cursor.
+    cx.simulate_keystroke("-");
+    cx.executor().run_until_parked();
+    cx.update_editor(|editor, _| {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+            assert_eq!(
+                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                &["bg-red", "bg-blue", "bg-yellow"]
+            );
+        } else {
+            panic!("expected completion menu to be open");
+        }
+    });
 
-//     // When filtering completions, consider the character after the '-' to
-//     // be the start of a subword.
-//     cx.set_state(r#"<p class="yelˇ" />"#);
-//     cx.simulate_keystroke("l");
-//     cx.executor().run_until_parked();
-//     cx.update_editor(|editor, _| {
-//         if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
-//             assert_eq!(
-//                 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
-//                 &["bg-yellow"]
-//             );
-//         } else {
-//             panic!("expected completion menu to be open");
-//         }
-//     });
-// }
+    cx.simulate_keystroke("l");
+    cx.executor().run_until_parked();
+    cx.update_editor(|editor, _| {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+            assert_eq!(
+                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                &["bg-blue", "bg-yellow"]
+            );
+        } else {
+            panic!("expected completion menu to be open");
+        }
+    });
+
+    // When filtering completions, consider the character after the '-' to
+    // be the start of a subword.
+    cx.set_state(r#"<p class="yelˇ" />"#);
+    cx.simulate_keystroke("l");
+    cx.executor().run_until_parked();
+    cx.update_editor(|editor, _| {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+            assert_eq!(
+                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                &["bg-yellow"]
+            );
+        } else {
+            panic!("expected completion menu to be open");
+        }
+    });
+}
 
 #[gpui::test]
 async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {

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

@@ -5,7 +5,9 @@ use crate::{
     },
     editor_settings::ShowScrollbar,
     git::{diff_hunk_to_display, DisplayDiffHunk},
-    hover_popover::hover_at,
+    hover_popover::{
+        self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
+    },
     link_go_to_definition::{
         go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
         update_inlay_link_and_hover_points, GoToDefinitionTrigger,
@@ -20,10 +22,11 @@ use collections::{BTreeMap, HashMap};
 use gpui::{
     div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
     BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId,
-    ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, IntoElement, LineLayout,
-    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce,
-    ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
-    TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
+    ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement,
+    IntoElement, LineLayout, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
+    ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString, Size,
+    StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View,
+    ViewContext, WeakView, WindowContext, WrappedLine,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -45,6 +48,7 @@ use std::{
 };
 use sum_tree::Bias;
 use theme::{ActiveTheme, PlayerColor};
+use ui::prelude::*;
 use ui::{h_stack, IconButton, Tooltip};
 use util::ResultExt;
 use workspace::item::Item;
@@ -126,6 +130,11 @@ impl EditorElement {
 
     fn register_actions(&self, cx: &mut WindowContext) {
         let view = &self.editor;
+        self.editor.update(cx, |editor, cx| {
+            for action in editor.editor_actions.iter() {
+                (action)(cx)
+            }
+        });
         register_action(view, cx, Editor::move_left);
         register_action(view, cx, Editor::move_right);
         register_action(view, cx, Editor::move_down);
@@ -257,6 +266,7 @@ impl EditorElement {
         // on_action(cx, Editor::open_excerpts); todo!()
         register_action(view, cx, Editor::toggle_soft_wrap);
         register_action(view, cx, Editor::toggle_inlay_hints);
+        register_action(view, cx, hover_popover::hover);
         register_action(view, cx, Editor::reveal_in_finder);
         register_action(view, cx, Editor::copy_path);
         register_action(view, cx, Editor::copy_relative_path);
@@ -308,6 +318,7 @@ impl EditorElement {
         position_map: &PositionMap,
         text_bounds: Bounds<Pixels>,
         gutter_bounds: Bounds<Pixels>,
+        stacking_order: &StackingOrder,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         let mut click_count = event.click_count;
@@ -318,6 +329,9 @@ impl EditorElement {
         } else if !text_bounds.contains_point(&event.position) {
             return false;
         }
+        if !cx.was_top_layer(&event.position, stacking_order) {
+            return false;
+        }
 
         let point_for_position = position_map.point_for_position(text_bounds, event.position);
         let position = point_for_position.previous_valid;
@@ -376,6 +390,7 @@ impl EditorElement {
         event: &MouseUpEvent,
         position_map: &PositionMap,
         text_bounds: Bounds<Pixels>,
+        stacking_order: &StackingOrder,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         let end_selection = editor.has_pending_selection();
@@ -388,6 +403,7 @@ impl EditorElement {
         if !pending_nonempty_selections
             && event.modifiers.command
             && text_bounds.contains_point(&event.position)
+            && cx.was_top_layer(&event.position, stacking_order)
         {
             let point = position_map.point_for_position(text_bounds, event.position);
             let could_be_inlay = point.as_valid().is_none();
@@ -410,6 +426,7 @@ impl EditorElement {
         position_map: &PositionMap,
         text_bounds: Bounds<Pixels>,
         gutter_bounds: Bounds<Pixels>,
+        stacking_order: &StackingOrder,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         let modifiers = event.modifiers;
@@ -449,10 +466,12 @@ impl EditorElement {
 
         let text_hovered = text_bounds.contains_point(&event.position);
         let gutter_hovered = gutter_bounds.contains_point(&event.position);
+        let was_top = cx.was_top_layer(&event.position, stacking_order);
+
         editor.set_gutter_hovered(gutter_hovered, cx);
 
         // Don't trigger hover popover if mouse is hovering over context menu
-        if text_hovered {
+        if text_hovered && was_top {
             let point_for_position = position_map.point_for_position(text_bounds, event.position);
 
             match point_for_position.as_valid() {
@@ -482,7 +501,7 @@ impl EditorElement {
         } else {
             update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx);
             hover_at(editor, None, cx);
-            gutter_hovered
+            gutter_hovered && was_top
         }
     }
 
@@ -490,10 +509,10 @@ impl EditorElement {
         editor: &mut Editor,
         event: &ScrollWheelEvent,
         position_map: &PositionMap,
-        bounds: Bounds<Pixels>,
+        bounds: &InteractiveBounds,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
-        if !bounds.contains_point(&event.position) {
+        if !bounds.visibly_contains(&event.position, cx) {
             return false;
         }
 
@@ -1024,8 +1043,8 @@ impl EditorElement {
                     }
                 });
 
-                if let Some((position, mut context_menu)) = layout.context_menu.take() {
-                    cx.with_z_index(1, |cx| {
+                cx.with_z_index(1, |cx| {
+                    if let Some((position, mut context_menu)) = layout.context_menu.take() {
                         let available_space =
                             size(AvailableSpace::MinContent, AvailableSpace::MinContent);
                         let context_menu_size = context_menu.measure(available_space, cx);
@@ -1052,81 +1071,74 @@ impl EditorElement {
                             list_origin.y -= layout.position_map.line_height + list_height;
                         }
 
-                        context_menu.draw(list_origin, available_space, cx);
-                    })
-                }
+                        cx.break_content_mask(|cx| {
+                            context_menu.draw(list_origin, available_space, cx)
+                        });
+                    }
 
-                // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
-                //     cx.scene().push_stacking_context(None, None);
-
-                //     // This is safe because we check on layout whether the required row is available
-                //     let hovered_row_layout =
-                //         &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
-
-                //     // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
-                //     // height. This is the size we will use to decide whether to render popovers above or below
-                //     // the hovered line.
-                //     let first_size = hover_popovers[0].size();
-                //     let height_to_reserve = first_size.y
-                //         + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height;
-
-                //     // Compute Hovered Point
-                //     let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
-                //     let y = position.row() as f32 * layout.position_map.line_height - scroll_top;
-                //     let hovered_point = content_origin + point(x, y);
-
-                //     if hovered_point.y - height_to_reserve > 0.0 {
-                //         // There is enough space above. Render popovers above the hovered point
-                //         let mut current_y = hovered_point.y;
-                //         for hover_popover in hover_popovers {
-                //             let size = hover_popover.size();
-                //             let mut popover_origin = point(hovered_point.x, current_y - size.y);
-
-                //             let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x);
-                //             if x_out_of_bounds < 0.0 {
-                //                 popover_origin.set_x(popover_origin.x + x_out_of_bounds);
-                //             }
-
-                //             hover_popover.paint(
-                //                 popover_origin,
-                //                 Bounds::<Pixels>::from_points(
-                //                     gpui::Point::<Pixels>::zero(),
-                //                     point(f32::MAX, f32::MAX),
-                //                 ), // Let content bleed outside of editor
-                //                 editor,
-                //                 cx,
-                //             );
-
-                //             current_y = popover_origin.y - HOVER_POPOVER_GAP;
-                //         }
-                //     } else {
-                //         // There is not enough space above. Render popovers below the hovered point
-                //         let mut current_y = hovered_point.y + layout.position_map.line_height;
-                //         for hover_popover in hover_popovers {
-                //             let size = hover_popover.size();
-                //             let mut popover_origin = point(hovered_point.x, current_y);
-
-                //             let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x);
-                //             if x_out_of_bounds < 0.0 {
-                //                 popover_origin.set_x(popover_origin.x + x_out_of_bounds);
-                //             }
-
-                //             hover_popover.paint(
-                //                 popover_origin,
-                //                 Bounds::<Pixels>::from_points(
-                //                     gpui::Point::<Pixels>::zero(),
-                //                     point(f32::MAX, f32::MAX),
-                //                 ), // Let content bleed outside of editor
-                //                 editor,
-                //                 cx,
-                //             );
-
-                //             current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP;
-                //         }
-                //     }
-
-                //     cx.scene().pop_stacking_context();
-                // }
+                    if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() {
+                        let available_space =
+                            size(AvailableSpace::MinContent, AvailableSpace::MinContent);
+
+                        // This is safe because we check on layout whether the required row is available
+                        let hovered_row_layout = &layout.position_map.line_layouts
+                            [(position.row() - start_row) as usize]
+                            .line;
+
+                        // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
+                        // height. This is the size we will use to decide whether to render popovers above or below
+                        // the hovered line.
+                        let first_size = hover_popovers[0].measure(available_space, cx);
+                        let height_to_reserve = first_size.height
+                            + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height;
+
+                        // Compute Hovered Point
+                        let x = hovered_row_layout.x_for_index(position.column() as usize)
+                            - layout.position_map.scroll_position.x;
+                        let y = position.row() as f32 * layout.position_map.line_height
+                            - layout.position_map.scroll_position.y;
+                        let hovered_point = content_origin + point(x, y);
+
+                        if hovered_point.y - height_to_reserve > Pixels::ZERO {
+                            // There is enough space above. Render popovers above the hovered point
+                            let mut current_y = hovered_point.y;
+                            for mut hover_popover in hover_popovers {
+                                let size = hover_popover.measure(available_space, cx);
+                                let mut popover_origin =
+                                    point(hovered_point.x, current_y - size.height);
+
+                                let x_out_of_bounds =
+                                    text_bounds.upper_right().x - (popover_origin.x + size.width);
+                                if x_out_of_bounds < Pixels::ZERO {
+                                    popover_origin.x = popover_origin.x + x_out_of_bounds;
+                                }
+
+                                cx.break_content_mask(|cx| {
+                                    hover_popover.draw(popover_origin, available_space, cx)
+                                });
+
+                                current_y = popover_origin.y - HOVER_POPOVER_GAP;
+                            }
+                        } else {
+                            // There is not enough space above. Render popovers below the hovered point
+                            let mut current_y = hovered_point.y + layout.position_map.line_height;
+                            for mut hover_popover in hover_popovers {
+                                let size = hover_popover.measure(available_space, cx);
+                                let mut popover_origin = point(hovered_point.x, current_y);
+
+                                let x_out_of_bounds =
+                                    text_bounds.upper_right().x - (popover_origin.x + size.width);
+                                if x_out_of_bounds < Pixels::ZERO {
+                                    popover_origin.x = popover_origin.x + x_out_of_bounds;
+                                }
+
+                                hover_popover.draw(popover_origin, available_space, cx);
+
+                                current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
+                            }
+                        }
+                    }
+                })
             },
         )
     }
@@ -1992,15 +2004,23 @@ impl EditorElement {
             }
 
             let visible_rows = start_row..start_row + line_layouts.len() as u32;
-            // todo!("hover")
-            // let mut hover = editor.hover_state.render(
-            //     &snapshot,
-            //     &style,
-            //     visible_rows,
-            //     editor.workspace.as_ref().map(|(w, _)| w.clone()),
-            //     cx,
-            // );
-            // let mode = editor.mode;
+            let max_size = size(
+                (120. * em_width) // Default size
+                    .min(bounds.size.width / 2.) // Shrink to half of the editor width
+                    .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
+                (16. * line_height) // Default size
+                    .min(bounds.size.height / 2.) // Shrink to half of the editor height
+                    .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
+            );
+
+            let mut hover = editor.hover_state.render(
+                &snapshot,
+                &style,
+                visible_rows,
+                max_size,
+                editor.workspace.as_ref().map(|(w, _)| w.clone()),
+                cx,
+            );
 
             let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| {
                 editor.render_fold_indicators(
@@ -2013,27 +2033,6 @@ impl EditorElement {
                 )
             });
 
-            // todo!("hover popovers")
-            // if let Some((_, hover_popovers)) = hover.as_mut() {
-            //     for hover_popover in hover_popovers.iter_mut() {
-            //         hover_popover.layout(
-            //             SizeConstraint {
-            //                 min: gpui::Point::<Pixels>::zero(),
-            //                 max: point(
-            //                     (120. * em_width) // Default size
-            //                         .min(size.x / 2.) // Shrink to half of the editor width
-            //                         .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
-            //                     (16. * line_height) // Default size
-            //                         .min(size.y / 2.) // Shrink to half of the editor height
-            //                         .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
-            //                 ),
-            //             },
-            //             editor,
-            //             cx,
-            //         );
-            //     }
-            // }
-
             let invisible_symbol_font_size = font_size / 2.;
             let tab_invisible = cx
                 .text_system()
@@ -2102,7 +2101,7 @@ impl EditorElement {
                 fold_indicators,
                 tab_invisible,
                 space_invisible,
-                // hover_popovers: hover,
+                hover_popovers: hover,
             }
         })
     }
@@ -2294,10 +2293,15 @@ impl EditorElement {
         cx: &mut WindowContext,
     ) {
         let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
+        let interactive_bounds = InteractiveBounds {
+            bounds: bounds.intersect(&cx.content_mask().bounds),
+            stacking_order: cx.stacking_order().clone(),
+        };
 
         cx.on_mouse_event({
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
+            let interactive_bounds = interactive_bounds.clone();
 
             move |event: &ScrollWheelEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
@@ -2305,7 +2309,7 @@ impl EditorElement {
                 }
 
                 let should_cancel = editor.update(cx, |editor, cx| {
-                    Self::scroll(editor, event, &position_map, bounds, cx)
+                    Self::scroll(editor, event, &position_map, &interactive_bounds, cx)
                 });
                 if should_cancel {
                     cx.stop_propagation();
@@ -2316,6 +2320,7 @@ impl EditorElement {
         cx.on_mouse_event({
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
+            let stacking_order = cx.stacking_order().clone();
 
             move |event: &MouseDownEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
@@ -2323,7 +2328,15 @@ impl EditorElement {
                 }
 
                 let should_cancel = editor.update(cx, |editor, cx| {
-                    Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx)
+                    Self::mouse_down(
+                        editor,
+                        event,
+                        &position_map,
+                        text_bounds,
+                        gutter_bounds,
+                        &stacking_order,
+                        cx,
+                    )
                 });
 
                 if should_cancel {
@@ -2335,9 +2348,18 @@ impl EditorElement {
         cx.on_mouse_event({
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
+            let stacking_order = cx.stacking_order().clone();
+
             move |event: &MouseUpEvent, phase, cx| {
                 let should_cancel = editor.update(cx, |editor, cx| {
-                    Self::mouse_up(editor, event, &position_map, text_bounds, cx)
+                    Self::mouse_up(
+                        editor,
+                        event,
+                        &position_map,
+                        text_bounds,
+                        &stacking_order,
+                        cx,
+                    )
                 });
 
                 if should_cancel {
@@ -2363,13 +2385,23 @@ impl EditorElement {
         cx.on_mouse_event({
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
+            let stacking_order = cx.stacking_order().clone();
+
             move |event: &MouseMoveEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
                     return;
                 }
 
                 let stop_propogating = editor.update(cx, |editor, cx| {
-                    Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx)
+                    Self::mouse_moved(
+                        editor,
+                        event,
+                        &position_map,
+                        text_bounds,
+                        gutter_bounds,
+                        &stacking_order,
+                        cx,
+                    )
                 });
 
                 if stop_propogating {
@@ -2629,9 +2661,11 @@ impl Element for EditorElement {
             // We call with_z_index to establish a new stacking context.
             cx.with_z_index(0, |cx| {
                 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
-                    // Paint mouse listeners first, so any elements we paint on top of the editor
+                    // Paint mouse listeners at z-index 0 so any elements we paint on top of the editor
                     // take precedence.
-                    self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
+                    cx.with_z_index(0, |cx| {
+                        self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
+                    });
                     let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx);
                     cx.handle_input(&focus_handle, input_handler);
 
@@ -3287,7 +3321,7 @@ pub struct LayoutState {
     max_row: u32,
     context_menu: Option<(DisplayPoint, AnyElement)>,
     code_actions_indicator: Option<CodeActionsIndicator>,
-    // hover_popovers: Option<(DisplayPoint, Vec<AnyElement>)>,
+    hover_popovers: Option<(DisplayPoint, Vec<AnyElement>)>,
     fold_indicators: Vec<Option<IconButton>>,
     tab_invisible: ShapedLine,
     space_invisible: ShapedLine,
@@ -4085,7 +4119,7 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
 //     }
 // }
 
-fn register_action<T: Action>(
+pub fn register_action<T: Action>(
     view: &View<Editor>,
     cx: &mut WindowContext,
     listener: impl Fn(&mut Editor, &T, &mut ViewContext<Editor>) + 'static,

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

@@ -1,15 +1,21 @@
 use crate::{
-    display_map::InlayOffset,
+    display_map::{InlayOffset, ToDisplayPoint},
     link_go_to_definition::{InlayHighlight, RangeInEditor},
     Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
     ExcerptId, RangeToAnchorExt,
 };
 use futures::FutureExt;
-use gpui::{AnyElement, AppContext, Model, Task, ViewContext, WeakView};
+use gpui::{
+    actions, div, px, AnyElement, AppContext, CursorStyle, InteractiveElement, IntoElement, Model,
+    MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled,
+    Task, ViewContext, WeakView,
+};
 use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
+use lsp::DiagnosticSeverity;
 use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
 use settings::Settings;
 use std::{ops::Range, sync::Arc, time::Duration};
+use ui::{StyledExt, Tooltip};
 use util::TryFutureExt;
 use workspace::Workspace;
 
@@ -17,22 +23,17 @@ pub const HOVER_DELAY_MILLIS: u64 = 350;
 pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
 
 pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
-pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
-pub const HOVER_POPOVER_GAP: f32 = 10.;
+pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
+pub const HOVER_POPOVER_GAP: Pixels = px(10.);
 
-// actions!(editor, [Hover]);
+actions!(Hover);
 
-pub fn init(cx: &mut AppContext) {
-    // cx.add_action(hover);
+/// Bindable action which uses the most recent selection head to trigger a hover
+pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
+    let head = editor.selections.newest_display(cx).head();
+    show_hover(editor, head, true, cx);
 }
 
-// todo!()
-// /// Bindable action which uses the most recent selection head to trigger a hover
-// pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
-//     let head = editor.selections.newest_display(cx).head();
-//     show_hover(editor, head, true, cx);
-// }
-
 /// The internal hover action dispatches between `show_hover` or `hide_hover`
 /// depending on whether a point to hover over is provided.
 pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
@@ -74,64 +75,63 @@ pub fn find_hovered_hint_part(
 }
 
 pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
-    todo!()
-    // if EditorSettings::get_global(cx).hover_popover_enabled {
-    //     if editor.pending_rename.is_some() {
-    //         return;
-    //     }
-
-    //     let Some(project) = editor.project.clone() else {
-    //         return;
-    //     };
-
-    //     if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
-    //         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;
-    //             }
-    //         }
-    //         hide_hover(editor, cx);
-    //     }
-
-    //     let task = cx.spawn(|this, mut cx| {
-    //         async move {
-    //             cx.background_executor()
-    //                 .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
-    //                 .await;
-    //             this.update(&mut cx, |this, _| {
-    //                 this.hover_state.diagnostic_popover = None;
-    //             })?;
-
-    //             let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
-    //             let blocks = vec![inlay_hover.tooltip];
-    //             let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
-
-    //             let hover_popover = InfoPopover {
-    //                 project: project.clone(),
-    //                 symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
-    //                 blocks,
-    //                 parsed_content,
-    //             };
-
-    //             this.update(&mut cx, |this, cx| {
-    //                 // Highlight the selected symbol using a background highlight
-    //                 this.highlight_inlay_background::<HoverState>(
-    //                     vec![inlay_hover.range],
-    //                     |theme| theme.editor.hover_popover.highlight,
-    //                     cx,
-    //                 );
-    //                 this.hover_state.info_popover = Some(hover_popover);
-    //                 cx.notify();
-    //             })?;
-
-    //             anyhow::Ok(())
-    //         }
-    //         .log_err()
-    //     });
-
-    //     editor.hover_state.info_task = Some(task);
-    // }
+    if EditorSettings::get_global(cx).hover_popover_enabled {
+        if editor.pending_rename.is_some() {
+            return;
+        }
+
+        let Some(project) = editor.project.clone() else {
+            return;
+        };
+
+        if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
+            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;
+                }
+            }
+            hide_hover(editor, cx);
+        }
+
+        let task = cx.spawn(|this, mut cx| {
+            async move {
+                cx.background_executor()
+                    .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
+                    .await;
+                this.update(&mut cx, |this, _| {
+                    this.hover_state.diagnostic_popover = None;
+                })?;
+
+                let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
+                let blocks = vec![inlay_hover.tooltip];
+                let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
+
+                let hover_popover = InfoPopover {
+                    project: project.clone(),
+                    symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
+                    blocks,
+                    parsed_content,
+                };
+
+                this.update(&mut cx, |this, cx| {
+                    // Highlight the selected symbol using a background highlight
+                    this.highlight_inlay_background::<HoverState>(
+                        vec![inlay_hover.range],
+                        |theme| theme.element_hover, // todo!("use a proper background here")
+                        cx,
+                    );
+                    this.hover_state.info_popover = Some(hover_popover);
+                    cx.notify();
+                })?;
+
+                anyhow::Ok(())
+            }
+            .log_err()
+        });
+
+        editor.hover_state.info_task = Some(task);
+    }
 }
 
 /// Hides the type information popup.
@@ -420,43 +420,42 @@ impl HoverState {
         snapshot: &EditorSnapshot,
         style: &EditorStyle,
         visible_rows: Range<u32>,
+        max_size: Size<Pixels>,
         workspace: Option<WeakView<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
-        todo!("old version below")
+        // If there is a diagnostic, position the popovers based on that.
+        // Otherwise use the start of the hover range
+        let anchor = self
+            .diagnostic_popover
+            .as_ref()
+            .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
+            .or_else(|| {
+                self.info_popover
+                    .as_ref()
+                    .map(|info_popover| match &info_popover.symbol_range {
+                        RangeInEditor::Text(range) => &range.start,
+                        RangeInEditor::Inlay(range) => &range.inlay_position,
+                    })
+            })?;
+        let point = anchor.to_display_point(&snapshot.display_snapshot);
+
+        // Don't render if the relevant point isn't on screen
+        if !self.visible() || !visible_rows.contains(&point.row()) {
+            return None;
+        }
+
+        let mut elements = Vec::new();
+
+        if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
+            elements.push(diagnostic_popover.render(style, max_size, cx));
+        }
+        if let Some(info_popover) = self.info_popover.as_mut() {
+            elements.push(info_popover.render(style, max_size, workspace, cx));
+        }
+
+        Some((point, elements))
     }
-    //     // If there is a diagnostic, position the popovers based on that.
-    //     // Otherwise use the start of the hover range
-    //     let anchor = self
-    //         .diagnostic_popover
-    //         .as_ref()
-    //         .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
-    //         .or_else(|| {
-    //             self.info_popover
-    //                 .as_ref()
-    //                 .map(|info_popover| match &info_popover.symbol_range {
-    //                     RangeInEditor::Text(range) => &range.start,
-    //                     RangeInEditor::Inlay(range) => &range.inlay_position,
-    //                 })
-    //         })?;
-    //     let point = anchor.to_display_point(&snapshot.display_snapshot);
-
-    //     // Don't render if the relevant point isn't on screen
-    //     if !self.visible() || !visible_rows.contains(&point.row()) {
-    //         return None;
-    //     }
-
-    //     let mut elements = Vec::new();
-
-    //     if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
-    //         elements.push(diagnostic_popover.render(style, cx));
-    //     }
-    //     if let Some(info_popover) = self.info_popover.as_mut() {
-    //         elements.push(info_popover.render(style, workspace, cx));
-    //     }
-
-    //     Some((point, elements))
-    // }
 }
 
 #[derive(Debug, Clone)]
@@ -467,35 +466,35 @@ pub struct InfoPopover {
     parsed_content: ParsedMarkdown,
 }
 
-// impl InfoPopover {
-//     pub fn render(
-//         &mut self,
-//         style: &EditorStyle,
-//         workspace: Option<WeakView<Workspace>>,
-//         cx: &mut ViewContext<Editor>,
-//     ) -> AnyElement<Editor> {
-//         MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
-//             Flex::column()
-//                 .scrollable::<HoverBlock>(0, None, cx)
-//                 .with_child(crate::render_parsed_markdown::<HoverBlock>(
-//                     &self.parsed_content,
-//                     style,
-//                     workspace,
-//                     cx,
-//                 ))
-//                 .contained()
-//                 .with_style(style.hover_popover.container)
-//         })
-//         .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
-//         .with_cursor_style(CursorStyle::Arrow)
-//         .with_padding(Padding {
-//             bottom: HOVER_POPOVER_GAP,
-//             top: HOVER_POPOVER_GAP,
-//             ..Default::default()
-//         })
-//         .into_any()
-//     }
-// }
+impl InfoPopover {
+    pub fn render(
+        &mut self,
+        style: &EditorStyle,
+        max_size: Size<Pixels>,
+        workspace: Option<WeakView<Workspace>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> AnyElement {
+        div()
+            .id("info_popover")
+            .elevation_2(cx)
+            .text_ui()
+            .p_2()
+            .overflow_y_scroll()
+            .max_w(max_size.width)
+            .max_h(max_size.height)
+            // Prevent a mouse move on the popover from being propagated to the editor,
+            // because that would dismiss the popover.
+            .on_mouse_move(|_, cx| cx.stop_propagation())
+            .child(crate::render_parsed_markdown(
+                "content",
+                &self.parsed_content,
+                style,
+                workspace,
+                cx,
+            ))
+            .into_any_element()
+    }
+}
 
 #[derive(Debug, Clone)]
 pub struct DiagnosticPopover {
@@ -504,57 +503,40 @@ pub struct DiagnosticPopover {
 }
 
 impl DiagnosticPopover {
-    pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement {
-        todo!()
-        // enum PrimaryDiagnostic {}
-
-        // let mut text_style = style.hover_popover.prose.clone();
-        // text_style.font_size = style.text.font_size;
-        // let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone();
-
-        // let text = match &self.local_diagnostic.diagnostic.source {
-        //     Some(source) => Text::new(
-        //         format!("{source}: {}", self.local_diagnostic.diagnostic.message),
-        //         text_style,
-        //     )
-        //     .with_highlights(vec![(0..source.len(), diagnostic_source_style)]),
-
-        //     None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style),
-        // };
-
-        // let container_style = match self.local_diagnostic.diagnostic.severity {
-        //     DiagnosticSeverity::HINT => style.hover_popover.info_container,
-        //     DiagnosticSeverity::INFORMATION => style.hover_popover.info_container,
-        //     DiagnosticSeverity::WARNING => style.hover_popover.warning_container,
-        //     DiagnosticSeverity::ERROR => style.hover_popover.error_container,
-        //     _ => style.hover_popover.container,
-        // };
-
-        // let tooltip_style = theme::current(cx).tooltip.clone();
-
-        // MouseEventHandler::new::<DiagnosticPopover, _>(0, cx, |_, _| {
-        //     text.with_soft_wrap(true)
-        //         .contained()
-        //         .with_style(container_style)
-        // })
-        // .with_padding(Padding {
-        //     top: HOVER_POPOVER_GAP,
-        //     bottom: HOVER_POPOVER_GAP,
-        //     ..Default::default()
-        // })
-        // .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
-        // .on_click(MouseButton::Left, |_, this, cx| {
-        //     this.go_to_diagnostic(&Default::default(), cx)
-        // })
-        // .with_cursor_style(CursorStyle::PointingHand)
-        // .with_tooltip::<PrimaryDiagnostic>(
-        //     0,
-        //     "Go To Diagnostic".to_string(),
-        //     Some(Box::new(crate::GoToDiagnostic)),
-        //     tooltip_style,
-        //     cx,
-        // )
-        // .into_any()
+    pub fn render(
+        &self,
+        style: &EditorStyle,
+        max_size: Size<Pixels>,
+        cx: &mut ViewContext<Editor>,
+    ) -> AnyElement {
+        let text = match &self.local_diagnostic.diagnostic.source {
+            Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message),
+            None => self.local_diagnostic.diagnostic.message.clone(),
+        };
+
+        let container_bg = crate::diagnostic_style(
+            self.local_diagnostic.diagnostic.severity,
+            true,
+            &style.diagnostic_style,
+        );
+
+        div()
+            .id("diagnostic")
+            .overflow_y_scroll()
+            .bg(container_bg)
+            .max_w(max_size.width)
+            .max_h(max_size.height)
+            .cursor(CursorStyle::PointingHand)
+            .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
+            // Prevent a mouse move on the popover from being propagated to the editor,
+            // because that would dismiss the popover.
+            .on_mouse_move(|_, cx| cx.stop_propagation())
+            // Prevent a mouse down on the popover from being propagated to the editor,
+            // because that would move the cursor.
+            .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
+            .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
+            .child(SharedString::from(text))
+            .into_any_element()
     }
 
     pub fn activation_info(&self) -> (usize, Anchor) {
@@ -567,763 +549,763 @@ impl DiagnosticPopover {
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use crate::{
-//         editor_tests::init_test,
-//         element::PointForPosition,
-//         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::{HighlightStyle, Underline, Weight};
-//     use indoc::indoc;
-//     use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
-//     use lsp::LanguageServerId;
-//     use project::{HoverBlock, HoverBlockKind};
-//     use smol::stream::StreamExt;
-//     use unindent::Unindent;
-//     use util::test::marked_text_ranges;
-
-//     #[gpui::test]
-//     async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         // Basic hover delays and then pops without moving the mouse
-//         cx.set_state(indoc! {"
-//             fn Λ‡test() { println!(); }
-//         "});
-//         let hover_point = cx.display_point(indoc! {"
-//             fn test() { printˇln!(); }
-//         "});
-
-//         cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
-//         assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
-
-//         // After delay, hover should be visible.
-//         let symbol_range = cx.lsp_range(indoc! {"
-//             fn test() { Β«println!Β»(); }
-//         "});
-//         let mut requests =
-//             cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
-//                 Ok(Some(lsp::Hover {
-//                     contents: lsp::HoverContents::Markup(lsp::MarkupContent {
-//                         kind: lsp::MarkupKind::Markdown,
-//                         value: "some basic docs".to_string(),
-//                     }),
-//                     range: Some(symbol_range),
-//                 }))
-//             });
-//         cx.foreground()
-//             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
-//         requests.next().await;
-
-//         cx.editor(|editor, _| {
-//             assert!(editor.hover_state.visible());
-//             assert_eq!(
-//                 editor.hover_state.info_popover.clone().unwrap().blocks,
-//                 vec![HoverBlock {
-//                     text: "some basic docs".to_string(),
-//                     kind: HoverBlockKind::Markdown,
-//                 },]
-//             )
-//         });
-
-//         // Mouse moved with no hover response dismisses
-//         let hover_point = cx.display_point(indoc! {"
-//             fn teˇst() { println!(); }
-//         "});
-//         let mut request = cx
-//             .lsp
-//             .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
-//         cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
-//         cx.foreground()
-//             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
-//         request.next().await;
-//         cx.editor(|editor, _| {
-//             assert!(!editor.hover_state.visible());
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         // Hover with keyboard has no delay
-//         cx.set_state(indoc! {"
-//             fˇn test() { println!(); }
-//         "});
-//         cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
-//         let symbol_range = cx.lsp_range(indoc! {"
-//             Β«fnΒ» test() { println!(); }
-//         "});
-//         cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
-//             Ok(Some(lsp::Hover {
-//                 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
-//                     kind: lsp::MarkupKind::Markdown,
-//                     value: "some other basic docs".to_string(),
-//                 }),
-//                 range: Some(symbol_range),
-//             }))
-//         })
-//         .next()
-//         .await;
-
-//         cx.condition(|editor, _| editor.hover_state.visible()).await;
-//         cx.editor(|editor, _| {
-//             assert_eq!(
-//                 editor.hover_state.info_popover.clone().unwrap().blocks,
-//                 vec![HoverBlock {
-//                     text: "some other basic docs".to_string(),
-//                     kind: HoverBlockKind::Markdown,
-//                 }]
-//             )
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         // Hover with keyboard has no delay
-//         cx.set_state(indoc! {"
-//             fˇn test() { println!(); }
-//         "});
-//         cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
-//         let symbol_range = cx.lsp_range(indoc! {"
-//             Β«fnΒ» test() { println!(); }
-//         "});
-//         cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
-//             Ok(Some(lsp::Hover {
-//                 contents: lsp::HoverContents::Array(vec![
-//                     lsp::MarkedString::String("regular text for hover to show".to_string()),
-//                     lsp::MarkedString::String("".to_string()),
-//                     lsp::MarkedString::LanguageString(lsp::LanguageString {
-//                         language: "Rust".to_string(),
-//                         value: "".to_string(),
-//                     }),
-//                 ]),
-//                 range: Some(symbol_range),
-//             }))
-//         })
-//         .next()
-//         .await;
-
-//         cx.condition(|editor, _| editor.hover_state.visible()).await;
-//         cx.editor(|editor, _| {
-//             assert_eq!(
-//                 editor.hover_state.info_popover.clone().unwrap().blocks,
-//                 vec![HoverBlock {
-//                     text: "regular text for hover to show".to_string(),
-//                     kind: HoverBlockKind::Markdown,
-//                 }],
-//                 "No empty string hovers should be shown"
-//             );
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         // Hover with keyboard has no delay
-//         cx.set_state(indoc! {"
-//             fˇn test() { println!(); }
-//         "});
-//         cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
-//         let symbol_range = cx.lsp_range(indoc! {"
-//             Β«fnΒ» test() { println!(); }
-//         "});
-
-//         let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
-//         let markdown_string = format!("\n```rust\n{code_str}```");
-
-//         let closure_markdown_string = markdown_string.clone();
-//         cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
-//             let future_markdown_string = closure_markdown_string.clone();
-//             async move {
-//                 Ok(Some(lsp::Hover {
-//                     contents: lsp::HoverContents::Markup(lsp::MarkupContent {
-//                         kind: lsp::MarkupKind::Markdown,
-//                         value: future_markdown_string,
-//                     }),
-//                     range: Some(symbol_range),
-//                 }))
-//             }
-//         })
-//         .next()
-//         .await;
-
-//         cx.condition(|editor, _| editor.hover_state.visible()).await;
-//         cx.editor(|editor, _| {
-//             let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
-//             assert_eq!(
-//                 blocks,
-//                 vec![HoverBlock {
-//                     text: markdown_string,
-//                     kind: HoverBlockKind::Markdown,
-//                 }],
-//             );
-
-//             let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
-//             assert_eq!(
-//                 rendered.text,
-//                 code_str.trim(),
-//                 "Should not have extra line breaks at end of rendered hover"
-//             );
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         // Hover with just diagnostic, pops DiagnosticPopover immediately and then
-//         // info popover once request completes
-//         cx.set_state(indoc! {"
-//             fn teˇst() { println!(); }
-//         "});
-
-//         // Send diagnostic to client
-//         let range = cx.text_anchor_range(indoc! {"
-//             fn Β«testΒ»() { println!(); }
-//         "});
-//         cx.update_buffer(|buffer, cx| {
-//             let snapshot = buffer.text_snapshot();
-//             let set = DiagnosticSet::from_sorted_entries(
-//                 vec![DiagnosticEntry {
-//                     range,
-//                     diagnostic: Diagnostic {
-//                         message: "A test diagnostic message.".to_string(),
-//                         ..Default::default()
-//                     },
-//                 }],
-//                 &snapshot,
-//             );
-//             buffer.update_diagnostics(LanguageServerId(0), set, cx);
-//         });
-
-//         // Hover pops diagnostic immediately
-//         cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
-//         cx.foreground().run_until_parked();
-
-//         cx.editor(|Editor { hover_state, .. }, _| {
-//             assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
-//         });
-
-//         // Info Popover shows after request responded to
-//         let range = cx.lsp_range(indoc! {"
-//             fn Β«testΒ»() { println!(); }
-//         "});
-//         cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
-//             Ok(Some(lsp::Hover {
-//                 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
-//                     kind: lsp::MarkupKind::Markdown,
-//                     value: "some new docs".to_string(),
-//                 }),
-//                 range: Some(range),
-//             }))
-//         });
-//         cx.foreground()
-//             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
-
-//         cx.foreground().run_until_parked();
-//         cx.editor(|Editor { hover_state, .. }, _| {
-//             hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
-//         });
-//     }
-
-//     #[gpui::test]
-//     fn test_render_blocks(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |_| {});
-
-//         cx.add_window(|cx| {
-//             let editor = Editor::single_line(None, cx);
-//             let style = editor.style(cx);
-
-//             struct Row {
-//                 blocks: Vec<HoverBlock>,
-//                 expected_marked_text: String,
-//                 expected_styles: Vec<HighlightStyle>,
-//             }
-
-//             let rows = &[
-//                 // Strong emphasis
-//                 Row {
-//                     blocks: vec![HoverBlock {
-//                         text: "one **two** three".to_string(),
-//                         kind: HoverBlockKind::Markdown,
-//                     }],
-//                     expected_marked_text: "one Β«twoΒ» three".to_string(),
-//                     expected_styles: vec![HighlightStyle {
-//                         weight: Some(Weight::BOLD),
-//                         ..Default::default()
-//                     }],
-//                 },
-//                 // Links
-//                 Row {
-//                     blocks: vec![HoverBlock {
-//                         text: "one [two](https://the-url) three".to_string(),
-//                         kind: HoverBlockKind::Markdown,
-//                     }],
-//                     expected_marked_text: "one Β«twoΒ» three".to_string(),
-//                     expected_styles: vec![HighlightStyle {
-//                         underline: Some(Underline {
-//                             thickness: 1.0.into(),
-//                             ..Default::default()
-//                         }),
-//                         ..Default::default()
-//                     }],
-//                 },
-//                 // Lists
-//                 Row {
-//                     blocks: vec![HoverBlock {
-//                         text: "
-//                             lists:
-//                             * one
-//                                 - a
-//                                 - b
-//                             * two
-//                                 - [c](https://the-url)
-//                                 - d"
-//                         .unindent(),
-//                         kind: HoverBlockKind::Markdown,
-//                     }],
-//                     expected_marked_text: "
-//                         lists:
-//                         - one
-//                           - a
-//                           - b
-//                         - two
-//                           - Β«cΒ»
-//                           - d"
-//                     .unindent(),
-//                     expected_styles: vec![HighlightStyle {
-//                         underline: Some(Underline {
-//                             thickness: 1.0.into(),
-//                             ..Default::default()
-//                         }),
-//                         ..Default::default()
-//                     }],
-//                 },
-//                 // Multi-paragraph list items
-//                 Row {
-//                     blocks: vec![HoverBlock {
-//                         text: "
-//                             * one two
-//                               three
-
-//                             * four five
-//                                 * six seven
-//                                   eight
-
-//                                   nine
-//                                 * ten
-//                             * six"
-//                             .unindent(),
-//                         kind: HoverBlockKind::Markdown,
-//                     }],
-//                     expected_marked_text: "
-//                         - one two three
-//                         - four five
-//                           - six seven eight
-
-//                             nine
-//                           - ten
-//                         - six"
-//                         .unindent(),
-//                     expected_styles: vec![HighlightStyle {
-//                         underline: Some(Underline {
-//                             thickness: 1.0.into(),
-//                             ..Default::default()
-//                         }),
-//                         ..Default::default()
-//                     }],
-//                 },
-//             ];
-
-//             for Row {
-//                 blocks,
-//                 expected_marked_text,
-//                 expected_styles,
-//             } in &rows[0..]
-//             {
-//                 let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
-
-//                 let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
-//                 let expected_highlights = ranges
-//                     .into_iter()
-//                     .zip(expected_styles.iter().cloned())
-//                     .collect::<Vec<_>>();
-//                 assert_eq!(
-//                     rendered.text, expected_text,
-//                     "wrong text for input {blocks:?}"
-//                 );
-
-//                 let rendered_highlights: Vec<_> = rendered
-//                     .highlights
-//                     .iter()
-//                     .filter_map(|(range, highlight)| {
-//                         let highlight = highlight.to_highlight_style(&style.syntax)?;
-//                         Some((range.clone(), highlight))
-//                     })
-//                     .collect();
-
-//                 assert_eq!(
-//                     rendered_highlights, expected_highlights,
-//                     "wrong highlights for input {blocks:?}"
-//                 );
-//             }
-
-//             editor
-//         });
-//     }
-
-//     #[gpui::test]
-//     async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
-//         init_test(cx, |settings| {
-//             settings.defaults.inlay_hints = Some(InlayHintSettings {
-//                 enabled: true,
-//                 show_type_hints: true,
-//                 show_parameter_hints: true,
-//                 show_other_hints: true,
-//             })
-//         });
-
-//         let mut cx = EditorLspTestContext::new_rust(
-//             lsp::ServerCapabilities {
-//                 inlay_hint_provider: Some(lsp::OneOf::Right(
-//                     lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
-//                         resolve_provider: Some(true),
-//                         ..Default::default()
-//                     }),
-//                 )),
-//                 ..Default::default()
-//             },
-//             cx,
-//         )
-//         .await;
-
-//         cx.set_state(indoc! {"
-//             struct TestStruct;
-
-//             // ==================
-
-//             struct TestNewType<T>(T);
-
-//             fn main() {
-//                 let variableˇ = TestNewType(TestStruct);
-//             }
-//         "});
-
-//         let hint_start_offset = cx.ranges(indoc! {"
-//             struct TestStruct;
-
-//             // ==================
-
-//             struct TestNewType<T>(T);
-
-//             fn main() {
-//                 let variableˇ = TestNewType(TestStruct);
-//             }
-//         "})[0]
-//             .start;
-//         let hint_position = cx.to_lsp(hint_start_offset);
-//         let new_type_target_range = cx.lsp_range(indoc! {"
-//             struct TestStruct;
-
-//             // ==================
-
-//             struct Β«TestNewTypeΒ»<T>(T);
-
-//             fn main() {
-//                 let variable = TestNewType(TestStruct);
-//             }
-//         "});
-//         let struct_target_range = cx.lsp_range(indoc! {"
-//             struct Β«TestStructΒ»;
-
-//             // ==================
-
-//             struct TestNewType<T>(T);
-
-//             fn main() {
-//                 let variable = TestNewType(TestStruct);
-//             }
-//         "});
-
-//         let uri = cx.buffer_lsp_url.clone();
-//         let new_type_label = "TestNewType";
-//         let struct_label = "TestStruct";
-//         let entire_hint_label = ": TestNewType<TestStruct>";
-//         let closure_uri = uri.clone();
-//         cx.lsp
-//             .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
-//                 let task_uri = closure_uri.clone();
-//                 async move {
-//                     assert_eq!(params.text_document.uri, task_uri);
-//                     Ok(Some(vec![lsp::InlayHint {
-//                         position: hint_position,
-//                         label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
-//                             value: entire_hint_label.to_string(),
-//                             ..Default::default()
-//                         }]),
-//                         kind: Some(lsp::InlayHintKind::TYPE),
-//                         text_edits: None,
-//                         tooltip: None,
-//                         padding_left: Some(false),
-//                         padding_right: Some(false),
-//                         data: None,
-//                     }]))
-//                 }
-//             })
-//             .next()
-//             .await;
-//         cx.foreground().run_until_parked();
-//         cx.update_editor(|editor, cx| {
-//             let expected_layers = vec![entire_hint_label.to_string()];
-//             assert_eq!(expected_layers, cached_hint_labels(editor));
-//             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-//         });
-
-//         let inlay_range = cx
-//             .ranges(indoc! {"
-//                 struct TestStruct;
-
-//                 // ==================
-
-//                 struct TestNewType<T>(T);
-
-//                 fn main() {
-//                     let variableΒ« Β»= TestNewType(TestStruct);
-//                 }
-//         "})
-//             .get(0)
-//             .cloned()
-//             .unwrap();
-//         let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
-//             let snapshot = editor.snapshot(cx);
-//             let previous_valid = inlay_range.start.to_display_point(&snapshot);
-//             let next_valid = inlay_range.end.to_display_point(&snapshot);
-//             assert_eq!(previous_valid.row(), next_valid.row());
-//             assert!(previous_valid.column() < next_valid.column());
-//             let exact_unclipped = DisplayPoint::new(
-//                 previous_valid.row(),
-//                 previous_valid.column()
-//                     + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
-//                         as u32,
-//             );
-//             PointForPosition {
-//                 previous_valid,
-//                 next_valid,
-//                 exact_unclipped,
-//                 column_overshoot_after_line_end: 0,
-//             }
-//         });
-//         cx.update_editor(|editor, cx| {
-//             update_inlay_link_and_hover_points(
-//                 &editor.snapshot(cx),
-//                 new_type_hint_part_hover_position,
-//                 editor,
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-
-//         let resolve_closure_uri = uri.clone();
-//         cx.lsp
-//             .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
-//                 move |mut hint_to_resolve, _| {
-//                     let mut resolved_hint_positions = BTreeSet::new();
-//                     let task_uri = resolve_closure_uri.clone();
-//                     async move {
-//                         let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
-//                         assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
-
-//                         // `: TestNewType<TestStruct>`
-//                         hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
-//                             lsp::InlayHintLabelPart {
-//                                 value: ": ".to_string(),
-//                                 ..Default::default()
-//                             },
-//                             lsp::InlayHintLabelPart {
-//                                 value: new_type_label.to_string(),
-//                                 location: Some(lsp::Location {
-//                                     uri: task_uri.clone(),
-//                                     range: new_type_target_range,
-//                                 }),
-//                                 tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
-//                                     "A tooltip for `{new_type_label}`"
-//                                 ))),
-//                                 ..Default::default()
-//                             },
-//                             lsp::InlayHintLabelPart {
-//                                 value: "<".to_string(),
-//                                 ..Default::default()
-//                             },
-//                             lsp::InlayHintLabelPart {
-//                                 value: struct_label.to_string(),
-//                                 location: Some(lsp::Location {
-//                                     uri: task_uri,
-//                                     range: struct_target_range,
-//                                 }),
-//                                 tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
-//                                     lsp::MarkupContent {
-//                                         kind: lsp::MarkupKind::Markdown,
-//                                         value: format!("A tooltip for `{struct_label}`"),
-//                                     },
-//                                 )),
-//                                 ..Default::default()
-//                             },
-//                             lsp::InlayHintLabelPart {
-//                                 value: ">".to_string(),
-//                                 ..Default::default()
-//                             },
-//                         ]);
-
-//                         Ok(hint_to_resolve)
-//                     }
-//                 },
-//             )
-//             .next()
-//             .await;
-//         cx.foreground().run_until_parked();
-
-//         cx.update_editor(|editor, cx| {
-//             update_inlay_link_and_hover_points(
-//                 &editor.snapshot(cx),
-//                 new_type_hint_part_hover_position,
-//                 editor,
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         cx.foreground()
-//             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
-//         cx.foreground().run_until_parked();
-//         cx.update_editor(|editor, 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));
-//             assert_eq!(
-//                 popover.symbol_range,
-//                 RangeInEditor::Inlay(InlayHighlight {
-//                     inlay: InlayId::Hint(0),
-//                     inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-//                     range: ": ".len()..": ".len() + new_type_label.len(),
-//                 }),
-//                 "Popover range should match the new type label part"
-//             );
-//             assert_eq!(
-//                 popover.parsed_content.text,
-//                 format!("A tooltip for `{new_type_label}`"),
-//                 "Rendered text should not anyhow alter backticks"
-//             );
-//         });
-
-//         let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
-//             let snapshot = editor.snapshot(cx);
-//             let previous_valid = inlay_range.start.to_display_point(&snapshot);
-//             let next_valid = inlay_range.end.to_display_point(&snapshot);
-//             assert_eq!(previous_valid.row(), next_valid.row());
-//             assert!(previous_valid.column() < next_valid.column());
-//             let exact_unclipped = DisplayPoint::new(
-//                 previous_valid.row(),
-//                 previous_valid.column()
-//                     + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
-//                         as u32,
-//             );
-//             PointForPosition {
-//                 previous_valid,
-//                 next_valid,
-//                 exact_unclipped,
-//                 column_overshoot_after_line_end: 0,
-//             }
-//         });
-//         cx.update_editor(|editor, cx| {
-//             update_inlay_link_and_hover_points(
-//                 &editor.snapshot(cx),
-//                 struct_hint_part_hover_position,
-//                 editor,
-//                 true,
-//                 false,
-//                 cx,
-//             );
-//         });
-//         cx.foreground()
-//             .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
-//         cx.foreground().run_until_parked();
-//         cx.update_editor(|editor, 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));
-//             assert_eq!(
-//                 popover.symbol_range,
-//                 RangeInEditor::Inlay(InlayHighlight {
-//                     inlay: InlayId::Hint(0),
-//                     inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-//                     range: ": ".len() + new_type_label.len() + "<".len()
-//                         ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
-//                 }),
-//                 "Popover range should match the struct label part"
-//             );
-//             assert_eq!(
-//                 popover.parsed_content.text,
-//                 format!("A tooltip for {struct_label}"),
-//                 "Rendered markdown element should remove backticks from text"
-//             );
-//         });
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        editor_tests::init_test,
+        element::PointForPosition,
+        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::{FontWeight, HighlightStyle, UnderlineStyle};
+    use indoc::indoc;
+    use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
+    use lsp::LanguageServerId;
+    use project::{HoverBlock, HoverBlockKind};
+    use smol::stream::StreamExt;
+    use unindent::Unindent;
+    use util::test::marked_text_ranges;
+
+    #[gpui::test]
+    async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Basic hover delays and then pops without moving the mouse
+        cx.set_state(indoc! {"
+            fn Λ‡test() { println!(); }
+        "});
+        let hover_point = cx.display_point(indoc! {"
+            fn test() { printˇln!(); }
+        "});
+
+        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
+        assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
+
+        // After delay, hover should be visible.
+        let symbol_range = cx.lsp_range(indoc! {"
+            fn test() { Β«println!Β»(); }
+        "});
+        let mut requests =
+            cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+                Ok(Some(lsp::Hover {
+                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                        kind: lsp::MarkupKind::Markdown,
+                        value: "some basic docs".to_string(),
+                    }),
+                    range: Some(symbol_range),
+                }))
+            });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        requests.next().await;
+
+        cx.editor(|editor, _| {
+            assert!(editor.hover_state.visible());
+            assert_eq!(
+                editor.hover_state.info_popover.clone().unwrap().blocks,
+                vec![HoverBlock {
+                    text: "some basic docs".to_string(),
+                    kind: HoverBlockKind::Markdown,
+                },]
+            )
+        });
+
+        // Mouse moved with no hover response dismisses
+        let hover_point = cx.display_point(indoc! {"
+            fn teˇst() { println!(); }
+        "});
+        let mut request = cx
+            .lsp
+            .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
+        cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
+        cx.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        request.next().await;
+        cx.editor(|editor, _| {
+            assert!(!editor.hover_state.visible());
+        });
+    }
+
+    #[gpui::test]
+    async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with keyboard has no delay
+        cx.set_state(indoc! {"
+            fˇn test() { println!(); }
+        "});
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        let symbol_range = cx.lsp_range(indoc! {"
+            Β«fnΒ» test() { println!(); }
+        "});
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                    kind: lsp::MarkupKind::Markdown,
+                    value: "some other basic docs".to_string(),
+                }),
+                range: Some(symbol_range),
+            }))
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
+        cx.editor(|editor, _| {
+            assert_eq!(
+                editor.hover_state.info_popover.clone().unwrap().blocks,
+                vec![HoverBlock {
+                    text: "some other basic docs".to_string(),
+                    kind: HoverBlockKind::Markdown,
+                }]
+            )
+        });
+    }
+
+    #[gpui::test]
+    async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with keyboard has no delay
+        cx.set_state(indoc! {"
+            fˇn test() { println!(); }
+        "});
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        let symbol_range = cx.lsp_range(indoc! {"
+            Β«fnΒ» test() { println!(); }
+        "});
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Array(vec![
+                    lsp::MarkedString::String("regular text for hover to show".to_string()),
+                    lsp::MarkedString::String("".to_string()),
+                    lsp::MarkedString::LanguageString(lsp::LanguageString {
+                        language: "Rust".to_string(),
+                        value: "".to_string(),
+                    }),
+                ]),
+                range: Some(symbol_range),
+            }))
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
+        cx.editor(|editor, _| {
+            assert_eq!(
+                editor.hover_state.info_popover.clone().unwrap().blocks,
+                vec![HoverBlock {
+                    text: "regular text for hover to show".to_string(),
+                    kind: HoverBlockKind::Markdown,
+                }],
+                "No empty string hovers should be shown"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with keyboard has no delay
+        cx.set_state(indoc! {"
+            fˇn test() { println!(); }
+        "});
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        let symbol_range = cx.lsp_range(indoc! {"
+            Β«fnΒ» test() { println!(); }
+        "});
+
+        let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
+        let markdown_string = format!("\n```rust\n{code_str}```");
+
+        let closure_markdown_string = markdown_string.clone();
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
+            let future_markdown_string = closure_markdown_string.clone();
+            async move {
+                Ok(Some(lsp::Hover {
+                    contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                        kind: lsp::MarkupKind::Markdown,
+                        value: future_markdown_string,
+                    }),
+                    range: Some(symbol_range),
+                }))
+            }
+        })
+        .next()
+        .await;
+
+        cx.condition(|editor, _| editor.hover_state.visible()).await;
+        cx.editor(|editor, _| {
+            let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
+            assert_eq!(
+                blocks,
+                vec![HoverBlock {
+                    text: markdown_string,
+                    kind: HoverBlockKind::Markdown,
+                }],
+            );
+
+            let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
+            assert_eq!(
+                rendered.text,
+                code_str.trim(),
+                "Should not have extra line breaks at end of rendered hover"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        // Hover with just diagnostic, pops DiagnosticPopover immediately and then
+        // info popover once request completes
+        cx.set_state(indoc! {"
+            fn teˇst() { println!(); }
+        "});
+
+        // Send diagnostic to client
+        let range = cx.text_anchor_range(indoc! {"
+            fn Β«testΒ»() { println!(); }
+        "});
+        cx.update_buffer(|buffer, cx| {
+            let snapshot = buffer.text_snapshot();
+            let set = DiagnosticSet::from_sorted_entries(
+                vec![DiagnosticEntry {
+                    range,
+                    diagnostic: Diagnostic {
+                        message: "A test diagnostic message.".to_string(),
+                        ..Default::default()
+                    },
+                }],
+                &snapshot,
+            );
+            buffer.update_diagnostics(LanguageServerId(0), set, cx);
+        });
+
+        // Hover pops diagnostic immediately
+        cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+        cx.background_executor.run_until_parked();
+
+        cx.editor(|Editor { hover_state, .. }, _| {
+            assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
+        });
+
+        // Info Popover shows after request responded to
+        let range = cx.lsp_range(indoc! {"
+            fn Β«testΒ»() { println!(); }
+        "});
+        cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+            Ok(Some(lsp::Hover {
+                contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+                    kind: lsp::MarkupKind::Markdown,
+                    value: "some new docs".to_string(),
+                }),
+                range: Some(range),
+            }))
+        });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+
+        cx.background_executor.run_until_parked();
+        cx.editor(|Editor { hover_state, .. }, _| {
+            hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
+        });
+    }
+
+    #[gpui::test]
+    fn test_render_blocks(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
+        let editor = cx.add_window(|cx| Editor::single_line(cx));
+        editor
+            .update(cx, |editor, cx| {
+                let style = editor.style.clone().unwrap();
+
+                struct Row {
+                    blocks: Vec<HoverBlock>,
+                    expected_marked_text: String,
+                    expected_styles: Vec<HighlightStyle>,
+                }
+
+                let rows = &[
+                    // Strong emphasis
+                    Row {
+                        blocks: vec![HoverBlock {
+                            text: "one **two** three".to_string(),
+                            kind: HoverBlockKind::Markdown,
+                        }],
+                        expected_marked_text: "one Β«twoΒ» three".to_string(),
+                        expected_styles: vec![HighlightStyle {
+                            font_weight: Some(FontWeight::BOLD),
+                            ..Default::default()
+                        }],
+                    },
+                    // Links
+                    Row {
+                        blocks: vec![HoverBlock {
+                            text: "one [two](https://the-url) three".to_string(),
+                            kind: HoverBlockKind::Markdown,
+                        }],
+                        expected_marked_text: "one Β«twoΒ» three".to_string(),
+                        expected_styles: vec![HighlightStyle {
+                            underline: Some(UnderlineStyle {
+                                thickness: 1.0.into(),
+                                ..Default::default()
+                            }),
+                            ..Default::default()
+                        }],
+                    },
+                    // Lists
+                    Row {
+                        blocks: vec![HoverBlock {
+                            text: "
+                            lists:
+                            * one
+                                - a
+                                - b
+                            * two
+                                - [c](https://the-url)
+                                - d"
+                            .unindent(),
+                            kind: HoverBlockKind::Markdown,
+                        }],
+                        expected_marked_text: "
+                        lists:
+                        - one
+                          - a
+                          - b
+                        - two
+                          - Β«cΒ»
+                          - d"
+                        .unindent(),
+                        expected_styles: vec![HighlightStyle {
+                            underline: Some(UnderlineStyle {
+                                thickness: 1.0.into(),
+                                ..Default::default()
+                            }),
+                            ..Default::default()
+                        }],
+                    },
+                    // Multi-paragraph list items
+                    Row {
+                        blocks: vec![HoverBlock {
+                            text: "
+                            * one two
+                              three
+
+                            * four five
+                                * six seven
+                                  eight
+
+                                  nine
+                                * ten
+                            * six"
+                                .unindent(),
+                            kind: HoverBlockKind::Markdown,
+                        }],
+                        expected_marked_text: "
+                        - one two three
+                        - four five
+                          - six seven eight
+
+                            nine
+                          - ten
+                        - six"
+                            .unindent(),
+                        expected_styles: vec![HighlightStyle {
+                            underline: Some(UnderlineStyle {
+                                thickness: 1.0.into(),
+                                ..Default::default()
+                            }),
+                            ..Default::default()
+                        }],
+                    },
+                ];
+
+                for Row {
+                    blocks,
+                    expected_marked_text,
+                    expected_styles,
+                } in &rows[0..]
+                {
+                    let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
+
+                    let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
+                    let expected_highlights = ranges
+                        .into_iter()
+                        .zip(expected_styles.iter().cloned())
+                        .collect::<Vec<_>>();
+                    assert_eq!(
+                        rendered.text, expected_text,
+                        "wrong text for input {blocks:?}"
+                    );
+
+                    let rendered_highlights: Vec<_> = rendered
+                        .highlights
+                        .iter()
+                        .filter_map(|(range, highlight)| {
+                            let highlight = highlight.to_highlight_style(&style.syntax)?;
+                            Some((range.clone(), highlight))
+                        })
+                        .collect();
+
+                    assert_eq!(
+                        rendered_highlights, expected_highlights,
+                        "wrong highlights for input {blocks:?}"
+                    );
+                }
+            })
+            .unwrap();
+    }
+
+    #[gpui::test]
+    async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: true,
+                show_parameter_hints: true,
+                show_other_hints: true,
+            })
+        });
+
+        let mut cx = EditorLspTestContext::new_rust(
+            lsp::ServerCapabilities {
+                inlay_hint_provider: Some(lsp::OneOf::Right(
+                    lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
+                        resolve_provider: Some(true),
+                        ..Default::default()
+                    }),
+                )),
+                ..Default::default()
+            },
+            cx,
+        )
+        .await;
+
+        cx.set_state(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variableˇ = TestNewType(TestStruct);
+            }
+        "});
+
+        let hint_start_offset = cx.ranges(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variableˇ = TestNewType(TestStruct);
+            }
+        "})[0]
+            .start;
+        let hint_position = cx.to_lsp(hint_start_offset);
+        let new_type_target_range = cx.lsp_range(indoc! {"
+            struct TestStruct;
+
+            // ==================
+
+            struct Β«TestNewTypeΒ»<T>(T);
+
+            fn main() {
+                let variable = TestNewType(TestStruct);
+            }
+        "});
+        let struct_target_range = cx.lsp_range(indoc! {"
+            struct Β«TestStructΒ»;
+
+            // ==================
+
+            struct TestNewType<T>(T);
+
+            fn main() {
+                let variable = TestNewType(TestStruct);
+            }
+        "});
+
+        let uri = cx.buffer_lsp_url.clone();
+        let new_type_label = "TestNewType";
+        let struct_label = "TestStruct";
+        let entire_hint_label = ": TestNewType<TestStruct>";
+        let closure_uri = uri.clone();
+        cx.lsp
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_uri = closure_uri.clone();
+                async move {
+                    assert_eq!(params.text_document.uri, task_uri);
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: hint_position,
+                        label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
+                            value: entire_hint_label.to_string(),
+                            ..Default::default()
+                        }]),
+                        kind: Some(lsp::InlayHintKind::TYPE),
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: Some(false),
+                        padding_right: Some(false),
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.background_executor.run_until_parked();
+        cx.update_editor(|editor, cx| {
+            let expected_layers = vec![entire_hint_label.to_string()];
+            assert_eq!(expected_layers, cached_hint_labels(editor));
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+        });
+
+        let inlay_range = cx
+            .ranges(indoc! {"
+                struct TestStruct;
+
+                // ==================
+
+                struct TestNewType<T>(T);
+
+                fn main() {
+                    let variableΒ« Β»= TestNewType(TestStruct);
+                }
+        "})
+            .get(0)
+            .cloned()
+            .unwrap();
+        let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let previous_valid = inlay_range.start.to_display_point(&snapshot);
+            let next_valid = inlay_range.end.to_display_point(&snapshot);
+            assert_eq!(previous_valid.row(), next_valid.row());
+            assert!(previous_valid.column() < next_valid.column());
+            let exact_unclipped = DisplayPoint::new(
+                previous_valid.row(),
+                previous_valid.column()
+                    + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
+                        as u32,
+            );
+            PointForPosition {
+                previous_valid,
+                next_valid,
+                exact_unclipped,
+                column_overshoot_after_line_end: 0,
+            }
+        });
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                new_type_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+
+        let resolve_closure_uri = uri.clone();
+        cx.lsp
+            .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
+                move |mut hint_to_resolve, _| {
+                    let mut resolved_hint_positions = BTreeSet::new();
+                    let task_uri = resolve_closure_uri.clone();
+                    async move {
+                        let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
+                        assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
+
+                        // `: TestNewType<TestStruct>`
+                        hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
+                            lsp::InlayHintLabelPart {
+                                value: ": ".to_string(),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: new_type_label.to_string(),
+                                location: Some(lsp::Location {
+                                    uri: task_uri.clone(),
+                                    range: new_type_target_range,
+                                }),
+                                tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
+                                    "A tooltip for `{new_type_label}`"
+                                ))),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: "<".to_string(),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: struct_label.to_string(),
+                                location: Some(lsp::Location {
+                                    uri: task_uri,
+                                    range: struct_target_range,
+                                }),
+                                tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
+                                    lsp::MarkupContent {
+                                        kind: lsp::MarkupKind::Markdown,
+                                        value: format!("A tooltip for `{struct_label}`"),
+                                    },
+                                )),
+                                ..Default::default()
+                            },
+                            lsp::InlayHintLabelPart {
+                                value: ">".to_string(),
+                                ..Default::default()
+                            },
+                        ]);
+
+                        Ok(hint_to_resolve)
+                    }
+                },
+            )
+            .next()
+            .await;
+        cx.background_executor.run_until_parked();
+
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                new_type_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        cx.background_executor.run_until_parked();
+        cx.update_editor(|editor, 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));
+            assert_eq!(
+                popover.symbol_range,
+                RangeInEditor::Inlay(InlayHighlight {
+                    inlay: InlayId::Hint(0),
+                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+                    range: ": ".len()..": ".len() + new_type_label.len(),
+                }),
+                "Popover range should match the new type label part"
+            );
+            assert_eq!(
+                popover.parsed_content.text,
+                format!("A tooltip for `{new_type_label}`"),
+                "Rendered text should not anyhow alter backticks"
+            );
+        });
+
+        let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
+            let snapshot = editor.snapshot(cx);
+            let previous_valid = inlay_range.start.to_display_point(&snapshot);
+            let next_valid = inlay_range.end.to_display_point(&snapshot);
+            assert_eq!(previous_valid.row(), next_valid.row());
+            assert!(previous_valid.column() < next_valid.column());
+            let exact_unclipped = DisplayPoint::new(
+                previous_valid.row(),
+                previous_valid.column()
+                    + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
+                        as u32,
+            );
+            PointForPosition {
+                previous_valid,
+                next_valid,
+                exact_unclipped,
+                column_overshoot_after_line_end: 0,
+            }
+        });
+        cx.update_editor(|editor, cx| {
+            update_inlay_link_and_hover_points(
+                &editor.snapshot(cx),
+                struct_hint_part_hover_position,
+                editor,
+                true,
+                false,
+                cx,
+            );
+        });
+        cx.background_executor
+            .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+        cx.background_executor.run_until_parked();
+        cx.update_editor(|editor, 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));
+            assert_eq!(
+                popover.symbol_range,
+                RangeInEditor::Inlay(InlayHighlight {
+                    inlay: InlayId::Hint(0),
+                    inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+                    range: ": ".len() + new_type_label.len() + "<".len()
+                        ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
+                }),
+                "Popover range should match the struct label part"
+            );
+            assert_eq!(
+                popover.parsed_content.text,
+                format!("A tooltip for {struct_label}"),
+                "Rendered markdown element should remove backticks from text"
+            );
+        });
+    }
+}
@@ -5,7 +5,7 @@ use crate::{
     Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId,
     SelectPhase,
 };
-use gpui::{Task, ViewContext};
+use gpui::{px, Task, ViewContext};
 use language::{Bias, ToOffset};
 use lsp::LanguageServerId;
 use project::{
@@ -13,6 +13,7 @@ use project::{
     ResolveState,
 };
 use std::ops::Range;
+use theme::ActiveTheme as _;
 use util::TryFutureExt;
 
 #[derive(Debug, Default)]
@@ -485,40 +486,45 @@ pub fn show_link_definition(
                         });
 
                     if any_definition_does_not_contain_current_location {
-                        // todo!()
-                        // // Highlight symbol using theme link definition highlight style
-                        // let style = theme::current(cx).editor.link_definition;
-                        // let highlight_range =
-                        //     symbol_range.unwrap_or_else(|| match &trigger_point {
-                        //         TriggerPoint::Text(trigger_anchor) => {
-                        //             let snapshot = &snapshot.buffer_snapshot;
-                        //             // If no symbol range returned from language server, use the surrounding word.
-                        //             let (offset_range, _) =
-                        //                 snapshot.surrounding_word(*trigger_anchor);
-                        //             RangeInEditor::Text(
-                        //                 snapshot.anchor_before(offset_range.start)
-                        //                     ..snapshot.anchor_after(offset_range.end),
-                        //             )
-                        //         }
-                        //         TriggerPoint::InlayHint(highlight, _, _) => {
-                        //             RangeInEditor::Inlay(highlight.clone())
-                        //         }
-                        //     });
-
-                        // match highlight_range {
-                        //     RangeInEditor::Text(text_range) => this
-                        //         .highlight_text::<LinkGoToDefinitionState>(
-                        //             vec![text_range],
-                        //             style,
-                        //             cx,
-                        //         ),
-                        //     RangeInEditor::Inlay(highlight) => this
-                        //         .highlight_inlays::<LinkGoToDefinitionState>(
-                        //             vec![highlight],
-                        //             style,
-                        //             cx,
-                        //         ),
-                        // }
+                        let style = gpui::HighlightStyle {
+                            underline: Some(gpui::UnderlineStyle {
+                                thickness: px(1.),
+                                ..Default::default()
+                            }),
+                            color: Some(gpui::red()),
+                            ..Default::default()
+                        };
+                        let highlight_range =
+                            symbol_range.unwrap_or_else(|| match &trigger_point {
+                                TriggerPoint::Text(trigger_anchor) => {
+                                    let snapshot = &snapshot.buffer_snapshot;
+                                    // If no symbol range returned from language server, use the surrounding word.
+                                    let (offset_range, _) =
+                                        snapshot.surrounding_word(*trigger_anchor);
+                                    RangeInEditor::Text(
+                                        snapshot.anchor_before(offset_range.start)
+                                            ..snapshot.anchor_after(offset_range.end),
+                                    )
+                                }
+                                TriggerPoint::InlayHint(highlight, _, _) => {
+                                    RangeInEditor::Inlay(highlight.clone())
+                                }
+                            });
+
+                        match highlight_range {
+                            RangeInEditor::Text(text_range) => this
+                                .highlight_text::<LinkGoToDefinitionState>(
+                                    vec![text_range],
+                                    style,
+                                    cx,
+                                ),
+                            RangeInEditor::Inlay(highlight) => this
+                                .highlight_inlays::<LinkGoToDefinitionState>(
+                                    vec![highlight],
+                                    style,
+                                    cx,
+                                ),
+                        }
                     } else {
                         hide_link_definition(this, cx);
                     }

crates/editor2/src/selections_collection.rs πŸ”—

@@ -595,31 +595,32 @@ impl<'a> MutableSelectionsCollection<'a> {
         self.select(selections)
     }
 
-    pub fn select_anchor_ranges<I: IntoIterator<Item = Range<Anchor>>>(&mut self, ranges: I) {
-        todo!()
-        // let buffer = self.buffer.read(self.cx).snapshot(self.cx);
-        // let selections = ranges
-        //     .into_iter()
-        //     .map(|range| {
-        //         let mut start = range.start;
-        //         let mut end = range.end;
-        //         let reversed = if start.cmp(&end, &buffer).is_gt() {
-        //             mem::swap(&mut start, &mut end);
-        //             true
-        //         } else {
-        //             false
-        //         };
-        //         Selection {
-        //             id: post_inc(&mut self.collection.next_selection_id),
-        //             start,
-        //             end,
-        //             reversed,
-        //             goal: SelectionGoal::None,
-        //         }
-        //     })
-        //     .collect::<Vec<_>>();
-
-        // self.select_anchors(selections)
+    pub fn select_anchor_ranges<I>(&mut self, ranges: I)
+    where
+        I: IntoIterator<Item = Range<Anchor>>,
+    {
+        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        let selections = ranges
+            .into_iter()
+            .map(|range| {
+                let mut start = range.start;
+                let mut end = range.end;
+                let reversed = if start.cmp(&end, &buffer).is_gt() {
+                    mem::swap(&mut start, &mut end);
+                    true
+                } else {
+                    false
+                };
+                Selection {
+                    id: post_inc(&mut self.collection.next_selection_id),
+                    start,
+                    end,
+                    reversed,
+                    goal: SelectionGoal::None,
+                }
+            })
+            .collect::<Vec<_>>();
+        self.select_anchors(selections)
     }
 
     pub fn new_selection_id(&mut self) -> usize {

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

@@ -27,7 +27,7 @@ pub fn marked_display_snapshot(
     let (unmarked_text, markers) = marked_text_offsets(text);
 
     let font = cx.text_style().font();
-    let font_size: Pixels = 14.into();
+    let font_size: Pixels = 14usize.into();
 
     let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
     let display_map = cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));

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

@@ -30,11 +30,11 @@ pub trait FeatureFlagViewExt<V: 'static> {
 
 impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V>
 where
-    V: 'static + Send + Sync,
+    V: 'static,
 {
     fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
     where
-        F: Fn(bool, &mut V, &mut ViewContext<V>) + Send + Sync + 'static,
+        F: Fn(bool, &mut V, &mut ViewContext<V>) + 'static,
     {
         self.observe_global::<FeatureFlags>(move |v, cx| {
             let feature_flags = cx.global::<FeatureFlags>();

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

@@ -518,6 +518,7 @@ impl PickerDelegate for FileFinderDelegate {
     }
 
     fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
+        let raw_query = raw_query.trim();
         if raw_query.is_empty() {
             let project = self.project.read(cx);
             self.latest_search_id = post_inc(&mut self.search_count);
@@ -539,7 +540,6 @@ impl PickerDelegate for FileFinderDelegate {
             cx.notify();
             Task::ready(())
         } else {
-            let raw_query = &raw_query;
             let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
                 Ok::<_, std::convert::Infallible>(FileSearchQuery {
                     raw_query: raw_query.to_owned(),
@@ -735,6 +735,7 @@ mod tests {
         cx.dispatch_action(window.into(), Toggle);
 
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+
         finder
             .update(cx, |finder, cx| {
                 finder.delegate_mut().update_matches("bna".to_string(), cx)
@@ -743,7 +744,6 @@ mod tests {
         finder.read_with(cx, |finder, _| {
             assert_eq!(finder.delegate().matches.len(), 2);
         });
-
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
         cx.dispatch_action(window.into(), SelectNext);
         cx.dispatch_action(window.into(), Confirm);
@@ -762,6 +762,49 @@ mod tests {
                 "bandana"
             );
         });
+
+        for bandana_query in [
+            "bandana",
+            " bandana",
+            "bandana ",
+            " bandana ",
+            " ndan ",
+            " band ",
+        ] {
+            finder
+                .update(cx, |finder, cx| {
+                    finder
+                        .delegate_mut()
+                        .update_matches(bandana_query.to_string(), cx)
+                })
+                .await;
+            finder.read_with(cx, |finder, _| {
+                assert_eq!(
+                    finder.delegate().matches.len(),
+                    1,
+                    "Wrong number of matches for bandana query '{bandana_query}'"
+                );
+            });
+            let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+            cx.dispatch_action(window.into(), SelectNext);
+            cx.dispatch_action(window.into(), Confirm);
+            active_pane
+                .condition(cx, |pane, _| pane.active_item().is_some())
+                .await;
+            cx.read(|cx| {
+                let active_item = active_pane.read(cx).active_item().unwrap();
+                assert_eq!(
+                    active_item
+                        .as_any()
+                        .downcast_ref::<Editor>()
+                        .unwrap()
+                        .read(cx)
+                        .title(cx),
+                    "bandana",
+                    "Wrong match for bandana query '{bandana_query}'"
+                );
+            });
+        }
     }
 
     #[gpui::test]

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

@@ -2,9 +2,8 @@ use collections::HashMap;
 use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
 use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
-    actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
-    IntoElement, Manager, Model, ParentElement, Render, Styled, Task, View, ViewContext,
-    VisualContext, WeakView,
+    actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
+    ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@@ -16,8 +15,7 @@ use std::{
     },
 };
 use text::Point;
-use theme::ActiveTheme;
-use ui::{v_stack, HighlightedLabel, StyledExt};
+use ui::{v_stack, HighlightedLabel, ListItem};
 use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
 use workspace::Workspace;
 
@@ -111,7 +109,7 @@ impl FileFinder {
     }
 }
 
-impl EventEmitter<Manager> for FileFinder {}
+impl EventEmitter<DismissEvent> for FileFinder {}
 impl FocusableView for FileFinder {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
         self.picker.focus_handle(cx)
@@ -530,7 +528,7 @@ impl FileFinderDelegate {
 }
 
 impl PickerDelegate for FileFinderDelegate {
-    type ListItem = Div;
+    type ListItem = ListItem;
 
     fn placeholder_text(&self) -> Arc<str> {
         "Search project files...".into()
@@ -554,6 +552,7 @@ impl PickerDelegate for FileFinderDelegate {
         raw_query: String,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> Task<()> {
+        let raw_query = raw_query.trim();
         if raw_query.is_empty() {
             let project = self.project.read(cx);
             self.latest_search_id = post_inc(&mut self.search_count);
@@ -575,7 +574,6 @@ impl PickerDelegate for FileFinderDelegate {
             cx.notify();
             Task::ready(())
         } else {
-            let raw_query = &raw_query;
             let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
                 Ok::<_, std::convert::Infallible>(FileSearchQuery {
                     raw_query: raw_query.to_owned(),
@@ -689,9 +687,7 @@ impl PickerDelegate for FileFinderDelegate {
                                 .log_err();
                         }
                     }
-                    finder
-                        .update(&mut cx, |_, cx| cx.emit(Manager::Dismiss))
-                        .ok()?;
+                    finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?;
 
                     Some(())
                 })
@@ -702,7 +698,7 @@ impl PickerDelegate for FileFinderDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
         self.file_finder
-            .update(cx, |_, cx| cx.emit(Manager::Dismiss))
+            .update(cx, |_, cx| cx.emit(DismissEvent))
             .log_err();
     }
 
@@ -711,30 +707,22 @@ impl PickerDelegate for FileFinderDelegate {
         ix: usize,
         selected: bool,
         cx: &mut ViewContext<Picker<Self>>,
-    ) -> Self::ListItem {
+    ) -> Option<Self::ListItem> {
         let path_match = self
             .matches
             .get(ix)
             .expect("Invalid matches state: no element for index {ix}");
-        let theme = cx.theme();
-        let colors = theme.colors();
 
         let (file_name, file_name_positions, full_path, full_path_positions) =
             self.labels_for_match(path_match, cx, ix);
 
-        div()
-            .px_1()
-            .text_color(colors.text)
-            .text_ui()
-            .bg(colors.ghost_element_background)
-            .rounded_md()
-            .when(selected, |this| this.bg(colors.ghost_element_selected))
-            .hover(|this| this.bg(colors.ghost_element_hover))
-            .child(
+        Some(
+            ListItem::new(ix).inset(true).selected(selected).child(
                 v_stack()
                     .child(HighlightedLabel::new(file_name, file_name_positions))
                     .child(HighlightedLabel::new(full_path, full_path_positions)),
-            )
+            ),
+        )
     }
 }
 
@@ -778,18 +766,49 @@ mod tests {
         let (picker, workspace, cx) = build_find_picker(project, cx);
 
         cx.simulate_input("bna");
-
         picker.update(cx, |picker, _| {
             assert_eq!(picker.delegate.matches.len(), 2);
         });
-
         cx.dispatch_action(SelectNext);
         cx.dispatch_action(Confirm);
-
         cx.read(|cx| {
             let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
             assert_eq!(active_editor.read(cx).title(cx), "bandana");
         });
+
+        for bandana_query in [
+            "bandana",
+            " bandana",
+            "bandana ",
+            " bandana ",
+            " ndan ",
+            " band ",
+        ] {
+            picker
+                .update(cx, |picker, cx| {
+                    picker
+                        .delegate
+                        .update_matches(bandana_query.to_string(), cx)
+                })
+                .await;
+            picker.update(cx, |picker, _| {
+                assert_eq!(
+                    picker.delegate.matches.len(),
+                    1,
+                    "Wrong number of matches for bandana query '{bandana_query}'"
+                );
+            });
+            cx.dispatch_action(SelectNext);
+            cx.dispatch_action(Confirm);
+            cx.read(|cx| {
+                let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
+                assert_eq!(
+                    active_editor.read(cx).title(cx),
+                    "bandana",
+                    "Wrong match for bandana query '{bandana_query}'"
+                );
+            });
+        }
     }
 
     #[gpui::test]

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

@@ -1,13 +1,13 @@
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
-    Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
+    actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
+    FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
+    WindowContext,
 };
 use text::{Bias, Point};
 use theme::ActiveTheme;
 use ui::{h_stack, v_stack, Color, Label, StyledExt};
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
-use workspace::Workspace;
 
 actions!(Toggle);
 
@@ -25,22 +25,24 @@ pub struct GoToLine {
 
 impl FocusableView for GoToLine {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
-        self.active_editor.focus_handle(cx)
+        self.line_editor.focus_handle(cx)
     }
 }
-impl EventEmitter<Manager> for GoToLine {}
+impl EventEmitter<DismissEvent> for GoToLine {}
 
 impl GoToLine {
-    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
-        workspace.register_action(|workspace, _: &Toggle, cx| {
-            let Some(editor) = workspace
-                .active_item(cx)
-                .and_then(|active_item| active_item.downcast::<Editor>())
-            else {
+    fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
+        let handle = cx.view().downgrade();
+        editor.register_action(move |_: &Toggle, cx| {
+            let Some(editor) = handle.upgrade() else {
                 return;
             };
-
-            workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
+            let Some(workspace) = editor.read(cx).workspace() else {
+                return;
+            };
+            workspace.update(cx, |workspace, cx| {
+                workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx));
+            })
         });
     }
 
@@ -88,7 +90,7 @@ impl GoToLine {
     ) {
         match event {
             // todo!() this isn't working...
-            editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss),
+            editor::EditorEvent::Blurred => cx.emit(DismissEvent),
             editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
             _ => {}
         }
@@ -123,7 +125,7 @@ impl GoToLine {
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(Manager::Dismiss);
+        cx.emit(DismissEvent);
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -140,7 +142,7 @@ impl GoToLine {
             self.prev_scroll_position.take();
         }
 
-        cx.emit(Manager::Dismiss);
+        cx.emit(DismissEvent);
     }
 }
 

crates/gpui2/build.rs πŸ”—

@@ -65,6 +65,8 @@ fn generate_shader_bindings() -> PathBuf {
         "MonochromeSprite".into(),
         "PolychromeSprite".into(),
         "PathSprite".into(),
+        "SurfaceInputIndex".into(),
+        "SurfaceBounds".into(),
     ]);
     config.no_includes = true;
     config.enumeration.prefix_with_name = true;

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

@@ -162,6 +162,7 @@ macro_rules! actions {
 
     ( $name:ident ) => {
         #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
+        #[serde(crate = "gpui::serde")]
         pub struct $name;
     };
 

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

@@ -520,6 +520,10 @@ impl AppContext {
         self.platform.should_auto_hide_scrollbars()
     }
 
+    pub fn restart(&self) {
+        self.platform.restart()
+    }
+
     pub(crate) fn push_effect(&mut self, effect: Effect) {
         match &effect {
             Effect::Notify { emitter } => {
@@ -580,7 +584,7 @@ impl AppContext {
             .windows
             .iter()
             .filter_map(|(_, window)| {
-                let window = window.as_ref().unwrap();
+                let window = window.as_ref()?;
                 if window.dirty {
                     Some(window.handle.clone())
                 } else {
@@ -1049,7 +1053,9 @@ impl Context for AppContext {
             let root_view = window.root_view.clone().unwrap();
             let result = update(root_view, &mut WindowContext::new(cx, &mut window));
 
-            if !window.removed {
+            if window.removed {
+                cx.windows.remove(handle.id);
+            } else {
                 cx.windows
                     .get_mut(handle.id)
                     .ok_or_else(|| anyhow!("window not found"))?

crates/gpui2/src/app/async_context.rs πŸ”—

@@ -1,7 +1,7 @@
 use crate::{
-    AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
-    ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext,
-    VisualContext, WindowContext, WindowHandle,
+    AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent,
+    FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View,
+    ViewContext, VisualContext, WindowContext, WindowHandle,
 };
 use anyhow::{anyhow, Context as _};
 use derive_more::{Deref, DerefMut};
@@ -325,8 +325,7 @@ impl VisualContext for AsyncWindowContext {
     where
         V: crate::ManagedView,
     {
-        self.window.update(self, |_, cx| {
-            view.update(cx, |_, cx| cx.emit(Manager::Dismiss))
-        })
+        self.window
+            .update(self, |_, cx| view.update(cx, |_, cx| cx.emit(DismissEvent)))
     }
 }

crates/gpui2/src/app/test_context.rs πŸ”—

@@ -611,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
     {
         self.window
             .update(self.cx, |_, cx| {
-                view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss))
+                view.update(cx, |_, cx| cx.emit(crate::DismissEvent))
             })
             .unwrap()
     }

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

@@ -111,7 +111,7 @@ pub struct Component<C> {
 
 pub struct CompositeElementState<C: RenderOnce> {
     rendered_element: Option<<C::Rendered as IntoElement>::Element>,
-    rendered_element_state: <<C::Rendered as IntoElement>::Element as Element>::State,
+    rendered_element_state: Option<<<C::Rendered as IntoElement>::Element as Element>::State>,
 }
 
 impl<C> Component<C> {
@@ -131,20 +131,40 @@ impl<C: RenderOnce> Element for Component<C> {
         cx: &mut WindowContext,
     ) -> (LayoutId, Self::State) {
         let mut element = self.component.take().unwrap().render(cx).into_element();
-        let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
-        let state = CompositeElementState {
-            rendered_element: Some(element),
-            rendered_element_state: state,
-        };
-        (layout_id, state)
+        if let Some(element_id) = element.element_id() {
+            let layout_id =
+                cx.with_element_state(element_id, |state, cx| element.layout(state, cx));
+            let state = CompositeElementState {
+                rendered_element: Some(element),
+                rendered_element_state: None,
+            };
+            (layout_id, state)
+        } else {
+            let (layout_id, state) =
+                element.layout(state.and_then(|s| s.rendered_element_state), cx);
+            let state = CompositeElementState {
+                rendered_element: Some(element),
+                rendered_element_state: Some(state),
+            };
+            (layout_id, state)
+        }
     }
 
     fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
-        state
-            .rendered_element
-            .take()
-            .unwrap()
-            .paint(bounds, &mut state.rendered_element_state, cx);
+        let element = state.rendered_element.take().unwrap();
+        if let Some(element_id) = element.element_id() {
+            cx.with_element_state(element_id, |element_state, cx| {
+                let mut element_state = element_state.unwrap();
+                element.paint(bounds, &mut element_state, cx);
+                ((), element_state)
+            });
+        } else {
+            element.paint(
+                bounds,
+                &mut state.rendered_element_state.as_mut().unwrap(),
+                cx,
+            );
+        }
     }
 }
 

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

@@ -3,7 +3,8 @@ use crate::{
     BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle,
     IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent,
     MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent,
-    SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext,
+    SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility,
+    WindowContext,
 };
 use collections::HashMap;
 use refineable::Refineable;
@@ -11,6 +12,7 @@ use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
     cell::RefCell,
+    cmp::Ordering,
     fmt::Debug,
     mem,
     rc::Rc,
@@ -84,7 +86,7 @@ pub trait InteractiveElement: Sized + Element {
             move |event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble
                     && event.button == button
-                    && bounds.contains_point(&event.position)
+                    && bounds.visibly_contains(&event.position, cx)
                 {
                     (listener)(event, cx)
                 }
@@ -99,7 +101,7 @@ pub trait InteractiveElement: Sized + Element {
     ) -> Self {
         self.interactivity().mouse_down_listeners.push(Box::new(
             move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
                     (listener)(event, cx)
                 }
             },
@@ -117,7 +119,7 @@ pub trait InteractiveElement: Sized + Element {
             .push(Box::new(move |event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble
                     && event.button == button
-                    && bounds.contains_point(&event.position)
+                    && bounds.visibly_contains(&event.position, cx)
                 {
                     (listener)(event, cx)
                 }
@@ -132,7 +134,7 @@ pub trait InteractiveElement: Sized + Element {
         self.interactivity()
             .mouse_up_listeners
             .push(Box::new(move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
                     (listener)(event, cx)
                 }
             }));
@@ -145,7 +147,8 @@ pub trait InteractiveElement: Sized + Element {
     ) -> Self {
         self.interactivity().mouse_down_listeners.push(Box::new(
             move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Capture && !bounds.visibly_contains(&event.position, cx)
+                {
                     (listener)(event, cx)
                 }
             },
@@ -163,7 +166,7 @@ pub trait InteractiveElement: Sized + Element {
             .push(Box::new(move |event, bounds, phase, cx| {
                 if phase == DispatchPhase::Capture
                     && event.button == button
-                    && !bounds.contains_point(&event.position)
+                    && !bounds.visibly_contains(&event.position, cx)
                 {
                     (listener)(event, cx);
                 }
@@ -177,7 +180,7 @@ pub trait InteractiveElement: Sized + Element {
     ) -> Self {
         self.interactivity().mouse_move_listeners.push(Box::new(
             move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
                     (listener)(event, cx);
                 }
             },
@@ -191,7 +194,7 @@ pub trait InteractiveElement: Sized + Element {
     ) -> Self {
         self.interactivity().scroll_wheel_listeners.push(Box::new(
             move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
                     (listener)(event, cx);
                 }
             },
@@ -355,6 +358,11 @@ pub trait StatefulInteractiveElement: InteractiveElement {
         self
     }
 
+    fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
+        self.interactivity().scroll_handle = Some(scroll_handle.clone());
+        self
+    }
+
     fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
     where
         Self: Sized,
@@ -526,15 +534,15 @@ pub type FocusListeners = SmallVec<[FocusListener; 2]>;
 pub type FocusListener = Box<dyn Fn(&FocusHandle, &FocusEvent, &mut WindowContext) + 'static>;
 
 pub type MouseDownListener =
-    Box<dyn Fn(&MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
+    Box<dyn Fn(&MouseDownEvent, &InteractiveBounds, DispatchPhase, &mut WindowContext) + 'static>;
 pub type MouseUpListener =
-    Box<dyn Fn(&MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
+    Box<dyn Fn(&MouseUpEvent, &InteractiveBounds, DispatchPhase, &mut WindowContext) + 'static>;
 
 pub type MouseMoveListener =
-    Box<dyn Fn(&MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
+    Box<dyn Fn(&MouseMoveEvent, &InteractiveBounds, DispatchPhase, &mut WindowContext) + 'static>;
 
 pub type ScrollWheelListener =
-    Box<dyn Fn(&ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
+    Box<dyn Fn(&ScrollWheelEvent, &InteractiveBounds, DispatchPhase, &mut WindowContext) + 'static>;
 
 pub type ClickListener = Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>;
 
@@ -624,6 +632,26 @@ impl Element for Div {
         let mut child_max = Point::default();
         let content_size = if element_state.child_layout_ids.is_empty() {
             bounds.size
+        } else if let Some(scroll_handle) = self.interactivity.scroll_handle.as_ref() {
+            let mut state = scroll_handle.0.borrow_mut();
+            state.child_bounds = Vec::with_capacity(element_state.child_layout_ids.len());
+            state.bounds = bounds;
+            let requested = state.requested_scroll_top.take();
+
+            for (ix, child_layout_id) in element_state.child_layout_ids.iter().enumerate() {
+                let child_bounds = cx.layout_bounds(*child_layout_id);
+                child_min = child_min.min(&child_bounds.origin);
+                child_max = child_max.max(&child_bounds.lower_right());
+                state.child_bounds.push(child_bounds);
+
+                if let Some(requested) = requested.as_ref() {
+                    if requested.0 == ix {
+                        *state.offset.borrow_mut() =
+                            bounds.origin - (child_bounds.origin - point(px(0.), requested.1));
+                    }
+                }
+            }
+            (child_max - child_min).into()
         } else {
             for child_layout_id in &element_state.child_layout_ids {
                 let child_bounds = cx.layout_bounds(*child_layout_id);
@@ -694,6 +722,7 @@ pub struct Interactivity {
     pub key_context: KeyContext,
     pub focusable: bool,
     pub tracked_focus_handle: Option<FocusHandle>,
+    pub scroll_handle: Option<ScrollHandle>,
     pub focus_listeners: FocusListeners,
     pub group: Option<SharedString>,
     pub base_style: StyleRefinement,
@@ -719,6 +748,18 @@ pub struct Interactivity {
     pub tooltip_builder: Option<TooltipBuilder>,
 }
 
+#[derive(Clone)]
+pub struct InteractiveBounds {
+    pub bounds: Bounds<Pixels>,
+    pub stacking_order: StackingOrder,
+}
+
+impl InteractiveBounds {
+    pub fn visibly_contains(&self, point: &Point<Pixels>, cx: &WindowContext) -> bool {
+        self.bounds.contains_point(point) && cx.was_top_layer(&point, &self.stacking_order)
+    }
+}
+
 impl Interactivity {
     pub fn layout(
         &mut self,
@@ -740,6 +781,10 @@ impl Interactivity {
             });
         }
 
+        if let Some(scroll_handle) = self.scroll_handle.as_ref() {
+            element_state.scroll_offset = Some(scroll_handle.0.borrow().offset.clone());
+        }
+
         let style = self.compute_style(None, &mut element_state, cx);
         let layout_id = f(style, cx);
         (layout_id, element_state)
@@ -755,34 +800,52 @@ impl Interactivity {
     ) {
         let style = self.compute_style(Some(bounds), element_state, cx);
 
+        if style
+            .background
+            .as_ref()
+            .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
+        {
+            cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds))
+        }
+
+        let interactive_bounds = Rc::new(InteractiveBounds {
+            bounds: bounds.intersect(&cx.content_mask().bounds),
+            stacking_order: cx.stacking_order().clone(),
+        });
+
         if let Some(mouse_cursor) = style.mouse_cursor {
-            let hovered = bounds.contains_point(&cx.mouse_position());
+            let mouse_position = &cx.mouse_position();
+            let hovered = interactive_bounds.visibly_contains(mouse_position, cx);
             if hovered {
                 cx.set_cursor_style(mouse_cursor);
             }
         }
 
         for listener in self.mouse_down_listeners.drain(..) {
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
-                listener(event, &bounds, phase, cx);
+                listener(event, &*interactive_bounds, phase, cx);
             })
         }
 
         for listener in self.mouse_up_listeners.drain(..) {
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
-                listener(event, &bounds, phase, cx);
+                listener(event, &*interactive_bounds, phase, cx);
             })
         }
 
         for listener in self.mouse_move_listeners.drain(..) {
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
-                listener(event, &bounds, phase, cx);
+                listener(event, &*interactive_bounds, phase, cx);
             })
         }
 
         for listener in self.scroll_wheel_listeners.drain(..) {
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
-                listener(event, &bounds, phase, cx);
+                listener(event, &*interactive_bounds, phase, cx);
             })
         }
 
@@ -803,8 +866,9 @@ impl Interactivity {
         }
 
         if self.hover_style.is_some()
-            || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty())
+            || cx.active_drag.is_some() && !self.drag_over_styles.is_empty()
         {
+            let bounds = bounds.intersect(&cx.content_mask().bounds);
             let hovered = bounds.contains_point(&cx.mouse_position());
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                 if phase == DispatchPhase::Capture {
@@ -817,8 +881,11 @@ impl Interactivity {
 
         if cx.active_drag.is_some() {
             let drop_listeners = mem::take(&mut self.drop_listeners);
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble
+                    && interactive_bounds.visibly_contains(&event.position, &cx)
+                {
                     if let Some(drag_state_type) =
                         cx.active_drag.as_ref().map(|drag| drag.view.entity_type())
                     {
@@ -847,6 +914,7 @@ impl Interactivity {
             if let Some(mouse_down) = mouse_down {
                 if let Some(drag_listener) = drag_listener {
                     let active_state = element_state.clicked_state.clone();
+                    let interactive_bounds = interactive_bounds.clone();
 
                     cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                         if cx.active_drag.is_some() {
@@ -854,7 +922,7 @@ impl Interactivity {
                                 cx.notify();
                             }
                         } else if phase == DispatchPhase::Bubble
-                            && bounds.contains_point(&event.position)
+                            && interactive_bounds.visibly_contains(&event.position, cx)
                             && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
                         {
                             *active_state.borrow_mut() = ElementClickedState::default();
@@ -867,8 +935,11 @@ impl Interactivity {
                     });
                 }
 
+                let interactive_bounds = interactive_bounds.clone();
                 cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
-                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    if phase == DispatchPhase::Bubble
+                        && interactive_bounds.visibly_contains(&event.position, cx)
+                    {
                         let mouse_click = ClickEvent {
                             down: mouse_down.clone(),
                             up: event.clone(),
@@ -881,8 +952,11 @@ impl Interactivity {
                     cx.notify();
                 });
             } else {
+                let interactive_bounds = interactive_bounds.clone();
                 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
-                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    if phase == DispatchPhase::Bubble
+                        && interactive_bounds.visibly_contains(&event.position, cx)
+                    {
                         *pending_mouse_down.borrow_mut() = Some(event.clone());
                         cx.notify();
                     }
@@ -893,13 +967,14 @@ impl Interactivity {
         if let Some(hover_listener) = self.hover_listener.take() {
             let was_hovered = element_state.hover_state.clone();
             let has_mouse_down = element_state.pending_mouse_down.clone();
+            let interactive_bounds = interactive_bounds.clone();
 
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
                     return;
                 }
-                let is_hovered =
-                    bounds.contains_point(&event.position) && has_mouse_down.borrow().is_none();
+                let is_hovered = interactive_bounds.visibly_contains(&event.position, cx)
+                    && has_mouse_down.borrow().is_none();
                 let mut was_hovered = was_hovered.borrow_mut();
 
                 if is_hovered != was_hovered.clone() {
@@ -914,14 +989,15 @@ impl Interactivity {
         if let Some(tooltip_builder) = self.tooltip_builder.take() {
             let active_tooltip = element_state.active_tooltip.clone();
             let pending_mouse_down = element_state.pending_mouse_down.clone();
+            let interactive_bounds = interactive_bounds.clone();
 
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
                     return;
                 }
 
-                let is_hovered =
-                    bounds.contains_point(&event.position) && pending_mouse_down.borrow().is_none();
+                let is_hovered = interactive_bounds.visibly_contains(&event.position, cx)
+                    && pending_mouse_down.borrow().is_none();
                 if !is_hovered {
                     active_tooltip.borrow_mut().take();
                     return;
@@ -979,11 +1055,12 @@ impl Interactivity {
                 .group_active_style
                 .as_ref()
                 .and_then(|group_active| GroupBounds::get(&group_active.group, cx));
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| {
                 if phase == DispatchPhase::Bubble {
                     let group = active_group_bounds
                         .map_or(false, |bounds| bounds.contains_point(&down.position));
-                    let element = bounds.contains_point(&down.position);
+                    let element = interactive_bounds.visibly_contains(&down.position, cx);
                     if group || element {
                         *active_state.borrow_mut() = ElementClickedState { group, element };
                         cx.notify();
@@ -1000,9 +1077,12 @@ impl Interactivity {
                 .clone();
             let line_height = cx.line_height();
             let scroll_max = (content_size - bounds.size).max(&Size::default());
+            let interactive_bounds = interactive_bounds.clone();
 
             cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble
+                    && interactive_bounds.visibly_contains(&event.position, cx)
+                {
                     let mut scroll_offset = scroll_offset.borrow_mut();
                     let old_scroll_offset = *scroll_offset;
                     let delta = event.delta.pixel_delta(line_height);
@@ -1093,19 +1173,22 @@ impl Interactivity {
             let mouse_position = cx.mouse_position();
             if let Some(group_hover) = self.group_hover_style.as_ref() {
                 if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
-                    if group_bounds.contains_point(&mouse_position) {
+                    if group_bounds.contains_point(&mouse_position)
+                        && cx.was_top_layer(&mouse_position, cx.stacking_order())
+                    {
                         style.refine(&group_hover.style);
                     }
                 }
             }
-            // if self.hover_style.is_some() {
-            if bounds.contains_point(&mouse_position) {
-                // eprintln!("div hovered {bounds:?} {mouse_position:?}");
-                style.refine(&self.hover_style);
-            } else {
-                // eprintln!("div NOT hovered {bounds:?} {mouse_position:?}");
+            if self.hover_style.is_some() {
+                if bounds
+                    .intersect(&cx.content_mask().bounds)
+                    .contains_point(&mouse_position)
+                    && cx.was_top_layer(&mouse_position, cx.stacking_order())
+                {
+                    style.refine(&self.hover_style);
+                }
             }
-            // }
 
             if let Some(drag) = cx.active_drag.take() {
                 for (state_type, group_drag_style) in &self.group_drag_over_styles {
@@ -1120,7 +1203,9 @@ impl Interactivity {
 
                 for (state_type, drag_over_style) in &self.drag_over_styles {
                     if *state_type == drag.view.entity_type()
-                        && bounds.contains_point(&mouse_position)
+                        && bounds
+                            .intersect(&cx.content_mask().bounds)
+                            .contains_point(&mouse_position)
                     {
                         style.refine(drag_over_style);
                     }
@@ -1152,6 +1237,7 @@ impl Default for Interactivity {
             key_context: KeyContext::default(),
             focusable: false,
             tracked_focus_handle: None,
+            scroll_handle: None,
             focus_listeners: SmallVec::default(),
             // scroll_offset: Point::default(),
             group: None,
@@ -1375,3 +1461,83 @@ where
         self.element.children_mut()
     }
 }
+
+#[derive(Default)]
+struct ScrollHandleState {
+    // not great to have the nested rc's...
+    offset: Rc<RefCell<Point<Pixels>>>,
+    bounds: Bounds<Pixels>,
+    child_bounds: Vec<Bounds<Pixels>>,
+    requested_scroll_top: Option<(usize, Pixels)>,
+}
+
+#[derive(Clone)]
+pub struct ScrollHandle(Rc<RefCell<ScrollHandleState>>);
+
+impl ScrollHandle {
+    pub fn new() -> Self {
+        Self(Rc::default())
+    }
+
+    pub fn offset(&self) -> Point<Pixels> {
+        self.0.borrow().offset.borrow().clone()
+    }
+
+    pub fn top_item(&self) -> usize {
+        let state = self.0.borrow();
+        let top = state.bounds.top() - state.offset.borrow().y;
+
+        match state.child_bounds.binary_search_by(|bounds| {
+            if top < bounds.top() {
+                Ordering::Greater
+            } else if top > bounds.bottom() {
+                Ordering::Less
+            } else {
+                Ordering::Equal
+            }
+        }) {
+            Ok(ix) => ix,
+            Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)),
+        }
+    }
+
+    pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
+        self.0.borrow().child_bounds.get(ix).cloned()
+    }
+
+    /// scroll_to_item scrolls the minimal amount to ensure that the item is
+    /// fully visible
+    pub fn scroll_to_item(&self, ix: usize) {
+        let state = self.0.borrow();
+
+        let Some(bounds) = state.child_bounds.get(ix) else {
+            return;
+        };
+
+        let scroll_offset = state.offset.borrow().y;
+
+        if bounds.top() + scroll_offset < state.bounds.top() {
+            state.offset.borrow_mut().y = state.bounds.top() - bounds.top();
+        } else if bounds.bottom() + scroll_offset > state.bounds.bottom() {
+            state.offset.borrow_mut().y = state.bounds.bottom() - bounds.bottom();
+        }
+    }
+
+    pub fn logical_scroll_top(&self) -> (usize, Pixels) {
+        let ix = self.top_item();
+        let state = self.0.borrow();
+
+        if let Some(child_bounds) = state.child_bounds.get(ix) {
+            (
+                ix,
+                child_bounds.top() + state.offset.borrow().y - state.bounds.top(),
+            )
+        } else {
+            (ix, px(0.))
+        }
+    }
+
+    pub fn set_logical_scroll_top(&self, ix: usize, px: Pixels) {
+        self.0.borrow_mut().requested_scroll_top = Some((ix, px));
+    }
+}

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

@@ -1,30 +1,67 @@
+use std::sync::Arc;
+
 use crate::{
-    Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, IntoElement,
-    LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
+    point, size, Bounds, DevicePixels, Element, ImageData, InteractiveElement,
+    InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedString, Size,
+    StyleRefinement, Styled, WindowContext,
 };
 use futures::FutureExt;
+use media::core_video::CVImageBuffer;
 use util::ResultExt;
 
+#[derive(Clone, Debug)]
+pub enum ImageSource {
+    /// Image content will be loaded from provided URI at render time.
+    Uri(SharedString),
+    Data(Arc<ImageData>),
+    Surface(CVImageBuffer),
+}
+
+impl From<SharedString> for ImageSource {
+    fn from(value: SharedString) -> Self {
+        Self::Uri(value)
+    }
+}
+
+impl From<&'static str> for ImageSource {
+    fn from(uri: &'static str) -> Self {
+        Self::Uri(uri.into())
+    }
+}
+
+impl From<String> for ImageSource {
+    fn from(uri: String) -> Self {
+        Self::Uri(uri.into())
+    }
+}
+
+impl From<Arc<ImageData>> for ImageSource {
+    fn from(value: Arc<ImageData>) -> Self {
+        Self::Data(value)
+    }
+}
+
+impl From<CVImageBuffer> for ImageSource {
+    fn from(value: CVImageBuffer) -> Self {
+        Self::Surface(value)
+    }
+}
+
 pub struct Img {
     interactivity: Interactivity,
-    uri: Option<SharedString>,
+    source: ImageSource,
     grayscale: bool,
 }
 
-pub fn img() -> Img {
+pub fn img(source: impl Into<ImageSource>) -> Img {
     Img {
         interactivity: Interactivity::default(),
-        uri: None,
+        source: source.into(),
         grayscale: false,
     }
 }
 
 impl Img {
-    pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
-        self.uri = Some(uri.into());
-        self
-    }
-
     pub fn grayscale(mut self, grayscale: bool) -> Self {
         self.grayscale = grayscale;
         self
@@ -39,9 +76,8 @@ impl Element for Img {
         element_state: Option<Self::State>,
         cx: &mut WindowContext,
     ) -> (LayoutId, Self::State) {
-        self.interactivity.layout(element_state, cx, |style, cx| {
-            cx.request_layout(&style, None)
-        })
+        self.interactivity
+            .layout(element_state, cx, |style, cx| cx.request_layout(&style, []))
     }
 
     fn paint(
@@ -56,31 +92,43 @@ impl Element for Img {
             element_state,
             cx,
             |style, _scroll_offset, cx| {
-                let corner_radii = style.corner_radii;
-
-                if let Some(uri) = self.uri.clone() {
-                    // eprintln!(">>> image_cache.get({uri}");
-                    let image_future = cx.image_cache.get(uri.clone());
-                    // eprintln!("<<< image_cache.get({uri}");
-                    if let Some(data) = image_future
-                        .clone()
-                        .now_or_never()
-                        .and_then(|result| result.ok())
-                    {
-                        let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
-                        cx.with_z_index(1, |cx| {
-                            cx.paint_image(bounds, corner_radii, data, self.grayscale)
-                                .log_err()
-                        });
-                    } else {
-                        cx.spawn(|mut cx| async move {
-                            if image_future.await.ok().is_some() {
-                                cx.on_next_frame(|cx| cx.notify());
+                let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
+                cx.with_z_index(1, |cx| {
+                    match self.source {
+                        ImageSource::Uri(uri) => {
+                            let image_future = cx.image_cache.get(uri.clone());
+                            if let Some(data) = image_future
+                                .clone()
+                                .now_or_never()
+                                .and_then(|result| result.ok())
+                            {
+                                let new_bounds = preserve_aspect_ratio(bounds, data.size());
+                                cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
+                                    .log_err();
+                            } else {
+                                cx.spawn(|mut cx| async move {
+                                    if image_future.await.ok().is_some() {
+                                        cx.on_next_frame(|cx| cx.notify());
+                                    }
+                                })
+                                .detach();
                             }
-                        })
-                        .detach()
-                    }
-                }
+                        }
+
+                        ImageSource::Data(data) => {
+                            let new_bounds = preserve_aspect_ratio(bounds, data.size());
+                            cx.paint_image(new_bounds, corner_radii, data, self.grayscale)
+                                .log_err();
+                        }
+
+                        ImageSource::Surface(surface) => {
+                            let size = size(surface.width().into(), surface.height().into());
+                            let new_bounds = preserve_aspect_ratio(bounds, size);
+                            // TODO: Add support for corner_radii and grayscale.
+                            cx.paint_surface(new_bounds, surface);
+                        }
+                    };
+                });
             },
         )
     }
@@ -109,3 +157,29 @@ impl InteractiveElement for Img {
         &mut self.interactivity
     }
 }
+
+fn preserve_aspect_ratio(bounds: Bounds<Pixels>, image_size: Size<DevicePixels>) -> Bounds<Pixels> {
+    let image_size = image_size.map(|dimension| Pixels::from(u32::from(dimension)));
+    let image_ratio = image_size.width / image_size.height;
+    let bounds_ratio = bounds.size.width / bounds.size.height;
+
+    let new_size = if bounds_ratio > image_ratio {
+        size(
+            image_size.width * (bounds.size.height / image_size.height),
+            bounds.size.height,
+        )
+    } else {
+        size(
+            bounds.size.width,
+            image_size.height * (bounds.size.width / image_size.width),
+        )
+    };
+
+    Bounds {
+        origin: point(
+            bounds.origin.x + (bounds.size.width - new_size.width) / 2.0,
+            bounds.origin.y + (bounds.size.height - new_size.height) / 2.0,
+        ),
+        size: new_size,
+    }
+}

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

@@ -144,9 +144,11 @@ impl Element for Overlay {
         }
 
         cx.with_element_offset(desired.origin - bounds.origin, |cx| {
-            for child in self.children {
-                child.paint(cx);
-            }
+            cx.break_content_mask(|cx| {
+                for child in self.children {
+                    child.paint(cx);
+                }
+            })
         })
     }
 }

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

@@ -287,7 +287,9 @@ impl TextState {
 pub struct InteractiveText {
     element_id: ElementId,
     text: StyledText,
-    click_listener: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
+    click_listener:
+        Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
+    clickable_ranges: Vec<Range<usize>>,
 }
 
 struct InteractiveTextClickEvent {
@@ -306,6 +308,7 @@ impl InteractiveText {
             element_id: id.into(),
             text,
             click_listener: None,
+            clickable_ranges: Vec::new(),
         }
     }
 
@@ -314,7 +317,7 @@ impl InteractiveText {
         ranges: Vec<Range<usize>>,
         listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
     ) -> Self {
-        self.click_listener = Some(Box::new(move |event, cx| {
+        self.click_listener = Some(Box::new(move |ranges, event, cx| {
             for (range_ix, range) in ranges.iter().enumerate() {
                 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
                 {
@@ -322,6 +325,7 @@ impl InteractiveText {
                 }
             }
         }));
+        self.clickable_ranges = ranges;
         self
     }
 }
@@ -356,6 +360,19 @@ impl Element for InteractiveText {
 
     fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
         if let Some(click_listener) = self.click_listener {
+            if let Some(ix) = state
+                .text_state
+                .index_for_position(bounds, cx.mouse_position())
+            {
+                if self
+                    .clickable_ranges
+                    .iter()
+                    .any(|range| range.contains(&ix))
+                {
+                    cx.set_cursor_style(crate::CursorStyle::PointingHand)
+                }
+            }
+
             let text_state = state.text_state.clone();
             let mouse_down = state.mouse_down_index.clone();
             if let Some(mouse_down_index) = mouse_down.get() {
@@ -365,6 +382,7 @@ impl Element for InteractiveText {
                             text_state.index_for_position(bounds, event.position)
                         {
                             click_listener(
+                                &self.clickable_ranges,
                                 InteractiveTextClickEvent {
                                     mouse_down_index,
                                     mouse_up_index,

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

@@ -9,7 +9,7 @@ use taffy::style::Overflow;
 
 /// uniform_list provides lazy rendering for a set of items that are of uniform height.
 /// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
-/// uniform_list will only render the visibile subset of items.
+/// uniform_list will only render the visible subset of items.
 pub fn uniform_list<I, R, V>(
     view: View<V>,
     id: I,
@@ -173,7 +173,7 @@ impl Element for UniformList {
         let item_size = element_state.item_size;
         let content_size = Size {
             width: padded_bounds.size.width,
-            height: item_size.height * self.item_count,
+            height: item_size.height * self.item_count + padding.top + padding.bottom,
         };
 
         let shared_scroll_offset = element_state
@@ -221,9 +221,7 @@ impl Element for UniformList {
 
                         let items = (self.render_items)(visible_range.clone(), cx);
                         cx.with_z_index(1, |cx| {
-                            let content_mask = ContentMask {
-                                bounds: padded_bounds,
-                            };
+                            let content_mask = ContentMask { bounds };
                             cx.with_content_mask(Some(content_mask), |cx| {
                                 for (item, ix) in items.into_iter().zip(visible_range) {
                                     let item_origin = padded_bounds.origin

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

@@ -740,7 +740,7 @@ impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {}
     Deserialize,
 )]
 #[repr(transparent)]
-pub struct Pixels(pub(crate) f32);
+pub struct Pixels(pub f32);
 
 impl std::ops::Div for Pixels {
     type Output = f32;
@@ -905,6 +905,12 @@ impl From<Pixels> for usize {
     }
 }
 
+impl From<usize> for Pixels {
+    fn from(pixels: usize) -> Self {
+        Pixels(pixels as f32)
+    }
+}
+
 #[derive(
     Add, AddAssign, Clone, Copy, Default, Div, Eq, Hash, Ord, PartialEq, PartialOrd, Sub, SubAssign,
 )]
@@ -959,6 +965,18 @@ impl From<u64> for DevicePixels {
     }
 }
 
+impl From<DevicePixels> for usize {
+    fn from(device_pixels: DevicePixels) -> Self {
+        device_pixels.0 as usize
+    }
+}
+
+impl From<usize> for DevicePixels {
+    fn from(device_pixels: usize) -> Self {
+        DevicePixels(device_pixels as i32)
+    }
+}
+
 #[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)]
 #[repr(transparent)]
 pub struct ScaledPixels(pub(crate) f32);
@@ -1034,7 +1052,7 @@ impl sqlez::bindable::Bind for GlobalPixels {
 }
 
 #[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)]
-pub struct Rems(f32);
+pub struct Rems(pub f32);
 
 impl Mul<Pixels> for Rems {
     type Output = Pixels;

crates/gpui2/src/platform/mac/metal_renderer.rs πŸ”—

@@ -1,7 +1,7 @@
 use crate::{
     point, size, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, ContentMask, DevicePixels,
     Hsla, MetalAtlas, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch,
-    Quad, ScaledPixels, Scene, Shadow, Size, Underline,
+    Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
 };
 use cocoa::{
     base::{NO, YES},
@@ -9,6 +9,9 @@ use cocoa::{
     quartzcore::AutoresizingMask,
 };
 use collections::HashMap;
+use core_foundation::base::TCFType;
+use foreign_types::ForeignType;
+use media::core_video::CVMetalTextureCache;
 use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
 use objc::{self, msg_send, sel, sel_impl};
 use smallvec::SmallVec;
@@ -27,9 +30,11 @@ pub(crate) struct MetalRenderer {
     underlines_pipeline_state: metal::RenderPipelineState,
     monochrome_sprites_pipeline_state: metal::RenderPipelineState,
     polychrome_sprites_pipeline_state: metal::RenderPipelineState,
+    surfaces_pipeline_state: metal::RenderPipelineState,
     unit_vertices: metal::Buffer,
     instances: metal::Buffer,
     sprite_atlas: Arc<MetalAtlas>,
+    core_video_texture_cache: CVMetalTextureCache,
 }
 
 impl MetalRenderer {
@@ -143,6 +148,14 @@ impl MetalRenderer {
             "polychrome_sprite_fragment",
             MTLPixelFormat::BGRA8Unorm,
         );
+        let surfaces_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "surfaces",
+            "surface_vertex",
+            "surface_fragment",
+            MTLPixelFormat::BGRA8Unorm,
+        );
 
         let command_queue = device.new_command_queue();
         let sprite_atlas = Arc::new(MetalAtlas::new(device.clone()));
@@ -157,9 +170,11 @@ impl MetalRenderer {
             underlines_pipeline_state,
             monochrome_sprites_pipeline_state,
             polychrome_sprites_pipeline_state,
+            surfaces_pipeline_state,
             unit_vertices,
             instances,
             sprite_atlas,
+            core_video_texture_cache: CVMetalTextureCache::new(device.as_ptr()).unwrap(),
         }
     }
 
@@ -268,6 +283,14 @@ impl MetalRenderer {
                         command_encoder,
                     );
                 }
+                PrimitiveBatch::Surfaces(surfaces) => {
+                    self.draw_surfaces(
+                        surfaces,
+                        &mut instance_offset,
+                        viewport_size,
+                        command_encoder,
+                    );
+                }
             }
         }
 
@@ -793,6 +816,102 @@ impl MetalRenderer {
         );
         *offset = next_offset;
     }
+
+    fn draw_surfaces(
+        &mut self,
+        surfaces: &[Surface],
+        offset: &mut usize,
+        viewport_size: Size<DevicePixels>,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        command_encoder.set_render_pipeline_state(&self.surfaces_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            SurfaceInputIndex::Vertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_bytes(
+            SurfaceInputIndex::ViewportSize as u64,
+            mem::size_of_val(&viewport_size) as u64,
+            &viewport_size as *const Size<DevicePixels> as *const _,
+        );
+
+        for surface in surfaces {
+            let texture_size = size(
+                DevicePixels::from(surface.image_buffer.width() as i32),
+                DevicePixels::from(surface.image_buffer.height() as i32),
+            );
+
+            assert_eq!(
+                surface.image_buffer.pixel_format_type(),
+                media::core_video::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
+            );
+
+            let y_texture = self
+                .core_video_texture_cache
+                .create_texture_from_image(
+                    surface.image_buffer.as_concrete_TypeRef(),
+                    ptr::null(),
+                    MTLPixelFormat::R8Unorm,
+                    surface.image_buffer.plane_width(0),
+                    surface.image_buffer.plane_height(0),
+                    0,
+                )
+                .unwrap();
+            let cb_cr_texture = self
+                .core_video_texture_cache
+                .create_texture_from_image(
+                    surface.image_buffer.as_concrete_TypeRef(),
+                    ptr::null(),
+                    MTLPixelFormat::RG8Unorm,
+                    surface.image_buffer.plane_width(1),
+                    surface.image_buffer.plane_height(1),
+                    1,
+                )
+                .unwrap();
+
+            align_offset(offset);
+            let next_offset = *offset + mem::size_of::<Surface>();
+            assert!(
+                next_offset <= INSTANCE_BUFFER_SIZE,
+                "instance buffer exhausted"
+            );
+
+            command_encoder.set_vertex_buffer(
+                SurfaceInputIndex::Surfaces as u64,
+                Some(&self.instances),
+                *offset as u64,
+            );
+            command_encoder.set_vertex_bytes(
+                SurfaceInputIndex::TextureSize as u64,
+                mem::size_of_val(&texture_size) as u64,
+                &texture_size as *const Size<DevicePixels> as *const _,
+            );
+            command_encoder.set_fragment_texture(
+                SurfaceInputIndex::YTexture as u64,
+                Some(y_texture.as_texture_ref()),
+            );
+            command_encoder.set_fragment_texture(
+                SurfaceInputIndex::CbCrTexture as u64,
+                Some(cb_cr_texture.as_texture_ref()),
+            );
+
+            unsafe {
+                let buffer_contents =
+                    (self.instances.contents() as *mut u8).add(*offset) as *mut SurfaceBounds;
+                ptr::write(
+                    buffer_contents,
+                    SurfaceBounds {
+                        bounds: surface.bounds,
+                        content_mask: surface.content_mask.clone(),
+                    },
+                );
+            }
+
+            command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6);
+            *offset = next_offset;
+        }
+    }
 }
 
 fn build_pipeline_state(
@@ -898,6 +1017,16 @@ enum SpriteInputIndex {
     AtlasTexture = 4,
 }
 
+#[repr(C)]
+enum SurfaceInputIndex {
+    Vertices = 0,
+    Surfaces = 1,
+    ViewportSize = 2,
+    TextureSize = 3,
+    YTexture = 4,
+    CbCrTexture = 5,
+}
+
 #[repr(C)]
 enum PathRasterizationInputIndex {
     Vertices = 0,
@@ -911,3 +1040,10 @@ pub struct PathSprite {
     pub color: Hsla,
     pub tile: AtlasTile,
 }
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+#[repr(C)]
+pub struct SurfaceBounds {
+    pub bounds: Bounds<ScaledPixels>,
+    pub content_mask: ContentMask<ScaledPixels>,
+}

crates/gpui2/src/platform/mac/shaders.metal πŸ”—

@@ -469,6 +469,58 @@ fragment float4 path_sprite_fragment(
   return color;
 }
 
+struct SurfaceVertexOutput {
+  float4 position [[position]];
+  float2 texture_position;
+  float clip_distance [[clip_distance]][4];
+};
+
+struct SurfaceFragmentInput {
+  float4 position [[position]];
+  float2 texture_position;
+};
+
+vertex SurfaceVertexOutput surface_vertex(
+    uint unit_vertex_id [[vertex_id]], uint surface_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(SurfaceInputIndex_Vertices)]],
+    constant SurfaceBounds *surfaces [[buffer(SurfaceInputIndex_Surfaces)]],
+    constant Size_DevicePixels *viewport_size
+    [[buffer(SurfaceInputIndex_ViewportSize)]],
+    constant Size_DevicePixels *texture_size
+    [[buffer(SurfaceInputIndex_TextureSize)]]) {
+  float2 unit_vertex = unit_vertices[unit_vertex_id];
+  SurfaceBounds surface = surfaces[surface_id];
+  float4 device_position =
+      to_device_position(unit_vertex, surface.bounds, viewport_size);
+  float4 clip_distance = distance_from_clip_rect(unit_vertex, surface.bounds,
+                                                 surface.content_mask.bounds);
+  // We are going to copy the whole texture, so the texture position corresponds
+  // to the current vertex of the unit triangle.
+  float2 texture_position = unit_vertex;
+  return SurfaceVertexOutput{
+      device_position,
+      texture_position,
+      {clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
+}
+
+fragment float4 surface_fragment(SurfaceFragmentInput input [[stage_in]],
+                                 texture2d<float> y_texture
+                                 [[texture(SurfaceInputIndex_YTexture)]],
+                                 texture2d<float> cb_cr_texture
+                                 [[texture(SurfaceInputIndex_CbCrTexture)]]) {
+  constexpr sampler texture_sampler(mag_filter::linear, min_filter::linear);
+  const float4x4 ycbcrToRGBTransform =
+      float4x4(float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
+               float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
+               float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
+               float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f));
+  float4 ycbcr = float4(
+      y_texture.sample(texture_sampler, input.texture_position).r,
+      cb_cr_texture.sample(texture_sampler, input.texture_position).rg, 1.0);
+
+  return ycbcrToRGBTransform * ycbcr;
+}
+
 float4 hsla_to_rgba(Hsla hsla) {
   float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
   float s = hsla.s;

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

@@ -683,6 +683,9 @@ impl Drop for MacWindow {
         this.executor
             .spawn(async move {
                 unsafe {
+                    // todo!() this panic()s when you click the red close button
+                    // unless should_close returns false.
+                    // (luckliy in zed it always returns false)
                     window.close();
                 }
             })

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

@@ -25,6 +25,7 @@ pub(crate) struct SceneBuilder {
     underlines: Vec<Underline>,
     monochrome_sprites: Vec<MonochromeSprite>,
     polychrome_sprites: Vec<PolychromeSprite>,
+    surfaces: Vec<Surface>,
 }
 
 impl Default for SceneBuilder {
@@ -38,6 +39,7 @@ impl Default for SceneBuilder {
             underlines: Vec::new(),
             monochrome_sprites: Vec::new(),
             polychrome_sprites: Vec::new(),
+            surfaces: Vec::new(),
         }
     }
 }
@@ -120,6 +122,7 @@ impl SceneBuilder {
                 (PrimitiveKind::PolychromeSprite, ix) => {
                     self.polychrome_sprites[ix].order = draw_order as DrawOrder
                 }
+                (PrimitiveKind::Surface, ix) => self.surfaces[ix].order = draw_order as DrawOrder,
             }
         }
 
@@ -129,6 +132,7 @@ impl SceneBuilder {
         self.underlines.sort_unstable();
         self.monochrome_sprites.sort_unstable();
         self.polychrome_sprites.sort_unstable();
+        self.surfaces.sort_unstable();
 
         Scene {
             shadows: mem::take(&mut self.shadows),
@@ -137,6 +141,7 @@ impl SceneBuilder {
             underlines: mem::take(&mut self.underlines),
             monochrome_sprites: mem::take(&mut self.monochrome_sprites),
             polychrome_sprites: mem::take(&mut self.polychrome_sprites),
+            surfaces: mem::take(&mut self.surfaces),
         }
     }
 
@@ -185,6 +190,10 @@ impl SceneBuilder {
                 sprite.order = layer_id;
                 self.polychrome_sprites.push(sprite);
             }
+            Primitive::Surface(mut surface) => {
+                surface.order = layer_id;
+                self.surfaces.push(surface);
+            }
         }
     }
 }
@@ -196,6 +205,7 @@ pub(crate) struct Scene {
     pub underlines: Vec<Underline>,
     pub monochrome_sprites: Vec<MonochromeSprite>,
     pub polychrome_sprites: Vec<PolychromeSprite>,
+    pub surfaces: Vec<Surface>,
 }
 
 impl Scene {
@@ -224,6 +234,9 @@ impl Scene {
             polychrome_sprites: &self.polychrome_sprites,
             polychrome_sprites_start: 0,
             polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(),
+            surfaces: &self.surfaces,
+            surfaces_start: 0,
+            surfaces_iter: self.surfaces.iter().peekable(),
         }
     }
 }
@@ -247,6 +260,9 @@ struct BatchIterator<'a> {
     polychrome_sprites: &'a [PolychromeSprite],
     polychrome_sprites_start: usize,
     polychrome_sprites_iter: Peekable<slice::Iter<'a, PolychromeSprite>>,
+    surfaces: &'a [Surface],
+    surfaces_start: usize,
+    surfaces_iter: Peekable<slice::Iter<'a, Surface>>,
 }
 
 impl<'a> Iterator for BatchIterator<'a> {
@@ -272,6 +288,10 @@ impl<'a> Iterator for BatchIterator<'a> {
                 self.polychrome_sprites_iter.peek().map(|s| s.order),
                 PrimitiveKind::PolychromeSprite,
             ),
+            (
+                self.surfaces_iter.peek().map(|s| s.order),
+                PrimitiveKind::Surface,
+            ),
         ];
         orders_and_kinds.sort_by_key(|(order, kind)| (order.unwrap_or(u32::MAX), *kind));
 
@@ -378,6 +398,21 @@ impl<'a> Iterator for BatchIterator<'a> {
                     sprites: &self.polychrome_sprites[sprites_start..sprites_end],
                 })
             }
+            PrimitiveKind::Surface => {
+                let surfaces_start = self.surfaces_start;
+                let mut surfaces_end = surfaces_start;
+                while self
+                    .surfaces_iter
+                    .next_if(|surface| surface.order <= max_order)
+                    .is_some()
+                {
+                    surfaces_end += 1;
+                }
+                self.surfaces_start = surfaces_end;
+                Some(PrimitiveBatch::Surfaces(
+                    &self.surfaces[surfaces_start..surfaces_end],
+                ))
+            }
         }
     }
 }
@@ -391,6 +426,7 @@ pub enum PrimitiveKind {
     Underline,
     MonochromeSprite,
     PolychromeSprite,
+    Surface,
 }
 
 pub enum Primitive {
@@ -400,6 +436,7 @@ pub enum Primitive {
     Underline(Underline),
     MonochromeSprite(MonochromeSprite),
     PolychromeSprite(PolychromeSprite),
+    Surface(Surface),
 }
 
 impl Primitive {
@@ -411,6 +448,7 @@ impl Primitive {
             Primitive::Underline(underline) => &underline.bounds,
             Primitive::MonochromeSprite(sprite) => &sprite.bounds,
             Primitive::PolychromeSprite(sprite) => &sprite.bounds,
+            Primitive::Surface(surface) => &surface.bounds,
         }
     }
 
@@ -422,6 +460,7 @@ impl Primitive {
             Primitive::Underline(underline) => &underline.content_mask,
             Primitive::MonochromeSprite(sprite) => &sprite.content_mask,
             Primitive::PolychromeSprite(sprite) => &sprite.content_mask,
+            Primitive::Surface(surface) => &surface.content_mask,
         }
     }
 }
@@ -440,6 +479,7 @@ pub(crate) enum PrimitiveBatch<'a> {
         texture_id: AtlasTextureId,
         sprites: &'a [PolychromeSprite],
     },
+    Surfaces(&'a [Surface]),
 }
 
 #[derive(Default, Debug, Clone, Eq, PartialEq)]
@@ -593,6 +633,32 @@ impl From<PolychromeSprite> for Primitive {
     }
 }
 
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Surface {
+    pub order: u32,
+    pub bounds: Bounds<ScaledPixels>,
+    pub content_mask: ContentMask<ScaledPixels>,
+    pub image_buffer: media::core_video::CVImageBuffer,
+}
+
+impl Ord for Surface {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.order.cmp(&other.order)
+    }
+}
+
+impl PartialOrd for Surface {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl From<Surface> for Primitive {
+    fn from(surface: Surface) -> Self {
+        Primitive::Surface(surface)
+    }
+}
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub(crate) struct PathId(pub(crate) usize);
 

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

@@ -8,8 +8,8 @@ use crate::{
     MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler,
     PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
     RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size,
-    Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
-    VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+    Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline,
+    UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::HashMap;
@@ -18,6 +18,7 @@ use futures::{
     channel::{mpsc, oneshot},
     StreamExt,
 };
+use media::core_video::CVImageBuffer;
 use parking_lot::RwLock;
 use slotmap::SlotMap;
 use smallvec::SmallVec;
@@ -39,8 +40,8 @@ use util::ResultExt;
 
 /// A global stacking order, which is created by stacking successive z-index values.
 /// Each z-index will always be interpreted in the context of its parent z-index.
-#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default)]
-pub(crate) struct StackingOrder(pub(crate) SmallVec<[u32; 16]>);
+#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default, Debug)]
+pub struct StackingOrder(pub(crate) SmallVec<[u32; 16]>);
 
 /// Represents the two different phases when dispatching events.
 #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
@@ -193,13 +194,11 @@ pub trait FocusableView: 'static + Render {
 
 /// ManagedView is a view (like a Modal, Popover, Menu, etc.)
 /// where the lifecycle of the view is handled by another view.
-pub trait ManagedView: FocusableView + EventEmitter<Manager> {}
+pub trait ManagedView: FocusableView + EventEmitter<DismissEvent> {}
 
-impl<M: FocusableView + EventEmitter<Manager>> ManagedView for M {}
+impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
 
-pub enum Manager {
-    Dismiss,
-}
+pub struct DismissEvent;
 
 // Holds the state for a specific window.
 pub struct Window {
@@ -243,7 +242,8 @@ pub(crate) struct Frame {
     pub(crate) dispatch_tree: DispatchTree,
     pub(crate) focus_listeners: Vec<AnyFocusListener>,
     pub(crate) scene_builder: SceneBuilder,
-    z_index_stack: StackingOrder,
+    pub(crate) depth_map: Vec<(StackingOrder, Bounds<Pixels>)>,
+    pub(crate) z_index_stack: StackingOrder,
     content_mask_stack: Vec<ContentMask<Pixels>>,
     element_offset_stack: Vec<Point<Pixels>>,
 }
@@ -257,6 +257,7 @@ impl Frame {
             focus_listeners: Vec::new(),
             scene_builder: SceneBuilder::default(),
             z_index_stack: StackingOrder::default(),
+            depth_map: Default::default(),
             content_mask_stack: Vec::new(),
             element_offset_stack: Vec::new(),
         }
@@ -806,6 +807,32 @@ impl<'a> WindowContext<'a> {
         result
     }
 
+    /// Called during painting to track which z-index is on top at each pixel position
+    pub fn add_opaque_layer(&mut self, bounds: Bounds<Pixels>) {
+        let stacking_order = self.window.current_frame.z_index_stack.clone();
+        let depth_map = &mut self.window.current_frame.depth_map;
+        match depth_map.binary_search_by(|(level, _)| stacking_order.cmp(&level)) {
+            Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, bounds)),
+        }
+    }
+
+    /// Returns true if the top-most opaque layer painted over this point was part of the
+    /// same layer as the given stacking order.
+    pub fn was_top_layer(&self, point: &Point<Pixels>, level: &StackingOrder) -> bool {
+        for (stack, bounds) in self.window.previous_frame.depth_map.iter() {
+            if bounds.contains_point(point) {
+                return level.starts_with(stack) || stack.starts_with(level);
+            }
+        }
+
+        false
+    }
+
+    /// Called during painting to get the current stacking order.
+    pub fn stacking_order(&self) -> &StackingOrder {
+        &self.window.current_frame.z_index_stack
+    }
+
     /// Paint one or more drop shadows into the scene for the current frame at the current z-index.
     pub fn paint_shadows(
         &mut self,
@@ -1090,6 +1117,23 @@ impl<'a> WindowContext<'a> {
         Ok(())
     }
 
+    /// Paint a surface into the scene for the current frame at the current z-index.
+    pub fn paint_surface(&mut self, bounds: Bounds<Pixels>, image_buffer: CVImageBuffer) {
+        let scale_factor = self.scale_factor();
+        let bounds = bounds.scale(scale_factor);
+        let content_mask = self.content_mask().scale(scale_factor);
+        let window = &mut *self.window;
+        window.current_frame.scene_builder.insert(
+            &window.current_frame.z_index_stack,
+            Surface {
+                order: 0,
+                bounds,
+                content_mask,
+                image_buffer,
+            },
+        );
+    }
+
     /// Draw pixels to the display for this window based on the contents of its scene.
     pub(crate) fn draw(&mut self) {
         let root_view = self.window.root_view.take().unwrap();
@@ -1153,6 +1197,7 @@ impl<'a> WindowContext<'a> {
         frame.mouse_listeners.values_mut().for_each(Vec::clear);
         frame.focus_listeners.clear();
         frame.dispatch_tree.clear();
+        frame.depth_map.clear();
     }
 
     /// Dispatch a mouse or keyboard event on the window.
@@ -1453,13 +1498,15 @@ impl<'a> WindowContext<'a> {
         }
     }
 
-    pub fn constructor_for<V: Render, R>(
+    pub fn handler_for<V: Render>(
         &self,
         view: &View<V>,
-        f: impl Fn(&mut V, &mut ViewContext<V>) -> R + 'static,
-    ) -> impl Fn(&mut WindowContext) -> R + 'static {
-        let view = view.clone();
-        move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx))
+        f: impl Fn(&mut V, &mut ViewContext<V>) + 'static,
+    ) -> impl Fn(&mut WindowContext) {
+        let view = view.downgrade();
+        move |cx: &mut WindowContext| {
+            view.update(cx, |view, cx| f(view, cx)).ok();
+        }
     }
 
     //========== ELEMENT RELATED FUNCTIONS ===========
@@ -1517,6 +1564,13 @@ impl<'a> WindowContext<'a> {
                 .set_input_handler(Box::new(input_handler));
         }
     }
+
+    pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) {
+        let mut this = self.to_async();
+        self.window
+            .platform_window
+            .on_should_close(Box::new(move || this.update(|_, cx| f(cx)).unwrap_or(true)))
+    }
 }
 
 impl Context for WindowContext<'_> {
@@ -1663,7 +1717,7 @@ impl VisualContext for WindowContext<'_> {
     where
         V: ManagedView,
     {
-        self.update_view(view, |_, cx| cx.emit(Manager::Dismiss))
+        self.update_view(view, |_, cx| cx.emit(DismissEvent))
     }
 }
 
@@ -1752,6 +1806,24 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         }
     }
 
+    /// Invoke the given function with the content mask reset to that
+    /// of the window.
+    fn break_content_mask<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
+        let mask = ContentMask {
+            bounds: Bounds {
+                origin: Point::default(),
+                size: self.window().viewport_size,
+            },
+        };
+        self.window_mut()
+            .current_frame
+            .content_mask_stack
+            .push(mask);
+        let result = f(self);
+        self.window_mut().current_frame.content_mask_stack.pop();
+        result
+    }
+
     /// Update the global element offset relative to the current offset. This is used to implement
     /// scrolling.
     fn with_element_offset<R>(
@@ -1885,23 +1957,6 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         })
     }
 
-    /// Like `with_element_state`, but for situations where the element_id is optional. If the
-    /// id is `None`, no state will be retrieved or stored.
-    fn with_optional_element_state<S, R>(
-        &mut self,
-        element_id: Option<ElementId>,
-        f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
-    ) -> R
-    where
-        S: 'static,
-    {
-        if let Some(element_id) = element_id {
-            self.with_element_state(element_id, f)
-        } else {
-            f(None, self).0
-        }
-    }
-
     /// Obtain the current content mask.
     fn content_mask(&self) -> ContentMask<Pixels> {
         self.window()
@@ -2349,7 +2404,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     where
         V: ManagedView,
     {
-        self.defer(|_, cx| cx.emit(Manager::Dismiss))
+        self.defer(|_, cx| cx.emit(DismissEvent))
     }
 
     pub fn listener<E>(
@@ -2545,7 +2600,7 @@ impl<V: 'static + Render> WindowHandle<V> {
         cx.read_window(self, |root_view, _cx| root_view.clone())
     }
 
-    pub fn is_active(&self, cx: &WindowContext) -> Option<bool> {
+    pub fn is_active(&self, cx: &AppContext) -> Option<bool> {
         cx.windows
             .get(self.id)
             .and_then(|window| window.as_ref().map(|window| window.active))

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

@@ -11,7 +11,7 @@ pub struct HighlightId(pub u32);
 const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
 
 impl HighlightMap {
-    pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self {
+    pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self {
         // For each capture name in the highlight query, find the longest
         // key in the theme's syntax styles that matches all of the
         // dot-separated components of the capture name.
@@ -98,9 +98,9 @@ mod tests {
         );
 
         let capture_names = &[
-            "function.special".to_string(),
-            "function.async.rust".to_string(),
-            "variable.builtin.self".to_string(),
+            "function.special",
+            "function.async.rust",
+            "variable.builtin.self",
         ];
 
         let map = HighlightMap::new(capture_names, &theme);

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

@@ -197,8 +197,12 @@ impl CachedLspAdapter {
         self.adapter.code_action_kinds()
     }
 
-    pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
-        self.adapter.workspace_configuration(cx)
+    pub fn workspace_configuration(
+        &self,
+        workspace_root: &Path,
+        cx: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
+        self.adapter.workspace_configuration(workspace_root, cx)
     }
 
     pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
@@ -312,7 +316,7 @@ pub trait LspAdapter: 'static + Send + Sync {
         None
     }
 
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(&self, _: &Path, _: &mut AppContext) -> BoxFuture<'static, Value> {
         futures::future::ready(serde_json::json!({})).boxed()
     }
 
@@ -1383,7 +1387,7 @@ impl Language {
         let query = Query::new(self.grammar_mut().ts_language, source)?;
 
         let mut override_configs_by_id = HashMap::default();
-        for (ix, name) in query.capture_names().iter().enumerate() {
+        for (ix, name) in query.capture_names().iter().copied().enumerate() {
             if !name.starts_with('_') {
                 let value = self.config.overrides.remove(name).unwrap_or_default();
                 for server_name in &value.opt_into_language_servers {
@@ -1396,7 +1400,7 @@ impl Language {
                     }
                 }
 
-                override_configs_by_id.insert(ix as u32, (name.clone(), value));
+                override_configs_by_id.insert(ix as u32, (name.into(), value));
             }
         }
 

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

@@ -1300,7 +1300,7 @@ fn assert_capture_ranges(
         .collect::<Vec<_>>();
     for capture in captures {
         let name = &queries[capture.grammar_index].capture_names()[capture.index as usize];
-        if highlight_query_capture_names.contains(&name.as_str()) {
+        if highlight_query_capture_names.contains(&name) {
             actual_ranges.push(capture.node.byte_range());
         }
     }

crates/language2/src/highlight_map.rs πŸ”—

@@ -11,7 +11,7 @@ pub struct HighlightId(pub u32);
 const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
 
 impl HighlightMap {
-    pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self {
+    pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self {
         // For each capture name in the highlight query, find the longest
         // key in the theme's syntax styles that matches all of the
         // dot-separated components of the capture name.
@@ -100,9 +100,9 @@ mod tests {
         };
 
         let capture_names = &[
-            "function.special".to_string(),
-            "function.async.rust".to_string(),
-            "variable.builtin.self".to_string(),
+            "function.special",
+            "function.async.rust",
+            "variable.builtin.self",
         ];
 
         let map = HighlightMap::new(capture_names, &theme);

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

@@ -200,8 +200,12 @@ impl CachedLspAdapter {
         self.adapter.code_action_kinds()
     }
 
-    pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
-        self.adapter.workspace_configuration(cx)
+    pub fn workspace_configuration(
+        &self,
+        workspace_root: &Path,
+        cx: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
+        self.adapter.workspace_configuration(workspace_root, cx)
     }
 
     pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
@@ -315,7 +319,7 @@ pub trait LspAdapter: 'static + Send + Sync {
         None
     }
 
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(&self, _: &Path, _: &mut AppContext) -> BoxFuture<'static, Value> {
         futures::future::ready(serde_json::json!({})).boxed()
     }
 
@@ -1391,7 +1395,7 @@ impl Language {
         let mut override_configs_by_id = HashMap::default();
         for (ix, name) in query.capture_names().iter().enumerate() {
             if !name.starts_with('_') {
-                let value = self.config.overrides.remove(name).unwrap_or_default();
+                let value = self.config.overrides.remove(*name).unwrap_or_default();
                 for server_name in &value.opt_into_language_servers {
                     if !self
                         .config
@@ -1402,7 +1406,7 @@ impl Language {
                     }
                 }
 
-                override_configs_by_id.insert(ix as u32, (name.clone(), value));
+                override_configs_by_id.insert(ix as u32, (name.to_string(), value));
             }
         }
 

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

@@ -1300,7 +1300,7 @@ fn assert_capture_ranges(
         .collect::<Vec<_>>();
     for capture in captures {
         let name = &queries[capture.grammar_index].capture_names()[capture.index as usize];
-        if highlight_query_capture_names.contains(&name.as_str()) {
+        if highlight_query_capture_names.contains(&name) {
             actual_ranges.push(capture.node.byte_range());
         }
     }

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

@@ -429,8 +429,8 @@ impl LanguageServer {
         let root_uri = Url::from_file_path(&self.root_path).unwrap();
         #[allow(deprecated)]
         let params = InitializeParams {
-            process_id: Default::default(),
-            root_path: Default::default(),
+            process_id: None,
+            root_path: None,
             root_uri: Some(root_uri.clone()),
             initialization_options: options,
             capabilities: ClientCapabilities {
@@ -451,12 +451,15 @@ impl LanguageServer {
                     inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
                         refresh_support: Some(true),
                     }),
+                    diagnostic: Some(DiagnosticWorkspaceClientCapabilities {
+                        refresh_support: None,
+                    }),
                     ..Default::default()
                 }),
                 text_document: Some(TextDocumentClientCapabilities {
                     definition: Some(GotoCapability {
                         link_support: Some(true),
-                        ..Default::default()
+                        dynamic_registration: None,
                     }),
                     code_action: Some(CodeActionClientCapabilities {
                         code_action_literal_support: Some(CodeActionLiteralSupport {
@@ -501,7 +504,7 @@ impl LanguageServer {
                     }),
                     hover: Some(HoverClientCapabilities {
                         content_format: Some(vec![MarkupKind::Markdown]),
-                        ..Default::default()
+                        dynamic_registration: None,
                     }),
                     inlay_hint: Some(InlayHintClientCapabilities {
                         resolve_support: Some(InlayHintResolveClientCapabilities {
@@ -515,6 +518,20 @@ impl LanguageServer {
                         }),
                         dynamic_registration: Some(false),
                     }),
+                    publish_diagnostics: Some(PublishDiagnosticsClientCapabilities {
+                        related_information: Some(true),
+                        ..Default::default()
+                    }),
+                    formatting: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: None,
+                    }),
+                    on_type_formatting: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: None,
+                    }),
+                    diagnostic: Some(DiagnosticClientCapabilities {
+                        related_document_support: Some(true),
+                        dynamic_registration: None,
+                    }),
                     ..Default::default()
                 }),
                 experimental: Some(json!({
@@ -524,15 +541,15 @@ impl LanguageServer {
                     work_done_progress: Some(true),
                     ..Default::default()
                 }),
-                ..Default::default()
+                general: None,
             },
-            trace: Default::default(),
+            trace: None,
             workspace_folders: Some(vec![WorkspaceFolder {
                 uri: root_uri,
                 name: Default::default(),
             }]),
-            client_info: Default::default(),
-            locale: Default::default(),
+            client_info: None,
+            locale: None,
         };
 
         let response = self.request::<request::Initialize>(params).await?;

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

@@ -434,8 +434,8 @@ impl LanguageServer {
         let root_uri = Url::from_file_path(&self.root_path).unwrap();
         #[allow(deprecated)]
         let params = InitializeParams {
-            process_id: Default::default(),
-            root_path: Default::default(),
+            process_id: None,
+            root_path: None,
             root_uri: Some(root_uri.clone()),
             initialization_options: options,
             capabilities: ClientCapabilities {
@@ -456,12 +456,15 @@ impl LanguageServer {
                     inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
                         refresh_support: Some(true),
                     }),
+                    diagnostic: Some(DiagnosticWorkspaceClientCapabilities {
+                        refresh_support: None,
+                    }),
                     ..Default::default()
                 }),
                 text_document: Some(TextDocumentClientCapabilities {
                     definition: Some(GotoCapability {
                         link_support: Some(true),
-                        ..Default::default()
+                        dynamic_registration: None,
                     }),
                     code_action: Some(CodeActionClientCapabilities {
                         code_action_literal_support: Some(CodeActionLiteralSupport {
@@ -503,7 +506,7 @@ impl LanguageServer {
                     }),
                     hover: Some(HoverClientCapabilities {
                         content_format: Some(vec![MarkupKind::Markdown]),
-                        ..Default::default()
+                        dynamic_registration: None,
                     }),
                     inlay_hint: Some(InlayHintClientCapabilities {
                         resolve_support: Some(InlayHintResolveClientCapabilities {
@@ -517,6 +520,20 @@ impl LanguageServer {
                         }),
                         dynamic_registration: Some(false),
                     }),
+                    publish_diagnostics: Some(PublishDiagnosticsClientCapabilities {
+                        related_information: Some(true),
+                        ..Default::default()
+                    }),
+                    formatting: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: None,
+                    }),
+                    on_type_formatting: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: None,
+                    }),
+                    diagnostic: Some(DiagnosticClientCapabilities {
+                        related_document_support: Some(true),
+                        dynamic_registration: None,
+                    }),
                     ..Default::default()
                 }),
                 experimental: Some(json!({
@@ -526,15 +543,15 @@ impl LanguageServer {
                     work_done_progress: Some(true),
                     ..Default::default()
                 }),
-                ..Default::default()
+                general: None,
             },
-            trace: Default::default(),
+            trace: None,
             workspace_folders: Some(vec![WorkspaceFolder {
                 uri: root_uri,
                 name: Default::default(),
             }]),
-            client_info: Default::default(),
-            locale: Default::default(),
+            client_info: None,
+            locale: None,
         };
 
         let response = self.request::<request::Initialize>(params).await?;

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

@@ -73,6 +73,7 @@ impl RealNodeRuntime {
         let npm_file = node_dir.join("bin/npm");
 
         let result = Command::new(&node_binary)
+            .env_clear()
             .arg(npm_file)
             .arg("--version")
             .stdin(Stdio::null())
@@ -149,6 +150,7 @@ impl NodeRuntime for RealNodeRuntime {
             }
 
             let mut command = Command::new(node_binary);
+            command.env_clear();
             command.env("PATH", env_path);
             command.arg(npm_file).arg(subcommand);
             command.args(["--cache".into(), installation_path.join("cache")]);
@@ -200,11 +202,11 @@ impl NodeRuntime for RealNodeRuntime {
                 &[
                     name,
                     "--json",
-                    "-fetch-retry-mintimeout",
+                    "--fetch-retry-mintimeout",
                     "2000",
-                    "-fetch-retry-maxtimeout",
+                    "--fetch-retry-maxtimeout",
                     "5000",
-                    "-fetch-timeout",
+                    "--fetch-timeout",
                     "5000",
                 ],
             )
@@ -229,11 +231,11 @@ impl NodeRuntime for RealNodeRuntime {
 
         let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect();
         arguments.extend_from_slice(&[
-            "-fetch-retry-mintimeout",
+            "--fetch-retry-mintimeout",
             "2000",
-            "-fetch-retry-maxtimeout",
+            "--fetch-retry-maxtimeout",
             "5000",
-            "-fetch-timeout",
+            "--fetch-timeout",
             "5000",
         ]);
 

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

@@ -1,7 +1,8 @@
 use editor::Editor;
 use gpui::{
-    div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,
-    MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
+    div, prelude::*, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView,
+    MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext,
+    WindowContext,
 };
 use std::{cmp, sync::Arc};
 use ui::{prelude::*, v_stack, Color, Divider, Label};
@@ -16,7 +17,6 @@ pub struct Picker<D: PickerDelegate> {
 
 pub trait PickerDelegate: Sized + 'static {
     type ListItem: IntoElement;
-
     fn match_count(&self) -> usize;
     fn selected_index(&self) -> usize;
     fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
@@ -32,7 +32,7 @@ pub trait PickerDelegate: Sized + 'static {
         ix: usize,
         selected: bool,
         cx: &mut ViewContext<Picker<Self>>,
-    ) -> Self::ListItem;
+    ) -> Option<Self::ListItem>;
 }
 
 impl<D: PickerDelegate> FocusableView for Picker<D> {
@@ -205,7 +205,6 @@ impl<D: PickerDelegate> Render for Picker<D> {
             .when(self.delegate.match_count() > 0, |el| {
                 el.child(
                     v_stack()
-                        .p_1()
                         .grow()
                         .child(
                             uniform_list(
@@ -229,7 +228,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
                                                             )
                                                         }),
                                                     )
-                                                    .child(picker.delegate.render_match(
+                                                    .children(picker.delegate.render_match(
                                                         ix,
                                                         ix == selected_index,
                                                         cx,
@@ -239,7 +238,8 @@ impl<D: PickerDelegate> Render for Picker<D> {
                                     }
                                 },
                             )
-                            .track_scroll(self.scroll_handle.clone()),
+                            .track_scroll(self.scroll_handle.clone())
+                            .p_1()
                         )
                         .max_h_72()
                         .overflow_hidden(),
@@ -256,3 +256,22 @@ impl<D: PickerDelegate> Render for Picker<D> {
             })
     }
 }
+
+pub fn simple_picker_match(
+    selected: bool,
+    cx: &mut WindowContext,
+    children: impl FnOnce(&mut WindowContext) -> AnyElement,
+) -> AnyElement {
+    let colors = cx.theme().colors();
+
+    div()
+        .px_1()
+        .text_color(colors.text)
+        .text_ui()
+        .bg(colors.ghost_element_background)
+        .rounded_md()
+        .when(selected, |this| this.bg(colors.ghost_element_selected))
+        .hover(|this| this.bg(colors.ghost_element_hover))
+        .child((children)(cx))
+        .into_any()
+}

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

@@ -13,12 +13,14 @@ use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
 use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
 
+#[derive(Clone)]
 pub enum Prettier {
     Real(RealPrettier),
     #[cfg(any(test, feature = "test-support"))]
     Test(TestPrettier),
 }
 
+#[derive(Clone)]
 pub struct RealPrettier {
     default: bool,
     prettier_dir: PathBuf,
@@ -26,11 +28,13 @@ pub struct RealPrettier {
 }
 
 #[cfg(any(test, feature = "test-support"))]
+#[derive(Clone)]
 pub struct TestPrettier {
     prettier_dir: PathBuf,
     default: bool,
 }
 
+pub const FAIL_THRESHOLD: usize = 4;
 pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
 pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
 const PRETTIER_PACKAGE_NAME: &str = "prettier";

crates/prettier/src/prettier_server.js πŸ”—

@@ -153,7 +153,10 @@ async function handleMessage(message, prettier) {
     const { method, id, params } = message;
     if (method === undefined) {
         throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
+    } else if (method == "initialized") {
+        return;
     }
+
     if (id === undefined) {
         throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
     }

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

@@ -13,12 +13,14 @@ use std::{
 };
 use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
 
+#[derive(Clone)]
 pub enum Prettier {
     Real(RealPrettier),
     #[cfg(any(test, feature = "test-support"))]
     Test(TestPrettier),
 }
 
+#[derive(Clone)]
 pub struct RealPrettier {
     default: bool,
     prettier_dir: PathBuf,
@@ -26,11 +28,13 @@ pub struct RealPrettier {
 }
 
 #[cfg(any(test, feature = "test-support"))]
+#[derive(Clone)]
 pub struct TestPrettier {
     prettier_dir: PathBuf,
     default: bool,
 }
 
+pub const FAIL_THRESHOLD: usize = 4;
 pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
 pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
 const PRETTIER_PACKAGE_NAME: &str = "prettier";

crates/prettier2/src/prettier_server.js πŸ”—

@@ -153,7 +153,10 @@ async function handleMessage(message, prettier) {
     const { method, id, params } = message;
     if (method === undefined) {
         throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
+    } else if (method == "initialized") {
+        return;
     }
+
     if (id === undefined) {
         throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
     }

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

@@ -0,0 +1,758 @@
+use std::{
+    ops::ControlFlow,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+
+use anyhow::Context;
+use collections::HashSet;
+use fs::Fs;
+use futures::{
+    future::{self, Shared},
+    FutureExt,
+};
+use gpui::{AsyncAppContext, ModelContext, ModelHandle, Task};
+use language::{
+    language_settings::{Formatter, LanguageSettings},
+    Buffer, Language, LanguageServerName, LocalFile,
+};
+use lsp::LanguageServerId;
+use node_runtime::NodeRuntime;
+use prettier::Prettier;
+use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt};
+
+use crate::{
+    Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
+};
+
+pub fn prettier_plugins_for_language(
+    language: &Language,
+    language_settings: &LanguageSettings,
+) -> Option<HashSet<&'static str>> {
+    match &language_settings.formatter {
+        Formatter::Prettier { .. } | Formatter::Auto => {}
+        Formatter::LanguageServer | Formatter::External { .. } => return None,
+    };
+    let mut prettier_plugins = None;
+    if language.prettier_parser_name().is_some() {
+        prettier_plugins
+            .get_or_insert_with(|| HashSet::default())
+            .extend(
+                language
+                    .lsp_adapters()
+                    .iter()
+                    .flat_map(|adapter| adapter.prettier_plugins()),
+            )
+    }
+
+    prettier_plugins
+}
+
+pub(super) async fn format_with_prettier(
+    project: &ModelHandle<Project>,
+    buffer: &ModelHandle<Buffer>,
+    cx: &mut AsyncAppContext,
+) -> Option<FormatOperation> {
+    if let Some((prettier_path, prettier_task)) = project
+        .update(cx, |project, cx| {
+            project.prettier_instance_for_buffer(buffer, cx)
+        })
+        .await
+    {
+        match prettier_task.await {
+            Ok(prettier) => {
+                let buffer_path = buffer.update(cx, |buffer, cx| {
+                    File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
+                });
+                match prettier.format(buffer, buffer_path, cx).await {
+                    Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)),
+                    Err(e) => {
+                        log::error!(
+                            "Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}"
+                        );
+                    }
+                }
+            }
+            Err(e) => project.update(cx, |project, _| {
+                let instance_to_update = match prettier_path {
+                    Some(prettier_path) => {
+                        log::error!(
+                            "Prettier instance from path {prettier_path:?} failed to spawn: {e:#}"
+                        );
+                        project.prettier_instances.get_mut(&prettier_path)
+                    }
+                    None => {
+                        log::error!("Default prettier instance failed to spawn: {e:#}");
+                        match &mut project.default_prettier.prettier {
+                            PrettierInstallation::NotInstalled { .. } => None,
+                            PrettierInstallation::Installed(instance) => Some(instance),
+                        }
+                    }
+                };
+
+                if let Some(instance) = instance_to_update {
+                    instance.attempt += 1;
+                    instance.prettier = None;
+                }
+            }),
+        }
+    }
+
+    None
+}
+
+pub struct DefaultPrettier {
+    prettier: PrettierInstallation,
+    installed_plugins: HashSet<&'static str>,
+}
+
+pub enum PrettierInstallation {
+    NotInstalled {
+        attempts: usize,
+        installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
+        not_installed_plugins: HashSet<&'static str>,
+    },
+    Installed(PrettierInstance),
+}
+
+pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
+
+#[derive(Clone)]
+pub struct PrettierInstance {
+    attempt: usize,
+    prettier: Option<PrettierTask>,
+}
+
+impl Default for DefaultPrettier {
+    fn default() -> Self {
+        Self {
+            prettier: PrettierInstallation::NotInstalled {
+                attempts: 0,
+                installation_task: None,
+                not_installed_plugins: HashSet::default(),
+            },
+            installed_plugins: HashSet::default(),
+        }
+    }
+}
+
+impl DefaultPrettier {
+    pub fn instance(&self) -> Option<&PrettierInstance> {
+        if let PrettierInstallation::Installed(instance) = &self.prettier {
+            Some(instance)
+        } else {
+            None
+        }
+    }
+
+    pub fn prettier_task(
+        &mut self,
+        node: &Arc<dyn NodeRuntime>,
+        worktree_id: Option<WorktreeId>,
+        cx: &mut ModelContext<'_, Project>,
+    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
+        match &mut self.prettier {
+            PrettierInstallation::NotInstalled { .. } => {
+                Some(start_default_prettier(Arc::clone(node), worktree_id, cx))
+            }
+            PrettierInstallation::Installed(existing_instance) => {
+                existing_instance.prettier_task(node, None, worktree_id, cx)
+            }
+        }
+    }
+}
+
+impl PrettierInstance {
+    pub fn prettier_task(
+        &mut self,
+        node: &Arc<dyn NodeRuntime>,
+        prettier_dir: Option<&Path>,
+        worktree_id: Option<WorktreeId>,
+        cx: &mut ModelContext<'_, Project>,
+    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
+        if self.attempt > prettier::FAIL_THRESHOLD {
+            match prettier_dir {
+                Some(prettier_dir) => log::warn!(
+                    "Prettier from path {prettier_dir:?} exceeded launch threshold, not starting"
+                ),
+                None => log::warn!("Default prettier exceeded launch threshold, not starting"),
+            }
+            return None;
+        }
+        Some(match &self.prettier {
+            Some(prettier_task) => Task::ready(Ok(prettier_task.clone())),
+            None => match prettier_dir {
+                Some(prettier_dir) => {
+                    let new_task = start_prettier(
+                        Arc::clone(node),
+                        prettier_dir.to_path_buf(),
+                        worktree_id,
+                        cx,
+                    );
+                    self.attempt += 1;
+                    self.prettier = Some(new_task.clone());
+                    Task::ready(Ok(new_task))
+                }
+                None => {
+                    self.attempt += 1;
+                    let node = Arc::clone(node);
+                    cx.spawn(|project, mut cx| async move {
+                        project
+                            .update(&mut cx, |_, cx| {
+                                start_default_prettier(node, worktree_id, cx)
+                            })
+                            .await
+                    })
+                }
+            },
+        })
+    }
+}
+
+fn start_default_prettier(
+    node: Arc<dyn NodeRuntime>,
+    worktree_id: Option<WorktreeId>,
+    cx: &mut ModelContext<'_, Project>,
+) -> Task<anyhow::Result<PrettierTask>> {
+    cx.spawn(|project, mut cx| async move {
+        loop {
+            let installation_task = project.update(&mut cx, |project, _| {
+                match &project.default_prettier.prettier {
+                    PrettierInstallation::NotInstalled {
+                        installation_task, ..
+                    } => ControlFlow::Continue(installation_task.clone()),
+                    PrettierInstallation::Installed(default_prettier) => {
+                        ControlFlow::Break(default_prettier.clone())
+                    }
+                }
+            });
+            match installation_task {
+                ControlFlow::Continue(None) => {
+                    anyhow::bail!("Default prettier is not installed and cannot be started")
+                }
+                ControlFlow::Continue(Some(installation_task)) => {
+                    log::info!("Waiting for default prettier to install");
+                    if let Err(e) = installation_task.await {
+                        project.update(&mut cx, |project, _| {
+                            if let PrettierInstallation::NotInstalled {
+                                installation_task,
+                                attempts,
+                                ..
+                            } = &mut project.default_prettier.prettier
+                            {
+                                *installation_task = None;
+                                *attempts += 1;
+                            }
+                        });
+                        anyhow::bail!(
+                            "Cannot start default prettier due to its installation failure: {e:#}"
+                        );
+                    }
+                    let new_default_prettier = project.update(&mut cx, |project, cx| {
+                        let new_default_prettier =
+                            start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
+                        project.default_prettier.prettier =
+                            PrettierInstallation::Installed(PrettierInstance {
+                                attempt: 0,
+                                prettier: Some(new_default_prettier.clone()),
+                            });
+                        new_default_prettier
+                    });
+                    return Ok(new_default_prettier);
+                }
+                ControlFlow::Break(instance) => match instance.prettier {
+                    Some(instance) => return Ok(instance),
+                    None => {
+                        let new_default_prettier = project.update(&mut cx, |project, cx| {
+                            let new_default_prettier =
+                                start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
+                            project.default_prettier.prettier =
+                                PrettierInstallation::Installed(PrettierInstance {
+                                    attempt: instance.attempt + 1,
+                                    prettier: Some(new_default_prettier.clone()),
+                                });
+                            new_default_prettier
+                        });
+                        return Ok(new_default_prettier);
+                    }
+                },
+            }
+        }
+    })
+}
+
+fn start_prettier(
+    node: Arc<dyn NodeRuntime>,
+    prettier_dir: PathBuf,
+    worktree_id: Option<WorktreeId>,
+    cx: &mut ModelContext<'_, Project>,
+) -> PrettierTask {
+    cx.spawn(|project, mut cx| async move {
+        log::info!("Starting prettier at path {prettier_dir:?}");
+        let new_server_id = project.update(&mut cx, |project, _| {
+            project.languages.next_language_server_id()
+        });
+
+        let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
+            .await
+            .context("default prettier spawn")
+            .map(Arc::new)
+            .map_err(Arc::new)?;
+        register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
+        Ok(new_prettier)
+    })
+    .shared()
+}
+
+fn register_new_prettier(
+    project: &ModelHandle<Project>,
+    prettier: &Prettier,
+    worktree_id: Option<WorktreeId>,
+    new_server_id: LanguageServerId,
+    cx: &mut AsyncAppContext,
+) {
+    let prettier_dir = prettier.prettier_dir();
+    let is_default = prettier.is_default();
+    if is_default {
+        log::info!("Started default prettier in {prettier_dir:?}");
+    } else {
+        log::info!("Started prettier in {prettier_dir:?}");
+    }
+    if let Some(prettier_server) = prettier.server() {
+        project.update(cx, |project, cx| {
+            let name = if is_default {
+                LanguageServerName(Arc::from("prettier (default)"))
+            } else {
+                let worktree_path = worktree_id
+                    .and_then(|id| project.worktree_for_id(id, cx))
+                    .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
+                let name = match worktree_path {
+                    Some(worktree_path) => {
+                        if prettier_dir == worktree_path.as_ref() {
+                            let name = prettier_dir
+                                .file_name()
+                                .and_then(|name| name.to_str())
+                                .unwrap_or_default();
+                            format!("prettier ({name})")
+                        } else {
+                            let dir_to_display = prettier_dir
+                                .strip_prefix(worktree_path.as_ref())
+                                .ok()
+                                .unwrap_or(prettier_dir);
+                            format!("prettier ({})", dir_to_display.display())
+                        }
+                    }
+                    None => format!("prettier ({})", prettier_dir.display()),
+                };
+                LanguageServerName(Arc::from(name))
+            };
+            project
+                .supplementary_language_servers
+                .insert(new_server_id, (name, Arc::clone(prettier_server)));
+            cx.emit(Event::LanguageServerAdded(new_server_id));
+        });
+    }
+}
+
+async fn install_prettier_packages(
+    plugins_to_install: HashSet<&'static str>,
+    node: Arc<dyn NodeRuntime>,
+) -> anyhow::Result<()> {
+    let packages_to_versions =
+        future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
+            |package_name| async {
+                let returned_package_name = package_name.to_string();
+                let latest_version = node
+                    .npm_package_latest_version(package_name)
+                    .await
+                    .with_context(|| {
+                        format!("fetching latest npm version for package {returned_package_name}")
+                    })?;
+                anyhow::Ok((returned_package_name, latest_version))
+            },
+        ))
+        .await
+        .context("fetching latest npm versions")?;
+
+    log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
+    let borrowed_packages = packages_to_versions
+        .iter()
+        .map(|(package, version)| (package.as_str(), version.as_str()))
+        .collect::<Vec<_>>();
+    node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
+        .await
+        .context("fetching formatter packages")?;
+    anyhow::Ok(())
+}
+
+async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
+    let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
+    fs.save(
+        &prettier_wrapper_path,
+        &text::Rope::from(prettier::PRETTIER_SERVER_JS),
+        text::LineEnding::Unix,
+    )
+    .await
+    .with_context(|| {
+        format!(
+            "writing {} file at {prettier_wrapper_path:?}",
+            prettier::PRETTIER_SERVER_FILE
+        )
+    })?;
+    Ok(())
+}
+
+impl Project {
+    pub fn update_prettier_settings(
+        &self,
+        worktree: &ModelHandle<Worktree>,
+        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        cx: &mut ModelContext<'_, Project>,
+    ) {
+        let prettier_config_files = Prettier::CONFIG_FILE_NAMES
+            .iter()
+            .map(Path::new)
+            .collect::<HashSet<_>>();
+
+        let prettier_config_file_changed = changes
+            .iter()
+            .filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
+            .filter(|(path, _, _)| {
+                !path
+                    .components()
+                    .any(|component| component.as_os_str().to_string_lossy() == "node_modules")
+            })
+            .find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
+        let current_worktree_id = worktree.read(cx).id();
+        if let Some((config_path, _, _)) = prettier_config_file_changed {
+            log::info!(
+                "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
+            );
+            let prettiers_to_reload =
+                self.prettiers_per_worktree
+                    .get(&current_worktree_id)
+                    .iter()
+                    .flat_map(|prettier_paths| prettier_paths.iter())
+                    .flatten()
+                    .filter_map(|prettier_path| {
+                        Some((
+                            current_worktree_id,
+                            Some(prettier_path.clone()),
+                            self.prettier_instances.get(prettier_path)?.clone(),
+                        ))
+                    })
+                    .chain(self.default_prettier.instance().map(|default_prettier| {
+                        (current_worktree_id, None, default_prettier.clone())
+                    }))
+                    .collect::<Vec<_>>();
+
+            cx.background()
+                .spawn(async move {
+                    let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| {
+                        async move {
+                            if let Some(instance) = prettier_instance.prettier {
+                                match instance.await {
+                                    Ok(prettier) => {
+                                        prettier.clear_cache().log_err().await;
+                                    },
+                                    Err(e) => {
+                                        match prettier_path {
+                                            Some(prettier_path) => log::error!(
+                                                "Failed to clear prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
+                                            ),
+                                            None => log::error!(
+                                                "Failed to clear default prettier cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
+                                            ),
+                                        }
+                                    },
+                                }
+                            }
+                        }
+                    }))
+                    .await;
+                })
+                .detach();
+        }
+    }
+
+    fn prettier_instance_for_buffer(
+        &mut self,
+        buffer: &ModelHandle<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Option<(Option<PathBuf>, PrettierTask)>> {
+        let buffer = buffer.read(cx);
+        let buffer_file = buffer.file();
+        let Some(buffer_language) = buffer.language() else {
+            return Task::ready(None);
+        };
+        if buffer_language.prettier_parser_name().is_none() {
+            return Task::ready(None);
+        }
+
+        if self.is_local() {
+            let Some(node) = self.node.as_ref().map(Arc::clone) else {
+                return Task::ready(None);
+            };
+            match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
+            {
+                Some((worktree_id, buffer_path)) => {
+                    let fs = Arc::clone(&self.fs);
+                    let installed_prettiers = self.prettier_instances.keys().cloned().collect();
+                    return cx.spawn(|project, mut cx| async move {
+                        match cx
+                            .background()
+                            .spawn(async move {
+                                Prettier::locate_prettier_installation(
+                                    fs.as_ref(),
+                                    &installed_prettiers,
+                                    &buffer_path,
+                                )
+                                .await
+                            })
+                            .await
+                        {
+                            Ok(ControlFlow::Break(())) => {
+                                return None;
+                            }
+                            Ok(ControlFlow::Continue(None)) => {
+                                let default_instance = project.update(&mut cx, |project, cx| {
+                                    project
+                                        .prettiers_per_worktree
+                                        .entry(worktree_id)
+                                        .or_default()
+                                        .insert(None);
+                                    project.default_prettier.prettier_task(
+                                        &node,
+                                        Some(worktree_id),
+                                        cx,
+                                    )
+                                });
+                                Some((None, default_instance?.log_err().await?))
+                            }
+                            Ok(ControlFlow::Continue(Some(prettier_dir))) => {
+                                project.update(&mut cx, |project, _| {
+                                    project
+                                        .prettiers_per_worktree
+                                        .entry(worktree_id)
+                                        .or_default()
+                                        .insert(Some(prettier_dir.clone()))
+                                });
+                                if let Some(prettier_task) =
+                                    project.update(&mut cx, |project, cx| {
+                                        project.prettier_instances.get_mut(&prettier_dir).map(
+                                            |existing_instance| {
+                                                existing_instance.prettier_task(
+                                                    &node,
+                                                    Some(&prettier_dir),
+                                                    Some(worktree_id),
+                                                    cx,
+                                                )
+                                            },
+                                        )
+                                    })
+                                {
+                                    log::debug!(
+                                        "Found already started prettier in {prettier_dir:?}"
+                                    );
+                                    return Some((
+                                        Some(prettier_dir),
+                                        prettier_task?.await.log_err()?,
+                                    ));
+                                }
+
+                                log::info!("Found prettier in {prettier_dir:?}, starting.");
+                                let new_prettier_task = project.update(&mut cx, |project, cx| {
+                                    let new_prettier_task = start_prettier(
+                                        node,
+                                        prettier_dir.clone(),
+                                        Some(worktree_id),
+                                        cx,
+                                    );
+                                    project.prettier_instances.insert(
+                                        prettier_dir.clone(),
+                                        PrettierInstance {
+                                            attempt: 0,
+                                            prettier: Some(new_prettier_task.clone()),
+                                        },
+                                    );
+                                    new_prettier_task
+                                });
+                                Some((Some(prettier_dir), new_prettier_task))
+                            }
+                            Err(e) => {
+                                log::error!("Failed to determine prettier path for buffer: {e:#}");
+                                return None;
+                            }
+                        }
+                    });
+                }
+                None => {
+                    let new_task = self.default_prettier.prettier_task(&node, None, cx);
+                    return cx
+                        .spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) });
+                }
+            }
+        } else {
+            return Task::ready(None);
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn install_default_prettier(
+        &mut self,
+        _worktree: Option<WorktreeId>,
+        plugins: HashSet<&'static str>,
+        _cx: &mut ModelContext<Self>,
+    ) {
+        // suppress unused code warnings
+        let _ = install_prettier_packages;
+        let _ = save_prettier_server_file;
+
+        self.default_prettier.installed_plugins.extend(plugins);
+        self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance {
+            attempt: 0,
+            prettier: None,
+        });
+    }
+
+    #[cfg(not(any(test, feature = "test-support")))]
+    pub fn install_default_prettier(
+        &mut self,
+        worktree: Option<WorktreeId>,
+        mut new_plugins: HashSet<&'static str>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let Some(node) = self.node.as_ref().cloned() else {
+            return;
+        };
+        log::info!("Initializing default prettier with plugins {new_plugins:?}");
+        let fs = Arc::clone(&self.fs);
+        let locate_prettier_installation = match worktree.and_then(|worktree_id| {
+            self.worktree_for_id(worktree_id, cx)
+                .map(|worktree| worktree.read(cx).abs_path())
+        }) {
+            Some(locate_from) => {
+                let installed_prettiers = self.prettier_instances.keys().cloned().collect();
+                cx.background().spawn(async move {
+                    Prettier::locate_prettier_installation(
+                        fs.as_ref(),
+                        &installed_prettiers,
+                        locate_from.as_ref(),
+                    )
+                    .await
+                })
+            }
+            None => Task::ready(Ok(ControlFlow::Continue(None))),
+        };
+        new_plugins.retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin));
+        let mut installation_attempt = 0;
+        let previous_installation_task = match &mut self.default_prettier.prettier {
+            PrettierInstallation::NotInstalled {
+                installation_task,
+                attempts,
+                not_installed_plugins,
+            } => {
+                installation_attempt = *attempts;
+                if installation_attempt > prettier::FAIL_THRESHOLD {
+                    *installation_task = None;
+                    log::warn!(
+                        "Default prettier installation had failed {installation_attempt} times, not attempting again",
+                    );
+                    return;
+                }
+                new_plugins.extend(not_installed_plugins.iter());
+                installation_task.clone()
+            }
+            PrettierInstallation::Installed { .. } => {
+                if new_plugins.is_empty() {
+                    return;
+                }
+                None
+            }
+        };
+
+        let plugins_to_install = new_plugins.clone();
+        let fs = Arc::clone(&self.fs);
+        let new_installation_task = cx
+            .spawn(|project, mut cx| async move {
+                match locate_prettier_installation
+                    .await
+                    .context("locate prettier installation")
+                    .map_err(Arc::new)?
+                {
+                    ControlFlow::Break(()) => return Ok(()),
+                    ControlFlow::Continue(prettier_path) => {
+                        if prettier_path.is_some() {
+                            new_plugins.clear();
+                        }
+                        let mut needs_install = false;
+                        if let Some(previous_installation_task) = previous_installation_task {
+                            if let Err(e) = previous_installation_task.await {
+                                log::error!("Failed to install default prettier: {e:#}");
+                                project.update(&mut cx, |project, _| {
+                                    if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier {
+                                        *attempts += 1;
+                                        new_plugins.extend(not_installed_plugins.iter());
+                                        installation_attempt = *attempts;
+                                        needs_install = true;
+                                    };
+                                });
+                            }
+                        };
+                        if installation_attempt > prettier::FAIL_THRESHOLD {
+                            project.update(&mut cx, |project, _| {
+                                if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut project.default_prettier.prettier {
+                                    *installation_task = None;
+                                };
+                            });
+                            log::warn!(
+                                "Default prettier installation had failed {installation_attempt} times, not attempting again",
+                            );
+                            return Ok(());
+                        }
+                        project.update(&mut cx, |project, _| {
+                            new_plugins.retain(|plugin| {
+                                !project.default_prettier.installed_plugins.contains(plugin)
+                            });
+                            if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut project.default_prettier.prettier {
+                                not_installed_plugins.retain(|plugin| {
+                                    !project.default_prettier.installed_plugins.contains(plugin)
+                                });
+                                not_installed_plugins.extend(new_plugins.iter());
+                            }
+                            needs_install |= !new_plugins.is_empty();
+                        });
+                        if needs_install {
+                            let installed_plugins = new_plugins.clone();
+                            cx.background()
+                                .spawn(async move {
+                                    save_prettier_server_file(fs.as_ref()).await?;
+                                    install_prettier_packages(new_plugins, node).await
+                                })
+                                .await
+                                .context("prettier & plugins install")
+                                .map_err(Arc::new)?;
+                            log::info!("Initialized prettier with plugins: {installed_plugins:?}");
+                            project.update(&mut cx, |project, _| {
+                                project.default_prettier.prettier =
+                                    PrettierInstallation::Installed(PrettierInstance {
+                                        attempt: 0,
+                                        prettier: None,
+                                    });
+                                project.default_prettier
+                                    .installed_plugins
+                                    .extend(installed_plugins);
+                            });
+                        }
+                    }
+                }
+                Ok(())
+            })
+            .shared();
+        self.default_prettier.prettier = PrettierInstallation::NotInstalled {
+            attempts: installation_attempt,
+            installation_task: Some(new_installation_task),
+            not_installed_plugins: plugins_to_install,
+        };
+    }
+}

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

@@ -1,5 +1,6 @@
 mod ignore;
 mod lsp_command;
+mod prettier_support;
 pub mod project_settings;
 pub mod search;
 pub mod terminals;
@@ -20,7 +21,7 @@ use futures::{
         mpsc::{self, UnboundedReceiver},
         oneshot,
     },
-    future::{self, try_join_all, Shared},
+    future::{try_join_all, Shared},
     stream::FuturesUnordered,
     AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
 };
@@ -31,9 +32,7 @@ use gpui::{
 };
 use itertools::Itertools;
 use language::{
-    language_settings::{
-        language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
-    },
+    language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
     point_to_lsp,
     proto::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -54,7 +53,7 @@ use lsp_command::*;
 use node_runtime::NodeRuntime;
 use parking_lot::Mutex;
 use postage::watch;
-use prettier::Prettier;
+use prettier_support::{DefaultPrettier, PrettierInstance};
 use project_settings::{LspSettings, ProjectSettings};
 use rand::prelude::*;
 use search::SearchQuery;
@@ -72,7 +71,7 @@ use std::{
     hash::Hash,
     mem,
     num::NonZeroU32,
-    ops::{ControlFlow, Range},
+    ops::Range,
     path::{self, Component, Path, PathBuf},
     process::Stdio,
     str,
@@ -85,11 +84,8 @@ use std::{
 use terminals::Terminals;
 use text::Anchor;
 use util::{
-    debug_panic, defer,
-    http::HttpClient,
-    merge_json_value_into,
-    paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
-    post_inc, ResultExt, TryFutureExt as _,
+    debug_panic, defer, http::HttpClient, merge_json_value_into,
+    paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
 };
 
 pub use fs::*;
@@ -168,16 +164,9 @@ pub struct Project {
     copilot_log_subscription: Option<lsp::Subscription>,
     current_lsp_settings: HashMap<Arc<str>, LspSettings>,
     node: Option<Arc<dyn NodeRuntime>>,
-    default_prettier: Option<DefaultPrettier>,
+    default_prettier: DefaultPrettier,
     prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
-    prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
-}
-
-struct DefaultPrettier {
-    instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
-    installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
-    #[cfg(not(any(test, feature = "test-support")))]
-    installed_plugins: HashSet<&'static str>,
+    prettier_instances: HashMap<PathBuf, PrettierInstance>,
 }
 
 struct DelayedDebounced {
@@ -542,6 +531,14 @@ struct ProjectLspAdapterDelegate {
     http_client: Arc<dyn HttpClient>,
 }
 
+// Currently, formatting operations are represented differently depending on
+// whether they come from a language server or an external command.
+enum FormatOperation {
+    Lsp(Vec<(Range<Anchor>, String)>),
+    External(Diff),
+    Prettier(Diff),
+}
+
 impl FormatTrigger {
     fn from_proto(value: i32) -> FormatTrigger {
         match value {
@@ -690,7 +687,7 @@ impl Project {
                 copilot_log_subscription: None,
                 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
                 node: Some(node),
-                default_prettier: None,
+                default_prettier: DefaultPrettier::default(),
                 prettiers_per_worktree: HashMap::default(),
                 prettier_instances: HashMap::default(),
             }
@@ -791,7 +788,7 @@ impl Project {
                 copilot_log_subscription: None,
                 current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
                 node: None,
-                default_prettier: None,
+                default_prettier: DefaultPrettier::default(),
                 prettiers_per_worktree: HashMap::default(),
                 prettier_instances: HashMap::default(),
             };
@@ -928,8 +925,19 @@ impl Project {
                 .detach();
         }
 
+        let mut prettier_plugins_by_worktree = HashMap::default();
         for (worktree, language, settings) in language_formatters_to_check {
-            self.install_default_formatters(worktree, &language, &settings, cx);
+            if let Some(plugins) =
+                prettier_support::prettier_plugins_for_language(&language, &settings)
+            {
+                prettier_plugins_by_worktree
+                    .entry(worktree)
+                    .or_insert_with(|| HashSet::default())
+                    .extend(plugins);
+            }
+        }
+        for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
+            self.install_default_prettier(worktree, prettier_plugins, cx);
         }
 
         // Start all the newly-enabled language servers.
@@ -2633,8 +2641,9 @@ impl Project {
                 });
 
                 for (adapter, server) in servers {
-                    let workspace_config =
-                        cx.update(|cx| adapter.workspace_configuration(cx)).await;
+                    let workspace_config = cx
+                        .update(|cx| adapter.workspace_configuration(server.root_path(), cx))
+                        .await;
                     server
                         .notify::<lsp::notification::DidChangeConfiguration>(
                             lsp::DidChangeConfigurationParams {
@@ -2685,8 +2694,11 @@ impl Project {
         let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
         let buffer_file = File::from_dyn(buffer_file.as_ref());
         let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
-
-        self.install_default_formatters(worktree, &new_language, &settings, cx);
+        if let Some(prettier_plugins) =
+            prettier_support::prettier_plugins_for_language(&new_language, &settings)
+        {
+            self.install_default_prettier(worktree, prettier_plugins, cx);
+        };
         if let Some(file) = buffer_file {
             let worktree = file.worktree.clone();
             if let Some(tree) = worktree.read(cx).as_local() {
@@ -2742,7 +2754,7 @@ impl Project {
             stderr_capture.clone(),
             language.clone(),
             adapter.clone(),
-            worktree_path,
+            Arc::clone(&worktree_path),
             ProjectLspAdapterDelegate::new(self, cx),
             cx,
         ) {
@@ -2765,6 +2777,7 @@ impl Project {
             cx.spawn_weak(|this, mut cx| async move {
                 let result = Self::setup_and_insert_language_server(
                     this,
+                    &worktree_path,
                     override_options,
                     pending_server,
                     adapter.clone(),
@@ -2880,6 +2893,7 @@ impl Project {
 
     async fn setup_and_insert_language_server(
         this: WeakModelHandle<Self>,
+        worktree_path: &Path,
         override_initialization_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
         adapter: Arc<CachedLspAdapter>,
@@ -2892,6 +2906,7 @@ impl Project {
             this,
             override_initialization_options,
             pending_server,
+            worktree_path,
             adapter.clone(),
             server_id,
             cx,
@@ -2921,11 +2936,14 @@ impl Project {
         this: WeakModelHandle<Self>,
         override_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
+        worktree_path: &Path,
         adapter: Arc<CachedLspAdapter>,
         server_id: LanguageServerId,
         cx: &mut AsyncAppContext,
     ) -> Result<Arc<LanguageServer>> {
-        let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await;
+        let workspace_config = cx
+            .update(|cx| adapter.workspace_configuration(worktree_path, cx))
+            .await;
         let language_server = pending_server.task.await?;
 
         language_server
@@ -2953,11 +2971,14 @@ impl Project {
         language_server
             .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
                 let adapter = adapter.clone();
+                let worktree_path = worktree_path.to_path_buf();
                 move |params, mut cx| {
                     let adapter = adapter.clone();
+                    let worktree_path = worktree_path.clone();
                     async move {
-                        let workspace_config =
-                            cx.update(|cx| adapter.workspace_configuration(cx)).await;
+                        let workspace_config = cx
+                            .update(|cx| adapter.workspace_configuration(&worktree_path, cx))
+                            .await;
                         Ok(params
                             .items
                             .into_iter()
@@ -4073,8 +4094,6 @@ impl Project {
 
                     let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
                     let ensure_final_newline = settings.ensure_final_newline_on_save;
-                    let format_on_save = settings.format_on_save.clone();
-                    let formatter = settings.formatter.clone();
                     let tab_size = settings.tab_size;
 
                     // First, format buffer's whitespace according to the settings.
@@ -4099,18 +4118,10 @@ impl Project {
                         buffer.end_transaction(cx)
                     });
 
-                    // Currently, formatting operations are represented differently depending on
-                    // whether they come from a language server or an external command.
-                    enum FormatOperation {
-                        Lsp(Vec<(Range<Anchor>, String)>),
-                        External(Diff),
-                        Prettier(Diff),
-                    }
-
                     // Apply language-specific formatting using either a language server
                     // or external command.
                     let mut format_operation = None;
-                    match (formatter, format_on_save) {
+                    match (&settings.formatter, &settings.format_on_save) {
                         (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
 
                         (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
@@ -4155,46 +4166,11 @@ impl Project {
                             }
                         }
                         (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
-                            if let Some((prettier_path, prettier_task)) = project
-                                .update(&mut cx, |project, cx| {
-                                    project.prettier_instance_for_buffer(buffer, cx)
-                                }).await {
-                                    match prettier_task.await
-                                    {
-                                        Ok(prettier) => {
-                                            let buffer_path = buffer.update(&mut cx, |buffer, cx| {
-                                                File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
-                                            });
-                                            format_operation = Some(FormatOperation::Prettier(
-                                                prettier
-                                                    .format(buffer, buffer_path, &cx)
-                                                    .await
-                                                    .context("formatting via prettier")?,
-                                            ));
-                                        }
-                                        Err(e) => {
-                                            project.update(&mut cx, |project, _| {
-                                                match &prettier_path {
-                                                    Some(prettier_path) => {
-                                                        project.prettier_instances.remove(prettier_path);
-                                                    },
-                                                    None => {
-                                                        if let Some(default_prettier) = project.default_prettier.as_mut() {
-                                                            default_prettier.instance = None;
-                                                        }
-                                                    },
-                                                }
-                                            });
-                                            match &prettier_path {
-                                                Some(prettier_path) => {
-                                                    log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
-                                                },
-                                                None => {
-                                                    log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
-                                                },
-                                            }
-                                        }
-                                    }
+                            if let Some(new_operation) =
+                                prettier_support::format_with_prettier(&project, buffer, &mut cx)
+                                    .await
+                            {
+                                format_operation = Some(new_operation);
                             } else if let Some((language_server, buffer_abs_path)) =
                                 language_server.as_ref().zip(buffer_abs_path.as_ref())
                             {
@@ -4212,48 +4188,13 @@ impl Project {
                                 ));
                             }
                         }
-                        (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
-                            if let Some((prettier_path, prettier_task)) = project
-                                .update(&mut cx, |project, cx| {
-                                    project.prettier_instance_for_buffer(buffer, cx)
-                                }).await {
-                                    match prettier_task.await
-                                    {
-                                        Ok(prettier) => {
-                                            let buffer_path = buffer.update(&mut cx, |buffer, cx| {
-                                                File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
-                                            });
-                                            format_operation = Some(FormatOperation::Prettier(
-                                                prettier
-                                                    .format(buffer, buffer_path, &cx)
-                                                    .await
-                                                    .context("formatting via prettier")?,
-                                            ));
-                                        }
-                                        Err(e) => {
-                                            project.update(&mut cx, |project, _| {
-                                                match &prettier_path {
-                                                    Some(prettier_path) => {
-                                                        project.prettier_instances.remove(prettier_path);
-                                                    },
-                                                    None => {
-                                                        if let Some(default_prettier) = project.default_prettier.as_mut() {
-                                                            default_prettier.instance = None;
-                                                        }
-                                                    },
-                                                }
-                                            });
-                                            match &prettier_path {
-                                                Some(prettier_path) => {
-                                                    log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
-                                                },
-                                                None => {
-                                                    log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
-                                                },
-                                            }
-                                        }
-                                    }
-                                }
+                        (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
+                            if let Some(new_operation) =
+                                prettier_support::format_with_prettier(&project, buffer, &mut cx)
+                                    .await
+                            {
+                                format_operation = Some(new_operation);
+                            }
                         }
                     };
 
@@ -6566,85 +6507,6 @@ impl Project {
         .detach();
     }
 
-    fn update_prettier_settings(
-        &self,
-        worktree: &ModelHandle<Worktree>,
-        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
-        cx: &mut ModelContext<'_, Project>,
-    ) {
-        let prettier_config_files = Prettier::CONFIG_FILE_NAMES
-            .iter()
-            .map(Path::new)
-            .collect::<HashSet<_>>();
-
-        let prettier_config_file_changed = changes
-            .iter()
-            .filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
-            .filter(|(path, _, _)| {
-                !path
-                    .components()
-                    .any(|component| component.as_os_str().to_string_lossy() == "node_modules")
-            })
-            .find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
-        let current_worktree_id = worktree.read(cx).id();
-        if let Some((config_path, _, _)) = prettier_config_file_changed {
-            log::info!(
-                "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
-            );
-            let prettiers_to_reload = self
-                .prettiers_per_worktree
-                .get(&current_worktree_id)
-                .iter()
-                .flat_map(|prettier_paths| prettier_paths.iter())
-                .flatten()
-                .filter_map(|prettier_path| {
-                    Some((
-                        current_worktree_id,
-                        Some(prettier_path.clone()),
-                        self.prettier_instances.get(prettier_path)?.clone(),
-                    ))
-                })
-                .chain(self.default_prettier.iter().filter_map(|default_prettier| {
-                    Some((
-                        current_worktree_id,
-                        None,
-                        default_prettier.instance.clone()?,
-                    ))
-                }))
-                .collect::<Vec<_>>();
-
-            cx.background()
-                .spawn(async move {
-                    for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
-                        async move {
-                            prettier_task.await?
-                                .clear_cache()
-                                .await
-                                .with_context(|| {
-                                    match prettier_path {
-                                        Some(prettier_path) => format!(
-                                            "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
-                                        ),
-                                        None => format!(
-                                            "clearing default prettier cache for worktree {worktree_id:?} on prettier settings update"
-                                        ),
-                                    }
-
-                                })
-                                .map_err(Arc::new)
-                        }
-                    }))
-                    .await
-                    {
-                        if let Err(e) = task_result {
-                            log::error!("Failed to clear cache for prettier: {e:#}");
-                        }
-                    }
-                })
-                .detach();
-        }
-    }
-
     pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
         let new_active_entry = entry.and_then(|project_path| {
             let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@@ -6671,9 +6533,15 @@ impl Project {
             })
     }
 
-    pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary {
+    pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary {
         let mut summary = DiagnosticSummary::default();
-        for (_, _, path_summary) in self.diagnostic_summaries(cx) {
+        for (_, _, path_summary) in
+            self.diagnostic_summaries(include_ignored, cx)
+                .filter(|(path, _, _)| {
+                    let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored);
+                    include_ignored || worktree == Some(false)
+                })
+        {
             summary.error_count += path_summary.error_count;
             summary.warning_count += path_summary.warning_count;
         }
@@ -6682,6 +6550,7 @@ impl Project {
 
     pub fn diagnostic_summaries<'a>(
         &'a self,
+        include_ignored: bool,
         cx: &'a AppContext,
     ) -> impl Iterator<Item = (ProjectPath, LanguageServerId, DiagnosticSummary)> + 'a {
         self.visible_worktrees(cx).flat_map(move |worktree| {
@@ -6692,6 +6561,10 @@ impl Project {
                 .map(move |(path, server_id, summary)| {
                     (ProjectPath { worktree_id, path }, server_id, summary)
                 })
+                .filter(move |(path, _, _)| {
+                    let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored);
+                    include_ignored || worktree == Some(false)
+                })
         })
     }
 
@@ -8536,446 +8409,6 @@ impl Project {
             Vec::new()
         }
     }
-
-    fn prettier_instance_for_buffer(
-        &mut self,
-        buffer: &ModelHandle<Buffer>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<
-        Option<(
-            Option<PathBuf>,
-            Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
-        )>,
-    > {
-        let buffer = buffer.read(cx);
-        let buffer_file = buffer.file();
-        let Some(buffer_language) = buffer.language() else {
-            return Task::ready(None);
-        };
-        if buffer_language.prettier_parser_name().is_none() {
-            return Task::ready(None);
-        }
-
-        if self.is_local() {
-            let Some(node) = self.node.as_ref().map(Arc::clone) else {
-                return Task::ready(None);
-            };
-            match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
-            {
-                Some((worktree_id, buffer_path)) => {
-                    let fs = Arc::clone(&self.fs);
-                    let installed_prettiers = self.prettier_instances.keys().cloned().collect();
-                    return cx.spawn(|project, mut cx| async move {
-                        match cx
-                            .background()
-                            .spawn(async move {
-                                Prettier::locate_prettier_installation(
-                                    fs.as_ref(),
-                                    &installed_prettiers,
-                                    &buffer_path,
-                                )
-                                .await
-                            })
-                            .await
-                        {
-                            Ok(ControlFlow::Break(())) => {
-                                return None;
-                            }
-                            Ok(ControlFlow::Continue(None)) => {
-                                let started_default_prettier =
-                                    project.update(&mut cx, |project, _| {
-                                        project
-                                            .prettiers_per_worktree
-                                            .entry(worktree_id)
-                                            .or_default()
-                                            .insert(None);
-                                        project.default_prettier.as_ref().and_then(
-                                            |default_prettier| default_prettier.instance.clone(),
-                                        )
-                                    });
-                                match started_default_prettier {
-                                    Some(old_task) => return Some((None, old_task)),
-                                    None => {
-                                        let new_default_prettier = project
-                                            .update(&mut cx, |_, cx| {
-                                                start_default_prettier(node, Some(worktree_id), cx)
-                                            })
-                                            .await;
-                                        return Some((None, new_default_prettier));
-                                    }
-                                }
-                            }
-                            Ok(ControlFlow::Continue(Some(prettier_dir))) => {
-                                project.update(&mut cx, |project, _| {
-                                    project
-                                        .prettiers_per_worktree
-                                        .entry(worktree_id)
-                                        .or_default()
-                                        .insert(Some(prettier_dir.clone()))
-                                });
-                                if let Some(existing_prettier) =
-                                    project.update(&mut cx, |project, _| {
-                                        project.prettier_instances.get(&prettier_dir).cloned()
-                                    })
-                                {
-                                    log::debug!(
-                                        "Found already started prettier in {prettier_dir:?}"
-                                    );
-                                    return Some((Some(prettier_dir), existing_prettier));
-                                }
-
-                                log::info!("Found prettier in {prettier_dir:?}, starting.");
-                                let new_prettier_task = project.update(&mut cx, |project, cx| {
-                                    let new_prettier_task = start_prettier(
-                                        node,
-                                        prettier_dir.clone(),
-                                        Some(worktree_id),
-                                        cx,
-                                    );
-                                    project
-                                        .prettier_instances
-                                        .insert(prettier_dir.clone(), new_prettier_task.clone());
-                                    new_prettier_task
-                                });
-                                Some((Some(prettier_dir), new_prettier_task))
-                            }
-                            Err(e) => {
-                                return Some((
-                                    None,
-                                    Task::ready(Err(Arc::new(
-                                        e.context("determining prettier path"),
-                                    )))
-                                    .shared(),
-                                ));
-                            }
-                        }
-                    });
-                }
-                None => {
-                    let started_default_prettier = self
-                        .default_prettier
-                        .as_ref()
-                        .and_then(|default_prettier| default_prettier.instance.clone());
-                    match started_default_prettier {
-                        Some(old_task) => return Task::ready(Some((None, old_task))),
-                        None => {
-                            let new_task = start_default_prettier(node, None, cx);
-                            return cx.spawn(|_, _| async move { Some((None, new_task.await)) });
-                        }
-                    }
-                }
-            }
-        } else if self.remote_id().is_some() {
-            return Task::ready(None);
-        } else {
-            Task::ready(Some((
-                None,
-                Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
-            )))
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    fn install_default_formatters(
-        &mut self,
-        _worktree: Option<WorktreeId>,
-        _new_language: &Language,
-        _language_settings: &LanguageSettings,
-        _cx: &mut ModelContext<Self>,
-    ) {
-    }
-
-    #[cfg(not(any(test, feature = "test-support")))]
-    fn install_default_formatters(
-        &mut self,
-        worktree: Option<WorktreeId>,
-        new_language: &Language,
-        language_settings: &LanguageSettings,
-        cx: &mut ModelContext<Self>,
-    ) {
-        match &language_settings.formatter {
-            Formatter::Prettier { .. } | Formatter::Auto => {}
-            Formatter::LanguageServer | Formatter::External { .. } => return,
-        };
-        let Some(node) = self.node.as_ref().cloned() else {
-            return;
-        };
-
-        let mut prettier_plugins = None;
-        if new_language.prettier_parser_name().is_some() {
-            prettier_plugins
-                .get_or_insert_with(|| HashSet::<&'static str>::default())
-                .extend(
-                    new_language
-                        .lsp_adapters()
-                        .iter()
-                        .flat_map(|adapter| adapter.prettier_plugins()),
-                )
-        }
-        let Some(prettier_plugins) = prettier_plugins else {
-            return;
-        };
-
-        let fs = Arc::clone(&self.fs);
-        let locate_prettier_installation = match worktree.and_then(|worktree_id| {
-            self.worktree_for_id(worktree_id, cx)
-                .map(|worktree| worktree.read(cx).abs_path())
-        }) {
-            Some(locate_from) => {
-                let installed_prettiers = self.prettier_instances.keys().cloned().collect();
-                cx.background().spawn(async move {
-                    Prettier::locate_prettier_installation(
-                        fs.as_ref(),
-                        &installed_prettiers,
-                        locate_from.as_ref(),
-                    )
-                    .await
-                })
-            }
-            None => Task::ready(Ok(ControlFlow::Break(()))),
-        };
-        let mut plugins_to_install = prettier_plugins;
-        let previous_installation_process =
-            if let Some(default_prettier) = &mut self.default_prettier {
-                plugins_to_install
-                    .retain(|plugin| !default_prettier.installed_plugins.contains(plugin));
-                if plugins_to_install.is_empty() {
-                    return;
-                }
-                default_prettier.installation_process.clone()
-            } else {
-                None
-            };
-        let fs = Arc::clone(&self.fs);
-        let default_prettier = self
-            .default_prettier
-            .get_or_insert_with(|| DefaultPrettier {
-                instance: None,
-                installation_process: None,
-                installed_plugins: HashSet::default(),
-            });
-        default_prettier.installation_process = Some(
-            cx.spawn(|this, mut cx| async move {
-                match locate_prettier_installation
-                    .await
-                    .context("locate prettier installation")
-                    .map_err(Arc::new)?
-                {
-                    ControlFlow::Break(()) => return Ok(()),
-                    ControlFlow::Continue(Some(_non_default_prettier)) => return Ok(()),
-                    ControlFlow::Continue(None) => {
-                        let mut needs_install = match previous_installation_process {
-                            Some(previous_installation_process) => {
-                                previous_installation_process.await.is_err()
-                            }
-                            None => true,
-                        };
-                        this.update(&mut cx, |this, _| {
-                            if let Some(default_prettier) = &mut this.default_prettier {
-                                plugins_to_install.retain(|plugin| {
-                                    !default_prettier.installed_plugins.contains(plugin)
-                                });
-                                needs_install |= !plugins_to_install.is_empty();
-                            }
-                        });
-                        if needs_install {
-                            let installed_plugins = plugins_to_install.clone();
-                            cx.background()
-                                .spawn(async move {
-                                    install_default_prettier(plugins_to_install, node, fs).await
-                                })
-                                .await
-                                .context("prettier & plugins install")
-                                .map_err(Arc::new)?;
-                            this.update(&mut cx, |this, _| {
-                                let default_prettier =
-                                    this.default_prettier
-                                        .get_or_insert_with(|| DefaultPrettier {
-                                            instance: None,
-                                            installation_process: Some(
-                                                Task::ready(Ok(())).shared(),
-                                            ),
-                                            installed_plugins: HashSet::default(),
-                                        });
-                                default_prettier.instance = None;
-                                default_prettier.installed_plugins.extend(installed_plugins);
-                            });
-                        }
-                    }
-                }
-                Ok(())
-            })
-            .shared(),
-        );
-    }
-}
-
-fn start_default_prettier(
-    node: Arc<dyn NodeRuntime>,
-    worktree_id: Option<WorktreeId>,
-    cx: &mut ModelContext<'_, Project>,
-) -> Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>> {
-    cx.spawn(|project, mut cx| async move {
-        loop {
-            let default_prettier_installing = project.update(&mut cx, |project, _| {
-                project
-                    .default_prettier
-                    .as_ref()
-                    .and_then(|default_prettier| default_prettier.installation_process.clone())
-            });
-            match default_prettier_installing {
-                Some(installation_task) => {
-                    if installation_task.await.is_ok() {
-                        break;
-                    }
-                }
-                None => break,
-            }
-        }
-
-        project.update(&mut cx, |project, cx| {
-            match project
-                .default_prettier
-                .as_mut()
-                .and_then(|default_prettier| default_prettier.instance.as_mut())
-            {
-                Some(default_prettier) => default_prettier.clone(),
-                None => {
-                    let new_default_prettier =
-                        start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
-                    project
-                        .default_prettier
-                        .get_or_insert_with(|| DefaultPrettier {
-                            instance: None,
-                            installation_process: None,
-                            #[cfg(not(any(test, feature = "test-support")))]
-                            installed_plugins: HashSet::default(),
-                        })
-                        .instance = Some(new_default_prettier.clone());
-                    new_default_prettier
-                }
-            }
-        })
-    })
-}
-
-fn start_prettier(
-    node: Arc<dyn NodeRuntime>,
-    prettier_dir: PathBuf,
-    worktree_id: Option<WorktreeId>,
-    cx: &mut ModelContext<'_, Project>,
-) -> Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>> {
-    cx.spawn(|project, mut cx| async move {
-        let new_server_id = project.update(&mut cx, |project, _| {
-            project.languages.next_language_server_id()
-        });
-        let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
-            .await
-            .context("default prettier spawn")
-            .map(Arc::new)
-            .map_err(Arc::new)?;
-        register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
-        Ok(new_prettier)
-    })
-    .shared()
-}
-
-fn register_new_prettier(
-    project: &ModelHandle<Project>,
-    prettier: &Prettier,
-    worktree_id: Option<WorktreeId>,
-    new_server_id: LanguageServerId,
-    cx: &mut AsyncAppContext,
-) {
-    let prettier_dir = prettier.prettier_dir();
-    let is_default = prettier.is_default();
-    if is_default {
-        log::info!("Started default prettier in {prettier_dir:?}");
-    } else {
-        log::info!("Started prettier in {prettier_dir:?}");
-    }
-    if let Some(prettier_server) = prettier.server() {
-        project.update(cx, |project, cx| {
-            let name = if is_default {
-                LanguageServerName(Arc::from("prettier (default)"))
-            } else {
-                let worktree_path = worktree_id
-                    .and_then(|id| project.worktree_for_id(id, cx))
-                    .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
-                let name = match worktree_path {
-                    Some(worktree_path) => {
-                        if prettier_dir == worktree_path.as_ref() {
-                            let name = prettier_dir
-                                .file_name()
-                                .and_then(|name| name.to_str())
-                                .unwrap_or_default();
-                            format!("prettier ({name})")
-                        } else {
-                            let dir_to_display = prettier_dir
-                                .strip_prefix(worktree_path.as_ref())
-                                .ok()
-                                .unwrap_or(prettier_dir);
-                            format!("prettier ({})", dir_to_display.display())
-                        }
-                    }
-                    None => format!("prettier ({})", prettier_dir.display()),
-                };
-                LanguageServerName(Arc::from(name))
-            };
-            project
-                .supplementary_language_servers
-                .insert(new_server_id, (name, Arc::clone(prettier_server)));
-            cx.emit(Event::LanguageServerAdded(new_server_id));
-        });
-    }
-}
-
-#[cfg(not(any(test, feature = "test-support")))]
-async fn install_default_prettier(
-    plugins_to_install: HashSet<&'static str>,
-    node: Arc<dyn NodeRuntime>,
-    fs: Arc<dyn Fs>,
-) -> anyhow::Result<()> {
-    let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
-    // method creates parent directory if it doesn't exist
-    fs.save(
-        &prettier_wrapper_path,
-        &text::Rope::from(prettier::PRETTIER_SERVER_JS),
-        text::LineEnding::Unix,
-    )
-    .await
-    .with_context(|| {
-        format!(
-            "writing {} file at {prettier_wrapper_path:?}",
-            prettier::PRETTIER_SERVER_FILE
-        )
-    })?;
-
-    let packages_to_versions =
-        future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
-            |package_name| async {
-                let returned_package_name = package_name.to_string();
-                let latest_version = node
-                    .npm_package_latest_version(package_name)
-                    .await
-                    .with_context(|| {
-                        format!("fetching latest npm version for package {returned_package_name}")
-                    })?;
-                anyhow::Ok((returned_package_name, latest_version))
-            },
-        ))
-        .await
-        .context("fetching latest npm versions")?;
-
-    log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
-    let borrowed_packages = packages_to_versions
-        .iter()
-        .map(|(package, version)| (package.as_str(), version.as_str()))
-        .collect::<Vec<_>>();
-    node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
-        .await
-        .context("fetching formatter packages")?;
-    anyhow::Ok(())
 }
 
 fn subscribe_for_copilot_events(

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

@@ -806,7 +806,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
 }
 
 #[gpui::test]
-async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
+async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
     let fs = FakeFs::new(cx.background());
@@ -814,7 +814,12 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
         "/root",
         json!({
             "dir": {
+                ".git": {
+                    "HEAD": "ref: refs/heads/main",
+                },
+                ".gitignore": "b.rs",
                 "a.rs": "let a = 1;",
+                "b.rs": "let b = 2;",
             },
             "other.rs": "let b = c;"
         }),
@@ -822,6 +827,13 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
+    let (worktree, _) = project
+        .update(cx, |project, cx| {
+            project.find_or_create_local_worktree("/root/dir", true, cx)
+        })
+        .await
+        .unwrap();
+    let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 
     let (worktree, _) = project
         .update(cx, |project, cx| {
@@ -829,12 +841,30 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
         })
         .await
         .unwrap();
-    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
+    let other_worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 
+    let server_id = LanguageServerId(0);
     project.update(cx, |project, cx| {
         project
             .update_diagnostics(
-                LanguageServerId(0),
+                server_id,
+                lsp::PublishDiagnosticsParams {
+                    uri: Url::from_file_path("/root/dir/b.rs").unwrap(),
+                    version: None,
+                    diagnostics: vec![lsp::Diagnostic {
+                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
+                        severity: Some(lsp::DiagnosticSeverity::ERROR),
+                        message: "unused variable 'b'".to_string(),
+                        ..Default::default()
+                    }],
+                },
+                &[],
+                cx,
+            )
+            .unwrap();
+        project
+            .update_diagnostics(
+                server_id,
                 lsp::PublishDiagnosticsParams {
                     uri: Url::from_file_path("/root/other.rs").unwrap(),
                     version: None,
@@ -851,11 +881,34 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
             .unwrap();
     });
 
-    let buffer = project
-        .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
+    let main_ignored_buffer = project
+        .update(cx, |project, cx| {
+            project.open_buffer((main_worktree_id, "b.rs"), cx)
+        })
         .await
         .unwrap();
-    buffer.read_with(cx, |buffer, _| {
+    main_ignored_buffer.read_with(cx, |buffer, _| {
+        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+        assert_eq!(
+            chunks
+                .iter()
+                .map(|(s, d)| (s.as_str(), *d))
+                .collect::<Vec<_>>(),
+            &[
+                ("let ", None),
+                ("b", Some(DiagnosticSeverity::ERROR)),
+                (" = 2;", None),
+            ],
+            "Gigitnored buffers should still get in-buffer diagnostics",
+        );
+    });
+    let other_buffer = project
+        .update(cx, |project, cx| {
+            project.open_buffer((other_worktree_id, ""), cx)
+        })
+        .await
+        .unwrap();
+    other_buffer.read_with(cx, |buffer, _| {
         let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
         assert_eq!(
             chunks
@@ -866,13 +919,29 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
                 ("let b = ", None),
                 ("c", Some(DiagnosticSeverity::ERROR)),
                 (";", None),
-            ]
+            ],
+            "Buffers from hidden projects should still get in-buffer diagnostics"
         );
     });
 
     project.read_with(cx, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).next(), None);
-        assert_eq!(project.diagnostic_summary(cx).error_count, 0);
+        assert_eq!(project.diagnostic_summaries(false, cx).next(), None);
+        assert_eq!(
+            project.diagnostic_summaries(true, cx).collect::<Vec<_>>(),
+            vec![(
+                ProjectPath {
+                    worktree_id: main_worktree_id,
+                    path: Arc::from(Path::new("b.rs")),
+                },
+                server_id,
+                DiagnosticSummary {
+                    error_count: 1,
+                    warning_count: 0,
+                }
+            )]
+        );
+        assert_eq!(project.diagnostic_summary(false, cx).error_count, 0);
+        assert_eq!(project.diagnostic_summary(true, cx).error_count, 1);
     });
 }
 
@@ -1145,7 +1214,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
     });
     project.read_with(cx, |project, cx| {
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 1,
                 warning_count: 0,
@@ -1171,7 +1240,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
     });
     project.read_with(cx, |project, cx| {
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 0,
                 warning_count: 0,
@@ -1763,7 +1832,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
             .unwrap();
 
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 2,
                 warning_count: 0,

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

@@ -717,8 +717,9 @@ async fn location_links_from_lsp(
             })?
             .await?;
 
-        buffer.update(&mut cx, |origin_buffer, cx| {
+        cx.update(|cx| {
             let origin_location = origin_range.map(|origin_range| {
+                let origin_buffer = buffer.read(cx);
                 let origin_start =
                     origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left);
                 let origin_end =

crates/project2/src/prettier_support.rs πŸ”—

@@ -0,0 +1,772 @@
+use std::{
+    ops::ControlFlow,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+
+use anyhow::Context;
+use collections::HashSet;
+use fs::Fs;
+use futures::{
+    future::{self, Shared},
+    FutureExt,
+};
+use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
+use language::{
+    language_settings::{Formatter, LanguageSettings},
+    Buffer, Language, LanguageServerName, LocalFile,
+};
+use lsp::LanguageServerId;
+use node_runtime::NodeRuntime;
+use prettier::Prettier;
+use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt};
+
+use crate::{
+    Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
+};
+
+pub fn prettier_plugins_for_language(
+    language: &Language,
+    language_settings: &LanguageSettings,
+) -> Option<HashSet<&'static str>> {
+    match &language_settings.formatter {
+        Formatter::Prettier { .. } | Formatter::Auto => {}
+        Formatter::LanguageServer | Formatter::External { .. } => return None,
+    };
+    let mut prettier_plugins = None;
+    if language.prettier_parser_name().is_some() {
+        prettier_plugins
+            .get_or_insert_with(|| HashSet::default())
+            .extend(
+                language
+                    .lsp_adapters()
+                    .iter()
+                    .flat_map(|adapter| adapter.prettier_plugins()),
+            )
+    }
+
+    prettier_plugins
+}
+
+pub(super) async fn format_with_prettier(
+    project: &WeakModel<Project>,
+    buffer: &Model<Buffer>,
+    cx: &mut AsyncAppContext,
+) -> Option<FormatOperation> {
+    if let Some((prettier_path, prettier_task)) = project
+        .update(cx, |project, cx| {
+            project.prettier_instance_for_buffer(buffer, cx)
+        })
+        .ok()?
+        .await
+    {
+        match prettier_task.await {
+            Ok(prettier) => {
+                let buffer_path = buffer
+                    .update(cx, |buffer, cx| {
+                        File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
+                    })
+                    .ok()?;
+                match prettier.format(buffer, buffer_path, cx).await {
+                    Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)),
+                    Err(e) => {
+                        log::error!(
+                            "Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}"
+                        );
+                    }
+                }
+            }
+            Err(e) => project
+                .update(cx, |project, _| {
+                    let instance_to_update = match prettier_path {
+                        Some(prettier_path) => {
+                            log::error!(
+                            "Prettier instance from path {prettier_path:?} failed to spawn: {e:#}"
+                        );
+                            project.prettier_instances.get_mut(&prettier_path)
+                        }
+                        None => {
+                            log::error!("Default prettier instance failed to spawn: {e:#}");
+                            match &mut project.default_prettier.prettier {
+                                PrettierInstallation::NotInstalled { .. } => None,
+                                PrettierInstallation::Installed(instance) => Some(instance),
+                            }
+                        }
+                    };
+
+                    if let Some(instance) = instance_to_update {
+                        instance.attempt += 1;
+                        instance.prettier = None;
+                    }
+                })
+                .ok()?,
+        }
+    }
+
+    None
+}
+
+pub struct DefaultPrettier {
+    prettier: PrettierInstallation,
+    installed_plugins: HashSet<&'static str>,
+}
+
+pub enum PrettierInstallation {
+    NotInstalled {
+        attempts: usize,
+        installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
+        not_installed_plugins: HashSet<&'static str>,
+    },
+    Installed(PrettierInstance),
+}
+
+pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
+
+#[derive(Clone)]
+pub struct PrettierInstance {
+    attempt: usize,
+    prettier: Option<PrettierTask>,
+}
+
+impl Default for DefaultPrettier {
+    fn default() -> Self {
+        Self {
+            prettier: PrettierInstallation::NotInstalled {
+                attempts: 0,
+                installation_task: None,
+                not_installed_plugins: HashSet::default(),
+            },
+            installed_plugins: HashSet::default(),
+        }
+    }
+}
+
+impl DefaultPrettier {
+    pub fn instance(&self) -> Option<&PrettierInstance> {
+        if let PrettierInstallation::Installed(instance) = &self.prettier {
+            Some(instance)
+        } else {
+            None
+        }
+    }
+
+    pub fn prettier_task(
+        &mut self,
+        node: &Arc<dyn NodeRuntime>,
+        worktree_id: Option<WorktreeId>,
+        cx: &mut ModelContext<'_, Project>,
+    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
+        match &mut self.prettier {
+            PrettierInstallation::NotInstalled { .. } => {
+                Some(start_default_prettier(Arc::clone(node), worktree_id, cx))
+            }
+            PrettierInstallation::Installed(existing_instance) => {
+                existing_instance.prettier_task(node, None, worktree_id, cx)
+            }
+        }
+    }
+}
+
+impl PrettierInstance {
+    pub fn prettier_task(
+        &mut self,
+        node: &Arc<dyn NodeRuntime>,
+        prettier_dir: Option<&Path>,
+        worktree_id: Option<WorktreeId>,
+        cx: &mut ModelContext<'_, Project>,
+    ) -> Option<Task<anyhow::Result<PrettierTask>>> {
+        if self.attempt > prettier::FAIL_THRESHOLD {
+            match prettier_dir {
+                Some(prettier_dir) => log::warn!(
+                    "Prettier from path {prettier_dir:?} exceeded launch threshold, not starting"
+                ),
+                None => log::warn!("Default prettier exceeded launch threshold, not starting"),
+            }
+            return None;
+        }
+        Some(match &self.prettier {
+            Some(prettier_task) => Task::ready(Ok(prettier_task.clone())),
+            None => match prettier_dir {
+                Some(prettier_dir) => {
+                    let new_task = start_prettier(
+                        Arc::clone(node),
+                        prettier_dir.to_path_buf(),
+                        worktree_id,
+                        cx,
+                    );
+                    self.attempt += 1;
+                    self.prettier = Some(new_task.clone());
+                    Task::ready(Ok(new_task))
+                }
+                None => {
+                    self.attempt += 1;
+                    let node = Arc::clone(node);
+                    cx.spawn(|project, mut cx| async move {
+                        project
+                            .update(&mut cx, |_, cx| {
+                                start_default_prettier(node, worktree_id, cx)
+                            })?
+                            .await
+                    })
+                }
+            },
+        })
+    }
+}
+
+fn start_default_prettier(
+    node: Arc<dyn NodeRuntime>,
+    worktree_id: Option<WorktreeId>,
+    cx: &mut ModelContext<'_, Project>,
+) -> Task<anyhow::Result<PrettierTask>> {
+    cx.spawn(|project, mut cx| async move {
+        loop {
+            let installation_task = project.update(&mut cx, |project, _| {
+                match &project.default_prettier.prettier {
+                    PrettierInstallation::NotInstalled {
+                        installation_task, ..
+                    } => ControlFlow::Continue(installation_task.clone()),
+                    PrettierInstallation::Installed(default_prettier) => {
+                        ControlFlow::Break(default_prettier.clone())
+                    }
+                }
+            })?;
+            match installation_task {
+                ControlFlow::Continue(None) => {
+                    anyhow::bail!("Default prettier is not installed and cannot be started")
+                }
+                ControlFlow::Continue(Some(installation_task)) => {
+                    log::info!("Waiting for default prettier to install");
+                    if let Err(e) = installation_task.await {
+                        project.update(&mut cx, |project, _| {
+                            if let PrettierInstallation::NotInstalled {
+                                installation_task,
+                                attempts,
+                                ..
+                            } = &mut project.default_prettier.prettier
+                            {
+                                *installation_task = None;
+                                *attempts += 1;
+                            }
+                        })?;
+                        anyhow::bail!(
+                            "Cannot start default prettier due to its installation failure: {e:#}"
+                        );
+                    }
+                    let new_default_prettier = project.update(&mut cx, |project, cx| {
+                        let new_default_prettier =
+                            start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
+                        project.default_prettier.prettier =
+                            PrettierInstallation::Installed(PrettierInstance {
+                                attempt: 0,
+                                prettier: Some(new_default_prettier.clone()),
+                            });
+                        new_default_prettier
+                    })?;
+                    return Ok(new_default_prettier);
+                }
+                ControlFlow::Break(instance) => match instance.prettier {
+                    Some(instance) => return Ok(instance),
+                    None => {
+                        let new_default_prettier = project.update(&mut cx, |project, cx| {
+                            let new_default_prettier =
+                                start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
+                            project.default_prettier.prettier =
+                                PrettierInstallation::Installed(PrettierInstance {
+                                    attempt: instance.attempt + 1,
+                                    prettier: Some(new_default_prettier.clone()),
+                                });
+                            new_default_prettier
+                        })?;
+                        return Ok(new_default_prettier);
+                    }
+                },
+            }
+        }
+    })
+}
+
+fn start_prettier(
+    node: Arc<dyn NodeRuntime>,
+    prettier_dir: PathBuf,
+    worktree_id: Option<WorktreeId>,
+    cx: &mut ModelContext<'_, Project>,
+) -> PrettierTask {
+    cx.spawn(|project, mut cx| async move {
+        log::info!("Starting prettier at path {prettier_dir:?}");
+        let new_server_id = project.update(&mut cx, |project, _| {
+            project.languages.next_language_server_id()
+        })?;
+
+        let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
+            .await
+            .context("default prettier spawn")
+            .map(Arc::new)
+            .map_err(Arc::new)?;
+        register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
+        Ok(new_prettier)
+    })
+    .shared()
+}
+
+fn register_new_prettier(
+    project: &WeakModel<Project>,
+    prettier: &Prettier,
+    worktree_id: Option<WorktreeId>,
+    new_server_id: LanguageServerId,
+    cx: &mut AsyncAppContext,
+) {
+    let prettier_dir = prettier.prettier_dir();
+    let is_default = prettier.is_default();
+    if is_default {
+        log::info!("Started default prettier in {prettier_dir:?}");
+    } else {
+        log::info!("Started prettier in {prettier_dir:?}");
+    }
+    if let Some(prettier_server) = prettier.server() {
+        project
+            .update(cx, |project, cx| {
+                let name = if is_default {
+                    LanguageServerName(Arc::from("prettier (default)"))
+                } else {
+                    let worktree_path = worktree_id
+                        .and_then(|id| project.worktree_for_id(id, cx))
+                        .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
+                    let name = match worktree_path {
+                        Some(worktree_path) => {
+                            if prettier_dir == worktree_path.as_ref() {
+                                let name = prettier_dir
+                                    .file_name()
+                                    .and_then(|name| name.to_str())
+                                    .unwrap_or_default();
+                                format!("prettier ({name})")
+                            } else {
+                                let dir_to_display = prettier_dir
+                                    .strip_prefix(worktree_path.as_ref())
+                                    .ok()
+                                    .unwrap_or(prettier_dir);
+                                format!("prettier ({})", dir_to_display.display())
+                            }
+                        }
+                        None => format!("prettier ({})", prettier_dir.display()),
+                    };
+                    LanguageServerName(Arc::from(name))
+                };
+                project
+                    .supplementary_language_servers
+                    .insert(new_server_id, (name, Arc::clone(prettier_server)));
+                cx.emit(Event::LanguageServerAdded(new_server_id));
+            })
+            .ok();
+    }
+}
+
+async fn install_prettier_packages(
+    plugins_to_install: HashSet<&'static str>,
+    node: Arc<dyn NodeRuntime>,
+) -> anyhow::Result<()> {
+    let packages_to_versions =
+        future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
+            |package_name| async {
+                let returned_package_name = package_name.to_string();
+                let latest_version = node
+                    .npm_package_latest_version(package_name)
+                    .await
+                    .with_context(|| {
+                        format!("fetching latest npm version for package {returned_package_name}")
+                    })?;
+                anyhow::Ok((returned_package_name, latest_version))
+            },
+        ))
+        .await
+        .context("fetching latest npm versions")?;
+
+    log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
+    let borrowed_packages = packages_to_versions
+        .iter()
+        .map(|(package, version)| (package.as_str(), version.as_str()))
+        .collect::<Vec<_>>();
+    node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
+        .await
+        .context("fetching formatter packages")?;
+    anyhow::Ok(())
+}
+
+async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
+    let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
+    fs.save(
+        &prettier_wrapper_path,
+        &text::Rope::from(prettier::PRETTIER_SERVER_JS),
+        text::LineEnding::Unix,
+    )
+    .await
+    .with_context(|| {
+        format!(
+            "writing {} file at {prettier_wrapper_path:?}",
+            prettier::PRETTIER_SERVER_FILE
+        )
+    })?;
+    Ok(())
+}
+
+impl Project {
+    pub fn update_prettier_settings(
+        &self,
+        worktree: &Model<Worktree>,
+        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
+        cx: &mut ModelContext<'_, Project>,
+    ) {
+        let prettier_config_files = Prettier::CONFIG_FILE_NAMES
+            .iter()
+            .map(Path::new)
+            .collect::<HashSet<_>>();
+
+        let prettier_config_file_changed = changes
+            .iter()
+            .filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
+            .filter(|(path, _, _)| {
+                !path
+                    .components()
+                    .any(|component| component.as_os_str().to_string_lossy() == "node_modules")
+            })
+            .find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
+        let current_worktree_id = worktree.read(cx).id();
+        if let Some((config_path, _, _)) = prettier_config_file_changed {
+            log::info!(
+                "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
+            );
+            let prettiers_to_reload =
+                self.prettiers_per_worktree
+                    .get(&current_worktree_id)
+                    .iter()
+                    .flat_map(|prettier_paths| prettier_paths.iter())
+                    .flatten()
+                    .filter_map(|prettier_path| {
+                        Some((
+                            current_worktree_id,
+                            Some(prettier_path.clone()),
+                            self.prettier_instances.get(prettier_path)?.clone(),
+                        ))
+                    })
+                    .chain(self.default_prettier.instance().map(|default_prettier| {
+                        (current_worktree_id, None, default_prettier.clone())
+                    }))
+                    .collect::<Vec<_>>();
+
+            cx.background_executor()
+                .spawn(async move {
+                    let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| {
+                        async move {
+                            if let Some(instance) = prettier_instance.prettier {
+                                match instance.await {
+                                    Ok(prettier) => {
+                                        prettier.clear_cache().log_err().await;
+                                    },
+                                    Err(e) => {
+                                        match prettier_path {
+                                            Some(prettier_path) => log::error!(
+                                                "Failed to clear prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
+                                            ),
+                                            None => log::error!(
+                                                "Failed to clear default prettier cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
+                                            ),
+                                        }
+                                    },
+                                }
+                            }
+                        }
+                    }))
+                    .await;
+                })
+                .detach();
+        }
+    }
+
+    fn prettier_instance_for_buffer(
+        &mut self,
+        buffer: &Model<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Option<(Option<PathBuf>, PrettierTask)>> {
+        let buffer = buffer.read(cx);
+        let buffer_file = buffer.file();
+        let Some(buffer_language) = buffer.language() else {
+            return Task::ready(None);
+        };
+        if buffer_language.prettier_parser_name().is_none() {
+            return Task::ready(None);
+        }
+
+        if self.is_local() {
+            let Some(node) = self.node.as_ref().map(Arc::clone) else {
+                return Task::ready(None);
+            };
+            match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
+            {
+                Some((worktree_id, buffer_path)) => {
+                    let fs = Arc::clone(&self.fs);
+                    let installed_prettiers = self.prettier_instances.keys().cloned().collect();
+                    return cx.spawn(|project, mut cx| async move {
+                        match cx
+                            .background_executor()
+                            .spawn(async move {
+                                Prettier::locate_prettier_installation(
+                                    fs.as_ref(),
+                                    &installed_prettiers,
+                                    &buffer_path,
+                                )
+                                .await
+                            })
+                            .await
+                        {
+                            Ok(ControlFlow::Break(())) => {
+                                return None;
+                            }
+                            Ok(ControlFlow::Continue(None)) => {
+                                let default_instance = project
+                                    .update(&mut cx, |project, cx| {
+                                        project
+                                            .prettiers_per_worktree
+                                            .entry(worktree_id)
+                                            .or_default()
+                                            .insert(None);
+                                        project.default_prettier.prettier_task(
+                                            &node,
+                                            Some(worktree_id),
+                                            cx,
+                                        )
+                                    })
+                                    .ok()?;
+                                Some((None, default_instance?.log_err().await?))
+                            }
+                            Ok(ControlFlow::Continue(Some(prettier_dir))) => {
+                                project
+                                    .update(&mut cx, |project, _| {
+                                        project
+                                            .prettiers_per_worktree
+                                            .entry(worktree_id)
+                                            .or_default()
+                                            .insert(Some(prettier_dir.clone()))
+                                    })
+                                    .ok()?;
+                                if let Some(prettier_task) = project
+                                    .update(&mut cx, |project, cx| {
+                                        project.prettier_instances.get_mut(&prettier_dir).map(
+                                            |existing_instance| {
+                                                existing_instance.prettier_task(
+                                                    &node,
+                                                    Some(&prettier_dir),
+                                                    Some(worktree_id),
+                                                    cx,
+                                                )
+                                            },
+                                        )
+                                    })
+                                    .ok()?
+                                {
+                                    log::debug!(
+                                        "Found already started prettier in {prettier_dir:?}"
+                                    );
+                                    return Some((
+                                        Some(prettier_dir),
+                                        prettier_task?.await.log_err()?,
+                                    ));
+                                }
+
+                                log::info!("Found prettier in {prettier_dir:?}, starting.");
+                                let new_prettier_task = project
+                                    .update(&mut cx, |project, cx| {
+                                        let new_prettier_task = start_prettier(
+                                            node,
+                                            prettier_dir.clone(),
+                                            Some(worktree_id),
+                                            cx,
+                                        );
+                                        project.prettier_instances.insert(
+                                            prettier_dir.clone(),
+                                            PrettierInstance {
+                                                attempt: 0,
+                                                prettier: Some(new_prettier_task.clone()),
+                                            },
+                                        );
+                                        new_prettier_task
+                                    })
+                                    .ok()?;
+                                Some((Some(prettier_dir), new_prettier_task))
+                            }
+                            Err(e) => {
+                                log::error!("Failed to determine prettier path for buffer: {e:#}");
+                                return None;
+                            }
+                        }
+                    });
+                }
+                None => {
+                    let new_task = self.default_prettier.prettier_task(&node, None, cx);
+                    return cx
+                        .spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) });
+                }
+            }
+        } else {
+            return Task::ready(None);
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn install_default_prettier(
+        &mut self,
+        _worktree: Option<WorktreeId>,
+        plugins: HashSet<&'static str>,
+        _cx: &mut ModelContext<Self>,
+    ) {
+        // suppress unused code warnings
+        let _ = install_prettier_packages;
+        let _ = save_prettier_server_file;
+
+        self.default_prettier.installed_plugins.extend(plugins);
+        self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance {
+            attempt: 0,
+            prettier: None,
+        });
+    }
+
+    #[cfg(not(any(test, feature = "test-support")))]
+    pub fn install_default_prettier(
+        &mut self,
+        worktree: Option<WorktreeId>,
+        mut new_plugins: HashSet<&'static str>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let Some(node) = self.node.as_ref().cloned() else {
+            return;
+        };
+        log::info!("Initializing default prettier with plugins {new_plugins:?}");
+        let fs = Arc::clone(&self.fs);
+        let locate_prettier_installation = match worktree.and_then(|worktree_id| {
+            self.worktree_for_id(worktree_id, cx)
+                .map(|worktree| worktree.read(cx).abs_path())
+        }) {
+            Some(locate_from) => {
+                let installed_prettiers = self.prettier_instances.keys().cloned().collect();
+                cx.background_executor().spawn(async move {
+                    Prettier::locate_prettier_installation(
+                        fs.as_ref(),
+                        &installed_prettiers,
+                        locate_from.as_ref(),
+                    )
+                    .await
+                })
+            }
+            None => Task::ready(Ok(ControlFlow::Continue(None))),
+        };
+        new_plugins.retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin));
+        let mut installation_attempt = 0;
+        let previous_installation_task = match &mut self.default_prettier.prettier {
+            PrettierInstallation::NotInstalled {
+                installation_task,
+                attempts,
+                not_installed_plugins,
+            } => {
+                installation_attempt = *attempts;
+                if installation_attempt > prettier::FAIL_THRESHOLD {
+                    *installation_task = None;
+                    log::warn!(
+                        "Default prettier installation had failed {installation_attempt} times, not attempting again",
+                    );
+                    return;
+                }
+                new_plugins.extend(not_installed_plugins.iter());
+                installation_task.clone()
+            }
+            PrettierInstallation::Installed { .. } => {
+                if new_plugins.is_empty() {
+                    return;
+                }
+                None
+            }
+        };
+
+        let plugins_to_install = new_plugins.clone();
+        let fs = Arc::clone(&self.fs);
+        let new_installation_task = cx
+            .spawn(|project, mut cx| async move {
+                match locate_prettier_installation
+                    .await
+                    .context("locate prettier installation")
+                    .map_err(Arc::new)?
+                {
+                    ControlFlow::Break(()) => return Ok(()),
+                    ControlFlow::Continue(prettier_path) => {
+                        if prettier_path.is_some() {
+                            new_plugins.clear();
+                        }
+                        let mut needs_install = false;
+                        if let Some(previous_installation_task) = previous_installation_task {
+                            if let Err(e) = previous_installation_task.await {
+                                log::error!("Failed to install default prettier: {e:#}");
+                                project.update(&mut cx, |project, _| {
+                                    if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier {
+                                        *attempts += 1;
+                                        new_plugins.extend(not_installed_plugins.iter());
+                                        installation_attempt = *attempts;
+                                        needs_install = true;
+                                    };
+                                })?;
+                            }
+                        };
+                        if installation_attempt > prettier::FAIL_THRESHOLD {
+                            project.update(&mut cx, |project, _| {
+                                if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut project.default_prettier.prettier {
+                                    *installation_task = None;
+                                };
+                            })?;
+                            log::warn!(
+                                "Default prettier installation had failed {installation_attempt} times, not attempting again",
+                            );
+                            return Ok(());
+                        }
+                        project.update(&mut cx, |project, _| {
+                            new_plugins.retain(|plugin| {
+                                !project.default_prettier.installed_plugins.contains(plugin)
+                            });
+                            if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut project.default_prettier.prettier {
+                                not_installed_plugins.retain(|plugin| {
+                                    !project.default_prettier.installed_plugins.contains(plugin)
+                                });
+                                not_installed_plugins.extend(new_plugins.iter());
+                            }
+                            needs_install |= !new_plugins.is_empty();
+                        })?;
+                        if needs_install {
+                            let installed_plugins = new_plugins.clone();
+                            cx.background_executor()
+                                .spawn(async move {
+                                    save_prettier_server_file(fs.as_ref()).await?;
+                                    install_prettier_packages(new_plugins, node).await
+                                })
+                                .await
+                                .context("prettier & plugins install")
+                                .map_err(Arc::new)?;
+                            log::info!("Initialized prettier with plugins: {installed_plugins:?}");
+                            project.update(&mut cx, |project, _| {
+                                project.default_prettier.prettier =
+                                    PrettierInstallation::Installed(PrettierInstance {
+                                        attempt: 0,
+                                        prettier: None,
+                                    });
+                                project.default_prettier
+                                    .installed_plugins
+                                    .extend(installed_plugins);
+                            })?;
+                        }
+                    }
+                }
+                Ok(())
+            })
+            .shared();
+        self.default_prettier.prettier = PrettierInstallation::NotInstalled {
+            attempts: installation_attempt,
+            installation_task: Some(new_installation_task),
+            not_installed_plugins: plugins_to_install,
+        };
+    }
+}

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

@@ -1,5 +1,6 @@
 mod ignore;
 mod lsp_command;
+mod prettier_support;
 pub mod project_settings;
 pub mod search;
 pub mod terminals;
@@ -20,7 +21,7 @@ use futures::{
         mpsc::{self, UnboundedReceiver},
         oneshot,
     },
-    future::{self, try_join_all, Shared},
+    future::{try_join_all, Shared},
     stream::FuturesUnordered,
     AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
 };
@@ -31,9 +32,7 @@ use gpui::{
 };
 use itertools::Itertools;
 use language::{
-    language_settings::{
-        language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
-    },
+    language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
     point_to_lsp,
     proto::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -54,7 +53,7 @@ use lsp_command::*;
 use node_runtime::NodeRuntime;
 use parking_lot::Mutex;
 use postage::watch;
-use prettier::Prettier;
+use prettier_support::{DefaultPrettier, PrettierInstance};
 use project_settings::{LspSettings, ProjectSettings};
 use rand::prelude::*;
 use search::SearchQuery;
@@ -70,7 +69,7 @@ use std::{
     hash::Hash,
     mem,
     num::NonZeroU32,
-    ops::{ControlFlow, Range},
+    ops::Range,
     path::{self, Component, Path, PathBuf},
     process::Stdio,
     str,
@@ -83,11 +82,8 @@ use std::{
 use terminals::Terminals;
 use text::Anchor;
 use util::{
-    debug_panic, defer,
-    http::HttpClient,
-    merge_json_value_into,
-    paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
-    post_inc, ResultExt, TryFutureExt as _,
+    debug_panic, defer, http::HttpClient, merge_json_value_into,
+    paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
 };
 
 pub use fs::*;
@@ -166,16 +162,9 @@ pub struct Project {
     copilot_log_subscription: Option<lsp::Subscription>,
     current_lsp_settings: HashMap<Arc<str>, LspSettings>,
     node: Option<Arc<dyn NodeRuntime>>,
-    default_prettier: Option<DefaultPrettier>,
+    default_prettier: DefaultPrettier,
     prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
-    prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
-}
-
-struct DefaultPrettier {
-    instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
-    installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
-    #[cfg(not(any(test, feature = "test-support")))]
-    installed_plugins: HashSet<&'static str>,
+    prettier_instances: HashMap<PathBuf, PrettierInstance>,
 }
 
 struct DelayedDebounced {
@@ -540,6 +529,14 @@ struct ProjectLspAdapterDelegate {
     http_client: Arc<dyn HttpClient>,
 }
 
+// Currently, formatting operations are represented differently depending on
+// whether they come from a language server or an external command.
+enum FormatOperation {
+    Lsp(Vec<(Range<Anchor>, String)>),
+    External(Diff),
+    Prettier(Diff),
+}
+
 impl FormatTrigger {
     fn from_proto(value: i32) -> FormatTrigger {
         match value {
@@ -689,7 +686,7 @@ impl Project {
                 copilot_log_subscription: None,
                 current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
                 node: Some(node),
-                default_prettier: None,
+                default_prettier: DefaultPrettier::default(),
                 prettiers_per_worktree: HashMap::default(),
                 prettier_instances: HashMap::default(),
             }
@@ -792,7 +789,7 @@ impl Project {
                 copilot_log_subscription: None,
                 current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
                 node: None,
-                default_prettier: None,
+                default_prettier: DefaultPrettier::default(),
                 prettiers_per_worktree: HashMap::default(),
                 prettier_instances: HashMap::default(),
             };
@@ -965,8 +962,19 @@ impl Project {
                 .detach();
         }
 
+        let mut prettier_plugins_by_worktree = HashMap::default();
         for (worktree, language, settings) in language_formatters_to_check {
-            self.install_default_formatters(worktree, &language, &settings, cx);
+            if let Some(plugins) =
+                prettier_support::prettier_plugins_for_language(&language, &settings)
+            {
+                prettier_plugins_by_worktree
+                    .entry(worktree)
+                    .or_insert_with(|| HashSet::default())
+                    .extend(plugins);
+            }
+        }
+        for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
+            self.install_default_prettier(worktree, prettier_plugins, cx);
         }
 
         // Start all the newly-enabled language servers.
@@ -2669,8 +2677,9 @@ impl Project {
                 })?;
 
                 for (adapter, server) in servers {
-                    let workspace_config =
-                        cx.update(|cx| adapter.workspace_configuration(cx))?.await;
+                    let workspace_config = cx
+                        .update(|cx| adapter.workspace_configuration(server.root_path(), cx))?
+                        .await;
                     server
                         .notify::<lsp::notification::DidChangeConfiguration>(
                             lsp::DidChangeConfigurationParams {
@@ -2722,8 +2731,11 @@ impl Project {
         let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
         let buffer_file = File::from_dyn(buffer_file.as_ref());
         let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
-
-        self.install_default_formatters(worktree, &new_language, &settings, cx);
+        if let Some(prettier_plugins) =
+            prettier_support::prettier_plugins_for_language(&new_language, &settings)
+        {
+            self.install_default_prettier(worktree, prettier_plugins, cx);
+        };
         if let Some(file) = buffer_file {
             let worktree = file.worktree.clone();
             if let Some(tree) = worktree.read(cx).as_local() {
@@ -2779,7 +2791,7 @@ impl Project {
             stderr_capture.clone(),
             language.clone(),
             adapter.clone(),
-            worktree_path,
+            Arc::clone(&worktree_path),
             ProjectLspAdapterDelegate::new(self, cx),
             cx,
         ) {
@@ -2811,6 +2823,7 @@ impl Project {
             cx.spawn(move |this, mut cx| async move {
                 let result = Self::setup_and_insert_language_server(
                     this.clone(),
+                    &worktree_path,
                     initialization_options,
                     pending_server,
                     adapter.clone(),
@@ -2931,6 +2944,7 @@ impl Project {
 
     async fn setup_and_insert_language_server(
         this: WeakModel<Self>,
+        worktree_path: &Path,
         initialization_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
         adapter: Arc<CachedLspAdapter>,
@@ -2943,6 +2957,7 @@ impl Project {
             this.clone(),
             initialization_options,
             pending_server,
+            worktree_path,
             adapter.clone(),
             server_id,
             cx,
@@ -2972,11 +2987,14 @@ impl Project {
         this: WeakModel<Self>,
         initialization_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
+        worktree_path: &Path,
         adapter: Arc<CachedLspAdapter>,
         server_id: LanguageServerId,
         cx: &mut AsyncAppContext,
     ) -> Result<Arc<LanguageServer>> {
-        let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx))?.await;
+        let workspace_config = cx
+            .update(|cx| adapter.workspace_configuration(worktree_path, cx))?
+            .await;
         let language_server = pending_server.task.await?;
 
         language_server
@@ -3005,11 +3023,14 @@ impl Project {
         language_server
             .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
                 let adapter = adapter.clone();
+                let worktree_path = worktree_path.to_path_buf();
                 move |params, cx| {
                     let adapter = adapter.clone();
+                    let worktree_path = worktree_path.clone();
                     async move {
-                        let workspace_config =
-                            cx.update(|cx| adapter.workspace_configuration(cx))?.await;
+                        let workspace_config = cx
+                            .update(|cx| adapter.workspace_configuration(&worktree_path, cx))?
+                            .await;
                         Ok(params
                             .items
                             .into_iter()
@@ -4126,7 +4147,8 @@ impl Project {
                                 this.buffers_being_formatted
                                     .remove(&buffer.read(cx).remote_id());
                             }
-                        }).ok();
+                        })
+                        .ok();
                     }
                 });
 
@@ -4138,8 +4160,6 @@ impl Project {
 
                     let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
                     let ensure_final_newline = settings.ensure_final_newline_on_save;
-                    let format_on_save = settings.format_on_save.clone();
-                    let formatter = settings.formatter.clone();
                     let tab_size = settings.tab_size;
 
                     // First, format buffer's whitespace according to the settings.
@@ -4164,18 +4184,10 @@ impl Project {
                         buffer.end_transaction(cx)
                     })?;
 
-                    // Currently, formatting operations are represented differently depending on
-                    // whether they come from a language server or an external command.
-                    enum FormatOperation {
-                        Lsp(Vec<(Range<Anchor>, String)>),
-                        External(Diff),
-                        Prettier(Diff),
-                    }
-
                     // Apply language-specific formatting using either a language server
                     // or external command.
                     let mut format_operation = None;
-                    match (formatter, format_on_save) {
+                    match (&settings.formatter, &settings.format_on_save) {
                         (_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
 
                         (Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
@@ -4220,46 +4232,11 @@ impl Project {
                             }
                         }
                         (Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
-                            if let Some((prettier_path, prettier_task)) = project
-                                .update(&mut cx, |project, cx| {
-                                    project.prettier_instance_for_buffer(buffer, cx)
-                                })?.await {
-                                    match prettier_task.await
-                                    {
-                                        Ok(prettier) => {
-                                            let buffer_path = buffer.update(&mut cx, |buffer, cx| {
-                                                File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
-                                            })?;
-                                            format_operation = Some(FormatOperation::Prettier(
-                                                prettier
-                                                    .format(buffer, buffer_path, &mut cx)
-                                                    .await
-                                                    .context("formatting via prettier")?,
-                                            ));
-                                        }
-                                        Err(e) => {
-                                            project.update(&mut cx, |project, _| {
-                                                match &prettier_path {
-                                                    Some(prettier_path) => {
-                                                        project.prettier_instances.remove(prettier_path);
-                                                    },
-                                                    None => {
-                                                        if let Some(default_prettier) = project.default_prettier.as_mut() {
-                                                            default_prettier.instance = None;
-                                                        }
-                                                    },
-                                                }
-                                            })?;
-                                            match &prettier_path {
-                                                Some(prettier_path) => {
-                                                    log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
-                                                },
-                                                None => {
-                                                    log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
-                                                },
-                                            }
-                                        }
-                                    }
+                            if let Some(new_operation) =
+                                prettier_support::format_with_prettier(&project, buffer, &mut cx)
+                                    .await
+                            {
+                                format_operation = Some(new_operation);
                             } else if let Some((language_server, buffer_abs_path)) =
                                 language_server.as_ref().zip(buffer_abs_path.as_ref())
                             {
@@ -4277,48 +4254,13 @@ impl Project {
                                 ));
                             }
                         }
-                        (Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
-                            if let Some((prettier_path, prettier_task)) = project
-                                .update(&mut cx, |project, cx| {
-                                    project.prettier_instance_for_buffer(buffer, cx)
-                                })?.await {
-                                    match prettier_task.await
-                                    {
-                                        Ok(prettier) => {
-                                            let buffer_path = buffer.update(&mut cx, |buffer, cx| {
-                                                File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
-                                            })?;
-                                            format_operation = Some(FormatOperation::Prettier(
-                                                prettier
-                                                    .format(buffer, buffer_path, &mut cx)
-                                                    .await
-                                                    .context("formatting via prettier")?,
-                                            ));
-                                        }
-                                        Err(e) => {
-                                            project.update(&mut cx, |project, _| {
-                                                match &prettier_path {
-                                                    Some(prettier_path) => {
-                                                        project.prettier_instances.remove(prettier_path);
-                                                    },
-                                                    None => {
-                                                        if let Some(default_prettier) = project.default_prettier.as_mut() {
-                                                            default_prettier.instance = None;
-                                                        }
-                                                    },
-                                                }
-                                            })?;
-                                            match &prettier_path {
-                                                Some(prettier_path) => {
-                                                    log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
-                                                },
-                                                None => {
-                                                    log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
-                                                },
-                                            }
-                                        }
-                                    }
-                                }
+                        (Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
+                            if let Some(new_operation) =
+                                prettier_support::format_with_prettier(&project, buffer, &mut cx)
+                                    .await
+                            {
+                                format_operation = Some(new_operation);
+                            }
                         }
                     };
 
@@ -6638,84 +6580,6 @@ impl Project {
         .detach();
     }
 
-    fn update_prettier_settings(
-        &self,
-        worktree: &Model<Worktree>,
-        changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
-        cx: &mut ModelContext<'_, Project>,
-    ) {
-        let prettier_config_files = Prettier::CONFIG_FILE_NAMES
-            .iter()
-            .map(Path::new)
-            .collect::<HashSet<_>>();
-
-        let prettier_config_file_changed = changes
-            .iter()
-            .filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
-            .filter(|(path, _, _)| {
-                !path
-                    .components()
-                    .any(|component| component.as_os_str().to_string_lossy() == "node_modules")
-            })
-            .find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
-        let current_worktree_id = worktree.read(cx).id();
-        if let Some((config_path, _, _)) = prettier_config_file_changed {
-            log::info!(
-                "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
-            );
-            let prettiers_to_reload = self
-                .prettiers_per_worktree
-                .get(&current_worktree_id)
-                .iter()
-                .flat_map(|prettier_paths| prettier_paths.iter())
-                .flatten()
-                .filter_map(|prettier_path| {
-                    Some((
-                        current_worktree_id,
-                        Some(prettier_path.clone()),
-                        self.prettier_instances.get(prettier_path)?.clone(),
-                    ))
-                })
-                .chain(self.default_prettier.iter().filter_map(|default_prettier| {
-                    Some((
-                        current_worktree_id,
-                        None,
-                        default_prettier.instance.clone()?,
-                    ))
-                }))
-                .collect::<Vec<_>>();
-
-            cx.background_executor()
-                .spawn(async move {
-                    for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
-                        async move {
-                            prettier_task.await?
-                                .clear_cache()
-                                .await
-                                .with_context(|| {
-                                    match prettier_path {
-                                        Some(prettier_path) => format!(
-                                            "clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
-                                        ),
-                                        None => format!(
-                                            "clearing default prettier cache for worktree {worktree_id:?} on prettier settings update"
-                                        ),
-                                    }
-                                })
-                                .map_err(Arc::new)
-                        }
-                    }))
-                    .await
-                    {
-                        if let Err(e) = task_result {
-                            log::error!("Failed to clear cache for prettier: {e:#}");
-                        }
-                    }
-                })
-                .detach();
-        }
-    }
-
     pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
         let new_active_entry = entry.and_then(|project_path| {
             let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@@ -6742,9 +6606,15 @@ impl Project {
             })
     }
 
-    pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary {
+    pub fn diagnostic_summary(&self, include_ignored: bool, cx: &AppContext) -> DiagnosticSummary {
         let mut summary = DiagnosticSummary::default();
-        for (_, _, path_summary) in self.diagnostic_summaries(cx) {
+        for (_, _, path_summary) in
+            self.diagnostic_summaries(include_ignored, cx)
+                .filter(|(path, _, _)| {
+                    let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored);
+                    include_ignored || worktree == Some(false)
+                })
+        {
             summary.error_count += path_summary.error_count;
             summary.warning_count += path_summary.warning_count;
         }
@@ -6753,17 +6623,23 @@ impl Project {
 
     pub fn diagnostic_summaries<'a>(
         &'a self,
+        include_ignored: bool,
         cx: &'a AppContext,
     ) -> impl Iterator<Item = (ProjectPath, LanguageServerId, DiagnosticSummary)> + 'a {
-        self.visible_worktrees(cx).flat_map(move |worktree| {
-            let worktree = worktree.read(cx);
-            let worktree_id = worktree.id();
-            worktree
-                .diagnostic_summaries()
-                .map(move |(path, server_id, summary)| {
-                    (ProjectPath { worktree_id, path }, server_id, summary)
-                })
-        })
+        self.visible_worktrees(cx)
+            .flat_map(move |worktree| {
+                let worktree = worktree.read(cx);
+                let worktree_id = worktree.id();
+                worktree
+                    .diagnostic_summaries()
+                    .map(move |(path, server_id, summary)| {
+                        (ProjectPath { worktree_id, path }, server_id, summary)
+                    })
+            })
+            .filter(move |(path, _, _)| {
+                let worktree = self.entry_for_path(&path, cx).map(|entry| entry.is_ignored);
+                include_ignored || worktree == Some(false)
+            })
     }
 
     pub fn disk_based_diagnostics_started(
@@ -8579,486 +8455,6 @@ impl Project {
             Vec::new()
         }
     }
-
-    fn prettier_instance_for_buffer(
-        &mut self,
-        buffer: &Model<Buffer>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<
-        Option<(
-            Option<PathBuf>,
-            Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
-        )>,
-    > {
-        let buffer = buffer.read(cx);
-        let buffer_file = buffer.file();
-        let Some(buffer_language) = buffer.language() else {
-            return Task::ready(None);
-        };
-        if buffer_language.prettier_parser_name().is_none() {
-            return Task::ready(None);
-        }
-
-        if self.is_local() {
-            let Some(node) = self.node.as_ref().map(Arc::clone) else {
-                return Task::ready(None);
-            };
-            match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
-            {
-                Some((worktree_id, buffer_path)) => {
-                    let fs = Arc::clone(&self.fs);
-                    let installed_prettiers = self.prettier_instances.keys().cloned().collect();
-                    return cx.spawn(|project, mut cx| async move {
-                        match cx
-                            .background_executor()
-                            .spawn(async move {
-                                Prettier::locate_prettier_installation(
-                                    fs.as_ref(),
-                                    &installed_prettiers,
-                                    &buffer_path,
-                                )
-                                .await
-                            })
-                            .await
-                        {
-                            Ok(ControlFlow::Break(())) => {
-                                return None;
-                            }
-                            Ok(ControlFlow::Continue(None)) => {
-                                match project.update(&mut cx, |project, _| {
-                                    project
-                                        .prettiers_per_worktree
-                                        .entry(worktree_id)
-                                        .or_default()
-                                        .insert(None);
-                                    project.default_prettier.as_ref().and_then(
-                                        |default_prettier| default_prettier.instance.clone(),
-                                    )
-                                }) {
-                                    Ok(Some(old_task)) => Some((None, old_task)),
-                                    Ok(None) => {
-                                        match project.update(&mut cx, |_, cx| {
-                                            start_default_prettier(node, Some(worktree_id), cx)
-                                        }) {
-                                            Ok(new_default_prettier) => {
-                                                return Some((None, new_default_prettier.await))
-                                            }
-                                            Err(e) => {
-                                                Some((
-                                                    None,
-                                                    Task::ready(Err(Arc::new(e.context("project is gone during default prettier startup"))))
-                                                        .shared(),
-                                                ))
-                                            }
-                                        }
-                                    }
-                                    Err(e) => Some((None, Task::ready(Err(Arc::new(e.context("project is gone during default prettier checks"))))
-                                        .shared())),
-                                }
-                            }
-                            Ok(ControlFlow::Continue(Some(prettier_dir))) => {
-                                match project.update(&mut cx, |project, _| {
-                                    project
-                                        .prettiers_per_worktree
-                                        .entry(worktree_id)
-                                        .or_default()
-                                        .insert(Some(prettier_dir.clone()));
-                                    project.prettier_instances.get(&prettier_dir).cloned()
-                                }) {
-                                    Ok(Some(existing_prettier)) => {
-                                        log::debug!(
-                                            "Found already started prettier in {prettier_dir:?}"
-                                        );
-                                        return Some((Some(prettier_dir), existing_prettier));
-                                    }
-                                    Err(e) => {
-                                        return Some((
-                                            Some(prettier_dir),
-                                            Task::ready(Err(Arc::new(e.context("project is gone during custom prettier checks"))))
-                                            .shared(),
-                                        ))
-                                    }
-                                    _ => {},
-                                }
-
-                                log::info!("Found prettier in {prettier_dir:?}, starting.");
-                                let new_prettier_task =
-                                    match project.update(&mut cx, |project, cx| {
-                                        let new_prettier_task = start_prettier(
-                                            node,
-                                            prettier_dir.clone(),
-                                            Some(worktree_id),
-                                            cx,
-                                        );
-                                        project.prettier_instances.insert(
-                                            prettier_dir.clone(),
-                                            new_prettier_task.clone(),
-                                        );
-                                        new_prettier_task
-                                    }) {
-                                        Ok(task) => task,
-                                        Err(e) => return Some((
-                                            Some(prettier_dir),
-                                            Task::ready(Err(Arc::new(e.context("project is gone during custom prettier startup"))))
-                                            .shared()
-                                        )),
-                                    };
-                                Some((Some(prettier_dir), new_prettier_task))
-                            }
-                            Err(e) => {
-                                return Some((
-                                    None,
-                                    Task::ready(Err(Arc::new(
-                                        e.context("determining prettier path"),
-                                    )))
-                                    .shared(),
-                                ));
-                            }
-                        }
-                    });
-                }
-                None => {
-                    let started_default_prettier = self
-                        .default_prettier
-                        .as_ref()
-                        .and_then(|default_prettier| default_prettier.instance.clone());
-                    match started_default_prettier {
-                        Some(old_task) => return Task::ready(Some((None, old_task))),
-                        None => {
-                            let new_task = start_default_prettier(node, None, cx);
-                            return cx.spawn(|_, _| async move { Some((None, new_task.await)) });
-                        }
-                    }
-                }
-            }
-        } else if self.remote_id().is_some() {
-            return Task::ready(None);
-        } else {
-            Task::ready(Some((
-                None,
-                Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
-            )))
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    fn install_default_formatters(
-        &mut self,
-        _: Option<WorktreeId>,
-        _: &Language,
-        _: &LanguageSettings,
-        _: &mut ModelContext<Self>,
-    ) {
-    }
-
-    #[cfg(not(any(test, feature = "test-support")))]
-    fn install_default_formatters(
-        &mut self,
-        worktree: Option<WorktreeId>,
-        new_language: &Language,
-        language_settings: &LanguageSettings,
-        cx: &mut ModelContext<Self>,
-    ) {
-        match &language_settings.formatter {
-            Formatter::Prettier { .. } | Formatter::Auto => {}
-            Formatter::LanguageServer | Formatter::External { .. } => return,
-        };
-        let Some(node) = self.node.as_ref().cloned() else {
-            return;
-        };
-
-        let mut prettier_plugins = None;
-        if new_language.prettier_parser_name().is_some() {
-            prettier_plugins
-                .get_or_insert_with(|| HashSet::<&'static str>::default())
-                .extend(
-                    new_language
-                        .lsp_adapters()
-                        .iter()
-                        .flat_map(|adapter| adapter.prettier_plugins()),
-                )
-        }
-        let Some(prettier_plugins) = prettier_plugins else {
-            return;
-        };
-
-        let fs = Arc::clone(&self.fs);
-        let locate_prettier_installation = match worktree.and_then(|worktree_id| {
-            self.worktree_for_id(worktree_id, cx)
-                .map(|worktree| worktree.read(cx).abs_path())
-        }) {
-            Some(locate_from) => {
-                let installed_prettiers = self.prettier_instances.keys().cloned().collect();
-                cx.background_executor().spawn(async move {
-                    Prettier::locate_prettier_installation(
-                        fs.as_ref(),
-                        &installed_prettiers,
-                        locate_from.as_ref(),
-                    )
-                    .await
-                })
-            }
-            None => Task::ready(Ok(ControlFlow::Break(()))),
-        };
-        let mut plugins_to_install = prettier_plugins;
-        let previous_installation_process =
-            if let Some(default_prettier) = &mut self.default_prettier {
-                plugins_to_install
-                    .retain(|plugin| !default_prettier.installed_plugins.contains(plugin));
-                if plugins_to_install.is_empty() {
-                    return;
-                }
-                default_prettier.installation_process.clone()
-            } else {
-                None
-            };
-
-        let fs = Arc::clone(&self.fs);
-        let default_prettier = self
-            .default_prettier
-            .get_or_insert_with(|| DefaultPrettier {
-                instance: None,
-                installation_process: None,
-                installed_plugins: HashSet::default(),
-            });
-        default_prettier.installation_process = Some(
-            cx.spawn(|this, mut cx| async move {
-                match locate_prettier_installation
-                    .await
-                    .context("locate prettier installation")
-                    .map_err(Arc::new)?
-                {
-                    ControlFlow::Break(()) => return Ok(()),
-                    ControlFlow::Continue(Some(_non_default_prettier)) => return Ok(()),
-                    ControlFlow::Continue(None) => {
-                        let mut needs_install = match previous_installation_process {
-                            Some(previous_installation_process) => {
-                                previous_installation_process.await.is_err()
-                            }
-                            None => true,
-                        };
-                        this.update(&mut cx, |this, _| {
-                            if let Some(default_prettier) = &mut this.default_prettier {
-                                plugins_to_install.retain(|plugin| {
-                                    !default_prettier.installed_plugins.contains(plugin)
-                                });
-                                needs_install |= !plugins_to_install.is_empty();
-                            }
-                        })?;
-                        if needs_install {
-                            let installed_plugins = plugins_to_install.clone();
-                            cx.background_executor()
-                                .spawn(async move {
-                                    install_default_prettier(plugins_to_install, node, fs).await
-                                })
-                                .await
-                                .context("prettier & plugins install")
-                                .map_err(Arc::new)?;
-                            this.update(&mut cx, |this, _| {
-                                let default_prettier =
-                                    this.default_prettier
-                                        .get_or_insert_with(|| DefaultPrettier {
-                                            instance: None,
-                                            installation_process: Some(
-                                                Task::ready(Ok(())).shared(),
-                                            ),
-                                            installed_plugins: HashSet::default(),
-                                        });
-                                default_prettier.instance = None;
-                                default_prettier.installed_plugins.extend(installed_plugins);
-                            })?;
-                        }
-                    }
-                }
-                Ok(())
-            })
-            .shared(),
-        );
-    }
-}
-
-fn start_default_prettier(
-    node: Arc<dyn NodeRuntime>,
-    worktree_id: Option<WorktreeId>,
-    cx: &mut ModelContext<'_, Project>,
-) -> Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>> {
-    cx.spawn(|project, mut cx| async move {
-        loop {
-            let default_prettier_installing = match project.update(&mut cx, |project, _| {
-                project
-                    .default_prettier
-                    .as_ref()
-                    .and_then(|default_prettier| default_prettier.installation_process.clone())
-            }) {
-                Ok(installation) => installation,
-                Err(e) => {
-                    return Task::ready(Err(Arc::new(
-                        e.context("project is gone during default prettier installation"),
-                    )))
-                    .shared()
-                }
-            };
-            match default_prettier_installing {
-                Some(installation_task) => {
-                    if installation_task.await.is_ok() {
-                        break;
-                    }
-                }
-                None => break,
-            }
-        }
-
-        match project.update(&mut cx, |project, cx| {
-            match project
-                .default_prettier
-                .as_mut()
-                .and_then(|default_prettier| default_prettier.instance.as_mut())
-            {
-                Some(default_prettier) => default_prettier.clone(),
-                None => {
-                    let new_default_prettier =
-                        start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
-                    project
-                        .default_prettier
-                        .get_or_insert_with(|| DefaultPrettier {
-                            instance: None,
-                            installation_process: None,
-                            #[cfg(not(any(test, feature = "test-support")))]
-                            installed_plugins: HashSet::default(),
-                        })
-                        .instance = Some(new_default_prettier.clone());
-                    new_default_prettier
-                }
-            }
-        }) {
-            Ok(task) => task,
-            Err(e) => Task::ready(Err(Arc::new(
-                e.context("project is gone during default prettier startup"),
-            )))
-            .shared(),
-        }
-    })
-}
-
-fn start_prettier(
-    node: Arc<dyn NodeRuntime>,
-    prettier_dir: PathBuf,
-    worktree_id: Option<WorktreeId>,
-    cx: &mut ModelContext<'_, Project>,
-) -> Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>> {
-    cx.spawn(|project, mut cx| async move {
-        let new_server_id = project.update(&mut cx, |project, _| {
-            project.languages.next_language_server_id()
-        })?;
-        let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
-            .await
-            .context("default prettier spawn")
-            .map(Arc::new)
-            .map_err(Arc::new)?;
-        register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
-        Ok(new_prettier)
-    })
-    .shared()
-}
-
-fn register_new_prettier(
-    project: &WeakModel<Project>,
-    prettier: &Prettier,
-    worktree_id: Option<WorktreeId>,
-    new_server_id: LanguageServerId,
-    cx: &mut AsyncAppContext,
-) {
-    let prettier_dir = prettier.prettier_dir();
-    let is_default = prettier.is_default();
-    if is_default {
-        log::info!("Started default prettier in {prettier_dir:?}");
-    } else {
-        log::info!("Started prettier in {prettier_dir:?}");
-    }
-    if let Some(prettier_server) = prettier.server() {
-        project
-            .update(cx, |project, cx| {
-                let name = if is_default {
-                    LanguageServerName(Arc::from("prettier (default)"))
-                } else {
-                    let worktree_path = worktree_id
-                        .and_then(|id| project.worktree_for_id(id, cx))
-                        .map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
-                    let name = match worktree_path {
-                        Some(worktree_path) => {
-                            if prettier_dir == worktree_path.as_ref() {
-                                let name = prettier_dir
-                                    .file_name()
-                                    .and_then(|name| name.to_str())
-                                    .unwrap_or_default();
-                                format!("prettier ({name})")
-                            } else {
-                                let dir_to_display = prettier_dir
-                                    .strip_prefix(worktree_path.as_ref())
-                                    .ok()
-                                    .unwrap_or(prettier_dir);
-                                format!("prettier ({})", dir_to_display.display())
-                            }
-                        }
-                        None => format!("prettier ({})", prettier_dir.display()),
-                    };
-                    LanguageServerName(Arc::from(name))
-                };
-                project
-                    .supplementary_language_servers
-                    .insert(new_server_id, (name, Arc::clone(prettier_server)));
-                cx.emit(Event::LanguageServerAdded(new_server_id));
-            })
-            .ok();
-    }
-}
-
-#[cfg(not(any(test, feature = "test-support")))]
-async fn install_default_prettier(
-    plugins_to_install: HashSet<&'static str>,
-    node: Arc<dyn NodeRuntime>,
-    fs: Arc<dyn Fs>,
-) -> anyhow::Result<()> {
-    let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
-    // method creates parent directory if it doesn't exist
-    fs.save(
-        &prettier_wrapper_path,
-        &text::Rope::from(prettier::PRETTIER_SERVER_JS),
-        text::LineEnding::Unix,
-    )
-    .await
-    .with_context(|| {
-        format!(
-            "writing {} file at {prettier_wrapper_path:?}",
-            prettier::PRETTIER_SERVER_FILE
-        )
-    })?;
-
-    let packages_to_versions =
-        future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
-            |package_name| async {
-                let returned_package_name = package_name.to_string();
-                let latest_version = node
-                    .npm_package_latest_version(package_name)
-                    .await
-                    .with_context(|| {
-                        format!("fetching latest npm version for package {returned_package_name}")
-                    })?;
-                anyhow::Ok((returned_package_name, latest_version))
-            },
-        ))
-        .await
-        .context("fetching latest npm versions")?;
-
-    log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
-    let borrowed_packages = packages_to_versions
-        .iter()
-        .map(|(package, version)| (package.as_str(), version.as_str()))
-        .collect::<Vec<_>>();
-    node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
-        .await
-        .context("fetching formatter packages")?;
-    anyhow::Ok(())
 }
 
 fn subscribe_for_copilot_events(

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

@@ -823,7 +823,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
 }
 
 #[gpui::test]
-async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
+async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
     init_test(cx);
 
     let fs = FakeFs::new(cx.executor());
@@ -831,7 +831,12 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
         "/root",
         json!({
             "dir": {
+                ".git": {
+                    "HEAD": "ref: refs/heads/main",
+                },
+                ".gitignore": "b.rs",
                 "a.rs": "let a = 1;",
+                "b.rs": "let b = 2;",
             },
             "other.rs": "let b = c;"
         }),
@@ -839,6 +844,13 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
     .await;
 
     let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
+    let (worktree, _) = project
+        .update(cx, |project, cx| {
+            project.find_or_create_local_worktree("/root/dir", true, cx)
+        })
+        .await
+        .unwrap();
+    let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 
     let (worktree, _) = project
         .update(cx, |project, cx| {
@@ -846,12 +858,30 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
         })
         .await
         .unwrap();
-    let worktree_id = worktree.update(cx, |tree, _| tree.id());
+    let other_worktree_id = worktree.update(cx, |tree, _| tree.id());
 
+    let server_id = LanguageServerId(0);
     project.update(cx, |project, cx| {
         project
             .update_diagnostics(
-                LanguageServerId(0),
+                server_id,
+                lsp::PublishDiagnosticsParams {
+                    uri: Url::from_file_path("/root/dir/b.rs").unwrap(),
+                    version: None,
+                    diagnostics: vec![lsp::Diagnostic {
+                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
+                        severity: Some(lsp::DiagnosticSeverity::ERROR),
+                        message: "unused variable 'b'".to_string(),
+                        ..Default::default()
+                    }],
+                },
+                &[],
+                cx,
+            )
+            .unwrap();
+        project
+            .update_diagnostics(
+                server_id,
                 lsp::PublishDiagnosticsParams {
                     uri: Url::from_file_path("/root/other.rs").unwrap(),
                     version: None,
@@ -868,11 +898,34 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
             .unwrap();
     });
 
-    let buffer = project
-        .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
+    let main_ignored_buffer = project
+        .update(cx, |project, cx| {
+            project.open_buffer((main_worktree_id, "b.rs"), cx)
+        })
         .await
         .unwrap();
-    buffer.update(cx, |buffer, _| {
+    main_ignored_buffer.update(cx, |buffer, _| {
+        let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
+        assert_eq!(
+            chunks
+                .iter()
+                .map(|(s, d)| (s.as_str(), *d))
+                .collect::<Vec<_>>(),
+            &[
+                ("let ", None),
+                ("b", Some(DiagnosticSeverity::ERROR)),
+                (" = 2;", None),
+            ],
+            "Gigitnored buffers should still get in-buffer diagnostics",
+        );
+    });
+    let other_buffer = project
+        .update(cx, |project, cx| {
+            project.open_buffer((other_worktree_id, ""), cx)
+        })
+        .await
+        .unwrap();
+    other_buffer.update(cx, |buffer, _| {
         let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
         assert_eq!(
             chunks
@@ -883,13 +936,29 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
                 ("let b = ", None),
                 ("c", Some(DiagnosticSeverity::ERROR)),
                 (";", None),
-            ]
+            ],
+            "Buffers from hidden projects should still get in-buffer diagnostics"
         );
     });
 
     project.update(cx, |project, cx| {
-        assert_eq!(project.diagnostic_summaries(cx).next(), None);
-        assert_eq!(project.diagnostic_summary(cx).error_count, 0);
+        assert_eq!(project.diagnostic_summaries(false, cx).next(), None);
+        assert_eq!(
+            project.diagnostic_summaries(true, cx).collect::<Vec<_>>(),
+            vec![(
+                ProjectPath {
+                    worktree_id: main_worktree_id,
+                    path: Arc::from(Path::new("b.rs")),
+                },
+                server_id,
+                DiagnosticSummary {
+                    error_count: 1,
+                    warning_count: 0,
+                }
+            )]
+        );
+        assert_eq!(project.diagnostic_summary(false, cx).error_count, 0);
+        assert_eq!(project.diagnostic_summary(true, cx).error_count, 1);
     });
 }
 
@@ -1162,7 +1231,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
     });
     project.update(cx, |project, cx| {
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 1,
                 warning_count: 0,
@@ -1188,7 +1257,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
     });
     project.update(cx, |project, cx| {
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 0,
                 warning_count: 0,
@@ -1777,7 +1846,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
             .unwrap();
 
         assert_eq!(
-            project.diagnostic_summary(cx),
+            project.diagnostic_summary(false, cx),
             DiagnosticSummary {
                 error_count: 2,
                 warning_count: 0,

crates/project_panel2/src/file_associations.rs πŸ”—

@@ -41,56 +41,47 @@ impl FileAssociations {
             })
     }
 
-    pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
-        maybe!({
-            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+    pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> {
+        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
-            // FIXME: Associate a type with the languages and have the file's langauge
-            //        override these associations
-            maybe!({
-                let suffix = path.icon_suffix()?;
+        // FIXME: Associate a type with the languages and have the file's langauge
+        //        override these associations
+        maybe!({
+            let suffix = path.icon_suffix()?;
 
-                this.suffixes
-                    .get(suffix)
-                    .and_then(|type_str| this.types.get(type_str))
-                    .map(|type_config| type_config.icon.clone())
-            })
-            .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
+            this.suffixes
+                .get(suffix)
+                .and_then(|type_str| this.types.get(type_str))
+                .map(|type_config| type_config.icon.clone())
         })
-        .unwrap_or_else(|| Arc::from("".to_string()))
+        .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
     }
 
-    pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
-        maybe!({
-            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+    pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
+        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
-            let key = if expanded {
-                EXPANDED_DIRECTORY_TYPE
-            } else {
-                COLLAPSED_DIRECTORY_TYPE
-            };
+        let key = if expanded {
+            EXPANDED_DIRECTORY_TYPE
+        } else {
+            COLLAPSED_DIRECTORY_TYPE
+        };
 
-            this.types
-                .get(key)
-                .map(|type_config| type_config.icon.clone())
-        })
-        .unwrap_or_else(|| Arc::from("".to_string()))
+        this.types
+            .get(key)
+            .map(|type_config| type_config.icon.clone())
     }
 
-    pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
-        maybe!({
-            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+    pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
+        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
-            let key = if expanded {
-                EXPANDED_CHEVRON_TYPE
-            } else {
-                COLLAPSED_CHEVRON_TYPE
-            };
+        let key = if expanded {
+            EXPANDED_CHEVRON_TYPE
+        } else {
+            COLLAPSED_CHEVRON_TYPE
+        };
 
-            this.types
-                .get(key)
-                .map(|type_config| type_config.icon.clone())
-        })
-        .unwrap_or_else(|| Arc::from("".to_string()))
+        this.types
+            .get(key)
+            .map(|type_config| type_config.icon.clone())
     }
 }

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

@@ -8,10 +8,10 @@ use file_associations::FileAssociations;
 
 use anyhow::{anyhow, Result};
 use gpui::{
-    actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
-    ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
-    IntoElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
-    Render, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View,
+    actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
+    ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
+    InteractiveElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
+    PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View,
     ViewContext, VisualContext as _, WeakView, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
@@ -30,7 +30,7 @@ use std::{
     sync::Arc,
 };
 use theme::ActiveTheme as _;
-use ui::{h_stack, v_stack, IconElement, Label};
+use ui::{v_stack, ContextMenu, IconElement, Label, ListItem};
 use unicase::UniCase;
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
@@ -50,6 +50,7 @@ pub struct ProjectPanel {
     last_worktree_root_id: Option<ProjectEntryId>,
     expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
     selection: Option<Selection>,
+    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
     edit_state: Option<EditState>,
     filename_editor: View<Editor>,
     clipboard_entry: Option<ClipboardEntry>,
@@ -232,6 +233,7 @@ impl ProjectPanel {
                 expanded_dir_ids: Default::default(),
                 selection: None,
                 edit_state: None,
+                context_menu: None,
                 filename_editor,
                 clipboard_entry: None,
                 // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
@@ -367,80 +369,93 @@ impl ProjectPanel {
 
     fn deploy_context_menu(
         &mut self,
-        _position: Point<Pixels>,
-        _entry_id: ProjectEntryId,
-        _cx: &mut ViewContext<Self>,
+        position: Point<Pixels>,
+        entry_id: ProjectEntryId,
+        cx: &mut ViewContext<Self>,
     ) {
-        // todo!()
-        //     let project = self.project.read(cx);
+        let this = cx.view().clone();
+        let project = self.project.read(cx);
 
-        //     let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
-        //         id
-        //     } else {
-        //         return;
-        //     };
+        let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
+            id
+        } else {
+            return;
+        };
 
-        //     self.selection = Some(Selection {
-        //         worktree_id,
-        //         entry_id,
-        //     });
-
-        //     let mut menu_entries = Vec::new();
-        //     if let Some((worktree, entry)) = self.selected_entry(cx) {
-        //         let is_root = Some(entry) == worktree.root_entry();
-        //         if !project.is_remote() {
-        //             menu_entries.push(ContextMenuItem::action(
-        //                 "Add Folder to Project",
-        //                 workspace::AddFolderToProject,
-        //             ));
-        //             if is_root {
-        //                 let project = self.project.clone();
-        //                 menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| {
-        //                     project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
-        //                 }));
-        //             }
-        //         }
-        //         menu_entries.push(ContextMenuItem::action("New File", NewFile));
-        //         menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory));
-        //         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,
-        //             ));
-        //         }
-
-        //         menu_entries.push(ContextMenuItem::Separator);
-        //         menu_entries.push(ContextMenuItem::action("Rename", Rename));
-        //         if !is_root {
-        //             menu_entries.push(ContextMenuItem::action("Delete", Delete));
-        //         }
-        //     }
-
-        //     // self.context_menu.update(cx, |menu, cx| {
-        //     //     menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
-        //     // });
-
-        //     cx.notify();
+        self.selection = Some(Selection {
+            worktree_id,
+            entry_id,
+        });
+
+        if let Some((worktree, entry)) = self.selected_entry(cx) {
+            let is_root = Some(entry) == worktree.root_entry();
+            let is_dir = entry.is_dir();
+            let worktree_id = worktree.id();
+            let is_local = project.is_local();
+
+            let context_menu = ContextMenu::build(cx, |mut menu, cx| {
+                if is_local {
+                    menu = menu.action(
+                        "Add Folder to Project",
+                        Box::new(workspace::AddFolderToProject),
+                        cx,
+                    );
+                    if is_root {
+                        menu = menu.entry(
+                            "Remove from Project",
+                            cx.handler_for(&this, move |this, cx| {
+                                this.project.update(cx, |project, cx| {
+                                    project.remove_worktree(worktree_id, cx)
+                                });
+                            }),
+                        );
+                    }
+                }
+
+                menu = menu
+                    .action("New File", Box::new(NewFile), cx)
+                    .action("New Folder", Box::new(NewDirectory), cx)
+                    .separator()
+                    .action("Cut", Box::new(Cut), cx)
+                    .action("Copy", Box::new(Copy), cx);
+
+                if let Some(clipboard_entry) = self.clipboard_entry {
+                    if clipboard_entry.worktree_id() == worktree_id {
+                        menu = menu.action("Paste", Box::new(Paste), cx);
+                    }
+                }
+
+                menu = menu
+                    .separator()
+                    .action("Copy Path", Box::new(CopyPath), cx)
+                    .action("Copy Relative Path", Box::new(CopyRelativePath), cx)
+                    .separator()
+                    .action("Reveal in Finder", Box::new(RevealInFinder), cx);
+
+                if is_dir {
+                    menu = menu
+                        .action("Open in Terminal", Box::new(OpenInTerminal), cx)
+                        .action("Search Inside", Box::new(NewSearchInDirectory), cx)
+                }
+
+                menu = menu.separator().action("Rename", Box::new(Rename), cx);
+
+                if !is_root {
+                    menu = menu.action("Delete", Box::new(Delete), cx);
+                }
+
+                menu
+            });
+
+            cx.focus_view(&context_menu);
+            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
+                this.context_menu.take();
+                cx.notify();
+            });
+            self.context_menu = Some((context_menu, position, subscription));
+        }
+
+        cx.notify();
     }
 
     fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
@@ -1268,16 +1283,16 @@ impl ProjectPanel {
                     let icon = match entry.kind {
                         EntryKind::File(_) => {
                             if show_file_icons {
-                                Some(FileAssociations::get_icon(&entry.path, cx))
+                                FileAssociations::get_icon(&entry.path, cx)
                             } else {
                                 None
                             }
                         }
                         _ => {
                             if show_folder_icons {
-                                Some(FileAssociations::get_folder_icon(is_expanded, cx))
+                                FileAssociations::get_folder_icon(is_expanded, cx)
                             } else {
-                                Some(FileAssociations::get_chevron_icon(is_expanded, cx))
+                                FileAssociations::get_chevron_icon(is_expanded, cx)
                             }
                         }
                     };
@@ -1334,13 +1349,19 @@ impl ProjectPanel {
         }
     }
 
-    fn render_entry_visual_element(
-        details: &EntryDetails,
-        editor: Option<&View<Editor>>,
-        padding: Pixels,
+    fn render_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        details: EntryDetails,
+        // dragged_entry_destination: &mut Option<Arc<Path>>,
         cx: &mut ViewContext<Self>,
-    ) -> Div {
+    ) -> ListItem {
+        let kind = details.kind;
+        let settings = ProjectPanelSettings::get_global(cx);
         let show_editor = details.is_editing && !details.is_processing;
+        let is_selected = self
+            .selection
+            .map_or(false, |selection| selection.entry_id == entry_id);
 
         let theme = cx.theme();
         let filename_text_color = details
@@ -1353,15 +1374,18 @@ impl ProjectPanel {
             })
             .unwrap_or(theme.status().info);
 
-        h_stack()
+        ListItem::new(entry_id.to_proto() as usize)
+            .indent_level(details.depth)
+            .indent_step_size(px(settings.indent_size))
+            .selected(is_selected)
             .child(if let Some(icon) = &details.icon {
                 div().child(IconElement::from_path(icon.to_string()))
             } else {
                 div()
             })
             .child(
-                if let (Some(editor), true) = (editor, show_editor) {
-                    div().w_full().child(editor.clone())
+                if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
+                    div().h_full().w_full().child(editor.clone())
                 } else {
                     div()
                         .text_color(filename_text_color)
@@ -1369,34 +1393,10 @@ impl ProjectPanel {
                 }
                 .ml_1(),
             )
-            .pl(padding)
-    }
-
-    fn render_entry(
-        &self,
-        entry_id: ProjectEntryId,
-        details: EntryDetails,
-        // dragged_entry_destination: &mut Option<Arc<Path>>,
-        cx: &mut ViewContext<Self>,
-    ) -> Stateful<Div> {
-        let kind = details.kind;
-        let settings = ProjectPanelSettings::get_global(cx);
-        const INDENT_SIZE: Pixels = px(16.0);
-        let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size);
-        let show_editor = details.is_editing && !details.is_processing;
-        let is_selected = self
-            .selection
-            .map_or(false, |selection| selection.entry_id == entry_id);
-
-        Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx)
-            .id(entry_id.to_proto() as usize)
-            .w_full()
-            .cursor_pointer()
-            .when(is_selected, |this| {
-                this.bg(cx.theme().colors().element_selected)
-            })
-            .hover(|style| style.bg(cx.theme().colors().element_hover))
             .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
+                if event.down.button == MouseButton::Right {
+                    return;
+                }
                 if !show_editor {
                     if kind.is_dir() {
                         this.toggle_expanded(entry_id, cx);
@@ -1409,12 +1409,9 @@ impl ProjectPanel {
                     }
                 }
             }))
-            .on_mouse_down(
-                MouseButton::Right,
-                cx.listener(move |this, event: &MouseDownEvent, cx| {
-                    this.deploy_context_menu(event.position, entry_id, cx);
-                }),
-            )
+            .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
+                this.deploy_context_menu(event.position, entry_id, cx);
+            }))
         // .on_drop::<ProjectEntryId>(|this, event, cx| {
         //     this.move_entry(
         //         *dragged_entry,
@@ -1436,6 +1433,7 @@ impl Render for ProjectPanel {
             div()
                 .id("project-panel")
                 .size_full()
+                .relative()
                 .key_context("ProjectPanel")
                 .on_action(cx.listener(Self::select_next))
                 .on_action(cx.listener(Self::select_prev))
@@ -1479,6 +1477,12 @@ impl Render for ProjectPanel {
                     .size_full()
                     .track_scroll(self.list.clone()),
                 )
+                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
+                    overlay()
+                        .position(*position)
+                        .anchor(gpui::AnchorCorner::TopLeft)
+                        .child(menu.clone())
+                }))
         } else {
             v_stack()
                 .id("empty-project_panel")

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

@@ -7,23 +7,23 @@ use crate::{
     ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
 };
 use collections::HashMap;
-use editor::Editor;
+use editor::{Editor, EditorMode};
 use futures::channel::oneshot;
 use gpui::{
     actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement,
     ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _,
-    WindowContext,
+    WeakView, WindowContext,
 };
 use project::search::SearchQuery;
 use serde::Deserialize;
 use std::{any::Any, sync::Arc};
 
-use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement};
+use ui::{h_stack, Icon, IconButton, IconElement};
 use util::ResultExt;
 use workspace::{
     item::ItemHandle,
     searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
-    ToolbarItemLocation, ToolbarItemView, Workspace,
+    ToolbarItemLocation, ToolbarItemView,
 };
 
 #[derive(PartialEq, Clone, Deserialize, Default, Action)]
@@ -38,7 +38,7 @@ pub enum Event {
 }
 
 pub fn init(cx: &mut AppContext) {
-    cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace))
+    cx.observe_new_views(|editor: &mut Editor, cx| BufferSearchBar::register(editor, cx))
         .detach();
 }
 
@@ -187,6 +187,7 @@ impl Render for BufferSearchBar {
             })
             .on_action(cx.listener(Self::previous_history_query))
             .on_action(cx.listener(Self::next_history_query))
+            .on_action(cx.listener(Self::dismiss))
             .w_full()
             .p_1()
             .child(
@@ -213,10 +214,11 @@ impl Render for BufferSearchBar {
             .child(
                 h_stack()
                     .flex_none()
-                    .child(ButtonGroup::new(vec![
-                        search_button_for_mode(SearchMode::Text),
-                        search_button_for_mode(SearchMode::Regex),
-                    ]))
+                    .child(
+                        h_stack()
+                            .child(search_button_for_mode(SearchMode::Text))
+                            .child(search_button_for_mode(SearchMode::Regex)),
+                    )
                     .when(supported_options.replacement, |this| {
                         this.child(super::toggle_replace_button(self.replace_enabled))
                     }),
@@ -294,9 +296,19 @@ impl ToolbarItemView for BufferSearchBar {
 }
 
 impl BufferSearchBar {
-    pub fn register(workspace: &mut Workspace) {
-        workspace.register_action(|workspace, a: &Deploy, cx| {
-            workspace.active_pane().update(cx, |this, cx| {
+    pub fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
+        if editor.mode() != EditorMode::Full {
+            return;
+        };
+
+        let handle = cx.view().downgrade();
+
+        editor.register_action(move |a: &Deploy, cx| {
+            let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) else {
+                return;
+            };
+
+            pane.update(cx, |this, cx| {
                 this.toolbar().update(cx, |this, cx| {
                     if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
                         search_bar.update(cx, |this, cx| {
@@ -316,11 +328,16 @@ impl BufferSearchBar {
             });
         });
         fn register_action<A: Action>(
-            workspace: &mut Workspace,
+            editor: &mut Editor,
+            handle: WeakView<Editor>,
             update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
         ) {
-            workspace.register_action(move |workspace, action: &A, cx| {
-                workspace.active_pane().update(cx, move |this, cx| {
+            editor.register_action(move |action: &A, cx| {
+                let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx))
+                else {
+                    return;
+                };
+                pane.update(cx, move |this, cx| {
                     this.toolbar().update(cx, move |this, cx| {
                         if let Some(search_bar) = this.item_of_type::<BufferSearchBar>() {
                             search_bar.update(cx, move |this, cx| update(this, action, cx));
@@ -331,49 +348,76 @@ impl BufferSearchBar {
             });
         }
 
-        register_action(workspace, |this, action: &ToggleCaseSensitive, cx| {
-            if this.supported_options().case {
-                this.toggle_case_sensitive(action, cx);
-            }
-        });
-        register_action(workspace, |this, action: &ToggleWholeWord, cx| {
-            if this.supported_options().word {
-                this.toggle_whole_word(action, cx);
-            }
-        });
-        register_action(workspace, |this, action: &ToggleReplace, cx| {
-            if this.supported_options().replacement {
-                this.toggle_replace(action, cx);
-            }
-        });
-        register_action(workspace, |this, _: &ActivateRegexMode, cx| {
+        let handle = cx.view().downgrade();
+        register_action(
+            editor,
+            handle.clone(),
+            |this, action: &ToggleCaseSensitive, cx| {
+                if this.supported_options().case {
+                    this.toggle_case_sensitive(action, cx);
+                }
+            },
+        );
+        register_action(
+            editor,
+            handle.clone(),
+            |this, action: &ToggleWholeWord, cx| {
+                if this.supported_options().word {
+                    this.toggle_whole_word(action, cx);
+                }
+            },
+        );
+        register_action(
+            editor,
+            handle.clone(),
+            |this, action: &ToggleReplace, cx| {
+                if this.supported_options().replacement {
+                    this.toggle_replace(action, cx);
+                }
+            },
+        );
+        register_action(editor, handle.clone(), |this, _: &ActivateRegexMode, cx| {
             if this.supported_options().regex {
                 this.activate_search_mode(SearchMode::Regex, cx);
             }
         });
-        register_action(workspace, |this, _: &ActivateTextMode, cx| {
+        register_action(editor, handle.clone(), |this, _: &ActivateTextMode, cx| {
             this.activate_search_mode(SearchMode::Text, cx);
         });
-        register_action(workspace, |this, action: &CycleMode, cx| {
+        register_action(editor, handle.clone(), |this, action: &CycleMode, cx| {
             if this.supported_options().regex {
                 // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
                 // cycling.
                 this.cycle_mode(action, cx)
             }
         });
-        register_action(workspace, |this, action: &SelectNextMatch, cx| {
-            this.select_next_match(action, cx);
-        });
-        register_action(workspace, |this, action: &SelectPrevMatch, cx| {
-            this.select_prev_match(action, cx);
-        });
-        register_action(workspace, |this, action: &SelectAllMatches, cx| {
-            this.select_all_matches(action, cx);
-        });
-        register_action(workspace, |this, _: &editor::Cancel, cx| {
+        register_action(
+            editor,
+            handle.clone(),
+            |this, action: &SelectNextMatch, cx| {
+                this.select_next_match(action, cx);
+            },
+        );
+        register_action(
+            editor,
+            handle.clone(),
+            |this, action: &SelectPrevMatch, cx| {
+                this.select_prev_match(action, cx);
+            },
+        );
+        register_action(
+            editor,
+            handle.clone(),
+            |this, action: &SelectAllMatches, cx| {
+                this.select_all_matches(action, cx);
+            },
+        );
+        register_action(editor, handle.clone(), |this, _: &editor::Cancel, cx| {
             if !this.dismissed {
                 this.dismiss(&Dismiss, cx);
+                return;
             }
+            cx.propagate();
         });
     }
     pub fn new(cx: &mut ViewContext<Self>) -> Self {
@@ -543,8 +587,7 @@ impl BufferSearchBar {
 
         // let style = theme.search.action_button.clone();
 
-        IconButton::new(0, ui::Icon::SelectAll)
-            .on_click(|_, cx| cx.dispatch_action(Box::new(SelectAllMatches)))
+        IconButton::new(0, ui::Icon::SelectAll).action(Box::new(SelectAllMatches))
     }
 
     pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {

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

@@ -3,7 +3,8 @@ pub use buffer_search::BufferSearchBar;
 use gpui::{actions, Action, AppContext, IntoElement};
 pub use mode::SearchMode;
 use project::search::SearchQuery;
-use ui::ButtonVariant;
+use ui::prelude::*;
+use ui::{ButtonStyle2, Icon, IconButton};
 //pub use project_search::{ProjectSearchBar, ProjectSearchView};
 // use theme::components::{
 //     action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle,
@@ -83,35 +84,35 @@ impl SearchOptions {
     }
 
     pub fn as_button(&self, active: bool) -> impl IntoElement {
-        ui::IconButton::new(0, self.icon())
+        IconButton::new(0, self.icon())
             .on_click({
                 let action = self.to_toggle_action();
                 move |_, cx| {
                     cx.dispatch_action(action.boxed_clone());
                 }
             })
-            .variant(ui::ButtonVariant::Ghost)
-            .when(active, |button| button.variant(ButtonVariant::Filled))
+            .style(ButtonStyle2::Subtle)
+            .when(active, |button| button.style(ButtonStyle2::Filled))
     }
 }
 
 fn toggle_replace_button(active: bool) -> impl IntoElement {
     // todo: add toggle_replace button
-    ui::IconButton::new(0, ui::Icon::Replace)
+    IconButton::new(0, Icon::Replace)
         .on_click(|_, cx| {
             cx.dispatch_action(Box::new(ToggleReplace));
             cx.notify();
         })
-        .variant(ui::ButtonVariant::Ghost)
-        .when(active, |button| button.variant(ButtonVariant::Filled))
+        .style(ButtonStyle2::Subtle)
+        .when(active, |button| button.style(ButtonStyle2::Filled))
 }
 
 fn render_replace_button(
     action: impl Action + 'static + Send + Sync,
-    icon: ui::Icon,
+    icon: Icon,
 ) -> impl IntoElement {
     // todo: add tooltip
-    ui::IconButton::new(0, icon).on_click(move |_, cx| {
+    IconButton::new(0, icon).on_click(move |_, cx| {
         cx.dispatch_action(action.boxed_clone());
     })
 }

crates/search2/src/search_bar.rs πŸ”—

@@ -1,12 +1,13 @@
-use gpui::{IntoElement, MouseDownEvent, WindowContext};
-use ui::{Button, ButtonVariant, IconButton};
+use gpui::{ClickEvent, IntoElement, WindowContext};
+use ui::prelude::*;
+use ui::{Button, IconButton};
 
 use crate::mode::SearchMode;
 
 pub(super) fn render_nav_button(
     icon: ui::Icon,
     _active: bool,
-    on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+    on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
 ) -> impl IntoElement {
     // let tooltip_style = cx.theme().tooltip.clone();
     // let cursor_style = if active {
@@ -21,15 +22,9 @@ pub(super) fn render_nav_button(
 pub(crate) fn render_search_mode_button(
     mode: SearchMode,
     is_active: bool,
-    on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+    on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
 ) -> Button {
-    let button_variant = if is_active {
-        ButtonVariant::Filled
-    } else {
-        ButtonVariant::Ghost
-    };
-
-    Button::new(mode.label())
+    Button::new(mode.label(), mode.label())
+        .selected(is_active)
         .on_click(on_click)
-        .variant(button_variant)
 }

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

@@ -1659,13 +1659,13 @@ fn elixir_lang() -> Arc<Language> {
                 target: (identifier) @name)
                 operator: "when")
                 ])
-                (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
+                (#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
                 )
 
             (call
                 target: (identifier) @name
                 (arguments (alias) @name)
-                (#match? @name "^(defmodule|defprotocol)$")) @item
+                (#any-match? @name "^(defmodule|defprotocol)$")) @item
             "#,
         )
         .unwrap(),

crates/storybook2/src/stories/focus.rs πŸ”—

@@ -2,7 +2,7 @@ use gpui::{
     actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,
     WindowContext,
 };
-use theme2::ActiveTheme;
+use ui::prelude::*;
 
 actions!(ActionA, ActionB, ActionC);
 
@@ -33,7 +33,6 @@ impl Render for FocusStory {
         let theme = cx.theme();
         let color_1 = theme.status().created;
         let color_2 = theme.status().modified;
-        let color_3 = theme.status().deleted;
         let color_4 = theme.status().conflict;
         let color_5 = theme.status().ignored;
         let color_6 = theme.status().renamed;
@@ -42,10 +41,10 @@ impl Render for FocusStory {
             .id("parent")
             .focusable()
             .key_context("parent")
-            .on_action(cx.listener(|_, action: &ActionA, cx| {
+            .on_action(cx.listener(|_, _action: &ActionA, _cx| {
                 println!("Action A dispatched on parent");
             }))
-            .on_action(cx.listener(|_, action: &ActionB, cx| {
+            .on_action(cx.listener(|_, _action: &ActionB, _cx| {
                 println!("Action B dispatched on parent");
             }))
             .on_focus(cx.listener(|_, _, _| println!("Parent focused")))
@@ -61,7 +60,7 @@ impl Render for FocusStory {
                 div()
                     .track_focus(&self.child_1_focus)
                     .key_context("child-1")
-                    .on_action(cx.listener(|_, action: &ActionB, cx| {
+                    .on_action(cx.listener(|_, _action: &ActionB, _cx| {
                         println!("Action B dispatched on child 1 during");
                     }))
                     .w_full()
@@ -83,7 +82,7 @@ impl Render for FocusStory {
                 div()
                     .track_focus(&self.child_2_focus)
                     .key_context("child-2")
-                    .on_action(cx.listener(|_, action: &ActionC, cx| {
+                    .on_action(cx.listener(|_, _action: &ActionC, _cx| {
                         println!("Action C dispatched on child 2");
                     }))
                     .w_full()

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

@@ -4,7 +4,8 @@ use gpui::{
 };
 use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
-use theme2::ActiveTheme;
+use ui::prelude::*;
+use ui::{Label, ListItem};
 
 pub struct PickerStory {
     picker: View<Picker<Delegate>>,
@@ -36,7 +37,7 @@ impl Delegate {
 }
 
 impl PickerDelegate for Delegate {
-    type ListItem = Div;
+    type ListItem = ListItem;
 
     fn match_count(&self) -> usize {
         self.candidates.len()
@@ -50,26 +51,20 @@ impl PickerDelegate for Delegate {
         &self,
         ix: usize,
         selected: bool,
-        cx: &mut gpui::ViewContext<Picker<Self>>,
-    ) -> Self::ListItem {
-        let colors = cx.theme().colors();
+        _cx: &mut gpui::ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
         let Some(candidate_ix) = self.matches.get(ix) else {
-            return div();
+            return None;
         };
         // TASK: Make StringMatchCandidate::string a SharedString
         let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone());
 
-        div()
-            .text_color(colors.text)
-            .when(selected, |s| {
-                s.border_l_10().border_color(colors.terminal_ansi_yellow)
-            })
-            .hover(|style| {
-                style
-                    .bg(colors.element_active)
-                    .text_color(colors.text_accent)
-            })
-            .child(candidate)
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .selected(selected)
+                .child(Label::new(candidate)),
+        )
     }
 
     fn selected_index(&self) -> usize {
@@ -81,7 +76,7 @@ impl PickerDelegate for Delegate {
         cx.notify();
     }
 
-    fn confirm(&mut self, secondary: bool, cx: &mut gpui::ViewContext<Picker<Self>>) {
+    fn confirm(&mut self, secondary: bool, _cx: &mut gpui::ViewContext<Picker<Self>>) {
         let candidate_ix = self.matches[self.selected_ix];
         let candidate = self.candidates[candidate_ix].string.clone();
 

crates/storybook2/src/stories/scroll.rs πŸ”—

@@ -1,12 +1,12 @@
 use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
-use theme2::ActiveTheme;
+use ui::prelude::*;
 use ui::Tooltip;
 
 pub struct ScrollStory;
 
 impl ScrollStory {
     pub fn view(cx: &mut WindowContext) -> View<ScrollStory> {
-        cx.build_view(|cx| ScrollStory)
+        cx.build_view(|_cx| ScrollStory)
     }
 }
 

crates/storybook2/src/stories/text.rs πŸ”—

@@ -8,7 +8,7 @@ pub struct TextStory;
 
 impl TextStory {
     pub fn view(cx: &mut WindowContext) -> View<Self> {
-        cx.build_view(|cx| Self)
+        cx.build_view(|_cx| Self)
     }
 }
 
@@ -66,7 +66,7 @@ impl Render for TextStory {
                         }),
                     ]),
                 )
-                .on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| {
+                .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
                     println!("Clicked range {range_ix}");
                 })
             )

crates/storybook2/src/stories/z_index.rs πŸ”—

@@ -9,7 +9,7 @@ pub struct ZIndexStory;
 impl Render for ZIndexStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container().child(Story::title("z-index")).child(
             div()
                 .flex()
@@ -84,7 +84,7 @@ struct ZIndexExample {
 impl RenderOnce for ZIndexExample {
     type Rendered = Div;
 
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
         div()
             .relative()
             .size_full()

crates/storybook2/src/story_selector.rs πŸ”—

@@ -8,7 +8,6 @@ use clap::ValueEnum;
 use gpui::{AnyView, VisualContext};
 use strum::{EnumIter, EnumString, IntoEnumIterator};
 use ui::prelude::*;
-use ui::{AvatarStory, ButtonStory, IconStory, InputStory, LabelStory};
 
 #[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
 #[strum(serialize_all = "snake_case")]
@@ -17,11 +16,14 @@ pub enum ComponentStory {
     Button,
     Checkbox,
     ContextMenu,
+    Disclosure,
     Focus,
     Icon,
-    Input,
+    IconButton,
     Keybinding,
     Label,
+    List,
+    ListItem,
     Scroll,
     Text,
     ZIndex,
@@ -31,15 +33,18 @@ pub enum ComponentStory {
 impl ComponentStory {
     pub fn story(&self, cx: &mut WindowContext) -> AnyView {
         match self {
-            Self::Avatar => cx.build_view(|_| AvatarStory).into(),
-            Self::Button => cx.build_view(|_| ButtonStory).into(),
+            Self::Avatar => cx.build_view(|_| ui::AvatarStory).into(),
+            Self::Button => cx.build_view(|_| ui::ButtonStory).into(),
             Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(),
             Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(),
+            Self::Disclosure => cx.build_view(|_| ui::DisclosureStory).into(),
             Self::Focus => FocusStory::view(cx).into(),
-            Self::Icon => cx.build_view(|_| IconStory).into(),
-            Self::Input => cx.build_view(|_| InputStory).into(),
+            Self::Icon => cx.build_view(|_| ui::IconStory).into(),
+            Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(),
             Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
-            Self::Label => cx.build_view(|_| LabelStory).into(),
+            Self::Label => cx.build_view(|_| ui::LabelStory).into(),
+            Self::List => cx.build_view(|_| ui::ListStory).into(),
+            Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
             Self::Scroll => ScrollStory::view(cx).into(),
             Self::Text => TextStory::view(cx).into(),
             Self::ZIndex => cx.build_view(|_| ZIndexStory).into(),

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

@@ -1,5 +1,3 @@
-#![allow(dead_code, unused_variables)]
-
 mod assets;
 mod stories;
 mod story_selector;
@@ -70,7 +68,7 @@ fn main() {
         language::init(cx);
         editor::init(cx);
 
-        let window = cx.open_window(
+        let _window = cx.open_window(
             WindowOptions {
                 bounds: WindowBounds::Fixed(Bounds {
                     origin: Default::default(),
@@ -104,7 +102,7 @@ impl StoryWrapper {
 impl Render for StoryWrapper {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         div()
             .flex()
             .flex_col()

crates/terminal_view2/src/terminal_view.rs πŸ”—

@@ -298,9 +298,12 @@ impl TerminalView {
         position: gpui::Point<Pixels>,
         cx: &mut ViewContext<Self>,
     ) {
-        self.context_menu = Some(ContextMenu::build(cx, |menu, _| {
-            menu.action("Clear", Box::new(Clear))
-                .action("Close", Box::new(CloseActiveItem { save_intent: None }))
+        self.context_menu = Some(ContextMenu::build(cx, |menu, cx| {
+            menu.action("Clear", Box::new(Clear), cx).action(
+                "Close",
+                Box::new(CloseActiveItem { save_intent: None }),
+                cx,
+            )
         }));
         dbg!(&position);
         // todo!()

crates/theme2/src/default_colors.rs πŸ”—

@@ -23,15 +23,15 @@ impl ThemeColors {
             surface_background: neutral().light().step_2(),
             background: neutral().light().step_1(),
             element_background: neutral().light().step_3(),
-            element_hover: neutral().light().step_4(),
-            element_active: neutral().light().step_5(),
-            element_selected: neutral().light().step_5(),
+            element_hover: neutral().light_alpha().step_4(),
+            element_active: neutral().light_alpha().step_5(),
+            element_selected: neutral().light_alpha().step_5(),
             element_disabled: neutral().light_alpha().step_3(),
             drop_target_background: blue().light_alpha().step_2(),
             ghost_element_background: system.transparent,
-            ghost_element_hover: neutral().light().step_4(),
-            ghost_element_active: neutral().light().step_5(),
-            ghost_element_selected: neutral().light().step_5(),
+            ghost_element_hover: neutral().light_alpha().step_4(),
+            ghost_element_active: neutral().light_alpha().step_5(),
+            ghost_element_selected: neutral().light_alpha().step_5(),
             ghost_element_disabled: neutral().light_alpha().step_3(),
             text: yellow().light().step_9(),
             text_muted: neutral().light().step_11(),
@@ -95,15 +95,15 @@ impl ThemeColors {
             surface_background: neutral().dark().step_2(),
             background: neutral().dark().step_1(),
             element_background: neutral().dark().step_3(),
-            element_hover: neutral().dark().step_4(),
-            element_active: neutral().dark().step_5(),
-            element_selected: neutral().dark().step_5(),
+            element_hover: neutral().dark_alpha().step_4(),
+            element_active: neutral().dark_alpha().step_5(),
+            element_selected: neutral().dark_alpha().step_5(),
             element_disabled: neutral().dark_alpha().step_3(),
             drop_target_background: blue().dark_alpha().step_2(),
             ghost_element_background: system.transparent,
-            ghost_element_hover: neutral().dark().step_4(),
-            ghost_element_active: neutral().dark().step_5(),
-            ghost_element_selected: neutral().dark().step_5(),
+            ghost_element_hover: neutral().dark_alpha().step_4(),
+            ghost_element_active: neutral().dark_alpha().step_5(),
+            ghost_element_selected: neutral().dark_alpha().step_5(),
             ghost_element_disabled: neutral().dark_alpha().step_3(),
             text: neutral().dark().step_12(),
             text_muted: neutral().dark().step_11(),

crates/theme2/src/one_themes.rs πŸ”—

@@ -20,7 +20,7 @@ pub fn one_family() -> ThemeFamily {
 pub(crate) fn one_dark() -> Theme {
     let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.);
     let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.);
-    let elevated_surface = hsla(220. / 360., 12. / 100., 18. / 100., 1.);
+    let elevated_surface = hsla(225. / 360., 12. / 100., 17. / 100., 1.);
 
     let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0);
     let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0);
@@ -48,7 +48,7 @@ pub(crate) fn one_dark() -> Theme {
                 elevated_surface_background: elevated_surface,
                 surface_background: bg,
                 background: bg,
-                element_background: elevated_surface,
+                element_background: hsla(223.0 / 360., 13. / 100., 21. / 100., 1.0),
                 element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
                 element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
                 element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),

crates/theme2/src/registry.rs πŸ”—

@@ -86,6 +86,10 @@ impl ThemeRegistry {
         }));
     }
 
+    pub fn clear(&mut self) {
+        self.themes.clear();
+    }
+
     pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
         self.themes.keys().cloned()
     }

crates/theme2/src/styles/stories/players.rs πŸ”—

@@ -55,9 +55,8 @@ impl Render for PlayerStory {
                             .border_2()
                             .border_color(player.cursor)
                             .child(
-                                img()
+                                img("https://avatars.githubusercontent.com/u/1714999?v=4")
                                     .rounded_full()
-                                    .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
                                     .size_6()
                                     .bg(gpui::red()),
                             )
@@ -67,51 +66,62 @@ impl Render for PlayerStory {
                 .child(div().flex().gap_1().children(
                     cx.theme().players().0.clone().iter_mut().map(|player| {
                         div()
-                                .my_1()
-                                .rounded_xl()
-                                .flex()
-                                .items_center()
-                                .h_8()
-                                .py_0p5()
-                                .px_1p5()
-                                .bg(player.background)
-                                .child(
-                                div().relative().neg_mx_1().rounded_full().z_index(3)
+                            .my_1()
+                            .rounded_xl()
+                            .flex()
+                            .items_center()
+                            .h_8()
+                            .py_0p5()
+                            .px_1p5()
+                            .bg(player.background)
+                            .child(
+                                div()
+                                    .relative()
+                                    .neg_mx_1()
+                                    .rounded_full()
+                                    .z_index(3)
                                     .border_2()
                                     .border_color(player.background)
                                     .size(px(28.))
                                     .child(
-                                    img()
-                                        .rounded_full()
-                                        .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
-                                        .size(px(24.))
-                                        .bg(gpui::red()),
-                                ),
-                            ).child(
-                            div().relative().neg_mx_1().rounded_full().z_index(2)
-                                .border_2()
-                                .border_color(player.background)
-                                .size(px(28.))
-                                .child(
-                                img()
+                                        img("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                            .rounded_full()
+                                            .size(px(24.))
+                                            .bg(gpui::red()),
+                                    ),
+                            )
+                            .child(
+                                div()
+                                    .relative()
+                                    .neg_mx_1()
                                     .rounded_full()
-                                    .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
-                                    .size(px(24.))
-                                    .bg(gpui::red()),
-                            ),
-                        ).child(
-                        div().relative().neg_mx_1().rounded_full().z_index(1)
-                            .border_2()
-                            .border_color(player.background)
-                            .size(px(28.))
+                                    .z_index(2)
+                                    .border_2()
+                                    .border_color(player.background)
+                                    .size(px(28.))
+                                    .child(
+                                        img("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                            .rounded_full()
+                                            .size(px(24.))
+                                            .bg(gpui::red()),
+                                    ),
+                            )
                             .child(
-                            img()
-                                .rounded_full()
-                                .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
-                                .size(px(24.))
-                                .bg(gpui::red()),
-                        ),
-                    )
+                                div()
+                                    .relative()
+                                    .neg_mx_1()
+                                    .rounded_full()
+                                    .z_index(1)
+                                    .border_2()
+                                    .border_color(player.background)
+                                    .size(px(28.))
+                                    .child(
+                                        img("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                            .rounded_full()
+                                            .size(px(24.))
+                                            .bg(gpui::red()),
+                                    ),
+                            )
                     }),
                 ))
                 .child(Story::label("Player Selections"))

crates/theme_selector2/Cargo.toml πŸ”—

@@ -0,0 +1,29 @@
+[package]
+name = "theme_selector2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/theme_selector.rs"
+doctest = false
+
+[dependencies]
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+fs = { package = "fs2", path = "../fs2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
+picker = { package = "picker2", path = "../picker2" }
+theme = { package = "theme2", path = "../theme2" }
+settings = { package = "settings2", path = "../settings2" }
+feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+util = { path = "../util" }
+log.workspace = true
+parking_lot.workspace = true
+postage.workspace = true
+smol.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/theme_selector2/src/theme_selector.rs πŸ”—

@@ -0,0 +1,276 @@
+use feature_flags::FeatureFlagAppExt;
+use fs::Fs;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+    actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render,
+    SharedString, View, ViewContext, VisualContext, WeakView,
+};
+use picker::{Picker, PickerDelegate};
+use settings::{update_settings_file, SettingsStore};
+use std::sync::Arc;
+use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings};
+use ui::ListItem;
+use util::ResultExt;
+use workspace::{ui::HighlightedLabel, Workspace};
+
+actions!(Toggle, Reload);
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(
+        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
+            workspace.register_action(toggle);
+        },
+    )
+    .detach();
+}
+
+pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+    let fs = workspace.app_state().fs.clone();
+    workspace.toggle_modal(cx, |cx| {
+        ThemeSelector::new(
+            ThemeSelectorDelegate::new(cx.view().downgrade(), fs, cx),
+            cx,
+        )
+    });
+}
+
+#[cfg(debug_assertions)]
+pub fn reload(cx: &mut AppContext) {
+    let current_theme_name = cx.theme().name.clone();
+    let current_theme = cx.update_global(|registry: &mut ThemeRegistry, _cx| {
+        registry.clear();
+        registry.get(&current_theme_name)
+    });
+    match current_theme {
+        Ok(theme) => {
+            ThemeSelectorDelegate::set_theme(theme, cx);
+            log::info!("reloaded theme {}", current_theme_name);
+        }
+        Err(error) => {
+            log::error!("failed to load theme {}: {:?}", current_theme_name, error)
+        }
+    }
+}
+
+pub struct ThemeSelector {
+    picker: View<Picker<ThemeSelectorDelegate>>,
+}
+
+impl EventEmitter<DismissEvent> for ThemeSelector {}
+
+impl FocusableView for ThemeSelector {
+    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+        self.picker.focus_handle(cx)
+    }
+}
+
+impl Render for ThemeSelector {
+    type Element = View<Picker<ThemeSelectorDelegate>>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        self.picker.clone()
+    }
+}
+
+impl ThemeSelector {
+    pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+        Self { picker }
+    }
+}
+
+pub struct ThemeSelectorDelegate {
+    fs: Arc<dyn Fs>,
+    theme_names: Vec<SharedString>,
+    matches: Vec<StringMatch>,
+    original_theme: Arc<Theme>,
+    selection_completed: bool,
+    selected_index: usize,
+    view: WeakView<ThemeSelector>,
+}
+
+impl ThemeSelectorDelegate {
+    fn new(
+        weak_view: WeakView<ThemeSelector>,
+        fs: Arc<dyn Fs>,
+        cx: &mut ViewContext<ThemeSelector>,
+    ) -> Self {
+        let original_theme = cx.theme().clone();
+
+        let staff_mode = cx.is_staff();
+        let registry = cx.global::<Arc<ThemeRegistry>>();
+        let theme_names = registry.list(staff_mode).collect::<Vec<_>>();
+        //todo!(theme sorting)
+        // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
+        let matches = theme_names
+            .iter()
+            .map(|meta| StringMatch {
+                candidate_id: 0,
+                score: 0.0,
+                positions: Default::default(),
+                string: meta.to_string(),
+            })
+            .collect();
+        let mut this = Self {
+            fs,
+            theme_names,
+            matches,
+            original_theme: original_theme.clone(),
+            selected_index: 0,
+            selection_completed: false,
+            view: weak_view,
+        };
+        this.select_if_matching(&original_theme.name);
+        this
+    }
+
+    fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
+        if let Some(mat) = self.matches.get(self.selected_index) {
+            let registry = cx.global::<Arc<ThemeRegistry>>();
+            match registry.get(&mat.string) {
+                Ok(theme) => {
+                    Self::set_theme(theme, cx);
+                }
+                Err(error) => {
+                    log::error!("error loading theme {}: {}", mat.string, error)
+                }
+            }
+        }
+    }
+
+    fn select_if_matching(&mut self, theme_name: &str) {
+        self.selected_index = self
+            .matches
+            .iter()
+            .position(|mat| mat.string == theme_name)
+            .unwrap_or(self.selected_index);
+    }
+
+    fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            let mut theme_settings = store.get::<ThemeSettings>(None).clone();
+            theme_settings.active_theme = theme;
+            store.override_global(theme_settings);
+            cx.refresh();
+        });
+    }
+}
+
+impl PickerDelegate for ThemeSelectorDelegate {
+    type ListItem = ui::ListItem;
+
+    fn placeholder_text(&self) -> Arc<str> {
+        "Select Theme...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
+        self.selection_completed = true;
+
+        let theme_name = cx.theme().name.clone();
+        update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
+            settings.theme = Some(theme_name.to_string());
+        });
+
+        self.view
+            .update(cx, |_, cx| {
+                cx.emit(DismissEvent);
+            })
+            .ok();
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
+        if !self.selection_completed {
+            Self::set_theme(self.original_theme.clone(), cx);
+            self.selection_completed = true;
+        }
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(
+        &mut self,
+        ix: usize,
+        cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
+    ) {
+        self.selected_index = ix;
+        self.show_selected_theme(cx);
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
+    ) -> gpui::Task<()> {
+        let background = cx.background_executor().clone();
+        let candidates = self
+            .theme_names
+            .iter()
+            .enumerate()
+            .map(|(id, meta)| StringMatchCandidate {
+                id,
+                char_bag: meta.as_ref().into(),
+                string: meta.to_string(),
+            })
+            .collect::<Vec<_>>();
+
+        cx.spawn(|this, mut cx| async move {
+            let matches = if query.is_empty() {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, candidate)| StringMatch {
+                        candidate_id: index,
+                        string: candidate.string,
+                        positions: Vec::new(),
+                        score: 0.0,
+                    })
+                    .collect()
+            } else {
+                match_strings(
+                    &candidates,
+                    &query,
+                    false,
+                    100,
+                    &Default::default(),
+                    background,
+                )
+                .await
+            };
+
+            this.update(&mut cx, |this, cx| {
+                this.delegate.matches = matches;
+                this.delegate.selected_index = this
+                    .delegate
+                    .selected_index
+                    .min(this.delegate.matches.len().saturating_sub(1));
+                this.delegate.show_selected_theme(cx);
+            })
+            .log_err();
+        })
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let theme_match = &self.matches[ix];
+
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .selected(selected)
+                .child(HighlightedLabel::new(
+                    theme_match.string.clone(),
+                    theme_match.positions.clone(),
+                )),
+        )
+    }
+}

crates/ui2/Cargo.toml πŸ”—

@@ -15,11 +15,11 @@ gpui = { package = "gpui2", path = "../gpui2" }
 itertools = { version = "0.11.0", optional = true }
 menu = { package = "menu2", path = "../menu2"}
 serde.workspace = true
-settings2 = { path = "../settings2" }
+settings = { package = "settings2", path = "../settings2" }
 smallvec.workspace = true
 story = { path = "../story", optional = true }
 strum = { version = "0.25.0", features = ["derive"] }
-theme2 = { path = "../theme2" }
+theme = { package = "theme2", path = "../theme2" }
 rand = "0.8"
 
 [features]

crates/ui2/src/clickable.rs πŸ”—

@@ -0,0 +1,7 @@
+use gpui::{ClickEvent, WindowContext};
+
+/// A trait for elements that can be clicked.
+pub trait Clickable {
+    /// Sets the click handler that will fire whenever the element is clicked.
+    fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
+}

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

@@ -5,15 +5,11 @@ mod context_menu;
 mod disclosure;
 mod divider;
 mod icon;
-mod icon_button;
-mod input;
 mod keybinding;
 mod label;
 mod list;
 mod popover;
-mod slot;
 mod stack;
-mod toggle;
 mod tooltip;
 
 #[cfg(feature = "stories")]
@@ -26,15 +22,11 @@ pub use context_menu::*;
 pub use disclosure::*;
 pub use divider::*;
 pub use icon::*;
-pub use icon_button::*;
-pub use input::*;
 pub use keybinding::*;
 pub use label::*;
 pub use list::*;
 pub use popover::*;
-pub use slot::*;
 pub use stack::*;
-pub use toggle::*;
 pub use tooltip::*;
 
 #[cfg(feature = "stories")]

crates/ui2/src/components/avatar.rs πŸ”—

@@ -1,5 +1,7 @@
+use std::sync::Arc;
+
 use crate::prelude::*;
-use gpui::{img, Img, IntoElement};
+use gpui::{img, rems, Div, ImageData, ImageSource, IntoElement, Styled};
 
 #[derive(Debug, Default, PartialEq, Clone)]
 pub enum Shape {
@@ -10,15 +12,16 @@ pub enum Shape {
 
 #[derive(IntoElement)]
 pub struct Avatar {
-    src: SharedString,
+    src: ImageSource,
+    is_available: Option<bool>,
     shape: Shape,
 }
 
 impl RenderOnce for Avatar {
-    type Rendered = Img;
+    type Rendered = Div;
 
-    fn render(self, _: &mut WindowContext) -> Self::Rendered {
-        let mut img = img();
+    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        let mut img = img(self.src);
 
         if self.shape == Shape::Circle {
             img = img.rounded_full();
@@ -26,23 +29,60 @@ impl RenderOnce for Avatar {
             img = img.rounded_md();
         }
 
-        img.uri(self.src.clone())
-            .size_4()
-            // todo!(Pull the avatar fallback background from the theme.)
-            .bg(gpui::red())
+        let size = rems(1.0);
+
+        div()
+            .size(size)
+            .child(
+                img.size(size)
+                    // todo!(Pull the avatar fallback background from the theme.)
+                    .bg(gpui::red()),
+            )
+            .children(self.is_available.map(|is_free| {
+                // HACK: non-integer sizes result in oval indicators.
+                let indicator_size = (size.0 * cx.rem_size() * 0.4).round();
+
+                div()
+                    .absolute()
+                    .z_index(1)
+                    .bg(if is_free { gpui::green() } else { gpui::red() })
+                    .size(indicator_size)
+                    .rounded(indicator_size)
+                    .bottom_0()
+                    .right_0()
+            }))
     }
 }
 
 impl Avatar {
-    pub fn new(src: impl Into<SharedString>) -> Self {
+    pub fn uri(src: impl Into<SharedString>) -> Self {
+        Self {
+            src: src.into().into(),
+            shape: Shape::Circle,
+            is_available: None,
+        }
+    }
+    pub fn data(src: Arc<ImageData>) -> Self {
         Self {
             src: src.into(),
             shape: Shape::Circle,
+            is_available: None,
         }
     }
 
+    pub fn source(src: ImageSource) -> Self {
+        Self {
+            src,
+            shape: Shape::Circle,
+            is_available: None,
+        }
+    }
     pub fn shape(mut self, shape: Shape) -> Self {
         self.shape = shape;
         self
     }
+    pub fn availability_indicator(mut self, is_available: impl Into<Option<bool>>) -> Self {
+        self.is_available = is_available.into();
+        self
+    }
 }

crates/ui2/src/components/button.rs πŸ”—

@@ -1,233 +0,0 @@
-use std::rc::Rc;
-
-use gpui::{
-    DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent,
-    StatefulInteractiveElement, WindowContext,
-};
-
-use crate::prelude::*;
-use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
-
-/// Provides the flexibility to use either a standard
-/// button or an icon button in a given context.
-pub enum ButtonOrIconButton {
-    Button(Button),
-    IconButton(IconButton),
-}
-
-impl From<Button> for ButtonOrIconButton {
-    fn from(value: Button) -> Self {
-        Self::Button(value)
-    }
-}
-
-impl From<IconButton> for ButtonOrIconButton {
-    fn from(value: IconButton) -> Self {
-        Self::IconButton(value)
-    }
-}
-
-#[derive(Default, PartialEq, Clone, Copy)]
-pub enum IconPosition {
-    #[default]
-    Left,
-    Right,
-}
-
-#[derive(Default, Copy, Clone, PartialEq)]
-pub enum ButtonVariant {
-    #[default]
-    Ghost,
-    Filled,
-}
-
-impl ButtonVariant {
-    pub fn bg_color(&self, cx: &mut WindowContext) -> Hsla {
-        match self {
-            ButtonVariant::Ghost => cx.theme().colors().ghost_element_background,
-            ButtonVariant::Filled => cx.theme().colors().element_background,
-        }
-    }
-
-    pub fn bg_color_hover(&self, cx: &mut WindowContext) -> Hsla {
-        match self {
-            ButtonVariant::Ghost => cx.theme().colors().ghost_element_hover,
-            ButtonVariant::Filled => cx.theme().colors().element_hover,
-        }
-    }
-
-    pub fn bg_color_active(&self, cx: &mut WindowContext) -> Hsla {
-        match self {
-            ButtonVariant::Ghost => cx.theme().colors().ghost_element_active,
-            ButtonVariant::Filled => cx.theme().colors().element_active,
-        }
-    }
-}
-
-#[derive(IntoElement)]
-pub struct Button {
-    disabled: bool,
-    click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
-    icon: Option<Icon>,
-    icon_position: Option<IconPosition>,
-    label: SharedString,
-    variant: ButtonVariant,
-    width: Option<DefiniteLength>,
-    color: Option<Color>,
-}
-
-impl RenderOnce for Button {
-    type Rendered = gpui::Stateful<Div>;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let (icon_color, label_color) = match (self.disabled, self.color) {
-            (true, _) => (Color::Disabled, Color::Disabled),
-            (_, None) => (Color::Default, Color::Default),
-            (_, Some(color)) => (Color::from(color), color),
-        };
-
-        let mut button = h_stack()
-            .id(SharedString::from(format!("{}", self.label)))
-            .relative()
-            .p_1()
-            .text_ui()
-            .rounded_md()
-            .bg(self.variant.bg_color(cx))
-            .cursor_pointer()
-            .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
-            .active(|style| style.bg(self.variant.bg_color_active(cx)));
-
-        match (self.icon, self.icon_position) {
-            (Some(_), Some(IconPosition::Left)) => {
-                button = button
-                    .gap_1()
-                    .child(self.render_label(label_color))
-                    .children(self.render_icon(icon_color))
-            }
-            (Some(_), Some(IconPosition::Right)) => {
-                button = button
-                    .gap_1()
-                    .children(self.render_icon(icon_color))
-                    .child(self.render_label(label_color))
-            }
-            (_, _) => button = button.child(self.render_label(label_color)),
-        }
-
-        if let Some(width) = self.width {
-            button = button.w(width).justify_center();
-        }
-
-        if let Some(click_handler) = self.click_handler.clone() {
-            button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
-                click_handler(event, cx);
-            });
-        }
-
-        button
-    }
-}
-
-impl Button {
-    pub fn new(label: impl Into<SharedString>) -> Self {
-        Self {
-            disabled: false,
-            click_handler: None,
-            icon: None,
-            icon_position: None,
-            label: label.into(),
-            variant: Default::default(),
-            width: Default::default(),
-            color: None,
-        }
-    }
-
-    pub fn ghost(label: impl Into<SharedString>) -> Self {
-        Self::new(label).variant(ButtonVariant::Ghost)
-    }
-
-    pub fn variant(mut self, variant: ButtonVariant) -> Self {
-        self.variant = variant;
-        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(&MouseDownEvent, &mut WindowContext) + 'static,
-    ) -> Self {
-        self.click_handler = Some(Rc::new(handler));
-        self
-    }
-
-    pub fn disabled(mut self, disabled: bool) -> Self {
-        self.disabled = disabled;
-        self
-    }
-
-    pub fn color(mut self, color: Option<Color>) -> Self {
-        self.color = color;
-        self
-    }
-
-    pub fn label_color(&self, color: Option<Color>) -> Color {
-        if self.disabled {
-            Color::Disabled
-        } else if let Some(color) = color {
-            color
-        } else {
-            Default::default()
-        }
-    }
-
-    fn render_label(&self, color: Color) -> Label {
-        Label::new(self.label.clone())
-            .color(color)
-            .line_height_style(LineHeightStyle::UILabel)
-    }
-
-    fn render_icon(&self, icon_color: Color) -> Option<IconElement> {
-        self.icon.map(|i| IconElement::new(i).color(icon_color))
-    }
-}
-
-#[derive(IntoElement)]
-pub struct ButtonGroup {
-    buttons: Vec<Button>,
-}
-
-impl RenderOnce for ButtonGroup {
-    type Rendered = Div;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let mut group = h_stack();
-
-        for button in self.buttons.into_iter() {
-            group = group.child(button.render(cx));
-        }
-
-        group
-    }
-}
-
-impl ButtonGroup {
-    pub fn new(buttons: Vec<Button>) -> Self {
-        Self { buttons }
-    }
-}

crates/ui2/src/components/button/button.rs πŸ”—

@@ -0,0 +1,91 @@
+use gpui::AnyView;
+
+use crate::prelude::*;
+use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Label, LineHeightStyle};
+
+#[derive(IntoElement)]
+pub struct Button {
+    base: ButtonLike,
+    label: SharedString,
+    label_color: Option<Color>,
+}
+
+impl Button {
+    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
+        Self {
+            base: ButtonLike::new(id),
+            label: label.into(),
+            label_color: None,
+        }
+    }
+
+    pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
+        self.label_color = label_color.into();
+        self
+    }
+}
+
+impl Selectable for Button {
+    fn selected(mut self, selected: bool) -> Self {
+        self.base = self.base.selected(selected);
+        self
+    }
+}
+
+impl Disableable for Button {
+    fn disabled(mut self, disabled: bool) -> Self {
+        self.base = self.base.disabled(disabled);
+        self
+    }
+}
+
+impl Clickable for Button {
+    fn on_click(
+        mut self,
+        handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        self.base = self.base.on_click(handler);
+        self
+    }
+}
+
+impl ButtonCommon for Button {
+    fn id(&self) -> &ElementId {
+        self.base.id()
+    }
+
+    fn style(mut self, style: ButtonStyle2) -> Self {
+        self.base = self.base.style(style);
+        self
+    }
+
+    fn size(mut self, size: ButtonSize2) -> Self {
+        self.base = self.base.size(size);
+        self
+    }
+
+    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.base = self.base.tooltip(tooltip);
+        self
+    }
+}
+
+impl RenderOnce for Button {
+    type Rendered = ButtonLike;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        let label_color = if self.base.disabled {
+            Color::Disabled
+        } else if self.base.selected {
+            Color::Selected
+        } else {
+            Color::Default
+        };
+
+        self.base.child(
+            Label::new(self.label)
+                .color(label_color)
+                .line_height_style(LineHeightStyle::UILabel),
+        )
+    }
+}

crates/ui2/src/components/button/button_like.rs πŸ”—

@@ -0,0 +1,273 @@
+use gpui::{rems, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
+use smallvec::SmallVec;
+
+use crate::h_stack;
+use crate::prelude::*;
+
+pub trait ButtonCommon: Clickable + Disableable {
+    fn id(&self) -> &ElementId;
+    fn style(self, style: ButtonStyle2) -> Self;
+    fn size(self, size: ButtonSize2) -> Self;
+    fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
+pub enum ButtonStyle2 {
+    #[default]
+    Filled,
+    // Tinted,
+    Subtle,
+    Transparent,
+}
+
+#[derive(Debug, Clone)]
+pub struct ButtonStyle {
+    pub background: Hsla,
+    pub border_color: Hsla,
+    pub label_color: Hsla,
+    pub icon_color: Hsla,
+}
+
+impl ButtonStyle2 {
+    pub fn enabled(self, cx: &mut WindowContext) -> ButtonStyle {
+        match self {
+            ButtonStyle2::Filled => ButtonStyle {
+                background: cx.theme().colors().element_background,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Subtle => ButtonStyle {
+                background: cx.theme().colors().ghost_element_background,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Transparent => ButtonStyle {
+                background: gpui::transparent_black(),
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+        }
+    }
+
+    pub fn hovered(self, cx: &mut WindowContext) -> ButtonStyle {
+        match self {
+            ButtonStyle2::Filled => ButtonStyle {
+                background: cx.theme().colors().element_hover,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Subtle => ButtonStyle {
+                background: cx.theme().colors().ghost_element_hover,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Transparent => ButtonStyle {
+                background: gpui::transparent_black(),
+                border_color: gpui::transparent_black(),
+                // TODO: These are not great
+                label_color: Color::Muted.color(cx),
+                // TODO: These are not great
+                icon_color: Color::Muted.color(cx),
+            },
+        }
+    }
+
+    pub fn active(self, cx: &mut WindowContext) -> ButtonStyle {
+        match self {
+            ButtonStyle2::Filled => ButtonStyle {
+                background: cx.theme().colors().element_active,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Subtle => ButtonStyle {
+                background: cx.theme().colors().ghost_element_active,
+                border_color: gpui::transparent_black(),
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Transparent => ButtonStyle {
+                background: gpui::transparent_black(),
+                border_color: gpui::transparent_black(),
+                // TODO: These are not great
+                label_color: Color::Muted.color(cx),
+                // TODO: These are not great
+                icon_color: Color::Muted.color(cx),
+            },
+        }
+    }
+
+    pub fn focused(self, cx: &mut WindowContext) -> ButtonStyle {
+        match self {
+            ButtonStyle2::Filled => ButtonStyle {
+                background: cx.theme().colors().element_background,
+                border_color: cx.theme().colors().border_focused,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Subtle => ButtonStyle {
+                background: cx.theme().colors().ghost_element_background,
+                border_color: cx.theme().colors().border_focused,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle2::Transparent => ButtonStyle {
+                background: gpui::transparent_black(),
+                border_color: cx.theme().colors().border_focused,
+                label_color: Color::Accent.color(cx),
+                icon_color: Color::Accent.color(cx),
+            },
+        }
+    }
+
+    pub fn disabled(self, cx: &mut WindowContext) -> ButtonStyle {
+        match self {
+            ButtonStyle2::Filled => ButtonStyle {
+                background: cx.theme().colors().element_disabled,
+                border_color: cx.theme().colors().border_disabled,
+                label_color: Color::Disabled.color(cx),
+                icon_color: Color::Disabled.color(cx),
+            },
+            ButtonStyle2::Subtle => ButtonStyle {
+                background: cx.theme().colors().ghost_element_disabled,
+                border_color: cx.theme().colors().border_disabled,
+                label_color: Color::Disabled.color(cx),
+                icon_color: Color::Disabled.color(cx),
+            },
+            ButtonStyle2::Transparent => ButtonStyle {
+                background: gpui::transparent_black(),
+                border_color: gpui::transparent_black(),
+                label_color: Color::Disabled.color(cx),
+                icon_color: Color::Disabled.color(cx),
+            },
+        }
+    }
+}
+
+#[derive(Default, PartialEq, Clone, Copy)]
+pub enum ButtonSize2 {
+    #[default]
+    Default,
+    Compact,
+    None,
+}
+
+impl ButtonSize2 {
+    fn height(self) -> Rems {
+        match self {
+            ButtonSize2::Default => rems(22. / 16.),
+            ButtonSize2::Compact => rems(18. / 16.),
+            ButtonSize2::None => rems(16. / 16.),
+        }
+    }
+}
+
+#[derive(IntoElement)]
+pub struct ButtonLike {
+    id: ElementId,
+    pub(super) style: ButtonStyle2,
+    pub(super) disabled: bool,
+    pub(super) selected: bool,
+    size: ButtonSize2,
+    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
+    on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl ButtonLike {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            style: ButtonStyle2::default(),
+            disabled: false,
+            selected: false,
+            size: ButtonSize2::Default,
+            tooltip: None,
+            children: SmallVec::new(),
+            on_click: None,
+        }
+    }
+}
+
+impl Disableable for ButtonLike {
+    fn disabled(mut self, disabled: bool) -> Self {
+        self.disabled = disabled;
+        self
+    }
+}
+
+impl Selectable for ButtonLike {
+    fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+}
+
+impl Clickable for ButtonLike {
+    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
+        self.on_click = Some(Box::new(handler));
+        self
+    }
+}
+
+impl ButtonCommon for ButtonLike {
+    fn id(&self) -> &ElementId {
+        &self.id
+    }
+
+    fn style(mut self, style: ButtonStyle2) -> Self {
+        self.style = style;
+        self
+    }
+
+    fn size(mut self, size: ButtonSize2) -> Self {
+        self.size = size;
+        self
+    }
+
+    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.tooltip = Some(Box::new(tooltip));
+        self
+    }
+}
+
+impl ParentElement for ButtonLike {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
+}
+
+impl RenderOnce for ButtonLike {
+    type Rendered = Stateful<Div>;
+
+    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        h_stack()
+            .id(self.id.clone())
+            .h(self.size.height())
+            .rounded_md()
+            .cursor_pointer()
+            .gap_1()
+            .px_1()
+            .bg(self.style.enabled(cx).background)
+            .hover(|hover| hover.bg(self.style.hovered(cx).background))
+            .active(|active| active.bg(self.style.active(cx).background))
+            .when_some(
+                self.on_click.filter(|_| !self.disabled),
+                |this, on_click| {
+                    this.on_click(move |event, cx| {
+                        cx.stop_propagation();
+                        (on_click)(event, cx)
+                    })
+                },
+            )
+            .when_some(self.tooltip, |this, tooltip| {
+                this.tooltip(move |cx| tooltip(cx))
+            })
+            .children(self.children)
+    }
+}

crates/ui2/src/components/button/icon_button.rs πŸ”—

@@ -0,0 +1,102 @@
+use gpui::{Action, AnyView};
+
+use crate::prelude::*;
+use crate::{ButtonCommon, ButtonLike, ButtonSize2, ButtonStyle2, Icon, IconElement, IconSize};
+
+#[derive(IntoElement)]
+pub struct IconButton {
+    base: ButtonLike,
+    icon: Icon,
+    icon_size: IconSize,
+    icon_color: Color,
+}
+
+impl IconButton {
+    pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
+        Self {
+            base: ButtonLike::new(id),
+            icon,
+            icon_size: IconSize::default(),
+            icon_color: Color::Default,
+        }
+    }
+
+    pub fn icon_size(mut self, icon_size: IconSize) -> Self {
+        self.icon_size = icon_size;
+        self
+    }
+
+    pub fn icon_color(mut self, icon_color: Color) -> Self {
+        self.icon_color = icon_color;
+        self
+    }
+
+    pub fn action(self, action: Box<dyn Action>) -> Self {
+        self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
+    }
+}
+
+impl Disableable for IconButton {
+    fn disabled(mut self, disabled: bool) -> Self {
+        self.base = self.base.disabled(disabled);
+        self
+    }
+}
+
+impl Selectable for IconButton {
+    fn selected(mut self, selected: bool) -> Self {
+        self.base = self.base.selected(selected);
+        self
+    }
+}
+
+impl Clickable for IconButton {
+    fn on_click(
+        mut self,
+        handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        self.base = self.base.on_click(handler);
+        self
+    }
+}
+
+impl ButtonCommon for IconButton {
+    fn id(&self) -> &ElementId {
+        self.base.id()
+    }
+
+    fn style(mut self, style: ButtonStyle2) -> Self {
+        self.base = self.base.style(style);
+        self
+    }
+
+    fn size(mut self, size: ButtonSize2) -> Self {
+        self.base = self.base.size(size);
+        self
+    }
+
+    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.base = self.base.tooltip(tooltip);
+        self
+    }
+}
+
+impl RenderOnce for IconButton {
+    type Rendered = ButtonLike;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        let icon_color = if self.base.disabled {
+            Color::Disabled
+        } else if self.base.selected {
+            Color::Selected
+        } else {
+            self.icon_color
+        };
+
+        self.base.child(
+            IconElement::new(self.icon)
+                .size(self.icon_size)
+                .color(icon_color),
+        )
+    }
+}

crates/ui2/src/components/checkbox.rs πŸ”—

@@ -1,7 +1,6 @@
 use gpui::{div, prelude::*, Div, Element, ElementId, IntoElement, Styled, WindowContext};
 
-use theme2::ActiveTheme;
-
+use crate::prelude::*;
 use crate::{Color, Icon, IconElement, Selection};
 
 pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;

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

@@ -1,23 +1,28 @@
-use std::cell::RefCell;
-use std::rc::Rc;
-
-use crate::{prelude::*, v_stack, Label, List};
-use crate::{ListItem, ListSeparator, ListSubHeader};
+use crate::{
+    h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader,
+};
 use gpui::{
-    overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DispatchPhase,
-    Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, Manager,
-    MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
+    overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DismissEvent, DispatchPhase,
+    Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton,
+    MouseDownEvent, Pixels, Point, Render, View, VisualContext,
 };
+use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
+use std::{cell::RefCell, rc::Rc};
 
 pub enum ContextMenuItem {
     Separator,
     Header(SharedString),
-    Entry(SharedString, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
+    Entry {
+        label: SharedString,
+        handler: Rc<dyn Fn(&mut WindowContext)>,
+        key_binding: Option<KeyBinding>,
+    },
 }
 
 pub struct ContextMenu {
     items: Vec<ContextMenuItem>,
     focus_handle: FocusHandle,
+    selected_index: Option<usize>,
 }
 
 impl FocusableView for ContextMenu {
@@ -26,7 +31,7 @@ impl FocusableView for ContextMenu {
     }
 }
 
-impl EventEmitter<Manager> for ContextMenu {}
+impl EventEmitter<DismissEvent> for ContextMenu {}
 
 impl ContextMenu {
     pub fn build(
@@ -39,6 +44,7 @@ impl ContextMenu {
                 Self {
                     items: Default::default(),
                     focus_handle: cx.focus_handle(),
+                    selected_index: None,
                 },
                 cx,
             )
@@ -58,27 +64,90 @@ impl ContextMenu {
     pub fn entry(
         mut self,
         label: impl Into<SharedString>,
-        on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
+        on_click: impl Fn(&mut WindowContext) + 'static,
     ) -> Self {
-        self.items
-            .push(ContextMenuItem::Entry(label.into(), Rc::new(on_click)));
+        self.items.push(ContextMenuItem::Entry {
+            label: label.into(),
+            handler: Rc::new(on_click),
+            key_binding: None,
+        });
         self
     }
 
-    pub fn action(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
-        // todo: add the keybindings to the list entry
-        self.entry(label.into(), move |_, cx| {
-            cx.dispatch_action(action.boxed_clone())
-        })
+    pub fn action(
+        mut self,
+        label: impl Into<SharedString>,
+        action: Box<dyn Action>,
+        cx: &mut WindowContext,
+    ) -> Self {
+        self.items.push(ContextMenuItem::Entry {
+            label: label.into(),
+            key_binding: KeyBinding::for_action(&*action, cx),
+            handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
+        });
+        self
     }
 
     pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
-        // todo!()
-        cx.emit(Manager::Dismiss);
+        if let Some(ContextMenuItem::Entry { handler, .. }) =
+            self.selected_index.and_then(|ix| self.items.get(ix))
+        {
+            (handler)(cx)
+        }
+        cx.emit(DismissEvent);
     }
 
     pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(Manager::Dismiss);
+        cx.emit(DismissEvent);
+    }
+
+    fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
+        self.selected_index = self.items.iter().position(|item| item.is_selectable());
+        cx.notify();
+    }
+
+    fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
+        for (ix, item) in self.items.iter().enumerate().rev() {
+            if item.is_selectable() {
+                self.selected_index = Some(ix);
+                cx.notify();
+                break;
+            }
+        }
+    }
+
+    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
+        if let Some(ix) = self.selected_index {
+            for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
+                if item.is_selectable() {
+                    self.selected_index = Some(ix);
+                    cx.notify();
+                    break;
+                }
+            }
+        } else {
+            self.select_first(&Default::default(), cx);
+        }
+    }
+
+    pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
+        if let Some(ix) = self.selected_index {
+            for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
+                if item.is_selectable() {
+                    self.selected_index = Some(ix);
+                    cx.notify();
+                    break;
+                }
+            }
+        } else {
+            self.select_last(&Default::default(), cx);
+        }
+    }
+}
+
+impl ContextMenuItem {
+    fn is_selectable(&self) -> bool {
+        matches!(self, Self::Entry { .. })
     }
 }
 
@@ -90,38 +159,51 @@ impl Render for ContextMenu {
             v_stack()
                 .min_w(px(200.))
                 .track_focus(&self.focus_handle)
-                .on_mouse_down_out(
-                    cx.listener(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)),
-                )
-                // .on_action(ContextMenu::select_first)
-                // .on_action(ContextMenu::select_last)
-                // .on_action(ContextMenu::select_next)
-                // .on_action(ContextMenu::select_prev)
+                .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx)))
+                .key_context("menu")
+                .on_action(cx.listener(ContextMenu::select_first))
+                .on_action(cx.listener(ContextMenu::select_last))
+                .on_action(cx.listener(ContextMenu::select_next))
+                .on_action(cx.listener(ContextMenu::select_prev))
                 .on_action(cx.listener(ContextMenu::confirm))
                 .on_action(cx.listener(ContextMenu::cancel))
                 .flex_none()
-                // .bg(cx.theme().colors().elevated_surface_background)
-                // .border()
-                // .border_color(cx.theme().colors().border)
                 .child(
-                    List::new().children(self.items.iter().map(|item| match item {
-                        ContextMenuItem::Separator => ListSeparator::new().into_any_element(),
-                        ContextMenuItem::Header(header) => {
-                            ListSubHeader::new(header.clone()).into_any_element()
-                        }
-                        ContextMenuItem::Entry(entry, callback) => {
-                            let callback = callback.clone();
-                            let dismiss = cx.listener(|_, _, cx| cx.emit(Manager::Dismiss));
-
-                            ListItem::new(entry.clone())
-                                .child(Label::new(entry.clone()))
-                                .on_click(move |event, cx| {
-                                    callback(event, cx);
-                                    dismiss(event, cx)
-                                })
-                                .into_any_element()
-                        }
-                    })),
+                    List::new().children(self.items.iter().enumerate().map(
+                        |(ix, item)| match item {
+                            ContextMenuItem::Separator => ListSeparator.into_any_element(),
+                            ContextMenuItem::Header(header) => {
+                                ListSubHeader::new(header.clone()).into_any_element()
+                            }
+                            ContextMenuItem::Entry {
+                                label: entry,
+                                handler: callback,
+                                key_binding,
+                            } => {
+                                let callback = callback.clone();
+                                let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
+
+                                ListItem::new(entry.clone())
+                                    .child(
+                                        h_stack()
+                                            .w_full()
+                                            .justify_between()
+                                            .child(Label::new(entry.clone()))
+                                            .children(
+                                                key_binding
+                                                    .clone()
+                                                    .map(|binding| div().ml_1().child(binding)),
+                                            ),
+                                    )
+                                    .selected(Some(ix) == self.selected_index)
+                                    .on_click(move |event, cx| {
+                                        callback(cx);
+                                        dismiss(event, cx)
+                                    })
+                                    .into_any_element()
+                            }
+                        },
+                    )),
                 ),
         )
     }
@@ -177,6 +259,7 @@ pub struct MenuHandleState<M> {
     child_element: Option<AnyElement>,
     menu_element: Option<AnyElement>,
 }
+
 impl<M: ManagedView> Element for MenuHandle<M> {
     type State = MenuHandleState<M>;
 
@@ -264,11 +347,9 @@ impl<M: ManagedView> Element for MenuHandle<M> {
 
                 let new_menu = (builder)(cx);
                 let menu2 = menu.clone();
-                cx.subscribe(&new_menu, move |modal, e, cx| match e {
-                    &Manager::Dismiss => {
-                        *menu2.borrow_mut() = None;
-                        cx.notify();
-                    }
+                cx.subscribe(&new_menu, move |_modal, _: &DismissEvent, cx| {
+                    *menu2.borrow_mut() = None;
+                    cx.notify();
                 })
                 .detach();
                 cx.focus_view(&new_menu);

crates/ui2/src/components/disclosure.rs πŸ”—

@@ -1,19 +1,48 @@
-use gpui::{div, Element, ParentElement};
+use std::rc::Rc;
 
-use crate::{Color, Icon, IconElement, IconSize, Toggle};
+use gpui::ClickEvent;
 
-pub fn disclosure_control(toggle: Toggle) -> impl Element {
-    match (toggle.is_toggleable(), toggle.is_toggled()) {
-        (false, _) => div(),
-        (_, true) => div().child(
-            IconElement::new(Icon::ChevronDown)
-                .color(Color::Muted)
-                .size(IconSize::Small),
-        ),
-        (_, false) => div().child(
-            IconElement::new(Icon::ChevronRight)
-                .color(Color::Muted)
-                .size(IconSize::Small),
-        ),
+use crate::prelude::*;
+use crate::{Color, Icon, IconButton, IconSize};
+
+#[derive(IntoElement)]
+pub struct Disclosure {
+    is_open: bool,
+    on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+}
+
+impl Disclosure {
+    pub fn new(is_open: bool) -> Self {
+        Self {
+            is_open,
+            on_toggle: None,
+        }
+    }
+
+    pub fn on_toggle(
+        mut self,
+        handler: impl Into<Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>>,
+    ) -> Self {
+        self.on_toggle = handler.into();
+        self
+    }
+}
+
+impl RenderOnce for Disclosure {
+    type Rendered = IconButton;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        IconButton::new(
+            "toggle",
+            match self.is_open {
+                true => Icon::ChevronDown,
+                false => Icon::ChevronRight,
+            },
+        )
+        .icon_color(Color::Muted)
+        .icon_size(IconSize::Small)
+        .when_some(self.on_toggle, move |this, on_toggle| {
+            this.on_click(move |event, cx| on_toggle(event, cx))
+        })
     }
 }

crates/ui2/src/components/divider.rs πŸ”—

@@ -49,17 +49,4 @@ impl Divider {
         self.inset = true;
         self
     }
-
-    fn render(self, cx: &mut WindowContext) -> impl Element {
-        div()
-            .map(|this| match self.direction {
-                DividerDirection::Horizontal => {
-                    this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
-                }
-                DividerDirection::Vertical => {
-                    this.w_px().h_full().when(self.inset, |this| this.my_1p5())
-                }
-            })
-            .bg(cx.theme().colors().border_variant)
-    }
 }

crates/ui2/src/components/icon.rs πŸ”—

@@ -14,6 +14,8 @@ pub enum IconSize {
 pub enum Icon {
     Ai,
     ArrowLeft,
+    ArrowUp,
+    ArrowDown,
     ArrowRight,
     ArrowUpRight,
     AtSign,
@@ -61,6 +63,7 @@ pub enum Icon {
     Mic,
     MicMute,
     Plus,
+    Public,
     Quote,
     Replace,
     ReplaceAll,
@@ -71,6 +74,11 @@ pub enum Icon {
     Terminal,
     WholeWord,
     XCircle,
+    Command,
+    Control,
+    Shift,
+    Option,
+    Return,
 }
 
 impl Icon {
@@ -79,6 +87,8 @@ impl Icon {
             Icon::Ai => "icons/ai.svg",
             Icon::ArrowLeft => "icons/arrow_left.svg",
             Icon::ArrowRight => "icons/arrow_right.svg",
+            Icon::ArrowUp => "icons/arrow_up.svg",
+            Icon::ArrowDown => "icons/arrow_down.svg",
             Icon::ArrowUpRight => "icons/arrow_up_right.svg",
             Icon::AtSign => "icons/at-sign.svg",
             Icon::AudioOff => "icons/speaker-off.svg",
@@ -125,6 +135,7 @@ impl Icon {
             Icon::Mic => "icons/mic.svg",
             Icon::MicMute => "icons/mic-mute.svg",
             Icon::Plus => "icons/plus.svg",
+            Icon::Public => "icons/public.svg",
             Icon::Quote => "icons/quote.svg",
             Icon::Replace => "icons/replace.svg",
             Icon::ReplaceAll => "icons/replace_all.svg",
@@ -135,6 +146,11 @@ impl Icon {
             Icon::Terminal => "icons/terminal.svg",
             Icon::WholeWord => "icons/word_search.svg",
             Icon::XCircle => "icons/error.svg",
+            Icon::Command => "icons/command.svg",
+            Icon::Control => "icons/control.svg",
+            Icon::Shift => "icons/shift.svg",
+            Icon::Option => "icons/option.svg",
+            Icon::Return => "icons/return.svg",
         }
     }
 }
@@ -151,8 +167,8 @@ impl RenderOnce for IconElement {
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
         let svg_size = match self.size {
-            IconSize::Small => rems(0.75),
-            IconSize::Medium => rems(0.9375),
+            IconSize::Small => rems(14. / 16.),
+            IconSize::Medium => rems(16. / 16.),
         };
 
         svg()
@@ -189,17 +205,4 @@ impl IconElement {
         self.size = size;
         self
     }
-
-    fn render(self, cx: &mut WindowContext) -> impl Element {
-        let svg_size = match self.size {
-            IconSize::Small => rems(0.75),
-            IconSize::Medium => rems(0.9375),
-        };
-
-        svg()
-            .size(svg_size)
-            .flex_none()
-            .path(self.path)
-            .text_color(self.color.color(cx))
-    }
 }

crates/ui2/src/components/icon_button.rs πŸ”—

@@ -1,129 +0,0 @@
-use crate::{h_stack, prelude::*, Icon, IconElement};
-use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
-
-#[derive(IntoElement)]
-pub struct IconButton {
-    id: ElementId,
-    icon: Icon,
-    color: Color,
-    variant: ButtonVariant,
-    state: InteractionState,
-    selected: bool,
-    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
-    on_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
-}
-
-impl RenderOnce for IconButton {
-    type Rendered = Stateful<Div>;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let icon_color = match (self.state, self.color) {
-            (InteractionState::Disabled, _) => Color::Disabled,
-            (InteractionState::Active, _) => Color::Selected,
-            _ => self.color,
-        };
-
-        let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
-            ButtonVariant::Filled => (
-                cx.theme().colors().element_background,
-                cx.theme().colors().element_hover,
-                cx.theme().colors().element_active,
-            ),
-            ButtonVariant::Ghost => (
-                cx.theme().colors().ghost_element_background,
-                cx.theme().colors().ghost_element_hover,
-                cx.theme().colors().ghost_element_active,
-            ),
-        };
-
-        if self.selected {
-            bg_color = cx.theme().colors().element_selected;
-        }
-
-        let mut button = h_stack()
-            .id(self.id.clone())
-            .justify_center()
-            .rounded_md()
-            .p_1()
-            .bg(bg_color)
-            .cursor_pointer()
-            // Nate: Trying to figure out the right places we want to show a
-            // hover state here. I think it is a bit heavy to have it on every
-            // place we use an icon button.
-            // .hover(|style| style.bg(bg_hover_color))
-            .active(|style| style.bg(bg_active_color))
-            .child(IconElement::new(self.icon).color(icon_color));
-
-        if let Some(click_handler) = self.on_mouse_down {
-            button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
-                cx.stop_propagation();
-                click_handler(event, cx);
-            })
-        }
-
-        if let Some(tooltip) = self.tooltip {
-            if !self.selected {
-                button = button.tooltip(move |cx| tooltip(cx))
-            }
-        }
-
-        button
-    }
-}
-
-impl IconButton {
-    pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
-        Self {
-            id: id.into(),
-            icon,
-            color: Color::default(),
-            variant: ButtonVariant::default(),
-            state: InteractionState::default(),
-            selected: false,
-            tooltip: None,
-            on_mouse_down: None,
-        }
-    }
-
-    pub fn icon(mut self, icon: Icon) -> Self {
-        self.icon = icon;
-        self
-    }
-
-    pub fn color(mut self, color: Color) -> 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
-    }
-
-    pub fn selected(mut self, selected: bool) -> Self {
-        self.selected = selected;
-        self
-    }
-
-    pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
-        self.tooltip = Some(Box::new(tooltip));
-        self
-    }
-
-    pub fn on_click(
-        mut self,
-        handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext),
-    ) -> Self {
-        self.on_mouse_down = Some(Box::new(handler));
-        self
-    }
-
-    pub fn action(self, action: Box<dyn Action>) -> Self {
-        self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
-    }
-}

crates/ui2/src/components/input.rs πŸ”—

@@ -1,108 +0,0 @@
-use crate::{prelude::*, Label};
-use gpui::{prelude::*, Div, IntoElement, Stateful};
-
-#[derive(Default, PartialEq)]
-pub enum InputVariant {
-    #[default]
-    Ghost,
-    Filled,
-}
-
-#[derive(IntoElement)]
-pub struct Input {
-    placeholder: SharedString,
-    value: String,
-    state: InteractionState,
-    variant: InputVariant,
-    disabled: bool,
-    is_active: bool,
-}
-
-impl RenderOnce for Input {
-    type Rendered = Stateful<Div>;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
-            InputVariant::Ghost => (
-                cx.theme().colors().ghost_element_background,
-                cx.theme().colors().ghost_element_hover,
-                cx.theme().colors().ghost_element_active,
-            ),
-            InputVariant::Filled => (
-                cx.theme().colors().element_background,
-                cx.theme().colors().element_hover,
-                cx.theme().colors().element_active,
-            ),
-        };
-
-        let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
-            Color::Disabled
-        } else {
-            Color::Placeholder
-        });
-
-        let label = Label::new(self.value.clone()).color(if self.disabled {
-            Color::Disabled
-        } else {
-            Color::Default
-        });
-
-        div()
-            .id("input")
-            .h_7()
-            .w_full()
-            .px_2()
-            .border()
-            .border_color(cx.theme().styles.system.transparent)
-            .bg(input_bg)
-            .hover(|style| style.bg(input_hover_bg))
-            .active(|style| style.bg(input_active_bg))
-            .flex()
-            .items_center()
-            .child(div().flex().items_center().text_ui_sm().map(move |this| {
-                if self.value.is_empty() {
-                    this.child(placeholder_label)
-                } else {
-                    this.child(label)
-                }
-            }))
-    }
-}
-
-impl Input {
-    pub fn new(placeholder: impl Into<SharedString>) -> Self {
-        Self {
-            placeholder: placeholder.into(),
-            value: "".to_string(),
-            state: InteractionState::default(),
-            variant: InputVariant::default(),
-            disabled: false,
-            is_active: false,
-        }
-    }
-
-    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
-    }
-
-    pub fn disabled(mut self, disabled: bool) -> Self {
-        self.disabled = disabled;
-        self
-    }
-
-    pub fn is_active(mut self, is_active: bool) -> Self {
-        self.is_active = is_active;
-        self
-    }
-}

crates/ui2/src/components/keybinding.rs πŸ”—

@@ -1,5 +1,5 @@
-use crate::prelude::*;
-use gpui::{Action, Div, IntoElement};
+use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
+use gpui::{relative, rems, Action, Div, IntoElement, Keystroke};
 
 #[derive(IntoElement, Clone)]
 pub struct KeyBinding {
@@ -14,19 +14,35 @@ impl RenderOnce for KeyBinding {
     type Rendered = Div;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        div()
-            .flex()
+        h_stack()
+            .flex_none()
             .gap_2()
             .children(self.key_binding.keystrokes().iter().map(|keystroke| {
-                div()
-                    .flex()
-                    .gap_1()
+                let key_icon = Self::icon_for_key(&keystroke);
+
+                h_stack()
+                    .flex_none()
+                    .gap_0p5()
+                    .bg(cx.theme().colors().element_background)
+                    .p_0p5()
+                    .rounded_sm()
                     .when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
-                    .when(keystroke.modifiers.control, |el| el.child(Key::new("^")))
-                    .when(keystroke.modifiers.alt, |el| el.child(Key::new("βŒ₯")))
-                    .when(keystroke.modifiers.command, |el| el.child(Key::new("⌘")))
-                    .when(keystroke.modifiers.shift, |el| el.child(Key::new("⇧")))
-                    .child(Key::new(keystroke.key.clone()))
+                    .when(keystroke.modifiers.control, |el| {
+                        el.child(KeyIcon::new(Icon::Control))
+                    })
+                    .when(keystroke.modifiers.alt, |el| {
+                        el.child(KeyIcon::new(Icon::Option))
+                    })
+                    .when(keystroke.modifiers.command, |el| {
+                        el.child(KeyIcon::new(Icon::Command))
+                    })
+                    .when(keystroke.modifiers.shift, |el| {
+                        el.child(KeyIcon::new(Icon::Shift))
+                    })
+                    .when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon)))
+                    .when(key_icon.is_none(), |el| {
+                        el.child(Key::new(keystroke.key.to_uppercase().clone()))
+                    })
             }))
     }
 }
@@ -39,6 +55,22 @@ impl KeyBinding {
         Some(Self::new(key_binding))
     }
 
+    fn icon_for_key(keystroke: &Keystroke) -> Option<Icon> {
+        let mut icon: Option<Icon> = None;
+
+        if keystroke.key == "left".to_string() {
+            icon = Some(Icon::ArrowLeft);
+        } else if keystroke.key == "right".to_string() {
+            icon = Some(Icon::ArrowRight);
+        } else if keystroke.key == "up".to_string() {
+            icon = Some(Icon::ArrowUp);
+        } else if keystroke.key == "down".to_string() {
+            icon = Some(Icon::ArrowDown);
+        }
+
+        icon
+    }
+
     pub fn new(key_binding: gpui::KeyBinding) -> Self {
         Self { key_binding }
     }
@@ -53,13 +85,18 @@ impl RenderOnce for Key {
     type Rendered = Div;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        let single_char = self.key.len() == 1;
+
         div()
-            .px_2()
             .py_0()
-            .rounded_md()
-            .text_ui_sm()
+            .when(single_char, |el| {
+                el.w(rems(14. / 16.)).flex().flex_none().justify_center()
+            })
+            .when(!single_char, |el| el.px_0p5())
+            .h(rems(14. / 16.))
+            .text_ui()
+            .line_height(relative(1.))
             .text_color(cx.theme().colors().text)
-            .bg(cx.theme().colors().element_background)
             .child(self.key.clone())
     }
 }
@@ -69,3 +106,24 @@ impl Key {
         Self { key: key.into() }
     }
 }
+
+#[derive(IntoElement)]
+pub struct KeyIcon {
+    icon: Icon,
+}
+
+impl RenderOnce for KeyIcon {
+    type Rendered = Div;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        div()
+            .w(rems(14. / 16.))
+            .child(IconElement::new(self.icon).size(IconSize::Small))
+    }
+}
+
+impl KeyIcon {
+    pub fn new(icon: Icon) -> Self {
+        Self { icon }
+    }
+}

crates/ui2/src/components/label.rs πŸ”—

@@ -2,7 +2,7 @@ use std::ops::Range;
 
 use crate::prelude::*;
 use crate::styled_ext::StyledExt;
-use gpui::{relative, Div, HighlightStyle, Hsla, IntoElement, StyledText, WindowContext};
+use gpui::{relative, Div, HighlightStyle, IntoElement, StyledText, WindowContext};
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
 pub enum LabelSize {
@@ -178,9 +178,3 @@ impl HighlightedLabel {
         self
     }
 }
-
-/// A run of text that receives the same style.
-struct Run {
-    pub text: String,
-    pub color: Hsla,
-}

crates/ui2/src/components/list.rs πŸ”—

@@ -1,409 +1,18 @@
-use gpui::{
-    div, px, AnyElement, ClickEvent, Div, IntoElement, Stateful, StatefulInteractiveElement,
-};
-use smallvec::SmallVec;
-use std::rc::Rc;
-
-use crate::{
-    disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
-};
-use crate::{prelude::*, GraphicSlot};
-
-#[derive(Clone, Copy, Default, Debug, PartialEq)]
-pub enum ListItemVariant {
-    /// The list item extends to the far left and right of the list.
-    FullWidth,
-    #[default]
-    Inset,
-}
-
-pub enum ListHeaderMeta {
-    // TODO: These should be IconButtons
-    Tools(Vec<Icon>),
-    // TODO: This should be a button
-    Button(Label),
-    Text(Label),
-}
-
-#[derive(IntoElement)]
-pub struct ListHeader {
-    label: SharedString,
-    left_icon: Option<Icon>,
-    meta: Option<ListHeaderMeta>,
-    variant: ListItemVariant,
-    toggle: Toggle,
-}
-
-impl RenderOnce for ListHeader {
-    type Rendered = Div;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let disclosure_control = disclosure_control(self.toggle);
-
-        let meta = match self.meta {
-            Some(ListHeaderMeta::Tools(icons)) => div().child(
-                h_stack()
-                    .gap_2()
-                    .items_center()
-                    .children(icons.into_iter().map(|i| {
-                        IconElement::new(i)
-                            .color(Color::Muted)
-                            .size(IconSize::Small)
-                    })),
-            ),
-            Some(ListHeaderMeta::Button(label)) => div().child(label),
-            Some(ListHeaderMeta::Text(label)) => div().child(label),
-            None => div(),
-        };
-
-        h_stack()
-            .w_full()
-            .bg(cx.theme().colors().surface_background)
-            .relative()
-            .child(
-                div()
-                    .h_5()
-                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
-                    .flex()
-                    .flex_1()
-                    .items_center()
-                    .justify_between()
-                    .w_full()
-                    .gap_1()
-                    .child(
-                        h_stack()
-                            .gap_1()
-                            .child(
-                                div()
-                                    .flex()
-                                    .gap_1()
-                                    .items_center()
-                                    .children(self.left_icon.map(|i| {
-                                        IconElement::new(i)
-                                            .color(Color::Muted)
-                                            .size(IconSize::Small)
-                                    }))
-                                    .child(Label::new(self.label.clone()).color(Color::Muted)),
-                            )
-                            .child(disclosure_control),
-                    )
-                    .child(meta),
-            )
-    }
-}
-
-impl ListHeader {
-    pub fn new(label: impl Into<SharedString>) -> Self {
-        Self {
-            label: label.into(),
-            left_icon: None,
-            meta: None,
-            variant: ListItemVariant::default(),
-            toggle: Toggle::NotToggleable,
-        }
-    }
-
-    pub fn toggle(mut self, toggle: Toggle) -> Self {
-        self.toggle = toggle;
-        self
-    }
-
-    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
-        self.left_icon = left_icon;
-        self
-    }
-
-    pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
-        self.meta = meta;
-        self
-    }
-
-    // before_ship!("delete")
-    // fn render<V: 'static>(self,  cx: &mut WindowContext) -> impl Element<V> {
-    //     let disclosure_control = disclosure_control(self.toggle);
-
-    //     let meta = match self.meta {
-    //         Some(ListHeaderMeta::Tools(icons)) => div().child(
-    //             h_stack()
-    //                 .gap_2()
-    //                 .items_center()
-    //                 .children(icons.into_iter().map(|i| {
-    //                     IconElement::new(i)
-    //                         .color(TextColor::Muted)
-    //                         .size(IconSize::Small)
-    //                 })),
-    //         ),
-    //         Some(ListHeaderMeta::Button(label)) => div().child(label),
-    //         Some(ListHeaderMeta::Text(label)) => div().child(label),
-    //         None => div(),
-    //     };
+mod list_header;
+mod list_item;
+mod list_separator;
+mod list_sub_header;
 
-    //     h_stack()
-    //         .w_full()
-    //         .bg(cx.theme().colors().surface_background)
-    //         // TODO: Add focus state
-    //         // .when(self.state == InteractionState::Focused, |this| {
-    //         //     this.border()
-    //         //         .border_color(cx.theme().colors().border_focused)
-    //         // })
-    //         .relative()
-    //         .child(
-    //             div()
-    //                 .h_5()
-    //                 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
-    //                 .flex()
-    //                 .flex_1()
-    //                 .items_center()
-    //                 .justify_between()
-    //                 .w_full()
-    //                 .gap_1()
-    //                 .child(
-    //                     h_stack()
-    //                         .gap_1()
-    //                         .child(
-    //                             div()
-    //                                 .flex()
-    //                                 .gap_1()
-    //                                 .items_center()
-    //                                 .children(self.left_icon.map(|i| {
-    //                                     IconElement::new(i)
-    //                                         .color(TextColor::Muted)
-    //                                         .size(IconSize::Small)
-    //                                 }))
-    //                                 .child(Label::new(self.label.clone()).color(TextColor::Muted)),
-    //                         )
-    //                         .child(disclosure_control),
-    //                 )
-    //                 .child(meta),
-    //         )
-    // }
-}
-
-#[derive(IntoElement, Clone)]
-pub struct ListSubHeader {
-    label: SharedString,
-    left_icon: Option<Icon>,
-    variant: ListItemVariant,
-}
-
-impl ListSubHeader {
-    pub fn new(label: impl Into<SharedString>) -> Self {
-        Self {
-            label: label.into(),
-            left_icon: None,
-            variant: ListItemVariant::default(),
-        }
-    }
-
-    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
-        self.left_icon = left_icon;
-        self
-    }
-}
-
-impl RenderOnce for ListSubHeader {
-    type Rendered = Div;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        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(Color::Muted)
-                                .size(IconSize::Small)
-                        }))
-                        .child(Label::new(self.label.clone()).color(Color::Muted)),
-                ),
-        )
-    }
-}
-
-#[derive(Default, PartialEq, Copy, Clone)]
-pub enum ListEntrySize {
-    #[default]
-    Small,
-    Medium,
-}
-
-#[derive(IntoElement)]
-pub struct ListItem {
-    id: ElementId,
-    disabled: bool,
-    // TODO: Reintroduce this
-    // disclosure_control_style: DisclosureControlVisibility,
-    indent_level: u32,
-    left_slot: Option<GraphicSlot>,
-    overflow: OverflowStyle,
-    size: ListEntrySize,
-    toggle: Toggle,
-    variant: ListItemVariant,
-    on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
-    children: SmallVec<[AnyElement; 2]>,
-}
-
-impl ListItem {
-    pub fn new(id: impl Into<ElementId>) -> Self {
-        Self {
-            id: id.into(),
-            disabled: false,
-            indent_level: 0,
-            left_slot: None,
-            overflow: OverflowStyle::Hidden,
-            size: ListEntrySize::default(),
-            toggle: Toggle::NotToggleable,
-            variant: ListItemVariant::default(),
-            on_click: Default::default(),
-            children: SmallVec::new(),
-        }
-    }
-
-    pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
-        self.on_click = Some(Rc::new(handler));
-        self
-    }
-
-    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 toggle(mut self, toggle: Toggle) -> Self {
-        self.toggle = toggle;
-        self
-    }
-
-    pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
-        self.left_slot = Some(left_content);
-        self
-    }
-
-    pub fn left_icon(mut self, left_icon: Icon) -> Self {
-        self.left_slot = Some(GraphicSlot::Icon(left_icon));
-        self
-    }
-
-    pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
-        self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
-        self
-    }
-
-    pub fn size(mut self, size: ListEntrySize) -> Self {
-        self.size = size;
-        self
-    }
-}
-
-impl RenderOnce for ListItem {
-    type Rendered = Stateful<Div>;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let left_content = match self.left_slot.clone() {
-            Some(GraphicSlot::Icon(i)) => Some(
-                h_stack().child(
-                    IconElement::new(i)
-                        .size(IconSize::Small)
-                        .color(Color::Muted),
-                ),
-            ),
-            Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
-            Some(GraphicSlot::PublicActor(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()
-            .id(self.id)
-            .relative()
-            .hover(|mut style| {
-                style.background = Some(cx.theme().colors().editor_background.into());
-                style
-            })
-            .on_click({
-                let on_click = self.on_click.clone();
-                move |event, cx| {
-                    if let Some(on_click) = &on_click {
-                        (on_click)(event, cx)
-                    }
-                }
-            })
-            // TODO: Add focus state
-            // .when(self.state == InteractionState::Focused, |this| {
-            //     this.border()
-            //         .border_color(cx.theme().colors().border_focused)
-            // })
-            .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
-            .active(|style| style.bg(cx.theme().colors().ghost_element_active))
-            .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(px(4.))
-                            .h_full()
-                            .flex()
-                            .justify_center()
-                            .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
-                            .child(
-                                h_stack()
-                                    .child(div().w_px().h_full())
-                                    .child(div().w_px().h_full().bg(cx.theme().colors().border)),
-                            )
-                    }))
-                    .flex()
-                    .gap_1()
-                    .items_center()
-                    .relative()
-                    .child(disclosure_control(self.toggle))
-                    .children(left_content)
-                    .children(self.children),
-            )
-    }
-}
-
-impl ParentElement for ListItem {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
-        &mut self.children
-    }
-}
-
-#[derive(IntoElement, Clone)]
-pub struct ListSeparator;
-
-impl ListSeparator {
-    pub fn new() -> Self {
-        Self
-    }
-}
+use gpui::{AnyElement, Div};
+use smallvec::SmallVec;
 
-impl RenderOnce for ListSeparator {
-    type Rendered = Div;
+use crate::prelude::*;
+use crate::{v_stack, Label};
 
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        div().h_px().w_full().bg(cx.theme().colors().border_variant)
-    }
-}
+pub use list_header::*;
+pub use list_item::*;
+pub use list_separator::*;
+pub use list_sub_header::*;
 
 #[derive(IntoElement)]
 pub struct List {
@@ -411,34 +20,16 @@ pub struct List {
     /// Defaults to "No items"
     empty_message: SharedString,
     header: Option<ListHeader>,
-    toggle: Toggle,
+    toggle: Option<bool>,
     children: SmallVec<[AnyElement; 2]>,
 }
 
-impl RenderOnce for List {
-    type Rendered = Div;
-
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let list_content = match (self.children.is_empty(), self.toggle) {
-            (false, _) => div().children(self.children),
-            (true, Toggle::Toggled(false)) => div(),
-            (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
-        };
-
-        v_stack()
-            .w_full()
-            .py_1()
-            .children(self.header.map(|header| header))
-            .child(list_content)
-    }
-}
-
 impl List {
     pub fn new() -> Self {
         Self {
             empty_message: "No items".into(),
             header: None,
-            toggle: Toggle::NotToggleable,
+            toggle: None,
             children: SmallVec::new(),
         }
     }
@@ -453,8 +44,8 @@ impl List {
         self
     }
 
-    pub fn toggle(mut self, toggle: Toggle) -> Self {
-        self.toggle = toggle;
+    pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
+        self.toggle = toggle.into();
         self
     }
 }
@@ -464,3 +55,19 @@ impl ParentElement for List {
         &mut self.children
     }
 }
+
+impl RenderOnce for List {
+    type Rendered = Div;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        v_stack()
+            .w_full()
+            .py_1()
+            .children(self.header.map(|header| header))
+            .map(|this| match (self.children.is_empty(), self.toggle) {
+                (false, _) => this.children(self.children),
+                (true, Some(false)) => this,
+                (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)),
+            })
+    }
+}

crates/ui2/src/components/list/list_header.rs πŸ”—

@@ -0,0 +1,124 @@
+use std::rc::Rc;
+
+use gpui::{ClickEvent, Div};
+
+use crate::prelude::*;
+use crate::{h_stack, Disclosure, Icon, IconButton, IconElement, IconSize, Label};
+
+pub enum ListHeaderMeta {
+    Tools(Vec<IconButton>),
+    // TODO: This should be a button
+    Button(Label),
+    Text(Label),
+}
+
+#[derive(IntoElement)]
+pub struct ListHeader {
+    label: SharedString,
+    left_icon: Option<Icon>,
+    meta: Option<ListHeaderMeta>,
+    toggle: Option<bool>,
+    on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    inset: bool,
+    selected: bool,
+}
+
+impl ListHeader {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+            left_icon: None,
+            meta: None,
+            inset: false,
+            toggle: None,
+            on_toggle: None,
+            selected: false,
+        }
+    }
+
+    pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
+        self.toggle = toggle.into();
+        self
+    }
+
+    pub fn on_toggle(
+        mut self,
+        on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        self.on_toggle = Some(Rc::new(on_toggle));
+        self
+    }
+
+    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+        self.left_icon = left_icon;
+        self
+    }
+
+    pub fn right_button(self, button: IconButton) -> Self {
+        self.meta(Some(ListHeaderMeta::Tools(vec![button])))
+    }
+
+    pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
+        self.meta = meta;
+        self
+    }
+
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+}
+
+impl RenderOnce for ListHeader {
+    type Rendered = Div;
+
+    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        let meta = match self.meta {
+            Some(ListHeaderMeta::Tools(icons)) => div().child(
+                h_stack()
+                    .gap_2()
+                    .items_center()
+                    .children(icons.into_iter().map(|i| i.icon_color(Color::Muted))),
+            ),
+            Some(ListHeaderMeta::Button(label)) => div().child(label),
+            Some(ListHeaderMeta::Text(label)) => div().child(label),
+            None => div(),
+        };
+
+        h_stack().w_full().relative().child(
+            div()
+                .h_5()
+                .when(self.inset, |this| this.px_2())
+                .when(self.selected, |this| {
+                    this.bg(cx.theme().colors().ghost_element_selected)
+                })
+                .flex()
+                .flex_1()
+                .items_center()
+                .justify_between()
+                .w_full()
+                .gap_1()
+                .child(
+                    h_stack()
+                        .gap_1()
+                        .child(
+                            div()
+                                .flex()
+                                .gap_1()
+                                .items_center()
+                                .children(self.left_icon.map(|i| {
+                                    IconElement::new(i)
+                                        .color(Color::Muted)
+                                        .size(IconSize::Small)
+                                }))
+                                .child(Label::new(self.label.clone()).color(Color::Muted)),
+                        )
+                        .children(
+                            self.toggle
+                                .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
+                        ),
+                )
+                .child(meta),
+        )
+    }
+}

crates/ui2/src/components/list/list_item.rs πŸ”—

@@ -0,0 +1,166 @@
+use std::rc::Rc;
+
+use gpui::{
+    px, AnyElement, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, Stateful,
+};
+use smallvec::SmallVec;
+
+use crate::prelude::*;
+use crate::{Avatar, Disclosure, Icon, IconElement, IconSize};
+
+#[derive(IntoElement)]
+pub struct ListItem {
+    id: ElementId,
+    selected: bool,
+    // TODO: Reintroduce this
+    // disclosure_control_style: DisclosureControlVisibility,
+    indent_level: usize,
+    indent_step_size: Pixels,
+    left_slot: Option<AnyElement>,
+    toggle: Option<bool>,
+    inset: bool,
+    on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl ListItem {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            selected: false,
+            indent_level: 0,
+            indent_step_size: px(12.),
+            left_slot: None,
+            toggle: None,
+            inset: false,
+            on_click: None,
+            on_secondary_mouse_down: None,
+            on_toggle: None,
+            children: SmallVec::new(),
+        }
+    }
+
+    pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
+        self.on_click = Some(Rc::new(handler));
+        self
+    }
+
+    pub fn on_secondary_mouse_down(
+        mut self,
+        handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        self.on_secondary_mouse_down = Some(Rc::new(handler));
+        self
+    }
+
+    pub fn inset(mut self, inset: bool) -> Self {
+        self.inset = inset;
+        self
+    }
+
+    pub fn indent_level(mut self, indent_level: usize) -> Self {
+        self.indent_level = indent_level;
+        self
+    }
+
+    pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
+        self.indent_step_size = indent_step_size;
+        self
+    }
+
+    pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
+        self.toggle = toggle.into();
+        self
+    }
+
+    pub fn on_toggle(
+        mut self,
+        on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        self.on_toggle = Some(Rc::new(on_toggle));
+        self
+    }
+
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+
+    pub fn left_child(mut self, left_content: impl IntoElement) -> Self {
+        self.left_slot = Some(left_content.into_any_element());
+        self
+    }
+
+    pub fn left_icon(mut self, left_icon: Icon) -> Self {
+        self.left_slot = Some(
+            IconElement::new(left_icon)
+                .size(IconSize::Small)
+                .color(Color::Muted)
+                .into_any_element(),
+        );
+        self
+    }
+
+    pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
+        self.left_slot = Some(Avatar::source(left_avatar.into()).into_any_element());
+        self
+    }
+}
+
+impl ParentElement for ListItem {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
+}
+
+impl RenderOnce for ListItem {
+    type Rendered = Stateful<Div>;
+
+    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        div()
+            .id(self.id)
+            .relative()
+            // TODO: Add focus state
+            // .when(self.state == InteractionState::Focused, |this| {
+            //     this.border()
+            //         .border_color(cx.theme().colors().border_focused)
+            // })
+            .when(self.inset, |this| this.rounded_md())
+            .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
+            .active(|style| style.bg(cx.theme().colors().ghost_element_active))
+            .when(self.selected, |this| {
+                this.bg(cx.theme().colors().ghost_element_selected)
+            })
+            .when_some(self.on_click, |this, on_click| {
+                this.cursor_pointer().on_click(move |event, cx| {
+                    // HACK: GPUI currently fires `on_click` with any mouse button,
+                    // but we only care about the left button.
+                    if event.down.button == MouseButton::Left {
+                        (on_click)(event, cx)
+                    }
+                })
+            })
+            .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
+                this.on_mouse_down(MouseButton::Right, move |event, cx| {
+                    (on_mouse_down)(event, cx)
+                })
+            })
+            .child(
+                div()
+                    .when(self.inset, |this| this.px_2())
+                    .ml(self.indent_level as f32 * self.indent_step_size)
+                    .flex()
+                    .gap_1()
+                    .items_center()
+                    .relative()
+                    .children(
+                        self.toggle
+                            .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
+                    )
+                    .children(self.left_slot)
+                    .children(self.children),
+            )
+    }
+}

crates/ui2/src/components/list/list_separator.rs πŸ”—

@@ -0,0 +1,14 @@
+use gpui::Div;
+
+use crate::prelude::*;
+
+#[derive(IntoElement)]
+pub struct ListSeparator;
+
+impl RenderOnce for ListSeparator {
+    type Rendered = Div;
+
+    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        div().h_px().w_full().bg(cx.theme().colors().border_variant)
+    }
+}

crates/ui2/src/components/list/list_sub_header.rs πŸ”—

@@ -0,0 +1,56 @@
+use gpui::Div;
+
+use crate::prelude::*;
+use crate::{h_stack, Icon, IconElement, IconSize, Label};
+
+#[derive(IntoElement)]
+pub struct ListSubHeader {
+    label: SharedString,
+    left_icon: Option<Icon>,
+    inset: bool,
+}
+
+impl ListSubHeader {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+            left_icon: None,
+            inset: false,
+        }
+    }
+
+    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+        self.left_icon = left_icon;
+        self
+    }
+}
+
+impl RenderOnce for ListSubHeader {
+    type Rendered = Div;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        h_stack().flex_1().w_full().relative().py_1().child(
+            div()
+                .h_6()
+                .when(self.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(Color::Muted)
+                                .size(IconSize::Small)
+                        }))
+                        .child(Label::new(self.label.clone()).color(Color::Muted)),
+                ),
+        )
+    }
+}

crates/ui2/src/components/popover.rs πŸ”—

@@ -1,10 +1,11 @@
 use gpui::{
-    AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled,
+    div, AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled,
     WindowContext,
 };
 use smallvec::SmallVec;
 
-use crate::{v_stack, StyledExt};
+use crate::prelude::*;
+use crate::v_stack;
 
 /// A popover is used to display a menu or show some options.
 ///
@@ -43,22 +44,16 @@ impl RenderOnce for Popover {
     type Rendered = Div;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        v_stack()
-            .relative()
-            .elevation_2(cx)
-            .p_1()
-            .children(self.children)
+        div()
+            .flex()
+            .gap_1()
+            .child(v_stack().elevation_2(cx).px_1().children(self.children))
             .when_some(self.aside, |this, aside| {
-                // TODO: This will statically position the aside to the top right of the popover.
-                // We should update this to use gpui2::overlay avoid collisions with the window edges.
                 this.child(
                     v_stack()
-                        .top_0()
-                        .left_full()
-                        .ml_1()
-                        .absolute()
                         .elevation_2(cx)
-                        .p_1()
+                        .bg(cx.theme().colors().surface_background)
+                        .px_1()
                         .child(aside),
                 )
             })

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

@@ -2,16 +2,22 @@ mod avatar;
 mod button;
 mod checkbox;
 mod context_menu;
+mod disclosure;
 mod icon;
-mod input;
+mod icon_button;
 mod keybinding;
 mod label;
+mod list;
+mod list_item;
 
 pub use avatar::*;
 pub use button::*;
 pub use checkbox::*;
 pub use context_menu::*;
+pub use disclosure::*;
 pub use icon::*;
-pub use input::*;
+pub use icon_button::*;
 pub use keybinding::*;
 pub use label::*;
+pub use list::*;
+pub use list_item::*;

crates/ui2/src/components/stories/avatar.rs πŸ”—

@@ -9,15 +9,23 @@ pub struct AvatarStory;
 impl Render for AvatarStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container()
             .child(Story::title_for::<Avatar>())
             .child(Story::label("Default"))
-            .child(Avatar::new(
+            .child(Avatar::uri(
                 "https://avatars.githubusercontent.com/u/1714999?v=4",
             ))
-            .child(Avatar::new(
+            .child(Avatar::uri(
                 "https://avatars.githubusercontent.com/u/326587?v=4",
             ))
+            .child(
+                Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4")
+                    .availability_indicator(true),
+            )
+            .child(
+                Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4")
+                    .availability_indicator(false),
+            )
     }
 }

crates/ui2/src/components/stories/button.rs πŸ”—

@@ -1,145 +1,22 @@
-use gpui::{rems, Div, Render};
+use gpui::{Div, Render};
 use story::Story;
-use strum::IntoEnumIterator;
 
 use crate::prelude::*;
-use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};
+use crate::{Button, ButtonStyle2};
 
 pub struct ButtonStory;
 
 impl Render for ButtonStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        let states = InteractionState::iter();
-
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container()
             .child(Story::title_for::<Button>())
-            .child(
-                div()
-                    .flex()
-                    .gap_8()
-                    .child(
-                        div()
-                            .child(Story::label("Ghost (Default)"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Ghost – Left Icon"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Ghost)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Left), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Ghost – Right Icon"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Ghost)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Right), // .state(state),
-                                    )
-                            }))),
-                    )
-                    .child(
-                        div()
-                            .child(Story::label("Filled"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Filled – Left Button"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Left), // .state(state),
-                                    )
-                            })))
-                            .child(Story::label("Filled – Right Button"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Right), // .state(state),
-                                    )
-                            }))),
-                    )
-                    .child(
-                        div()
-                            .child(Story::label("Fixed With"))
-                            .child(h_stack().gap_2().children(states.clone().map(|state| {
-                                v_stack()
-                                    .gap_1()
-                                    .child(Label::new(state.to_string()).color(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            // .state(state)
-                                            .width(Some(rems(6.).into())),
-                                    )
-                            })))
-                            .child(Story::label("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(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            // .state(state)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Left)
-                                            .width(Some(rems(6.).into())),
-                                    )
-                            })))
-                            .child(Story::label("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(Color::Muted))
-                                    .child(
-                                        Button::new("Label")
-                                            .variant(ButtonVariant::Filled)
-                                            // .state(state)
-                                            .icon(Icon::Plus)
-                                            .icon_position(IconPosition::Right)
-                                            .width(Some(rems(6.).into())),
-                                    )
-                            }))),
-                    ),
-            )
-            .child(Story::label("Button with `on_click`"))
-            .child(
-                Button::new("Label")
-                    .variant(ButtonVariant::Ghost)
-                    .on_click(|_, cx| println!("Button clicked.")),
-            )
+            .child(Story::label("Default"))
+            .child(Button::new("default_filled", "Click me"))
+            .child(Story::label("Default (Subtle)"))
+            .child(Button::new("default_subtle", "Click me").style(ButtonStyle2::Subtle))
+            .child(Story::label("Default (Transparent)"))
+            .child(Button::new("default_transparent", "Click me").style(ButtonStyle2::Transparent))
     }
 }

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

@@ -10,11 +10,11 @@ fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<C
     ContextMenu::build(cx, |menu, _| {
         menu.header(header)
             .separator()
-            .entry("Print current time", |v, cx| {
+            .entry("Print current time", |cx| {
                 println!("dispatching PrintCurrentTime action");
                 cx.dispatch_action(PrintCurrentDate.boxed_clone())
             })
-            .entry("Print best foot", |v, cx| {
+            .entry("Print best foot", |cx| {
                 cx.dispatch_action(PrintBestFood.boxed_clone())
             })
     })
@@ -25,7 +25,7 @@ pub struct ContextMenuStory;
 impl Render for ContextMenuStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container()
             .on_action(|_: &PrintCurrentDate, _| {
                 println!("printing unix time!");

crates/ui2/src/components/stories/disclosure.rs πŸ”—

@@ -0,0 +1,20 @@
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::prelude::*;
+use crate::Disclosure;
+
+pub struct DisclosureStory;
+
+impl Render for DisclosureStory {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        Story::container()
+            .child(Story::title_for::<Disclosure>())
+            .child(Story::label("Toggled"))
+            .child(Disclosure::new(true))
+            .child(Story::label("Not Toggled"))
+            .child(Disclosure::new(false))
+    }
+}

crates/ui2/src/components/stories/icon.rs πŸ”—

@@ -10,7 +10,7 @@ pub struct IconStory;
 impl Render for IconStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         let icons = Icon::iter();
 
         Story::container()

crates/ui2/src/components/stories/icon_button.rs πŸ”—

@@ -0,0 +1,47 @@
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::{prelude::*, Tooltip};
+use crate::{Icon, IconButton};
+
+pub struct IconButtonStory;
+
+impl Render for IconButtonStory {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        Story::container()
+            .child(Story::title_for::<IconButton>())
+            .child(Story::label("Default"))
+            .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
+            .child(Story::label("Selected"))
+            .child(
+                div()
+                    .w_8()
+                    .child(IconButton::new("icon_a", Icon::Hash).selected(true)),
+            )
+            .child(Story::label("Disabled"))
+            .child(
+                div()
+                    .w_8()
+                    .child(IconButton::new("icon_a", Icon::Hash).disabled(true)),
+            )
+            .child(Story::label("With `on_click`"))
+            .child(
+                div()
+                    .w_8()
+                    .child(
+                        IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| {
+                            println!("Clicked!");
+                        }),
+                    ),
+            )
+            .child(Story::label("With `tooltip`"))
+            .child(
+                div().w_8().child(
+                    IconButton::new("with_tooltip", Icon::MessageBubbles)
+                        .tooltip(|cx| Tooltip::text("Open messages", cx)),
+                ),
+            )
+    }
+}

crates/ui2/src/components/stories/input.rs πŸ”—

@@ -1,18 +0,0 @@
-use gpui::{Div, Render};
-use story::Story;
-
-use crate::prelude::*;
-use crate::Input;
-
-pub struct InputStory;
-
-impl Render for InputStory {
-    type Element = Div;
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        Story::container()
-            .child(Story::title_for::<Input>())
-            .child(Story::label("Default"))
-            .child(div().flex().child(Input::new("Search")))
-    }
-}

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

@@ -16,7 +16,7 @@ pub fn binding(key: &str) -> gpui::KeyBinding {
 impl Render for KeybindingStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
 
         Story::container()

crates/ui2/src/components/stories/label.rs πŸ”—

@@ -9,7 +9,7 @@ pub struct LabelStory;
 impl Render for LabelStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container()
             .child(Story::title_for::<Label>())
             .child(Story::label("Default"))

crates/ui2/src/components/stories/list.rs πŸ”—

@@ -0,0 +1,38 @@
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::{prelude::*, ListHeader, ListSeparator, ListSubHeader};
+use crate::{List, ListItem};
+
+pub struct ListStory;
+
+impl Render for ListStory {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        Story::container()
+            .child(Story::title_for::<List>())
+            .child(Story::label("Default"))
+            .child(
+                List::new()
+                    .child(ListItem::new("apple").child("Apple"))
+                    .child(ListItem::new("banana").child("Banana"))
+                    .child(ListItem::new("cherry").child("Cherry")),
+            )
+            .child(Story::label("With sections"))
+            .child(
+                List::new()
+                    .child(ListHeader::new("Fruits"))
+                    .child(ListItem::new("apple").child("Apple"))
+                    .child(ListItem::new("banana").child("Banana"))
+                    .child(ListItem::new("cherry").child("Cherry"))
+                    .child(ListSeparator)
+                    .child(ListHeader::new("Vegetables"))
+                    .child(ListSubHeader::new("Root Vegetables"))
+                    .child(ListItem::new("carrot").child("Carrot"))
+                    .child(ListItem::new("potato").child("Potato"))
+                    .child(ListSubHeader::new("Leafy Vegetables"))
+                    .child(ListItem::new("kale").child("Kale")),
+            )
+    }
+}

crates/ui2/src/components/stories/list_item.rs πŸ”—

@@ -0,0 +1,48 @@
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::prelude::*;
+use crate::{Icon, ListItem};
+
+pub struct ListItemStory;
+
+impl Render for ListItemStory {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        Story::container()
+            .child(Story::title_for::<ListItem>())
+            .child(Story::label("Default"))
+            .child(ListItem::new("hello_world").child("Hello, world!"))
+            .child(Story::label("With left icon"))
+            .child(
+                ListItem::new("with_left_icon")
+                    .child("Hello, world!")
+                    .left_icon(Icon::Bell),
+            )
+            .child(Story::label("With left avatar"))
+            .child(
+                ListItem::new("with_left_avatar")
+                    .child("Hello, world!")
+                    .left_avatar(SharedString::from(
+                        "https://avatars.githubusercontent.com/u/1714999?v=4",
+                    )),
+            )
+            .child(Story::label("With `on_click`"))
+            .child(
+                ListItem::new("with_on_click")
+                    .child("Click me")
+                    .on_click(|_event, _cx| {
+                        println!("Clicked!");
+                    }),
+            )
+            .child(Story::label("With `on_secondary_mouse_down`"))
+            .child(
+                ListItem::new("with_on_secondary_mouse_down")
+                    .child("Right click me")
+                    .on_secondary_mouse_down(|_event, _cx| {
+                        println!("Right mouse down!");
+                    }),
+            )
+    }
+}

crates/ui2/src/components/toggle.rs πŸ”—

@@ -1,41 +0,0 @@
-/// Whether the entry is toggleable, and if so, whether it is currently toggled.
-///
-/// To make an element toggleable, simply add a `Toggle::Toggled(_)` and handle it's cases.
-///
-/// You can check if an element is toggleable with `.is_toggleable()`
-///
-/// Possible values:
-/// - `Toggle::NotToggleable` - The entry is not toggleable
-/// - `Toggle::Toggled(true)` - The entry is toggleable and toggled
-/// - `Toggle::Toggled(false)` - The entry is toggleable and not toggled
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-pub enum Toggle {
-    NotToggleable,
-    Toggled(bool),
-}
-
-impl Toggle {
-    /// Returns true if the entry is toggled (or is not toggleable.)
-    ///
-    /// As element that isn't toggleable is always "expanded" or "enabled"
-    /// returning true in that case makes sense.
-    pub fn is_toggled(&self) -> bool {
-        match self {
-            Self::Toggled(false) => false,
-            _ => true,
-        }
-    }
-
-    pub fn is_toggleable(&self) -> bool {
-        match self {
-            Self::Toggled(_) => true,
-            _ => false,
-        }
-    }
-}
-
-impl From<bool> for Toggle {
-    fn from(toggled: bool) -> Self {
-        Toggle::Toggled(toggled)
-    }
-}

crates/ui2/src/components/tooltip.rs πŸ”—

@@ -1,6 +1,6 @@
 use gpui::{overlay, Action, AnyView, IntoElement, Overlay, Render, VisualContext};
-use settings2::Settings;
-use theme2::{ActiveTheme, ThemeSettings};
+use settings::Settings;
+use theme::ThemeSettings;
 
 use crate::prelude::*;
 use crate::{h_stack, v_stack, Color, KeyBinding, Label, LabelSize, StyledExt};
@@ -13,7 +13,7 @@ pub struct Tooltip {
 
 impl Tooltip {
     pub fn text(title: impl Into<SharedString>, cx: &mut WindowContext) -> AnyView {
-        cx.build_view(|cx| Self {
+        cx.build_view(|_cx| Self {
             title: title.into(),
             meta: None,
             key_binding: None,

crates/ui2/src/disableable.rs πŸ”—

@@ -0,0 +1,5 @@
+/// A trait for elements that can be disabled.
+pub trait Disableable {
+    /// Sets whether the element is disabled.
+    fn disabled(self, disabled: bool) -> Self;
+}

crates/ui2/src/fixed.rs πŸ”—

@@ -0,0 +1,10 @@
+use gpui::DefiniteLength;
+
+/// A trait for elements that have a fixed with.
+pub trait FixedWidth {
+    /// Sets the width of the element.
+    fn width(self, width: DefiniteLength) -> Self;
+
+    /// Sets the element's width to the full width of its container.
+    fn full_width(self) -> Self;
+}

crates/ui2/src/prelude.rs πŸ”—

@@ -1,70 +1,12 @@
+pub use gpui::prelude::*;
 pub use gpui::{
     div, Element, ElementId, InteractiveElement, ParentElement, RenderOnce, SharedString, Styled,
     ViewContext, WindowContext,
 };
 
-pub use crate::StyledExt;
-pub use crate::{ButtonVariant, Color};
-pub use theme2::ActiveTheme;
-
-use strum::EnumIter;
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum IconSide {
-    #[default]
-    Left,
-    Right,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumIter)]
-pub enum OverflowStyle {
-    Hidden,
-    Wrap,
-}
-
-#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
-pub enum InteractionState {
-    /// An element that is enabled and not hovered, active, focused, or disabled.
-    ///
-    /// This is often referred to as the "default" state.
-    #[default]
-    Enabled,
-    /// An element that is hovered.
-    Hovered,
-    /// An element has an active mouse down or touch start event on it.
-    Active,
-    /// An element that is focused using the keyboard.
-    Focused,
-    /// An element that is disabled.
-    Disabled,
-    /// A toggleable element that is selected, like the active button in a
-    /// button toggle group.
-    Selected,
-}
-
-impl InteractionState {
-    pub fn if_enabled(&self, enabled: bool) -> Self {
-        if enabled {
-            *self
-        } else {
-            InteractionState::Disabled
-        }
-    }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
-pub enum Selection {
-    #[default]
-    Unselected,
-    Indeterminate,
-    Selected,
-}
-
-impl Selection {
-    pub fn inverse(&self) -> Self {
-        match self {
-            Self::Unselected | Self::Indeterminate => Self::Selected,
-            Self::Selected => Self::Unselected,
-        }
-    }
-}
+pub use crate::clickable::*;
+pub use crate::disableable::*;
+pub use crate::fixed::*;
+pub use crate::selectable::*;
+pub use crate::{ButtonCommon, Color, StyledExt};
+pub use theme::ActiveTheme;

crates/ui2/src/selectable.rs πŸ”—

@@ -0,0 +1,22 @@
+/// A trait for elements that can be selected.
+pub trait Selectable {
+    /// Sets whether the element is selected.
+    fn selected(self, selected: bool) -> Self;
+}
+
+#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
+pub enum Selection {
+    #[default]
+    Unselected,
+    Indeterminate,
+    Selected,
+}
+
+impl Selection {
+    pub fn inverse(&self) -> Self {
+        match self {
+            Self::Unselected | Self::Indeterminate => Self::Selected,
+            Self::Selected => Self::Unselected,
+        }
+    }
+}

crates/ui2/src/components/slot.rs β†’ crates/ui2/src/slot.rs πŸ”—

@@ -1,14 +1,12 @@
-use gpui::SharedString;
+use gpui::{ImageSource, SharedString};
 
 use crate::Icon;
 
-#[derive(Debug, Clone)]
 /// A slot utility that provides a way to to pass either
 /// an icon or an image to a component.
-///
-/// Can be filled with a []
+#[derive(Debug, Clone)]
 pub enum GraphicSlot {
     Icon(Icon),
-    Avatar(SharedString),
+    Avatar(ImageSource),
     PublicActor(SharedString),
 }

crates/ui2/src/styled_ext.rs πŸ”—

@@ -1,12 +1,12 @@
-use gpui::{Styled, WindowContext};
-use theme2::ActiveTheme;
+use gpui::{px, Styled, WindowContext};
 
+use crate::prelude::*;
 use crate::{ElevationIndex, UITextSize};
 
 fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E {
     this.bg(cx.theme().colors().elevated_surface_background)
         .z_index(index.z_index())
-        .rounded_lg()
+        .rounded(px(8.))
         .border()
         .border_color(cx.theme().colors().border_variant)
         .shadow(index.shadow())

crates/ui2/src/styles/color.rs πŸ”—

@@ -1,7 +1,7 @@
 use gpui::{Hsla, WindowContext};
-use theme2::ActiveTheme;
+use theme::ActiveTheme;
 
-#[derive(Default, PartialEq, Copy, Clone)]
+#[derive(Debug, Default, PartialEq, Copy, Clone)]
 pub enum Color {
     #[default]
     Default,

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

@@ -11,16 +11,24 @@
 #![doc = include_str!("../docs/hello-world.md")]
 #![doc = include_str!("../docs/building-ui.md")]
 #![doc = include_str!("../docs/todo.md")]
-// TODO: Fix warnings instead of supressing.
-#![allow(dead_code, unused_variables)]
 
+mod clickable;
 mod components;
+mod disableable;
+mod fixed;
 pub mod prelude;
+mod selectable;
+mod slot;
 mod styled_ext;
 mod styles;
 pub mod utils;
 
+pub use clickable::*;
 pub use components::*;
+pub use disableable::*;
+pub use fixed::*;
 pub use prelude::*;
+pub use selectable::*;
+pub use slot::*;
 pub use styled_ext::*;
 pub use styles::*;

crates/ui2/src/utils/format_distance.rs πŸ”—

@@ -16,55 +16,54 @@ fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
 fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> String {
     let suffix = if distance < 0 { " from now" } else { " ago" };
 
-    let d = distance.abs();
+    let distance = distance.abs();
 
-    let minutes = d / 60;
-    let hours = d / 3600;
-    let days = d / 86400;
-    let months = d / 2592000;
-    let years = d / 31536000;
+    let minutes = distance / 60;
+    let hours = distance / 3_600;
+    let days = distance / 86_400;
+    let months = distance / 2_592_000;
 
-    let string = if d < 5 && include_seconds {
+    let string = if distance < 5 && include_seconds {
         "less than 5 seconds".to_string()
-    } else if d < 10 && include_seconds {
+    } else if distance < 10 && include_seconds {
         "less than 10 seconds".to_string()
-    } else if d < 20 && include_seconds {
+    } else if distance < 20 && include_seconds {
         "less than 20 seconds".to_string()
-    } else if d < 40 && include_seconds {
+    } else if distance < 40 && include_seconds {
         "half a minute".to_string()
-    } else if d < 60 && include_seconds {
+    } else if distance < 60 && include_seconds {
         "less than a minute".to_string()
-    } else if d < 90 && include_seconds {
+    } else if distance < 90 && include_seconds {
         "1 minute".to_string()
-    } else if d < 30 {
+    } else if distance < 30 {
         "less than a minute".to_string()
-    } else if d < 90 {
+    } else if distance < 90 {
         "1 minute".to_string()
-    } else if d < 2700 {
+    } else if distance < 2_700 {
         format!("{} minutes", minutes)
-    } else if d < 5400 {
+    } else if distance < 5_400 {
         "about 1 hour".to_string()
-    } else if d < 86400 {
+    } else if distance < 86_400 {
         format!("about {} hours", hours)
-    } else if d < 172800 {
+    } else if distance < 172_800 {
         "1 day".to_string()
-    } else if d < 2592000 {
+    } else if distance < 2_592_000 {
         format!("{} days", days)
-    } else if d < 5184000 {
+    } else if distance < 5_184_000 {
         "about 1 month".to_string()
-    } else if d < 7776000 {
+    } else if distance < 7_776_000 {
         "about 2 months".to_string()
-    } else if d < 31540000 {
+    } else if distance < 31_540_000 {
         format!("{} months", months)
-    } else if d < 39425000 {
+    } else if distance < 39_425_000 {
         "about 1 year".to_string()
-    } else if d < 55195000 {
+    } else if distance < 55_195_000 {
         "over 1 year".to_string()
-    } else if d < 63080000 {
+    } else if distance < 63_080_000 {
         "almost 2 years".to_string()
     } else {
-        let years = d / 31536000;
-        let remaining_months = (d % 31536000) / 2592000;
+        let years = distance / 31_536_000;
+        let remaining_months = (distance % 31_536_000) / 2_592_000;
 
         if remaining_months < 3 {
             format!("about {} years", years)
@@ -76,7 +75,7 @@ fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> St
     };
 
     if add_suffix {
-        return format!("{}{}", string, suffix);
+        format!("{}{}", string, suffix)
     } else {
         string
     }

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

@@ -19,7 +19,7 @@ lazy_static! {
 
 pub struct AppCommitSha(pub String);
 
-#[derive(Copy, Clone, PartialEq, Eq, Default)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
 pub enum ReleaseChannel {
     #[default]
     Dev,

crates/welcome2/Cargo.toml πŸ”—

@@ -0,0 +1,37 @@
+[package]
+name = "welcome2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/welcome.rs"
+
+[features]
+test-support = []
+
+[dependencies]
+client = { package = "client2", path = "../client2" }
+editor = { package = "editor2", path = "../editor2" }
+fs = { package = "fs2", path = "../fs2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
+db = { package = "db2", path = "../db2" }
+install_cli = { package = "install_cli2", path = "../install_cli2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
+util = { path = "../util" }
+picker = { package = "picker2", path = "../picker2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+# vim = { package = "vim2", path = "../vim2" }
+
+anyhow.workspace = true
+log.workspace = true
+schemars.workspace = true
+serde.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/welcome2/src/base_keymap_picker.rs πŸ”—

@@ -0,0 +1,208 @@
+use super::base_keymap_setting::BaseKeymap;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+    actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Task,
+    View, ViewContext, VisualContext, WeakView,
+};
+use picker::{Picker, PickerDelegate};
+use project::Fs;
+use settings::{update_settings_file, Settings};
+use std::sync::Arc;
+use ui::ListItem;
+use util::ResultExt;
+use workspace::{ui::HighlightedLabel, Workspace};
+
+actions!(ToggleBaseKeymapSelector);
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+        workspace.register_action(toggle);
+    })
+    .detach();
+}
+
+pub fn toggle(
+    workspace: &mut Workspace,
+    _: &ToggleBaseKeymapSelector,
+    cx: &mut ViewContext<Workspace>,
+) {
+    let fs = workspace.app_state().fs.clone();
+    workspace.toggle_modal(cx, |cx| {
+        BaseKeymapSelector::new(
+            BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx),
+            cx,
+        )
+    });
+}
+
+pub struct BaseKeymapSelector {
+    focus_handle: gpui::FocusHandle,
+    picker: View<Picker<BaseKeymapSelectorDelegate>>,
+}
+
+impl FocusableView for BaseKeymapSelector {
+    fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl EventEmitter<DismissEvent> for BaseKeymapSelector {}
+
+impl BaseKeymapSelector {
+    pub fn new(
+        delegate: BaseKeymapSelectorDelegate,
+        cx: &mut ViewContext<BaseKeymapSelector>,
+    ) -> Self {
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+        let focus_handle = cx.focus_handle();
+        Self {
+            focus_handle,
+            picker,
+        }
+    }
+}
+
+impl Render for BaseKeymapSelector {
+    type Element = View<Picker<BaseKeymapSelectorDelegate>>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        self.picker.clone()
+    }
+}
+
+pub struct BaseKeymapSelectorDelegate {
+    view: WeakView<BaseKeymapSelector>,
+    matches: Vec<StringMatch>,
+    selected_index: usize,
+    fs: Arc<dyn Fs>,
+}
+
+impl BaseKeymapSelectorDelegate {
+    fn new(
+        weak_view: WeakView<BaseKeymapSelector>,
+        fs: Arc<dyn Fs>,
+        cx: &mut ViewContext<BaseKeymapSelector>,
+    ) -> Self {
+        let base = BaseKeymap::get(None, cx);
+        let selected_index = BaseKeymap::OPTIONS
+            .iter()
+            .position(|(_, value)| value == base)
+            .unwrap_or(0);
+        Self {
+            view: weak_view,
+            matches: Vec::new(),
+            selected_index,
+            fs,
+        }
+    }
+}
+
+impl PickerDelegate for BaseKeymapSelectorDelegate {
+    type ListItem = ui::ListItem;
+
+    fn placeholder_text(&self) -> Arc<str> {
+        "Select a base keymap...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(
+        &mut self,
+        ix: usize,
+        _: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
+    ) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
+    ) -> Task<()> {
+        let background = cx.background_executor().clone();
+        let candidates = BaseKeymap::names()
+            .enumerate()
+            .map(|(id, name)| StringMatchCandidate {
+                id,
+                char_bag: name.into(),
+                string: name.into(),
+            })
+            .collect::<Vec<_>>();
+
+        cx.spawn(|this, mut cx| async move {
+            let matches = if query.is_empty() {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, candidate)| StringMatch {
+                        candidate_id: index,
+                        string: candidate.string,
+                        positions: Vec::new(),
+                        score: 0.0,
+                    })
+                    .collect()
+            } else {
+                match_strings(
+                    &candidates,
+                    &query,
+                    false,
+                    100,
+                    &Default::default(),
+                    background,
+                )
+                .await
+            };
+
+            this.update(&mut cx, |this, _| {
+                this.delegate.matches = matches;
+                this.delegate.selected_index = this
+                    .delegate
+                    .selected_index
+                    .min(this.delegate.matches.len().saturating_sub(1));
+            })
+            .log_err();
+        })
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
+        if let Some(selection) = self.matches.get(self.selected_index) {
+            let base_keymap = BaseKeymap::from_names(&selection.string);
+            update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
+                *setting = Some(base_keymap)
+            });
+        }
+
+        self.view
+            .update(cx, |_, cx| {
+                cx.emit(DismissEvent);
+            })
+            .ok();
+    }
+
+    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {}
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _cx: &mut gpui::ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let keymap_match = &self.matches[ix];
+
+        Some(
+            ListItem::new(ix)
+                .selected(selected)
+                .inset(true)
+                .child(HighlightedLabel::new(
+                    keymap_match.string.clone(),
+                    keymap_match.positions.clone(),
+                )),
+        )
+    }
+}

crates/welcome2/src/base_keymap_setting.rs πŸ”—

@@ -0,0 +1,65 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Settings;
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
+pub enum BaseKeymap {
+    #[default]
+    VSCode,
+    JetBrains,
+    SublimeText,
+    Atom,
+    TextMate,
+}
+
+impl BaseKeymap {
+    pub const OPTIONS: [(&'static str, Self); 5] = [
+        ("VSCode (Default)", Self::VSCode),
+        ("Atom", Self::Atom),
+        ("JetBrains", Self::JetBrains),
+        ("Sublime Text", Self::SublimeText),
+        ("TextMate", Self::TextMate),
+    ];
+
+    pub fn asset_path(&self) -> Option<&'static str> {
+        match self {
+            BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
+            BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
+            BaseKeymap::Atom => Some("keymaps/atom.json"),
+            BaseKeymap::TextMate => Some("keymaps/textmate.json"),
+            BaseKeymap::VSCode => None,
+        }
+    }
+
+    pub fn names() -> impl Iterator<Item = &'static str> {
+        Self::OPTIONS.iter().map(|(name, _)| *name)
+    }
+
+    pub fn from_names(option: &str) -> BaseKeymap {
+        Self::OPTIONS
+            .iter()
+            .copied()
+            .find_map(|(name, value)| (name == option).then(|| value))
+            .unwrap_or_default()
+    }
+}
+
+impl Settings for BaseKeymap {
+    const KEY: Option<&'static str> = Some("base_keymap");
+
+    type FileContent = Option<Self>;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &mut gpui::AppContext,
+    ) -> anyhow::Result<Self>
+    where
+        Self: Sized,
+    {
+        Ok(user_values
+            .first()
+            .and_then(|v| **v)
+            .unwrap_or(default_value.unwrap()))
+    }
+}

crates/welcome2/src/welcome.rs πŸ”—

@@ -0,0 +1,281 @@
+mod base_keymap_picker;
+mod base_keymap_setting;
+
+use db::kvp::KEY_VALUE_STORE;
+use gpui::{
+    div, red, AnyElement, AppContext, Div, Element, EventEmitter, FocusHandle, Focusable,
+    FocusableView, InteractiveElement, ParentElement, Render, Styled, Subscription, View,
+    ViewContext, VisualContext, WeakView, WindowContext,
+};
+use settings::{Settings, SettingsStore};
+use std::sync::Arc;
+use workspace::{
+    dock::DockPosition,
+    item::{Item, ItemEvent},
+    open_new, AppState, Welcome, Workspace, WorkspaceId,
+};
+
+pub use base_keymap_setting::BaseKeymap;
+
+pub const FIRST_OPEN: &str = "first_open";
+
+pub fn init(cx: &mut AppContext) {
+    BaseKeymap::register(cx);
+
+    cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+        workspace.register_action(|workspace, _: &Welcome, cx| {
+            let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx));
+            workspace.add_item(Box::new(welcome_page), cx)
+        });
+    })
+    .detach();
+
+    base_keymap_picker::init(cx);
+}
+
+pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    open_new(&app_state, cx, |workspace, cx| {
+        workspace.toggle_dock(DockPosition::Left, cx);
+        let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx));
+        workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
+        cx.focus_view(&welcome_page);
+        cx.notify();
+    })
+    .detach();
+
+    db::write_and_log(cx, || {
+        KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
+    });
+}
+
+pub struct WelcomePage {
+    workspace: WeakView<Workspace>,
+    focus_handle: FocusHandle,
+    _settings_subscription: Subscription,
+}
+
+impl Render for WelcomePage {
+    type Element = Focusable<Div>;
+
+    fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+        // todo!(welcome_ui)
+        // let self_handle = cx.handle();
+        // let theme = cx.theme();
+        // let width = theme.welcome.page_width;
+
+        // let telemetry_settings = TelemetrySettings::get(None, cx);
+        // let vim_mode_setting = VimModeSettings::get(cx);
+
+        div()
+            .track_focus(&self.focus_handle)
+            .child(div().size_full().bg(red()).child("Welcome!"))
+        //todo!()
+        //     PaneBackdrop::new(
+        //         self_handle.id(),
+        //         Flex::column()
+        //             .with_child(
+        //                 Flex::column()
+        //                     .with_child(
+        //                         theme::ui::svg(&theme.welcome.logo)
+        //                             .aligned()
+        //                             .contained()
+        //                             .aligned(),
+        //                     )
+        //                     .with_child(
+        //                         Label::new(
+        //                             "Code at the speed of thought",
+        //                             theme.welcome.logo_subheading.text.clone(),
+        //                         )
+        //                         .aligned()
+        //                         .contained()
+        //                         .with_style(theme.welcome.logo_subheading.container),
+        //                     )
+        //                     .contained()
+        //                     .with_style(theme.welcome.heading_group)
+        //                     .constrained()
+        //                     .with_width(width),
+        //             )
+        //             .with_child(
+        //                 Flex::column()
+        //                     .with_child(theme::ui::cta_button::<theme_selector::Toggle, _, _, _>(
+        //                         "Choose a theme",
+        //                         width,
+        //                         &theme.welcome.button,
+        //                         cx,
+        //                         |_, this, cx| {
+        //                             if let Some(workspace) = this.workspace.upgrade(cx) {
+        //                                 workspace.update(cx, |workspace, cx| {
+        //                                     theme_selector::toggle(workspace, &Default::default(), cx)
+        //                                 })
+        //                             }
+        //                         },
+        //                     ))
+        //                     .with_child(theme::ui::cta_button::<ToggleBaseKeymapSelector, _, _, _>(
+        //                         "Choose a keymap",
+        //                         width,
+        //                         &theme.welcome.button,
+        //                         cx,
+        //                         |_, this, cx| {
+        //                             if let Some(workspace) = this.workspace.upgrade(cx) {
+        //                                 workspace.update(cx, |workspace, cx| {
+        //                                     base_keymap_picker::toggle(
+        //                                         workspace,
+        //                                         &Default::default(),
+        //                                         cx,
+        //                                     )
+        //                                 })
+        //                             }
+        //                         },
+        //                     ))
+        //                     .with_child(theme::ui::cta_button::<install_cli::Install, _, _, _>(
+        //                         "Install the CLI",
+        //                         width,
+        //                         &theme.welcome.button,
+        //                         cx,
+        //                         |_, _, cx| {
+        //                             cx.app_context()
+        //                                 .spawn(|cx| async move { install_cli::install_cli(&cx).await })
+        //                                 .detach_and_log_err(cx);
+        //                         },
+        //                     ))
+        //                     .contained()
+        //                     .with_style(theme.welcome.button_group)
+        //                     .constrained()
+        //                     .with_width(width),
+        //             )
+        //             .with_child(
+        //                 Flex::column()
+        //                     .with_child(
+        //                         theme::ui::checkbox::<Diagnostics, Self, _>(
+        //                             "Enable vim mode",
+        //                             &theme.welcome.checkbox,
+        //                             vim_mode_setting,
+        //                             0,
+        //                             cx,
+        //                             |this, checked, cx| {
+        //                                 if let Some(workspace) = this.workspace.upgrade(cx) {
+        //                                     let fs = workspace.read(cx).app_state().fs.clone();
+        //                                     update_settings_file::<VimModeSetting>(
+        //                                         fs,
+        //                                         cx,
+        //                                         move |setting| *setting = Some(checked),
+        //                                     )
+        //                                 }
+        //                             },
+        //                         )
+        //                         .contained()
+        //                         .with_style(theme.welcome.checkbox_container),
+        //                     )
+        //                     .with_child(
+        //                         theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
+        //                             Flex::column()
+        //                                 .with_child(
+        //                                     Label::new(
+        //                                         "Send anonymous usage data",
+        //                                         theme.welcome.checkbox.label.text.clone(),
+        //                                     )
+        //                                     .contained()
+        //                                     .with_style(theme.welcome.checkbox.label.container),
+        //                                 )
+        //                                 .with_child(
+        //                                     Label::new(
+        //                                         "Help > View Telemetry",
+        //                                         theme.welcome.usage_note.text.clone(),
+        //                                     )
+        //                                     .contained()
+        //                                     .with_style(theme.welcome.usage_note.container),
+        //                                 ),
+        //                             &theme.welcome.checkbox,
+        //                             telemetry_settings.metrics,
+        //                             0,
+        //                             cx,
+        //                             |this, checked, cx| {
+        //                                 if let Some(workspace) = this.workspace.upgrade(cx) {
+        //                                     let fs = workspace.read(cx).app_state().fs.clone();
+        //                                     update_settings_file::<TelemetrySettings>(
+        //                                         fs,
+        //                                         cx,
+        //                                         move |setting| setting.metrics = Some(checked),
+        //                                     )
+        //                                 }
+        //                             },
+        //                         )
+        //                         .contained()
+        //                         .with_style(theme.welcome.checkbox_container),
+        //                     )
+        //                     .with_child(
+        //                         theme::ui::checkbox::<Diagnostics, Self, _>(
+        //                             "Send crash reports",
+        //                             &theme.welcome.checkbox,
+        //                             telemetry_settings.diagnostics,
+        //                             1,
+        //                             cx,
+        //                             |this, checked, cx| {
+        //                                 if let Some(workspace) = this.workspace.upgrade(cx) {
+        //                                     let fs = workspace.read(cx).app_state().fs.clone();
+        //                                     update_settings_file::<TelemetrySettings>(
+        //                                         fs,
+        //                                         cx,
+        //                                         move |setting| setting.diagnostics = Some(checked),
+        //                                     )
+        //                                 }
+        //                             },
+        //                         )
+        //                         .contained()
+        //                         .with_style(theme.welcome.checkbox_container),
+        //                     )
+        //                     .contained()
+        //                     .with_style(theme.welcome.checkbox_group)
+        //                     .constrained()
+        //                     .with_width(width),
+        //             )
+        //             .constrained()
+        //             .with_max_width(width)
+        //             .contained()
+        //             .with_uniform_padding(10.)
+        //             .aligned()
+        //             .into_any(),
+        //     )
+        //     .into_any_named("welcome page")
+    }
+}
+
+impl WelcomePage {
+    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
+        WelcomePage {
+            focus_handle: cx.focus_handle(),
+            workspace: workspace.weak_handle(),
+            _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
+        }
+    }
+}
+
+impl EventEmitter<ItemEvent> for WelcomePage {}
+
+impl FocusableView for WelcomePage {
+    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl Item for WelcomePage {
+    fn tab_content(&self, _: Option<usize>, _: &WindowContext) -> AnyElement {
+        "Welcome to Zed!".into_any()
+    }
+
+    fn show_toolbar(&self) -> bool {
+        false
+    }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<View<Self>> {
+        Some(cx.build_view(|cx| WelcomePage {
+            focus_handle: cx.focus_handle(),
+            workspace: self.workspace.clone(),
+            _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
+        }))
+    }
+}

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

@@ -56,14 +56,16 @@ use std::{
 };
 
 use crate::{
-    notifications::{simple_message_notification::MessageNotification, NotificationTracker},
+    notifications::NotificationTracker,
     persistence::model::{
         DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
     },
 };
 use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
 use lazy_static::lazy_static;
-use notifications::{NotificationHandle, NotifyResultExt};
+use notifications::{
+    simple_message_notification::MessageNotification, NotificationHandle, NotifyResultExt,
+};
 pub use pane::*;
 pub use pane_group::*;
 use persistence::{model::SerializedItem, DB};
@@ -776,7 +778,9 @@ impl Workspace {
             }),
         ];
 
-        cx.defer(|this, cx| this.update_window_title(cx));
+        cx.defer(|this, cx| {
+            this.update_window_title(cx);
+        });
         Workspace {
             weak_self: weak_handle.clone(),
             modal: None,

crates/workspace2/src/dock.rs πŸ”—

@@ -7,8 +7,8 @@ use gpui::{
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
-use theme2::ActiveTheme;
-use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
+use ui::prelude::*;
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -686,20 +686,19 @@ impl Render for PanelButtons {
                 let name = entry.panel.persistent_name();
                 let panel = entry.panel.clone();
 
-                let mut button: IconButton = if i == active_index && is_open {
+                let is_active_button = i == active_index && is_open;
+
+                let (action, tooltip) = if is_active_button {
                     let action = dock.toggle_action();
+
                     let tooltip: SharedString =
                         format!("Close {} dock", dock.position.to_label()).into();
-                    IconButton::new(name, icon)
-                        .state(InteractionState::Active)
-                        .action(action.boxed_clone())
-                        .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+
+                    (action, tooltip)
                 } else {
                     let action = entry.panel.toggle_action(cx);
 
-                    IconButton::new(name, icon)
-                        .action(action.boxed_clone())
-                        .tooltip(move |cx| Tooltip::for_action(name, &*action, cx))
+                    (action, name.into())
                 };
 
                 Some(
@@ -717,7 +716,7 @@ impl Render for PanelButtons {
                                         && panel.position_is_valid(position, cx)
                                     {
                                         let panel = panel.clone();
-                                        menu = menu.entry(position.to_label(), move |_, cx| {
+                                        menu = menu.entry(position.to_label(), move |cx| {
                                             panel.set_position(position, cx);
                                         })
                                     }
@@ -727,7 +726,14 @@ impl Render for PanelButtons {
                         })
                         .anchor(menu_anchor)
                         .attach(menu_attach)
-                        .child(|is_open| button.selected(is_open)),
+                        .child(move |_is_open| {
+                            IconButton::new(name, icon)
+                                .selected(is_active_button)
+                                .action(action.boxed_clone())
+                                .tooltip(move |cx| {
+                                    Tooltip::for_action(tooltip.clone(), &*action, cx)
+                                })
+                        }),
                 )
             });
 

crates/workspace2/src/modal_layer.rs πŸ”—

@@ -95,10 +95,6 @@ impl Render for ModalLayer {
                     .track_focus(&active_modal.focus_handle)
                     .child(
                         h_stack()
-                            // needed to prevent mouse events leaking to the
-                            // UI below. // todo! for gpui3.
-                            .on_any_mouse_down(|_, cx| cx.stop_propagation())
-                            .on_any_mouse_up(|_, cx| cx.stop_propagation())
                             .on_mouse_down_out(cx.listener(|this, _, cx| {
                                 this.hide_modal(cx);
                             }))

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

@@ -1,6 +1,9 @@
 use crate::{Toast, Workspace};
 use collections::HashMap;
-use gpui::{AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext};
+use gpui::{
+    AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
+    View, ViewContext, VisualContext,
+};
 use std::{any::TypeId, ops::DerefMut};
 
 pub fn init(cx: &mut AppContext) {
@@ -9,13 +12,9 @@ pub fn init(cx: &mut AppContext) {
     // simple_message_notification::init(cx);
 }
 
-pub enum NotificationEvent {
-    Dismiss,
-}
-
-pub trait Notification: EventEmitter<NotificationEvent> + Render {}
+pub trait Notification: EventEmitter<DismissEvent> + Render {}
 
-impl<V: EventEmitter<NotificationEvent> + Render> Notification for V {}
+impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
 
 pub trait NotificationHandle: Send {
     fn id(&self) -> EntityId;
@@ -107,10 +106,8 @@ impl Workspace {
             let notification = build_notification(cx);
             cx.subscribe(
                 &notification,
-                move |this, handle, event: &NotificationEvent, cx| match event {
-                    NotificationEvent::Dismiss => {
-                        this.dismiss_notification_internal(type_id, id, cx);
-                    }
+                move |this, handle, event: &DismissEvent, cx| {
+                    this.dismiss_notification_internal(type_id, id, cx);
                 },
             )
             .detach();
@@ -120,6 +117,17 @@ impl Workspace {
         }
     }
 
+    pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
+    where
+        E: std::fmt::Debug,
+    {
+        self.show_notification(0, cx, |cx| {
+            cx.build_view(|_cx| {
+                simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
+            })
+        });
+    }
+
     pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
         let type_id = TypeId::of::<V>();
 
@@ -166,13 +174,15 @@ impl Workspace {
 }
 
 pub mod simple_message_notification {
-    use super::NotificationEvent;
-    use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext};
+    use gpui::{
+        div, AnyElement, AppContext, DismissEvent, Div, EventEmitter, InteractiveElement,
+        ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
+        ViewContext,
+    };
     use serde::Deserialize;
     use std::{borrow::Cow, sync::Arc};
-
-    // todo!()
-    // actions!(message_notifications, [CancelMessageNotification]);
+    use ui::prelude::*;
+    use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
 
     #[derive(Clone, Default, Deserialize, PartialEq)]
     pub struct OsOpen(pub Cow<'static, str>);
@@ -197,22 +207,22 @@ pub mod simple_message_notification {
     //     }
 
     enum NotificationMessage {
-        Text(Cow<'static, str>),
+        Text(SharedString),
         Element(fn(TextStyle, &AppContext) -> AnyElement),
     }
 
     pub struct MessageNotification {
         message: NotificationMessage,
         on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>) + Send + Sync>>,
-        click_message: Option<Cow<'static, str>>,
+        click_message: Option<SharedString>,
     }
 
-    impl EventEmitter<NotificationMessage> for MessageNotification {}
+    impl EventEmitter<DismissEvent> for MessageNotification {}
 
     impl MessageNotification {
         pub fn new<S>(message: S) -> MessageNotification
         where
-            S: Into<Cow<'static, str>>,
+            S: Into<SharedString>,
         {
             Self {
                 message: NotificationMessage::Text(message.into()),
@@ -221,19 +231,20 @@ pub mod simple_message_notification {
             }
         }
 
-        pub fn new_element(
-            message: fn(TextStyle, &AppContext) -> AnyElement,
-        ) -> MessageNotification {
-            Self {
-                message: NotificationMessage::Element(message),
-                on_click: None,
-                click_message: None,
-            }
-        }
+        // not needed I think (only for the "new panel" toast, which is outdated now)
+        // pub fn new_element(
+        //     message: fn(TextStyle, &AppContext) -> AnyElement,
+        // ) -> MessageNotification {
+        //     Self {
+        //         message: NotificationMessage::Element(message),
+        //         on_click: None,
+        //         click_message: None,
+        //     }
+        // }
 
         pub fn with_click_message<S>(mut self, message: S) -> Self
         where
-            S: Into<Cow<'static, str>>,
+            S: Into<SharedString>,
         {
             self.click_message = Some(message.into());
             self
@@ -247,17 +258,45 @@ pub mod simple_message_notification {
             self
         }
 
-        // todo!()
-        // pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
-        //     cx.emit(MessageNotificationEvent::Dismiss);
-        // }
+        pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+            cx.emit(DismissEvent);
+        }
     }
 
     impl Render for MessageNotification {
         type Element = Div;
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-            todo!()
+            v_stack()
+                .elevation_3(cx)
+                .p_4()
+                .child(
+                    h_stack()
+                        .justify_between()
+                        .child(div().max_w_80().child(match &self.message {
+                            NotificationMessage::Text(text) => Label::new(text.clone()),
+                            NotificationMessage::Element(element) => {
+                                todo!()
+                            }
+                        }))
+                        .child(
+                            div()
+                                .id("cancel")
+                                .child(IconElement::new(Icon::Close))
+                                .cursor_pointer()
+                                .on_click(cx.listener(|this, event, cx| this.dismiss(cx))),
+                        ),
+                )
+                .children(self.click_message.iter().map(|message| {
+                    Button::new(message.clone(), message.clone()).on_click(cx.listener(
+                        |this, _, cx| {
+                            if let Some(on_click) = this.on_click.as_ref() {
+                                (on_click)(cx)
+                            };
+                            this.dismiss(cx)
+                        },
+                    ))
+                }))
         }
     }
     // todo!()
@@ -359,8 +398,6 @@ pub mod simple_message_notification {
     //                 .into_any()
     //         }
     //     }
-
-    impl EventEmitter<NotificationEvent> for MessageNotification {}
 }
 
 pub trait NotifyResultExt {
@@ -371,6 +408,8 @@ pub trait NotifyResultExt {
         workspace: &mut Workspace,
         cx: &mut ViewContext<Workspace>,
     ) -> Option<Self::Ok>;
+
+    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
 }
 
 impl<T, E> NotifyResultExt for Result<T, E>
@@ -384,14 +423,23 @@ where
             Ok(value) => Some(value),
             Err(err) => {
                 log::error!("TODO {err:?}");
-                // todo!()
-                // workspace.show_notification(0, cx, |cx| {
-                //     cx.add_view(|_cx| {
-                //         simple_message_notification::MessageNotification::new(format!(
-                //             "Error: {err:?}",
-                //         ))
-                //     })
-                // });
+                workspace.show_error(&err, cx);
+                None
+            }
+        }
+    }
+
+    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
+        match self {
+            Ok(value) => Some(value),
+            Err(err) => {
+                log::error!("TODO {err:?}");
+                cx.update(|view, cx| {
+                    if let Ok(workspace) = view.downcast::<Workspace>() {
+                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
+                    }
+                })
+                .ok();
                 None
             }
         }

crates/workspace2/src/pane.rs πŸ”—

@@ -1534,18 +1534,14 @@ impl Pane {
                             .gap_px()
                             .child(
                                 div().border().border_color(gpui::red()).child(
-                                    IconButton::new("navigate_backward", Icon::ArrowLeft).state(
-                                        InteractionState::Enabled
-                                            .if_enabled(self.can_navigate_backward()),
-                                    ),
+                                    IconButton::new("navigate_backward", Icon::ArrowLeft)
+                                        .disabled(!self.can_navigate_backward()),
                                 ),
                             )
                             .child(
                                 div().border().border_color(gpui::red()).child(
-                                    IconButton::new("navigate_forward", Icon::ArrowRight).state(
-                                        InteractionState::Enabled
-                                            .if_enabled(self.can_navigate_forward()),
-                                    ),
+                                    IconButton::new("navigate_forward", Icon::ArrowRight)
+                                        .disabled(!self.can_navigate_forward()),
                                 ),
                             ),
                     ),

crates/workspace2/src/shared_screen.rs πŸ”—

@@ -1,151 +0,0 @@
-use crate::{
-    item::{Item, ItemEvent},
-    ItemNavHistory, WorkspaceId,
-};
-use anyhow::Result;
-use call::participant::{Frame, RemoteVideoTrack};
-use client::{proto::PeerId, User};
-use futures::StreamExt;
-use gpui::{
-    elements::*,
-    geometry::{rect::RectF, vector::vec2f},
-    platform::MouseButton,
-    AppContext, Entity, Task, View, ViewContext,
-};
-use smallvec::SmallVec;
-use std::{
-    borrow::Cow,
-    sync::{Arc, Weak},
-};
-
-pub enum Event {
-    Close,
-}
-
-pub struct SharedScreen {
-    track: Weak<RemoteVideoTrack>,
-    frame: Option<Frame>,
-    pub peer_id: PeerId,
-    user: Arc<User>,
-    nav_history: Option<ItemNavHistory>,
-    _maintain_frame: Task<Result<()>>,
-}
-
-impl SharedScreen {
-    pub fn new(
-        track: &Arc<RemoteVideoTrack>,
-        peer_id: PeerId,
-        user: Arc<User>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let mut frames = track.frames();
-        Self {
-            track: Arc::downgrade(track),
-            frame: None,
-            peer_id,
-            user,
-            nav_history: Default::default(),
-            _maintain_frame: cx.spawn(|this, mut cx| async move {
-                while let Some(frame) = frames.next().await {
-                    this.update(&mut cx, |this, cx| {
-                        this.frame = Some(frame);
-                        cx.notify();
-                    })?;
-                }
-                this.update(&mut cx, |_, cx| cx.emit(Event::Close))?;
-                Ok(())
-            }),
-        }
-    }
-}
-
-impl Entity for SharedScreen {
-    type Event = Event;
-}
-
-impl View for SharedScreen {
-    fn ui_name() -> &'static str {
-        "SharedScreen"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        enum Focus {}
-
-        let frame = self.frame.clone();
-        MouseEventHandler::new::<Focus, _>(0, cx, |_, cx| {
-            Canvas::new(move |bounds, _, _, cx| {
-                if let Some(frame) = frame.clone() {
-                    let size = constrain_size_preserving_aspect_ratio(
-                        bounds.size(),
-                        vec2f(frame.width() as f32, frame.height() as f32),
-                    );
-                    let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.;
-                    cx.scene().push_surface(gpui::platform::mac::Surface {
-                        bounds: RectF::new(origin, size),
-                        image_buffer: frame.image(),
-                    });
-                }
-            })
-            .contained()
-            .with_style(theme::current(cx).shared_screen)
-        })
-        .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent())
-        .into_any()
-    }
-}
-
-impl Item for SharedScreen {
-    fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
-        Some(format!("{}'s screen", self.user.github_login).into())
-    }
-    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(nav_history) = self.nav_history.as_mut() {
-            nav_history.push::<()>(None, cx);
-        }
-    }
-
-    fn tab_content<V: 'static>(
-        &self,
-        _: Option<usize>,
-        style: &theme::Tab,
-        _: &AppContext,
-    ) -> gpui::AnyElement<V> {
-        Flex::row()
-            .with_child(
-                Svg::new("icons/desktop.svg")
-                    .with_color(style.label.text.color)
-                    .constrained()
-                    .with_width(style.type_icon_width)
-                    .aligned()
-                    .contained()
-                    .with_margin_right(style.spacing),
-            )
-            .with_child(
-                Label::new(
-                    format!("{}'s screen", self.user.github_login),
-                    style.label.clone(),
-                )
-                .aligned(),
-            )
-            .into_any()
-    }
-
-    fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
-        self.nav_history = Some(history);
-    }
-
-    fn clone_on_split(
-        &self,
-        _workspace_id: WorkspaceId,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Self> {
-        let track = self.track.upgrade()?;
-        Some(Self::new(&track, self.peer_id, self.user.clone(), cx))
-    }
-
-    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
-        match event {
-            Event::Close => smallvec::smallvec!(ItemEvent::CloseItem),
-        }
-    }
-}

crates/workspace2/src/status_bar.rs πŸ”—

@@ -5,7 +5,7 @@ use gpui::{
     div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
     WindowContext,
 };
-use theme2::ActiveTheme;
+use ui::prelude::*;
 use ui::{h_stack, Button, Icon, IconButton};
 use util::ResultExt;
 
@@ -47,19 +47,7 @@ impl Render for StatusBar {
             .w_full()
             .h_8()
             .bg(cx.theme().colors().status_bar_background)
-            // Nate: I know this isn't how we render status bar tools
-            // We can move these to the correct place once we port their tools
-            .child(
-                h_stack().gap_1().child(self.render_left_tools(cx)).child(
-                    h_stack().gap_4().child(
-                        // TODO: Language Server status
-                        div()
-                            .border()
-                            .border_color(gpui::red())
-                            .child("Checking..."),
-                    ),
-                ),
-            )
+            .child(h_stack().gap_1().child(self.render_left_tools(cx)))
             .child(
                 h_stack()
                     .gap_4()
@@ -71,14 +59,14 @@ impl Render for StatusBar {
                                 div()
                                     .border()
                                     .border_color(gpui::red())
-                                    .child(Button::new("15:22")),
+                                    .child(Button::new("status_line_column_numbers", "15:22")),
                             )
                             .child(
                                 // TODO: Language picker
                                 div()
                                     .border()
                                     .border_color(gpui::red())
-                                    .child(Button::new("Rust")),
+                                    .child(Button::new("status_buffer_language", "Rust")),
                             ),
                     )
                     .child(

crates/workspace2/src/toolbar.rs πŸ”—

@@ -3,8 +3,8 @@ use gpui::{
     div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
     ViewContext, WindowContext,
 };
-use theme2::ActiveTheme;
-use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};
+use ui::prelude::*;
+use ui::{h_stack, v_stack, ButtonLike, Color, Icon, IconButton, Label};
 
 pub enum ToolbarItemEvent {
     ChangeLocation(ToolbarItemLocation),
@@ -90,13 +90,14 @@ impl Render for Toolbar {
                     .justify_between()
                     .child(
                         // Toolbar left side
-                        h_stack()
-                            .border()
-                            .border_color(gpui::red())
-                            .p_1()
-                            .child(Button::new("crates"))
-                            .child(Label::new("/").color(Color::Muted))
-                            .child(Button::new("workspace2")),
+                        h_stack().border().border_color(gpui::red()).p_1().child(
+                            ButtonLike::new("breadcrumb")
+                                .child(Label::new("crates/workspace2/src/toolbar.rs"))
+                                .child(Label::new("β€Ί").color(Color::Muted))
+                                .child(Label::new("impl Render for Toolbar"))
+                                .child(Label::new("β€Ί").color(Color::Muted))
+                                .child(Label::new("fn render")),
+                        ),
                     )
                     // Toolbar right side
                     .child(

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

@@ -9,7 +9,6 @@ pub mod pane_group;
 mod persistence;
 pub mod searchable;
 // todo!()
-// pub mod shared_screen;
 mod modal_layer;
 mod status_bar;
 mod toolbar;
@@ -19,7 +18,7 @@ use anyhow::{anyhow, Context as _, Result};
 use async_trait::async_trait;
 use client2::{
     proto::{self, PeerId},
-    Client, TypedEnvelope, UserStore,
+    Client, TypedEnvelope, User, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
 use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
@@ -327,7 +326,12 @@ pub struct TestCallHandler;
 
 #[cfg(any(test, feature = "test-support"))]
 impl CallHandler for TestCallHandler {
-    fn peer_state(&mut self, id: PeerId, cx: &mut ViewContext<Workspace>) -> Option<(bool, bool)> {
+    fn peer_state(
+        &mut self,
+        id: PeerId,
+        project: &Model<Project>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<(bool, bool)> {
         None
     }
 
@@ -344,14 +348,42 @@ impl CallHandler for TestCallHandler {
         None
     }
 
-    fn hang_up(&self, cx: AsyncWindowContext) -> Result<Task<Result<()>>> {
-        anyhow::bail!("TestCallHandler should not be hanging up")
+    fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
+        Task::ready(Err(anyhow!("TestCallHandler should not be hanging up")))
     }
 
     fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
         None
     }
+
+    fn invite(
+        &mut self,
+        called_user_id: u64,
+        initial_project: Option<Model<Project>>,
+        cx: &mut AppContext,
+    ) -> Task<Result<()>> {
+        unimplemented!()
+    }
+
+    fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
+        None
+    }
+
+    fn is_muted(&self, cx: &AppContext) -> Option<bool> {
+        None
+    }
+
+    fn toggle_mute(&self, cx: &mut AppContext) {}
+
+    fn toggle_screen_share(&self, cx: &mut AppContext) {}
+
+    fn toggle_deafen(&self, cx: &mut AppContext) {}
+
+    fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
+        None
+    }
 }
+
 impl AppState {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut AppContext) -> Arc<Self> {
@@ -382,7 +414,7 @@ impl AppState {
             workspace_store,
             node_runtime: FakeNodeRuntime::new(),
             build_window_options: |_, _, _| Default::default(),
-            call_factory: |_, _| Box::new(TestCallHandler),
+            call_factory: |_| Box::new(TestCallHandler),
         })
     }
 }
@@ -441,7 +473,12 @@ pub enum Event {
 
 #[async_trait(?Send)]
 pub trait CallHandler {
-    fn peer_state(&mut self, id: PeerId, cx: &mut ViewContext<Workspace>) -> Option<(bool, bool)>;
+    fn peer_state(
+        &mut self,
+        id: PeerId,
+        project: &Model<Project>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<(bool, bool)>;
     fn shared_screen_for_peer(
         &self,
         peer_id: PeerId,
@@ -452,8 +489,20 @@ pub trait CallHandler {
     fn is_in_room(&self, cx: &mut ViewContext<Workspace>) -> bool {
         self.room_id(cx).is_some()
     }
-    fn hang_up(&self, cx: AsyncWindowContext) -> Result<Task<Result<()>>>;
+    fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>>;
     fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>>;
+    fn invite(
+        &mut self,
+        called_user_id: u64,
+        initial_project: Option<Model<Project>>,
+        cx: &mut AppContext,
+    ) -> Task<Result<()>>;
+    fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>>;
+    fn is_muted(&self, cx: &AppContext) -> Option<bool>;
+    fn is_deafened(&self, cx: &AppContext) -> Option<bool>;
+    fn toggle_mute(&self, cx: &mut AppContext);
+    fn toggle_deafen(&self, cx: &mut AppContext);
+    fn toggle_screen_share(&self, cx: &mut AppContext);
 }
 
 pub struct Workspace {
@@ -507,7 +556,7 @@ struct FollowerState {
 
 enum WorkspaceBounds {}
 
-type CallFactory = fn(WeakView<Workspace>, &mut ViewContext<Workspace>) -> Box<dyn CallHandler>;
+type CallFactory = fn(&mut ViewContext<Workspace>) -> Box<dyn CallHandler>;
 impl Workspace {
     pub fn new(
         workspace_id: WorkspaceId,
@@ -683,7 +732,21 @@ impl Workspace {
             }),
         ];
 
-        cx.defer(|this, cx| this.update_window_title(cx));
+        cx.defer(|this, cx| {
+            this.update_window_title(cx);
+            // todo! @nate - these are useful for testing notifications
+            // this.show_error(
+            //     &anyhow::anyhow!("what happens if this message is very very very very very long"),
+            //     cx,
+            // );
+
+            // this.show_notification(1, cx, |cx| {
+            //     cx.build_view(|_cx| {
+            //         simple_message_notification::MessageNotification::new(format!("Error:"))
+            //             .with_click_message("click here because!")
+            //     })
+            // });
+        });
         Workspace {
             window_self: window_handle,
             weak_self: weak_handle.clone(),
@@ -707,7 +770,7 @@ impl Workspace {
             last_leaders_by_pane: Default::default(),
             window_edited: false,
 
-            call_handler: (app_state.call_factory)(weak_handle.clone(), cx),
+            call_handler: (app_state.call_factory)(cx),
             database_id: workspace_id,
             app_state,
             _observe_current_user,
@@ -1183,7 +1246,7 @@ impl Workspace {
                 if answer.await.log_err() == Some(1) {
                     return anyhow::Ok(false);
                 } else {
-                    this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx.to_async()))??
+                    this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx))?
                         .await
                         .log_err();
                 }
@@ -1755,22 +1818,22 @@ impl Workspace {
         pane
     }
 
-    //     pub fn add_item_to_center(
-    //         &mut self,
-    //         item: Box<dyn ItemHandle>,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> bool {
-    //         if let Some(center_pane) = self.last_active_center_pane.clone() {
-    //             if let Some(center_pane) = center_pane.upgrade(cx) {
-    //                 center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
-    //                 true
-    //             } else {
-    //                 false
-    //             }
-    //         } else {
-    //             false
-    //         }
-    //     }
+    pub fn add_item_to_center(
+        &mut self,
+        item: Box<dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) -> bool {
+        if let Some(center_pane) = self.last_active_center_pane.clone() {
+            if let Some(center_pane) = center_pane.upgrade() {
+                center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
+                true
+            } else {
+                false
+            }
+        } else {
+            false
+        }
+    }
 
     pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
         self.active_pane
@@ -1967,13 +2030,13 @@ impl Workspace {
         item
     }
 
-    //     pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
-    //         if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
-    //             self.active_pane.update(cx, |pane, cx| {
-    //                 pane.add_item(Box::new(shared_screen), false, true, None, cx)
-    //             });
-    //         }
-    //     }
+    pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
+        if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
+            self.active_pane.update(cx, |pane, cx| {
+                pane.add_item(shared_screen, false, true, None, cx)
+            });
+        }
+    }
 
     pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
         let result = self.panes.iter().find_map(|pane| {
@@ -2289,6 +2352,11 @@ impl Workspace {
         &self.active_pane
     }
 
+    pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
+        let weak_pane = self.panes_by_item.get(&handle.item_id())?;
+        weak_pane.upgrade()
+    }
+
     fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
         self.follower_states.retain(|_, state| {
             if state.leader_id == peer_id {
@@ -2566,32 +2634,30 @@ impl Workspace {
     //         }
     //     }
 
-    //     fn render_notifications(
-    //         &self,
-    //         theme: &theme::Workspace,
-    //         cx: &AppContext,
-    //     ) -> Option<AnyElement<Workspace>> {
-    //         if self.notifications.is_empty() {
-    //             None
-    //         } else {
-    //             Some(
-    //                 Flex::column()
-    //                     .with_children(self.notifications.iter().map(|(_, _, notification)| {
-    //                         ChildView::new(notification.as_any(), cx)
-    //                             .contained()
-    //                             .with_style(theme.notification)
-    //                     }))
-    //                     .constrained()
-    //                     .with_width(theme.notifications.width)
-    //                     .contained()
-    //                     .with_style(theme.notifications.container)
-    //                     .aligned()
-    //                     .bottom()
-    //                     .right()
-    //                     .into_any(),
-    //             )
-    //         }
-    //     }
+    fn render_notifications(&self, cx: &ViewContext<Self>) -> Option<Div> {
+        if self.notifications.is_empty() {
+            None
+        } else {
+            Some(
+                div()
+                    .absolute()
+                    .z_index(100)
+                    .right_3()
+                    .bottom_3()
+                    .w_96()
+                    .h_full()
+                    .flex()
+                    .flex_col()
+                    .justify_end()
+                    .gap_2()
+                    .children(
+                        self.notifications
+                            .iter()
+                            .map(|(_, _, notification)| notification.to_any()),
+                    ),
+            )
+        }
+    }
 
     //     // RPC handlers
 
@@ -2828,7 +2894,7 @@ impl Workspace {
         cx.notify();
 
         let (leader_in_this_project, leader_in_this_app) =
-            self.call_handler.peer_state(leader_id, cx)?;
+            self.call_handler.peer_state(leader_id, &self.project, cx)?;
         let mut items_to_activate = Vec::new();
         for (pane, state) in &self.follower_states {
             if state.leader_id != leader_id {
@@ -2848,10 +2914,10 @@ impl Workspace {
                 }
                 continue;
             }
-            // todo!()
-            // if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
-            //     items_to_activate.push((pane.clone(), Box::new(shared_screen)));
-            // }
+
+            if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
+                items_to_activate.push((pane.clone(), shared_screen));
+            }
         }
 
         for (pane, item) in items_to_activate {
@@ -2872,27 +2938,27 @@ impl Workspace {
         None
     }
 
-    // todo!()
-    //     fn shared_screen_for_peer(
-    //         &self,
-    //         peer_id: PeerId,
-    //         pane: &View<Pane>,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<View<SharedScreen>> {
-    //         let call = self.active_call()?;
-    //         let room = call.read(cx).room()?.read(cx);
-    //         let participant = room.remote_participant_for_peer_id(peer_id)?;
-    //         let track = participant.video_tracks.values().next()?.clone();
-    //         let user = participant.user.clone();
-
-    //         for item in pane.read(cx).items_of_type::<SharedScreen>() {
-    //             if item.read(cx).peer_id == peer_id {
-    //                 return Some(item);
-    //             }
-    //         }
+    fn shared_screen_for_peer(
+        &self,
+        peer_id: PeerId,
+        pane: &View<Pane>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Box<dyn ItemHandle>> {
+        self.call_handler.shared_screen_for_peer(peer_id, pane, cx)
+        // let call = self.active_call()?;
+        // let room = call.read(cx).room()?.read(cx);
+        // let participant = room.remote_participant_for_peer_id(peer_id)?;
+        // let track = participant.video_tracks.values().next()?.clone();
+        // let user = participant.user.clone();
+
+        // for item in pane.read(cx).items_of_type::<SharedScreen>() {
+        //     if item.read(cx).peer_id == peer_id {
+        //         return Some(item);
+        //     }
+        // }
 
-    //         Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
-    //     }
+        // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
+    }
 
     pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
         if cx.is_window_active() {
@@ -3329,7 +3395,7 @@ impl Workspace {
             fs: project.read(cx).fs().clone(),
             build_window_options: |_, _, _| Default::default(),
             node_runtime: FakeNodeRuntime::new(),
-            call_factory: |_, _| Box::new(TestCallHandler),
+            call_factory: |_| Box::new(TestCallHandler),
         });
         let workspace = Self::new(0, project, app_state, cx);
         workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
@@ -3408,6 +3474,10 @@ impl Workspace {
         self.modal_layer
             .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
     }
+
+    pub fn call_state(&mut self) -> &mut dyn CallHandler {
+        &mut *self.call_handler
+    }
 }
 
 fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
@@ -3653,7 +3723,6 @@ impl Render for Workspace {
             .bg(cx.theme().colors().background)
             .children(self.titlebar_item.clone())
             .child(
-                // todo! should this be a component a view?
                 div()
                     .id("workspace")
                     .relative()
@@ -3703,7 +3772,8 @@ impl Render for Workspace {
                                     .overflow_hidden()
                                     .child(self.right_dock.clone()),
                             ),
-                    ),
+                    )
+                    .children(self.render_notifications(cx)),
             )
             .child(self.status_bar.clone())
     }
@@ -4421,50 +4491,54 @@ pub fn create_and_open_local_file(
 //     })
 // }
 
-// pub fn restart(_: &Restart, cx: &mut AppContext) {
-//     let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
-//     cx.spawn(|mut cx| async move {
-//         let mut workspace_windows = cx
-//             .windows()
-//             .into_iter()
-//             .filter_map(|window| window.downcast::<Workspace>())
-//             .collect::<Vec<_>>();
-
-//         // If multiple windows have unsaved changes, and need a save prompt,
-//         // prompt in the active window before switching to a different window.
-//         workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
+pub fn restart(_: &Restart, cx: &mut AppContext) {
+    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
+    let mut workspace_windows = cx
+        .windows()
+        .into_iter()
+        .filter_map(|window| window.downcast::<Workspace>())
+        .collect::<Vec<_>>();
+
+    // If multiple windows have unsaved changes, and need a save prompt,
+    // prompt in the active window before switching to a different window.
+    workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
+
+    let mut prompt = None;
+    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
+        prompt = window
+            .update(cx, |_, cx| {
+                cx.prompt(
+                    PromptLevel::Info,
+                    "Are you sure you want to restart?",
+                    &["Restart", "Cancel"],
+                )
+            })
+            .ok();
+    }
 
-//         if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
-//             let answer = window.prompt(
-//                 PromptLevel::Info,
-//                 "Are you sure you want to restart?",
-//                 &["Restart", "Cancel"],
-//                 &mut cx,
-//             );
+    cx.spawn(|mut cx| async move {
+        if let Some(mut prompt) = prompt {
+            let answer = prompt.await?;
+            if answer != 0 {
+                return Ok(());
+            }
+        }
 
-//             if let Some(mut answer) = answer {
-//                 let answer = answer.next().await;
-//                 if answer != Some(0) {
-//                     return Ok(());
-//                 }
-//             }
-//         }
+        // If the user cancels any save prompt, then keep the app open.
+        for window in workspace_windows {
+            if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
+                workspace.prepare_to_close(true, cx)
+            }) {
+                if !should_close.await? {
+                    return Ok(());
+                }
+            }
+        }
 
-//         // If the user cancels any save prompt, then keep the app open.
-//         for window in workspace_windows {
-//             if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| {
-//                 workspace.prepare_to_close(true, cx)
-//             }) {
-//                 if !should_close.await? {
-//                     return Ok(());
-//                 }
-//             }
-//         }
-//         cx.platform().restart();
-//         anyhow::Ok(())
-//     })
-//     .detach_and_log_err(cx);
-// }
+        cx.update(|cx| cx.restart())
+    })
+    .detach_and_log_err(cx);
+}
 
 fn parse_pixel_position_env_var(value: &str) -> Option<Point<GlobalPixels>> {
     let mut parts = value.split(',');

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.115.0"
+version = "0.116.0"
 publish = false
 
 [lib]
@@ -140,6 +140,7 @@ tree-sitter-lua.workspace = true
 tree-sitter-nix.workspace = true
 tree-sitter-nu.workspace = true
 tree-sitter-vue.workspace = true
+tree-sitter-uiua.workspace = true
 
 url = "2.2"
 urlencoding = "2.1.2"

crates/zed/src/languages.rs πŸ”—

@@ -17,6 +17,7 @@ mod json;
 #[cfg(feature = "plugin_runtime")]
 mod language_plugin;
 mod lua;
+mod nu;
 mod php;
 mod python;
 mod ruby;
@@ -24,6 +25,7 @@ mod rust;
 mod svelte;
 mod tailwind;
 mod typescript;
+mod uiua;
 mod vue;
 mod yaml;
 
@@ -210,12 +212,21 @@ pub fn init(
     language("elm", tree_sitter_elm::language(), vec![]);
     language("glsl", tree_sitter_glsl::language(), vec![]);
     language("nix", tree_sitter_nix::language(), vec![]);
-    language("nu", tree_sitter_nu::language(), vec![]);
+    language(
+        "nu",
+        tree_sitter_nu::language(),
+        vec![Arc::new(nu::NuLanguageServer {})],
+    );
     language(
         "vue",
         tree_sitter_vue::language(),
         vec![Arc::new(vue::VueLspAdapter::new(node_runtime))],
     );
+    language(
+        "uiua",
+        tree_sitter_uiua::language(),
+        vec![Arc::new(uiua::UiuaLanguageServer {})],
+    );
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/zed/src/languages/elixir/embedding.scm πŸ”—

@@ -18,10 +18,10 @@
                     target: (identifier) @name)
                     operator: "when")
             ])
-        (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
+        (#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
         )
 
     (call
         target: (identifier) @name
         (arguments (alias) @name)
-        (#match? @name "^(defmodule|defprotocol)$")) @item
+        (#any-match? @name "^(defmodule|defprotocol)$")) @item

crates/zed/src/languages/json.rs πŸ”—

@@ -105,6 +105,7 @@ impl LspAdapter for JsonLspAdapter {
 
     fn workspace_configuration(
         &self,
+        _workspace_root: &Path,
         cx: &mut AppContext,
     ) -> BoxFuture<'static, serde_json::Value> {
         let action_names = cx.all_action_names().collect::<Vec<_>>();

crates/zed/src/languages/nu.rs πŸ”—

@@ -0,0 +1,81 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{CodeLabel, Language, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf, sync::Arc};
+
+pub struct NuLanguageServer;
+
+#[async_trait]
+impl LspAdapter for NuLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("nu".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "nu"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!(
+            "nu v0.87.0 or greater must be installed and available in your $PATH"
+        ))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "nu".into(),
+            arguments: vec!["--lsp".into()],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+
+    async fn label_for_completion(
+        &self,
+        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(),
+        });
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        _: lsp::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/php.rs πŸ”—

@@ -29,7 +29,6 @@ pub struct IntelephenseLspAdapter {
 impl IntelephenseLspAdapter {
     const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js";
 
-    #[allow(unused)]
     pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         Self { node }
     }

crates/zed/src/languages/tailwind.rs πŸ”—

@@ -107,7 +107,11 @@ impl LspAdapter for TailwindLspAdapter {
         }))
     }
 
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        _workspace_root: &Path,
+        _: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         future::ready(json!({
             "tailwindCSS": {
                 "emmetCompletions": true,

crates/zed/src/languages/typescript.rs πŸ”—

@@ -205,7 +205,6 @@ pub struct EsLintLspAdapter {
 impl EsLintLspAdapter {
     const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
 
-    #[allow(unused)]
     pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         EsLintLspAdapter { node }
     }
@@ -213,13 +212,23 @@ impl EsLintLspAdapter {
 
 #[async_trait]
 impl LspAdapter for EsLintLspAdapter {
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        workspace_root: &Path,
+        _: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         future::ready(json!({
             "": {
                 "validate": "on",
                 "rulesCustomizations": [],
                 "run": "onType",
                 "nodePath": null,
+                "workingDirectory": {"mode": "auto"},
+                "workspaceFolder": {
+                    "uri": workspace_root,
+                    "name": workspace_root.file_name()
+                        .unwrap_or_else(|| workspace_root.as_os_str()),
+                },
             }
         }))
         .boxed()

crates/zed/src/languages/uiua.rs πŸ”—

@@ -0,0 +1,55 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf};
+
+pub struct UiuaLanguageServer;
+
+#[async_trait]
+impl LspAdapter for UiuaLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("uiua".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "uiua"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!(
+            "uiua must be installed and available in your $PATH"
+        ))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "uiua".into(),
+            arguments: vec!["lsp".into()],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+}

crates/zed/src/languages/uiua/config.toml πŸ”—

@@ -0,0 +1,10 @@
+name = "Uiua"
+path_suffixes = ["ua"]
+line_comment = "# "
+autoclose_before = ")]}\""
+brackets = [
+    { start = "{", end = "}", close = true, newline = false },
+    { start = "[", end = "]", close = true, newline = false },
+    { start = "(", end = ")", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]

crates/zed/src/languages/uiua/highlights.scm πŸ”—

@@ -0,0 +1,50 @@
+[
+  (openParen)
+  (closeParen)
+  (openCurly)
+  (closeCurly)
+  (openBracket)
+  (closeBracket)
+] @punctuation.bracket
+
+[
+  (branchSeparator)
+  (underscore)
+] @constructor
+; ] @punctuation.delimiter
+
+[ (character) ] @constant.character
+[ (comment) ] @comment
+[ (constant) ] @constant.numeric
+[ (identifier) ] @variable
+[ (leftArrow) ] @keyword
+[ (function) ] @function
+[ (modifier1) ] @operator
+[ (modifier2) ] @operator
+[ (number) ] @constant.numeric
+[ (placeHolder) ] @special
+[ (otherConstant) ] @string.special
+[ (signature) ] @type
+[ (system) ] @function.builtin
+[ (tripleMinus) ] @module
+
+; planet
+[
+  "id"
+  "identity"
+  "∘"
+  "dip"
+  "βŠ™"
+  "gap"
+  "β‹…"
+] @tag
+
+[
+  (string)
+  (multiLineString)
+] @string
+
+; [
+;   (deprecated)
+;   (identifierDeprecated)
+; ] @warning

crates/zed/src/languages/yaml.rs πŸ”—

@@ -93,7 +93,11 @@ impl LspAdapter for YamlLspAdapter {
     ) -> Option<LanguageServerBinary> {
         get_cached_server_binary(container_dir, &*self.node).await
     }
-    fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        _workspace_root: &Path,
+        cx: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         let tab_size = all_language_settings(None, cx)
             .language(Some("YAML"))
             .tab_size;

crates/zed2/Cargo.toml πŸ”—

@@ -16,12 +16,12 @@ path = "src/main.rs"
 
 [dependencies]
 ai = { package = "ai2", path = "../ai2"}
-# audio = { path = "../audio" }
-# activity_indicator = { path = "../activity_indicator" }
+audio = { package = "audio2", path = "../audio2" }
+activity_indicator = { package = "activity_indicator2", path = "../activity_indicator2"}
 auto_update = { package = "auto_update2", path = "../auto_update2" }
 breadcrumbs = { package = "breadcrumbs2", path = "../breadcrumbs2" }
 call = { package = "call2", path = "../call2" }
-# channel = { path = "../channel" }
+channel = { package = "channel2", path = "../channel2" }
 cli = { path = "../cli" }
 collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
 collections = { path = "../collections" }
@@ -66,12 +66,12 @@ shellexpand = "2.1.0"
 text = { package = "text2", path = "../text2" }
 terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
 theme = { package = "theme2", path = "../theme2" }
-# theme_selector = { path = "../theme_selector" }
+theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
 util = { path = "../util" }
 # semantic_index = { path = "../semantic_index" }
 # vim = { path = "../vim" }
 workspace = { package = "workspace2", path = "../workspace2" }
-# welcome = { path = "../welcome" }
+welcome = { package = "welcome2", path = "../welcome2" }
 zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
 anyhow.workspace = true
 async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
@@ -136,6 +136,7 @@ tree-sitter-lua.workspace = true
 tree-sitter-nix.workspace = true
 tree-sitter-nu.workspace = true
 tree-sitter-vue.workspace = true
+tree-sitter-uiua.workspace = true
 
 url = "2.2"
 urlencoding = "2.1.2"

crates/zed2/src/languages.rs πŸ”—

@@ -18,6 +18,7 @@ mod json;
 #[cfg(feature = "plugin_runtime")]
 mod language_plugin;
 mod lua;
+mod nu;
 mod php;
 mod python;
 mod ruby;
@@ -25,6 +26,7 @@ mod rust;
 mod svelte;
 mod tailwind;
 mod typescript;
+mod uiua;
 mod vue;
 mod yaml;
 
@@ -211,12 +213,21 @@ pub fn init(
     language("elm", tree_sitter_elm::language(), vec![]);
     language("glsl", tree_sitter_glsl::language(), vec![]);
     language("nix", tree_sitter_nix::language(), vec![]);
-    language("nu", tree_sitter_nu::language(), vec![]);
+    language(
+        "nu",
+        tree_sitter_nu::language(),
+        vec![Arc::new(nu::NuLanguageServer {})],
+    );
     language(
         "vue",
         tree_sitter_vue::language(),
         vec![Arc::new(vue::VueLspAdapter::new(node_runtime))],
     );
+    language(
+        "uiua",
+        tree_sitter_uiua::language(),
+        vec![Arc::new(uiua::UiuaLanguageServer {})],
+    );
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/zed2/src/languages/json.rs πŸ”—

@@ -105,6 +105,7 @@ impl LspAdapter for JsonLspAdapter {
 
     fn workspace_configuration(
         &self,
+        _workspace_root: &Path,
         cx: &mut AppContext,
     ) -> BoxFuture<'static, serde_json::Value> {
         let action_names = cx.all_action_names();

crates/zed2/src/languages/nu.rs πŸ”—

@@ -0,0 +1,55 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf};
+
+pub struct NuLanguageServer;
+
+#[async_trait]
+impl LspAdapter for NuLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("nu".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "nu"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!(
+            "nu v0.87.0 or greater must be installed and available in your $PATH"
+        ))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "nu".into(),
+            arguments: vec!["--lsp".into()],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+}

crates/zed2/src/languages/php.rs πŸ”—

@@ -29,7 +29,6 @@ pub struct IntelephenseLspAdapter {
 impl IntelephenseLspAdapter {
     const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js";
 
-    #[allow(unused)]
     pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         Self { node }
     }

crates/zed2/src/languages/tailwind.rs πŸ”—

@@ -107,7 +107,11 @@ impl LspAdapter for TailwindLspAdapter {
         }))
     }
 
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        _workspace_root: &Path,
+        _: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         future::ready(json!({
             "tailwindCSS": {
                 "emmetCompletions": true,

crates/zed2/src/languages/typescript.rs πŸ”—

@@ -205,7 +205,6 @@ pub struct EsLintLspAdapter {
 impl EsLintLspAdapter {
     const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
 
-    #[allow(unused)]
     pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
         EsLintLspAdapter { node }
     }
@@ -213,13 +212,23 @@ impl EsLintLspAdapter {
 
 #[async_trait]
 impl LspAdapter for EsLintLspAdapter {
-    fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        workspace_root: &Path,
+        _: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         future::ready(json!({
             "": {
                 "validate": "on",
                 "rulesCustomizations": [],
                 "run": "onType",
                 "nodePath": null,
+                "workingDirectory": {"mode": "auto"},
+                "workspaceFolder": {
+                    "uri": workspace_root,
+                    "name": workspace_root.file_name()
+                        .unwrap_or_else(|| workspace_root.as_os_str()),
+                },
             }
         }))
         .boxed()

crates/zed2/src/languages/uiua.rs πŸ”—

@@ -0,0 +1,55 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf};
+
+pub struct UiuaLanguageServer;
+
+#[async_trait]
+impl LspAdapter for UiuaLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("uiua".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "uiua"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!(
+            "uiua must be installed and available in your $PATH"
+        ))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "uiua".into(),
+            arguments: vec!["lsp".into()],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+}

crates/zed2/src/languages/uiua/config.toml πŸ”—

@@ -0,0 +1,10 @@
+name = "Uiua"
+path_suffixes = ["ua"]
+line_comment = "# "
+autoclose_before = ")]}\""
+brackets = [
+    { start = "{", end = "}", close = true, newline = false},
+    { start = "[", end = "]", close = true, newline = false },
+    { start = "(", end = ")", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]

crates/zed2/src/languages/uiua/highlights.scm πŸ”—

@@ -0,0 +1,50 @@
+[
+  (openParen)
+  (closeParen)
+  (openCurly)
+  (closeCurly)
+  (openBracket)
+  (closeBracket)
+] @punctuation.bracket
+
+[
+  (branchSeparator)
+  (underscore)
+] @constructor
+; ] @punctuation.delimiter
+
+[ (character) ] @constant.character
+[ (comment) ] @comment
+[ (constant) ] @constant.numeric
+[ (identifier) ] @variable
+[ (leftArrow) ] @keyword
+[ (function) ] @function
+[ (modifier1) ] @operator
+[ (modifier2) ] @operator
+[ (number) ] @constant.numeric
+[ (placeHolder) ] @special
+[ (otherConstant) ] @string.special
+[ (signature) ] @type
+[ (system) ] @function.builtin
+[ (tripleMinus) ] @module
+
+; planet
+[
+  "id"
+  "identity"
+  "∘"
+  "dip"
+  "βŠ™"
+  "gap"
+  "β‹…"
+] @tag
+
+[
+  (string)
+  (multiLineString)
+] @string
+
+; [
+;   (deprecated)
+;   (identifierDeprecated)
+; ] @warning

crates/zed2/src/languages/yaml.rs πŸ”—

@@ -93,7 +93,11 @@ impl LspAdapter for YamlLspAdapter {
     ) -> Option<LanguageServerBinary> {
         get_cached_server_binary(container_dir, &*self.node).await
     }
-    fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
+    fn workspace_configuration(
+        &self,
+        _workspace_root: &Path,
+        cx: &mut AppContext,
+    ) -> BoxFuture<'static, Value> {
         let tab_size = all_language_settings(None, cx)
             .language(Some("YAML"))
             .tab_size;

crates/zed2/src/main.rs πŸ”—

@@ -8,12 +8,12 @@ use anyhow::{anyhow, Context as _, Result};
 use backtrace::Backtrace;
 use chrono::Utc;
 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
-use client::UserStore;
+use client::{Client, UserStore};
 use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
 use fs::RealFs;
 use futures::StreamExt;
-use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
+use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
 use isahc::{prelude::Configurable, Request};
 use language::LanguageRegistry;
 use log::LevelFilter;
@@ -36,7 +36,7 @@ use std::{
     path::{Path, PathBuf},
     sync::{
         atomic::{AtomicU32, Ordering},
-        Arc,
+        Arc, Weak,
     },
     thread,
 };
@@ -48,6 +48,7 @@ use util::{
     paths, ResultExt,
 };
 use uuid::Uuid;
+use welcome::{show_welcome_experience, FIRST_OPEN};
 use workspace::{AppState, WorkspaceStore};
 use zed2::{
     build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
@@ -103,16 +104,15 @@ fn main() {
     let listener = Arc::new(listener);
     let open_listener = listener.clone();
     app.on_open_urls(move |urls, _| open_listener.open_urls(&urls));
-    app.on_reopen(move |_cx| {
-        // todo!("workspace")
-        // if cx.has_global::<Weak<AppState>>() {
-        // if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
-        // workspace::open_new(&app_state, cx, |workspace, cx| {
-        //     Editor::new_file(workspace, &Default::default(), cx)
-        // })
-        // .detach();
-        // }
-        // }
+    app.on_reopen(move |cx| {
+        if cx.has_global::<Weak<AppState>>() {
+            if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
+                workspace::open_new(&app_state, cx, |workspace, cx| {
+                    Editor::new_file(workspace, &Default::default(), cx)
+                })
+                .detach();
+            }
+        }
     });
 
     app.run(move |cx| {
@@ -164,17 +164,16 @@ fn main() {
         // assistant::init(cx);
         // component_test::init(cx);
 
-        // cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
         // cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
         //     .detach();
-        // watch_file_types(fs.clone(), cx);
+        watch_file_types(fs.clone(), cx);
 
         languages.set_theme(cx.theme().clone());
-        // cx.observe_global::<SettingsStore, _>({
-        //     let languages = languages.clone();
-        //     move |cx| languages.set_theme(theme::current(cx).clone())
-        // })
-        // .detach();
+        cx.observe_global::<SettingsStore>({
+            let languages = languages.clone();
+            move |cx| languages.set_theme(cx.theme().clone())
+        })
+        .detach();
 
         client.telemetry().start(installation_id, session_id, cx);
         let telemetry_settings = *client::TelemetrySettings::get_global(cx);
@@ -189,17 +188,16 @@ fn main() {
         let app_state = Arc::new(AppState {
             languages,
             client: client.clone(),
-            user_store,
+            user_store: user_store.clone(),
             fs,
             build_window_options,
             call_factory: call::Call::new,
-            // background_actions: todo!("ask Mikayla"),
             workspace_store,
             node_runtime,
         });
         cx.set_global(Arc::downgrade(&app_state));
 
-        // audio::init(Assets, cx);
+        audio::init(Assets, cx);
         auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
 
         workspace::init(app_state.clone(), cx);
@@ -210,7 +208,7 @@ fn main() {
         // outline::init(cx);
         // project_symbols::init(cx);
         project_panel::init(Assets, cx);
-        // channel::init(&client, user_store.clone(), cx);
+        channel::init(&client, user_store.clone(), cx);
         // diagnostics::init(cx);
         search::init(cx);
         // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
@@ -219,14 +217,13 @@ fn main() {
 
         // journal2::init(app_state.clone(), cx);
         // language_selector::init(cx);
-        // theme_selector::init(cx);
+        theme_selector::init(cx);
         // activity_indicator::init(cx);
         // language_tools::init(cx);
         call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
         collab_ui::init(&app_state, cx);
         // feedback::init(cx);
-        // welcome::init(cx);
-        // zed::init(&app_state, cx);
+        welcome::init(cx);
 
         // cx.set_menus(menus::menus());
         initialize_workspace(app_state.clone(), cx);
@@ -249,7 +246,7 @@ fn main() {
             }
         }
 
-        let mut _triggered_authentication = false;
+        let mut triggered_authentication = false;
 
         fn open_paths_and_log_errs(
             paths: &[PathBuf],
@@ -279,17 +276,18 @@ fn main() {
                     .detach();
             }
             Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => {
-                todo!()
-                // triggered_authentication = true;
-                // let app_state = app_state.clone();
-                // let client = client.clone();
-                // cx.spawn(|mut cx| async move {
-                //     // ignore errors here, we'll show a generic "not signed in"
-                //     let _ = authenticate(client, &cx).await;
-                //     cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))
-                //         .await
-                // })
-                // .detach_and_log_err(cx)
+                triggered_authentication = true;
+                let app_state = app_state.clone();
+                let client = client.clone();
+                cx.spawn(|mut cx| async move {
+                    // ignore errors here, we'll show a generic "not signed in"
+                    let _ = authenticate(client, &cx).await;
+                    //todo!()
+                    // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))
+                    // .await
+                    anyhow::Ok(())
+                })
+                .detach_and_log_err(cx)
             }
             Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => {
                 todo!()
@@ -328,23 +326,23 @@ fn main() {
         })
         .detach();
 
-        // if !triggered_authentication {
-        //     cx.spawn(|cx| async move { authenticate(client, &cx).await })
-        //         .detach_and_log_err(cx);
-        // }
+        if !triggered_authentication {
+            cx.spawn(|cx| async move { authenticate(client, &cx).await })
+                .detach_and_log_err(cx);
+        }
     });
 }
 
-// async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
-//     if stdout_is_a_pty() {
-//         if client::IMPERSONATE_LOGIN.is_some() {
-//             client.authenticate_and_connect(false, &cx).await?;
-//         }
-//     } else if client.has_keychain_credentials(&cx) {
-//         client.authenticate_and_connect(true, &cx).await?;
-//     }
-//     Ok::<_, anyhow::Error>(())
-// }
+async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
+    if stdout_is_a_pty() {
+        if client::IMPERSONATE_LOGIN.is_some() {
+            client.authenticate_and_connect(false, &cx).await?;
+        }
+    } else if client.has_keychain_credentials(&cx) {
+        client.authenticate_and_connect(true, &cx).await?;
+    }
+    Ok::<_, anyhow::Error>(())
+}
 
 async fn installation_id() -> Result<(String, bool)> {
     let legacy_key_name = "device_id";
@@ -368,10 +366,9 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp
             cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
                 .await
                 .log_err();
-            // todo!(welcome)
-            //} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
-            //todo!()
-            // cx.update(|cx| show_welcome_experience(app_state, cx));
+        } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
+            cx.update(|cx| show_welcome_experience(app_state, cx))
+                .log_err();
         } else {
             cx.update(|cx| {
                 workspace::open_new(app_state, cx, |workspace, cx| {
@@ -709,84 +706,49 @@ fn load_embedded_fonts(cx: &AppContext) {
         .unwrap();
 }
 
-// #[cfg(debug_assertions)]
-// async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> {
-//     let mut events = fs
-//         .watch("styles/src".as_ref(), Duration::from_millis(100))
-//         .await;
-//     while (events.next().await).is_some() {
-//         let output = Command::new("npm")
-//             .current_dir("styles")
-//             .args(["run", "build"])
-//             .output()
-//             .await
-//             .log_err()?;
-//         if output.status.success() {
-//             cx.update(|cx| theme_selector::reload(cx))
-//         } else {
-//             eprintln!(
-//                 "build script failed {}",
-//                 String::from_utf8_lossy(&output.stderr)
-//             );
-//         }
-//     }
-//     Some(())
-// }
-
-// #[cfg(debug_assertions)]
-// async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
-//     let mut events = fs
-//         .watch(
-//             "crates/zed/src/languages".as_ref(),
-//             Duration::from_millis(100),
-//         )
-//         .await;
-//     while (events.next().await).is_some() {
-//         languages.reload();
-//     }
-//     Some(())
-// }
-
-// #[cfg(debug_assertions)]
-// fn watch_file_types(fs: Arc<dyn Fs>, cx: &mut AppContext) {
-//     cx.spawn(|mut cx| async move {
-//         let mut events = fs
-//             .watch(
-//                 "assets/icons/file_icons/file_types.json".as_ref(),
-//                 Duration::from_millis(100),
-//             )
-//             .await;
-//         while (events.next().await).is_some() {
-//             cx.update(|cx| {
-//                 cx.update_global(|file_types, _| {
-//                     *file_types = project_panel::file_associations::FileAssociations::new(Assets);
-//                 });
-//             })
-//         }
-//     })
-//     .detach()
-// }
-
-// #[cfg(not(debug_assertions))]
-// async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
-//     None
-// }
-
-// #[cfg(not(debug_assertions))]
-// async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
-//     None
-//
-
-// #[cfg(not(debug_assertions))]
-// fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
-
-pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
-    // &[
-    //     ("Go to file", &file_finder::Toggle),
-    //     ("Open command palette", &command_palette::Toggle),
-    //     ("Open recent projects", &recent_projects::OpenRecent),
-    //     ("Change your settings", &zed_actions::OpenSettings),
-    // ]
-    // todo!()
-    &[]
+#[cfg(debug_assertions)]
+async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
+    use std::time::Duration;
+
+    let mut events = fs
+        .watch(
+            "crates/zed2/src/languages".as_ref(),
+            Duration::from_millis(100),
+        )
+        .await;
+    while (events.next().await).is_some() {
+        languages.reload();
+    }
+    Some(())
 }
+
+#[cfg(debug_assertions)]
+fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
+    use std::time::Duration;
+
+    cx.spawn(|mut cx| async move {
+        let mut events = fs
+            .watch(
+                "assets/icons/file_icons/file_types.json".as_ref(),
+                Duration::from_millis(100),
+            )
+            .await;
+        while (events.next().await).is_some() {
+            cx.update(|cx| {
+                cx.update_global(|file_types, _| {
+                    *file_types = project_panel::file_associations::FileAssociations::new(Assets);
+                });
+            })
+            .ok();
+        }
+    })
+    .detach()
+}
+
+#[cfg(not(debug_assertions))]
+async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
+    None
+}
+
+#[cfg(not(debug_assertions))]
+fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}

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

@@ -140,11 +140,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
         //         cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
         let diagnostic_summary =
             cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
-        //     let activity_indicator = activity_indicator::ActivityIndicator::new(
-        //         workspace,
-        //         app_state.languages.clone(),
-        //         cx,
-        //     );
+        let activity_indicator =
+            activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
         //     let active_buffer_language =
         //         cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
         //     let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
@@ -154,7 +151,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
         //     let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
         workspace.status_bar().update(cx, |status_bar, cx| {
             status_bar.add_left_item(diagnostic_summary, cx);
-            // status_bar.add_left_item(activity_indicator, cx);
+            status_bar.add_left_item(activity_indicator, cx);
 
             // status_bar.add_right_item(feedback_button, cx);
             // status_bar.add_right_item(copilot, cx);
@@ -167,12 +164,17 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
 
         //     vim::observe_keystrokes(cx);
 
-        //     cx.on_window_should_close(|workspace, cx| {
-        //         if let Some(task) = workspace.close(&Default::default(), cx) {
-        //             task.detach_and_log_err(cx);
-        //         }
-        //         false
-        //     });
+        let handle = cx.view().downgrade();
+        cx.on_window_should_close(move |cx| {
+            handle
+                .update(cx, |workspace, cx| {
+                    if let Some(task) = workspace.close(&Default::default(), cx) {
+                        task.detach_and_log_err(cx);
+                    }
+                    false
+                })
+                .unwrap_or(true)
+        });
 
         cx.spawn(|workspace_handle, mut cx| async move {
             let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());

script/crate-dep-graph πŸ”—

@@ -11,7 +11,7 @@ graph_file=target/crate-graph.html
 cargo depgraph \
     --workspace-only \
     --offline \
-    --root=zed,cli,collab \
+    --root=zed2,cli,collab2 \
     --dedup-transitive-deps \
     | dot -Tsvg > $graph_file