Merge branch 'main' into v0.173.x

Joseph T. Lyons created

Change summary

.github/workflows/docs.yml                                      |   6 
.gitignore                                                      |  47 
Cargo.lock                                                      |  80 
Cargo.toml                                                      |  10 
assets/icons/file_icons/file_types.json                         |  11 
assets/icons/lock_outlined.svg                                  |   6 
assets/keymaps/default-linux.json                               |   9 
assets/keymaps/default-macos.json                               |  14 
assets/keymaps/vim.json                                         |   6 
assets/themes/one/one.json                                      |   4 
crates/assistant/src/assistant_panel.rs                         |   6 
crates/assistant/src/inline_assistant.rs                        |  32 
crates/assistant/src/terminal_inline_assistant.rs               |  32 
crates/assistant2/src/assistant_model_selector.rs               |  18 
crates/assistant2/src/assistant_panel.rs                        |  12 
crates/assistant2/src/context_strip.rs                          |  28 
crates/assistant_context_editor/src/context_editor.rs           |  12 
crates/assistant_context_editor/src/slash_command_picker.rs     |  25 
crates/component/Cargo.toml                                     |  23 
crates/component/LICENSE-GPL                                    |   0 
crates/component/src/component.rs                               | 305 +
crates/component_preview/Cargo.toml                             |  16 
crates/component_preview/LICENSE-GPL                            |   1 
crates/component_preview/src/component_preview.rs               | 178 +
crates/copilot/src/copilot_completion_provider.rs               |   1 
crates/editor/src/editor.rs                                     |  48 
crates/editor/src/editor_tests.rs                               | 170 -
crates/editor/src/element.rs                                    |   5 
crates/editor/src/hunk_diff.rs                                  |  15 
crates/editor/src/inline_completion_tests.rs                    |   1 
crates/git_ui/Cargo.toml                                        |   2 
crates/git_ui/src/branch_picker.rs                              | 123 
crates/git_ui/src/git_panel.rs                                  | 324 +
crates/git_ui/src/git_ui.rs                                     |   4 
crates/git_ui/src/quick_commit.rs                               | 307 +
crates/git_ui/src/repository_selector.rs                        |  28 
crates/gpui/examples/data_table.rs                              | 479 ++
crates/gpui/src/arena.rs                                        |   1 
crates/inline_completion/src/inline_completion.rs               |   6 
crates/inline_completion_button/src/inline_completion_button.rs |  77 
crates/language_model_selector/src/language_model_selector.rs   |  29 
crates/language_tools/src/syntax_tree_view.rs                   |   2 
crates/multi_buffer/src/multi_buffer.rs                         |   1 
crates/outline_panel/src/outline_panel.rs                       | 483 ++
crates/panel/Cargo.toml                                         |   3 
crates/panel/src/panel.rs                                       |  65 
crates/paths/src/paths.rs                                       |  12 
crates/project/src/git.rs                                       |  28 
crates/project/src/project.rs                                   |  12 
crates/project_panel/src/project_panel.rs                       |  86 
crates/repl/src/components/kernel_options.rs                    |  25 
crates/settings/src/keymap_file.rs                              |  18 
crates/settings/src/settings_store.rs                           |  27 
crates/storybook/src/story_selector.rs                          |   2 
crates/supermaven/src/supermaven_completion_provider.rs         |   1 
crates/terminal_view/src/terminal_panel.rs                      |  13 
crates/title_bar/src/application_menu.rs                        |   8 
crates/title_bar/src/title_bar.rs                               |  25 
crates/ui/Cargo.toml                                            |   6 
crates/ui/src/components.rs                                     |   2 
crates/ui/src/components/avatar/avatar.rs                       |  61 
crates/ui/src/components/button/button.rs                       | 229 
crates/ui/src/components/button/button_like.rs                  |   4 
crates/ui/src/components/button/icon_button.rs                  |   9 
crates/ui/src/components/content_group.rs                       |  27 
crates/ui/src/components/context_menu.rs                        |  12 
crates/ui/src/components/facepile.rs                            | 112 
crates/ui/src/components/icon.rs                                |  66 
crates/ui/src/components/icon/decorated_icon.rs                 |  59 
crates/ui/src/components/icon/icon_decoration.rs                |  22 
crates/ui/src/components/indicator.rs                           |  31 
crates/ui/src/components/keybinding_hint.rs                     | 187 
crates/ui/src/components/label/highlighted_label.rs             |   5 
crates/ui/src/components/label/label.rs                         |  59 
crates/ui/src/components/label/label_like.rs                    |  10 
crates/ui/src/components/popover_menu.rs                        |  26 
crates/ui/src/components/radio.rs                               |   3 
crates/ui/src/components/stories.rs                             |   2 
crates/ui/src/components/stories/tool_strip.rs                  |  33 
crates/ui/src/components/tab.rs                                 |  47 
crates/ui/src/components/table.rs                               | 186 
crates/ui/src/components/toggle.rs                              | 502 +-
crates/ui/src/components/tool_strip.rs                          |  58 
crates/ui/src/components/tooltip.rs                             |  31 
crates/ui/src/prelude.rs                                        |   4 
crates/ui/src/styles/color.rs                                   |   6 
crates/ui/src/styles/typography.rs                              |  47 
crates/ui/src/traits.rs                                         |   1 
crates/ui/src/traits/component_preview.rs                       | 205 -
crates/ui_macros/Cargo.toml                                     |   3 
crates/ui_macros/src/derive_component.rs                        |  97 
crates/ui_macros/src/ui_macros.rs                               |  25 
crates/util/src/paths.rs                                        |   2 
crates/vim/src/object.rs                                        | 174 
crates/vim/test_data/test_anybrackets_trailing_space.json       |  11 
crates/workspace/Cargo.toml                                     |   1 
crates/workspace/src/notifications.rs                           |  16 
crates/workspace/src/pane.rs                                    |  13 
crates/workspace/src/theme_preview.rs                           |  27 
crates/workspace/src/workspace.rs                               |  12 
crates/worktree/Cargo.toml                                      |   6 
crates/worktree/src/worktree.rs                                 | 324 +
crates/worktree/src/worktree_tests.rs                           |  51 
crates/zed/Cargo.toml                                           |   4 
crates/zed/src/main.rs                                          |   2 
crates/zed/src/zed.rs                                           | 105 
crates/zed/src/zed/quick_action_bar.rs                          | 296 
crates/zed/src/zed/quick_action_bar/repl_menu.rs                |   8 
crates/zed_actions/src/lib.rs                                   |   6 
crates/zeta/src/input_excerpt.rs                                | 238 +
crates/zeta/src/zeta.rs                                         | 313 -
docs/src/extensions/icon-themes.md                              |   2 
docs/src/languages/elm.md                                       |   2 
docs/theme/css/general.css                                      |   6 
docs/theme/css/variables.css                                    |   7 
extensions/emmet/.gitignore                                     |   2 
extensions/php/languages/php/brackets.scm                       |   3 
117 files changed, 4,610 insertions(+), 2,510 deletions(-)

Detailed changes

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

@@ -24,11 +24,13 @@ jobs:
       - name: Prettier Check on /docs
         working-directory: ./docs
         run: |
-          pnpm dlx prettier . --check || {
+          pnpm dlx prettier@${PRETTIER_VERSION} . --check || {
             echo "To fix, run from the root of the zed repo:"
-            echo "  cd docs && pnpm dlx prettier . --write && cd .."
+            echo "  cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .."
             false
           }
+        env:
+          PRETTIER_VERSION: 3.5.0
 
       - name: Check for Typos with Typos-CLI
         uses: crate-ci/typos@8e6a4285bcbde632c5d79900a7779746e8b7ea3f # v1.24.6

.gitignore πŸ”—

@@ -1,36 +1,35 @@
-/.direnv
-.envrc
-.idea
-**/target
+**/*.db
 **/cargo-target
-/zed.xcworkspace
-.DS_Store
-/plugins/bin
-/script/node_modules
-/crates/theme/schemas/theme.json
-/crates/collab/seed.json
-/crates/zed/resources/flatpak/flatpak-cargo-sources.json
-/dev.zed.Zed*.json
-/assets/*licenses.*
+**/target
 **/venv
-.build
 *.wasm
-Packages
 *.xcodeproj
-xcuserdata/
-DerivedData/
-.swiftpm/config/registries.json
-.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.DS_Store
+.blob_store
+.build
+.envrc
+.flatpak-builder
+.idea
 .netrc
-.swiftpm
-**/*.db
 .pytest_cache
+.swiftpm
+.swiftpm/config/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
 .venv
-.blob_store
 .vscode
 .wrangler
-.flatpak-builder
-.envrc
+/.direnv
+/assets/*licenses.*
+/crates/collab/seed.json
+/crates/theme/schemas/theme.json
+/crates/zed/resources/flatpak/flatpak-cargo-sources.json
+/dev.zed.Zed*.json
+/plugins/bin
+/script/node_modules
+/zed.xcworkspace
+DerivedData/
+Packages
+xcuserdata/
 
 # Don't commit any secrets to the repo.
 .env.secret.toml

Cargo.lock πŸ”—

@@ -2942,6 +2942,28 @@ dependencies = [
  "gpui",
 ]
 
+[[package]]
+name = "component"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "gpui",
+ "linkme",
+ "once_cell",
+ "parking_lot",
+ "theme",
+]
+
+[[package]]
+name = "component_preview"
+version = "0.1.0"
+dependencies = [
+ "component",
+ "gpui",
+ "ui",
+ "workspace",
+]
+
 [[package]]
 name = "concurrent-queue"
 version = "2.5.0"
@@ -5330,6 +5352,7 @@ dependencies = [
  "editor",
  "feature_flags",
  "futures 0.3.31",
+ "fuzzy",
  "git",
  "gpui",
  "language",
@@ -5349,6 +5372,7 @@ dependencies = [
  "util",
  "windows 0.58.0",
  "workspace",
+ "zed_actions",
 ]
 
 [[package]]
@@ -7184,7 +7208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
 dependencies = [
  "cfg-if",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -7278,6 +7302,26 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "linkme"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "566336154b9e58a4f055f6dd4cbab62c7dc0826ce3c0a04e63b2d2ecd784cdae"
+dependencies = [
+ "linkme-impl",
+]
+
+[[package]]
+name = "linkme-impl"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edbe595006d355eaf9ae11db92707d4338cd2384d16866131cc1afdbdd35d8d9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.90",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.14"
@@ -8691,9 +8735,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.20.2"
+version = "1.20.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
 
 [[package]]
 name = "oo7"
@@ -9000,7 +9044,10 @@ dependencies = [
 name = "panel"
 version = "0.1.0"
 dependencies = [
+ "editor",
  "gpui",
+ "settings",
+ "theme",
  "ui",
  "workspace",
 ]
@@ -14318,8 +14365,10 @@ name = "ui"
 version = "0.1.0"
 dependencies = [
  "chrono",
+ "component",
  "gpui",
  "itertools 0.14.0",
+ "linkme",
  "menu",
  "serde",
  "settings",
@@ -14347,6 +14396,7 @@ name = "ui_macros"
 version = "0.1.0"
 dependencies = [
  "convert_case 0.7.1",
+ "linkme",
  "proc-macro2",
  "quote",
  "syn 1.0.109",
@@ -14616,22 +14666,6 @@ version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
-[[package]]
-name = "vcs_menu"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "fuzzy",
- "git",
- "gpui",
- "picker",
- "project",
- "ui",
- "util",
- "workspace",
- "zed_actions",
-]
-
 [[package]]
 name = "version-compare"
 version = "0.2.0"
@@ -16134,6 +16168,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "component",
  "db",
  "derive_more",
  "env_logger 0.11.6",
@@ -16568,6 +16603,7 @@ dependencies = [
  "collections",
  "command_palette",
  "command_palette_hooks",
+ "component_preview",
  "copilot",
  "db",
  "diagnostics",
@@ -16657,7 +16693,6 @@ dependencies = [
  "urlencoding",
  "util",
  "uuid",
- "vcs_menu",
  "vim",
  "vim_mode_setting",
  "welcome",
@@ -16766,12 +16801,13 @@ dependencies = [
 
 [[package]]
 name = "zed_llm_client"
-version = "0.2.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ea4d8ead1e1158e5ebdd6735df25973781da70de5c8008e3a13595865ca4f31"
+checksum = "614669bead4741b2fc352ae1967318be16949cf46f59013e548c6dbfdfc01252"
 dependencies = [
  "serde",
  "serde_json",
+ "uuid",
 ]
 
 [[package]]

Cargo.toml πŸ”—

@@ -26,6 +26,8 @@ members = [
     "crates/collections",
     "crates/command_palette",
     "crates/command_palette_hooks",
+    "crates/component",
+    "crates/component_preview",
     "crates/context_server",
     "crates/context_server_settings",
     "crates/copilot",
@@ -147,7 +149,6 @@ members = [
     "crates/ui_macros",
     "crates/util",
     "crates/util_macros",
-    "crates/vcs_menu",
     "crates/vim",
     "crates/vim_mode_setting",
     "crates/welcome",
@@ -227,6 +228,8 @@ collab_ui = { path = "crates/collab_ui" }
 collections = { path = "crates/collections" }
 command_palette = { path = "crates/command_palette" }
 command_palette_hooks = { path = "crates/command_palette_hooks" }
+component = { path = "crates/component" }
+component_preview = { path = "crates/component_preview" }
 context_server = { path = "crates/context_server" }
 context_server_settings = { path = "crates/context_server_settings" }
 copilot = { path = "crates/copilot" }
@@ -346,7 +349,6 @@ ui_input = { path = "crates/ui_input" }
 ui_macros = { path = "crates/ui_macros" }
 util = { path = "crates/util" }
 util_macros = { path = "crates/util_macros" }
-vcs_menu = { path = "crates/vcs_menu" }
 vim = { path = "crates/vim" }
 vim_mode_setting = { path = "crates/vim_mode_setting" }
 welcome = { path = "crates/welcome" }
@@ -428,6 +430,7 @@ jupyter-websocket-client = { version = "0.9.0" }
 libc = "0.2"
 libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
 linkify = "0.10.0"
+linkme = "0.3.31"
 livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [
     "dispatcher",
     "services-dispatcher",
@@ -561,7 +564,7 @@ wasmtime = { version = "24", default-features = false, features = [
 wasmtime-wasi = "24"
 which = "6.0.0"
 wit-component = "0.201"
-zed_llm_client = "0.2"
+zed_llm_client = "0.4"
 zstd = "0.11"
 metal = "0.31"
 
@@ -676,7 +679,6 @@ telemetry_events = { codegen-units = 1 }
 theme_selector = { codegen-units = 1 }
 time_format = { codegen-units = 1 }
 ui_input = { codegen-units = 1 }
-vcs_menu = { codegen-units = 1 }
 zed_actions = { codegen-units = 1 }
 
 [profile.release]

assets/icons/file_icons/file_types.json πŸ”—

@@ -150,8 +150,19 @@
     "postcss": "css",
     "ppt": "document",
     "pptx": "document",
+    "prettier.config.cjs": "prettier",
+    "prettier.config.js": "prettier",
+    "prettier.config.mjs": "prettier",
     "prettierignore": "prettier",
     "prettierrc": "prettier",
+    "prettierrc.cjs": "prettier",
+    "prettierrc.js": "prettier",
+    "prettierrc.json": "prettier",
+    "prettierrc.json5": "prettier",
+    "prettierrc.mjs": "prettier",
+    "prettierrc.toml": "prettier",
+    "prettierrc.yaml": "prettier",
+    "prettierrc.yml": "prettier",
     "prisma": "prisma",
     "profile": "terminal",
     "ps1": "terminal",

assets/icons/lock_outlined.svg πŸ”—

@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.5"/>
+<path d="M8 9V11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<circle cx="8" cy="9" r="1" fill="black"/>
+<rect x="3.75" y="5.75" width="8.5" height="7.5" rx="1.25" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
+</svg>

assets/keymaps/default-linux.json πŸ”—

@@ -122,7 +122,8 @@
       "ctrl-i": "editor::ShowSignatureHelp",
       "alt-g b": "editor::ToggleGitBlame",
       "menu": "editor::OpenContextMenu",
-      "shift-f10": "editor::OpenContextMenu"
+      "shift-f10": "editor::OpenContextMenu",
+      "ctrl-shift-e": "editor::ToggleEditPrediction"
     }
   },
   {
@@ -535,8 +536,7 @@
   {
     "bindings": {
       "ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
-      "ctrl-alt-i": "zed::DebugElements",
-      "ctrl-:": "editor::ToggleInlayHints"
+      "ctrl-alt-i": "zed::DebugElements"
     }
   },
   {
@@ -554,7 +554,8 @@
       "ctrl-shift-e": "pane::RevealInProjectPanel",
       "ctrl-f8": "editor::GoToHunk",
       "ctrl-shift-f8": "editor::GoToPrevHunk",
-      "ctrl-enter": "assistant::InlineAssist"
+      "ctrl-enter": "assistant::InlineAssist",
+      "ctrl-:": "editor::ToggleInlayHints"
     }
   },
   {

assets/keymaps/default-macos.json πŸ”—

@@ -39,8 +39,8 @@
       "cmd-m": "zed::Minimize",
       "fn-f": "zed::ToggleFullScreen",
       "ctrl-cmd-f": "zed::ToggleFullScreen",
-      "ctrl-shift-z": "zeta::RateCompletions",
-      "ctrl-shift-i": "edit_prediction::ToggleMenu"
+      "ctrl-cmd-z": "zeta::RateCompletions",
+      "ctrl-cmd-i": "edit_prediction::ToggleMenu"
     }
   },
   {
@@ -132,7 +132,8 @@
       "cmd-alt-g b": "editor::ToggleGitBlame",
       "cmd-i": "editor::ShowSignatureHelp",
       "ctrl-f12": "editor::GoToDeclaration",
-      "alt-ctrl-f12": "editor::GoToDeclarationSplit"
+      "alt-ctrl-f12": "editor::GoToDeclarationSplit",
+      "ctrl-cmd-e": "editor::ToggleEditPrediction"
     }
   },
   {
@@ -619,8 +620,7 @@
       "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
       // TODO: Move this to a dock open action
       "cmd-shift-c": "collab_panel::ToggleFocus",
-      "cmd-alt-i": "zed::DebugElements",
-      "ctrl-:": "editor::ToggleInlayHints"
+      "cmd-alt-i": "zed::DebugElements"
     }
   },
   {
@@ -633,7 +633,8 @@
       "cmd-shift-e": "pane::RevealInProjectPanel",
       "cmd-f8": "editor::GoToHunk",
       "cmd-shift-f8": "editor::GoToPrevHunk",
-      "ctrl-enter": "assistant::InlineAssist"
+      "ctrl-enter": "assistant::InlineAssist",
+      "ctrl-:": "editor::ToggleInlayHints"
     }
   },
   {
@@ -737,6 +738,7 @@
     "context": "GitPanel > Editor",
     "use_key_equivalents": true,
     "bindings": {
+      "enter": "editor::Newline",
       "cmd-enter": "git::Commit",
       "tab": "git_panel::FocusChanges",
       "shift-tab": "git_panel::FocusChanges",

assets/keymaps/vim.json πŸ”—

@@ -381,6 +381,12 @@
       "ctrl-q": ["vim::PushLiteral", {}]
     }
   },
+  {
+    "context": "Editor && vim_mode == waiting && (vim_operator == ys || vim_operator == cs)",
+    "bindings": {
+      "escape": "vim::SwitchToNormalMode"
+    }
+  },
   {
     "context": "vim_mode == operator",
     "bindings": {

assets/themes/one/one.json πŸ”—

@@ -81,7 +81,7 @@
         "terminal.ansi.bright_green": "#4d6140ff",
         "terminal.ansi.dim_green": "#d1e0bfff",
         "terminal.ansi.yellow": "#dec184ff",
-        "terminal.ansi.bright_yellow": "#786441ff",
+        "terminal.ansi.bright_yellow": "#e5c07bff",
         "terminal.ansi.dim_yellow": "#f1dfc1ff",
         "terminal.ansi.blue": "#74ade8ff",
         "terminal.ansi.bright_blue": "#385378ff",
@@ -457,7 +457,7 @@
         "terminal.ansi.bright_green": "#b2cfa9ff",
         "terminal.ansi.dim_green": "#354d2eff",
         "terminal.ansi.yellow": "#dec184ff",
-        "terminal.ansi.bright_yellow": "#f1dfc1ff",
+        "terminal.ansi.bright_yellow": "#826221ff",
         "terminal.ansi.dim_yellow": "#786441ff",
         "terminal.ansi.blue": "#5c78e2ff",
         "terminal.ansi.bright_blue": "#b5baf2ff",

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

@@ -250,10 +250,10 @@ impl AssistantPanel {
                     )
                     .child(
                         PopoverMenu::new("assistant-panel-popover-menu")
-                            .trigger(
+                            .trigger_with_tooltip(
                                 IconButton::new("menu", IconName::EllipsisVertical)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("Toggle Assistant Menu")),
+                                    .icon_size(IconSize::Small),
+                                Tooltip::text("Toggle Assistant Menu"),
                             )
                             .menu(move |window, cx| {
                                 let zoom_label = if _pane.read(cx).is_zoomed() {

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

@@ -1595,22 +1595,22 @@ impl Render for PromptEditor {
                         IconButton::new("context", IconName::SettingsAlt)
                             .shape(IconButtonShape::Square)
                             .icon_size(IconSize::Small)
-                            .icon_color(Color::Muted)
-                            .tooltip(move |window, cx| {
-                                Tooltip::with_meta(
-                                    format!(
-                                        "Using {}",
-                                        LanguageModelRegistry::read_global(cx)
-                                            .active_model()
-                                            .map(|model| model.name().0)
-                                            .unwrap_or_else(|| "No model selected".into()),
-                                    ),
-                                    None,
-                                    "Change Model",
-                                    window,
-                                    cx,
-                                )
-                            }),
+                            .icon_color(Color::Muted),
+                        move |window, cx| {
+                            Tooltip::with_meta(
+                                format!(
+                                    "Using {}",
+                                    LanguageModelRegistry::read_global(cx)
+                                        .active_model()
+                                        .map(|model| model.name().0)
+                                        .unwrap_or_else(|| "No model selected".into()),
+                                ),
+                                None,
+                                "Change Model",
+                                window,
+                                cx,
+                            )
+                        },
                     ))
                     .map(|el| {
                         let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {

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

@@ -646,22 +646,22 @@ impl Render for PromptEditor {
                         IconButton::new("context", IconName::SettingsAlt)
                             .shape(IconButtonShape::Square)
                             .icon_size(IconSize::Small)
-                            .icon_color(Color::Muted)
-                            .tooltip(move |window, cx| {
-                                Tooltip::with_meta(
-                                    format!(
-                                        "Using {}",
-                                        LanguageModelRegistry::read_global(cx)
-                                            .active_model()
-                                            .map(|model| model.name().0)
-                                            .unwrap_or_else(|| "No model selected".into()),
-                                    ),
-                                    None,
-                                    "Change Model",
-                                    window,
-                                    cx,
-                                )
-                            }),
+                            .icon_color(Color::Muted),
+                        move |window, cx| {
+                            Tooltip::with_meta(
+                                format!(
+                                    "Using {}",
+                                    LanguageModelRegistry::read_global(cx)
+                                        .active_model()
+                                        .map(|model| model.name().0)
+                                        .unwrap_or_else(|| "No model selected".into()),
+                                ),
+                                None,
+                                "Change Model",
+                                window,
+                                cx,
+                            )
+                        },
                     ))
                     .children(
                         if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {

crates/assistant2/src/assistant_model_selector.rs πŸ”—

@@ -74,16 +74,16 @@ impl Render for AssistantModelSelector {
                                 .color(Color::Muted)
                                 .size(IconSize::XSmall),
                         ),
+                ),
+            move |window, cx| {
+                Tooltip::for_action_in(
+                    "Change Model",
+                    &ToggleModelSelector,
+                    &focus_handle,
+                    window,
+                    cx,
                 )
-                .tooltip(move |window, cx| {
-                    Tooltip::for_action_in(
-                        "Change Model",
-                        &ToggleModelSelector,
-                        &focus_handle,
-                        window,
-                        cx,
-                    )
-                }),
+            },
         )
         .with_handle(self.menu_handle.clone())
     }

crates/assistant2/src/assistant_panel.rs πŸ”—

@@ -660,11 +660,11 @@ impl AssistantPanel {
                     .gap(DynamicSpacing::Base02.rems(cx))
                     .child(
                         PopoverMenu::new("assistant-toolbar-new-popover-menu")
-                            .trigger(
+                            .trigger_with_tooltip(
                                 IconButton::new("new", IconName::Plus)
                                     .icon_size(IconSize::Small)
-                                    .style(ButtonStyle::Subtle)
-                                    .tooltip(Tooltip::text("New…")),
+                                    .style(ButtonStyle::Subtle),
+                                Tooltip::text("New…"),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(self.new_item_context_menu_handle.clone())
@@ -677,11 +677,11 @@ impl AssistantPanel {
                     )
                     .child(
                         PopoverMenu::new("assistant-toolbar-history-popover-menu")
-                            .trigger(
+                            .trigger_with_tooltip(
                                 IconButton::new("open-history", IconName::HistoryRerun)
                                     .icon_size(IconSize::Small)
-                                    .style(ButtonStyle::Subtle)
-                                    .tooltip(Tooltip::text("History…")),
+                                    .style(ButtonStyle::Subtle),
+                                Tooltip::text("History…"),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(self.open_history_context_menu_handle.clone())

crates/assistant2/src/context_strip.rs πŸ”—

@@ -411,22 +411,22 @@ impl Render for ContextStrip {
 
                         Some(context_picker.clone())
                     })
-                    .trigger(
+                    .trigger_with_tooltip(
                         IconButton::new("add-context", IconName::Plus)
                             .icon_size(IconSize::Small)
-                            .style(ui::ButtonStyle::Filled)
-                            .tooltip({
-                                let focus_handle = focus_handle.clone();
-                                move |window, cx| {
-                                    Tooltip::for_action_in(
-                                        "Add Context",
-                                        &ToggleContextPicker,
-                                        &focus_handle,
-                                        window,
-                                        cx,
-                                    )
-                                }
-                            }),
+                            .style(ui::ButtonStyle::Filled),
+                        {
+                            let focus_handle = focus_handle.clone();
+                            move |window, cx| {
+                                Tooltip::for_action_in(
+                                    "Add Context",
+                                    &ToggleContextPicker,
+                                    &focus_handle,
+                                    window,
+                                    cx,
+                                )
+                            }
+                        },
                     )
                     .attach(gpui::Corner::TopLeft)
                     .anchor(gpui::Corner::BottomLeft)

crates/assistant_context_editor/src/context_editor.rs πŸ”—

@@ -2359,8 +2359,8 @@ impl ContextEditor {
                 .icon(IconName::Plus)
                 .icon_size(IconSize::Small)
                 .icon_color(Color::Muted)
-                .icon_position(IconPosition::Start)
-                .tooltip(Tooltip::text("Type / to insert via keyboard")),
+                .icon_position(IconPosition::Start),
+            Tooltip::text("Type / to insert via keyboard"),
         )
     }
 
@@ -3323,10 +3323,10 @@ impl Render for ContextEditorToolbarItem {
                                         .color(Color::Muted)
                                         .size(IconSize::XSmall),
                                 ),
-                        )
-                        .tooltip(move |window, cx| {
-                            Tooltip::for_action("Change Model", &ToggleModelSelector, window, cx)
-                        }),
+                        ),
+                    move |window, cx| {
+                        Tooltip::for_action("Change Model", &ToggleModelSelector, window, cx)
+                    },
                 )
                 .with_handle(self.language_model_selector_menu_handle.clone()),
             )

crates/assistant_context_editor/src/slash_command_picker.rs πŸ”—

@@ -1,17 +1,22 @@
 use std::sync::Arc;
 
 use assistant_slash_command::SlashCommandWorkingSet;
-use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakEntity};
+use gpui::{AnyElement, AnyView, DismissEvent, SharedString, Task, WeakEntity};
 use picker::{Picker, PickerDelegate, PickerEditorPosition};
 use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
 
 use crate::context_editor::ContextEditor;
 
 #[derive(IntoElement)]
-pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
+pub(super) struct SlashCommandSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     working_set: Arc<SlashCommandWorkingSet>,
     active_context_editor: WeakEntity<ContextEditor>,
     trigger: T,
+    tooltip: TT,
 }
 
 #[derive(Clone)]
@@ -48,16 +53,22 @@ pub(crate) struct SlashCommandDelegate {
     selected_index: usize,
 }
 
-impl<T: PopoverTrigger> SlashCommandSelector<T> {
+impl<T, TT> SlashCommandSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     pub(crate) fn new(
         working_set: Arc<SlashCommandWorkingSet>,
         active_context_editor: WeakEntity<ContextEditor>,
         trigger: T,
+        tooltip: TT,
     ) -> Self {
         SlashCommandSelector {
             working_set,
             active_context_editor,
             trigger,
+            tooltip,
         }
     }
 }
@@ -241,7 +252,11 @@ impl PickerDelegate for SlashCommandDelegate {
     }
 }
 
-impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
+impl<T, TT> RenderOnce for SlashCommandSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
         let all_models = self
             .working_set
@@ -322,7 +337,7 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
             .ok();
         PopoverMenu::new("model-switcher")
             .menu(move |_window, _cx| Some(picker_view.clone()))
-            .trigger(self.trigger)
+            .trigger_with_tooltip(self.trigger, self.tooltip)
             .attach(gpui::Corner::TopLeft)
             .anchor(gpui::Corner::BottomLeft)
             .offset(gpui::Point {

crates/component/Cargo.toml πŸ”—

@@ -0,0 +1,23 @@
+[package]
+name = "component"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/component.rs"
+
+[dependencies]
+collections.workspace = true
+gpui.workspace = true
+linkme.workspace = true
+once_cell = "1.20.3"
+parking_lot.workspace = true
+theme.workspace = true
+
+[features]
+default = []

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

@@ -0,0 +1,305 @@
+use std::ops::{Deref, DerefMut};
+
+use collections::HashMap;
+use gpui::{div, prelude::*, AnyElement, App, IntoElement, RenderOnce, SharedString, Window};
+use linkme::distributed_slice;
+use once_cell::sync::Lazy;
+use parking_lot::RwLock;
+use theme::ActiveTheme;
+
+pub trait Component {
+    fn scope() -> Option<&'static str>;
+    fn name() -> &'static str {
+        std::any::type_name::<Self>()
+    }
+    fn description() -> Option<&'static str> {
+        None
+    }
+}
+
+pub trait ComponentPreview: Component {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement;
+}
+
+#[distributed_slice]
+pub static __ALL_COMPONENTS: [fn()] = [..];
+
+#[distributed_slice]
+pub static __ALL_PREVIEWS: [fn()] = [..];
+
+pub static COMPONENT_DATA: Lazy<RwLock<ComponentRegistry>> =
+    Lazy::new(|| RwLock::new(ComponentRegistry::new()));
+
+pub struct ComponentRegistry {
+    components: Vec<(Option<&'static str>, &'static str, Option<&'static str>)>,
+    previews: HashMap<&'static str, fn(&mut Window, &App) -> AnyElement>,
+}
+
+impl ComponentRegistry {
+    fn new() -> Self {
+        ComponentRegistry {
+            components: Vec::new(),
+            previews: HashMap::default(),
+        }
+    }
+}
+
+pub fn init() {
+    let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
+    let preview_fns: Vec<_> = __ALL_PREVIEWS.iter().cloned().collect();
+
+    for f in component_fns {
+        f();
+    }
+    for f in preview_fns {
+        f();
+    }
+}
+
+pub fn register_component<T: Component>() {
+    let component_data = (T::scope(), T::name(), T::description());
+    COMPONENT_DATA.write().components.push(component_data);
+}
+
+pub fn register_preview<T: ComponentPreview>() {
+    let preview_data = (T::name(), T::preview as fn(&mut Window, &App) -> AnyElement);
+    COMPONENT_DATA
+        .write()
+        .previews
+        .insert(preview_data.0, preview_data.1);
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct ComponentId(pub &'static str);
+
+#[derive(Clone)]
+pub struct ComponentMetadata {
+    name: SharedString,
+    scope: Option<SharedString>,
+    description: Option<SharedString>,
+    preview: Option<fn(&mut Window, &App) -> AnyElement>,
+}
+
+impl ComponentMetadata {
+    pub fn name(&self) -> SharedString {
+        self.name.clone()
+    }
+
+    pub fn scope(&self) -> Option<SharedString> {
+        self.scope.clone()
+    }
+
+    pub fn description(&self) -> Option<SharedString> {
+        self.description.clone()
+    }
+
+    pub fn preview(&self) -> Option<fn(&mut Window, &App) -> AnyElement> {
+        self.preview
+    }
+}
+
+pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
+
+impl AllComponents {
+    pub fn new() -> Self {
+        AllComponents(HashMap::default())
+    }
+
+    /// Returns all components with previews
+    pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
+        self.0.values().filter(|c| c.preview.is_some()).collect()
+    }
+
+    /// Returns all components with previews sorted by name
+    pub fn all_previews_sorted(&self) -> Vec<ComponentMetadata> {
+        let mut previews: Vec<ComponentMetadata> =
+            self.all_previews().into_iter().cloned().collect();
+        previews.sort_by_key(|a| a.name());
+        previews
+    }
+
+    /// Returns all components
+    pub fn all(&self) -> Vec<&ComponentMetadata> {
+        self.0.values().collect()
+    }
+
+    /// Returns all components sorted by name
+    pub fn all_sorted(&self) -> Vec<ComponentMetadata> {
+        let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
+        components.sort_by_key(|a| a.name());
+        components
+    }
+}
+
+impl Deref for AllComponents {
+    type Target = HashMap<ComponentId, ComponentMetadata>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for AllComponents {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+pub fn components() -> AllComponents {
+    let data = COMPONENT_DATA.read();
+    let mut all_components = AllComponents::new();
+
+    for &(scope, name, description) in &data.components {
+        let scope = scope.map(Into::into);
+        let preview = data.previews.get(name).cloned();
+        all_components.insert(
+            ComponentId(name),
+            ComponentMetadata {
+                name: name.into(),
+                scope,
+                description: description.map(Into::into),
+                preview,
+            },
+        );
+    }
+
+    all_components
+}
+
+/// Which side of the preview to show labels on
+#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ExampleLabelSide {
+    /// Left side
+    Left,
+    /// Right side
+    Right,
+    #[default]
+    /// Top side
+    Top,
+    /// Bottom side
+    Bottom,
+}
+
+/// A single example of a component.
+#[derive(IntoElement)]
+pub struct ComponentExample {
+    variant_name: SharedString,
+    element: AnyElement,
+    label_side: ExampleLabelSide,
+    grow: bool,
+}
+
+impl RenderOnce for ComponentExample {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let base = div().flex();
+
+        let base = match self.label_side {
+            ExampleLabelSide::Right => base.flex_row(),
+            ExampleLabelSide::Left => base.flex_row_reverse(),
+            ExampleLabelSide::Bottom => base.flex_col(),
+            ExampleLabelSide::Top => base.flex_col_reverse(),
+        };
+
+        base.gap_1()
+            .text_xs()
+            .text_color(cx.theme().colors().text_muted)
+            .when(self.grow, |this| this.flex_1())
+            .child(self.element)
+            .child(self.variant_name)
+            .into_any_element()
+    }
+}
+
+impl ComponentExample {
+    /// Create a new example with the given variant name and example value.
+    pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
+        Self {
+            variant_name: variant_name.into(),
+            element,
+            label_side: ExampleLabelSide::default(),
+            grow: false,
+        }
+    }
+
+    /// Set the example to grow to fill the available horizontal space.
+    pub fn grow(mut self) -> Self {
+        self.grow = true;
+        self
+    }
+}
+
+/// A group of component examples.
+#[derive(IntoElement)]
+pub struct ComponentExampleGroup {
+    pub title: Option<SharedString>,
+    pub examples: Vec<ComponentExample>,
+    pub grow: bool,
+}
+
+impl RenderOnce for ComponentExampleGroup {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        div()
+            .flex_col()
+            .text_sm()
+            .text_color(cx.theme().colors().text_muted)
+            .when(self.grow, |this| this.w_full().flex_1())
+            .when_some(self.title, |this, title| this.gap_4().child(title))
+            .child(
+                div()
+                    .flex()
+                    .items_start()
+                    .w_full()
+                    .gap_6()
+                    .children(self.examples)
+                    .into_any_element(),
+            )
+            .into_any_element()
+    }
+}
+
+impl ComponentExampleGroup {
+    /// Create a new group of examples with the given title.
+    pub fn new(examples: Vec<ComponentExample>) -> Self {
+        Self {
+            title: None,
+            examples,
+            grow: false,
+        }
+    }
+
+    /// Create a new group of examples with the given title.
+    pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
+        Self {
+            title: Some(title.into()),
+            examples,
+            grow: false,
+        }
+    }
+
+    /// Set the group to grow to fill the available horizontal space.
+    pub fn grow(mut self) -> Self {
+        self.grow = true;
+        self
+    }
+}
+
+/// Create a single example
+pub fn single_example(
+    variant_name: impl Into<SharedString>,
+    example: AnyElement,
+) -> ComponentExample {
+    ComponentExample::new(variant_name, example)
+}
+
+/// Create a group of examples without a title
+pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
+    ComponentExampleGroup::new(examples)
+}
+
+/// Create a group of examples with a title
+pub fn example_group_with_title(
+    title: impl Into<SharedString>,
+    examples: Vec<ComponentExample>,
+) -> ComponentExampleGroup {
+    ComponentExampleGroup::with_title(title, examples)
+}

crates/vcs_menu/Cargo.toml β†’ crates/component_preview/Cargo.toml πŸ”—

@@ -1,5 +1,5 @@
 [package]
-name = "vcs_menu"
+name = "component_preview"
 version = "0.1.0"
 edition.workspace = true
 publish.workspace = true
@@ -8,14 +8,14 @@ license = "GPL-3.0-or-later"
 [lints]
 workspace = true
 
+[lib]
+path = "src/component_preview.rs"
+
+[features]
+default = []
+
 [dependencies]
-anyhow.workspace = true
-fuzzy.workspace = true
-git.workspace = true
+component.workspace = true
 gpui.workspace = true
-picker.workspace = true
-project.workspace = true
 ui.workspace = true
-util.workspace = true
 workspace.workspace = true
-zed_actions.workspace = true

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

@@ -0,0 +1,178 @@
+//! # Component Preview
+//!
+//! A view for exploring Zed components.
+
+use component::{components, ComponentMetadata};
+use gpui::{prelude::*, App, EventEmitter, FocusHandle, Focusable, Window};
+use ui::prelude::*;
+
+use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
+
+pub fn init(cx: &mut App) {
+    cx.observe_new(|workspace: &mut Workspace, _, _cx| {
+        workspace.register_action(
+            |workspace, _: &workspace::OpenComponentPreview, window, cx| {
+                let component_preview = cx.new(ComponentPreview::new);
+                workspace.add_item_to_active_pane(
+                    Box::new(component_preview),
+                    None,
+                    true,
+                    window,
+                    cx,
+                )
+            },
+        );
+    })
+    .detach();
+}
+
+struct ComponentPreview {
+    focus_handle: FocusHandle,
+}
+
+impl ComponentPreview {
+    pub fn new(cx: &mut Context<Self>) -> Self {
+        Self {
+            focus_handle: cx.focus_handle(),
+        }
+    }
+
+    fn render_sidebar(&self, _window: &Window, _cx: &Context<Self>) -> impl IntoElement {
+        let components = components().all_sorted();
+        let sorted_components = components.clone();
+
+        v_flex().gap_px().p_1().children(
+            sorted_components
+                .into_iter()
+                .map(|component| self.render_sidebar_entry(&component, _cx)),
+        )
+    }
+
+    fn render_sidebar_entry(
+        &self,
+        component: &ComponentMetadata,
+        _cx: &Context<Self>,
+    ) -> impl IntoElement {
+        h_flex()
+            .w_40()
+            .px_1p5()
+            .py_1()
+            .child(component.name().clone())
+    }
+
+    fn render_preview(
+        &self,
+        component: &ComponentMetadata,
+        window: &mut Window,
+        cx: &Context<Self>,
+    ) -> impl IntoElement {
+        let name = component.name();
+        let scope = component.scope();
+
+        let description = component.description();
+
+        v_group()
+            .w_full()
+            .gap_4()
+            .p_8()
+            .rounded_md()
+            .child(
+                v_flex()
+                    .gap_1()
+                    .child(
+                        h_flex()
+                            .gap_1()
+                            .text_xl()
+                            .child(div().child(name))
+                            .when_some(scope, |this, scope| {
+                                this.child(div().opacity(0.5).child(format!("({})", scope)))
+                            }),
+                    )
+                    .when_some(description, |this, description| {
+                        this.child(
+                            div()
+                                .text_ui_sm(cx)
+                                .text_color(cx.theme().colors().text_muted)
+                                .max_w(px(600.0))
+                                .child(description),
+                        )
+                    }),
+            )
+            .when_some(component.preview(), |this, preview| {
+                this.child(preview(window, cx))
+            })
+            .into_any_element()
+    }
+
+    fn render_previews(&self, window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
+        v_flex()
+            .id("component-previews")
+            .size_full()
+            .overflow_y_scroll()
+            .p_4()
+            .gap_2()
+            .children(
+                components()
+                    .all_previews_sorted()
+                    .iter()
+                    .map(|component| self.render_preview(component, window, cx)),
+            )
+    }
+}
+
+impl Render for ComponentPreview {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
+        h_flex()
+            .id("component-preview")
+            .key_context("ComponentPreview")
+            .items_start()
+            .overflow_hidden()
+            .size_full()
+            .max_h_full()
+            .track_focus(&self.focus_handle)
+            .px_2()
+            .bg(cx.theme().colors().editor_background)
+            .child(self.render_sidebar(window, cx))
+            .child(self.render_previews(window, cx))
+    }
+}
+
+impl EventEmitter<ItemEvent> for ComponentPreview {}
+
+impl Focusable for ComponentPreview {
+    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl Item for ComponentPreview {
+    type Event = ItemEvent;
+
+    fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
+        Some("Component Preview".into())
+    }
+
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        None
+    }
+
+    fn show_toolbar(&self) -> bool {
+        false
+    }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: Option<WorkspaceId>,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Option<gpui::Entity<Self>>
+    where
+        Self: Sized,
+    {
+        Some(cx.new(Self::new))
+    }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+        f(*event)
+    }
+}

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

@@ -242,6 +242,7 @@ impl EditPredictionProvider for CopilotCompletionProvider {
             } else {
                 let position = cursor_position.bias_right(buffer);
                 Some(InlineCompletion {
+                    id: None,
                     edits: vec![(position..position, completion_text.into())],
                     edit_preview: None,
                 })

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

@@ -490,6 +490,7 @@ enum InlineCompletion {
 struct InlineCompletionState {
     inlay_ids: Vec<InlayId>,
     completion: InlineCompletion,
+    completion_id: Option<SharedString>,
     invalidation_range: Range<Anchor>,
 }
 
@@ -4893,7 +4894,11 @@ impl Editor {
             return;
         };
 
-        self.report_inline_completion_event(true, cx);
+        self.report_inline_completion_event(
+            active_inline_completion.completion_id.clone(),
+            true,
+            cx,
+        );
 
         match &active_inline_completion.completion {
             InlineCompletion::Move { target, .. } => {
@@ -4942,7 +4947,11 @@ impl Editor {
             return;
         }
 
-        self.report_inline_completion_event(true, cx);
+        self.report_inline_completion_event(
+            active_inline_completion.completion_id.clone(),
+            true,
+            cx,
+        );
 
         match &active_inline_completion.completion {
             InlineCompletion::Move { target, .. } => {
@@ -5000,7 +5009,12 @@ impl Editor {
         cx: &mut Context<Self>,
     ) -> bool {
         if should_report_inline_completion_event {
-            self.report_inline_completion_event(false, cx);
+            let completion_id = self
+                .active_inline_completion
+                .as_ref()
+                .and_then(|active_completion| active_completion.completion_id.clone());
+
+            self.report_inline_completion_event(completion_id, false, cx);
         }
 
         if let Some(provider) = self.edit_prediction_provider() {
@@ -5010,7 +5024,7 @@ impl Editor {
         self.take_active_inline_completion(cx)
     }
 
-    fn report_inline_completion_event(&self, accepted: bool, cx: &App) {
+    fn report_inline_completion_event(&self, id: Option<SharedString>, accepted: bool, cx: &App) {
         let Some(provider) = self.edit_prediction_provider() else {
             return;
         };
@@ -5035,6 +5049,7 @@ impl Editor {
         telemetry::event!(
             event_type,
             provider = provider.name(),
+            prediction_id = id,
             suggestion_accepted = accepted,
             file_extension = extension,
         );
@@ -5250,6 +5265,7 @@ impl Editor {
         self.active_inline_completion = Some(InlineCompletionState {
             inlay_ids,
             completion,
+            completion_id: inline_completion.id,
             invalidation_range,
         });
 
@@ -10283,26 +10299,14 @@ impl Editor {
                     if entry.diagnostic.is_primary
                         && entry.diagnostic.severity <= DiagnosticSeverity::WARNING
                         && entry.range.start != entry.range.end
+                        // if we match with the active diagnostic, skip it
+                        && Some(entry.diagnostic.group_id)
+                            != self.active_diagnostics.as_ref().map(|d| d.group_id)
                     {
-                        let entry_group = entry.diagnostic.group_id;
-                        let in_next_group = self.active_diagnostics.as_ref().map_or(
-                            true,
-                            |active| match direction {
-                                Direction::Prev => {
-                                    entry_group != active.group_id
-                                        && (active.group_id == 0 || entry_group < active.group_id)
-                                }
-                                Direction::Next => {
-                                    entry_group != active.group_id
-                                        && (entry_group == 0 || entry_group > active.group_id)
-                                }
-                            },
-                        );
-                        if in_next_group {
-                            return Some((entry.range, entry.diagnostic.group_id));
-                        }
+                        Some((entry.range, entry.diagnostic.group_id))
+                    } else {
+                        None
                     }
-                    None
                 });
 
             if let Some((primary_range, group_id)) = group {

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

@@ -10653,176 +10653,6 @@ async fn go_to_prev_overlapping_diagnostic(
     "});
 }
 
-#[gpui::test]
-async fn cycle_through_same_place_diagnostics(
-    executor: BackgroundExecutor,
-    cx: &mut gpui::TestAppContext,
-) {
-    init_test(cx, |_| {});
-
-    let mut cx = EditorTestContext::new(cx).await;
-    let lsp_store =
-        cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
-
-    cx.set_state(indoc! {"
-        Λ‡fn func(abc def: i32) -> u32 {
-        }
-    "});
-
-    cx.update(|_, cx| {
-        lsp_store.update(cx, |lsp_store, cx| {
-            lsp_store
-                .update_diagnostics(
-                    LanguageServerId(0),
-                    lsp::PublishDiagnosticsParams {
-                        uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
-                        version: None,
-                        diagnostics: vec![
-                            lsp::Diagnostic {
-                                range: lsp::Range::new(
-                                    lsp::Position::new(0, 11),
-                                    lsp::Position::new(0, 12),
-                                ),
-                                severity: Some(lsp::DiagnosticSeverity::ERROR),
-                                ..Default::default()
-                            },
-                            lsp::Diagnostic {
-                                range: lsp::Range::new(
-                                    lsp::Position::new(0, 12),
-                                    lsp::Position::new(0, 15),
-                                ),
-                                severity: Some(lsp::DiagnosticSeverity::ERROR),
-                                ..Default::default()
-                            },
-                            lsp::Diagnostic {
-                                range: lsp::Range::new(
-                                    lsp::Position::new(0, 12),
-                                    lsp::Position::new(0, 15),
-                                ),
-                                severity: Some(lsp::DiagnosticSeverity::ERROR),
-                                ..Default::default()
-                            },
-                            lsp::Diagnostic {
-                                range: lsp::Range::new(
-                                    lsp::Position::new(0, 25),
-                                    lsp::Position::new(0, 28),
-                                ),
-                                severity: Some(lsp::DiagnosticSeverity::ERROR),
-                                ..Default::default()
-                            },
-                        ],
-                    },
-                    &[],
-                    cx,
-                )
-                .unwrap()
-        });
-    });
-    executor.run_until_parked();
-
-    //// Backward
-
-    // Fourth diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc def: i32) -> Λ‡u32 {
-        }
-    "});
-
-    // Third diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc Λ‡def: i32) -> u32 {
-        }
-    "});
-
-    // Second diagnostic, same place
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc Λ‡def: i32) -> u32 {
-        }
-    "});
-
-    // First diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abcˇ def: i32) -> u32 {
-        }
-    "});
-
-    // Wrapped over, fourth diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc def: i32) -> Λ‡u32 {
-        }
-    "});
-
-    cx.update_editor(|editor, window, cx| {
-        editor.move_to_beginning(&MoveToBeginning, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        Λ‡fn func(abc def: i32) -> u32 {
-        }
-    "});
-
-    //// Forward
-
-    // First diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abcˇ def: i32) -> u32 {
-        }
-    "});
-
-    // Second diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc Λ‡def: i32) -> u32 {
-        }
-    "});
-
-    // Third diagnostic, same place
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc Λ‡def: i32) -> u32 {
-        }
-    "});
-
-    // Fourth diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc def: i32) -> Λ‡u32 {
-        }
-    "});
-
-    // Wrapped around, first diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abcˇ def: i32) -> u32 {
-        }
-    "});
-}
-
 #[gpui::test]
 async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
     init_test(cx, |_| {});

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

@@ -5800,6 +5800,9 @@ fn inline_completion_accept_indicator(
         .child(accept_keystroke.key.clone());
 
     let padding_right = if icon.is_some() { px(4.) } else { px(8.) };
+    let accent_color = cx.theme().colors().text_accent;
+    let editor_bg_color = cx.theme().colors().editor_background;
+    let bg_color = editor_bg_color.blend(accent_color.opacity(0.2));
 
     Some(
         h_flex()
@@ -5807,7 +5810,7 @@ fn inline_completion_accept_indicator(
             .pl_1()
             .pr(padding_right)
             .gap_1()
-            .bg(cx.theme().colors().text_accent.opacity(0.15))
+            .bg(bg_color)
             .border_1()
             .border_color(cx.theme().colors().text_accent.opacity(0.8))
             .rounded_md()

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

@@ -763,7 +763,7 @@ impl Editor {
                                                 this.child({
                                                     let focus = editor.focus_handle(cx);
                                                     PopoverMenu::new("hunk-controls-dropdown")
-                                                        .trigger(
+                                                        .trigger_with_tooltip(
                                                             IconButton::new(
                                                                 "toggle_editor_selections_icon",
                                                                 IconName::EllipsisVertical,
@@ -774,19 +774,8 @@ impl Editor {
                                                             .toggle_state(
                                                                 hunk_controls_menu_handle
                                                                     .is_deployed(),
-                                                            )
-                                                            .when(
-                                                                !hunk_controls_menu_handle
-                                                                    .is_deployed(),
-                                                                |this| {
-                                                                    this.tooltip(|_, cx| {
-                                                                        Tooltip::simple(
-                                                                            "Hunk Controls",
-                                                                            cx,
-                                                                        )
-                                                                    })
-                                                                },
                                                             ),
+                                                            Tooltip::simple("Hunk Controls", cx),
                                                         )
                                                         .anchor(Corner::TopRight)
                                                         .with_handle(hunk_controls_menu_handle)

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

@@ -333,6 +333,7 @@ fn propose_edits<T: ToOffset>(
     cx.update(|_, cx| {
         provider.update(cx, |provider, _| {
             provider.set_inline_completion(Some(inline_completion::InlineCompletion {
+                id: None,
                 edits: edits.collect(),
                 edit_preview: None,
             }))

crates/git_ui/Cargo.toml πŸ”—

@@ -20,6 +20,7 @@ diff.workspace = true
 editor.workspace = true
 feature_flags.workspace = true
 futures.workspace = true
+fuzzy.workspace = true
 git.workspace = true
 gpui.workspace = true
 language.workspace = true
@@ -38,6 +39,7 @@ theme.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
+zed_actions.workspace = true
 
 [target.'cfg(windows)'.dependencies]
 windows.workspace = true

crates/vcs_menu/src/lib.rs β†’ crates/git_ui/src/branch_picker.rs πŸ”—

@@ -1,27 +1,49 @@
 use anyhow::{anyhow, Context as _, Result};
 use fuzzy::{StringMatch, StringMatchCandidate};
+
 use git::repository::Branch;
 use gpui::{
-    rems, AnyElement, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
-    Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
-    Subscription, Task, WeakEntity, Window,
+    rems, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+    InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
+    Task, WeakEntity, Window,
 };
 use picker::{Picker, PickerDelegate};
 use project::ProjectPath;
-use std::{ops::Not, sync::Arc};
+use std::sync::Arc;
 use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
 use util::ResultExt;
 use workspace::notifications::DetachAndPromptErr;
 use workspace::{ModalView, Workspace};
-use zed_actions::branches::OpenRecent;
 
 pub fn init(cx: &mut App) {
     cx.observe_new(|workspace: &mut Workspace, _, _| {
-        workspace.register_action(BranchList::open);
+        workspace.register_action(open);
     })
     .detach();
 }
 
+pub fn open(
+    _: &mut Workspace,
+    _: &zed_actions::git::Branch,
+    window: &mut Window,
+    cx: &mut Context<Workspace>,
+) {
+    let this = cx.entity().clone();
+    cx.spawn_in(window, |_, mut cx| async move {
+        // Modal branch picker has a longer trailoff than a popover one.
+        let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?;
+
+        this.update_in(&mut cx, |workspace, window, cx| {
+            workspace.toggle_modal(window, cx, |window, cx| {
+                BranchList::new(delegate, 34., window, cx)
+            })
+        })?;
+
+        Ok(())
+    })
+    .detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None)
+}
+
 pub struct BranchList {
     pub picker: Entity<Picker<BranchListDelegate>>,
     rem_width: f32,
@@ -29,29 +51,7 @@ pub struct BranchList {
 }
 
 impl BranchList {
-    pub fn open(
-        _: &mut Workspace,
-        _: &OpenRecent,
-        window: &mut Window,
-        cx: &mut Context<Workspace>,
-    ) {
-        let this = cx.entity().clone();
-        cx.spawn_in(window, |_, mut cx| async move {
-            // Modal branch picker has a longer trailoff than a popover one.
-            let delegate = BranchListDelegate::new(this.clone(), 70, &cx).await?;
-
-            this.update_in(&mut cx, |workspace, window, cx| {
-                workspace.toggle_modal(window, cx, |window, cx| {
-                    BranchList::new(delegate, 34., window, cx)
-                })
-            })?;
-
-            Ok(())
-        })
-        .detach_and_prompt_err("Failed to read branches", window, cx, |_, _, _| None)
-    }
-
-    fn new(
+    pub fn new(
         delegate: BranchListDelegate,
         rem_width: f32,
         window: &mut Window,
@@ -91,6 +91,7 @@ impl Render for BranchList {
 #[derive(Debug, Clone)]
 enum BranchEntry {
     Branch(StringMatch),
+    History(String),
     NewBranch { name: String },
 }
 
@@ -98,6 +99,7 @@ impl BranchEntry {
     fn name(&self) -> &str {
         match self {
             Self::Branch(branch) => &branch.string,
+            Self::History(branch) => &branch,
             Self::NewBranch { name } => &name,
         }
     }
@@ -114,7 +116,7 @@ pub struct BranchListDelegate {
 }
 
 impl BranchListDelegate {
-    async fn new(
+    pub async fn new(
         workspace: Entity<Workspace>,
         branch_name_trailoff_after: usize,
         cx: &AsyncApp,
@@ -141,7 +143,7 @@ impl BranchListDelegate {
         })
     }
 
-    fn branch_count(&self) -> usize {
+    pub fn branch_count(&self) -> usize {
         self.matches
             .iter()
             .filter(|item| matches!(item, BranchEntry::Branch(_)))
@@ -207,16 +209,10 @@ impl PickerDelegate for BranchListDelegate {
             let Some(candidates) = candidates.log_err() else {
                 return;
             };
-            let matches = if query.is_empty() {
+            let matches: Vec<BranchEntry> = if query.is_empty() {
                 candidates
                     .into_iter()
-                    .enumerate()
-                    .map(|(index, candidate)| StringMatch {
-                        candidate_id: index,
-                        string: candidate.string,
-                        positions: Vec::new(),
-                        score: 0.0,
-                    })
+                    .map(|candidate| BranchEntry::History(candidate.string))
                     .collect()
             } else {
                 fuzzy::match_strings(
@@ -228,11 +224,15 @@ impl PickerDelegate for BranchListDelegate {
                     cx.background_executor().clone(),
                 )
                 .await
+                .iter()
+                .cloned()
+                .map(BranchEntry::Branch)
+                .collect()
             };
             picker
                 .update(&mut cx, |picker, _| {
                     let delegate = &mut picker.delegate;
-                    delegate.matches = matches.into_iter().map(BranchEntry::Branch).collect();
+                    delegate.matches = matches;
                     if delegate.matches.is_empty() {
                         if !query.is_empty() {
                             delegate.matches.push(BranchEntry::NewBranch {
@@ -268,6 +268,7 @@ impl PickerDelegate for BranchListDelegate {
                     let project = workspace.read(cx).project().read(cx);
                     let branch_to_checkout = match branch {
                         BranchEntry::Branch(branch) => branch.string,
+                        BranchEntry::History(string) => string,
                         BranchEntry::NewBranch { name: branch_name } => branch_name,
                     };
                     let worktree = project
@@ -311,7 +312,14 @@ impl PickerDelegate for BranchListDelegate {
                 .inset(true)
                 .spacing(ListItemSpacing::Sparse)
                 .toggle_state(selected)
-                .map(|parent| match hit {
+                .when(matches!(hit, BranchEntry::History(_)), |el| {
+                    el.end_slot(
+                        Icon::new(IconName::HistoryRerun)
+                            .color(Color::Muted)
+                            .size(IconSize::Small),
+                    )
+                })
+                .map(|el| match hit {
                     BranchEntry::Branch(branch) => {
                         let highlights: Vec<_> = branch
                             .positions
@@ -320,40 +328,13 @@ impl PickerDelegate for BranchListDelegate {
                             .copied()
                             .collect();
 
-                        parent.child(HighlightedLabel::new(shortened_branch_name, highlights))
+                        el.child(HighlightedLabel::new(shortened_branch_name, highlights))
                     }
+                    BranchEntry::History(_) => el.child(Label::new(shortened_branch_name)),
                     BranchEntry::NewBranch { name } => {
-                        parent.child(Label::new(format!("Create branch '{name}'")))
+                        el.child(Label::new(format!("Create branch '{name}'")))
                     }
                 }),
         )
     }
-
-    fn render_header(
-        &self,
-        _window: &mut Window,
-        _: &mut Context<Picker<Self>>,
-    ) -> Option<AnyElement> {
-        let label = if self.last_query.is_empty() {
-            Label::new("Recent Branches")
-                .size(LabelSize::Small)
-                .mt_1()
-                .ml_3()
-                .into_any_element()
-        } else {
-            let match_label = self.matches.is_empty().not().then(|| {
-                let suffix = if self.branch_count() == 1 { "" } else { "es" };
-                Label::new(format!("{} match{}", self.branch_count(), suffix))
-                    .color(Color::Muted)
-                    .size(LabelSize::Small)
-            });
-            h_flex()
-                .px_3()
-                .justify_between()
-                .child(Label::new("Branches").size(LabelSize::Small))
-                .children(match_label)
-                .into_any_element()
-        };
-        Some(v_flex().mt_1().child(label).into_any_element())
-    }
 }

crates/git_ui/src/git_panel.rs πŸ”—

@@ -6,33 +6,32 @@ use crate::{
 };
 use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
-use editor::actions::MoveToEnd;
-use editor::scroll::ScrollbarAutoHide;
-use editor::{Editor, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar};
-use git::repository::RepoPath;
-use git::status::FileStatus;
-use git::{Commit, ToggleStaged};
+use editor::{
+    actions::MoveToEnd, scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode,
+    EditorSettings, MultiBuffer, ShowScrollbar,
+};
+use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
 use gpui::*;
 use language::{Buffer, File};
 use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
 use multi_buffer::ExcerptInfo;
-use panel::PanelHeader;
-use project::git::{GitEvent, Repository};
-use project::{Fs, Project, ProjectPath};
+use panel::{panel_editor_container, panel_editor_style, panel_filled_button, PanelHeader};
+use project::{
+    git::{GitEvent, Repository},
+    Fs, Project, ProjectPath,
+};
 use serde::{Deserialize, Serialize};
 use settings::Settings as _;
 use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
-use theme::ThemeSettings;
 use ui::{
-    prelude::*, ButtonLike, Checkbox, Divider, DividerColor, ElevationIndex, IndentGuideColors,
-    ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
+    prelude::*, ButtonLike, Checkbox, CheckboxWithLabel, Divider, DividerColor, ElevationIndex,
+    IndentGuideColors, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
 };
 use util::{maybe, ResultExt, TryFutureExt};
-use workspace::notifications::{DetachAndPromptErr, NotificationId};
-use workspace::Toast;
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
-    Workspace,
+    notifications::{DetachAndPromptErr, NotificationId},
+    Toast, Workspace,
 };
 
 actions!(
@@ -147,33 +146,33 @@ struct PendingOperation {
 }
 
 pub struct GitPanel {
+    active_repository: Option<Entity<Repository>>,
+    commit_editor: Entity<Editor>,
+    conflicted_count: usize,
+    conflicted_staged_count: usize,
     current_modifiers: Modifiers,
+    enable_auto_coauthors: bool,
+    entries: Vec<GitListEntry>,
+    entries_by_path: collections::HashMap<RepoPath, usize>,
     focus_handle: FocusHandle,
     fs: Arc<dyn Fs>,
     hide_scrollbar_task: Option<Task<()>>,
+    new_count: usize,
+    new_staged_count: usize,
+    pending: Vec<PendingOperation>,
+    pending_commit: Option<Task<()>>,
     pending_serialization: Task<Option<()>>,
-    workspace: WeakEntity<Workspace>,
     project: Entity<Project>,
-    active_repository: Option<Entity<Repository>>,
+    repository_selector: Entity<RepositorySelector>,
     scroll_handle: UniformListScrollHandle,
     scrollbar_state: ScrollbarState,
     selected_entry: Option<usize>,
     show_scrollbar: bool,
+    tracked_count: usize,
+    tracked_staged_count: usize,
     update_visible_entries_task: Task<()>,
-    repository_selector: Entity<RepositorySelector>,
-    commit_editor: Entity<Editor>,
-    entries: Vec<GitListEntry>,
-    entries_by_path: collections::HashMap<RepoPath, usize>,
     width: Option<Pixels>,
-    pending: Vec<PendingOperation>,
-    pending_commit: Option<Task<()>>,
-
-    conflicted_staged_count: usize,
-    conflicted_count: usize,
-    tracked_staged_count: usize,
-    tracked_count: usize,
-    new_staged_count: usize,
-    new_count: usize,
+    workspace: WeakEntity<Workspace>,
 }
 
 fn commit_message_editor(
@@ -181,23 +180,10 @@ fn commit_message_editor(
     window: &mut Window,
     cx: &mut Context<'_, Editor>,
 ) -> Editor {
-    let theme = ThemeSettings::get_global(cx);
-
-    let mut text_style = window.text_style();
-    let refinement = TextStyleRefinement {
-        font_family: Some(theme.buffer_font.family.clone()),
-        font_features: Some(FontFeatures::disable_ligatures()),
-        font_size: Some(px(12.).into()),
-        color: Some(cx.theme().colors().editor_foreground),
-        background_color: Some(gpui::transparent_black()),
-        ..Default::default()
-    };
-    text_style.refine(&refinement);
-
     let mut commit_editor = if let Some(commit_message_buffer) = commit_message_buffer {
         let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
         Editor::new(
-            EditorMode::AutoHeight { max_lines: 10 },
+            EditorMode::AutoHeight { max_lines: 6 },
             buffer,
             None,
             false,
@@ -205,13 +191,12 @@ fn commit_message_editor(
             cx,
         )
     } else {
-        Editor::auto_height(10, window, cx)
+        Editor::auto_height(6, window, cx)
     };
     commit_editor.set_use_autoclose(false);
     commit_editor.set_show_gutter(false, cx);
     commit_editor.set_show_wrap_guides(false, cx);
     commit_editor.set_show_indent_guides(false, cx);
-    commit_editor.set_text_style_refinement(refinement);
     commit_editor.set_placeholder_text("Enter commit message", cx);
     commit_editor
 }
@@ -260,37 +245,40 @@ impl GitPanel {
             )
             .detach();
 
+            let scrollbar_state =
+                ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity());
+
             let repository_selector =
                 cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
 
             let mut git_panel = Self {
-                focus_handle: cx.focus_handle(),
-                pending_serialization: Task::ready(None),
+                active_repository,
+                commit_editor,
+                conflicted_count: 0,
+                conflicted_staged_count: 0,
+                current_modifiers: window.modifiers(),
+                enable_auto_coauthors: true,
                 entries: Vec::new(),
                 entries_by_path: HashMap::default(),
+                focus_handle: cx.focus_handle(),
+                fs,
+                hide_scrollbar_task: None,
+                new_count: 0,
+                new_staged_count: 0,
                 pending: Vec::new(),
-                current_modifiers: window.modifiers(),
-                width: Some(px(360.)),
-                scrollbar_state: ScrollbarState::new(scroll_handle.clone())
-                    .parent_entity(&cx.entity()),
+                pending_commit: None,
+                pending_serialization: Task::ready(None),
+                project,
                 repository_selector,
+                scroll_handle,
+                scrollbar_state,
                 selected_entry: None,
                 show_scrollbar: false,
-                hide_scrollbar_task: None,
+                tracked_count: 0,
+                tracked_staged_count: 0,
                 update_visible_entries_task: Task::ready(()),
-                pending_commit: None,
-                active_repository,
-                scroll_handle,
-                fs,
-                commit_editor,
-                project,
+                width: Some(px(360.)),
                 workspace,
-                conflicted_count: 0,
-                conflicted_staged_count: 0,
-                tracked_staged_count: 0,
-                tracked_count: 0,
-                new_staged_count: 0,
-                new_count: 0,
             };
             git_panel.schedule_update(false, window, cx);
             git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx);
@@ -990,6 +978,26 @@ impl GitPanel {
         cx.notify();
     }
 
+    fn toggle_auto_coauthors(&mut self, cx: &mut Context<Self>) {
+        self.enable_auto_coauthors = !self.enable_auto_coauthors;
+        cx.notify();
+    }
+
+    fn header_state(&self, header_type: Section) -> ToggleState {
+        let (staged_count, count) = match header_type {
+            Section::New => (self.new_staged_count, self.new_count),
+            Section::Tracked => (self.tracked_staged_count, self.tracked_count),
+            Section::Conflict => (self.conflicted_staged_count, self.conflicted_count),
+        };
+        if staged_count == 0 {
+            ToggleState::Unselected
+        } else if count == staged_count {
+            ToggleState::Selected
+        } else {
+            ToggleState::Indeterminate
+        }
+    }
+
     fn update_counts(&mut self, repo: &Repository) {
         self.conflicted_count = 0;
         self.conflicted_staged_count = 0;
@@ -1043,21 +1051,6 @@ impl GitPanel {
         self.conflicted_count > 0 && self.conflicted_count != self.conflicted_staged_count
     }
 
-    fn header_state(&self, header_type: Section) -> ToggleState {
-        let (staged_count, count) = match header_type {
-            Section::New => (self.new_staged_count, self.new_count),
-            Section::Tracked => (self.tracked_staged_count, self.tracked_count),
-            Section::Conflict => (self.conflicted_staged_count, self.conflicted_count),
-        };
-        if staged_count == 0 {
-            ToggleState::Unselected
-        } else if count == staged_count {
-            ToggleState::Selected
-        } else {
-            ToggleState::Indeterminate
-        }
-    }
-
     fn show_err_toast(&self, e: anyhow::Error, cx: &mut App) {
         let Some(workspace) = self.workspace.upgrade() else {
             return;
@@ -1110,33 +1103,43 @@ impl GitPanel {
             .git_state()
             .read(cx)
             .all_repositories();
-        let entry_count = self
+
+        let branch = self
             .active_repository
             .as_ref()
-            .map_or(0, |repo| repo.read(cx).entry_count());
+            .and_then(|repository| repository.read(cx).branch())
+            .unwrap_or_else(|| "(no current branch)".into());
+
+        let has_repo_above = all_repositories.iter().any(|repo| {
+            repo.read(cx)
+                .repository_entry
+                .work_directory
+                .is_above_project()
+        });
 
-        let changes_string = match entry_count {
-            0 => "No changes".to_string(),
-            1 => "1 change".to_string(),
-            n => format!("{} changes", n),
-        };
+        let icon_button = Button::new("branch-selector", branch)
+            .color(Color::Muted)
+            .style(ButtonStyle::Subtle)
+            .icon(IconName::GitBranch)
+            .icon_size(IconSize::Small)
+            .icon_color(Color::Muted)
+            .size(ButtonSize::Compact)
+            .icon_position(IconPosition::Start)
+            .tooltip(Tooltip::for_action_title(
+                "Switch Branch",
+                &zed_actions::git::Branch,
+            ))
+            .on_click(cx.listener(|_, _, window, cx| {
+                window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
+            }))
+            .style(ButtonStyle::Transparent);
 
         self.panel_header_container(window, cx)
-            .child(h_flex().gap_2().child(if all_repositories.len() <= 1 {
-                div()
-                    .id("changes-label")
-                    .text_buffer(cx)
-                    .text_ui_sm(cx)
-                    .child(
-                        Label::new(changes_string)
-                            .single_line()
-                            .size(LabelSize::Small),
-                    )
-                    .into_any_element()
-            } else {
-                self.render_repository_selector(cx).into_any_element()
-            }))
+            .child(h_flex().pl_1().child(icon_button))
             .child(div().flex_grow())
+            .when(all_repositories.len() > 1 || has_repo_above, |el| {
+                el.child(self.render_repository_selector(cx))
+            })
     }
 
     pub fn render_repository_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -1146,45 +1149,30 @@ impl GitPanel {
             .map(|repo| repo.read(cx).display_name(self.project.read(cx), cx))
             .unwrap_or_default();
 
-        let entry_count = self.entries.len();
-
         RepositorySelectorPopoverMenu::new(
             self.repository_selector.clone(),
             ButtonLike::new("active-repository")
                 .style(ButtonStyle::Subtle)
-                .child(
-                    h_flex().w_full().gap_0p5().child(
-                        div()
-                            .overflow_x_hidden()
-                            .flex_grow()
-                            .whitespace_nowrap()
-                            .child(
-                                h_flex()
-                                    .gap_1()
-                                    .child(
-                                        Label::new(repository_display_name).size(LabelSize::Small),
-                                    )
-                                    .when(entry_count > 0, |flex| {
-                                        flex.child(
-                                            Label::new(format!("({})", entry_count))
-                                                .size(LabelSize::Small)
-                                                .color(Color::Muted),
-                                        )
-                                    })
-                                    .into_any_element(),
-                            ),
-                    ),
-                ),
+                .child(Label::new(repository_display_name).size(LabelSize::Small)),
+            Tooltip::text("Select a repository"),
         )
     }
 
-    pub fn render_commit_editor(&self, cx: &Context<Self>) -> impl IntoElement {
+    pub fn render_commit_editor(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
         let editor = self.commit_editor.clone();
         let can_commit = (self.has_staged_changes() || self.has_tracked_changes())
             && self.pending_commit.is_none()
             && !editor.read(cx).is_empty(cx)
             && !self.has_unstaged_conflicts()
             && self.has_write_access(cx);
+        // let can_commit_all =
+        //     !self.commit_pending && self.can_commit_all && !editor.read(cx).is_empty(cx);
+        let panel_editor_style = panel_editor_style(true, window, cx);
+
         let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
 
         let focus_handle_1 = self.focus_handle(cx).clone();
@@ -1199,8 +1187,7 @@ impl GitPanel {
             "Commit All"
         };
 
-        let commit_button = self
-            .panel_button("commit-changes", title)
+        let commit_button = panel_filled_button(title)
             .tooltip(move |window, cx| {
                 let focus_handle = focus_handle_1.clone();
                 Tooltip::for_action_in(tooltip, &Commit, &focus_handle, window, cx)
@@ -1210,28 +1197,50 @@ impl GitPanel {
                 cx.listener(move |this, _: &ClickEvent, window, cx| this.commit_changes(window, cx))
             });
 
-        div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
-            v_flex()
-                .id("commit-editor-container")
-                .relative()
-                .h_full()
-                .py_2p5()
-                .px_3()
-                .bg(cx.theme().colors().editor_background)
-                .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
-                    window.focus(&editor_focus_handle);
-                }))
-                .child(self.commit_editor.clone())
-                .child(
-                    h_flex()
-                        .absolute()
-                        .bottom_2p5()
-                        .right_3()
-                        .gap_1p5()
-                        .child(div().gap_1().flex_grow())
-                        .child(commit_button),
-                ),
-        )
+        let enable_coauthors = CheckboxWithLabel::new(
+            "enable-coauthors",
+            Label::new("Add Co-authors")
+                .color(Color::Disabled)
+                .size(LabelSize::XSmall),
+            self.enable_auto_coauthors.into(),
+            cx.listener(move |this, _, _, cx| this.toggle_auto_coauthors(cx)),
+        );
+
+        let footer_size = px(32.);
+        let gap = px(16.0);
+
+        let max_height = window.line_height() * 6. + gap + footer_size;
+
+        panel_editor_container(window, cx)
+            .id("commit-editor-container")
+            .relative()
+            .h(max_height)
+            .w_full()
+            .border_t_1()
+            .border_color(cx.theme().colors().border)
+            .bg(cx.theme().colors().editor_background)
+            .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
+                window.focus(&editor_focus_handle);
+            }))
+            .child(EditorElement::new(&self.commit_editor, panel_editor_style))
+            .child(
+                h_flex()
+                    .absolute()
+                    .bottom_0()
+                    .left_2()
+                    .h(footer_size)
+                    .flex_none()
+                    .child(enable_coauthors),
+            )
+            .child(
+                h_flex()
+                    .absolute()
+                    .bottom_0()
+                    .right_2()
+                    .h(footer_size)
+                    .flex_none()
+                    .child(commit_button),
+            )
     }
 
     fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -1243,7 +1252,11 @@ impl GitPanel {
             .child(
                 v_flex()
                     .gap_3()
-                    .child("No changes to commit")
+                    .child(if self.active_repository.is_some() {
+                        "No changes to commit"
+                    } else {
+                        "No Git repositories"
+                    })
                     .text_ui_sm(cx)
                     .mx_auto()
                     .text_color(Color::Placeholder.color(cx)),
@@ -1357,6 +1370,7 @@ impl GitPanel {
 
         v_flex()
             .size_full()
+            .flex_grow()
             .overflow_hidden()
             .child(
                 uniform_list(cx.entity().clone(), "entries", entry_count, {
@@ -1505,7 +1519,7 @@ impl GitPanel {
                     .spacing(ListItemSpacing::Sparse)
                     .start_slot(start_slot)
                     .toggle_state(selected)
-                    .focused(selected && self.focus_handle.is_focused(window))
+                    .focused(selected && self.focus_handle(cx).is_focused(window))
                     .disabled(!has_write_access)
                     .on_click({
                         cx.listener(move |this, _, _, cx| {
@@ -1608,7 +1622,7 @@ impl GitPanel {
                     .spacing(ListItemSpacing::Sparse)
                     .start_slot(start_slot)
                     .toggle_state(selected)
-                    .focused(selected && self.focus_handle.is_focused(window))
+                    .focused(selected && self.focus_handle(cx).is_focused(window))
                     .disabled(!has_write_access)
                     .on_click({
                         cx.listener(move |this, _, window, cx| {
@@ -1714,7 +1728,7 @@ impl Render for GitPanel {
             } else {
                 self.render_empty_state(cx).into_any_element()
             })
-            .child(self.render_commit_editor(cx))
+            .child(self.render_commit_editor(window, cx))
     }
 }
 

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

@@ -5,14 +5,18 @@ use gpui::App;
 use project_diff::ProjectDiff;
 use ui::{ActiveTheme, Color, Icon, IconName, IntoElement};
 
+pub mod branch_picker;
 pub mod git_panel;
 mod git_panel_settings;
 pub mod project_diff;
+// mod quick_commit;
 pub mod repository_selector;
 
 pub fn init(cx: &mut App) {
     GitPanelSettings::register(cx);
+    branch_picker::init(cx);
     cx.observe_new(ProjectDiff::register).detach();
+    // quick_commit::init(cx);
 }
 
 // TODO: Add updated status colors to theme

crates/git_ui/src/quick_commit.rs πŸ”—

@@ -0,0 +1,307 @@
+#![allow(unused, dead_code)]
+
+use crate::repository_selector::RepositorySelector;
+use anyhow::Result;
+use git::{CommitAllChanges, CommitChanges};
+use language::Buffer;
+use panel::{panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button};
+use ui::{prelude::*, Tooltip};
+
+use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
+use gpui::*;
+use project::git::Repository;
+use project::{Fs, Project};
+use std::sync::Arc;
+use workspace::{ModalView, Workspace};
+
+actions!(
+    git,
+    [QuickCommitWithMessage, QuickCommitStaged, QuickCommitAll]
+);
+
+pub fn init(cx: &mut App) {
+    cx.observe_new(|workspace: &mut Workspace, window, cx| {
+        let Some(window) = window else {
+            return;
+        };
+        QuickCommitModal::register(workspace, window, cx)
+    })
+    .detach();
+}
+
+fn commit_message_editor(
+    commit_message_buffer: Option<Entity<Buffer>>,
+    window: &mut Window,
+    cx: &mut Context<'_, Editor>,
+) -> Editor {
+    let mut commit_editor = if let Some(commit_message_buffer) = commit_message_buffer {
+        let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
+        Editor::new(
+            EditorMode::AutoHeight { max_lines: 10 },
+            buffer,
+            None,
+            false,
+            window,
+            cx,
+        )
+    } else {
+        Editor::auto_height(10, window, cx)
+    };
+    commit_editor.set_use_autoclose(false);
+    commit_editor.set_show_gutter(false, cx);
+    commit_editor.set_show_wrap_guides(false, cx);
+    commit_editor.set_show_indent_guides(false, cx);
+    commit_editor.set_placeholder_text("Enter commit message", cx);
+    commit_editor
+}
+
+pub struct QuickCommitModal {
+    focus_handle: FocusHandle,
+    fs: Arc<dyn Fs>,
+    project: Entity<Project>,
+    active_repository: Option<Entity<Repository>>,
+    repository_selector: Entity<RepositorySelector>,
+    commit_editor: Entity<Editor>,
+    width: Option<Pixels>,
+    commit_task: Task<Result<()>>,
+    commit_pending: bool,
+    can_commit: bool,
+    can_commit_all: bool,
+    enable_auto_coauthors: bool,
+}
+
+impl Focusable for QuickCommitModal {
+    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl EventEmitter<DismissEvent> for QuickCommitModal {}
+impl ModalView for QuickCommitModal {}
+
+impl QuickCommitModal {
+    pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
+        workspace.register_action(|workspace, _: &QuickCommitWithMessage, window, cx| {
+            let project = workspace.project().clone();
+            let fs = workspace.app_state().fs.clone();
+
+            workspace.toggle_modal(window, cx, move |window, cx| {
+                QuickCommitModal::new(project, fs, window, None, cx)
+            });
+        });
+    }
+
+    pub fn new(
+        project: Entity<Project>,
+        fs: Arc<dyn Fs>,
+        window: &mut Window,
+        commit_message_buffer: Option<Entity<Buffer>>,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let git_state = project.read(cx).git_state().clone();
+        let active_repository = project.read(cx).active_repository(cx);
+
+        let focus_handle = cx.focus_handle();
+
+        let commit_editor = cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx));
+        commit_editor.update(cx, |editor, cx| {
+            editor.clear(window, cx);
+        });
+
+        let repository_selector = cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
+
+        Self {
+            focus_handle,
+            fs,
+            project,
+            active_repository,
+            repository_selector,
+            commit_editor,
+            width: None,
+            commit_task: Task::ready(Ok(())),
+            commit_pending: false,
+            can_commit: false,
+            can_commit_all: false,
+            enable_auto_coauthors: true,
+        }
+    }
+
+    pub fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let all_repositories = self
+            .project
+            .read(cx)
+            .git_state()
+            .read(cx)
+            .all_repositories();
+        let entry_count = self
+            .active_repository
+            .as_ref()
+            .map_or(0, |repo| repo.read(cx).entry_count());
+
+        let changes_string = match entry_count {
+            0 => "No changes".to_string(),
+            1 => "1 change".to_string(),
+            n => format!("{} changes", n),
+        };
+
+        div().absolute().top_0().right_0().child(
+            panel_icon_button("open_change_list", IconName::PanelRight)
+                .disabled(true)
+                .tooltip(Tooltip::text("Changes list coming soon!")),
+        )
+    }
+
+    pub fn render_commit_editor(
+        &self,
+        name_and_email: Option<(SharedString, SharedString)>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
+        let editor = self.commit_editor.clone();
+        let can_commit = !self.commit_pending && self.can_commit && !editor.read(cx).is_empty(cx);
+        let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
+
+        let focus_handle_1 = self.focus_handle(cx).clone();
+        let focus_handle_2 = self.focus_handle(cx).clone();
+
+        let panel_editor_style = panel_editor_style(true, window, cx);
+
+        let commit_staged_button = panel_filled_button("Commit")
+            .tooltip(move |window, cx| {
+                let focus_handle = focus_handle_1.clone();
+                Tooltip::for_action_in(
+                    "Commit all staged changes",
+                    &CommitChanges,
+                    &focus_handle,
+                    window,
+                    cx,
+                )
+            })
+            .when(!can_commit, |this| {
+                this.disabled(true).style(ButtonStyle::Transparent)
+            });
+        // .on_click({
+        //     let name_and_email = name_and_email.clone();
+        //     cx.listener(move |this, _: &ClickEvent, window, cx| {
+        //         this.commit_changes(&CommitChanges, name_and_email.clone(), window, cx)
+        //     })
+        // });
+
+        let commit_all_button = panel_filled_button("Commit All")
+            .tooltip(move |window, cx| {
+                let focus_handle = focus_handle_2.clone();
+                Tooltip::for_action_in(
+                    "Commit all changes, including unstaged changes",
+                    &CommitAllChanges,
+                    &focus_handle,
+                    window,
+                    cx,
+                )
+            })
+            .when(!can_commit, |this| {
+                this.disabled(true).style(ButtonStyle::Transparent)
+            });
+        // .on_click({
+        //     let name_and_email = name_and_email.clone();
+        //     cx.listener(move |this, _: &ClickEvent, window, cx| {
+        //         this.commit_tracked_changes(
+        //             &CommitAllChanges,
+        //             name_and_email.clone(),
+        //             window,
+        //             cx,
+        //         )
+        //     })
+        // });
+
+        let co_author_button = panel_icon_button("add-co-author", IconName::UserGroup)
+            .icon_color(if self.enable_auto_coauthors {
+                Color::Muted
+            } else {
+                Color::Accent
+            })
+            .icon_size(IconSize::Small)
+            .toggle_state(self.enable_auto_coauthors)
+            // .on_click({
+            //     cx.listener(move |this, _: &ClickEvent, _, cx| {
+            //         this.toggle_auto_coauthors(cx);
+            //     })
+            // })
+            .tooltip(move |window, cx| {
+                Tooltip::with_meta(
+                    "Toggle automatic co-authors",
+                    None,
+                    "Automatically adds current collaborators",
+                    window,
+                    cx,
+                )
+            });
+
+        panel_editor_container(window, cx)
+            .id("commit-editor-container")
+            .relative()
+            .w_full()
+            .border_t_1()
+            .border_color(cx.theme().colors().border)
+            .h(px(140.))
+            .bg(cx.theme().colors().editor_background)
+            .on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
+                window.focus(&editor_focus_handle);
+            }))
+            .child(EditorElement::new(&self.commit_editor, panel_editor_style))
+            .child(div().flex_1())
+            .child(
+                h_flex()
+                    .items_center()
+                    .h_8()
+                    .justify_between()
+                    .gap_1()
+                    .child(co_author_button)
+                    .child(commit_all_button)
+                    .child(commit_staged_button),
+            )
+    }
+
+    pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        h_flex()
+            .w_full()
+            .justify_between()
+            .child(h_flex().child("cmd+esc clear message"))
+            .child(
+                h_flex()
+                    .child(panel_filled_button("Commit"))
+                    .child(panel_filled_button("Commit All")),
+            )
+    }
+
+    fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+        cx.emit(DismissEvent);
+    }
+}
+
+impl Render for QuickCommitModal {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
+        v_flex()
+            .id("quick-commit-modal")
+            .key_context("QuickCommit")
+            .on_action(cx.listener(Self::dismiss))
+            .relative()
+            .bg(cx.theme().colors().elevated_surface_background)
+            .rounded(px(16.))
+            .border_1()
+            .border_color(cx.theme().colors().border)
+            .py_2()
+            .px_4()
+            .w(self.width.unwrap_or(px(640.)))
+            .h(px(450.))
+            .flex_1()
+            .overflow_hidden()
+            .child(self.render_header(window, cx))
+            .child(
+                v_flex()
+                    .flex_1()
+                    // TODO: pass name_and_email
+                    .child(self.render_commit_editor(None, window, cx)),
+            )
+            .child(self.render_footer(window, cx))
+    }
+}

crates/git_ui/src/repository_selector.rs πŸ”—

@@ -1,6 +1,6 @@
 use gpui::{
-    AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
-    Task, WeakEntity,
+    AnyElement, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+    Subscription, Task, WeakEntity,
 };
 use picker::{Picker, PickerDelegate};
 use project::{
@@ -34,6 +34,7 @@ impl RepositorySelector {
         let picker = cx.new(|cx| {
             Picker::nonsearchable_uniform_list(delegate, window, cx)
                 .max_height(Some(rems(20.).into()))
+                .width(rems(15.))
         });
 
         let _subscriptions =
@@ -78,20 +79,27 @@ impl Render for RepositorySelector {
 }
 
 #[derive(IntoElement)]
-pub struct RepositorySelectorPopoverMenu<T>
+pub struct RepositorySelectorPopoverMenu<T, TT>
 where
-    T: PopoverTrigger,
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
 {
     repository_selector: Entity<RepositorySelector>,
     trigger: T,
+    tooltip: TT,
     handle: Option<PopoverMenuHandle<RepositorySelector>>,
 }
 
-impl<T: PopoverTrigger> RepositorySelectorPopoverMenu<T> {
-    pub fn new(repository_selector: Entity<RepositorySelector>, trigger: T) -> Self {
+impl<T, TT> RepositorySelectorPopoverMenu<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
+    pub fn new(repository_selector: Entity<RepositorySelector>, trigger: T, tooltip: TT) -> Self {
         Self {
             repository_selector,
             trigger,
+            tooltip,
             handle: None,
         }
     }
@@ -102,13 +110,17 @@ impl<T: PopoverTrigger> RepositorySelectorPopoverMenu<T> {
     }
 }
 
-impl<T: PopoverTrigger> RenderOnce for RepositorySelectorPopoverMenu<T> {
+impl<T, TT> RenderOnce for RepositorySelectorPopoverMenu<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
         let repository_selector = self.repository_selector.clone();
 
         PopoverMenu::new("repository-switcher")
             .menu(move |_window, _cx| Some(repository_selector.clone()))
-            .trigger(self.trigger)
+            .trigger_with_tooltip(self.trigger, self.tooltip)
             .attach(gpui::Corner::BottomLeft)
             .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
     }

crates/gpui/examples/data_table.rs πŸ”—

@@ -0,0 +1,479 @@
+use std::{
+    ops::Range,
+    rc::Rc,
+    time::{Duration, Instant},
+};
+
+use gpui::{
+    canvas, div, point, prelude::*, px, rgb, size, uniform_list, App, Application, Bounds, Context,
+    MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, SharedString,
+    UniformListScrollHandle, Window, WindowBounds, WindowOptions,
+};
+
+const TOTAL_ITEMS: usize = 10000;
+const SCROLLBAR_THUMB_WIDTH: Pixels = px(8.);
+const SCROLLBAR_THUMB_HEIGHT: Pixels = px(100.);
+
+pub struct Quote {
+    name: SharedString,
+    symbol: SharedString,
+    last_done: f64,
+    prev_close: f64,
+    open: f64,
+    high: f64,
+    low: f64,
+    timestamp: Instant,
+    volume: i64,
+    turnover: f64,
+    ttm: f64,
+    market_cap: f64,
+    float_cap: f64,
+    shares: f64,
+    pb: f64,
+    pe: f64,
+    eps: f64,
+    dividend: f64,
+    dividend_yield: f64,
+    dividend_per_share: f64,
+    dividend_date: SharedString,
+    dividend_payment: f64,
+}
+
+impl Quote {
+    pub fn random() -> Self {
+        use rand::Rng;
+        let mut rng = rand::thread_rng();
+        // simulate a base price in a realistic range
+        let prev_close = rng.gen_range(100.0..200.0);
+        let change = rng.gen_range(-5.0..5.0);
+        let last_done = prev_close + change;
+        let open = prev_close + rng.gen_range(-3.0..3.0);
+        let high = (prev_close + rng.gen_range::<f64, _>(0.0..10.0)).max(open);
+        let low = (prev_close - rng.gen_range::<f64, _>(0.0..10.0)).min(open);
+        // Randomize the timestamp in the past 24 hours
+        let timestamp = Instant::now() - Duration::from_secs(rng.gen_range(0..86400));
+        let volume = rng.gen_range(1_000_000..100_000_000);
+        let turnover = last_done * volume as f64;
+        let symbol = {
+            let mut ticker = String::new();
+            if rng.gen_bool(0.5) {
+                ticker.push_str(&format!(
+                    "{:03}.{}",
+                    rng.gen_range(100..1000),
+                    rng.gen_range(0..10)
+                ));
+            } else {
+                ticker.push_str(&format!(
+                    "{}{}",
+                    rng.gen_range('A'..='Z'),
+                    rng.gen_range('A'..='Z')
+                ));
+            }
+            ticker.push_str(&format!(".{}", rng.gen_range('A'..='Z')));
+            ticker
+        };
+        let name = format!(
+            "{} {} - #{}",
+            symbol,
+            rng.gen_range(1..100),
+            rng.gen_range(10000..100000)
+        );
+        let ttm = rng.gen_range(0.0..10.0);
+        let market_cap = rng.gen_range(1_000_000.0..10_000_000.0);
+        let float_cap = market_cap + rng.gen_range(1_000.0..10_000.0);
+        let shares = rng.gen_range(100.0..1000.0);
+        let pb = market_cap / shares;
+        let pe = market_cap / shares;
+        let eps = market_cap / shares;
+        let dividend = rng.gen_range(0.0..10.0);
+        let dividend_yield = rng.gen_range(0.0..10.0);
+        let dividend_per_share = rng.gen_range(0.0..10.0);
+        let dividend_date = SharedString::new(format!(
+            "{}-{}-{}",
+            rng.gen_range(2000..2023),
+            rng.gen_range(1..12),
+            rng.gen_range(1..28)
+        ));
+        let dividend_payment = rng.gen_range(0.0..10.0);
+
+        Self {
+            name: name.into(),
+            symbol: symbol.into(),
+            last_done,
+            prev_close,
+            open,
+            high,
+            low,
+            timestamp,
+            volume,
+            turnover,
+            pb,
+            pe,
+            eps,
+            ttm,
+            market_cap,
+            float_cap,
+            shares,
+            dividend,
+            dividend_yield,
+            dividend_per_share,
+            dividend_date,
+            dividend_payment,
+        }
+    }
+
+    fn change(&self) -> f64 {
+        (self.last_done - self.prev_close) / self.prev_close * 100.0
+    }
+
+    fn change_color(&self) -> gpui::Hsla {
+        if self.change() > 0.0 {
+            gpui::green()
+        } else {
+            gpui::red()
+        }
+    }
+
+    fn turnover_ratio(&self) -> f64 {
+        self.volume as f64 / self.turnover * 100.0
+    }
+}
+
+#[derive(IntoElement)]
+struct TableRow {
+    ix: usize,
+    quote: Rc<Quote>,
+}
+impl TableRow {
+    fn new(ix: usize, quote: Rc<Quote>) -> Self {
+        Self { ix, quote }
+    }
+
+    fn render_cell(&self, key: &str, width: Pixels, color: gpui::Hsla) -> impl IntoElement {
+        div()
+            .whitespace_nowrap()
+            .truncate()
+            .w(width)
+            .px_1()
+            .child(match key {
+                "id" => div().child(format!("{}", self.ix)),
+                "symbol" => div().child(self.quote.symbol.clone()),
+                "name" => div().child(self.quote.name.clone()),
+                "last_done" => div()
+                    .text_color(color)
+                    .child(format!("{:.3}", self.quote.last_done)),
+                "prev_close" => div()
+                    .text_color(color)
+                    .child(format!("{:.3}", self.quote.prev_close)),
+                "change" => div()
+                    .text_color(color)
+                    .child(format!("{:.2}%", self.quote.change())),
+                "timestamp" => div()
+                    .text_color(color)
+                    .child(format!("{:?}", self.quote.timestamp.elapsed().as_secs())),
+                "open" => div()
+                    .text_color(color)
+                    .child(format!("{:.2}", self.quote.open)),
+                "low" => div()
+                    .text_color(color)
+                    .child(format!("{:.2}", self.quote.low)),
+                "high" => div()
+                    .text_color(color)
+                    .child(format!("{:.2}", self.quote.high)),
+                "ttm" => div()
+                    .text_color(color)
+                    .child(format!("{:.2}", self.quote.ttm)),
+                "eps" => div()
+                    .text_color(color)
+                    .child(format!("{:.2}", self.quote.eps)),
+                "market_cap" => {
+                    div().child(format!("{:.2} M", self.quote.market_cap / 1_000_000.0))
+                }
+                "float_cap" => div().child(format!("{:.2} M", self.quote.float_cap / 1_000_000.0)),
+                "turnover" => div().child(format!("{:.2} M", self.quote.turnover / 1_000_000.0)),
+                "volume" => div().child(format!("{:.2} M", self.quote.volume as f64 / 1_000_000.0)),
+                "turnover_ratio" => div().child(format!("{:.2}%", self.quote.turnover_ratio())),
+                "pe" => div().child(format!("{:.2}", self.quote.pe)),
+                "pb" => div().child(format!("{:.2}", self.quote.pb)),
+                "shares" => div().child(format!("{:.2}", self.quote.shares)),
+                "dividend" => div().child(format!("{:.2}", self.quote.dividend)),
+                "yield" => div().child(format!("{:.2}%", self.quote.dividend_yield)),
+                "dividend_per_share" => {
+                    div().child(format!("{:.2}", self.quote.dividend_per_share))
+                }
+                "dividend_date" => div().child(format!("{}", self.quote.dividend_date)),
+                "dividend_payment" => div().child(format!("{:.2}", self.quote.dividend_payment)),
+                _ => div().child("--"),
+            })
+    }
+}
+
+const FIELDS: [(&str, f32); 24] = [
+    ("id", 64.),
+    ("symbol", 64.),
+    ("name", 180.),
+    ("last_done", 80.),
+    ("prev_close", 80.),
+    ("open", 80.),
+    ("low", 80.),
+    ("high", 80.),
+    ("ttm", 50.),
+    ("market_cap", 96.),
+    ("float_cap", 96.),
+    ("turnover", 120.),
+    ("volume", 100.),
+    ("turnover_ratio", 96.),
+    ("pe", 64.),
+    ("pb", 64.),
+    ("eps", 64.),
+    ("shares", 96.),
+    ("dividend", 64.),
+    ("yield", 64.),
+    ("dividend_per_share", 64.),
+    ("dividend_date", 96.),
+    ("dividend_payment", 64.),
+    ("timestamp", 120.),
+];
+
+impl RenderOnce for TableRow {
+    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+        let color = self.quote.change_color();
+        div()
+            .flex()
+            .flex_row()
+            .border_b_1()
+            .border_color(rgb(0xE0E0E0))
+            .bg(if self.ix % 2 == 0 {
+                rgb(0xFFFFFF)
+            } else {
+                rgb(0xFAFAFA)
+            })
+            .py_0p5()
+            .px_2()
+            .children(FIELDS.map(|(key, width)| self.render_cell(key, px(width), color)))
+    }
+}
+
+struct DataTable {
+    /// Use `Rc` to share the same quote data across multiple items, avoid cloning.
+    quotes: Vec<Rc<Quote>>,
+    visible_range: Range<usize>,
+    scroll_handle: UniformListScrollHandle,
+    /// The position in thumb bounds when dragging start mouse down.
+    drag_position: Option<Point<Pixels>>,
+}
+
+impl DataTable {
+    fn new() -> Self {
+        Self {
+            quotes: Vec::new(),
+            visible_range: 0..0,
+            scroll_handle: UniformListScrollHandle::new(),
+            drag_position: None,
+        }
+    }
+
+    fn generate(&mut self) {
+        self.quotes = (0..TOTAL_ITEMS).map(|_| Rc::new(Quote::random())).collect();
+    }
+
+    fn table_bounds(&self) -> Bounds<Pixels> {
+        self.scroll_handle.0.borrow().base_handle.bounds()
+    }
+
+    fn scroll_top(&self) -> Pixels {
+        self.scroll_handle.0.borrow().base_handle.offset().y
+    }
+
+    fn scroll_height(&self) -> Pixels {
+        self.scroll_handle
+            .0
+            .borrow()
+            .last_item_size
+            .unwrap_or_default()
+            .contents
+            .height
+    }
+
+    fn render_scrollbar(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let scroll_height = self.scroll_height();
+        let table_bounds = self.table_bounds();
+        let table_height = table_bounds.size.height;
+        if table_height == px(0.) {
+            return div().id("scrollbar");
+        }
+
+        let percentage = -self.scroll_top() / scroll_height;
+        let offset_top = (table_height * percentage).clamp(
+            px(4.),
+            (table_height - SCROLLBAR_THUMB_HEIGHT - px(4.)).max(px(4.)),
+        );
+        let entity = cx.entity();
+        let scroll_handle = self.scroll_handle.0.borrow().base_handle.clone();
+
+        div()
+            .id("scrollbar")
+            .absolute()
+            .top(offset_top)
+            .right_1()
+            .h(SCROLLBAR_THUMB_HEIGHT)
+            .w(SCROLLBAR_THUMB_WIDTH)
+            .bg(rgb(0xC0C0C0))
+            .hover(|this| this.bg(rgb(0xA0A0A0)))
+            .rounded_lg()
+            .child(
+                canvas(
+                    |_, _, _| (),
+                    move |thumb_bounds, _, window, _| {
+                        window.on_mouse_event({
+                            let entity = entity.clone();
+                            move |ev: &MouseDownEvent, _, _, cx| {
+                                if !thumb_bounds.contains(&ev.position) {
+                                    return;
+                                }
+
+                                entity.update(cx, |this, _| {
+                                    this.drag_position = Some(
+                                        ev.position - thumb_bounds.origin - table_bounds.origin,
+                                    );
+                                })
+                            }
+                        });
+                        window.on_mouse_event({
+                            let entity = entity.clone();
+                            move |_: &MouseUpEvent, _, _, cx| {
+                                entity.update(cx, |this, _| {
+                                    this.drag_position = None;
+                                })
+                            }
+                        });
+
+                        window.on_mouse_event(move |ev: &MouseMoveEvent, _, _, cx| {
+                            if !ev.dragging() {
+                                return;
+                            }
+
+                            let Some(drag_pos) = entity.read(cx).drag_position else {
+                                return;
+                            };
+
+                            let inside_offset = drag_pos.y;
+                            let percentage = ((ev.position.y - table_bounds.origin.y
+                                + inside_offset)
+                                / (table_bounds.size.height))
+                                .clamp(0., 1.);
+
+                            let offset_y = ((scroll_height - table_bounds.size.height)
+                                * percentage)
+                                .clamp(px(0.), scroll_height - SCROLLBAR_THUMB_HEIGHT);
+                            scroll_handle.set_offset(point(px(0.), -offset_y));
+                            cx.notify(entity.entity_id());
+                        })
+                    },
+                )
+                .size_full(),
+            )
+    }
+}
+
+impl Render for DataTable {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let entity = cx.entity();
+
+        div()
+            .font_family(".SystemUIFont")
+            .bg(gpui::white())
+            .text_sm()
+            .size_full()
+            .p_4()
+            .gap_2()
+            .flex()
+            .flex_col()
+            .child(format!(
+                "Total {} items, visible range: {:?}",
+                self.quotes.len(),
+                self.visible_range
+            ))
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
+                    .flex_1()
+                    .overflow_hidden()
+                    .border_1()
+                    .border_color(rgb(0xE0E0E0))
+                    .rounded_md()
+                    .child(
+                        div()
+                            .flex()
+                            .flex_row()
+                            .w_full()
+                            .overflow_hidden()
+                            .border_b_1()
+                            .border_color(rgb(0xE0E0E0))
+                            .text_color(rgb(0x555555))
+                            .bg(rgb(0xF0F0F0))
+                            .py_1()
+                            .px_2()
+                            .text_xs()
+                            .children(FIELDS.map(|(key, width)| {
+                                div()
+                                    .whitespace_nowrap()
+                                    .flex_shrink_0()
+                                    .truncate()
+                                    .px_1()
+                                    .w(px(width))
+                                    .child(key.replace("_", " ").to_uppercase())
+                            })),
+                    )
+                    .child(
+                        div()
+                            .relative()
+                            .size_full()
+                            .child(
+                                uniform_list(entity, "items", self.quotes.len(), {
+                                    move |this, range, _, _| {
+                                        this.visible_range = range.clone();
+                                        let mut items = Vec::with_capacity(range.end - range.start);
+                                        for i in range {
+                                            if let Some(quote) = this.quotes.get(i) {
+                                                items.push(TableRow::new(i, quote.clone()));
+                                            }
+                                        }
+                                        items
+                                    }
+                                })
+                                .size_full()
+                                .track_scroll(self.scroll_handle.clone()),
+                            )
+                            .child(self.render_scrollbar(window, cx)),
+                    ),
+            )
+    }
+}
+
+fn main() {
+    Application::new().run(|cx: &mut App| {
+        cx.open_window(
+            WindowOptions {
+                focus: true,
+                window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
+                    None,
+                    size(px(1280.0), px(1000.0)),
+                    cx,
+                ))),
+                ..Default::default()
+            },
+            |_, cx| {
+                cx.new(|_| {
+                    let mut table = DataTable::new();
+                    table.generate();
+                    table
+                })
+            },
+        )
+        .unwrap();
+
+        cx.activate(true);
+    });
+}

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

@@ -1,4 +1,4 @@
-use gpui::{App, Context, Entity};
+use gpui::{App, Context, Entity, SharedString};
 use language::Buffer;
 use project::Project;
 use std::ops::Range;
@@ -15,6 +15,8 @@ pub enum Direction {
 
 #[derive(Clone)]
 pub struct InlineCompletion {
+    /// The ID of the completion, if it has one.
+    pub id: Option<SharedString>,
     pub edits: Vec<(Range<language::Anchor>, String)>,
     pub edit_preview: Option<language::EditPreview>,
 }
@@ -22,7 +24,7 @@ pub struct InlineCompletion {
 pub enum DataCollectionState {
     /// The provider doesn't support data collection.
     Unsupported,
-    /// Data collection is enabled
+    /// Data collection is enabled.
     Enabled,
     /// Data collection is disabled or unanswered.
     Disabled,

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

@@ -1,7 +1,11 @@
 use anyhow::Result;
 use client::UserStore;
 use copilot::{Copilot, Status};
-use editor::{actions::ShowEditPrediction, scroll::Autoscroll, Editor};
+use editor::{
+    actions::{ShowEditPrediction, ToggleEditPrediction},
+    scroll::Autoscroll,
+    Editor,
+};
 use feature_flags::{
     FeatureFlagAppExt, PredictEditsFeatureFlag, PredictEditsRateCompletionsFeatureFlag,
 };
@@ -44,6 +48,7 @@ struct CopilotErrorToast;
 pub struct InlineCompletionButton {
     editor_subscription: Option<(Subscription, usize)>,
     editor_enabled: Option<bool>,
+    editor_show_predictions: bool,
     editor_focus_handle: Option<FocusHandle>,
     language: Option<Arc<Language>>,
     file: Option<Arc<dyn File>>,
@@ -137,9 +142,12 @@ impl Render for InlineCompletionButton {
                             })
                         })
                         .anchor(Corner::BottomRight)
-                        .trigger(IconButton::new("copilot-icon", icon).tooltip(|window, cx| {
-                            Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx)
-                        }))
+                        .trigger_with_tooltip(
+                            IconButton::new("copilot-icon", icon),
+                            |window, cx| {
+                                Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx)
+                            },
+                        )
                         .with_handle(self.popover_menu_handle.clone()),
                 )
             }
@@ -206,7 +214,8 @@ impl Render for InlineCompletionButton {
                             _ => None,
                         })
                         .anchor(Corner::BottomRight)
-                        .trigger(IconButton::new("supermaven-icon", icon).tooltip(
+                        .trigger_with_tooltip(
+                            IconButton::new("supermaven-icon", icon),
                             move |window, cx| {
                                 if has_menu {
                                     Tooltip::for_action(
@@ -219,7 +228,7 @@ impl Render for InlineCompletionButton {
                                     Tooltip::text(tooltip_text.clone())(window, cx)
                                 }
                             },
-                        ))
+                        )
                         .with_handle(self.popover_menu_handle.clone()),
                 );
             }
@@ -275,15 +284,29 @@ impl Render for InlineCompletionButton {
                     );
                 }
 
+                let show_editor_predictions = self.editor_show_predictions;
+
                 let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
                     .shape(IconButtonShape::Square)
+                    .when(enabled && !show_editor_predictions, |this| {
+                        this.indicator(Indicator::dot().color(Color::Muted))
+                            .indicator_border_color(Some(cx.theme().colors().status_bar_background))
+                    })
                     .when(!self.popover_menu_handle.is_deployed(), |element| {
-                        if enabled {
-                            element.tooltip(|window, cx| {
-                                Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
-                            })
-                        } else {
-                            element.tooltip(|window, cx| {
+                        element.tooltip(move |window, cx| {
+                            if enabled {
+                                if show_editor_predictions {
+                                    Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx)
+                                } else {
+                                    Tooltip::with_meta(
+                                        "Edit Prediction",
+                                        Some(&ToggleMenu),
+                                        "Hidden For This File",
+                                        window,
+                                        cx,
+                                    )
+                                }
+                            } else {
                                 Tooltip::with_meta(
                                     "Edit Prediction",
                                     Some(&ToggleMenu),
@@ -291,8 +314,8 @@ impl Render for InlineCompletionButton {
                                     window,
                                     cx,
                                 )
-                            })
-                        }
+                            }
+                        })
                     });
 
                 let this = cx.entity().clone();
@@ -347,6 +370,7 @@ impl InlineCompletionButton {
         Self {
             editor_subscription: None,
             editor_enabled: None,
+            editor_show_predictions: true,
             editor_focus_handle: None,
             language: None,
             file: None,
@@ -384,6 +408,21 @@ impl InlineCompletionButton {
 
         menu = menu.header("Show Edit Predictions For");
 
+        if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
+            menu = menu.toggleable_entry(
+                "This File",
+                self.editor_show_predictions,
+                IconPosition::Start,
+                Some(Box::new(ToggleEditPrediction)),
+                {
+                    let editor_focus_handle = editor_focus_handle.clone();
+                    move |window, cx| {
+                        editor_focus_handle.dispatch_action(&ToggleEditPrediction, window, cx);
+                    }
+                },
+            );
+        }
+
         if let Some(language) = self.language.clone() {
             let fs = fs.clone();
             let language_enabled =
@@ -393,7 +432,7 @@ impl InlineCompletionButton {
             menu = menu.toggleable_entry(
                 language.name(),
                 language_enabled,
-                IconPosition::End,
+                IconPosition::Start,
                 None,
                 move |_, cx| {
                     toggle_show_inline_completions_for_language(language.clone(), fs.clone(), cx)
@@ -406,7 +445,7 @@ impl InlineCompletionButton {
         menu = menu.toggleable_entry(
             "All Files",
             globally_enabled,
-            IconPosition::End,
+            IconPosition::Start,
             None,
             move |_, cx| toggle_inline_completions_globally(fs.clone(), cx),
         );
@@ -422,7 +461,7 @@ impl InlineCompletionButton {
                     // TODO: We want to add something later that communicates whether
                     // the current project is open-source.
                     ContextMenuEntry::new("Share Training Data")
-                        .toggleable(IconPosition::End, data_collection.is_enabled())
+                        .toggleable(IconPosition::Start, data_collection.is_enabled())
                         .documentation_aside(|_| {
                             Label::new(indoc!{"
                                 Help us improve our open model by sharing data from open source repositories. \
@@ -450,6 +489,8 @@ impl InlineCompletionButton {
 
         menu = menu.item(
             ContextMenuEntry::new("Configure Excluded Files")
+                .icon(IconName::LockOutlined)
+                .icon_color(Color::Muted)
                 .documentation_aside(|_| {
                     Label::new(indoc!{"
                         Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element()
@@ -486,7 +527,6 @@ impl InlineCompletionButton {
                     Some(Box::new(ShowEditPrediction)),
                     {
                         let editor_focus_handle = editor_focus_handle.clone();
-
                         move |window, cx| {
                             editor_focus_handle.dispatch_action(&ShowEditPrediction, window, cx);
                         }
@@ -571,6 +611,7 @@ impl InlineCompletionButton {
                 .unwrap_or(true),
             )
         };
+        self.editor_show_predictions = editor.should_show_inline_completions(cx);
         self.edit_prediction_provider = editor.edit_prediction_provider();
         self.language = language.cloned();
         self.file = file;

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

@@ -2,7 +2,7 @@ use std::sync::Arc;
 
 use feature_flags::ZedPro;
 use gpui::{
-    Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+    Action, AnyElement, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
     Subscription, Task, WeakEntity,
 };
 use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
@@ -115,20 +115,31 @@ impl Render for LanguageModelSelector {
 }
 
 #[derive(IntoElement)]
-pub struct LanguageModelSelectorPopoverMenu<T>
+pub struct LanguageModelSelectorPopoverMenu<T, TT>
 where
-    T: PopoverTrigger,
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
 {
     language_model_selector: Entity<LanguageModelSelector>,
     trigger: T,
+    tooltip: TT,
     handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
 }
 
-impl<T: PopoverTrigger> LanguageModelSelectorPopoverMenu<T> {
-    pub fn new(language_model_selector: Entity<LanguageModelSelector>, trigger: T) -> Self {
+impl<T, TT> LanguageModelSelectorPopoverMenu<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
+    pub fn new(
+        language_model_selector: Entity<LanguageModelSelector>,
+        trigger: T,
+        tooltip: TT,
+    ) -> Self {
         Self {
             language_model_selector,
             trigger,
+            tooltip,
             handle: None,
         }
     }
@@ -139,13 +150,17 @@ impl<T: PopoverTrigger> LanguageModelSelectorPopoverMenu<T> {
     }
 }
 
-impl<T: PopoverTrigger> RenderOnce for LanguageModelSelectorPopoverMenu<T> {
+impl<T, TT> RenderOnce for LanguageModelSelectorPopoverMenu<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
         let language_model_selector = self.language_model_selector.clone();
 
         PopoverMenu::new("model-switcher")
             .menu(move |_window, _cx| Some(language_model_selector.clone()))
-            .trigger(self.trigger)
+            .trigger_with_tooltip(self.trigger, self.tooltip)
             .anchor(gpui::Corner::BottomRight)
             .when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
             .offset(gpui::Point {

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

@@ -293,7 +293,7 @@ impl SyntaxTreeView {
 
 impl Render for SyntaxTreeView {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let mut rendered = div().flex_1();
+        let mut rendered = div().flex_1().bg(cx.theme().colors().editor_background);
 
         if let Some(layer) = self
             .editor

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

@@ -7320,6 +7320,7 @@ impl ToOffset for Point {
 }
 
 impl ToOffset for usize {
+    #[track_caller]
     fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
         assert!(*self <= snapshot.len(), "offset is out of range");
         *self

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

@@ -2,10 +2,14 @@ mod outline_panel_settings;
 
 use std::{
     cmp,
+    collections::BTreeMap,
     hash::Hash,
     ops::Range,
     path::{Path, PathBuf, MAIN_SEPARATOR_STR},
-    sync::{atomic::AtomicBool, Arc, OnceLock},
+    sync::{
+        atomic::{self, AtomicBool},
+        Arc, OnceLock,
+    },
     time::Duration,
     u32,
 };
@@ -103,6 +107,7 @@ pub struct OutlinePanel {
     active_item: Option<ActiveItem>,
     _subscriptions: Vec<Subscription>,
     updating_fs_entries: bool,
+    updating_cached_entries: bool,
     new_entries_for_fs_update: HashSet<ExcerptId>,
     fs_entries_update_task: Task<()>,
     cached_entries_update_task: Task<()>,
@@ -777,7 +782,10 @@ impl OutlinePanel {
                                 excerpt.invalidate_outlines();
                             }
                         }
-                        outline_panel.update_non_fs_items(window, cx);
+                        let update_cached_items = outline_panel.update_non_fs_items(window, cx);
+                        if update_cached_items {
+                            outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
+                        }
                     } else if &outline_panel_settings != new_settings {
                         outline_panel_settings = *new_settings;
                         cx.notify();
@@ -814,6 +822,7 @@ impl OutlinePanel {
                 active_item: None,
                 pending_serialization: Task::ready(None),
                 updating_fs_entries: false,
+                updating_cached_entries: false,
                 new_entries_for_fs_update: HashSet::default(),
                 preserve_selection_on_buffer_fold_toggles: HashSet::default(),
                 fs_entries_update_task: Task::ready(()),
@@ -922,7 +931,7 @@ impl OutlinePanel {
             cx.propagate()
         } else if let Some(selected_entry) = self.selected_entry().cloned() {
             self.toggle_expanded(&selected_entry, window, cx);
-            self.scroll_editor_to_entry(&selected_entry, true, false, window, cx);
+            self.scroll_editor_to_entry(&selected_entry, true, true, window, cx);
         }
     }
 
@@ -977,7 +986,7 @@ impl OutlinePanel {
         &mut self,
         entry: &PanelEntry,
         prefer_selection_change: bool,
-        change_focus: bool,
+        prefer_focus_change: bool,
         window: &mut Window,
         cx: &mut Context<OutlinePanel>,
     ) {
@@ -987,9 +996,13 @@ impl OutlinePanel {
         let active_multi_buffer = active_editor.read(cx).buffer().clone();
         let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
         let mut change_selection = prefer_selection_change;
+        let mut change_focus = prefer_focus_change;
         let mut scroll_to_buffer = None;
         let scroll_target = match entry {
-            PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => None,
+            PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => {
+                change_focus = false;
+                None
+            }
             PanelEntry::Fs(FsEntry::ExternalFile(file)) => {
                 change_selection = false;
                 scroll_to_buffer = Some(file.buffer_id);
@@ -1033,6 +1046,7 @@ impl OutlinePanel {
                 }),
             PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
                 change_selection = false;
+                change_focus = false;
                 multi_buffer_snapshot.anchor_in_excerpt(excerpt.id, excerpt.range.context.start)
             }
             PanelEntry::Search(search_entry) => Some(search_entry.match_range.start),
@@ -2613,7 +2627,7 @@ impl OutlinePanel {
                 .spawn(async move {
                     let mut processed_external_buffers = HashSet::default();
                     let mut new_worktree_entries =
-                        HashMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
+                        BTreeMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
                     let mut worktree_excerpts = HashMap::<
                         WorktreeId,
                         HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
@@ -2896,8 +2910,8 @@ impl OutlinePanel {
                     outline_panel.fs_entries = new_fs_entries;
                     outline_panel.fs_entries_depth = new_depth_map;
                     outline_panel.fs_children_count = new_children_count;
-                    outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
                     outline_panel.update_non_fs_items(window, cx);
+                    outline_panel.update_cached_entries(debounce, window, cx);
 
                     cx.notify();
                 })
@@ -2922,7 +2936,11 @@ impl OutlinePanel {
              window: &mut Window,
              cx: &mut Context<Self>| {
                 if matches!(e, SearchEvent::MatchesInvalidated) {
-                    outline_panel.update_search_matches(window, cx);
+                    let update_cached_items = outline_panel.update_search_matches(window, cx);
+                    if update_cached_items {
+                        outline_panel.selected_entry.invalidate();
+                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
+                    }
                 };
                 outline_panel.autoscroll(cx);
             },
@@ -3188,10 +3206,12 @@ impl OutlinePanel {
         }
 
         let syntax_theme = cx.theme().syntax().clone();
+        let first_update = Arc::new(AtomicBool::new(true));
         for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
             for (excerpt_id, excerpt_range) in excerpt_ranges {
                 let syntax_theme = syntax_theme.clone();
                 let buffer_snapshot = buffer_snapshot.clone();
+                let first_update = first_update.clone();
                 self.outline_fetch_tasks.insert(
                     (buffer_id, excerpt_id),
                     cx.spawn_in(window, |outline_panel, mut cx| async move {
@@ -3215,13 +3235,16 @@ impl OutlinePanel {
                                     .or_default()
                                     .get_mut(&excerpt_id)
                                 {
+                                    let debounce = if first_update
+                                        .fetch_and(false, atomic::Ordering::AcqRel)
+                                    {
+                                        None
+                                    } else {
+                                        Some(UPDATE_DEBOUNCE)
+                                    };
                                     excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
+                                    outline_panel.update_cached_entries(debounce, window, cx);
                                 }
-                                outline_panel.update_cached_entries(
-                                    Some(UPDATE_DEBOUNCE),
-                                    window,
-                                    cx,
-                                );
                             })
                             .ok();
                     }),
@@ -3376,6 +3399,7 @@ impl OutlinePanel {
 
         let is_singleton = self.is_singleton_active(cx);
         let query = self.query(cx);
+        self.updating_cached_entries = true;
         self.cached_entries_update_task = cx.spawn_in(window, |outline_panel, mut cx| async move {
             if let Some(debounce) = debounce {
                 cx.background_executor().timer(debounce).await;
@@ -3410,6 +3434,7 @@ impl OutlinePanel {
                     }
 
                     outline_panel.autoscroll(cx);
+                    outline_panel.updating_cached_entries = false;
                     cx.notify();
                 })
                 .ok();
@@ -3468,7 +3493,8 @@ impl OutlinePanel {
                                 .copied()
                                 .unwrap_or(0);
                             while let Some(parent) = parent_dirs.last() {
-                                if directory_entry.entry.path.starts_with(&parent.path) {
+                                if !is_root && directory_entry.entry.path.starts_with(&parent.path)
+                                {
                                     break;
                                 }
                                 parent_dirs.pop();
@@ -3915,19 +3941,27 @@ impl OutlinePanel {
         !self.collapsed_entries.contains(&entry_to_check)
     }
 
-    fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) {
+    fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) -> bool {
         if !self.active {
-            return;
+            return false;
         }
 
-        self.update_search_matches(window, cx);
+        let mut update_cached_items = false;
+        update_cached_items |= self.update_search_matches(window, cx);
         self.fetch_outdated_outlines(window, cx);
-        self.autoscroll(cx);
+        if update_cached_items {
+            self.selected_entry.invalidate();
+        }
+        update_cached_items
     }
 
-    fn update_search_matches(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) {
+    fn update_search_matches(
+        &mut self,
+        window: &mut Window,
+        cx: &mut Context<OutlinePanel>,
+    ) -> bool {
         if !self.active {
-            return;
+            return false;
         }
 
         let project_search = self
@@ -4010,10 +4044,7 @@ impl OutlinePanel {
                 cx,
             ));
         }
-        if update_cached_entries {
-            self.selected_entry.invalidate();
-            self.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
-        }
+        update_cached_entries
     }
 
     #[allow(clippy::too_many_arguments)]
@@ -4426,41 +4457,42 @@ impl OutlinePanel {
         cx: &mut Context<Self>,
     ) -> Div {
         let contents = if self.cached_entries.is_empty() {
-            let header = if self.updating_fs_entries {
-                "Loading outlines"
+            let header = if self.updating_fs_entries || self.updating_cached_entries {
+                None
             } else if query.is_some() {
-                "No matches for query"
+                Some("No matches for query")
             } else {
-                "No outlines available"
+                Some("No outlines available")
             };
 
             v_flex()
                 .flex_1()
                 .justify_center()
                 .size_full()
-                .child(h_flex().justify_center().child(Label::new(header)))
-                .when_some(query.clone(), |panel, query| {
-                    panel.child(h_flex().justify_center().child(Label::new(query)))
+                .when_some(header, |panel, header| {
+                    panel
+                        .child(h_flex().justify_center().child(Label::new(header)))
+                        .when_some(query.clone(), |panel, query| {
+                            panel.child(h_flex().justify_center().child(Label::new(query)))
+                        })
+                        .child(
+                            h_flex()
+                                .pt(DynamicSpacing::Base04.rems(cx))
+                                .justify_center()
+                                .child({
+                                    let keystroke =
+                                        match self.position(window, cx) {
+                                            DockPosition::Left => window
+                                                .keystroke_text_for(&workspace::ToggleLeftDock),
+                                            DockPosition::Bottom => window
+                                                .keystroke_text_for(&workspace::ToggleBottomDock),
+                                            DockPosition::Right => window
+                                                .keystroke_text_for(&workspace::ToggleRightDock),
+                                        };
+                                    Label::new(format!("Toggle this panel with {keystroke}"))
+                                }),
+                        )
                 })
-                .child(
-                    h_flex()
-                        .pt(DynamicSpacing::Base04.rems(cx))
-                        .justify_center()
-                        .child({
-                            let keystroke = match self.position(window, cx) {
-                                DockPosition::Left => {
-                                    window.keystroke_text_for(&workspace::ToggleLeftDock)
-                                }
-                                DockPosition::Bottom => {
-                                    window.keystroke_text_for(&workspace::ToggleBottomDock)
-                                }
-                                DockPosition::Right => {
-                                    window.keystroke_text_for(&workspace::ToggleRightDock)
-                                }
-                            };
-                            Label::new(format!("Toggle this panel with {keystroke}"))
-                        }),
-                )
         } else {
             let list_contents = {
                 let items_len = self.cached_entries.len();
@@ -4995,11 +5027,17 @@ fn subscribe_for_editor_events(
                 }
                 EditorEvent::ExcerptsExpanded { ids } => {
                     outline_panel.invalidate_outlines(ids);
-                    outline_panel.update_non_fs_items(window, cx);
+                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
+                    if update_cached_items {
+                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
+                    }
                 }
                 EditorEvent::ExcerptsEdited { ids } => {
                     outline_panel.invalidate_outlines(ids);
-                    outline_panel.update_non_fs_items(window, cx);
+                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
+                    if update_cached_items {
+                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
+                    }
                 }
                 EditorEvent::BufferFoldToggled { ids, .. } => {
                     outline_panel.invalidate_outlines(ids);
@@ -5073,7 +5111,10 @@ fn subscribe_for_editor_events(
                             excerpt.invalidate_outlines();
                         }
                     }
-                    outline_panel.update_non_fs_items(window, cx);
+                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
+                    if update_cached_items {
+                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
+                    }
                 }
                 _ => {}
             }
@@ -5117,6 +5158,7 @@ mod tests {
     use project::FakeFs;
     use search::project_search::{self, perform_project_search};
     use serde_json::json;
+    use workspace::OpenVisible;
 
     use super::*;
 
@@ -5173,7 +5215,7 @@ mod tests {
                 });
         });
 
-        let all_matches = r#"/
+        let all_matches = r#"/rust-analyzer/
   crates/
     ide/src/
       inlay_hints/
@@ -5208,9 +5250,11 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 select_first_in_all_matches(
                     "search: match config.param_names_for_lifetime_elision_hints {"
@@ -5222,9 +5266,11 @@ mod tests {
             outline_panel.select_parent(&SelectParent, window, cx);
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 select_first_in_all_matches("fn_lifetime_fn.rs")
             );
@@ -5238,12 +5284,14 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 format!(
-                    r#"/
+                    r#"/rust-analyzer/
   crates/
     ide/src/
       inlay_hints/
@@ -5273,9 +5321,11 @@ mod tests {
             outline_panel.select_parent(&SelectParent, window, cx);
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 select_first_in_all_matches("inlay_hints/")
             );
@@ -5285,9 +5335,11 @@ mod tests {
             outline_panel.select_parent(&SelectParent, window, cx);
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 select_first_in_all_matches("ide/src/")
             );
@@ -5302,12 +5354,14 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 format!(
-                    r#"/
+                    r#"/rust-analyzer/
   crates/
     ide/src/{SELECTED_MARKER}
     rust-analyzer/src/
@@ -5328,9 +5382,11 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 select_first_in_all_matches("ide/src/")
             );
@@ -5387,7 +5443,7 @@ mod tests {
                     );
                 });
         });
-        let all_matches = r#"/
+        let all_matches = r#"/rust-analyzer/
   crates/
     ide/src/
       inlay_hints/
@@ -5414,9 +5470,11 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
                     None,
+                    cx,
                 ),
                 all_matches,
             );
@@ -5435,12 +5493,15 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
                     None,
+                    cx,
                 ),
                 all_matches
                     .lines()
+                    .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
                     .filter(|item| item.contains(filter_text))
                     .collect::<Vec<_>>()
                     .join("\n"),
@@ -5458,9 +5519,11 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
                     None,
+                    cx,
                 ),
                 all_matches,
             );
@@ -5517,7 +5580,7 @@ mod tests {
                     );
                 });
         });
-        let all_matches = r#"/
+        let all_matches = r#"/rust-analyzer/
   crates/
     ide/src/
       inlay_hints/
@@ -5559,9 +5622,11 @@ mod tests {
         outline_panel.update_in(cx, |outline_panel, window, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
+                    cx,
                 ),
                 select_first_in_all_matches(initial_outline_selection)
             );
@@ -5580,9 +5645,11 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
+                    cx,
                 ),
                 select_first_in_all_matches(navigated_outline_selection)
             );
@@ -5616,9 +5683,11 @@ mod tests {
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
+                    cx,
                 ),
                 select_first_in_all_matches(next_navigated_outline_selection)
             );
@@ -5651,9 +5720,11 @@ mod tests {
             );
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
                     outline_panel.selected_entry(),
+                    cx,
                 ),
                 "fn_lifetime_fn.rs  <==== selected"
             );
@@ -5665,6 +5736,176 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_multiple_workrees(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.background_executor.clone());
+        fs.insert_tree(
+            "/root",
+            json!({
+                "one": {
+                    "a.txt": "aaa aaa"
+                },
+                "two": {
+                    "b.txt": "a aaa"
+                }
+
+            }),
+        )
+        .await;
+        let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await;
+        let workspace = add_outline_panel(&project, cx).await;
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let outline_panel = outline_panel(&workspace, cx);
+        outline_panel.update_in(cx, |outline_panel, window, cx| {
+            outline_panel.set_active(true, window, cx)
+        });
+
+        let items = workspace
+            .update(cx, |workspace, window, cx| {
+                workspace.open_paths(
+                    vec![PathBuf::from("/root/two")],
+                    OpenVisible::OnlyDirectories,
+                    None,
+                    window,
+                    cx,
+                )
+            })
+            .unwrap()
+            .await;
+        assert_eq!(items.len(), 1, "Were opening another worktree directory");
+        assert!(
+            items[0].is_none(),
+            "Directory should be opened successfully"
+        );
+
+        workspace
+            .update(cx, |workspace, window, cx| {
+                ProjectSearchView::deploy_search(
+                    workspace,
+                    &workspace::DeploySearch::default(),
+                    window,
+                    cx,
+                )
+            })
+            .unwrap();
+        let search_view = workspace
+            .update(cx, |workspace, _, cx| {
+                workspace
+                    .active_pane()
+                    .read(cx)
+                    .items()
+                    .find_map(|item| item.downcast::<ProjectSearchView>())
+                    .expect("Project search view expected to appear after new search event trigger")
+            })
+            .unwrap();
+
+        let query = "aaa";
+        perform_project_search(&search_view, query, cx);
+        search_view.update(cx, |search_view, cx| {
+            search_view
+                .results_editor()
+                .update(cx, |results_editor, cx| {
+                    assert_eq!(
+                        results_editor.display_text(cx).match_indices(query).count(),
+                        3
+                    );
+                });
+        });
+
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+        cx.run_until_parked();
+        outline_panel.update(cx, |outline_panel, cx| {
+            assert_eq!(
+                display_entries(
+                    &project,
+                    &snapshot(&outline_panel, cx),
+                    &outline_panel.cached_entries,
+                    outline_panel.selected_entry(),
+                    cx,
+                ),
+                r#"/root/one/
+  a.txt
+    search: aaa aaa  <==== selected
+    search: aaa aaa
+/root/two/
+  b.txt
+    search: a aaa"#
+            );
+        });
+
+        outline_panel.update_in(cx, |outline_panel, window, cx| {
+            outline_panel.select_prev(&SelectPrev, window, cx);
+            outline_panel.open(&Open, window, cx);
+        });
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+        cx.run_until_parked();
+        outline_panel.update(cx, |outline_panel, cx| {
+            assert_eq!(
+                display_entries(
+                    &project,
+                    &snapshot(&outline_panel, cx),
+                    &outline_panel.cached_entries,
+                    outline_panel.selected_entry(),
+                    cx,
+                ),
+                r#"/root/one/
+  a.txt  <==== selected
+/root/two/
+  b.txt
+    search: a aaa"#
+            );
+        });
+
+        outline_panel.update_in(cx, |outline_panel, window, cx| {
+            outline_panel.select_next(&SelectNext, window, cx);
+            outline_panel.open(&Open, window, cx);
+        });
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+        cx.run_until_parked();
+        outline_panel.update(cx, |outline_panel, cx| {
+            assert_eq!(
+                display_entries(
+                    &project,
+                    &snapshot(&outline_panel, cx),
+                    &outline_panel.cached_entries,
+                    outline_panel.selected_entry(),
+                    cx,
+                ),
+                r#"/root/one/
+  a.txt
+/root/two/  <==== selected"#
+            );
+        });
+
+        outline_panel.update_in(cx, |outline_panel, window, cx| {
+            outline_panel.open(&Open, window, cx);
+        });
+        cx.executor()
+            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
+        cx.run_until_parked();
+        outline_panel.update(cx, |outline_panel, cx| {
+            assert_eq!(
+                display_entries(
+                    &project,
+                    &snapshot(&outline_panel, cx),
+                    &outline_panel.cached_entries,
+                    outline_panel.selected_entry(),
+                    cx,
+                ),
+                r#"/root/one/
+  a.txt
+/root/two/  <==== selected
+  b.txt
+    search: a aaa"#
+            );
+        });
+    }
+
     #[gpui::test]
     async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
         init_test(cx);
@@ -5730,9 +5971,11 @@ struct OutlineEntryExcerpt {
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 indoc!(
                     "
@@ -5755,9 +5998,11 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 indoc!(
                     "
@@ -5780,9 +6025,11 @@ outline: struct OutlineEntryExcerpt  <==== selected
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 indoc!(
                     "
@@ -5805,9 +6052,11 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 indoc!(
                     "
@@ -5830,9 +6079,11 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 indoc!(
                     "
@@ -5855,9 +6106,11 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 indoc!(
                     "
@@ -5880,9 +6133,11 @@ outline: struct OutlineEntryExcerpt  <==== selected
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 indoc!(
                     "
@@ -5905,9 +6160,11 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 indoc!(
                     "
@@ -5930,9 +6187,11 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 indoc!(
                     "
@@ -5955,9 +6214,11 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 indoc!(
                     "
@@ -5980,9 +6241,11 @@ outline: struct OutlineEntryExcerpt  <==== selected
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
                 indoc!(
                     "
@@ -6084,11 +6347,13 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
-                r#"/
+                r#"/frontend-project/
   public/lottie/
     syntax-tree.json
       search: { "something": "static" }  <==== selected
@@ -6119,11 +6384,13 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
-                r#"/
+                r#"/frontend-project/
   public/lottie/
     syntax-tree.json
       search: { "something": "static" }
@@ -6145,11 +6412,13 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
-                r#"/
+                r#"/frontend-project/
   public/lottie/
     syntax-tree.json
       search: { "something": "static" }
@@ -6175,11 +6444,13 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
-                r#"/
+                r#"/frontend-project/
   public/lottie/
     syntax-tree.json
       search: { "something": "static" }
@@ -6204,11 +6475,13 @@ outline: struct OutlineEntryExcerpt
         outline_panel.update(cx, |outline_panel, cx| {
             assert_eq!(
                 display_entries(
+                    &project,
                     &snapshot(&outline_panel, cx),
                     &outline_panel.cached_entries,
-                    outline_panel.selected_entry()
+                    outline_panel.selected_entry(),
+                    cx,
                 ),
-                r#"/
+                r#"/frontend-project/
   public/lottie/
     syntax-tree.json
       search: { "something": "static" }
@@ -6255,9 +6528,11 @@ outline: struct OutlineEntryExcerpt
     }
 
     fn display_entries(
+        project: &Entity<Project>,
         multi_buffer_snapshot: &MultiBufferSnapshot,
         cached_entries: &[CachedEntry],
         selected_entry: Option<&PanelEntry>,
+        cx: &mut App,
     ) -> String {
         let mut display_string = String::new();
         for entry in cached_entries {
@@ -6272,15 +6547,33 @@ outline: struct OutlineEntryExcerpt
                     FsEntry::ExternalFile(_) => {
                         panic!("Did not cover external files with tests")
                     }
-                    FsEntry::Directory(directory) => format!(
-                        "{}/",
-                        directory
-                            .entry
-                            .path
-                            .file_name()
-                            .map(|name| name.to_string_lossy().to_string())
-                            .unwrap_or_default()
-                    ),
+                    FsEntry::Directory(directory) => {
+                        match project
+                            .read(cx)
+                            .worktree_for_id(directory.worktree_id, cx)
+                            .and_then(|worktree| {
+                                if worktree.read(cx).root_entry() == Some(&directory.entry.entry) {
+                                    Some(worktree.read(cx).abs_path())
+                                } else {
+                                    None
+                                }
+                            }) {
+                            Some(root_path) => format!(
+                                "{}/{}",
+                                root_path.display(),
+                                directory.entry.path.display(),
+                            ),
+                            None => format!(
+                                "{}/",
+                                directory
+                                    .entry
+                                    .path
+                                    .file_name()
+                                    .unwrap_or_default()
+                                    .to_string_lossy()
+                            ),
+                        }
+                    }
                     FsEntry::File(file) => file
                         .entry
                         .path

crates/panel/Cargo.toml πŸ”—

@@ -12,6 +12,9 @@ workspace = true
 path = "src/panel.rs"
 
 [dependencies]
+editor.workspace = true
 gpui.workspace = true
+settings.workspace = true
+theme.workspace = true
 ui.workspace = true
 workspace.workspace = true

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

@@ -1,5 +1,8 @@
 //! # panel
-use gpui::actions;
+use editor::{Editor, EditorElement, EditorStyle};
+use gpui::{actions, Entity, TextStyle};
+use settings::Settings;
+use theme::ThemeSettings;
 use ui::{prelude::*, Tab};
 
 actions!(panel, [NextPanelTab, PreviousPanelTab]);
@@ -46,7 +49,8 @@ pub fn panel_button(label: impl Into<SharedString>) -> ui::Button {
     let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into());
     ui::Button::new(id, label)
         .label_size(ui::LabelSize::Small)
-        .layer(ui::ElevationIndex::Surface)
+        // TODO: Change this once we use on_surface_bg in button_like
+        .layer(ui::ElevationIndex::ModalSurface)
         .size(ui::ButtonSize::Compact)
 }
 
@@ -57,10 +61,65 @@ pub fn panel_filled_button(label: impl Into<SharedString>) -> ui::Button {
 pub fn panel_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
     let id = ElementId::Name(id.into());
     ui::IconButton::new(id, icon)
-        .layer(ui::ElevationIndex::Surface)
+        // TODO: Change this once we use on_surface_bg in button_like
+        .layer(ui::ElevationIndex::ModalSurface)
         .size(ui::ButtonSize::Compact)
 }
 
 pub fn panel_filled_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
     panel_icon_button(id, icon).style(ui::ButtonStyle::Filled)
 }
+
+pub fn panel_editor_container(_window: &mut Window, cx: &mut App) -> Div {
+    v_flex()
+        .size_full()
+        .gap(px(8.))
+        .p_2()
+        .bg(cx.theme().colors().editor_background)
+}
+
+pub fn panel_editor_style(monospace: bool, window: &mut Window, cx: &mut App) -> EditorStyle {
+    let settings = ThemeSettings::get_global(cx);
+
+    let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
+
+    let (font_family, font_features, font_weight, line_height) = if monospace {
+        (
+            settings.buffer_font.family.clone(),
+            settings.buffer_font.features.clone(),
+            settings.buffer_font.weight,
+            font_size * settings.buffer_line_height.value(),
+        )
+    } else {
+        (
+            settings.ui_font.family.clone(),
+            settings.ui_font.features.clone(),
+            settings.ui_font.weight,
+            window.line_height(),
+        )
+    };
+
+    EditorStyle {
+        background: cx.theme().colors().editor_background,
+        local_player: cx.theme().players().local(),
+        text: TextStyle {
+            color: cx.theme().colors().text,
+            font_family,
+            font_features,
+            font_size: TextSize::Small.rems(cx).into(),
+            font_weight,
+            line_height: line_height.into(),
+            ..Default::default()
+        },
+        ..Default::default()
+    }
+}
+
+pub fn panel_editor_element(
+    editor: &Entity<Editor>,
+    monospace: bool,
+    window: &mut Window,
+    cx: &mut App,
+) -> EditorElement {
+    EditorElement::new(editor, panel_editor_style(monospace, window, cx))
+}

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

@@ -145,12 +145,24 @@ pub fn settings_file() -> &'static PathBuf {
     SETTINGS_FILE.get_or_init(|| config_dir().join("settings.json"))
 }
 
+/// Returns the path to the `settings_backup.json` file.
+pub fn settings_backup_file() -> &'static PathBuf {
+    static SETTINGS_FILE: OnceLock<PathBuf> = OnceLock::new();
+    SETTINGS_FILE.get_or_init(|| config_dir().join("settings_backup.json"))
+}
+
 /// Returns the path to the `keymap.json` file.
 pub fn keymap_file() -> &'static PathBuf {
     static KEYMAP_FILE: OnceLock<PathBuf> = OnceLock::new();
     KEYMAP_FILE.get_or_init(|| config_dir().join("keymap.json"))
 }
 
+/// Returns the path to the `keymap_backup.json` file.
+pub fn keymap_backup_file() -> &'static PathBuf {
+    static KEYMAP_FILE: OnceLock<PathBuf> = OnceLock::new();
+    KEYMAP_FILE.get_or_init(|| config_dir().join("keymap_backup.json"))
+}
+
 /// Returns the path to the `tasks.json` file.
 pub fn tasks_file() -> &'static PathBuf {
     static TASKS_FILE: OnceLock<PathBuf> = OnceLock::new();

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

@@ -15,7 +15,7 @@ use gpui::{
 use language::{Buffer, LanguageRegistry};
 use rpc::{proto, AnyProtoClient};
 use settings::WorktreeId;
-use std::path::Path;
+use std::path::{Path, PathBuf};
 use std::sync::Arc;
 use text::BufferId;
 use util::{maybe, ResultExt};
@@ -299,19 +299,25 @@ impl Repository {
         (self.worktree_id, self.repository_entry.work_directory_id())
     }
 
+    pub fn branch(&self) -> Option<Arc<str>> {
+        self.repository_entry.branch()
+    }
+
     pub fn display_name(&self, project: &Project, cx: &App) -> SharedString {
         maybe!({
-            let path = self.repo_path_to_project_path(&"".into())?;
-            Some(
-                project
-                    .absolute_path(&path, cx)?
-                    .file_name()?
-                    .to_string_lossy()
-                    .to_string()
-                    .into(),
-            )
+            let project_path = self.repo_path_to_project_path(&"".into())?;
+            let worktree_name = project
+                .worktree_for_id(project_path.worktree_id, cx)?
+                .read(cx)
+                .root_name();
+
+            let mut path = PathBuf::new();
+            path = path.join(worktree_name);
+            path = path.join(project_path.path);
+            Some(path.to_string_lossy().to_string())
         })
-        .unwrap_or("".into())
+        .unwrap_or_else(|| self.repository_entry.work_directory.display_name())
+        .into()
     }
 
     pub fn activate(&self, cx: &mut Context<Self>) {

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

@@ -1535,6 +1535,10 @@ impl Project {
         })
     }
 
+    /// Renames the project entry with given `entry_id`.
+    ///
+    /// `new_path` is a relative path to worktree root.
+    /// If root entry is renamed then its new root name is used instead.
     pub fn rename_entry(
         &mut self,
         entry_id: ProjectEntryId,
@@ -1551,12 +1555,18 @@ impl Project {
         };
 
         let worktree_id = worktree.read(cx).id();
+        let is_root_entry = self.entry_is_worktree_root(entry_id, cx);
 
         let lsp_store = self.lsp_store().downgrade();
         cx.spawn(|_, mut cx| async move {
             let (old_abs_path, new_abs_path) = {
                 let root_path = worktree.update(&mut cx, |this, _| this.abs_path())?;
-                (root_path.join(&old_path), root_path.join(&new_path))
+                let new_abs_path = if is_root_entry {
+                    root_path.parent().unwrap().join(&new_path)
+                } else {
+                    root_path.join(&new_path)
+                };
+                (root_path.join(&old_path), new_abs_path)
             };
             LspStore::will_rename_entry(
                 lsp_store.clone(),

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

@@ -733,7 +733,9 @@ impl ProjectPanel {
                             .action("Copy Path", Box::new(CopyPath))
                             .action("Copy Relative Path", Box::new(CopyRelativePath))
                             .separator()
-                            .action("Rename", Box::new(Rename))
+                            .when(!is_root || !cfg!(target_os = "windows"), |menu| {
+                                menu.action("Rename", Box::new(Rename))
+                            })
                             .when(!is_root & !is_remote, |menu| {
                                 menu.action("Trash", Box::new(Trash { skip_prompt: false }))
                             })
@@ -1348,6 +1350,10 @@ impl ProjectPanel {
             if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
                 let sub_entry_id = self.unflatten_entry_id(entry_id);
                 if let Some(entry) = worktree.read(cx).entry_for_id(sub_entry_id) {
+                    #[cfg(target_os = "windows")]
+                    if Some(entry) == worktree.read(cx).root_entry() {
+                        return;
+                    }
                     self.edit_state = Some(EditState {
                         worktree_id,
                         entry_id: sub_entry_id,
@@ -7280,6 +7286,84 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    #[cfg_attr(target_os = "windows", ignore)]
+    async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
+        init_test_with_editor(cx);
+
+        let fs = FakeFs::new(cx.executor().clone());
+        fs.insert_tree(
+            "/root1",
+            json!({
+                "dir1": {
+                    "file1.txt": "content 1",
+                },
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
+        let workspace =
+            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+        let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+
+        toggle_expand_dir(&panel, "root1/dir1", cx);
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..20, cx),
+            &["v root1", "    v dir1  <== selected", "          file1.txt",],
+            "Initial state with worktrees"
+        );
+
+        select_path(&panel, "root1", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..20, cx),
+            &["v root1  <== selected", "    v dir1", "          file1.txt",],
+        );
+
+        // Rename root1 to new_root1
+        panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
+
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..20, cx),
+            &[
+                "v [EDITOR: 'root1']  <== selected",
+                "    v dir1",
+                "          file1.txt",
+            ],
+        );
+
+        let confirm = panel.update_in(cx, |panel, window, cx| {
+            panel
+                .filename_editor
+                .update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
+            panel.confirm_edit(window, cx).unwrap()
+        });
+        confirm.await.unwrap();
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..20, cx),
+            &[
+                "v new_root1  <== selected",
+                "    v dir1",
+                "          file1.txt",
+            ],
+            "Should update worktree name"
+        );
+
+        // Ensure internal paths have been updated
+        select_path(&panel, "new_root1/dir1/file1.txt", cx);
+        assert_eq!(
+            visible_entries_as_strings(&panel, 0..20, cx),
+            &[
+                "v new_root1",
+                "    v dir1",
+                "          file1.txt  <== selected",
+            ],
+            "Files in renamed worktree are selectable"
+        );
+    }
+
     #[gpui::test]
     async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
         init_test_with_editor(cx);

crates/repl/src/components/kernel_options.rs πŸ”—

@@ -2,6 +2,7 @@ use crate::kernels::KernelSpecification;
 use crate::repl_store::ReplStore;
 use crate::KERNEL_DOCS_URL;
 
+use gpui::AnyView;
 use gpui::DismissEvent;
 
 use gpui::FontWeight;
@@ -19,10 +20,15 @@ use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
 type OnSelect = Box<dyn Fn(KernelSpecification, &mut Window, &mut App)>;
 
 #[derive(IntoElement)]
-pub struct KernelSelector<T: PopoverTrigger> {
+pub struct KernelSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     handle: Option<PopoverMenuHandle<Picker<KernelPickerDelegate>>>,
     on_select: OnSelect,
     trigger: T,
+    tooltip: TT,
     info_text: Option<SharedString>,
     worktree_id: WorktreeId,
 }
@@ -44,12 +50,17 @@ fn truncate_path(path: &SharedString, max_length: usize) -> SharedString {
     }
 }
 
-impl<T: PopoverTrigger> KernelSelector<T> {
-    pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T) -> Self {
+impl<T, TT> KernelSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
+    pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T, tooltip: TT) -> Self {
         KernelSelector {
             on_select,
             handle: None,
             trigger,
+            tooltip,
             info_text: None,
             worktree_id,
         }
@@ -235,7 +246,11 @@ impl PickerDelegate for KernelPickerDelegate {
     }
 }
 
-impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
+impl<T, TT> RenderOnce for KernelSelector<T, TT>
+where
+    T: PopoverTrigger + ButtonCommon,
+    TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
+{
     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
         let store = ReplStore::global(cx).read(cx);
 
@@ -262,7 +277,7 @@ impl<T: PopoverTrigger> RenderOnce for KernelSelector<T> {
 
         PopoverMenu::new("kernel-switcher")
             .menu(move |_window, _cx| Some(picker_view.clone()))
-            .trigger(self.trigger)
+            .trigger_with_tooltip(self.trigger, self.tooltip)
             .attach(gpui::Corner::BottomLeft)
             .when_some(self.handle, |menu, handle| menu.with_handle(handle))
     }

crates/settings/src/keymap_file.rs πŸ”—

@@ -588,24 +588,24 @@ impl KeymapFile {
         let Some(new_text) = migrate_keymap(&old_text) else {
             return Ok(());
         };
-        let initial_path = paths::keymap_file().as_path();
-        if fs.is_file(initial_path).await {
-            let backup_path = paths::home_dir().join(".zed_keymap_backup");
-            fs.atomic_write(backup_path, old_text)
+        let keymap_path = paths::keymap_file().as_path();
+        if fs.is_file(keymap_path).await {
+            fs.atomic_write(paths::keymap_backup_file().to_path_buf(), old_text)
                 .await
                 .with_context(|| {
                     "Failed to create settings backup in home directory".to_string()
                 })?;
-            let resolved_path = fs.canonicalize(initial_path).await.with_context(|| {
-                format!("Failed to canonicalize keymap path {:?}", initial_path)
-            })?;
+            let resolved_path = fs
+                .canonicalize(keymap_path)
+                .await
+                .with_context(|| format!("Failed to canonicalize keymap path {:?}", keymap_path))?;
             fs.atomic_write(resolved_path.clone(), new_text)
                 .await
                 .with_context(|| format!("Failed to write keymap to file {:?}", resolved_path))?;
         } else {
-            fs.atomic_write(initial_path.to_path_buf(), new_text)
+            fs.atomic_write(keymap_path.to_path_buf(), new_text)
                 .await
-                .with_context(|| format!("Failed to write keymap to file {:?}", initial_path))?;
+                .with_context(|| format!("Failed to write keymap to file {:?}", keymap_path))?;
         }
 
         Ok(())

crates/settings/src/settings_store.rs πŸ”—

@@ -415,11 +415,11 @@ impl SettingsStore {
                     let new_text = cx.read_global(|store: &SettingsStore, cx| {
                         store.new_text_for_update::<T>(old_text, |content| update(content, cx))
                     })?;
-                    let initial_path = paths::settings_file().as_path();
-                    if fs.is_file(initial_path).await {
+                    let settings_path = paths::settings_file().as_path();
+                    if fs.is_file(settings_path).await {
                         let resolved_path =
-                            fs.canonicalize(initial_path).await.with_context(|| {
-                                format!("Failed to canonicalize settings path {:?}", initial_path)
+                            fs.canonicalize(settings_path).await.with_context(|| {
+                                format!("Failed to canonicalize settings path {:?}", settings_path)
                             })?;
 
                         fs.atomic_write(resolved_path.clone(), new_text)
@@ -428,10 +428,10 @@ impl SettingsStore {
                                 format!("Failed to write settings to file {:?}", resolved_path)
                             })?;
                     } else {
-                        fs.atomic_write(initial_path.to_path_buf(), new_text)
+                        fs.atomic_write(settings_path.to_path_buf(), new_text)
                             .await
                             .with_context(|| {
-                                format!("Failed to write settings to file {:?}", initial_path)
+                                format!("Failed to write settings to file {:?}", settings_path)
                             })?;
                     }
 
@@ -1011,17 +1011,16 @@ impl SettingsStore {
                     let Some(new_text) = migrate_settings(&old_text) else {
                         return anyhow::Ok(());
                     };
-                    let initial_path = paths::settings_file().as_path();
-                    if fs.is_file(initial_path).await {
-                        let backup_path = paths::home_dir().join(".zed_settings_backup");
-                        fs.atomic_write(backup_path, old_text)
+                    let settings_path = paths::settings_file().as_path();
+                    if fs.is_file(settings_path).await {
+                        fs.atomic_write(paths::settings_backup_file().to_path_buf(), old_text)
                             .await
                             .with_context(|| {
                                 "Failed to create settings backup in home directory".to_string()
                             })?;
                         let resolved_path =
-                            fs.canonicalize(initial_path).await.with_context(|| {
-                                format!("Failed to canonicalize settings path {:?}", initial_path)
+                            fs.canonicalize(settings_path).await.with_context(|| {
+                                format!("Failed to canonicalize settings path {:?}", settings_path)
                             })?;
                         fs.atomic_write(resolved_path.clone(), new_text)
                             .await
@@ -1029,10 +1028,10 @@ impl SettingsStore {
                                 format!("Failed to write settings to file {:?}", resolved_path)
                             })?;
                     } else {
-                        fs.atomic_write(initial_path.to_path_buf(), new_text)
+                        fs.atomic_write(settings_path.to_path_buf(), new_text)
                             .await
                             .with_context(|| {
-                                format!("Failed to write settings to file {:?}", initial_path)
+                                format!("Failed to write settings to file {:?}", settings_path)
                             })?;
                     }
                     anyhow::Ok(())

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

@@ -36,7 +36,6 @@ pub enum ComponentStory {
     TabBar,
     Text,
     ToggleButton,
-    ToolStrip,
     ViewportUnits,
     WithRemSize,
     Vector,
@@ -73,7 +72,6 @@ impl ComponentStory {
             Self::TabBar => cx.new(|_| ui::TabBarStory).into(),
             Self::Text => TextStory::model(cx).into(),
             Self::ToggleButton => cx.new(|_| ui::ToggleButtonStory).into(),
-            Self::ToolStrip => cx.new(|_| ui::ToolStripStory).into(),
             Self::ViewportUnits => cx.new(|_| crate::stories::ViewportUnitsStory).into(),
             Self::WithRemSize => cx.new(|_| crate::stories::WithRemSizeStory).into(),
             Self::Vector => cx.new(|_| ui::VectorStory).into(),

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

@@ -139,10 +139,9 @@ impl TerminalPanel {
                     .gap(DynamicSpacing::Base02.rems(cx))
                     .child(
                         PopoverMenu::new("terminal-tab-bar-popover-menu")
-                            .trigger(
-                                IconButton::new("plus", IconName::Plus)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("New…")),
+                            .trigger_with_tooltip(
+                                IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
+                                Tooltip::text("New…"),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(pane.new_item_context_menu_handle.clone())
@@ -169,10 +168,10 @@ impl TerminalPanel {
                     .children(assistant_tab_bar_button.clone())
                     .child(
                         PopoverMenu::new("terminal-pane-tab-bar-split")
-                            .trigger(
+                            .trigger_with_tooltip(
                                 IconButton::new("terminal-pane-split", IconName::Split)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("Split Pane")),
+                                    .icon_size(IconSize::Small),
+                                Tooltip::text("Split Pane"),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(pane.split_item_context_menu_handle.clone())

crates/title_bar/src/application_menu.rs πŸ”—

@@ -133,16 +133,14 @@ impl ApplicationMenu {
                     .menu(move |window, cx| {
                         Self::build_menu_from_items(entry.clone(), window, cx).into()
                     })
-                    .trigger(
+                    .trigger_with_tooltip(
                         IconButton::new(
                             SharedString::from(format!("{}-menu-trigger", menu_name)),
                             ui::IconName::Menu,
                         )
                         .style(ButtonStyle::Subtle)
-                        .icon_size(IconSize::Small)
-                        .when(!handle.is_deployed(), |this| {
-                            this.tooltip(Tooltip::text("Open Application Menu"))
-                        }),
+                        .icon_size(IconSize::Small),
+                        Tooltip::text("Open Application Menu"),
                     )
                     .with_handle(handle),
             )

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

@@ -530,7 +530,7 @@ impl TitleBar {
                 .tooltip(move |window, cx| {
                     Tooltip::with_meta(
                         "Recent Branches",
-                        Some(&zed_actions::branches::OpenRecent),
+                        Some(&zed_actions::git::Branch),
                         "Local branches only",
                         window,
                         cx,
@@ -538,7 +538,7 @@ impl TitleBar {
                 })
                 .on_click(move |_, window, cx| {
                     let _ = workspace.update(cx, |_this, cx| {
-                        window.dispatch_action(zed_actions::branches::OpenRecent.boxed_clone(), cx);
+                        window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
                     });
                 }),
         )
@@ -673,6 +673,10 @@ impl TitleBar {
                             "Themes…",
                             zed_actions::theme_selector::Toggle::default().boxed_clone(),
                         )
+                        .action(
+                            "Icon Themes…",
+                            zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
+                        )
                         .action("Extensions", zed_actions::Extensions.boxed_clone())
                         .separator()
                         .link(
@@ -686,7 +690,7 @@ impl TitleBar {
                     })
                     .into()
                 })
-                .trigger(
+                .trigger_with_tooltip(
                     ButtonLike::new("user-menu")
                         .child(
                             h_flex()
@@ -702,8 +706,8 @@ impl TitleBar {
                                         .color(Color::Muted),
                                 ),
                         )
-                        .style(ButtonStyle::Subtle)
-                        .tooltip(Tooltip::text("Toggle User Menu")),
+                        .style(ButtonStyle::Subtle),
+                    Tooltip::text("Toggle User Menu"),
                 )
                 .anchor(gpui::Corner::TopRight)
         } else {
@@ -716,6 +720,10 @@ impl TitleBar {
                                 "Themes…",
                                 zed_actions::theme_selector::Toggle::default().boxed_clone(),
                             )
+                            .action(
+                                "Icon Themes…",
+                                zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
+                            )
                             .action("Extensions", zed_actions::Extensions.boxed_clone())
                             .separator()
                             .link(
@@ -728,10 +736,9 @@ impl TitleBar {
                     })
                     .into()
                 })
-                .trigger(
-                    IconButton::new("user-menu", IconName::ChevronDown)
-                        .icon_size(IconSize::Small)
-                        .tooltip(Tooltip::text("Toggle User Menu")),
+                .trigger_with_tooltip(
+                    IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small),
+                    Tooltip::text("Toggle User Menu"),
                 )
         }
     }

crates/ui/Cargo.toml πŸ”—

@@ -14,8 +14,10 @@ path = "src/ui.rs"
 
 [dependencies]
 chrono.workspace = true
+component.workspace = true
 gpui.workspace = true
 itertools = { workspace = true, optional = true }
+linkme.workspace = true
 menu.workspace = true
 serde.workspace = true
 settings.workspace = true
@@ -31,3 +33,7 @@ windows.workspace = true
 [features]
 default = []
 stories = ["dep:itertools", "dep:story"]
+
+# cargo-machete doesn't understand that linkme is used in the component macro
+[package.metadata.cargo-machete]
+ignored = ["linkme"]

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

@@ -29,7 +29,6 @@ mod tab;
 mod tab_bar;
 mod table;
 mod toggle;
-mod tool_strip;
 mod tooltip;
 
 #[cfg(feature = "stories")]
@@ -66,7 +65,6 @@ pub use tab::*;
 pub use tab_bar::*;
 pub use table::*;
 pub use toggle::*;
-pub use tool_strip::*;
 pub use tooltip::*;
 
 #[cfg(feature = "stories")]

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

@@ -1,4 +1,4 @@
-use crate::prelude::*;
+use crate::{prelude::*, Indicator};
 
 use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
 
@@ -14,7 +14,7 @@ use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
 ///     .grayscale(true)
 ///     .border_color(gpui::red());
 /// ```
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Avatar {
     image: Img,
     size: Option<AbsoluteLength>,
@@ -96,3 +96,60 @@ impl RenderOnce for Avatar {
             .children(self.indicator.map(|indicator| div().child(indicator)))
     }
 }
+
+impl ComponentPreview for Avatar {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4";
+
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Sizes",
+                    vec![
+                        single_example(
+                            "Default",
+                            Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Small",
+                            Avatar::new(example_avatar).size(px(24.)).into_any_element(),
+                        ),
+                        single_example(
+                            "Large",
+                            Avatar::new(example_avatar).size(px(48.)).into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Styles",
+                    vec![
+                        single_example("Default", Avatar::new(example_avatar).into_any_element()),
+                        single_example(
+                            "Grayscale",
+                            Avatar::new(example_avatar)
+                                .grayscale(true)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "With Border",
+                            Avatar::new(example_avatar)
+                                .border_color(gpui::red())
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "With Indicator",
+                    vec![single_example(
+                        "Dot",
+                        Avatar::new(example_avatar)
+                            .indicator(Indicator::dot().color(Color::Success))
+                            .into_any_element(),
+                    )],
+                ),
+            ])
+            .into_any_element()
+    }
+}

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

@@ -1,5 +1,7 @@
 #![allow(missing_docs)]
-use gpui::{AnyView, DefiniteLength};
+use component::{example_group_with_title, single_example, ComponentPreview};
+use gpui::{AnyElement, AnyView, DefiniteLength};
+use ui_macros::IntoComponent;
 
 use crate::{
     prelude::*, Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding,
@@ -78,7 +80,7 @@ use super::button_icon::ButtonIcon;
 ///     });
 /// ```
 ///
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Button {
     base: ButtonLike,
     label: SharedString,
@@ -93,7 +95,7 @@ pub struct Button {
     selected_icon: Option<IconName>,
     selected_icon_color: Option<Color>,
     key_binding: Option<KeyBinding>,
-    keybinding_position: KeybindingPosition,
+    key_binding_position: KeybindingPosition,
     alpha: Option<f32>,
 }
 
@@ -119,7 +121,7 @@ impl Button {
             selected_icon: None,
             selected_icon_color: None,
             key_binding: None,
-            keybinding_position: KeybindingPosition::default(),
+            key_binding_position: KeybindingPosition::default(),
             alpha: None,
         }
     }
@@ -195,7 +197,7 @@ impl Button {
     /// This method allows you to specify where the keybinding should be displayed
     /// in relation to the button's label.
     pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self {
-        self.keybinding_position = position;
+        self.key_binding_position = position;
         self
     }
 
@@ -425,7 +427,7 @@ impl RenderOnce for Button {
                 .child(
                     h_flex()
                         .when(
-                            self.keybinding_position == KeybindingPosition::Start,
+                            self.key_binding_position == KeybindingPosition::Start,
                             |this| this.flex_row_reverse(),
                         )
                         .gap(DynamicSpacing::Base06.rems(cx))
@@ -455,101 +457,124 @@ impl RenderOnce for Button {
 }
 
 impl ComponentPreview for Button {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A button allows users to take actions, and make choices, with a single tap."
-    }
-
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![
-            example_group_with_title(
-                "Styles",
-                vec![
-                    single_example("Default", Button::new("default", "Default")),
-                    single_example(
-                        "Filled",
-                        Button::new("filled", "Filled").style(ButtonStyle::Filled),
-                    ),
-                    single_example(
-                        "Subtle",
-                        Button::new("outline", "Subtle").style(ButtonStyle::Subtle),
-                    ),
-                    single_example(
-                        "Transparent",
-                        Button::new("transparent", "Transparent").style(ButtonStyle::Transparent),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Tinted",
-                vec![
-                    single_example(
-                        "Accent",
-                        Button::new("tinted_accent", "Accent")
-                            .style(ButtonStyle::Tinted(TintColor::Accent)),
-                    ),
-                    single_example(
-                        "Error",
-                        Button::new("tinted_negative", "Error")
-                            .style(ButtonStyle::Tinted(TintColor::Error)),
-                    ),
-                    single_example(
-                        "Warning",
-                        Button::new("tinted_warning", "Warning")
-                            .style(ButtonStyle::Tinted(TintColor::Warning)),
-                    ),
-                    single_example(
-                        "Success",
-                        Button::new("tinted_positive", "Success")
-                            .style(ButtonStyle::Tinted(TintColor::Success)),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "States",
-                vec![
-                    single_example("Default", Button::new("default_state", "Default")),
-                    single_example(
-                        "Disabled",
-                        Button::new("disabled", "Disabled").disabled(true),
-                    ),
-                    single_example(
-                        "Selected",
-                        Button::new("selected", "Selected").toggle_state(true),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "With Icons",
-                vec![
-                    single_example(
-                        "Icon Start",
-                        Button::new("icon_start", "Icon Start")
-                            .icon(IconName::Check)
-                            .icon_position(IconPosition::Start),
-                    ),
-                    single_example(
-                        "Icon End",
-                        Button::new("icon_end", "Icon End")
-                            .icon(IconName::Check)
-                            .icon_position(IconPosition::End),
-                    ),
-                    single_example(
-                        "Icon Color",
-                        Button::new("icon_color", "Icon Color")
-                            .icon(IconName::Check)
-                            .icon_color(Color::Accent),
-                    ),
-                    single_example(
-                        "Tinted Icons",
-                        Button::new("tinted_icons", "Error")
-                            .style(ButtonStyle::Tinted(TintColor::Error))
-                            .color(Color::Error)
-                            .icon_color(Color::Error)
-                            .icon(IconName::Trash)
-                            .icon_position(IconPosition::Start),
-                    ),
-                ],
-            ),
-        ]
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Styles",
+                    vec![
+                        single_example(
+                            "Default",
+                            Button::new("default", "Default").into_any_element(),
+                        ),
+                        single_example(
+                            "Filled",
+                            Button::new("filled", "Filled")
+                                .style(ButtonStyle::Filled)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Subtle",
+                            Button::new("outline", "Subtle")
+                                .style(ButtonStyle::Subtle)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Transparent",
+                            Button::new("transparent", "Transparent")
+                                .style(ButtonStyle::Transparent)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Tinted",
+                    vec![
+                        single_example(
+                            "Accent",
+                            Button::new("tinted_accent", "Accent")
+                                .style(ButtonStyle::Tinted(TintColor::Accent))
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Error",
+                            Button::new("tinted_negative", "Error")
+                                .style(ButtonStyle::Tinted(TintColor::Error))
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Warning",
+                            Button::new("tinted_warning", "Warning")
+                                .style(ButtonStyle::Tinted(TintColor::Warning))
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Success",
+                            Button::new("tinted_positive", "Success")
+                                .style(ButtonStyle::Tinted(TintColor::Success))
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "States",
+                    vec![
+                        single_example(
+                            "Default",
+                            Button::new("default_state", "Default").into_any_element(),
+                        ),
+                        single_example(
+                            "Disabled",
+                            Button::new("disabled", "Disabled")
+                                .disabled(true)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Selected",
+                            Button::new("selected", "Selected")
+                                .toggle_state(true)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "With Icons",
+                    vec![
+                        single_example(
+                            "Icon Start",
+                            Button::new("icon_start", "Icon Start")
+                                .icon(IconName::Check)
+                                .icon_position(IconPosition::Start)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Icon End",
+                            Button::new("icon_end", "Icon End")
+                                .icon(IconName::Check)
+                                .icon_position(IconPosition::End)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Icon Color",
+                            Button::new("icon_color", "Icon Color")
+                                .icon(IconName::Check)
+                                .icon_color(Color::Accent)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Tinted Icons",
+                            Button::new("tinted_icons", "Error")
+                                .style(ButtonStyle::Tinted(TintColor::Error))
+                                .color(Color::Error)
+                                .icon_color(Color::Error)
+                                .icon(IconName::Trash)
+                                .icon_position(IconPosition::Start)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+            ])
+            .into_any_element()
     }
 }

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

@@ -506,7 +506,9 @@ impl RenderOnce for ButtonLike {
             .group("")
             .flex_none()
             .h(self.height.unwrap_or(self.size.rems().into()))
-            .when_some(self.width, |this, width| this.w(width).justify_center())
+            .when_some(self.width, |this, width| {
+                this.w(width).justify_center().text_center()
+            })
             .when_some(self.rounding, |this, rounding| match rounding {
                 ButtonLikeRounding::All => this.rounded_md(),
                 ButtonLikeRounding::Left => this.rounded_l_md(),

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

@@ -22,6 +22,7 @@ pub struct IconButton {
     icon_size: IconSize,
     icon_color: Color,
     selected_icon: Option<IconName>,
+    selected_icon_color: Option<Color>,
     indicator: Option<Indicator>,
     indicator_border_color: Option<Hsla>,
     alpha: Option<f32>,
@@ -36,6 +37,7 @@ impl IconButton {
             icon_size: IconSize::default(),
             icon_color: Color::Default,
             selected_icon: None,
+            selected_icon_color: None,
             indicator: None,
             indicator_border_color: None,
             alpha: None,
@@ -69,6 +71,12 @@ impl IconButton {
         self
     }
 
+    /// Sets the icon color used when the button is in a selected state.
+    pub fn selected_icon_color(mut self, color: impl Into<Option<Color>>) -> Self {
+        self.selected_icon_color = color.into();
+        self
+    }
+
     pub fn indicator(mut self, indicator: Indicator) -> Self {
         self.indicator = Some(indicator);
         self
@@ -181,6 +189,7 @@ impl RenderOnce for IconButton {
                     .disabled(is_disabled)
                     .toggle_state(is_selected)
                     .selected_icon(self.selected_icon)
+                    .selected_icon_color(self.selected_icon_color)
                     .when_some(selected_style, |this, style| this.selected_style(style))
                     .when_some(self.indicator, |this, indicator| {
                         this.indicator(indicator)

crates/ui/src/components/content_group.rs πŸ”—

@@ -1,4 +1,5 @@
 use crate::prelude::*;
+use component::{example_group, single_example, ComponentPreview};
 use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled};
 use smallvec::SmallVec;
 
@@ -22,7 +23,8 @@ pub fn h_group() -> ContentGroup {
 }
 
 /// A flexible container component that can hold other elements.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "layout")]
 pub struct ContentGroup {
     base: Div,
     border: bool,
@@ -87,16 +89,8 @@ impl RenderOnce for ContentGroup {
 }
 
 impl ComponentPreview for ContentGroup {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A flexible container component that can hold other elements. It can be customized with or without a border and background fill."
-    }
-
-    fn example_label_side() -> ExampleLabelSide {
-        ExampleLabelSide::Bottom
-    }
-
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![example_group(vec![
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        example_group(vec![
             single_example(
                 "Default",
                 ContentGroup::new()
@@ -104,7 +98,8 @@ impl ComponentPreview for ContentGroup {
                     .items_center()
                     .justify_center()
                     .h_48()
-                    .child(Label::new("Default ContentBox")),
+                    .child(Label::new("Default ContentBox"))
+                    .into_any_element(),
             )
             .grow(),
             single_example(
@@ -115,7 +110,8 @@ impl ComponentPreview for ContentGroup {
                     .justify_center()
                     .h_48()
                     .borderless()
-                    .child(Label::new("Borderless ContentBox")),
+                    .child(Label::new("Borderless ContentBox"))
+                    .into_any_element(),
             )
             .grow(),
             single_example(
@@ -126,10 +122,11 @@ impl ComponentPreview for ContentGroup {
                     .justify_center()
                     .h_48()
                     .unfilled()
-                    .child(Label::new("Unfilled ContentBox")),
+                    .child(Label::new("Unfilled ContentBox"))
+                    .into_any_element(),
             )
             .grow(),
         ])
-        .grow()]
+        .into_any_element()
     }
 }

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

@@ -672,14 +672,16 @@ impl Render for ContextMenu {
                                                             *toggle,
                                                             |list_item, (position, toggled)| {
                                                                 let contents = if toggled {
-                                                                    v_flex().flex_none().child(
+                                                                    div().flex_none().child(
                                                                         Icon::new(IconName::Check)
-                                                                            .color(Color::Accent),
+                                                                            .color(Color::Accent)
+                                                                            .size(*icon_size)
                                                                     )
                                                                 } else {
-                                                                    v_flex().flex_none().size(
-                                                                        IconSize::default().rems(),
-                                                                    )
+                                                                    div().flex_none().child(
+                                                                        Icon::new(IconName::Check)
+                                                                            .size(*icon_size)
+                                                                    ).invisible()
                                                                 };
                                                                 match position {
                                                                     IconPosition::Start => {

crates/ui/src/components/facepile.rs πŸ”—

@@ -1,4 +1,4 @@
-use crate::{prelude::*, Avatar};
+use crate::prelude::*;
 use gpui::{AnyElement, StyleRefinement};
 use smallvec::SmallVec;
 
@@ -60,60 +60,60 @@ impl RenderOnce for Facepile {
     }
 }
 
-impl ComponentPreview for Facepile {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A facepile is a collection of faces stacked horizontally–\
-        always with the leftmost face on top and descending in z-index.\
-        \n\nFacepiles are used to display a group of people or things,\
-        such as a list of participants in a collaboration session."
-    }
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        let few_faces: [&'static str; 3] = [
-            "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
-        ];
+// impl ComponentPreview for Facepile {
+//     fn description() -> impl Into<Option<&'static str>> {
+//         "A facepile is a collection of faces stacked horizontally–\
+//         always with the leftmost face on top and descending in z-index.\
+//         \n\nFacepiles are used to display a group of people or things,\
+//         such as a list of participants in a collaboration session."
+//     }
+//     fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
+//         let few_faces: [&'static str; 3] = [
+//             "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
+//         ];
 
-        let many_faces: [&'static str; 6] = [
-            "https://avatars.githubusercontent.com/u/326587?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/2280405?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/1789?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
-            "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
-        ];
+//         let many_faces: [&'static str; 6] = [
+//             "https://avatars.githubusercontent.com/u/326587?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/2280405?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/1789?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/67129314?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/482957?s=60&v=4",
+//             "https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
+//         ];
 
-        vec![example_group_with_title(
-            "Examples",
-            vec![
-                single_example(
-                    "Few Faces",
-                    Facepile::new(
-                        few_faces
-                            .iter()
-                            .map(|&url| Avatar::new(url).into_any_element())
-                            .collect(),
-                    ),
-                ),
-                single_example(
-                    "Many Faces",
-                    Facepile::new(
-                        many_faces
-                            .iter()
-                            .map(|&url| Avatar::new(url).into_any_element())
-                            .collect(),
-                    ),
-                ),
-                single_example(
-                    "Custom Size",
-                    Facepile::new(
-                        few_faces
-                            .iter()
-                            .map(|&url| Avatar::new(url).size(px(24.)).into_any_element())
-                            .collect(),
-                    ),
-                ),
-            ],
-        )]
-    }
-}
+//         vec![example_group_with_title(
+//             "Examples",
+//             vec![
+//                 single_example(
+//                     "Few Faces",
+//                     Facepile::new(
+//                         few_faces
+//                             .iter()
+//                             .map(|&url| Avatar::new(url).into_any_element())
+//                             .collect(),
+//                     ),
+//                 ),
+//                 single_example(
+//                     "Many Faces",
+//                     Facepile::new(
+//                         many_faces
+//                             .iter()
+//                             .map(|&url| Avatar::new(url).into_any_element())
+//                             .collect(),
+//                     ),
+//                 ),
+//                 single_example(
+//                     "Custom Size",
+//                     Facepile::new(
+//                         few_faces
+//                             .iter()
+//                             .map(|&url| Avatar::new(url).size(px(24.)).into_any_element())
+//                             .collect(),
+//                     ),
+//                 ),
+//             ],
+//         )]
+//     }
+// }

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

@@ -7,17 +7,13 @@ use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 pub use decorated_icon::*;
-use gpui::{img, svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
+use gpui::{img, svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation};
 pub use icon_decoration::*;
 use serde::{Deserialize, Serialize};
 use strum::{EnumIter, EnumString, IntoStaticStr};
 use ui_macros::DerivePathStr;
 
-use crate::{
-    prelude::*,
-    traits::component_preview::{ComponentExample, ComponentPreview},
-    Indicator,
-};
+use crate::{prelude::*, Indicator};
 
 #[derive(IntoElement)]
 pub enum AnyIcon {
@@ -234,6 +230,7 @@ pub enum IconName {
     Link,
     ListTree,
     ListX,
+    LockOutlined,
     MagnifyingGlass,
     MailOpen,
     Maximize,
@@ -363,7 +360,7 @@ impl IconSource {
     }
 }
 
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Icon {
     source: IconSource,
     color: Color,
@@ -493,24 +490,41 @@ impl RenderOnce for IconWithIndicator {
 }
 
 impl ComponentPreview for Icon {
-    fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Icon>> {
-        let arrow_icons = vec![
-            IconName::ArrowDown,
-            IconName::ArrowLeft,
-            IconName::ArrowRight,
-            IconName::ArrowUp,
-            IconName::ArrowCircle,
-        ];
-
-        vec![example_group_with_title(
-            "Arrow Icons",
-            arrow_icons
-                .into_iter()
-                .map(|icon| {
-                    let name = format!("{:?}", icon).to_string();
-                    ComponentExample::new(name, Icon::new(icon))
-                })
-                .collect(),
-        )]
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Sizes",
+                    vec![
+                        single_example("Default", Icon::new(IconName::Star).into_any_element()),
+                        single_example(
+                            "Small",
+                            Icon::new(IconName::Star)
+                                .size(IconSize::Small)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Large",
+                            Icon::new(IconName::Star)
+                                .size(IconSize::XLarge)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Colors",
+                    vec![
+                        single_example("Default", Icon::new(IconName::Bell).into_any_element()),
+                        single_example(
+                            "Custom Color",
+                            Icon::new(IconName::Bell)
+                                .color(Color::Error)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+            ])
+            .into_any_element()
     }
 }

crates/ui/src/components/icon/decorated_icon.rs πŸ”—

@@ -1,10 +1,8 @@
-use gpui::{IntoElement, Point};
+use gpui::{AnyElement, IntoElement, Point};
 
-use crate::{
-    prelude::*, traits::component_preview::ComponentPreview, IconDecoration, IconDecorationKind,
-};
+use crate::{prelude::*, IconDecoration, IconDecorationKind};
 
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct DecoratedIcon {
     icon: Icon,
     decoration: Option<IconDecoration>,
@@ -27,12 +25,7 @@ impl RenderOnce for DecoratedIcon {
 }
 
 impl ComponentPreview for DecoratedIcon {
-    fn examples(_: &mut Window, cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        let icon_1 = Icon::new(IconName::FileDoc);
-        let icon_2 = Icon::new(IconName::FileDoc);
-        let icon_3 = Icon::new(IconName::FileDoc);
-        let icon_4 = Icon::new(IconName::FileDoc);
-
+    fn preview(_window: &mut Window, cx: &App) -> AnyElement {
         let decoration_x = IconDecoration::new(
             IconDecorationKind::X,
             cx.theme().colors().surface_background,
@@ -66,22 +59,32 @@ impl ComponentPreview for DecoratedIcon {
             y: px(-2.),
         });
 
-        let examples = vec![
-            single_example("no_decoration", DecoratedIcon::new(icon_1, None)),
-            single_example(
-                "with_decoration",
-                DecoratedIcon::new(icon_2, Some(decoration_x)),
-            ),
-            single_example(
-                "with_decoration",
-                DecoratedIcon::new(icon_3, Some(decoration_triangle)),
-            ),
-            single_example(
-                "with_decoration",
-                DecoratedIcon::new(icon_4, Some(decoration_dot)),
-            ),
-        ];
-
-        vec![example_group(examples)]
+        v_flex()
+            .gap_6()
+            .children(vec![example_group_with_title(
+                "Decorations",
+                vec![
+                    single_example(
+                        "No Decoration",
+                        DecoratedIcon::new(Icon::new(IconName::FileDoc), None).into_any_element(),
+                    ),
+                    single_example(
+                        "X Decoration",
+                        DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_x))
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Triangle Decoration",
+                        DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_triangle))
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Dot Decoration",
+                        DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_dot))
+                            .into_any_element(),
+                    ),
+                ],
+            )])
+            .into_any_element()
     }
 }

crates/ui/src/components/icon/icon_decoration.rs πŸ”—

@@ -1,8 +1,8 @@
 use gpui::{svg, Hsla, IntoElement, Point};
-use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
+use strum::{EnumIter, EnumString, IntoStaticStr};
 use ui_macros::DerivePathStr;
 
-use crate::{prelude::*, traits::component_preview::ComponentPreview};
+use crate::prelude::*;
 
 const ICON_DECORATION_SIZE: Pixels = px(11.);
 
@@ -149,21 +149,3 @@ impl RenderOnce for IconDecoration {
             .child(background)
     }
 }
-
-impl ComponentPreview for IconDecoration {
-    fn examples(_: &mut Window, cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        let all_kinds = IconDecorationKind::iter().collect::<Vec<_>>();
-
-        let examples = all_kinds
-            .iter()
-            .map(|kind| {
-                single_example(
-                    format!("{kind:?}"),
-                    IconDecoration::new(*kind, cx.theme().colors().surface_background, cx),
-                )
-            })
-            .collect();
-
-        vec![example_group(examples)]
-    }
-}

crates/ui/src/components/indicator.rs πŸ”—

@@ -83,34 +83,3 @@ impl RenderOnce for Indicator {
         }
     }
 }
-
-impl ComponentPreview for Indicator {
-    fn description() -> impl Into<Option<&'static str>> {
-        "An indicator visually represents a status or state."
-    }
-
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![
-            example_group_with_title(
-                "Types",
-                vec![
-                    single_example("Dot", Indicator::dot().color(Color::Info)),
-                    single_example("Bar", Indicator::bar().color(Color::Player(2))),
-                    single_example(
-                        "Icon",
-                        Indicator::icon(Icon::new(IconName::Check).color(Color::Success)),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Examples",
-                vec![
-                    single_example("Info", Indicator::dot().color(Color::Info)),
-                    single_example("Success", Indicator::dot().color(Color::Success)),
-                    single_example("Warning", Indicator::dot().color(Color::Warning)),
-                    single_example("Error", Indicator::dot().color(Color::Error)),
-                ],
-            ),
-        ]
-    }
-}

crates/ui/src/components/keybinding_hint.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{h_flex, prelude::*};
 use crate::{ElevationIndex, KeyBinding};
-use gpui::{point, App, BoxShadow, IntoElement, Window};
+use gpui::{point, AnyElement, App, BoxShadow, IntoElement, Window};
 use smallvec::smallvec;
 
 /// Represents a hint for a keybinding, optionally with a prefix and suffix.
@@ -17,7 +17,7 @@ use smallvec::smallvec;
 ///     .prefix("Save:")
 ///     .size(Pixels::from(14.0));
 /// ```
-#[derive(Debug, IntoElement, Clone)]
+#[derive(Debug, IntoElement, IntoComponent)]
 pub struct KeybindingHint {
     prefix: Option<SharedString>,
     suffix: Option<SharedString>,
@@ -206,102 +206,99 @@ impl RenderOnce for KeybindingHint {
 }
 
 impl ComponentPreview for KeybindingHint {
-    fn description() -> impl Into<Option<&'static str>> {
-        "Used to display hint text for keyboard shortcuts. Can have a prefix and suffix."
-    }
-
-    fn examples(window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        let home_fallback = gpui::KeyBinding::new("home", menu::SelectFirst, None);
-        let home = KeyBinding::for_action(&menu::SelectFirst, window)
-            .unwrap_or(KeyBinding::new(home_fallback));
-
-        let end_fallback = gpui::KeyBinding::new("end", menu::SelectLast, None);
-        let end = KeyBinding::for_action(&menu::SelectLast, window)
-            .unwrap_or(KeyBinding::new(end_fallback));
-
+    fn preview(window: &mut Window, _cx: &App) -> AnyElement {
         let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
         let enter = KeyBinding::for_action(&menu::Confirm, window)
             .unwrap_or(KeyBinding::new(enter_fallback));
 
-        let escape_fallback = gpui::KeyBinding::new("escape", menu::Cancel, None);
-        let escape = KeyBinding::for_action(&menu::Cancel, window)
-            .unwrap_or(KeyBinding::new(escape_fallback));
-
-        vec![
-            example_group_with_title(
-                "Basic",
-                vec![
-                    single_example(
-                        "With Prefix",
-                        KeybindingHint::with_prefix("Go to Start:", home.clone()),
-                    ),
-                    single_example(
-                        "With Suffix",
-                        KeybindingHint::with_suffix(end.clone(), "Go to End"),
-                    ),
-                    single_example(
-                        "With Prefix and Suffix",
-                        KeybindingHint::new(enter.clone())
-                            .prefix("Confirm:")
-                            .suffix("Execute selected action"),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Sizes",
-                vec![
-                    single_example(
-                        "Small",
-                        KeybindingHint::new(home.clone())
-                            .size(Pixels::from(12.0))
-                            .prefix("Small:"),
-                    ),
-                    single_example(
-                        "Medium",
-                        KeybindingHint::new(end.clone())
-                            .size(Pixels::from(16.0))
-                            .suffix("Medium"),
-                    ),
-                    single_example(
-                        "Large",
-                        KeybindingHint::new(enter.clone())
-                            .size(Pixels::from(20.0))
-                            .prefix("Large:")
-                            .suffix("Size"),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Elevations",
-                vec![
-                    single_example(
-                        "Surface",
-                        KeybindingHint::new(home.clone())
-                            .elevation(ElevationIndex::Surface)
-                            .prefix("Surface:"),
-                    ),
-                    single_example(
-                        "Elevated Surface",
-                        KeybindingHint::new(end.clone())
-                            .elevation(ElevationIndex::ElevatedSurface)
-                            .suffix("Elevated"),
-                    ),
-                    single_example(
-                        "Editor Surface",
-                        KeybindingHint::new(enter.clone())
-                            .elevation(ElevationIndex::EditorSurface)
-                            .prefix("Editor:")
-                            .suffix("Surface"),
-                    ),
-                    single_example(
-                        "Modal Surface",
-                        KeybindingHint::new(escape.clone())
-                            .elevation(ElevationIndex::ModalSurface)
-                            .prefix("Modal:")
-                            .suffix("Escape"),
-                    ),
-                ],
-            ),
-        ]
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Basic",
+                    vec![
+                        single_example(
+                            "With Prefix",
+                            KeybindingHint::with_prefix("Go to Start:", enter.clone())
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "With Suffix",
+                            KeybindingHint::with_suffix(enter.clone(), "Go to End")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "With Prefix and Suffix",
+                            KeybindingHint::new(enter.clone())
+                                .prefix("Confirm:")
+                                .suffix("Execute selected action")
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Sizes",
+                    vec![
+                        single_example(
+                            "Small",
+                            KeybindingHint::new(enter.clone())
+                                .size(Pixels::from(12.0))
+                                .prefix("Small:")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Medium",
+                            KeybindingHint::new(enter.clone())
+                                .size(Pixels::from(16.0))
+                                .suffix("Medium")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Large",
+                            KeybindingHint::new(enter.clone())
+                                .size(Pixels::from(20.0))
+                                .prefix("Large:")
+                                .suffix("Size")
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Elevations",
+                    vec![
+                        single_example(
+                            "Surface",
+                            KeybindingHint::new(enter.clone())
+                                .elevation(ElevationIndex::Surface)
+                                .prefix("Surface:")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Elevated Surface",
+                            KeybindingHint::new(enter.clone())
+                                .elevation(ElevationIndex::ElevatedSurface)
+                                .suffix("Elevated")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Editor Surface",
+                            KeybindingHint::new(enter.clone())
+                                .elevation(ElevationIndex::EditorSurface)
+                                .prefix("Editor:")
+                                .suffix("Surface")
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Modal Surface",
+                            KeybindingHint::new(enter.clone())
+                                .elevation(ElevationIndex::ModalSurface)
+                                .prefix("Modal:")
+                                .suffix("Enter")
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+            ])
+            .into_any_element()
     }
 }

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

@@ -1,6 +1,6 @@
 #![allow(missing_docs)]
 
-use gpui::{App, StyleRefinement, Window};
+use gpui::{AnyElement, App, StyleRefinement, Window};
 
 use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
 
@@ -32,7 +32,7 @@ use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle};
 ///
 /// let my_label = Label::new("Deleted").strikethrough(true);
 /// ```
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Label {
     base: LabelLike,
     label: SharedString,
@@ -172,6 +172,11 @@ impl LabelCommon for Label {
         self.base = self.base.single_line();
         self
     }
+
+    fn buffer_font(mut self, cx: &App) -> Self {
+        self.base = self.base.buffer_font(cx);
+        self
+    }
 }
 
 impl RenderOnce for Label {
@@ -179,3 +184,53 @@ impl RenderOnce for Label {
         self.base.child(self.label)
     }
 }
+
+impl ComponentPreview for Label {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Sizes",
+                    vec![
+                        single_example("Default", Label::new("Default Label").into_any_element()),
+                        single_example("Small", Label::new("Small Label").size(LabelSize::Small).into_any_element()),
+                        single_example("Large", Label::new("Large Label").size(LabelSize::Large).into_any_element()),
+                    ],
+                ),
+                example_group_with_title(
+                    "Colors",
+                    vec![
+                        single_example("Default", Label::new("Default Color").into_any_element()),
+                        single_example("Accent", Label::new("Accent Color").color(Color::Accent).into_any_element()),
+                        single_example("Error", Label::new("Error Color").color(Color::Error).into_any_element()),
+                    ],
+                ),
+                example_group_with_title(
+                    "Styles",
+                    vec![
+                        single_example("Default", Label::new("Default Style").into_any_element()),
+                        single_example("Bold", Label::new("Bold Style").weight(gpui::FontWeight::BOLD).into_any_element()),
+                        single_example("Italic", Label::new("Italic Style").italic(true).into_any_element()),
+                        single_example("Strikethrough", Label::new("Strikethrough Style").strikethrough(true).into_any_element()),
+                        single_example("Underline", Label::new("Underline Style").underline(true).into_any_element()),
+                    ],
+                ),
+                example_group_with_title(
+                    "Line Height Styles",
+                    vec![
+                        single_example("Default", Label::new("Default Line Height").into_any_element()),
+                        single_example("UI Label", Label::new("UI Label Line Height").line_height_style(LineHeightStyle::UiLabel).into_any_element()),
+                    ],
+                ),
+                example_group_with_title(
+                    "Special Cases",
+                    vec![
+                        single_example("Single Line", Label::new("Single\nLine\nText").single_line().into_any_element()),
+                        single_example("Text Ellipsis", Label::new("This is a very long text that should be truncated with an ellipsis").text_ellipsis().into_any_element()),
+                    ],
+                ),
+            ])
+            .into_any_element()
+    }
+}

crates/ui/src/components/label/label_like.rs πŸ”—

@@ -55,6 +55,9 @@ pub trait LabelCommon {
 
     /// Sets the label to render as a single line.
     fn single_line(self) -> Self;
+
+    /// Sets the font to the buffer's
+    fn buffer_font(self, cx: &App) -> Self;
 }
 
 #[derive(IntoElement)]
@@ -159,6 +162,13 @@ impl LabelCommon for LabelLike {
         self.single_line = true;
         self
     }
+
+    fn buffer_font(mut self, cx: &App) -> Self {
+        self.base = self
+            .base
+            .font(theme::ThemeSettings::get_global(cx).buffer_font.clone());
+        self
+    }
 }
 
 impl ParentElement for LabelLike {

crates/ui/src/components/popover_menu.rs πŸ”—

@@ -3,8 +3,8 @@
 use std::{cell::RefCell, rc::Rc};
 
 use gpui::{
-    anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnyElement, App, Bounds,
-    Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable as _,
+    anchored, deferred, div, point, prelude::FluentBuilder, px, size, AnyElement, AnyView, App,
+    Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, Focusable as _,
     GlobalElementId, HitboxId, InteractiveElement, IntoElement, LayoutId, Length, ManagedView,
     MouseDownEvent, ParentElement, Pixels, Point, Style, Window,
 };
@@ -178,6 +178,28 @@ impl<M: ManagedView> PopoverMenu<M> {
         self
     }
 
+    pub fn trigger_with_tooltip<T: PopoverTrigger + ButtonCommon>(
+        mut self,
+        t: T,
+        tooltip_builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
+    ) -> Self {
+        let on_open = self.on_open.clone();
+        self.child_builder = Some(Box::new(move |menu, builder| {
+            let open = menu.borrow().is_some();
+            t.toggle_state(open)
+                .when_some(builder, |el, builder| {
+                    el.on_click(move |_, window, cx| {
+                        show_menu(&builder, &menu, on_open.clone(), window, cx)
+                    })
+                    .when(!open, |t| {
+                        t.tooltip(move |window, cx| tooltip_builder(window, cx))
+                    })
+                })
+                .into_any_element()
+        }));
+        self
+    }
+
     /// anchor defines which corner of the menu to anchor to the attachment point
     /// (by default the cursor position, but see attach)
     pub fn anchor(mut self, anchor: Corner) -> Self {

crates/ui/src/components/radio.rs πŸ”—

@@ -4,9 +4,6 @@ use std::sync::Arc;
 
 use crate::prelude::*;
 
-/// A [`Checkbox`] that has a [`Label`].
-///
-/// [`Checkbox`]: crate::components::Checkbox
 #[derive(IntoElement)]
 pub struct RadioWithLabel {
     id: ElementId,

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

@@ -15,7 +15,6 @@ mod list_item;
 mod tab;
 mod tab_bar;
 mod toggle_button;
-mod tool_strip;
 
 pub use avatar::*;
 pub use button::*;
@@ -31,4 +30,3 @@ pub use list_item::*;
 pub use tab::*;
 pub use tab_bar::*;
 pub use toggle_button::*;
-pub use tool_strip::*;

crates/ui/src/components/stories/tool_strip.rs πŸ”—

@@ -1,33 +0,0 @@
-use gpui::Render;
-use story::{Story, StoryItem, StorySection};
-
-use crate::{prelude::*, ToolStrip, Tooltip};
-
-pub struct ToolStripStory;
-
-impl Render for ToolStripStory {
-    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        Story::container()
-            .child(Story::title_for::<ToolStrip>())
-            .child(
-                StorySection::new().child(StoryItem::new(
-                    "Vertical Tool Strip",
-                    h_flex().child(
-                        ToolStrip::vertical("tool_strip_example")
-                            .tool(
-                                IconButton::new("example_tool", IconName::AudioOn)
-                                    .tooltip(Tooltip::text("Example tool")),
-                            )
-                            .tool(
-                                IconButton::new("example_tool_2", IconName::MicMute)
-                                    .tooltip(Tooltip::text("Example tool 2")),
-                            )
-                            .tool(
-                                IconButton::new("example_tool_3", IconName::Screen)
-                                    .tooltip(Tooltip::text("Example tool 3")),
-                            ),
-                    ),
-                )),
-            )
-    }
-}

crates/ui/src/components/tab.rs πŸ”—

@@ -27,7 +27,7 @@ pub enum TabCloseSide {
     End,
 }
 
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Tab {
     div: Stateful<Div>,
     selected: bool,
@@ -171,3 +171,48 @@ impl RenderOnce for Tab {
             )
     }
 }
+
+impl ComponentPreview for Tab {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![example_group_with_title(
+                "Variations",
+                vec![
+                    single_example(
+                        "Default",
+                        Tab::new("default").child("Default Tab").into_any_element(),
+                    ),
+                    single_example(
+                        "Selected",
+                        Tab::new("selected")
+                            .toggle_state(true)
+                            .child("Selected Tab")
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "First",
+                        Tab::new("first")
+                            .position(TabPosition::First)
+                            .child("First Tab")
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Middle",
+                        Tab::new("middle")
+                            .position(TabPosition::Middle(Ordering::Equal))
+                            .child("Middle Tab")
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Last",
+                        Tab::new("last")
+                            .position(TabPosition::Last)
+                            .child("Last Tab")
+                            .into_any_element(),
+                    ),
+                ],
+            )])
+            .into_any_element()
+    }
+}

crates/ui/src/components/table.rs πŸ”—

@@ -2,7 +2,7 @@ use crate::{prelude::*, Indicator};
 use gpui::{div, AnyElement, FontWeight, IntoElement, Length};
 
 /// A table component
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Table {
     column_headers: Vec<SharedString>,
     rows: Vec<Vec<TableCell>>,
@@ -152,88 +152,110 @@ where
 }
 
 impl ComponentPreview for Table {
-    fn description() -> impl Into<Option<&'static str>> {
-        "Used for showing tabular data. Tables may show both text and elements in their cells."
-    }
-
-    fn example_label_side() -> ExampleLabelSide {
-        ExampleLabelSide::Top
-    }
-
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![
-            example_group(vec![
-                single_example(
-                    "Simple Table",
-                    Table::new(vec!["Name", "Age", "City"])
-                        .width(px(400.))
-                        .row(vec!["Alice", "28", "New York"])
-                        .row(vec!["Bob", "32", "San Francisco"])
-                        .row(vec!["Charlie", "25", "London"]),
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "Basic Tables",
+                    vec![
+                        single_example(
+                            "Simple Table",
+                            Table::new(vec!["Name", "Age", "City"])
+                                .width(px(400.))
+                                .row(vec!["Alice", "28", "New York"])
+                                .row(vec!["Bob", "32", "San Francisco"])
+                                .row(vec!["Charlie", "25", "London"])
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Two Column Table",
+                            Table::new(vec!["Category", "Value"])
+                                .width(px(300.))
+                                .row(vec!["Revenue", "$100,000"])
+                                .row(vec!["Expenses", "$75,000"])
+                                .row(vec!["Profit", "$25,000"])
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Styled Tables",
+                    vec![
+                        single_example(
+                            "Default",
+                            Table::new(vec!["Product", "Price", "Stock"])
+                                .width(px(400.))
+                                .row(vec!["Laptop", "$999", "In Stock"])
+                                .row(vec!["Phone", "$599", "Low Stock"])
+                                .row(vec!["Tablet", "$399", "Out of Stock"])
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Striped",
+                            Table::new(vec!["Product", "Price", "Stock"])
+                                .width(px(400.))
+                                .striped()
+                                .row(vec!["Laptop", "$999", "In Stock"])
+                                .row(vec!["Phone", "$599", "Low Stock"])
+                                .row(vec!["Tablet", "$399", "Out of Stock"])
+                                .row(vec!["Headphones", "$199", "In Stock"])
+                                .into_any_element(),
+                        ),
+                    ],
                 ),
-                single_example(
-                    "Two Column Table",
-                    Table::new(vec!["Category", "Value"])
-                        .width(px(300.))
-                        .row(vec!["Revenue", "$100,000"])
-                        .row(vec!["Expenses", "$75,000"])
-                        .row(vec!["Profit", "$25,000"]),
+                example_group_with_title(
+                    "Mixed Content Table",
+                    vec![single_example(
+                        "Table with Elements",
+                        Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
+                            .width(px(840.))
+                            .row(vec![
+                                element_cell(
+                                    Indicator::dot().color(Color::Success).into_any_element(),
+                                ),
+                                string_cell("Project A"),
+                                string_cell("High"),
+                                string_cell("2023-12-31"),
+                                element_cell(
+                                    Button::new("view_a", "View")
+                                        .style(ButtonStyle::Filled)
+                                        .full_width()
+                                        .into_any_element(),
+                                ),
+                            ])
+                            .row(vec![
+                                element_cell(
+                                    Indicator::dot().color(Color::Warning).into_any_element(),
+                                ),
+                                string_cell("Project B"),
+                                string_cell("Medium"),
+                                string_cell("2024-03-15"),
+                                element_cell(
+                                    Button::new("view_b", "View")
+                                        .style(ButtonStyle::Filled)
+                                        .full_width()
+                                        .into_any_element(),
+                                ),
+                            ])
+                            .row(vec![
+                                element_cell(
+                                    Indicator::dot().color(Color::Error).into_any_element(),
+                                ),
+                                string_cell("Project C"),
+                                string_cell("Low"),
+                                string_cell("2024-06-30"),
+                                element_cell(
+                                    Button::new("view_c", "View")
+                                        .style(ButtonStyle::Filled)
+                                        .full_width()
+                                        .into_any_element(),
+                                ),
+                            ])
+                            .into_any_element(),
+                    )],
                 ),
-            ]),
-            example_group(vec![single_example(
-                "Striped Table",
-                Table::new(vec!["Product", "Price", "Stock"])
-                    .width(px(600.))
-                    .striped()
-                    .row(vec!["Laptop", "$999", "In Stock"])
-                    .row(vec!["Phone", "$599", "Low Stock"])
-                    .row(vec!["Tablet", "$399", "Out of Stock"])
-                    .row(vec!["Headphones", "$199", "In Stock"]),
-            )]),
-            example_group_with_title(
-                "Mixed Content Table",
-                vec![single_example(
-                    "Table with Elements",
-                    Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
-                        .width(px(840.))
-                        .row(vec![
-                            element_cell(Indicator::dot().color(Color::Success).into_any_element()),
-                            string_cell("Project A"),
-                            string_cell("High"),
-                            string_cell("2023-12-31"),
-                            element_cell(
-                                Button::new("view_a", "View")
-                                    .style(ButtonStyle::Filled)
-                                    .full_width()
-                                    .into_any_element(),
-                            ),
-                        ])
-                        .row(vec![
-                            element_cell(Indicator::dot().color(Color::Warning).into_any_element()),
-                            string_cell("Project B"),
-                            string_cell("Medium"),
-                            string_cell("2024-03-15"),
-                            element_cell(
-                                Button::new("view_b", "View")
-                                    .style(ButtonStyle::Filled)
-                                    .full_width()
-                                    .into_any_element(),
-                            ),
-                        ])
-                        .row(vec![
-                            element_cell(Indicator::dot().color(Color::Error).into_any_element()),
-                            string_cell("Project C"),
-                            string_cell("Low"),
-                            string_cell("2024-06-30"),
-                            element_cell(
-                                Button::new("view_c", "View")
-                                    .style(ButtonStyle::Filled)
-                                    .full_width()
-                                    .into_any_element(),
-                            ),
-                        ]),
-                )],
-            ),
-        ]
+            ])
+            .into_any_element()
     }
 }

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

@@ -1,5 +1,6 @@
 use gpui::{
-    div, hsla, prelude::*, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled, Window,
+    div, hsla, prelude::*, AnyElement, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled,
+    Window,
 };
 use std::sync::Arc;
 
@@ -38,7 +39,8 @@ pub enum ToggleStyle {
 /// Checkboxes are used for multiple choices, not for mutually exclusive choices.
 /// Each checkbox works independently from other checkboxes in the list,
 /// therefore checking an additional box does not affect any other selections.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
 pub struct Checkbox {
     id: ElementId,
     toggle_state: ToggleState,
@@ -237,7 +239,8 @@ impl RenderOnce for Checkbox {
 }
 
 /// A [`Checkbox`] that has a [`Label`].
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
 pub struct CheckboxWithLabel {
     id: ElementId,
     label: Label,
@@ -314,7 +317,8 @@ impl RenderOnce for CheckboxWithLabel {
 /// # Switch
 ///
 /// Switches are used to represent opposite states, such as enabled or disabled.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "input")]
 pub struct Switch {
     id: ElementId,
     toggle_state: ToggleState,
@@ -429,6 +433,7 @@ impl RenderOnce for Switch {
         h_flex()
             .id(self.id)
             .gap(DynamicSpacing::Base06.rems(cx))
+            .cursor_pointer()
             .child(switch)
             .when_some(
                 self.on_click.filter(|_| !self.disabled),
@@ -445,286 +450,249 @@ impl RenderOnce for Switch {
     }
 }
 
+/// A [`Switch`] that has a [`Label`].
+#[derive(IntoElement)]
+// #[component(scope = "input")]
+pub struct SwitchWithLabel {
+    id: ElementId,
+    label: Label,
+    toggle_state: ToggleState,
+    on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
+    disabled: bool,
+}
+
+impl SwitchWithLabel {
+    /// Creates a switch with an attached label.
+    pub fn new(
+        id: impl Into<ElementId>,
+        label: Label,
+        toggle_state: impl Into<ToggleState>,
+        on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            label,
+            toggle_state: toggle_state.into(),
+            on_click: Arc::new(on_click),
+            disabled: false,
+        }
+    }
+
+    /// Sets the disabled state of the [`SwitchWithLabel`].
+    pub fn disabled(mut self, disabled: bool) -> Self {
+        self.disabled = disabled;
+        self
+    }
+}
+
+impl RenderOnce for SwitchWithLabel {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        h_flex()
+            .id(SharedString::from(format!("{}-container", self.id)))
+            .gap(DynamicSpacing::Base08.rems(cx))
+            .child(
+                Switch::new(self.id.clone(), self.toggle_state)
+                    .disabled(self.disabled)
+                    .on_click({
+                        let on_click = self.on_click.clone();
+                        move |checked, window, cx| {
+                            (on_click)(checked, window, cx);
+                        }
+                    }),
+            )
+            .child(
+                div()
+                    .id(SharedString::from(format!("{}-label", self.id)))
+                    .child(self.label),
+            )
+    }
+}
+
 impl ComponentPreview for Checkbox {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "States",
+                    vec![
+                        single_example(
+                            "Unselected",
+                            Checkbox::new("checkbox_unselected", ToggleState::Unselected)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Indeterminate",
+                            Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Selected",
+                            Checkbox::new("checkbox_selected", ToggleState::Selected)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Styles",
+                    vec![
+                        single_example(
+                            "Default",
+                            Checkbox::new("checkbox_default", ToggleState::Selected)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Filled",
+                            Checkbox::new("checkbox_filled", ToggleState::Selected)
+                                .fill()
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "ElevationBased",
+                            Checkbox::new("checkbox_elevation", ToggleState::Selected)
+                                .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface))
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Custom Color",
+                            Checkbox::new("checkbox_custom", ToggleState::Selected)
+                                .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7)))
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Disabled",
+                    vec![
+                        single_example(
+                            "Unselected",
+                            Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
+                                .disabled(true)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "Selected",
+                            Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
+                                .disabled(true)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "With Label",
+                    vec![single_example(
+                        "Default",
+                        Checkbox::new("checkbox_with_label", ToggleState::Selected)
+                            .label("Always save on quit")
+                            .into_any_element(),
+                    )],
+                ),
+            ])
+            .into_any_element()
     }
+}
 
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![
-            example_group_with_title(
-                "Default",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_unselected", ToggleState::Unselected),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_selected", ToggleState::Selected),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Default (Filled)",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_unselected", ToggleState::Unselected).fill(),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate).fill(),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_selected", ToggleState::Selected).fill(),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "ElevationBased",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_unfilled_unselected", ToggleState::Unselected)
-                            .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new(
-                            "checkbox_unfilled_indeterminate",
-                            ToggleState::Indeterminate,
-                        )
-                        .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_unfilled_selected", ToggleState::Selected)
-                            .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "ElevationBased (Filled)",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_filled_unselected", ToggleState::Unselected)
-                            .fill()
-                            .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new("checkbox_filled_indeterminate", ToggleState::Indeterminate)
-                            .fill()
-                            .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_filled_selected", ToggleState::Selected)
-                            .fill()
-                            .style(ToggleStyle::ElevationBased(ElevationIndex::EditorSurface)),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Custom Color",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_custom_unselected", ToggleState::Unselected)
-                            .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new("checkbox_custom_indeterminate", ToggleState::Indeterminate)
-                            .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_custom_selected", ToggleState::Selected)
-                            .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Custom Color (Filled)",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_custom_filled_unselected", ToggleState::Unselected)
-                            .fill()
-                            .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new(
-                            "checkbox_custom_filled_indeterminate",
-                            ToggleState::Indeterminate,
-                        )
-                        .fill()
-                        .style(ToggleStyle::Custom(hsla(
-                            142.0 / 360.,
-                            0.68,
-                            0.45,
-                            0.7,
-                        ))),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_custom_filled_selected", ToggleState::Selected)
-                            .fill()
-                            .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Disabled",
-                vec![
-                    single_example(
-                        "Unselected",
-                        Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
-                            .disabled(true),
-                    ),
-                    single_example(
-                        "Indeterminate",
-                        Checkbox::new(
-                            "checkbox_disabled_indeterminate",
-                            ToggleState::Indeterminate,
-                        )
-                        .disabled(true),
-                    ),
-                    single_example(
-                        "Selected",
-                        Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
-                            .disabled(true),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Disabled (Filled)",
+impl ComponentPreview for Switch {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![
+                example_group_with_title(
+                    "States",
+                    vec![
+                        single_example(
+                            "Off",
+                            Switch::new("switch_off", ToggleState::Unselected)
+                                .on_click(|_, _, _cx| {})
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "On",
+                            Switch::new("switch_on", ToggleState::Selected)
+                                .on_click(|_, _, _cx| {})
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "Disabled",
+                    vec![
+                        single_example(
+                            "Off",
+                            Switch::new("switch_disabled_off", ToggleState::Unselected)
+                                .disabled(true)
+                                .into_any_element(),
+                        ),
+                        single_example(
+                            "On",
+                            Switch::new("switch_disabled_on", ToggleState::Selected)
+                                .disabled(true)
+                                .into_any_element(),
+                        ),
+                    ],
+                ),
+                example_group_with_title(
+                    "With Label",
+                    vec![
+                        single_example(
+                            "Label",
+                            Switch::new("switch_with_label", ToggleState::Selected)
+                                .label("Always save on quit")
+                                .into_any_element(),
+                        ),
+                        // TODO: Where did theme_preview_keybinding go?
+                        // single_example(
+                        //     "Keybinding",
+                        //     Switch::new("switch_with_keybinding", ToggleState::Selected)
+                        //         .key_binding(theme_preview_keybinding("cmd-shift-e"))
+                        //         .into_any_element(),
+                        // ),
+                    ],
+                ),
+            ])
+            .into_any_element()
+    }
+}
+
+impl ComponentPreview for CheckboxWithLabel {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![example_group_with_title(
+                "States",
                 vec![
                     single_example(
                         "Unselected",
-                        Checkbox::new(
-                            "checkbox_disabled_filled_unselected",
+                        CheckboxWithLabel::new(
+                            "checkbox_with_label_unselected",
+                            Label::new("Always save on quit"),
                             ToggleState::Unselected,
+                            |_, _, _| {},
                         )
-                        .fill()
-                        .disabled(true),
+                        .into_any_element(),
                     ),
                     single_example(
                         "Indeterminate",
-                        Checkbox::new(
-                            "checkbox_disabled_filled_indeterminate",
+                        CheckboxWithLabel::new(
+                            "checkbox_with_label_indeterminate",
+                            Label::new("Always save on quit"),
                             ToggleState::Indeterminate,
+                            |_, _, _| {},
                         )
-                        .fill()
-                        .disabled(true),
+                        .into_any_element(),
                     ),
                     single_example(
                         "Selected",
-                        Checkbox::new("checkbox_disabled_filled_selected", ToggleState::Selected)
-                            .fill()
-                            .disabled(true),
-                    ),
-                ],
-            ),
-        ]
-    }
-}
-
-impl ComponentPreview for Switch {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting."
-    }
-
-    fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![
-            example_group_with_title(
-                "Default",
-                vec![
-                    single_example(
-                        "Off",
-                        Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _, _cx| {}),
-                    ),
-                    single_example(
-                        "On",
-                        Switch::new("switch_on", ToggleState::Selected).on_click(|_, _, _cx| {}),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Disabled",
-                vec![
-                    single_example(
-                        "Off",
-                        Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true),
-                    ),
-                    single_example(
-                        "On",
-                        Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true),
-                    ),
-                ],
-            ),
-            example_group_with_title(
-                "Label Permutations",
-                vec![
-                    single_example(
-                        "Label",
-                        Switch::new("switch_with_label", ToggleState::Selected)
-                            .label("Always save on quit"),
-                    ),
-                    single_example(
-                        "Keybinding",
-                        Switch::new("switch_with_label", ToggleState::Selected)
-                            .key_binding(theme_preview_keybinding("cmd-shift-e")),
+                        CheckboxWithLabel::new(
+                            "checkbox_with_label_selected",
+                            Label::new("Always save on quit"),
+                            ToggleState::Selected,
+                            |_, _, _| {},
+                        )
+                        .into_any_element(),
                     ),
                 ],
-            ),
-        ]
-    }
-}
-
-impl ComponentPreview for CheckboxWithLabel {
-    fn description() -> impl Into<Option<&'static str>> {
-        "A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
-    }
-
-    fn examples(_window: &mut Window, _: &mut App) -> Vec<ComponentExampleGroup<Self>> {
-        vec![example_group(vec![
-            single_example(
-                "Unselected",
-                CheckboxWithLabel::new(
-                    "checkbox_with_label_unselected",
-                    Label::new("Always save on quit"),
-                    ToggleState::Unselected,
-                    |_, _, _| {},
-                ),
-            ),
-            single_example(
-                "Indeterminate",
-                CheckboxWithLabel::new(
-                    "checkbox_with_label_indeterminate",
-                    Label::new("Always save on quit"),
-                    ToggleState::Indeterminate,
-                    |_, _, _| {},
-                ),
-            ),
-            single_example(
-                "Selected",
-                CheckboxWithLabel::new(
-                    "checkbox_with_label_selected",
-                    Label::new("Always save on quit"),
-                    ToggleState::Selected,
-                    |_, _, _| {},
-                ),
-            ),
-        ])]
+            )])
+            .into_any_element()
     }
 }

crates/ui/src/components/tool_strip.rs πŸ”—

@@ -1,58 +0,0 @@
-#![allow(missing_docs)]
-
-use gpui::Axis;
-
-use crate::prelude::*;
-
-#[derive(IntoElement)]
-pub struct ToolStrip {
-    id: ElementId,
-    tools: Vec<IconButton>,
-    axis: Axis,
-}
-
-impl ToolStrip {
-    fn new(id: ElementId, axis: Axis) -> Self {
-        Self {
-            id,
-            tools: vec![],
-            axis,
-        }
-    }
-
-    pub fn vertical(id: impl Into<ElementId>) -> Self {
-        Self::new(id.into(), Axis::Vertical)
-    }
-
-    pub fn tools(mut self, tools: Vec<IconButton>) -> Self {
-        self.tools = tools;
-        self
-    }
-
-    pub fn tool(mut self, tool: IconButton) -> Self {
-        self.tools.push(tool);
-        self
-    }
-}
-
-impl RenderOnce for ToolStrip {
-    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let group = format!("tool_strip_{}", self.id.clone());
-
-        div()
-            .id(self.id.clone())
-            .group(group)
-            .map(|element| match self.axis {
-                Axis::Vertical => element.v_flex(),
-                Axis::Horizontal => element.h_flex(),
-            })
-            .flex_none()
-            .gap(DynamicSpacing::Base04.rems(cx))
-            .p(DynamicSpacing::Base02.rems(cx))
-            .border_1()
-            .border_color(cx.theme().colors().border)
-            .rounded(rems_from_px(6.0))
-            .bg(cx.theme().colors().elevated_surface_background)
-            .children(self.tools)
-    }
-}

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

@@ -1,12 +1,13 @@
 #![allow(missing_docs)]
 
-use gpui::{Action, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
+use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render};
 use settings::Settings;
 use theme::ThemeSettings;
 
 use crate::prelude::*;
 use crate::{h_flex, v_flex, Color, KeyBinding, Label, LabelSize, StyledExt};
 
+#[derive(IntoComponent)]
 pub struct Tooltip {
     title: SharedString,
     meta: Option<SharedString>,
@@ -35,6 +36,22 @@ impl Tooltip {
         }
     }
 
+    pub fn for_action_title(
+        title: impl Into<SharedString>,
+        action: &dyn Action,
+    ) -> impl Fn(&mut Window, &mut App) -> AnyView {
+        let title = title.into();
+        let action = action.boxed_clone();
+        move |window, cx| {
+            cx.new(|_| Self {
+                title: title.clone(),
+                meta: None,
+                key_binding: KeyBinding::for_action(action.as_ref(), window),
+            })
+            .into()
+        }
+    }
+
     pub fn for_action(
         title: impl Into<SharedString>,
         action: &dyn Action,
@@ -188,3 +205,15 @@ impl Render for LinkPreview {
         })
     }
 }
+
+impl ComponentPreview for Tooltip {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        example_group(vec![single_example(
+            "Text only",
+            Button::new("delete-example", "Delete")
+                .tooltip(Tooltip::text("This is a tooltip!"))
+                .into_any_element(),
+        )])
+        .into_any_element()
+    }
+}

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

@@ -6,9 +6,11 @@ pub use gpui::{
     InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, Window,
 };
 
+pub use component::{example_group, example_group_with_title, single_example, ComponentPreview};
+pub use ui_macros::IntoComponent;
+
 pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle, StyledTypography, TextSize};
 pub use crate::traits::clickable::*;
-pub use crate::traits::component_preview::*;
 pub use crate::traits::disableable::*;
 pub use crate::traits::fixed::*;
 pub use crate::traits::styled_ext::*;

crates/ui/src/styles/typography.rs πŸ”—

@@ -1,5 +1,7 @@
+use crate::prelude::*;
 use gpui::{
-    div, rems, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, Window,
+    div, rems, AnyElement, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled,
+    Window,
 };
 use settings::Settings;
 use theme::{ActiveTheme, ThemeSettings};
@@ -188,7 +190,7 @@ impl HeadlineSize {
 
 /// A headline element, used to emphasize some text and
 /// create a visual hierarchy.
-#[derive(IntoElement)]
+#[derive(IntoElement, IntoComponent)]
 pub struct Headline {
     size: HeadlineSize,
     text: SharedString,
@@ -230,3 +232,44 @@ impl Headline {
         self
     }
 }
+
+impl ComponentPreview for Headline {
+    fn preview(_window: &mut Window, _cx: &App) -> AnyElement {
+        v_flex()
+            .gap_6()
+            .children(vec![example_group_with_title(
+                "Headline Sizes",
+                vec![
+                    single_example(
+                        "XLarge",
+                        Headline::new("XLarge Headline")
+                            .size(HeadlineSize::XLarge)
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Large",
+                        Headline::new("Large Headline")
+                            .size(HeadlineSize::Large)
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "Medium (Default)",
+                        Headline::new("Medium Headline").into_any_element(),
+                    ),
+                    single_example(
+                        "Small",
+                        Headline::new("Small Headline")
+                            .size(HeadlineSize::Small)
+                            .into_any_element(),
+                    ),
+                    single_example(
+                        "XSmall",
+                        Headline::new("XSmall Headline")
+                            .size(HeadlineSize::XSmall)
+                            .into_any_element(),
+                    ),
+                ],
+            )])
+            .into_any_element()
+    }
+}

crates/ui/src/traits/component_preview.rs πŸ”—

@@ -1,205 +0,0 @@
-#![allow(missing_docs)]
-use crate::{prelude::*, KeyBinding};
-use gpui::{AnyElement, SharedString};
-
-/// Which side of the preview to show labels on
-#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ExampleLabelSide {
-    /// Left side
-    Left,
-    /// Right side
-    Right,
-    #[default]
-    /// Top side
-    Top,
-    /// Bottom side
-    Bottom,
-}
-
-/// Implement this trait to enable rich UI previews with metadata in the Theme Preview tool.
-pub trait ComponentPreview: IntoElement {
-    fn title() -> &'static str {
-        std::any::type_name::<Self>()
-    }
-
-    fn description() -> impl Into<Option<&'static str>> {
-        None
-    }
-
-    fn example_label_side() -> ExampleLabelSide {
-        ExampleLabelSide::default()
-    }
-
-    fn examples(_window: &mut Window, _cx: &mut App) -> Vec<ComponentExampleGroup<Self>>;
-
-    fn custom_example(_window: &mut Window, _cx: &mut App) -> impl Into<Option<AnyElement>> {
-        None::<AnyElement>
-    }
-
-    fn component_previews(window: &mut Window, cx: &mut App) -> Vec<AnyElement> {
-        Self::examples(window, cx)
-            .into_iter()
-            .map(|example| Self::render_example_group(example))
-            .collect()
-    }
-
-    fn render_component_previews(window: &mut Window, cx: &mut App) -> AnyElement {
-        let title = Self::title();
-        let (source, title) = title
-            .rsplit_once("::")
-            .map_or((None, title), |(s, t)| (Some(s), t));
-        let description = Self::description().into();
-
-        v_flex()
-            .w_full()
-            .gap_6()
-            .p_4()
-            .border_1()
-            .border_color(cx.theme().colors().border)
-            .rounded_md()
-            .child(
-                v_flex()
-                    .gap_1()
-                    .child(
-                        h_flex()
-                            .gap_1()
-                            .child(Headline::new(title).size(HeadlineSize::Small))
-                            .when_some(source, |this, source| {
-                                this.child(Label::new(format!("({})", source)).color(Color::Muted))
-                            }),
-                    )
-                    .when_some(description, |this, description| {
-                        this.child(
-                            div()
-                                .text_ui_sm(cx)
-                                .text_color(cx.theme().colors().text_muted)
-                                .max_w(px(600.0))
-                                .child(description),
-                        )
-                    }),
-            )
-            .when_some(
-                Self::custom_example(window, cx).into(),
-                |this, custom_example| this.child(custom_example),
-            )
-            .children(Self::component_previews(window, cx))
-            .into_any_element()
-    }
-
-    fn render_example_group(group: ComponentExampleGroup<Self>) -> AnyElement {
-        v_flex()
-            .gap_6()
-            .when(group.grow, |this| this.w_full().flex_1())
-            .when_some(group.title, |this, title| {
-                this.child(Label::new(title).size(LabelSize::Small))
-            })
-            .child(
-                h_flex()
-                    .w_full()
-                    .gap_6()
-                    .children(group.examples.into_iter().map(Self::render_example))
-                    .into_any_element(),
-            )
-            .into_any_element()
-    }
-
-    fn render_example(example: ComponentExample<Self>) -> AnyElement {
-        let base = div().flex();
-
-        let base = match Self::example_label_side() {
-            ExampleLabelSide::Right => base.flex_row(),
-            ExampleLabelSide::Left => base.flex_row_reverse(),
-            ExampleLabelSide::Bottom => base.flex_col(),
-            ExampleLabelSide::Top => base.flex_col_reverse(),
-        };
-
-        base.gap_1()
-            .when(example.grow, |this| this.flex_1())
-            .child(example.element)
-            .child(
-                Label::new(example.variant_name)
-                    .size(LabelSize::XSmall)
-                    .color(Color::Muted),
-            )
-            .into_any_element()
-    }
-}
-
-/// A single example of a component.
-pub struct ComponentExample<T> {
-    variant_name: SharedString,
-    element: T,
-    grow: bool,
-}
-
-impl<T> ComponentExample<T> {
-    /// Create a new example with the given variant name and example value.
-    pub fn new(variant_name: impl Into<SharedString>, example: T) -> Self {
-        Self {
-            variant_name: variant_name.into(),
-            element: example,
-            grow: false,
-        }
-    }
-
-    /// Set the example to grow to fill the available horizontal space.
-    pub fn grow(mut self) -> Self {
-        self.grow = true;
-        self
-    }
-}
-
-/// A group of component examples.
-pub struct ComponentExampleGroup<T> {
-    pub title: Option<SharedString>,
-    pub examples: Vec<ComponentExample<T>>,
-    pub grow: bool,
-}
-
-impl<T> ComponentExampleGroup<T> {
-    /// Create a new group of examples with the given title.
-    pub fn new(examples: Vec<ComponentExample<T>>) -> Self {
-        Self {
-            title: None,
-            examples,
-            grow: false,
-        }
-    }
-
-    /// Create a new group of examples with the given title.
-    pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample<T>>) -> Self {
-        Self {
-            title: Some(title.into()),
-            examples,
-            grow: false,
-        }
-    }
-
-    /// Set the group to grow to fill the available horizontal space.
-    pub fn grow(mut self) -> Self {
-        self.grow = true;
-        self
-    }
-}
-
-/// Create a single example
-pub fn single_example<T>(variant_name: impl Into<SharedString>, example: T) -> ComponentExample<T> {
-    ComponentExample::new(variant_name, example)
-}
-
-/// Create a group of examples without a title
-pub fn example_group<T>(examples: Vec<ComponentExample<T>>) -> ComponentExampleGroup<T> {
-    ComponentExampleGroup::new(examples)
-}
-
-/// Create a group of examples with a title
-pub fn example_group_with_title<T>(
-    title: impl Into<SharedString>,
-    examples: Vec<ComponentExample<T>>,
-) -> ComponentExampleGroup<T> {
-    ComponentExampleGroup::with_title(title, examples)
-}
-
-pub fn theme_preview_keybinding(keystrokes: &str) -> KeyBinding {
-    KeyBinding::new(gpui::KeyBinding::new(keystrokes, gpui::NoAction {}, None))
-}

crates/ui_macros/Cargo.toml πŸ”—

@@ -13,7 +13,8 @@ path = "src/ui_macros.rs"
 proc-macro = true
 
 [dependencies]
+convert_case.workspace = true
+linkme.workspace = true
 proc-macro2.workspace = true
 quote.workspace = true
 syn.workspace = true
-convert_case.workspace = true

crates/ui_macros/src/derive_component.rs πŸ”—

@@ -0,0 +1,97 @@
+use convert_case::{Case, Casing};
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput, Lit, Meta, MetaList, MetaNameValue, NestedMeta};
+
+pub fn derive_into_component(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    let mut scope_val = None;
+    let mut description_val = None;
+
+    for attr in &input.attrs {
+        if attr.path.is_ident("component") {
+            if let Ok(Meta::List(MetaList { nested, .. })) = attr.parse_meta() {
+                for item in nested {
+                    if let NestedMeta::Meta(Meta::NameValue(MetaNameValue {
+                        path,
+                        lit: Lit::Str(s),
+                        ..
+                    })) = item
+                    {
+                        let ident = path.get_ident().map(|i| i.to_string()).unwrap_or_default();
+                        if ident == "scope" {
+                            scope_val = Some(s.value());
+                        } else if ident == "description" {
+                            description_val = Some(s.value());
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    let name = &input.ident;
+
+    let scope_impl = if let Some(s) = scope_val {
+        quote! {
+            fn scope() -> Option<&'static str> {
+                Some(#s)
+            }
+        }
+    } else {
+        quote! {
+            fn scope() -> Option<&'static str> {
+                None
+            }
+        }
+    };
+
+    let description_impl = if let Some(desc) = description_val {
+        quote! {
+            fn description() -> Option<&'static str> {
+                Some(#desc)
+            }
+        }
+    } else {
+        quote! {}
+    };
+
+    let register_component_name = syn::Ident::new(
+        &format!(
+            "__register_component_{}",
+            Casing::to_case(&name.to_string(), Case::Snake)
+        ),
+        name.span(),
+    );
+    let register_preview_name = syn::Ident::new(
+        &format!(
+            "__register_preview_{}",
+            Casing::to_case(&name.to_string(), Case::Snake)
+        ),
+        name.span(),
+    );
+
+    let expanded = quote! {
+        impl component::Component for #name {
+            #scope_impl
+
+            fn name() -> &'static str {
+                stringify!(#name)
+            }
+
+            #description_impl
+        }
+
+        #[linkme::distributed_slice(component::__ALL_COMPONENTS)]
+        fn #register_component_name() {
+            component::register_component::<#name>();
+        }
+
+        #[linkme::distributed_slice(component::__ALL_PREVIEWS)]
+        fn #register_preview_name() {
+            component::register_preview::<#name>();
+        }
+    };
+
+    expanded.into()
+}

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

@@ -1,3 +1,4 @@
+mod derive_component;
 mod derive_path_str;
 mod dynamic_spacing;
 
@@ -58,3 +59,27 @@ pub fn path_str(_args: TokenStream, input: TokenStream) -> TokenStream {
 pub fn derive_dynamic_spacing(input: TokenStream) -> TokenStream {
     dynamic_spacing::derive_spacing(input)
 }
+
+/// Derives the `Component` trait for a struct.
+///
+/// This macro generates implementations for the `Component` trait and associated
+/// registration functions for the component system.
+///
+/// # Attributes
+///
+/// - `#[component(scope = "...")]`: Required. Specifies the scope of the component.
+/// - `#[component(description = "...")]`: Optional. Provides a description for the component.
+///
+/// # Example
+///
+/// ```
+/// use ui_macros::Component;
+///
+/// #[derive(Component)]
+/// #[component(scope = "toggle", description = "A element that can be toggled on and off")]
+/// struct Checkbox;
+/// ```
+#[proc_macro_derive(IntoComponent, attributes(component))]
+pub fn derive_component(input: TokenStream) -> TokenStream {
+    derive_component::derive_into_component(input)
+}

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

@@ -105,7 +105,7 @@ impl<T: AsRef<Path>> PathExt for T {
 /// leverages Rust's type system to ensure that all paths entering Zed are always "sanitized" by removing the `\\\\?\\` prefix.
 /// On non-Windows operating systems, this struct is effectively a no-op.
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct SanitizedPath(Arc<Path>);
+pub struct SanitizedPath(pub Arc<Path>);
 
 impl SanitizedPath {
     pub fn starts_with(&self, prefix: &SanitizedPath) -> bool {

crates/vim/src/object.rs πŸ”—

@@ -407,6 +407,9 @@ impl Object {
         if let Some(range) = self.range(map, selection.clone(), around) {
             selection.start = range.start;
             selection.end = range.end;
+            if !around && self.is_multiline() {
+                preserve_indented_newline(map, selection);
+            }
             true
         } else {
             false
@@ -414,6 +417,49 @@ impl Object {
     }
 }
 
+/// Returns a range without the final newline char.
+///
+/// If the selection spans multiple lines and is preceded by an opening brace (`{`),
+/// this function will trim the selection to exclude the final newline
+/// in order to preserve a properly indented line.
+fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
+    let (start_point, end_point) = (selection.start.to_point(map), selection.end.to_point(map));
+
+    if start_point.row == end_point.row {
+        return;
+    }
+
+    let start_offset = selection.start.to_offset(map, Bias::Left);
+    let mut pos = start_offset;
+
+    while pos > 0 {
+        pos -= 1;
+        let current_char = map.buffer_chars_at(pos).next().map(|(ch, _)| ch);
+
+        match current_char {
+            Some(ch) if !ch.is_whitespace() => break,
+            Some('\n') if pos > 0 => {
+                let prev_char = map.buffer_chars_at(pos - 1).next().map(|(ch, _)| ch);
+                if prev_char == Some('{') {
+                    let end_pos = selection.end.to_offset(map, Bias::Left);
+                    for (ch, offset) in map.reverse_buffer_chars_at(end_pos) {
+                        match ch {
+                            '\n' => {
+                                selection.end = offset.to_display_point(map);
+                                break;
+                            }
+                            ch if !ch.is_whitespace() => break,
+                            _ => continue,
+                        }
+                    }
+                }
+                break;
+            }
+            _ => continue,
+        }
+    }
+}
+
 /// Returns a range that surrounds the word `relative_to` is in.
 ///
 /// If `relative_to` is at the start of a word, return the word.
@@ -1333,20 +1379,38 @@ fn surrounding_markers(
     }
 
     if !around && search_across_lines {
+        // Handle trailing newline after opening
         if let Some((ch, range)) = movement::chars_after(map, opening.end).next() {
             if ch == '\n' {
-                opening.end = range.end
+                opening.end = range.end;
+
+                // After newline, skip leading whitespace
+                let mut chars = movement::chars_after(map, opening.end).peekable();
+                while let Some((ch, range)) = chars.peek() {
+                    if !ch.is_whitespace() {
+                        break;
+                    }
+                    opening.end = range.end;
+                    chars.next();
+                }
             }
         }
 
+        // Handle leading whitespace before closing
+        let mut last_newline_end = None;
         for (ch, range) in movement::chars_before(map, closing.start) {
             if !ch.is_whitespace() {
                 break;
             }
-            if ch != '\n' {
-                closing.start = range.start
+            if ch == '\n' {
+                last_newline_end = Some(range.end);
+                break;
             }
         }
+        // Adjust closing.start to exclude whitespace after a newline, if present
+        if let Some(end) = last_newline_end {
+            closing.start = end;
+        }
     }
 
     let result = if around {
@@ -1681,60 +1745,46 @@ mod test {
 
     #[gpui::test]
     async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
-        let mut cx = NeovimBackedTestContext::new(cx).await;
+        let mut cx = VimTestContext::new(cx, true).await;
 
-        cx.set_shared_state(indoc! {
-            "func empty(a string) bool {
-               if a == \"\" {
-                  return true
-               }
-               Λ‡return false
-            }"
-        })
-        .await;
-        cx.simulate_shared_keystrokes("v i {").await;
-        cx.shared_state().await.assert_eq(indoc! {"
-            func empty(a string) bool {
-            Β«   if a == \"\" {
-                  return true
-               }
-               return false
-            Λ‡Β»}"});
-        cx.set_shared_state(indoc! {
-            "func empty(a string) bool {
-                 if a == \"\" {
-                     Λ‡return true
-                 }
-                 return false
-            }"
-        })
-        .await;
-        cx.simulate_shared_keystrokes("v i {").await;
-        cx.shared_state().await.assert_eq(indoc! {"
-            func empty(a string) bool {
-                 if a == \"\" {
-            Β«         return true
-            Λ‡Β»     }
-                 return false
-            }"});
+        cx.set_state(
+            indoc! {
+                "func empty(a string) bool {
+                   if a == \"\" {
+                      return true
+                   }
+                   Λ‡return false
+                }"
+            },
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("v i {");
 
-        cx.set_shared_state(indoc! {
-            "func empty(a string) bool {
-                 if a == \"\" Λ‡{
-                     return true
-                 }
-                 return false
-            }"
-        })
-        .await;
-        cx.simulate_shared_keystrokes("v i {").await;
-        cx.shared_state().await.assert_eq(indoc! {"
-            func empty(a string) bool {
-                 if a == \"\" {
-            Β«         return true
-            Λ‡Β»     }
-                 return false
-            }"});
+        cx.set_state(
+            indoc! {
+                "func empty(a string) bool {
+                     if a == \"\" {
+                         Λ‡return true
+                     }
+                     return false
+                }"
+            },
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("v i {");
+
+        cx.set_state(
+            indoc! {
+                "func empty(a string) bool {
+                     if a == \"\" Λ‡{
+                         return true
+                     }
+                     return false
+                }"
+            },
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("v i {");
     }
 
     #[gpui::test]
@@ -2254,6 +2304,20 @@ mod test {
         }
     }
 
+    #[gpui::test]
+    async fn test_anybrackets_trailing_space(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("(trailingˇ whitespace          )")
+            .await;
+        cx.simulate_shared_keystrokes("v i b").await;
+        cx.shared_state().await.assert_matches();
+        cx.simulate_shared_keystrokes("escape y i b").await;
+        cx.shared_clipboard()
+            .await
+            .assert_eq("trailing whitespace          ");
+    }
+
     #[gpui::test]
     async fn test_tags(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new_html(cx).await;

crates/vim/test_data/test_anybrackets_trailing_space.json πŸ”—

@@ -0,0 +1,11 @@
+{"Put":{"state":"(trailingˇ whitespace          )"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"b"}
+{"Get":{"state":"(Β«trailing whitespace          Λ‡Β»)","mode":"Visual"}}
+{"Key":"escape"}
+{"Key":"y"}
+{"Key":"i"}
+{"Key":"b"}
+{"Get":{"state":"(Λ‡trailing whitespace          )","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"trailing whitespace          "}}

crates/workspace/Cargo.toml πŸ”—

@@ -34,6 +34,7 @@ call.workspace = true
 client.workspace = true
 clock.workspace = true
 collections.workspace = true
+component.workspace = true
 db.workspace = true
 derive_more.workspace = true
 fs.workspace = true

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

@@ -448,6 +448,14 @@ pub mod simple_message_notification {
             self
         }
 
+        pub fn primary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
+        where
+            F: 'static + Fn(&mut Window, &mut Context<Self>),
+        {
+            self.primary_on_click = Some(on_click);
+            self
+        }
+
         pub fn secondary_message<S>(mut self, message: S) -> Self
         where
             S: Into<SharedString>,
@@ -474,6 +482,14 @@ pub mod simple_message_notification {
             self
         }
 
+        pub fn secondary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
+        where
+            F: 'static + Fn(&mut Window, &mut Context<Self>),
+        {
+            self.secondary_on_click = Some(on_click);
+            self
+        }
+
         pub fn more_info_message<S>(mut self, message: S) -> Self
         where
             S: Into<SharedString>,

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

@@ -441,10 +441,9 @@ impl Pane {
                     .gap(DynamicSpacing::Base04.rems(cx))
                     .child(
                         PopoverMenu::new("pane-tab-bar-popover-menu")
-                            .trigger(
-                                IconButton::new("plus", IconName::Plus)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("New...")),
+                            .trigger_with_tooltip(
+                                IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),
+                                Tooltip::text("New..."),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(pane.new_item_context_menu_handle.clone())
@@ -474,10 +473,10 @@ impl Pane {
                     )
                     .child(
                         PopoverMenu::new("pane-tab-bar-split")
-                            .trigger(
+                            .trigger_with_tooltip(
                                 IconButton::new("split", IconName::Split)
-                                    .icon_size(IconSize::Small)
-                                    .tooltip(Tooltip::text("Split Pane")),
+                                    .icon_size(IconSize::Small),
+                                Tooltip::text("Split Pane"),
                             )
                             .anchor(Corner::TopRight)
                             .with_handle(pane.split_item_context_menu_handle.clone())

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

@@ -27,7 +27,6 @@ pub fn init(cx: &mut App) {
 enum ThemePreviewPage {
     Overview,
     Typography,
-    Components,
 }
 
 impl ThemePreviewPage {
@@ -35,7 +34,6 @@ impl ThemePreviewPage {
         match self {
             Self::Overview => "Overview",
             Self::Typography => "Typography",
-            Self::Components => "Components",
         }
     }
 }
@@ -64,9 +62,6 @@ impl ThemePreview {
             ThemePreviewPage::Typography => {
                 self.render_typography_page(window, cx).into_any_element()
             }
-            ThemePreviewPage::Components => {
-                self.render_components_page(window, cx).into_any_element()
-            }
         }
     }
 }
@@ -392,28 +387,6 @@ impl ThemePreview {
             )
     }
 
-    fn render_components_page(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let layer = ElevationIndex::Surface;
-
-        v_flex()
-            .id("theme-preview-components")
-            .overflow_scroll()
-            .size_full()
-            .gap_2()
-            .child(Button::render_component_previews(window, cx))
-            .child(Checkbox::render_component_previews(window, cx))
-            .child(CheckboxWithLabel::render_component_previews(window, cx))
-            .child(ContentGroup::render_component_previews(window, cx))
-            .child(DecoratedIcon::render_component_previews(window, cx))
-            .child(Facepile::render_component_previews(window, cx))
-            .child(Icon::render_component_previews(window, cx))
-            .child(IconDecoration::render_component_previews(window, cx))
-            .child(KeybindingHint::render_component_previews(window, cx))
-            .child(Indicator::render_component_previews(window, cx))
-            .child(Switch::render_component_previews(window, cx))
-            .child(Table::render_component_previews(window, cx))
-    }
-
     fn render_page_nav(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         h_flex()
             .id("theme-preview-nav")

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

@@ -148,6 +148,7 @@ actions!(
         Open,
         OpenFiles,
         OpenInTerminal,
+        OpenComponentPreview,
         ReloadActiveItem,
         SaveAs,
         SaveWithoutFormat,
@@ -378,6 +379,7 @@ fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, c
 
 pub fn init(app_state: Arc<AppState>, cx: &mut App) {
     init_settings(cx);
+    component::init();
     theme_preview::init(cx);
 
     cx.on_action(Workspace::close_global);
@@ -4440,10 +4442,12 @@ impl Workspace {
         if let Some(focus_on) = focus_on {
             focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
         } else {
-            self.panes
-                .last()
-                .unwrap()
-                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
+            if self.active_pane() == pane {
+                self.panes
+                    .last()
+                    .unwrap()
+                    .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
+            }
         }
         if self.last_active_center_pane == Some(pane.downgrade()) {
             self.last_active_center_pane = None;

crates/worktree/Cargo.toml πŸ”—

@@ -14,11 +14,12 @@ workspace = true
 
 [features]
 test-support = [
+    "gpui/test-support",
+    "http_client/test-support",
     "language/test-support",
     "settings/test-support",
     "text/test-support",
-    "gpui/test-support",
-    "http_client/test-support",
+    "util/test-support",
 ]
 
 [dependencies]
@@ -59,3 +60,4 @@ pretty_assertions.workspace = true
 rand.workspace = true
 rpc = { workspace = true, features = ["test-support"] }
 settings = { workspace = true, features = ["test-support"] }
+util = { workspace = true, features = ["test-support"] }

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

@@ -213,12 +213,6 @@ impl Deref for RepositoryEntry {
     }
 }
 
-impl AsRef<Path> for RepositoryEntry {
-    fn as_ref(&self) -> &Path {
-        &self.path
-    }
-}
-
 impl RepositoryEntry {
     pub fn branch(&self) -> Option<Arc<str>> {
         self.branch.clone()
@@ -326,33 +320,53 @@ impl RepositoryEntry {
 /// But if a sub-folder of a git repository is opened, this corresponds to the
 /// project root and the .git folder is located in a parent directory.
 #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
-pub struct WorkDirectory {
-    path: Arc<Path>,
-
-    /// If location_in_repo is set, it means the .git folder is external
-    /// and in a parent folder of the project root.
-    /// In that case, the work_directory field will point to the
-    /// project-root and location_in_repo contains the location of the
-    /// project-root in the repository.
-    ///
-    /// Example:
-    ///
-    ///     my_root_folder/          <-- repository root
-    ///       .git
-    ///       my_sub_folder_1/
-    ///         project_root/        <-- Project root, Zed opened here
-    ///           ...
-    ///
-    /// For this setup, the attributes will have the following values:
-    ///
-    ///     work_directory: pointing to "" entry
-    ///     location_in_repo: Some("my_sub_folder_1/project_root")
-    pub(crate) location_in_repo: Option<Arc<Path>>,
+pub enum WorkDirectory {
+    InProject {
+        relative_path: Arc<Path>,
+    },
+    AboveProject {
+        absolute_path: Arc<Path>,
+        location_in_repo: Arc<Path>,
+    },
 }
 
 impl WorkDirectory {
-    pub fn path_key(&self) -> PathKey {
-        PathKey(self.path.clone())
+    #[cfg(test)]
+    fn in_project(path: &str) -> Self {
+        let path = Path::new(path);
+        Self::InProject {
+            relative_path: path.into(),
+        }
+    }
+
+    #[cfg(test)]
+    fn canonicalize(&self) -> Self {
+        match self {
+            WorkDirectory::InProject { relative_path } => WorkDirectory::InProject {
+                relative_path: relative_path.clone(),
+            },
+            WorkDirectory::AboveProject {
+                absolute_path,
+                location_in_repo,
+            } => WorkDirectory::AboveProject {
+                absolute_path: absolute_path.canonicalize().unwrap().into(),
+                location_in_repo: location_in_repo.clone(),
+            },
+        }
+    }
+
+    pub fn is_above_project(&self) -> bool {
+        match self {
+            WorkDirectory::InProject { .. } => false,
+            WorkDirectory::AboveProject { .. } => true,
+        }
+    }
+
+    fn path_key(&self) -> PathKey {
+        match self {
+            WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()),
+            WorkDirectory::AboveProject { .. } => PathKey(Path::new("").into()),
+        }
     }
 
     /// Returns true if the given path is a child of the work directory.
@@ -360,9 +374,14 @@ impl WorkDirectory {
     /// Note that the path may not be a member of this repository, if there
     /// is a repository in a directory between these two paths
     /// external .git folder in a parent folder of the project root.
+    #[track_caller]
     pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool {
         let path = path.as_ref();
-        path.starts_with(&self.path)
+        debug_assert!(path.is_relative());
+        match self {
+            WorkDirectory::InProject { relative_path } => path.starts_with(relative_path),
+            WorkDirectory::AboveProject { .. } => true,
+        }
     }
 
     /// relativize returns the given project path relative to the root folder of the
@@ -371,53 +390,71 @@ impl WorkDirectory {
     /// of the project root folder, then the returned RepoPath is relative to the root
     /// of the repository and not a valid path inside the project.
     pub fn relativize(&self, path: &Path) -> Result<RepoPath> {
-        let repo_path = if let Some(location_in_repo) = &self.location_in_repo {
-            // Avoid joining a `/` to location_in_repo in the case of a single-file worktree.
-            if path == Path::new("") {
-                RepoPath(location_in_repo.clone())
-            } else {
-                location_in_repo.join(path).into()
+        // path is assumed to be relative to worktree root.
+        debug_assert!(path.is_relative());
+        match self {
+            WorkDirectory::InProject { relative_path } => Ok(path
+                .strip_prefix(relative_path)
+                .map_err(|_| {
+                    anyhow!(
+                        "could not relativize {:?} against {:?}",
+                        path,
+                        relative_path
+                    )
+                })?
+                .into()),
+            WorkDirectory::AboveProject {
+                location_in_repo, ..
+            } => {
+                // Avoid joining a `/` to location_in_repo in the case of a single-file worktree.
+                if path == Path::new("") {
+                    Ok(RepoPath(location_in_repo.clone()))
+                } else {
+                    Ok(location_in_repo.join(path).into())
+                }
             }
-        } else {
-            path.strip_prefix(&self.path)
-                .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, self.path))?
-                .into()
-        };
-        Ok(repo_path)
+        }
     }
 
     /// This is the opposite operation to `relativize` above
     pub fn unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {
-        if let Some(location) = &self.location_in_repo {
-            // If we fail to strip the prefix, that means this status entry is
-            // external to this worktree, and we definitely won't have an entry_id
-            path.strip_prefix(location).ok().map(Into::into)
-        } else {
-            Some(self.path.join(path).into())
+        match self {
+            WorkDirectory::InProject { relative_path } => Some(relative_path.join(path).into()),
+            WorkDirectory::AboveProject {
+                location_in_repo, ..
+            } => {
+                // If we fail to strip the prefix, that means this status entry is
+                // external to this worktree, and we definitely won't have an entry_id
+                path.strip_prefix(location_in_repo).ok().map(Into::into)
+            }
         }
     }
-}
 
-impl Default for WorkDirectory {
-    fn default() -> Self {
-        Self {
-            path: Arc::from(Path::new("")),
-            location_in_repo: None,
+    pub fn display_name(&self) -> String {
+        match self {
+            WorkDirectory::InProject { relative_path } => relative_path.display().to_string(),
+            WorkDirectory::AboveProject {
+                absolute_path,
+                location_in_repo,
+            } => {
+                let num_of_dots = location_in_repo.components().count();
+
+                "../".repeat(num_of_dots)
+                    + &absolute_path
+                        .file_name()
+                        .map(|s| s.to_string_lossy())
+                        .unwrap_or_default()
+                    + "/"
+            }
         }
     }
 }
 
-impl Deref for WorkDirectory {
-    type Target = Path;
-
-    fn deref(&self) -> &Self::Target {
-        self.as_ref()
-    }
-}
-
-impl AsRef<Path> for WorkDirectory {
-    fn as_ref(&self) -> &Path {
-        self.path.as_ref()
+impl Default for WorkDirectory {
+    fn default() -> Self {
+        Self::InProject {
+            relative_path: Arc::from(Path::new("")),
+        }
     }
 }
 
@@ -487,7 +524,7 @@ impl sum_tree::Item for LocalRepositoryEntry {
 
     fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
         PathSummary {
-            max_path: self.work_directory.path.clone(),
+            max_path: self.work_directory.path_key().0,
             item_summary: Unit,
         }
     }
@@ -497,7 +534,7 @@ impl KeyedItem for LocalRepositoryEntry {
     type Key = PathKey;
 
     fn key(&self) -> Self::Key {
-        PathKey(self.work_directory.path.clone())
+        self.work_directory.path_key()
     }
 }
 
@@ -1395,16 +1432,7 @@ impl LocalWorktree {
                             drop(barrier);
                         }
                         ScanState::RootUpdated { new_path } => {
-                            if let Some(new_path) = new_path {
-                                this.snapshot.git_repositories = Default::default();
-                                this.snapshot.ignores_by_parent_abs_path = Default::default();
-                                let root_name = new_path
-                                    .as_path()
-                                    .file_name()
-                                    .map_or(String::new(), |f| f.to_string_lossy().to_string());
-                                this.snapshot.update_abs_path(new_path, root_name);
-                            }
-                            this.restart_background_scanners(cx);
+                            this.update_abs_path_and_refresh(new_path, cx);
                         }
                     }
                     cx.notify();
@@ -1844,6 +1872,10 @@ impl LocalWorktree {
         }))
     }
 
+    /// Rename an entry.
+    ///
+    /// `new_path` is the new relative path to the worktree root.
+    /// If the root entry is renamed then `new_path` is the new root name instead.
     fn rename_entry(
         &self,
         entry_id: ProjectEntryId,
@@ -1856,8 +1888,18 @@ impl LocalWorktree {
         };
         let new_path = new_path.into();
         let abs_old_path = self.absolutize(&old_path);
-        let Ok(abs_new_path) = self.absolutize(&new_path) else {
-            return Task::ready(Err(anyhow!("absolutizing path {new_path:?}")));
+
+        let is_root_entry = self.root_entry().is_some_and(|e| e.id == entry_id);
+        let abs_new_path = if is_root_entry {
+            let Some(root_parent_path) = self.abs_path().parent() else {
+                return Task::ready(Err(anyhow!("no parent for path {:?}", self.abs_path)));
+            };
+            root_parent_path.join(&new_path)
+        } else {
+            let Ok(absolutize_path) = self.absolutize(&new_path) else {
+                return Task::ready(Err(anyhow!("absolutizing path {new_path:?}")));
+            };
+            absolutize_path
         };
         let abs_path = abs_new_path.clone();
         let fs = self.fs.clone();
@@ -1891,9 +1933,19 @@ impl LocalWorktree {
             rename.await?;
             Ok(this
                 .update(&mut cx, |this, cx| {
-                    this.as_local_mut()
-                        .unwrap()
-                        .refresh_entry(new_path.clone(), Some(old_path), cx)
+                    let local = this.as_local_mut().unwrap();
+                    if is_root_entry {
+                        // We eagerly update `abs_path` and refresh this worktree.
+                        // Otherwise, the FS watcher would do it on the `RootUpdated` event,
+                        // but with a noticeable delay, so we handle it proactively.
+                        local.update_abs_path_and_refresh(
+                            Some(SanitizedPath::from(abs_path.clone())),
+                            cx,
+                        );
+                        Task::ready(Ok(this.root_entry().cloned()))
+                    } else {
+                        local.refresh_entry(new_path.clone(), Some(old_path), cx)
+                    }
                 })?
                 .await?
                 .map(CreatedEntry::Included)
@@ -2158,6 +2210,23 @@ impl LocalWorktree {
         self.share_private_files = true;
         self.restart_background_scanners(cx);
     }
+
+    fn update_abs_path_and_refresh(
+        &mut self,
+        new_path: Option<SanitizedPath>,
+        cx: &Context<Worktree>,
+    ) {
+        if let Some(new_path) = new_path {
+            self.snapshot.git_repositories = Default::default();
+            self.snapshot.ignores_by_parent_abs_path = Default::default();
+            let root_name = new_path
+                .as_path()
+                .file_name()
+                .map_or(String::new(), |f| f.to_string_lossy().to_string());
+            self.snapshot.update_abs_path(new_path, root_name);
+        }
+        self.restart_background_scanners(cx);
+    }
 }
 
 impl RemoteWorktree {
@@ -2574,12 +2643,11 @@ impl Snapshot {
                     self.repositories.insert_or_replace(
                         RepositoryEntry {
                             work_directory_id,
-                            work_directory: WorkDirectory {
-                                path: work_dir_entry.path.clone(),
-                                // When syncing repository entries from a peer, we don't need
-                                // the location_in_repo field, since git operations don't happen locally
-                                // anyway.
-                                location_in_repo: None,
+                            // When syncing repository entries from a peer, we don't need
+                            // the location_in_repo field, since git operations don't happen locally
+                            // anyway.
+                            work_directory: WorkDirectory::InProject {
+                                relative_path: work_dir_entry.path.clone(),
                             },
                             branch: repository.branch.map(Into::into),
                             statuses_by_path: statuses,
@@ -2690,23 +2758,13 @@ impl Snapshot {
         &self.repositories
     }
 
-    pub fn repositories_with_abs_paths(
-        &self,
-    ) -> impl '_ + Iterator<Item = (&RepositoryEntry, PathBuf)> {
-        let base = self.abs_path();
-        self.repositories.iter().map(|repo| {
-            let path = repo.work_directory.location_in_repo.as_deref();
-            let path = path.unwrap_or(repo.work_directory.as_ref());
-            (repo, base.join(path))
-        })
-    }
-
     /// Get the repository whose work directory corresponds to the given path.
     pub(crate) fn repository(&self, work_directory: PathKey) -> Option<RepositoryEntry> {
         self.repositories.get(&work_directory, &()).cloned()
     }
 
     /// Get the repository whose work directory contains the given path.
+    #[track_caller]
     pub fn repository_for_path(&self, path: &Path) -> Option<&RepositoryEntry> {
         self.repositories
             .iter()
@@ -2716,6 +2774,7 @@ impl Snapshot {
 
     /// Given an ordered iterator of entries, returns an iterator of those entries,
     /// along with their containing git repository.
+    #[track_caller]
     pub fn entries_with_repositories<'a>(
         &'a self,
         entries: impl 'a + Iterator<Item = &'a Entry>,
@@ -2817,6 +2876,7 @@ impl Snapshot {
 
     pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
         let path = path.as_ref();
+        debug_assert!(path.is_relative());
         self.traverse_from_path(true, true, true, path)
             .entry()
             .and_then(|entry| {
@@ -3080,7 +3140,7 @@ impl LocalSnapshot {
         let work_dir_paths = self
             .repositories
             .iter()
-            .map(|repo| repo.work_directory.path.clone())
+            .map(|repo| repo.work_directory.path_key())
             .collect::<HashSet<_>>();
         assert_eq!(dotgit_paths.len(), work_dir_paths.len());
         assert_eq!(self.repositories.iter().count(), work_dir_paths.len());
@@ -3288,7 +3348,7 @@ impl BackgroundScannerState {
             .git_repositories
             .retain(|id, _| removed_ids.binary_search(id).is_err());
         self.snapshot.repositories.retain(&(), |repository| {
-            !repository.work_directory.starts_with(path)
+            !repository.work_directory.path_key().0.starts_with(path)
         });
 
         #[cfg(test)]
@@ -3326,20 +3386,26 @@ impl BackgroundScannerState {
             }
         };
 
-        self.insert_git_repository_for_path(work_dir_path, dot_git_path, None, fs, watcher)
+        self.insert_git_repository_for_path(
+            WorkDirectory::InProject {
+                relative_path: work_dir_path,
+            },
+            dot_git_path,
+            fs,
+            watcher,
+        )
     }
 
     fn insert_git_repository_for_path(
         &mut self,
-        work_dir_path: Arc<Path>,
+        work_directory: WorkDirectory,
         dot_git_path: Arc<Path>,
-        location_in_repo: Option<Arc<Path>>,
         fs: &dyn Fs,
         watcher: &dyn Watcher,
     ) -> Option<LocalRepositoryEntry> {
         let work_dir_id = self
             .snapshot
-            .entry_for_path(work_dir_path.clone())
+            .entry_for_path(work_directory.path_key().0)
             .map(|entry| entry.id)?;
 
         if self.snapshot.git_repositories.get(&work_dir_id).is_some() {
@@ -3373,10 +3439,6 @@ impl BackgroundScannerState {
         };
 
         log::trace!("constructed libgit2 repo in {:?}", t0.elapsed());
-        let work_directory = WorkDirectory {
-            path: work_dir_path.clone(),
-            location_in_repo,
-        };
 
         if let Some(git_hosting_provider_registry) = self.git_hosting_provider_registry.clone() {
             git_hosting_providers::register_additional_providers(
@@ -3839,7 +3901,7 @@ impl sum_tree::Item for RepositoryEntry {
 
     fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {
         PathSummary {
-            max_path: self.work_directory.path.clone(),
+            max_path: self.work_directory.path_key().0,
             item_summary: Unit,
         }
     }
@@ -3849,7 +3911,7 @@ impl sum_tree::KeyedItem for RepositoryEntry {
     type Key = PathKey;
 
     fn key(&self) -> Self::Key {
-        PathKey(self.work_directory.path.clone())
+        self.work_directory.path_key()
     }
 }
 
@@ -4088,7 +4150,7 @@ impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId {
     }
 }
 
-#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
+#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
 pub struct PathKey(Arc<Path>);
 
 impl Default for PathKey {
@@ -4167,15 +4229,15 @@ impl BackgroundScanner {
                         // We associate the external git repo with our root folder and
                         // also mark where in the git repo the root folder is located.
                         self.state.lock().insert_git_repository_for_path(
-                            Path::new("").into(),
-                            ancestor_dot_git.into(),
-                            Some(
-                                root_abs_path
+                            WorkDirectory::AboveProject {
+                                absolute_path: ancestor.into(),
+                                location_in_repo: root_abs_path
                                     .as_path()
                                     .strip_prefix(ancestor)
                                     .unwrap()
                                     .into(),
-                            ),
+                            },
+                            ancestor_dot_git.into(),
                             self.fs.as_ref(),
                             self.watcher.as_ref(),
                         );
@@ -4401,6 +4463,14 @@ impl BackgroundScanner {
                         return false;
                     };
 
+                if abs_path.0.file_name() == Some(*GITIGNORE) {
+                    for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&relative_path)) {
+                        if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.dot_git_dir_abs_path.as_ref()) {
+                            dot_git_abs_paths.push(repo.dot_git_dir_abs_path.to_path_buf());
+                        }
+                    }
+                }
+
                 let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
                     snapshot
                         .entry_for_path(parent)
@@ -4984,7 +5054,7 @@ impl BackgroundScanner {
                 snapshot
                     .snapshot
                     .repositories
-                    .remove(&PathKey(repository.work_directory.path.clone()), &());
+                    .remove(&repository.work_directory.path_key(), &());
                 return Some(());
             }
         }
@@ -5169,8 +5239,12 @@ impl BackgroundScanner {
 
                 let local_repository = match existing_repository_entry {
                     None => {
+                        let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path())
+                        else {
+                            return;
+                        };
                         match state.insert_git_repository(
-                            dot_git_dir.into(),
+                            relative.into(),
                             self.fs.as_ref(),
                             self.watcher.as_ref(),
                         ) {
@@ -5274,7 +5348,7 @@ impl BackgroundScanner {
     fn update_git_statuses(&self, job: UpdateGitStatusesJob) {
         log::trace!(
             "updating git statuses for repo {:?}",
-            job.local_repository.work_directory.path
+            job.local_repository.work_directory.display_name()
         );
         let t0 = Instant::now();
 
@@ -5288,7 +5362,7 @@ impl BackgroundScanner {
         };
         log::trace!(
             "computed git statuses for repo {:?} in {:?}",
-            job.local_repository.work_directory.path,
+            job.local_repository.work_directory.display_name(),
             t0.elapsed()
         );
 
@@ -5299,8 +5373,8 @@ impl BackgroundScanner {
         let Some(mut repository) =
             snapshot.repository(job.local_repository.work_directory.path_key())
         else {
-            log::error!("Got an UpdateGitStatusesJob for a repository that isn't in the snapshot");
-            debug_assert!(false);
+            // happens when a folder is deleted
+            log::debug!("Got an UpdateGitStatusesJob for a repository that isn't in the snapshot");
             return;
         };
 
@@ -5352,7 +5426,7 @@ impl BackgroundScanner {
 
         log::trace!(
             "applied git status updates for repo {:?} in {:?}",
-            job.local_repository.work_directory.path,
+            job.local_repository.work_directory.display_name(),
             t0.elapsed(),
         );
     }

crates/worktree/src/worktree_tests.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{
-    worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot, Worktree,
-    WorktreeModelHandle,
+    worktree_settings::WorktreeSettings, Entry, EntryKind, Event, PathChange, Snapshot,
+    WorkDirectory, Worktree, WorktreeModelHandle,
 };
 use anyhow::Result;
 use fs::{FakeFs, Fs, RealFs, RemoveOptions};
@@ -2200,7 +2200,10 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
     cx.read(|cx| {
         let tree = tree.read(cx);
         let repo = tree.repositories().iter().next().unwrap();
-        assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
+        assert_eq!(
+            repo.work_directory,
+            WorkDirectory::in_project("projects/project1")
+        );
         assert_eq!(
             tree.status_for_file(Path::new("projects/project1/a")),
             Some(StatusCode::Modified.worktree()),
@@ -2221,7 +2224,10 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
     cx.read(|cx| {
         let tree = tree.read(cx);
         let repo = tree.repositories().iter().next().unwrap();
-        assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
+        assert_eq!(
+            repo.work_directory,
+            WorkDirectory::in_project("projects/project2")
+        );
         assert_eq!(
             tree.status_for_file(Path::new("projects/project2/a")),
             Some(StatusCode::Modified.worktree()),
@@ -2275,12 +2281,15 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
         assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
 
         let repo = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
-        assert_eq!(repo.path.as_ref(), Path::new("dir1"));
+        assert_eq!(repo.work_directory, WorkDirectory::in_project("dir1"));
 
         let repo = tree
             .repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
             .unwrap();
-        assert_eq!(repo.path.as_ref(), Path::new("dir1/deps/dep1"));
+        assert_eq!(
+            repo.work_directory,
+            WorkDirectory::in_project("dir1/deps/dep1")
+        );
 
         let entries = tree.files(false, 0);
 
@@ -2289,7 +2298,7 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
             .map(|(entry, repo)| {
                 (
                     entry.path.as_ref(),
-                    repo.map(|repo| repo.path.to_path_buf()),
+                    repo.map(|repo| repo.work_directory.clone()),
                 )
             })
             .collect::<Vec<_>>();
@@ -2300,9 +2309,12 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
                 (Path::new("c.txt"), None),
                 (
                     Path::new("dir1/deps/dep1/src/a.txt"),
-                    Some(Path::new("dir1/deps/dep1").into())
+                    Some(WorkDirectory::in_project("dir1/deps/dep1"))
+                ),
+                (
+                    Path::new("dir1/src/b.txt"),
+                    Some(WorkDirectory::in_project("dir1"))
                 ),
-                (Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
             ]
         );
     });
@@ -2408,8 +2420,10 @@ async fn test_file_status(cx: &mut TestAppContext) {
         let snapshot = tree.snapshot();
         assert_eq!(snapshot.repositories().iter().count(), 1);
         let repo_entry = snapshot.repositories().iter().next().unwrap();
-        assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
-        assert!(repo_entry.location_in_repo.is_none());
+        assert_eq!(
+            repo_entry.work_directory,
+            WorkDirectory::in_project("project")
+        );
 
         assert_eq!(
             snapshot.status_for_file(project_path.join(B_TXT)),
@@ -2760,15 +2774,14 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
         let snapshot = tree.snapshot();
         assert_eq!(snapshot.repositories().iter().count(), 1);
         let repo = snapshot.repositories().iter().next().unwrap();
-        // Path is blank because the working directory of
-        // the git repository is located at the root of the project
-        assert_eq!(repo.path.as_ref(), Path::new(""));
-
-        // This is the missing path between the root of the project (sub-folder-2) and its
-        // location relative to the root of the repository.
         assert_eq!(
-            repo.location_in_repo,
-            Some(Arc::from(Path::new("sub-folder-1/sub-folder-2")))
+            repo.work_directory.canonicalize(),
+            WorkDirectory::AboveProject {
+                absolute_path: Arc::from(root.path().join("my-repo").canonicalize().unwrap()),
+                location_in_repo: Arc::from(Path::new(util::separator!(
+                    "sub-folder-1/sub-folder-2"
+                )))
+            }
         );
 
         assert_eq!(snapshot.status_for_file("c.txt"), None);

crates/zed/Cargo.toml πŸ”—

@@ -39,6 +39,7 @@ collab_ui.workspace = true
 collections.workspace = true
 command_palette.workspace = true
 command_palette_hooks.workspace = true
+component_preview.workspace = true
 copilot.workspace = true
 db.workspace = true
 diagnostics.workspace = true
@@ -54,8 +55,8 @@ file_icons.workspace = true
 fs.workspace = true
 futures.workspace = true
 git.workspace = true
-git_ui.workspace = true
 git_hosting_providers.workspace = true
+git_ui.workspace = true
 go_to_line.workspace = true
 gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] }
 gpui_tokio.workspace = true
@@ -126,7 +127,6 @@ url.workspace = true
 urlencoding = "2.1.2"
 util.workspace = true
 uuid.workspace = true
-vcs_menu.workspace = true
 vim.workspace = true
 vim_mode_setting.workspace = true
 welcome.workspace = true

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

@@ -490,6 +490,7 @@ fn main() {
         project_panel::init(Assets, cx);
         git_ui::git_panel::init(cx);
         outline_panel::init(Assets, cx);
+        component_preview::init(cx);
         tasks_ui::init(cx);
         snippets_ui::init(cx);
         channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
@@ -505,7 +506,6 @@ fn main() {
         notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
         collab_ui::init(&app_state, cx);
         git_ui::init(cx);
-        vcs_menu::init(cx);
         feedback::init(cx);
         markdown_preview::init(cx);
         welcome::init(cx);

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

@@ -1217,25 +1217,29 @@ fn show_keymap_migration_notification_if_needed(
     if !KeymapFile::should_migrate_keymap(keymap_file) {
         return false;
     }
-    show_app_notification(notification_id, cx, move |cx| {
-        cx.new(move |_cx| {
-            let message = "A newer version of Zed has simplified several keymaps. Your existing keymaps may be deprecated. You can migrate them by clicking below. A backup will be created in your home directory.";
-            let button_text = "Backup and Migrate Keymap";
-            MessageNotification::new_from_builder(move |_, _| {
-                gpui::div().text_xs().child(message).into_any()
-            })
-            .primary_message(button_text)
-            .primary_on_click(move |_, cx| {
-                let fs = <dyn Fs>::global(cx);
-                cx.spawn(move |weak_notification, mut cx| async move {
-                    KeymapFile::migrate_keymap(fs).await.ok();
-                    weak_notification.update(&mut cx, |_, cx| {
+    let message = MarkdownString(format!(
+        "Keymap migration needed, as the format for some actions has changed. \
+        You can migrate your keymap by clicking below. A backup will be created at {}.",
+        MarkdownString::inline_code(&paths::keymap_backup_file().to_string_lossy())
+    ));
+    show_markdown_app_notification(
+        notification_id,
+        message,
+        "Backup and Migrate Keymap".into(),
+        move |_, cx| {
+            let fs = <dyn Fs>::global(cx);
+            cx.spawn(move |weak_notification, mut cx| async move {
+                KeymapFile::migrate_keymap(fs).await.ok();
+                weak_notification
+                    .update(&mut cx, |_, cx| {
                         cx.emit(DismissEvent);
-                    }).ok();
-                }).detach();
+                    })
+                    .ok();
             })
-        })
-    });
+            .detach();
+        },
+        cx,
+    );
     return true;
 }
 
@@ -1247,33 +1251,55 @@ fn show_settings_migration_notification_if_needed(
     if !SettingsStore::should_migrate_settings(&settings) {
         return;
     }
-    show_app_notification(notification_id, cx, move |cx| {
-        cx.new(move |_cx| {
-            let message = "A newer version of Zed has updated some settings. Your existing settings may be deprecated. You can migrate them by clicking below. A backup will be created in your home directory.";
-            let button_text = "Backup and Migrate Settings";
-            MessageNotification::new_from_builder(move |_, _| {
-                gpui::div().text_xs().child(message).into_any()
-            })
-            .primary_message(button_text)
-            .primary_on_click(move |_, cx| {
-                let fs = <dyn Fs>::global(cx);
-                cx.update_global(|store: &mut SettingsStore, _| store.migrate_settings(fs));
-                cx.emit(DismissEvent);
-            })
-        })
-    });
+    let message = MarkdownString(format!(
+        "Settings migration needed, as the format for some settings has changed. \
+            You can migrate your settings by clicking below. A backup will be created at {}.",
+        MarkdownString::inline_code(&paths::settings_backup_file().to_string_lossy())
+    ));
+    show_markdown_app_notification(
+        notification_id,
+        message,
+        "Backup and Migrate Settings".into(),
+        move |_, cx| {
+            let fs = <dyn Fs>::global(cx);
+            cx.update_global(|store: &mut SettingsStore, _| store.migrate_settings(fs));
+            cx.emit(DismissEvent);
+        },
+        cx,
+    );
 }
 
 fn show_keymap_file_load_error(
     notification_id: NotificationId,
-    markdown_error_message: MarkdownString,
+    error_message: MarkdownString,
     cx: &mut App,
 ) {
+    show_markdown_app_notification(
+        notification_id.clone(),
+        error_message,
+        "Open Keymap File".into(),
+        |window, cx| {
+            window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
+            cx.emit(DismissEvent);
+        },
+        cx,
+    )
+}
+
+fn show_markdown_app_notification<F>(
+    notification_id: NotificationId,
+    message: MarkdownString,
+    primary_button_message: SharedString,
+    primary_button_on_click: F,
+    cx: &mut App,
+) where
+    F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
+{
     let parsed_markdown = cx.background_executor().spawn(async move {
         let file_location_directory = None;
         let language_registry = None;
         markdown_preview::markdown_parser::parse_markdown(
-            &markdown_error_message.0,
+            &message.0,
             file_location_directory,
             language_registry,
         )
@@ -1282,10 +1308,14 @@ fn show_keymap_file_load_error(
 
     cx.spawn(move |cx| async move {
         let parsed_markdown = Arc::new(parsed_markdown.await);
+        let primary_button_message = primary_button_message.clone();
+        let primary_button_on_click = Arc::new(primary_button_on_click);
         cx.update(|cx| {
             show_app_notification(notification_id, cx, move |cx| {
                 let workspace_handle = cx.entity().downgrade();
                 let parsed_markdown = parsed_markdown.clone();
+                let primary_button_message = primary_button_message.clone();
+                let primary_button_on_click = primary_button_on_click.clone();
                 cx.new(move |_cx| {
                     MessageNotification::new_from_builder(move |window, cx| {
                         gpui::div()
@@ -1298,11 +1328,8 @@ fn show_keymap_file_load_error(
                             ))
                             .into_any()
                     })
-                    .primary_message("Open Keymap File")
-                    .primary_on_click(|window, cx| {
-                        window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
-                        cx.emit(DismissEvent);
-                    })
+                    .primary_message(primary_button_message)
+                    .primary_on_click_arc(primary_button_on_click)
                 })
             })
         })

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

@@ -168,15 +168,13 @@ impl Render for QuickActionBar {
             let focus = editor.focus_handle(cx);
 
             PopoverMenu::new("editor-selections-dropdown")
-                .trigger(
+                .trigger_with_tooltip(
                     IconButton::new("toggle_editor_selections_icon", IconName::CursorIBeam)
                         .shape(IconButtonShape::Square)
                         .icon_size(IconSize::Small)
                         .style(ButtonStyle::Subtle)
-                        .toggle_state(self.toggle_selections_handle.is_deployed())
-                        .when(!self.toggle_selections_handle.is_deployed(), |this| {
-                            this.tooltip(Tooltip::text("Selection Controls"))
-                        }),
+                        .toggle_state(self.toggle_selections_handle.is_deployed()),
+                    Tooltip::text("Selection Controls"),
                 )
                 .with_handle(self.toggle_selections_handle.clone())
                 .anchor(Corner::TopRight)
@@ -213,38 +211,84 @@ impl Render for QuickActionBar {
                 })
         });
 
+        let editor_focus_handle = editor.focus_handle(cx);
         let editor = editor.downgrade();
         let editor_settings_dropdown = {
             let vim_mode_enabled = VimModeSetting::get_global(cx).0;
 
             PopoverMenu::new("editor-settings")
-                .trigger(
+                .trigger_with_tooltip(
                     IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
                         .shape(IconButtonShape::Square)
                         .icon_size(IconSize::Small)
                         .style(ButtonStyle::Subtle)
-                        .toggle_state(self.toggle_settings_handle.is_deployed())
-                        .when(!self.toggle_settings_handle.is_deployed(), |this| {
-                            this.tooltip(Tooltip::text("Editor Controls"))
-                        }),
+                        .toggle_state(self.toggle_settings_handle.is_deployed()),
+                    Tooltip::text("Editor Controls"),
                 )
                 .anchor(Corner::TopRight)
                 .with_handle(self.toggle_settings_handle.clone())
                 .menu(move |window, cx| {
-                    let menu = ContextMenu::build(window, cx, |mut menu, _, _| {
-                        if supports_inlay_hints {
+                    let menu = ContextMenu::build(window, cx, {
+                        let focus_handle = editor_focus_handle.clone();
+                        |mut menu, _, _| {
+                            menu = menu.context(focus_handle);
+
+                            if supports_inlay_hints {
+                                menu = menu.toggleable_entry(
+                                    "Inlay Hints",
+                                    inlay_hints_enabled,
+                                    IconPosition::Start,
+                                    Some(editor::actions::ToggleInlayHints.boxed_clone()),
+                                    {
+                                        let editor = editor.clone();
+                                        move |window, cx| {
+                                            editor
+                                                .update(cx, |editor, cx| {
+                                                    editor.toggle_inlay_hints(
+                                                        &editor::actions::ToggleInlayHints,
+                                                        window,
+                                                        cx,
+                                                    );
+                                                })
+                                                .ok();
+                                        }
+                                    },
+                                );
+                            }
+
+                            menu = menu.toggleable_entry(
+                                "Selection Menu",
+                                selection_menu_enabled,
+                                IconPosition::Start,
+                                Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
+                                {
+                                    let editor = editor.clone();
+                                    move |window, cx| {
+                                        editor
+                                            .update(cx, |editor, cx| {
+                                                editor.toggle_selection_menu(
+                                                    &editor::actions::ToggleSelectionMenu,
+                                                    window,
+                                                    cx,
+                                                )
+                                            })
+                                            .ok();
+                                    }
+                                },
+                            );
+
                             menu = menu.toggleable_entry(
-                                "Inlay Hints",
-                                inlay_hints_enabled,
+                                "Auto Signature Help",
+                                auto_signature_help_enabled,
                                 IconPosition::Start,
-                                Some(editor::actions::ToggleInlayHints.boxed_clone()),
+                                Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
                                 {
                                     let editor = editor.clone();
                                     move |window, cx| {
                                         editor
                                             .update(cx, |editor, cx| {
-                                                editor.toggle_inlay_hints(
-                                                    &editor::actions::ToggleInlayHints,
+                                                editor.toggle_auto_signature_help_menu(
+                                                    &editor::actions::ToggleAutoSignatureHelp,
                                                     window,
                                                     cx,
                                                 );
@@ -253,138 +297,96 @@ impl Render for QuickActionBar {
                                     }
                                 },
                             );
-                        }
 
-                        menu = menu.toggleable_entry(
-                            "Selection Menu",
-                            selection_menu_enabled,
-                            IconPosition::Start,
-                            Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
-                            {
-                                let editor = editor.clone();
-                                move |window, cx| {
-                                    editor
-                                        .update(cx, |editor, cx| {
-                                            editor.toggle_selection_menu(
-                                                &editor::actions::ToggleSelectionMenu,
-                                                window,
-                                                cx,
-                                            )
-                                        })
-                                        .ok();
-                                }
-                            },
-                        );
-
-                        menu = menu.toggleable_entry(
-                            "Auto Signature Help",
-                            auto_signature_help_enabled,
-                            IconPosition::Start,
-                            Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
-                            {
-                                let editor = editor.clone();
-                                move |window, cx| {
-                                    editor
-                                        .update(cx, |editor, cx| {
-                                            editor.toggle_auto_signature_help_menu(
-                                                &editor::actions::ToggleAutoSignatureHelp,
-                                                window,
-                                                cx,
-                                            );
-                                        })
-                                        .ok();
-                                }
-                            },
-                        );
-
-                        let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions")
-                            .toggleable(IconPosition::Start, inline_completion_enabled && show_inline_completions)
-                            .disabled(!inline_completion_enabled)
-                            .action(Some(
-                                editor::actions::ToggleEditPrediction.boxed_clone(),
-                            )).handler({
-                                let editor = editor.clone();
-                                move |window, cx| {
-                                    editor
-                                        .update(cx, |editor, cx| {
-                                            editor.toggle_inline_completions(
-                                                &editor::actions::ToggleEditPrediction,
-                                                window,
-                                                cx,
-                                            );
-                                        })
-                                        .ok();
-                                }
-                            });
-                        if !inline_completion_enabled {
-                            inline_completion_entry = inline_completion_entry.documentation_aside(|_| {
-                                Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element()
-                            });
-                        }
+                            let mut inline_completion_entry = ContextMenuEntry::new("Edit Predictions")
+                                .toggleable(IconPosition::Start, inline_completion_enabled && show_inline_completions)
+                                .disabled(!inline_completion_enabled)
+                                .action(Some(
+                                    editor::actions::ToggleEditPrediction.boxed_clone(),
+                                )).handler({
+                                    let editor = editor.clone();
+                                    move |window, cx| {
+                                        editor
+                                            .update(cx, |editor, cx| {
+                                                editor.toggle_inline_completions(
+                                                    &editor::actions::ToggleEditPrediction,
+                                                    window,
+                                                    cx,
+                                                );
+                                            })
+                                            .ok();
+                                    }
+                                });
+                            if !inline_completion_enabled {
+                                inline_completion_entry = inline_completion_entry.documentation_aside(|_| {
+                                    Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element()
+                                });
+                            }
+
+                            menu = menu.item(inline_completion_entry);
+
+                            menu = menu.separator();
+
+                            menu = menu.toggleable_entry(
+                                "Inline Git Blame",
+                                git_blame_inline_enabled,
+                                IconPosition::Start,
+                                Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
+                                {
+                                    let editor = editor.clone();
+                                    move |window, cx| {
+                                        editor
+                                            .update(cx, |editor, cx| {
+                                                editor.toggle_git_blame_inline(
+                                                    &editor::actions::ToggleGitBlameInline,
+                                                    window,
+                                                    cx,
+                                                )
+                                            })
+                                            .ok();
+                                    }
+                                },
+                            );
 
-                        menu = menu.item(inline_completion_entry);
-
-                        menu = menu.separator();
-
-                        menu = menu.toggleable_entry(
-                            "Inline Git Blame",
-                            git_blame_inline_enabled,
-                            IconPosition::Start,
-                            Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
-                            {
-                                let editor = editor.clone();
-                                move |window, cx| {
-                                    editor
-                                        .update(cx, |editor, cx| {
-                                            editor.toggle_git_blame_inline(
-                                                &editor::actions::ToggleGitBlameInline,
-                                                window,
-                                                cx,
-                                            )
-                                        })
-                                        .ok();
-                                }
-                            },
-                        );
-
-                        menu = menu.toggleable_entry(
-                            "Column Git Blame",
-                            show_git_blame_gutter,
-                            IconPosition::Start,
-                            Some(editor::actions::ToggleGitBlame.boxed_clone()),
-                            {
-                                let editor = editor.clone();
-                                move |window, cx| {
-                                    editor
-                                        .update(cx, |editor, cx| {
-                                            editor.toggle_git_blame(
-                                                &editor::actions::ToggleGitBlame,
-                                                window,
-                                                cx,
-                                            )
-                                        })
-                                        .ok();
-                                }
-                            },
-                        );
-
-                        menu = menu.separator();
-
-                        menu = menu.toggleable_entry(
-                            "Vim Mode",
-                            vim_mode_enabled,
-                            IconPosition::Start,
-                            None,
-                            {
-                                move |window, cx| {
-                                    let new_value = !vim_mode_enabled;
-                                    VimModeSetting::override_global(VimModeSetting(new_value), cx);
-                                    window.refresh();
-                                }
-                            },
-                        );
-
-                        menu
+                            menu = menu.toggleable_entry(
+                                "Column Git Blame",
+                                show_git_blame_gutter,
+                                IconPosition::Start,
+                                Some(editor::actions::ToggleGitBlame.boxed_clone()),
+                                {
+                                    let editor = editor.clone();
+                                    move |window, cx| {
+                                        editor
+                                            .update(cx, |editor, cx| {
+                                                editor.toggle_git_blame(
+                                                    &editor::actions::ToggleGitBlame,
+                                                    window,
+                                                    cx,
+                                                )
+                                            })
+                                            .ok();
+                                    }
+                                },
+                            );
+
+                            menu = menu.separator();
+
+                            menu = menu.toggleable_entry(
+                                "Vim Mode",
+                                vim_mode_enabled,
+                                IconPosition::Start,
+                                None,
+                                {
+                                    move |window, cx| {
+                                        let new_value = !vim_mode_enabled;
+                                        VimModeSetting::override_global(VimModeSetting(new_value), cx);
+                                        window.refresh();
+                                    }
+                                },
+                            );
+
+                            menu
+                        }
                     });
                     Some(menu)
                 })

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

@@ -209,16 +209,16 @@ impl QuickActionBar {
                 })
                 .into()
             })
-            .trigger(
+            .trigger_with_tooltip(
                 ButtonLike::new_rounded_right(element_id("dropdown"))
                     .child(
                         Icon::new(IconName::ChevronDownSmall)
                             .size(IconSize::XSmall)
                             .color(Color::Muted),
                     )
-                    .tooltip(Tooltip::text("REPL Menu"))
                     .width(rems(1.).into())
                     .disabled(menu_state.popover_disabled),
+                Tooltip::text("REPL Menu"),
             );
 
         let button = ButtonLike::new_rounded_left("toggle_repl_icon")
@@ -343,8 +343,8 @@ impl QuickActionBar {
                                 .color(Color::Muted)
                                 .size(IconSize::XSmall),
                         ),
-                )
-                .tooltip(Tooltip::text("Select Kernel")),
+                ),
+            Tooltip::text("Select Kernel"),
         )
         .with_handle(menu_handle.clone())
         .into_any_element()

crates/zed_actions/src/lib.rs πŸ”—

@@ -47,10 +47,10 @@ actions!(
     ]
 );
 
-pub mod branches {
-    use gpui::actions;
+pub mod git {
+    use gpui::action_with_deprecated_aliases;
 
-    actions!(branches, [OpenRecent]);
+    action_with_deprecated_aliases!(git, Branch, ["branches::OpenRecent"]);
 }
 
 pub mod command_palette {

crates/zeta/src/input_excerpt.rs πŸ”—

@@ -0,0 +1,238 @@
+use crate::{
+    tokens_for_bytes, CURSOR_MARKER, EDITABLE_REGION_END_MARKER, EDITABLE_REGION_START_MARKER,
+    START_OF_FILE_MARKER,
+};
+use language::{BufferSnapshot, Point};
+use std::{fmt::Write, ops::Range};
+
+#[derive(Debug)]
+pub struct InputExcerpt {
+    pub editable_range: Range<Point>,
+    pub prompt: String,
+    pub speculated_output: String,
+}
+
+pub fn excerpt_for_cursor_position(
+    position: Point,
+    path: &str,
+    snapshot: &BufferSnapshot,
+    editable_region_token_limit: usize,
+    context_token_limit: usize,
+) -> InputExcerpt {
+    let mut scope_range = position..position;
+    let mut remaining_edit_tokens = editable_region_token_limit;
+
+    while let Some(parent) = snapshot.syntax_ancestor(scope_range.clone()) {
+        let parent_tokens = tokens_for_bytes(parent.byte_range().len());
+        let parent_point_range = Point::new(
+            parent.start_position().row as u32,
+            parent.start_position().column as u32,
+        )
+            ..Point::new(
+                parent.end_position().row as u32,
+                parent.end_position().column as u32,
+            );
+        if parent_point_range == scope_range {
+            break;
+        } else if parent_tokens <= editable_region_token_limit {
+            scope_range = parent_point_range;
+            remaining_edit_tokens = editable_region_token_limit - parent_tokens;
+        } else {
+            break;
+        }
+    }
+
+    let editable_range = expand_range(snapshot, scope_range, remaining_edit_tokens);
+    let context_range = expand_range(snapshot, editable_range.clone(), context_token_limit);
+
+    let mut prompt = String::new();
+    let mut speculated_output = String::new();
+
+    writeln!(&mut prompt, "```{path}").unwrap();
+    if context_range.start == Point::zero() {
+        writeln!(&mut prompt, "{START_OF_FILE_MARKER}").unwrap();
+    }
+
+    for chunk in snapshot.chunks(context_range.start..editable_range.start, false) {
+        prompt.push_str(chunk.text);
+    }
+
+    push_editable_range(position, snapshot, editable_range.clone(), &mut prompt);
+    push_editable_range(
+        position,
+        snapshot,
+        editable_range.clone(),
+        &mut speculated_output,
+    );
+
+    for chunk in snapshot.chunks(editable_range.end..context_range.end, false) {
+        prompt.push_str(chunk.text);
+    }
+    write!(prompt, "\n```").unwrap();
+
+    InputExcerpt {
+        editable_range,
+        prompt,
+        speculated_output,
+    }
+}
+
+fn push_editable_range(
+    cursor_position: Point,
+    snapshot: &BufferSnapshot,
+    editable_range: Range<Point>,
+    prompt: &mut String,
+) {
+    writeln!(prompt, "{EDITABLE_REGION_START_MARKER}").unwrap();
+    for chunk in snapshot.chunks(editable_range.start..cursor_position, false) {
+        prompt.push_str(chunk.text);
+    }
+    prompt.push_str(CURSOR_MARKER);
+    for chunk in snapshot.chunks(cursor_position..editable_range.end, false) {
+        prompt.push_str(chunk.text);
+    }
+    write!(prompt, "\n{EDITABLE_REGION_END_MARKER}").unwrap();
+}
+
+fn expand_range(
+    snapshot: &BufferSnapshot,
+    range: Range<Point>,
+    mut remaining_tokens: usize,
+) -> Range<Point> {
+    let mut expanded_range = range.clone();
+    expanded_range.start.column = 0;
+    expanded_range.end.column = snapshot.line_len(expanded_range.end.row);
+    loop {
+        let mut expanded = false;
+
+        if remaining_tokens > 0 && expanded_range.start.row > 0 {
+            expanded_range.start.row -= 1;
+            let line_tokens =
+                tokens_for_bytes(snapshot.line_len(expanded_range.start.row) as usize);
+            remaining_tokens = remaining_tokens.saturating_sub(line_tokens);
+            expanded = true;
+        }
+
+        if remaining_tokens > 0 && expanded_range.end.row < snapshot.max_point().row {
+            expanded_range.end.row += 1;
+            expanded_range.end.column = snapshot.line_len(expanded_range.end.row);
+            let line_tokens = tokens_for_bytes(expanded_range.end.column as usize);
+            remaining_tokens = remaining_tokens.saturating_sub(line_tokens);
+            expanded = true;
+        }
+
+        if !expanded {
+            break;
+        }
+    }
+    expanded_range
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::{App, AppContext};
+    use indoc::indoc;
+    use language::{Buffer, Language, LanguageConfig, LanguageMatcher};
+    use std::sync::Arc;
+
+    #[gpui::test]
+    fn test_excerpt_for_cursor_position(cx: &mut App) {
+        let text = indoc! {r#"
+            fn foo() {
+                let x = 42;
+                println!("Hello, world!");
+            }
+
+            fn bar() {
+                let x = 42;
+                let mut sum = 0;
+                for i in 0..x {
+                    sum += i;
+                }
+                println!("Sum: {}", sum);
+                return sum;
+            }
+
+            fn generate_random_numbers() -> Vec<i32> {
+                let mut rng = rand::thread_rng();
+                let mut numbers = Vec::new();
+                for _ in 0..5 {
+                    numbers.push(rng.gen_range(1..101));
+                }
+                numbers
+            }
+        "#};
+        let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
+        let snapshot = buffer.read(cx).snapshot();
+
+        // Ensure we try to fit the largest possible syntax scope, resorting to line-based expansion
+        // when a larger scope doesn't fit the editable region.
+        let excerpt = excerpt_for_cursor_position(Point::new(12, 5), "main.rs", &snapshot, 50, 32);
+        assert_eq!(
+            excerpt.prompt,
+            indoc! {r#"
+            ```main.rs
+                let x = 42;
+                println!("Hello, world!");
+            <|editable_region_start|>
+            }
+
+            fn bar() {
+                let x = 42;
+                let mut sum = 0;
+                for i in 0..x {
+                    sum += i;
+                }
+                println!("Sum: {}", sum);
+                r<|user_cursor_is_here|>eturn sum;
+            }
+
+            fn generate_random_numbers() -> Vec<i32> {
+            <|editable_region_end|>
+                let mut rng = rand::thread_rng();
+                let mut numbers = Vec::new();
+            ```"#}
+        );
+
+        // The `bar` function won't fit within the editable region, so we resort to line-based expansion.
+        let excerpt = excerpt_for_cursor_position(Point::new(12, 5), "main.rs", &snapshot, 40, 32);
+        assert_eq!(
+            excerpt.prompt,
+            indoc! {r#"
+            ```main.rs
+            fn bar() {
+                let x = 42;
+                let mut sum = 0;
+            <|editable_region_start|>
+                for i in 0..x {
+                    sum += i;
+                }
+                println!("Sum: {}", sum);
+                r<|user_cursor_is_here|>eturn sum;
+            }
+
+            fn generate_random_numbers() -> Vec<i32> {
+                let mut rng = rand::thread_rng();
+            <|editable_region_end|>
+                let mut numbers = Vec::new();
+                for _ in 0..5 {
+                    numbers.push(rng.gen_range(1..101));
+            ```"#}
+        );
+    }
+
+    fn rust_lang() -> Language {
+        Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                matcher: LanguageMatcher {
+                    path_suffixes: vec!["rs".to_string()],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::LANGUAGE.into()),
+        )
+    }
+}

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

@@ -1,5 +1,6 @@
 mod completion_diff_element;
 mod init;
+mod input_excerpt;
 mod license_detection;
 mod onboarding_banner;
 mod onboarding_modal;
@@ -25,9 +26,8 @@ use gpui::{
     actions, App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, Subscription, Task,
 };
 use http_client::{HttpClient, Method};
-use language::{
-    Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, Point, ToOffset, ToPoint,
-};
+use input_excerpt::excerpt_for_cursor_position;
+use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToOffset, ToPoint};
 use language_models::LlmApiToken;
 use postage::watch;
 use project::Project;
@@ -57,38 +57,13 @@ const EDITABLE_REGION_END_MARKER: &'static str = "<|editable_region_end|>";
 const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1);
 const ZED_PREDICT_DATA_COLLECTION_CHOICE: &str = "zed_predict_data_collection_choice";
 
-// TODO(mgsloan): more systematic way to choose or tune these fairly arbitrary constants?
-
-/// Typical number of string bytes per token for the purposes of limiting model input. This is
-/// intentionally low to err on the side of underestimating limits.
-const BYTES_PER_TOKEN_GUESS: usize = 3;
-
-/// Output token limit, used to inform the size of the input. A copy of this constant is also in
-/// `crates/collab/src/llm.rs`.
-const MAX_OUTPUT_TOKENS: usize = 2048;
-
-/// Total bytes limit for editable region of buffer excerpt.
-///
-/// The number of output tokens is relevant to the size of the input excerpt because the model is
-/// tasked with outputting a modified excerpt. `2/3` is chosen so that there are some output tokens
-/// remaining for the model to specify insertions.
-const BUFFER_EXCERPT_BYTE_LIMIT: usize = (MAX_OUTPUT_TOKENS * 2 / 3) * BYTES_PER_TOKEN_GUESS;
+const MAX_CONTEXT_TOKENS: usize = 100;
+const MAX_REWRITE_TOKENS: usize = 300;
+const MAX_EVENT_TOKENS: usize = 400;
 
-/// Total line limit for editable region of buffer excerpt.
-const BUFFER_EXCERPT_LINE_LIMIT: u32 = 64;
-
-/// Note that this is not the limit for the overall prompt, just for the inputs to the template
-/// instantiated in `crates/collab/src/llm.rs`.
-const TOTAL_BYTE_LIMIT: usize = BUFFER_EXCERPT_BYTE_LIMIT * 2;
-
-/// Maximum number of events to include in the prompt.
+/// Maximum number of events to track.
 const MAX_EVENT_COUNT: usize = 16;
 
-/// Maximum number of string bytes in a single event. Arbitrarily choosing this to be 4x the size of
-/// equally splitting up the the remaining bytes after the largest possible buffer excerpt.
-const PER_EVENT_BYTE_LIMIT: usize =
-    (TOTAL_BYTE_LIMIT - BUFFER_EXCERPT_BYTE_LIMIT) / MAX_EVENT_COUNT * 4;
-
 actions!(edit_prediction, [ClearHistory]);
 
 #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
@@ -106,12 +81,6 @@ impl std::fmt::Display for InlineCompletionId {
     }
 }
 
-impl InlineCompletionId {
-    fn new() -> Self {
-        Self(Uuid::new_v4())
-    }
-}
-
 #[derive(Clone)]
 struct ZetaGlobal(Entity<Zeta>);
 
@@ -214,7 +183,8 @@ pub struct Zeta {
     data_collection_choice: Entity<DataCollectionChoice>,
     llm_token: LlmApiToken,
     _llm_token_subscription: Subscription,
-    tos_accepted: bool, // Terms of service accepted
+    /// Whether the terms of service have been accepted.
+    tos_accepted: bool,
     _user_store_subscription: Subscription,
     license_detection_watchers: HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
 }
@@ -418,7 +388,8 @@ impl Zeta {
             struct BackgroundValues {
                 input_events: String,
                 input_excerpt: String,
-                excerpt_range: Range<usize>,
+                speculated_output: String,
+                editable_range: Range<usize>,
                 input_outline: String,
             }
 
@@ -429,32 +400,21 @@ impl Zeta {
                     let path = path.clone();
                     async move {
                         let path = path.to_string_lossy();
-                        let (excerpt_range, excerpt_len_guess) = excerpt_range_for_position(
+                        let input_excerpt = excerpt_for_cursor_position(
                             cursor_point,
-                            BUFFER_EXCERPT_BYTE_LIMIT,
-                            BUFFER_EXCERPT_LINE_LIMIT,
-                            &path,
-                            &snapshot,
-                        )?;
-                        let input_excerpt = prompt_for_excerpt(
-                            cursor_offset,
-                            &excerpt_range,
-                            excerpt_len_guess,
                             &path,
                             &snapshot,
+                            MAX_REWRITE_TOKENS,
+                            MAX_CONTEXT_TOKENS,
                         );
-
-                        let bytes_remaining = TOTAL_BYTE_LIMIT.saturating_sub(input_excerpt.len());
-                        let input_events = prompt_for_events(events.iter(), bytes_remaining);
-
-                        // Note that input_outline is not currently used in prompt generation and so
-                        // is not counted towards TOTAL_BYTE_LIMIT.
+                        let input_events = prompt_for_events(&events, MAX_EVENT_TOKENS);
                         let input_outline = prompt_for_outline(&snapshot);
 
                         anyhow::Ok(BackgroundValues {
                             input_events,
-                            input_excerpt,
-                            excerpt_range,
+                            input_excerpt: input_excerpt.prompt,
+                            speculated_output: input_excerpt.speculated_output,
+                            editable_range: input_excerpt.editable_range.to_offset(&snapshot),
                             input_outline,
                         })
                     }
@@ -462,7 +422,7 @@ impl Zeta {
                 .await?;
 
             log::debug!(
-                "Events:\n{}\nExcerpt:\n{}",
+                "Events:\n{}\nExcerpt:\n{:?}",
                 values.input_events,
                 values.input_excerpt
             );
@@ -470,6 +430,7 @@ impl Zeta {
             let body = PredictEditsBody {
                 input_events: values.input_events.clone(),
                 input_excerpt: values.input_excerpt.clone(),
+                speculated_output: Some(values.speculated_output),
                 outline: Some(values.input_outline.clone()),
                 can_collect_data,
                 diagnostic_groups: diagnostic_groups.and_then(|diagnostic_groups| {
@@ -485,14 +446,13 @@ impl Zeta {
 
             let response = perform_predict_edits(client, llm_token, is_staff, body).await?;
 
-            let output_excerpt = response.output_excerpt;
-            log::debug!("completion response: {}", output_excerpt);
+            log::debug!("completion response: {}", &response.output_excerpt);
 
             Self::process_completion_response(
-                output_excerpt,
+                response,
                 buffer,
                 &snapshot,
-                values.excerpt_range,
+                values.editable_range,
                 cursor_offset,
                 path,
                 values.input_outline,
@@ -508,6 +468,8 @@ impl Zeta {
     // Generates several example completions of various states to fill the Zeta completion modal
     #[cfg(any(test, feature = "test-support"))]
     pub fn fill_with_fake_completions(&mut self, cx: &mut Context<Self>) -> Task<()> {
+        use language::Point;
+
         let test_buffer_text = indoc::indoc! {r#"a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
             And maybe a short line
 
@@ -526,6 +488,7 @@ impl Zeta {
                 &buffer,
                 position,
                 PredictEditsResponse {
+                    request_id: Uuid::parse_str("e7861db5-0cea-4761-b1c5-ad083ac53a80").unwrap(),
                     output_excerpt: format!("{EDITABLE_REGION_START_MARKER}
 a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
 [here's an edit]
@@ -542,6 +505,7 @@ and then another
                 &buffer,
                 position,
                 PredictEditsResponse {
+                    request_id: Uuid::parse_str("077c556a-2c49-44e2-bbc6-dafc09032a5e").unwrap(),
                     output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
 a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
 And maybe a short line
@@ -558,6 +522,7 @@ and then another
                 &buffer,
                 position,
                 PredictEditsResponse {
+                    request_id: Uuid::parse_str("df8c7b23-3d1d-4f99-a306-1f6264a41277").unwrap(),
                     output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
 a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
 And maybe a short line
@@ -575,6 +540,7 @@ and then another
                 &buffer,
                 position,
                 PredictEditsResponse {
+                    request_id: Uuid::parse_str("c743958d-e4d8-44a8-aa5b-eb1e305c5f5c").unwrap(),
                     output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
 a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
 And maybe a short line
@@ -592,6 +558,7 @@ and then another
                 &buffer,
                 position,
                 PredictEditsResponse {
+                    request_id: Uuid::parse_str("ff5cd7ab-ad06-4808-986e-d3391e7b8355").unwrap(),
                     output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
 a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
 And maybe a short line
@@ -608,6 +575,7 @@ and then another
                 &buffer,
                 position,
                 PredictEditsResponse {
+                    request_id: Uuid::parse_str("83cafa55-cdba-4b27-8474-1865ea06be94").unwrap(),
                     output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
 a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
 And maybe a short line
@@ -623,6 +591,7 @@ and then another
                 &buffer,
                 position,
                 PredictEditsResponse {
+                    request_id: Uuid::parse_str("d5bd3afd-8723-47c7-bd77-15a3a926867b").unwrap(),
                     output_excerpt: format!(r#"{EDITABLE_REGION_START_MARKER}
 a longggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg line
 And maybe a short line
@@ -697,7 +666,7 @@ and then another
             loop {
                 let request_builder = http_client::Request::builder().method(Method::POST).uri(
                     http_client
-                        .build_zed_llm_url("/predict_edits", &[])?
+                        .build_zed_llm_url("/predict_edits/v2", &[])?
                         .as_ref(),
                 );
                 let request = request_builder
@@ -734,10 +703,10 @@ and then another
 
     #[allow(clippy::too_many_arguments)]
     fn process_completion_response(
-        output_excerpt: String,
+        prediction_response: PredictEditsResponse,
         buffer: Entity<Buffer>,
         snapshot: &BufferSnapshot,
-        excerpt_range: Range<usize>,
+        editable_range: Range<usize>,
         cursor_offset: usize,
         path: Arc<Path>,
         input_outline: String,
@@ -747,6 +716,8 @@ and then another
         cx: &AsyncApp,
     ) -> Task<Result<Option<InlineCompletion>>> {
         let snapshot = snapshot.clone();
+        let request_id = prediction_response.request_id;
+        let output_excerpt = prediction_response.output_excerpt;
         cx.spawn(|cx| async move {
             let output_excerpt: Arc<str> = output_excerpt.into();
 
@@ -754,9 +725,9 @@ and then another
                 .background_executor()
                 .spawn({
                     let output_excerpt = output_excerpt.clone();
-                    let excerpt_range = excerpt_range.clone();
+                    let editable_range = editable_range.clone();
                     let snapshot = snapshot.clone();
-                    async move { Self::parse_edits(output_excerpt, excerpt_range, &snapshot) }
+                    async move { Self::parse_edits(output_excerpt, editable_range, &snapshot) }
                 })
                 .await?
                 .into();
@@ -777,9 +748,9 @@ and then another
             let edit_preview = edit_preview.await;
 
             Ok(Some(InlineCompletion {
-                id: InlineCompletionId::new(),
+                id: InlineCompletionId(request_id),
                 path,
-                excerpt_range,
+                excerpt_range: editable_range,
                 cursor_offset,
                 edits,
                 edit_preview,
@@ -796,7 +767,7 @@ and then another
 
     fn parse_edits(
         output_excerpt: Arc<str>,
-        excerpt_range: Range<usize>,
+        editable_range: Range<usize>,
         snapshot: &BufferSnapshot,
     ) -> Result<Vec<(Range<Anchor>, String)>> {
         let content = output_excerpt.replace(CURSOR_MARKER, "");
@@ -840,13 +811,13 @@ and then another
         let new_text = &content[..codefence_end];
 
         let old_text = snapshot
-            .text_for_range(excerpt_range.clone())
+            .text_for_range(editable_range.clone())
             .collect::<String>();
 
         Ok(Self::compute_edits(
             old_text,
             new_text,
-            excerpt_range.start,
+            editable_range.start,
             &snapshot,
         ))
     }
@@ -1080,9 +1051,7 @@ fn prompt_for_outline(snapshot: &BufferSnapshot) -> String {
     .unwrap();
 
     if let Some(outline) = snapshot.outline(None) {
-        let guess_size = outline.items.len() * 15;
-        input_outline.reserve(guess_size);
-        for item in outline.items.iter() {
+        for item in &outline.items {
             let spacing = " ".repeat(item.depth);
             writeln!(input_outline, "{}{}", spacing, item.text).unwrap();
         }
@@ -1093,181 +1062,20 @@ fn prompt_for_outline(snapshot: &BufferSnapshot) -> String {
     input_outline
 }
 
-fn prompt_for_excerpt(
-    offset: usize,
-    excerpt_range: &Range<usize>,
-    mut len_guess: usize,
-    path: &str,
-    snapshot: &BufferSnapshot,
-) -> String {
-    let point_range = excerpt_range.to_point(snapshot);
-
-    // Include one line of extra context before and after editable range, if those lines are non-empty.
-    let extra_context_before_range =
-        if point_range.start.row > 0 && !snapshot.is_line_blank(point_range.start.row - 1) {
-            let range =
-                (Point::new(point_range.start.row - 1, 0)..point_range.start).to_offset(snapshot);
-            len_guess += range.end - range.start;
-            Some(range)
-        } else {
-            None
-        };
-    let extra_context_after_range = if point_range.end.row < snapshot.max_point().row
-        && !snapshot.is_line_blank(point_range.end.row + 1)
-    {
-        let range = (point_range.end
-            ..Point::new(
-                point_range.end.row + 1,
-                snapshot.line_len(point_range.end.row + 1),
-            ))
-            .to_offset(snapshot);
-        len_guess += range.end - range.start;
-        Some(range)
-    } else {
-        None
-    };
-
-    let mut prompt_excerpt = String::with_capacity(len_guess);
-    writeln!(prompt_excerpt, "```{}", path).unwrap();
-
-    if excerpt_range.start == 0 {
-        writeln!(prompt_excerpt, "{START_OF_FILE_MARKER}").unwrap();
-    }
-
-    if let Some(extra_context_before_range) = extra_context_before_range {
-        for chunk in snapshot.text_for_range(extra_context_before_range) {
-            prompt_excerpt.push_str(chunk);
-        }
-    }
-    writeln!(prompt_excerpt, "{EDITABLE_REGION_START_MARKER}").unwrap();
-    for chunk in snapshot.text_for_range(excerpt_range.start..offset) {
-        prompt_excerpt.push_str(chunk);
-    }
-    prompt_excerpt.push_str(CURSOR_MARKER);
-    for chunk in snapshot.text_for_range(offset..excerpt_range.end) {
-        prompt_excerpt.push_str(chunk);
-    }
-    write!(prompt_excerpt, "\n{EDITABLE_REGION_END_MARKER}").unwrap();
-
-    if let Some(extra_context_after_range) = extra_context_after_range {
-        for chunk in snapshot.text_for_range(extra_context_after_range) {
-            prompt_excerpt.push_str(chunk);
-        }
-    }
-
-    write!(prompt_excerpt, "\n```").unwrap();
-    debug_assert!(
-        prompt_excerpt.len() <= len_guess,
-        "Excerpt length {} exceeds estimated length {}",
-        prompt_excerpt.len(),
-        len_guess
-    );
-    prompt_excerpt
-}
-
-fn excerpt_range_for_position(
-    cursor_point: Point,
-    byte_limit: usize,
-    line_limit: u32,
-    path: &str,
-    snapshot: &BufferSnapshot,
-) -> Result<(Range<usize>, usize)> {
-    let cursor_row = cursor_point.row;
-    let last_buffer_row = snapshot.max_point().row;
-
-    // This is an overestimate because it includes parts of prompt_for_excerpt which are
-    // conditionally skipped.
-    let mut len_guess = 0;
-    len_guess += "```".len() + path.len() + 1;
-    len_guess += START_OF_FILE_MARKER.len() + 1;
-    len_guess += EDITABLE_REGION_START_MARKER.len() + 1;
-    len_guess += CURSOR_MARKER.len();
-    len_guess += EDITABLE_REGION_END_MARKER.len() + 1;
-    len_guess += "```".len() + 1;
-
-    len_guess += usize::try_from(snapshot.line_len(cursor_row) + 1).unwrap();
-
-    if len_guess > byte_limit {
-        return Err(anyhow!("Current line too long to send to model."));
-    }
-
-    let mut excerpt_start_row = cursor_row;
-    let mut excerpt_end_row = cursor_row;
-    let mut no_more_before = cursor_row == 0;
-    let mut no_more_after = cursor_row >= last_buffer_row;
-    let mut row_delta = 1;
-    loop {
-        if !no_more_before {
-            let row = cursor_point.row - row_delta;
-            let line_len: usize = usize::try_from(snapshot.line_len(row) + 1).unwrap();
-            let mut new_len_guess = len_guess + line_len;
-            if row == 0 {
-                new_len_guess += START_OF_FILE_MARKER.len() + 1;
-            }
-            if new_len_guess <= byte_limit {
-                len_guess = new_len_guess;
-                excerpt_start_row = row;
-                if row == 0 {
-                    no_more_before = true;
-                }
-            } else {
-                no_more_before = true;
-            }
-        }
-        if excerpt_end_row - excerpt_start_row >= line_limit {
-            break;
-        }
-        if !no_more_after {
-            let row = cursor_point.row + row_delta;
-            let line_len: usize = usize::try_from(snapshot.line_len(row) + 1).unwrap();
-            let new_len_guess = len_guess + line_len;
-            if new_len_guess <= byte_limit {
-                len_guess = new_len_guess;
-                excerpt_end_row = row;
-                if row >= last_buffer_row {
-                    no_more_after = true;
-                }
-            } else {
-                no_more_after = true;
-            }
-        }
-        if excerpt_end_row - excerpt_start_row >= line_limit {
-            break;
-        }
-        if no_more_before && no_more_after {
+fn prompt_for_events(events: &VecDeque<Event>, mut remaining_tokens: usize) -> String {
+    let mut result = String::new();
+    for event in events.iter().rev() {
+        let event_string = event.to_prompt();
+        let event_tokens = tokens_for_bytes(event_string.len());
+        if event_tokens > remaining_tokens {
             break;
         }
-        row_delta += 1;
-    }
-
-    let excerpt_start = Point::new(excerpt_start_row, 0);
-    let excerpt_end = Point::new(excerpt_end_row, snapshot.line_len(excerpt_end_row));
-    Ok((
-        excerpt_start.to_offset(snapshot)..excerpt_end.to_offset(snapshot),
-        len_guess,
-    ))
-}
 
-fn prompt_for_events<'a>(
-    events: impl Iterator<Item = &'a Event>,
-    mut bytes_remaining: usize,
-) -> String {
-    let mut result = String::new();
-    for event in events {
         if !result.is_empty() {
-            result.push('\n');
-            result.push('\n');
-        }
-        let event_string = event.to_prompt();
-        let len = event_string.len();
-        if len > PER_EVENT_BYTE_LIMIT {
-            continue;
+            result.insert_str(0, "\n\n");
         }
-        if len > bytes_remaining {
-            break;
-        }
-        bytes_remaining -= len;
-        result.push_str(&event_string);
+        result.insert_str(0, &event_string);
+        remaining_tokens -= event_tokens;
     }
     result
 }
@@ -1744,12 +1552,20 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider
         }
 
         Some(inline_completion::InlineCompletion {
+            id: Some(completion.id.to_string().into()),
             edits: edits[edit_start_ix..edit_end_ix].to_vec(),
             edit_preview: Some(completion.edit_preview.clone()),
         })
     }
 }
 
+fn tokens_for_bytes(bytes: usize) -> usize {
+    /// Typical number of string bytes per token for the purposes of limiting model input. This is
+    /// intentionally low to err on the side of underestimating limits.
+    const BYTES_PER_TOKEN_GUESS: usize = 3;
+    bytes / BYTES_PER_TOKEN_GUESS
+}
+
 #[cfg(test)]
 mod tests {
     use client::test::FakeServer;
@@ -1757,6 +1573,7 @@ mod tests {
     use gpui::TestAppContext;
     use http_client::FakeHttpClient;
     use indoc::indoc;
+    use language::Point;
     use language_models::RefreshLlmTokenListener;
     use rpc::proto;
     use settings::SettingsStore;
@@ -1784,7 +1601,7 @@ mod tests {
             edit_preview,
             path: Path::new("").into(),
             snapshot: cx.read(|cx| buffer.read(cx).snapshot()),
-            id: InlineCompletionId::new(),
+            id: InlineCompletionId(Uuid::new_v4()),
             excerpt_range: 0..0,
             cursor_offset: 0,
             input_outline: "".into(),
@@ -1903,6 +1720,8 @@ mod tests {
                 .status(200)
                 .body(
                     serde_json::to_string(&PredictEditsResponse {
+                        request_id: Uuid::parse_str("7e86480f-3536-4d2c-9334-8213e3445d45")
+                            .unwrap(),
                         output_excerpt: completion_response.to_string(),
                     })
                     .unwrap()

docs/src/extensions/icon-themes.md πŸ”—

@@ -30,7 +30,7 @@ Here is an example of the structure of an icon theme:
         "collapsed": "./icons/folder.svg",
         "expanded": "./icons/folder-open.svg"
       },
-      "chevon_icons": {
+      "chevron_icons": {
         "collapsed": "./icons/chevron-right.svg",
         "expanded": "./icons/chevron-down.svg"
       },

docs/src/languages/elm.md πŸ”—

@@ -7,7 +7,7 @@ Elm support is available through the [Elm extension](https://github.com/zed-exte
 
 ## Setup
 
-Zed support for Elm requires installation of `elm`, `elm-format`, `elm-review` and `elm`.
+Zed support for Elm requires installation of `elm`, `elm-format`, and `elm-review`.
 
 1. [Install Elm](https://guide.elm-lang.org/install/elm.html) (or run `brew install elm` on macOS).
 2. Install `elm-review` to support code linting:

docs/theme/css/general.css πŸ”—

@@ -79,6 +79,12 @@ h6 code {
   display: none !important;
 }
 
+h2 {
+  padding-bottom: 1rem;
+  border-bottom: 1px solid;
+  border-color: var(--border-light);
+}
+
 h2,
 h3 {
   margin-block-start: 1.5em;

docs/theme/css/variables.css πŸ”—

@@ -13,8 +13,9 @@
   --menu-bar-height: 64px;
   --font: "IA Writer Quattro S", sans-serif;
   --title-font: "Lora", "Helvetica Neue", Helvetica, Arial, sans-serif;
-  --mono-font: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
-    Liberation Mono, Courier New, monospace;
+  --mono-font:
+    ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono,
+    Courier New, monospace;
   --code-font-size: 0.875em
     /* please adjust the ace font size accordingly in editor.js */;
 
@@ -97,7 +98,7 @@
   --title-color: hsl(220, 92%, 80%);
 
   --border: hsl(220, 13%, 20%);
-  --border-light: hsl(220, 13%, 90%);
+  --border-light: hsl(220, 13%, 15%);
   --border-hover: hsl(220, 13%, 40%);
 
   --media-bg: hsl(220, 13%, 8%);