Merge remote-tracking branch 'origin/main' into settings-ui-elements

Anthony created

Change summary

Cargo.lock                                                        |   46 
Cargo.toml                                                        |    4 
assets/keymaps/default-linux.json                                 |   33 
assets/keymaps/default-macos.json                                 |   35 
assets/keymaps/default-windows.json                               |   33 
assets/keymaps/vim.json                                           |    8 
assets/settings/default.json                                      |   19 
crates/agent/src/thread.rs                                        |    3 
crates/agent2/src/agent.rs                                        |    6 
crates/agent2/src/thread.rs                                       |   43 
crates/agent_settings/src/agent_settings.rs                       |    2 
crates/agent_ui/src/acp/entry_view_state.rs                       |    3 
crates/agent_ui/src/acp/thread_view.rs                            |   55 
crates/agent_ui/src/agent_configuration.rs                        |    8 
crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs |    4 
crates/agent_ui/src/agent_diff.rs                                 |    5 
crates/assistant_tools/src/terminal_tool.rs                       |    3 
crates/audio/src/audio.rs                                         |    2 
crates/audio/src/audio_settings.rs                                |    2 
crates/auto_update/src/auto_update.rs                             |    2 
crates/call/src/call_settings.rs                                  |    9 
crates/client/src/client.rs                                       |    6 
crates/cloud_llm_client/src/predict_edits_v3.rs                   |    8 
crates/codestral/Cargo.toml                                       |   28 
crates/codestral/LICENSE-GPL                                      |    1 
crates/codestral/src/codestral.rs                                 |  381 
crates/collab/src/tests/editor_tests.rs                           |   16 
crates/collab/src/tests/integration_tests.rs                      |   14 
crates/collab/src/tests/randomized_test_helpers.rs                |    5 
crates/collab/src/tests/test_server.rs                            |    1 
crates/collab_ui/src/panel_settings.rs                            |    4 
crates/command_palette_hooks/src/command_palette_hooks.rs         |    7 
crates/copilot/src/copilot.rs                                     |   30 
crates/dap/src/adapters.rs                                        |    1 
crates/dap/src/debugger_settings.rs                               |    3 
crates/dap_adapters/src/javascript.rs                             |    7 
crates/edit_prediction/src/edit_prediction.rs                     |   50 
crates/edit_prediction_button/Cargo.toml                          |    1 
crates/edit_prediction_button/src/edit_prediction_button.rs       |   82 
crates/edit_prediction_context/Cargo.toml                         |    4 
crates/edit_prediction_context/src/declaration.rs                 |   81 
crates/edit_prediction_context/src/declaration_scoring.rs         |  439 
crates/edit_prediction_context/src/edit_prediction_context.rs     |   64 
crates/edit_prediction_context/src/imports.rs                     | 1319 +
crates/edit_prediction_context/src/syntax_index.rs                |  180 
crates/edit_prediction_context/src/text_similarity.rs             |   46 
crates/editor/src/actions.rs                                      |   27 
crates/editor/src/editor.rs                                       |  179 
crates/editor/src/editor_settings.rs                              |    2 
crates/editor/src/editor_tests.rs                                 |  120 
crates/editor/src/element.rs                                      |    9 
crates/editor/src/highlight_matching_bracket.rs                   |   31 
crates/editor/src/inlay_hint_cache.rs                             |    4 
crates/editor/src/selections_collection.rs                        |   21 
crates/editor/src/test/editor_lsp_test_context.rs                 |   73 
crates/extension_host/src/extension_settings.rs                   |    3 
crates/file_finder/src/file_finder.rs                             |   49 
crates/file_finder/src/file_finder_settings.rs                    |    2 
crates/file_finder/src/file_finder_tests.rs                       |   46 
crates/file_icons/Cargo.toml                                      |    1 
crates/file_icons/src/file_icons.rs                               |   45 
crates/git/src/git.rs                                             |    2 
crates/git_hosting_providers/src/settings.rs                      |    2 
crates/git_ui/src/commit_view.rs                                  |    8 
crates/git_ui/src/file_diff_view.rs                               |    4 
crates/git_ui/src/git_panel.rs                                    |  146 
crates/git_ui/src/git_panel_settings.rs                           |    2 
crates/git_ui/src/project_diff.rs                                 |   50 
crates/git_ui/src/repository_selector.rs                          |   36 
crates/git_ui/src/text_diff_view.rs                               |    4 
crates/go_to_line/src/cursor_position.rs                          |    6 
crates/gpui/Cargo.toml                                            |    2 
crates/gpui/examples/animation.rs                                 |   85 
crates/gpui/src/platform.rs                                       |    8 
crates/gpui/src/platform/blade/shaders.wgsl                       |    8 
crates/gpui/src/platform/linux/wayland/client.rs                  |    3 
crates/gpui/src/platform/linux/wayland/window.rs                  |   16 
crates/gpui/src/platform/linux/x11/client.rs                      |    5 
crates/gpui/src/platform/linux/x11/window.rs                      |   38 
crates/gpui/src/platform/mac/shaders.metal                        |   30 
crates/gpui/src/platform/mac/window.rs                            |    4 
crates/gpui/src/platform/windows/shaders.hlsl                     |    8 
crates/gpui/src/svg_renderer.rs                                   |   30 
crates/gpui/src/window.rs                                         |   29 
crates/image_viewer/src/image_viewer_settings.rs                  |    3 
crates/inspector_ui/Cargo.toml                                    |    1 
crates/inspector_ui/src/div_inspector.rs                          |    7 
crates/inspector_ui/src/inspector.rs                              |   43 
crates/journal/src/journal.rs                                     |    2 
crates/json_schema_store/src/json_schema_store.rs                 |    4 
crates/keymap_editor/src/keymap_editor.rs                         |   79 
crates/language/src/language.rs                                   |   72 
crates/language/src/language_registry.rs                          |    2 
crates/language/src/language_settings.rs                          |   19 
crates/language_model/src/registry.rs                             |   12 
crates/language_model/src/request.rs                              |    4 
crates/language_models/src/language_models.rs                     |   46 
crates/language_models/src/provider/mistral.rs                    |  286 
crates/language_models/src/settings.rs                            |    2 
crates/language_tools/src/lsp_log_view.rs                         |    2 
crates/language_tools/src/lsp_log_view_tests.rs                   |    2 
crates/languages/Cargo.toml                                       |    1 
crates/languages/src/c/config.toml                                |    1 
crates/languages/src/c/imports.scm                                |    7 
crates/languages/src/cpp/config.toml                              |    1 
crates/languages/src/cpp/imports.scm                              |    5 
crates/languages/src/go.rs                                        |   53 
crates/languages/src/go/imports.scm                               |   14 
crates/languages/src/go/runnables.scm                             |    9 
crates/languages/src/javascript/config.toml                       |    1 
crates/languages/src/javascript/imports.scm                       |   14 
crates/languages/src/python.rs                                    |   15 
crates/languages/src/python/config.toml                           |    1 
crates/languages/src/python/imports.scm                           |   32 
crates/languages/src/rust/config.toml                             |    2 
crates/languages/src/rust/imports.scm                             |   27 
crates/languages/src/tsx/imports.scm                              |   14 
crates/languages/src/typescript/config.toml                       |    1 
crates/languages/src/typescript/imports.scm                       |   20 
crates/lsp/src/lsp.rs                                             |   88 
crates/markdown_preview/src/markdown_parser.rs                    |   77 
crates/migrator/src/migrations/m_2025_01_29/keymap.rs             |   10 
crates/migrator/src/migrator.rs                                   |   11 
crates/mistral/src/mistral.rs                                     |    1 
crates/multi_buffer/src/multi_buffer.rs                           |   22 
crates/multi_buffer/src/multi_buffer_tests.rs                     |    8 
crates/onboarding/Cargo.toml                                      |    5 
crates/onboarding/src/ai_setup_page.rs                            |  427 
crates/onboarding/src/basics_page.rs                              |  256 
crates/onboarding/src/editing_page.rs                             |  611 
crates/onboarding/src/onboarding.rs                               |  436 
crates/onboarding/src/welcome.rs                                  |   14 
crates/outline_panel/src/outline_panel.rs                         |    2 
crates/outline_panel/src/outline_panel_settings.rs                |    2 
crates/project/src/agent_server_store.rs                          |   46 
crates/project/src/debugger/breakpoint_store.rs                   |    1 
crates/project/src/debugger/dap_store.rs                          |   82 
crates/project/src/debugger/session.rs                            |  339 
crates/project/src/lsp_store.rs                                   |   73 
crates/project/src/lsp_store/json_language_server_ext.rs          |    4 
crates/project/src/lsp_store/rust_analyzer_ext.rs                 |   29 
crates/project/src/project.rs                                     |   23 
crates/project/src/project_settings.rs                            |    4 
crates/project/src/project_tests.rs                               |   16 
crates/project/src/terminals.rs                                   |   15 
crates/project_panel/src/project_panel.rs                         |   15 
crates/project_panel/src/project_panel_settings.rs                |    2 
crates/recent_projects/src/remote_connections.rs                  |    2 
crates/remote/src/remote_client.rs                                |   35 
crates/remote/src/transport/ssh.rs                                |   17 
crates/remote/src/transport/wsl.rs                                |    9 
crates/remote_server/src/headless_project.rs                      |   10 
crates/remote_server/src/remote_editing_tests.rs                  |    4 
crates/repl/src/jupyter_settings.rs                               |    2 
crates/repl/src/repl_settings.rs                                  |    3 
crates/rules_library/src/rules_library.rs                         |    1 
crates/settings/src/base_keymap_setting.rs                        |    3 
crates/settings/src/settings_content.rs                           |    4 
crates/settings/src/settings_content/language.rs                  |   20 
crates/settings/src/settings_content/project.rs                   |    7 
crates/settings/src/settings_content/terminal.rs                  |   70 
crates/settings/src/settings_content/theme.rs                     |    2 
crates/settings/src/settings_content/workspace.rs                 |    6 
crates/settings/src/settings_file.rs                              |    4 
crates/settings/src/settings_store.rs                             |  173 
crates/settings/src/vscode_import.rs                              |   51 
crates/settings_profile_selector/src/settings_profile_selector.rs |    1 
crates/settings_ui/Cargo.toml                                     |    6 
crates/settings_ui/examples/.zed/settings.json                    |    1 
crates/settings_ui/examples/ui.rs                                 |  113 
crates/settings_ui/src/page_data.rs                               | 1232 
crates/settings_ui/src/settings_ui.rs                             |  607 
crates/storybook/src/storybook.rs                                 |    6 
crates/task/src/shell_builder.rs                                  |    8 
crates/task/src/task.rs                                           |    9 
crates/terminal/src/terminal.rs                                   |   34 
crates/terminal/src/terminal_settings.rs                          |   74 
crates/theme/src/fallback_themes.rs                               |    8 
crates/theme/src/settings.rs                                      |  222 
crates/theme/src/theme.rs                                         |  137 
crates/theme_extension/src/theme_extension.rs                     |    6 
crates/theme_selector/src/icon_theme_selector.rs                  |   51 
crates/theme_selector/src/theme_selector.rs                       |    7 
crates/title_bar/src/title_bar_settings.rs                        |    3 
crates/ui/src/components/button/button_like.rs                    |  130 
crates/ui/src/components/button/toggle_button.rs                  |   64 
crates/ui/src/components/toggle.rs                                |   46 
crates/ui/src/components/tree_view_item.rs                        |   88 
crates/ui_input/src/number_field.rs                               |  110 
crates/util/src/paths.rs                                          |  133 
crates/util/src/shell.rs                                          |   37 
crates/util/src/shell_env.rs                                      |   11 
crates/vim/src/motion.rs                                          |   36 
crates/vim/src/normal.rs                                          |    4 
crates/vim/src/object.rs                                          |   73 
crates/vim/src/state.rs                                           |   11 
crates/vim/src/surrounds.rs                                       |  161 
crates/vim/src/test/neovim_backed_test_context.rs                 |   20 
crates/vim/src/test/vim_test_context.rs                           |   22 
crates/vim/src/vim.rs                                             |    8 
crates/vim/test_data/test_matching_nested_brackets.json           |    5 
crates/vim_mode_setting/src/vim_mode_setting.rs                   |    4 
crates/workspace/src/item.rs                                      |    4 
crates/workspace/src/workspace.rs                                 |    6 
crates/workspace/src/workspace_settings.rs                        |    7 
crates/worktree/src/worktree.rs                                   |    2 
crates/worktree/src/worktree_settings.rs                          |    3 
crates/zed/Cargo.toml                                             |    3 
crates/zed/src/main.rs                                            |  105 
crates/zed/src/zed.rs                                             |   65 
crates/zed/src/zed/edit_prediction_registry.rs                    |   11 
crates/zeta/src/zeta.rs                                           |   53 
crates/zeta2/src/zeta2.rs                                         |  119 
crates/zeta2_tools/Cargo.toml                                     |    2 
crates/zeta2_tools/src/zeta2_tools.rs                             |  373 
crates/zeta_cli/Cargo.toml                                        |    3 
crates/zeta_cli/src/main.rs                                       |  988 
crates/zlog_settings/src/zlog_settings.rs                         |    2 
docs/src/configuring-zed.md                                       |    6 
script/bump-gpui-minor-version                                    |   28 
script/lib/bump-version.sh                                        |   16 
tooling/xtask/src/tasks/publish_gpui.rs                           |   73 
222 files changed, 9,233 insertions(+), 4,211 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3316,6 +3316,27 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "codestral"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "edit_prediction",
+ "edit_prediction_context",
+ "futures 0.3.31",
+ "gpui",
+ "language",
+ "language_models",
+ "log",
+ "mistral",
+ "serde",
+ "serde_json",
+ "smol",
+ "text",
+ "workspace-hack",
+ "zed-http-client",
+]
+
 [[package]]
 name = "collab"
 version = "0.44.0"
@@ -5115,6 +5136,7 @@ dependencies = [
  "anyhow",
  "client",
  "cloud_llm_client",
+ "codestral",
  "copilot",
  "edit_prediction",
  "editor",
@@ -5167,6 +5189,9 @@ dependencies = [
  "strum 0.27.1",
  "text",
  "tree-sitter",
+ "tree-sitter-c",
+ "tree-sitter-cpp",
+ "tree-sitter-go",
  "workspace-hack",
  "zed-collections",
  "zed-util",
@@ -5921,7 +5946,6 @@ version = "0.1.0"
 dependencies = [
  "gpui",
  "serde",
- "settings",
  "theme",
  "workspace-hack",
  "zed-util",
@@ -6991,7 +7015,7 @@ dependencies = [
 
 [[package]]
 name = "gpui"
-version = "0.1.0"
+version = "0.2.0"
 dependencies = [
  "anyhow",
  "as-raw-xcb-connection",
@@ -8027,6 +8051,7 @@ dependencies = [
  "serde_json",
  "serde_json_lenient",
  "theme",
+ "title_bar",
  "ui",
  "workspace",
  "workspace-hack",
@@ -8779,7 +8804,6 @@ dependencies = [
  "serde_json",
  "serde_json_lenient",
  "settings",
- "shlex",
  "smol",
  "task",
  "text",
@@ -10536,20 +10560,15 @@ dependencies = [
 name = "onboarding"
 version = "0.1.0"
 dependencies = [
- "ai_onboarding",
  "anyhow",
  "client",
  "component",
  "db",
  "documented",
- "editor",
  "fs",
  "fuzzy",
  "git",
  "gpui",
- "itertools 0.14.0",
- "language",
- "language_model",
  "menu",
  "notifications",
  "picker",
@@ -14363,7 +14382,6 @@ dependencies = [
  "anyhow",
  "assets",
  "client",
- "command_palette_hooks",
  "editor",
  "feature_flags",
  "fs",
@@ -14372,6 +14390,7 @@ dependencies = [
  "gpui",
  "heck 0.5.0",
  "language",
+ "log",
  "menu",
  "node_runtime",
  "paths",
@@ -16944,8 +16963,7 @@ dependencies = [
 [[package]]
 name = "tree-sitter-typescript"
 version = "0.23.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c5f76ed8d947a75cc446d5fccd8b602ebf0cde64ccf2ffa434d873d7a575eff"
+source = "git+https://github.com/zed-industries/tree-sitter-typescript?rev=e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899#e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899"
 dependencies = [
  "cc",
  "tree-sitter-language",
@@ -19984,7 +20002,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.208.0"
+version = "0.209.0"
 dependencies = [
  "acp_tools",
  "activity_indicator",
@@ -20007,6 +20025,7 @@ dependencies = [
  "clap",
  "cli",
  "client",
+ "codestral",
  "collab_ui",
  "command_palette",
  "component",
@@ -20711,6 +20730,8 @@ dependencies = [
  "indoc",
  "language",
  "log",
+ "multi_buffer",
+ "ordered-float 2.10.1",
  "pretty_assertions",
  "project",
  "serde",
@@ -20764,6 +20785,7 @@ dependencies = [
  "terminal_view",
  "watch",
  "workspace-hack",
+ "zed-collections",
  "zed-util",
  "zeta",
  "zeta2",

Cargo.toml 🔗

@@ -164,6 +164,7 @@ members = [
     "crates/sum_tree",
     "crates/supermaven",
     "crates/supermaven_api",
+    "crates/codestral",
     "crates/svg_preview",
     "crates/system_specs",
     "crates/tab_switcher",
@@ -398,6 +399,7 @@ streaming_diff = { path = "crates/streaming_diff" }
 sum_tree = { path = "crates/sum_tree", package = "zed-sum-tree", version = "0.1.0" }
 supermaven = { path = "crates/supermaven" }
 supermaven_api = { path = "crates/supermaven_api" }
+codestral = { path = "crates/codestral" }
 system_specs = { path = "crates/system_specs" }
 tab_switcher = { path = "crates/tab_switcher" }
 task = { path = "crates/task" }
@@ -691,7 +693,7 @@ tree-sitter-python = "0.25"
 tree-sitter-regex = "0.24"
 tree-sitter-ruby = "0.23"
 tree-sitter-rust = "0.24"
-tree-sitter-typescript = "0.23"
+tree-sitter-typescript = { git = "https://github.com/zed-industries/tree-sitter-typescript", rev = "e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" } # https://github.com/tree-sitter/tree-sitter-typescript/pull/347
 tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
 unicase = "2.6"
 unicode-script = "0.5.7"

assets/keymaps/default-linux.json 🔗

@@ -527,15 +527,15 @@
       "ctrl-k ctrl-l": "editor::ToggleFold",
       "ctrl-k ctrl-[": "editor::FoldRecursive",
       "ctrl-k ctrl-]": "editor::UnfoldRecursive",
-      "ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
-      "ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
-      "ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
-      "ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
-      "ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
-      "ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
-      "ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
-      "ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
-      "ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
+      "ctrl-k ctrl-1": "editor::FoldAtLevel_1",
+      "ctrl-k ctrl-2": "editor::FoldAtLevel_2",
+      "ctrl-k ctrl-3": "editor::FoldAtLevel_3",
+      "ctrl-k ctrl-4": "editor::FoldAtLevel_4",
+      "ctrl-k ctrl-5": "editor::FoldAtLevel_5",
+      "ctrl-k ctrl-6": "editor::FoldAtLevel_6",
+      "ctrl-k ctrl-7": "editor::FoldAtLevel_7",
+      "ctrl-k ctrl-8": "editor::FoldAtLevel_8",
+      "ctrl-k ctrl-9": "editor::FoldAtLevel_9",
       "ctrl-k ctrl-0": "editor::FoldAll",
       "ctrl-k ctrl-j": "editor::UnfoldAll",
       "ctrl-space": "editor::ShowCompletions",
@@ -1229,9 +1229,6 @@
     "context": "Onboarding",
     "use_key_equivalents": true,
     "bindings": {
-      "ctrl-1": "onboarding::ActivateBasicsPage",
-      "ctrl-2": "onboarding::ActivateEditingPage",
-      "ctrl-3": "onboarding::ActivateAISetupPage",
       "ctrl-enter": "onboarding::Finish",
       "alt-shift-l": "onboarding::SignIn",
       "alt-shift-a": "onboarding::OpenAccount"
@@ -1267,5 +1264,17 @@
       "ctrl-pageup": "settings_editor::FocusPreviousFile",
       "ctrl-pagedown": "settings_editor::FocusNextFile"
     }
+  },
+  {
+    "context": "SettingsWindow > NavigationMenu",
+    "use_key_equivalents": true,
+    "bindings": {
+      "right": "settings_editor::ExpandNavEntry",
+      "left": "settings_editor::CollapseNavEntry",
+      "pageup": "settings_editor::FocusPreviousRootNavEntry",
+      "pagedown": "settings_editor::FocusNextRootNavEntry",
+      "home": "settings_editor::FocusFirstNavEntry",
+      "end": "settings_editor::FocusLastNavEntry"
+    }
   }
 ]

assets/keymaps/default-macos.json 🔗

@@ -582,15 +582,15 @@
       "cmd-k cmd-l": "editor::ToggleFold",
       "cmd-k cmd-[": "editor::FoldRecursive",
       "cmd-k cmd-]": "editor::UnfoldRecursive",
-      "cmd-k cmd-1": ["editor::FoldAtLevel", 1],
-      "cmd-k cmd-2": ["editor::FoldAtLevel", 2],
-      "cmd-k cmd-3": ["editor::FoldAtLevel", 3],
-      "cmd-k cmd-4": ["editor::FoldAtLevel", 4],
-      "cmd-k cmd-5": ["editor::FoldAtLevel", 5],
-      "cmd-k cmd-6": ["editor::FoldAtLevel", 6],
-      "cmd-k cmd-7": ["editor::FoldAtLevel", 7],
-      "cmd-k cmd-8": ["editor::FoldAtLevel", 8],
-      "cmd-k cmd-9": ["editor::FoldAtLevel", 9],
+      "cmd-k cmd-1": "editor::FoldAtLevel_1",
+      "cmd-k cmd-2": "editor::FoldAtLevel_2",
+      "cmd-k cmd-3": "editor::FoldAtLevel_3",
+      "cmd-k cmd-4": "editor::FoldAtLevel_4",
+      "cmd-k cmd-5": "editor::FoldAtLevel_5",
+      "cmd-k cmd-6": "editor::FoldAtLevel_6",
+      "cmd-k cmd-7": "editor::FoldAtLevel_7",
+      "cmd-k cmd-8": "editor::FoldAtLevel_8",
+      "cmd-k cmd-9": "editor::FoldAtLevel_9",
       "cmd-k cmd-0": "editor::FoldAll",
       "cmd-k cmd-j": "editor::UnfoldAll",
       // Using `ctrl-space` / `ctrl-shift-space` in Zed requires disabling the macOS global shortcut.
@@ -1334,10 +1334,7 @@
     "context": "Onboarding",
     "use_key_equivalents": true,
     "bindings": {
-      "cmd-1": "onboarding::ActivateBasicsPage",
-      "cmd-2": "onboarding::ActivateEditingPage",
-      "cmd-3": "onboarding::ActivateAISetupPage",
-      "cmd-escape": "onboarding::Finish",
+      "cmd-enter": "onboarding::Finish",
       "alt-tab": "onboarding::SignIn",
       "alt-shift-a": "onboarding::OpenAccount"
     }
@@ -1372,5 +1369,17 @@
       "cmd-{": "settings_editor::FocusPreviousFile",
       "cmd-}": "settings_editor::FocusNextFile"
     }
+  },
+  {
+    "context": "SettingsWindow > NavigationMenu",
+    "use_key_equivalents": true,
+    "bindings": {
+      "right": "settings_editor::ExpandNavEntry",
+      "left": "settings_editor::CollapseNavEntry",
+      "pageup": "settings_editor::FocusPreviousRootNavEntry",
+      "pagedown": "settings_editor::FocusNextRootNavEntry",
+      "home": "settings_editor::FocusFirstNavEntry",
+      "end": "settings_editor::FocusLastNavEntry"
+    }
   }
 ]

assets/keymaps/default-windows.json 🔗

@@ -536,15 +536,15 @@
       "ctrl-k ctrl-l": "editor::ToggleFold",
       "ctrl-k ctrl-[": "editor::FoldRecursive",
       "ctrl-k ctrl-]": "editor::UnfoldRecursive",
-      "ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
-      "ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
-      "ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
-      "ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
-      "ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
-      "ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
-      "ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
-      "ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
-      "ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
+      "ctrl-k ctrl-1": "editor::FoldAtLevel_1",
+      "ctrl-k ctrl-2": "editor::FoldAtLevel_2",
+      "ctrl-k ctrl-3": "editor::FoldAtLevel_3",
+      "ctrl-k ctrl-4": "editor::FoldAtLevel_4",
+      "ctrl-k ctrl-5": "editor::FoldAtLevel_5",
+      "ctrl-k ctrl-6": "editor::FoldAtLevel_6",
+      "ctrl-k ctrl-7": "editor::FoldAtLevel_7",
+      "ctrl-k ctrl-8": "editor::FoldAtLevel_8",
+      "ctrl-k ctrl-9": "editor::FoldAtLevel_9",
       "ctrl-k ctrl-0": "editor::FoldAll",
       "ctrl-k ctrl-j": "editor::UnfoldAll",
       "ctrl-space": "editor::ShowCompletions",
@@ -1257,9 +1257,6 @@
     "context": "Onboarding",
     "use_key_equivalents": true,
     "bindings": {
-      "ctrl-1": "onboarding::ActivateBasicsPage",
-      "ctrl-2": "onboarding::ActivateEditingPage",
-      "ctrl-3": "onboarding::ActivateAISetupPage",
       "ctrl-enter": "onboarding::Finish",
       "alt-shift-l": "onboarding::SignIn",
       "shift-alt-a": "onboarding::OpenAccount"
@@ -1288,5 +1285,17 @@
       "ctrl-pageup": "settings_editor::FocusPreviousFile",
       "ctrl-pagedown": "settings_editor::FocusNextFile"
     }
+  },
+  {
+    "context": "SettingsWindow > NavigationMenu",
+    "use_key_equivalents": true,
+    "bindings": {
+      "right": "settings_editor::ExpandNavEntry",
+      "left": "settings_editor::CollapseNavEntry",
+      "pageup": "settings_editor::FocusPreviousRootNavEntry",
+      "pagedown": "settings_editor::FocusNextRootNavEntry",
+      "home": "settings_editor::FocusFirstNavEntry",
+      "end": "settings_editor::FocusLastNavEntry"
+    }
   }
 ]

assets/keymaps/vim.json 🔗

@@ -580,18 +580,18 @@
       // "q": "vim::AnyQuotes",
       "q": "vim::MiniQuotes",
       "|": "vim::VerticalBars",
-      "(": "vim::Parentheses",
+      "(": ["vim::Parentheses", { "opening": true }],
       ")": "vim::Parentheses",
       "b": "vim::Parentheses",
       // "b": "vim::AnyBrackets",
       // "b": "vim::MiniBrackets",
-      "[": "vim::SquareBrackets",
+      "[": ["vim::SquareBrackets", { "opening": true }],
       "]": "vim::SquareBrackets",
       "r": "vim::SquareBrackets",
-      "{": "vim::CurlyBrackets",
+      "{": ["vim::CurlyBrackets", { "opening": true }],
       "}": "vim::CurlyBrackets",
       "shift-b": "vim::CurlyBrackets",
-      "<": "vim::AngleBrackets",
+      "<": ["vim::AngleBrackets", { "opening": true }],
       ">": "vim::AngleBrackets",
       "a": "vim::Argument",
       "i": "vim::IndentObj",

assets/settings/default.json 🔗

@@ -76,7 +76,7 @@
   "ui_font_size": 16,
   // The default font size for agent responses in the agent panel. Falls back to the UI font size if unset.
   "agent_ui_font_size": null,
-  // The default font size for user messages in the agent panel. Falls back to the buffer font size if unset.
+  // The default font size for user messages in the agent panel.
   "agent_buffer_font_size": 12,
   // How much to fade out unused code.
   "unnecessary_code_fade": 0.3,
@@ -1311,15 +1311,18 @@
     //   "proxy": "",
     //   "proxy_no_verify": false
     // },
-    // Whether edit predictions are enabled when editing text threads.
-    // This setting has no effect if globally disabled.
-    "enabled_in_text_threads": true,
-
     "copilot": {
       "enterprise_uri": null,
       "proxy": null,
       "proxy_no_verify": null
-    }
+    },
+    "codestral": {
+      "model": null,
+      "max_tokens": null
+    },
+    // Whether edit predictions are enabled when editing text threads.
+    // This setting has no effect if globally disabled.
+    "enabled_in_text_threads": true
   },
   // Settings specific to journaling
   "journal": {
@@ -1424,8 +1427,8 @@
     // Whether or not selecting text in the terminal will automatically
     // copy to the system clipboard.
     "copy_on_select": false,
-    // Whether to keep the text selection after copying it to the clipboard
-    "keep_selection_on_copy": false,
+    // Whether to keep the text selection after copying it to the clipboard.
+    "keep_selection_on_copy": true,
     // Whether to show the terminal button in the status bar
     "button": true,
     // Any key-value pairs added to this list will be added to the terminal's

crates/agent/src/thread.rs 🔗

@@ -3220,7 +3220,6 @@ mod tests {
     use settings::{LanguageModelParameters, Settings, SettingsStore};
     use std::sync::Arc;
     use std::time::Duration;
-    use theme::ThemeSettings;
     use util::path;
     use workspace::Workspace;
 
@@ -5281,7 +5280,7 @@ fn main() {{
             thread_store::init(fs.clone(), cx);
             workspace::init_settings(cx);
             language_model::init_settings(cx);
-            ThemeSettings::register(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             ToolRegistry::default_global(cx);
             assistant_tool::init(cx);
 

crates/agent2/src/agent.rs 🔗

@@ -1418,7 +1418,6 @@ mod tests {
     }
 
     #[gpui::test]
-    #[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
     async fn test_save_load_thread(cx: &mut TestAppContext) {
         init_test(cx);
         let fs = FakeFs::new(cx.executor());
@@ -1498,7 +1497,8 @@ mod tests {
         model.send_last_completion_stream_text_chunk("Lorem.");
         model.end_last_completion_stream();
         cx.run_until_parked();
-        summary_model.send_last_completion_stream_text_chunk("Explaining /a/b.md");
+        summary_model
+            .send_last_completion_stream_text_chunk(&format!("Explaining {}", path!("/a/b.md")));
         summary_model.end_last_completion_stream();
 
         send.await.unwrap();
@@ -1538,7 +1538,7 @@ mod tests {
             history_entries(&history_store, cx),
             vec![(
                 HistoryEntryId::AcpThread(session_id.clone()),
-                "Explaining /a/b.md".into()
+                format!("Explaining {}", path!("/a/b.md"))
             )]
         );
         let acp_thread = agent

crates/agent2/src/thread.rs 🔗

@@ -15,10 +15,11 @@ use agent_settings::{
 use anyhow::{Context as _, Result, anyhow};
 use assistant_tool::adapt_schema_to_format;
 use chrono::{DateTime, Utc};
-use client::{ModelRequestUsage, RequestUsage};
-use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
+use client::{ModelRequestUsage, RequestUsage, UserStore};
+use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
 use collections::{HashMap, HashSet, IndexMap};
 use fs::Fs;
+use futures::stream;
 use futures::{
     FutureExt,
     channel::{mpsc, oneshot},
@@ -34,7 +35,7 @@ use language_model::{
     LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
     LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
     LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
-    LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage,
+    LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
 };
 use project::{
     Project,
@@ -585,6 +586,7 @@ pub struct Thread {
     pending_title_generation: Option<Task<()>>,
     summary: Option<SharedString>,
     messages: Vec<Message>,
+    user_store: Entity<UserStore>,
     completion_mode: CompletionMode,
     /// Holds the task that handles agent interaction until the end of the turn.
     /// Survives across multiple requests as the model performs tool calls and
@@ -641,6 +643,7 @@ impl Thread {
             pending_title_generation: None,
             summary: None,
             messages: Vec::new(),
+            user_store: project.read(cx).user_store(),
             completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
             running_turn: None,
             pending_message: None,
@@ -820,6 +823,7 @@ impl Thread {
             pending_title_generation: None,
             summary: db_thread.detailed_summary,
             messages: db_thread.messages,
+            user_store: project.read(cx).user_store(),
             completion_mode: db_thread.completion_mode.unwrap_or_default(),
             running_turn: None,
             pending_message: None,
@@ -1249,12 +1253,12 @@ impl Thread {
             );
 
             log::debug!("Calling model.stream_completion, attempt {}", attempt);
-            let mut events = model
-                .stream_completion(request, cx)
-                .await
-                .map_err(|error| anyhow!(error))?;
+
+            let (mut events, mut error) = match model.stream_completion(request, cx).await {
+                Ok(events) => (events, None),
+                Err(err) => (stream::empty().boxed(), Some(err)),
+            };
             let mut tool_results = FuturesUnordered::new();
-            let mut error = None;
             while let Some(event) = events.next().await {
                 log::trace!("Received completion event: {:?}", event);
                 match event {
@@ -1302,8 +1306,10 @@ impl Thread {
 
             if let Some(error) = error {
                 attempt += 1;
-                let retry =
-                    this.update(cx, |this, _| this.handle_completion_error(error, attempt))??;
+                let retry = this.update(cx, |this, cx| {
+                    let user_store = this.user_store.read(cx);
+                    this.handle_completion_error(error, attempt, user_store.plan())
+                })??;
                 let timer = cx.background_executor().timer(retry.duration);
                 event_stream.send_retry(retry);
                 timer.await;
@@ -1330,8 +1336,23 @@ impl Thread {
         &mut self,
         error: LanguageModelCompletionError,
         attempt: u8,
+        plan: Option<Plan>,
     ) -> Result<acp_thread::RetryStatus> {
-        if self.completion_mode == CompletionMode::Normal {
+        let Some(model) = self.model.as_ref() else {
+            return Err(anyhow!(error));
+        };
+
+        let auto_retry = if model.provider_id() == ZED_CLOUD_PROVIDER_ID {
+            match plan {
+                Some(Plan::V2(_)) => true,
+                Some(Plan::V1(_)) => self.completion_mode == CompletionMode::Burn,
+                None => false,
+            }
+        } else {
+            true
+        };
+
+        if !auto_retry {
             return Err(anyhow!(error));
         }
 

crates/agent_settings/src/agent_settings.rs 🔗

@@ -151,7 +151,7 @@ impl Default for AgentProfileId {
 }
 
 impl Settings for AgentSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let agent = content.agent.clone().unwrap();
         Self {
             enabled: agent.enabled.unwrap(),

crates/agent_ui/src/acp/entry_view_state.rs 🔗

@@ -414,7 +414,6 @@ mod tests {
     use project::Project;
     use serde_json::json;
     use settings::{Settings as _, SettingsStore};
-    use theme::ThemeSettings;
     use util::path;
     use workspace::Workspace;
 
@@ -544,7 +543,7 @@ mod tests {
             Project::init_settings(cx);
             AgentSettings::register(cx);
             workspace::init_settings(cx);
-            ThemeSettings::register(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             release_channel::init(SemanticVersion::default(), cx);
             EditorSettings::register(cx);
         });

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -1046,32 +1046,33 @@ impl AcpThreadView {
             };
 
             let connection = thread.read(cx).connection().clone();
-            let auth_methods = connection.auth_methods();
-            let has_supported_auth = auth_methods.iter().any(|method| {
-                let id = method.id.0.as_ref();
-                id == "claude-login" || id == "spawn-gemini-cli"
-            });
-            let can_login = has_supported_auth || auth_methods.is_empty() || self.login.is_some();
-            if !can_login {
+            let can_login = !connection.auth_methods().is_empty() || self.login.is_some();
+            // Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
+            let logout_supported = text == "/logout"
+                && self
+                    .available_commands
+                    .borrow()
+                    .iter()
+                    .any(|command| command.name == "logout");
+            if can_login && !logout_supported {
+                let this = cx.weak_entity();
+                let agent = self.agent.clone();
+                window.defer(cx, |window, cx| {
+                    Self::handle_auth_required(
+                        this,
+                        AuthRequired {
+                            description: None,
+                            provider_id: None,
+                        },
+                        agent,
+                        connection,
+                        window,
+                        cx,
+                    );
+                });
+                cx.notify();
                 return;
-            };
-            let this = cx.weak_entity();
-            let agent = self.agent.clone();
-            window.defer(cx, |window, cx| {
-                Self::handle_auth_required(
-                    this,
-                    AuthRequired {
-                        description: None,
-                        provider_id: None,
-                    },
-                    agent,
-                    connection,
-                    window,
-                    cx,
-                );
-            });
-            cx.notify();
-            return;
+            }
         }
 
         self.send_impl(self.message_editor.clone(), window, cx)
@@ -2727,7 +2728,7 @@ impl AcpThreadView {
         let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
 
         let command_failed = command_finished
-            && output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
+            && output.is_some_and(|o| o.exit_status.is_some_and(|status| !status.success()));
 
         let time_elapsed = if let Some(output) = output {
             output.ended_at.duration_since(started_at)
@@ -6086,7 +6087,7 @@ pub(crate) mod tests {
             Project::init_settings(cx);
             AgentSettings::register(cx);
             workspace::init_settings(cx);
-            ThemeSettings::register(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             release_channel::init(SemanticVersion::default(), cx);
             EditorSettings::register(cx);
             prompt_store::init(cx)

crates/agent_ui/src/agent_configuration.rs 🔗

@@ -409,7 +409,7 @@ impl AgentConfiguration {
 
         SwitchField::new(
             "always-allow-tool-actions-switch",
-            "Allow running commands without asking for confirmation",
+            Some("Allow running commands without asking for confirmation"),
             Some(
                 "The agent can perform potentially destructive actions without asking for your confirmation.".into(),
             ),
@@ -429,7 +429,7 @@ impl AgentConfiguration {
 
         SwitchField::new(
             "single-file-review",
-            "Enable single-file agent reviews",
+            Some("Enable single-file agent reviews"),
             Some("Agent edits are also displayed in single-file editors for review.".into()),
             single_file_review,
             move |state, _window, cx| {
@@ -450,7 +450,7 @@ impl AgentConfiguration {
 
         SwitchField::new(
             "sound-notification",
-            "Play sound when finished generating",
+            Some("Play sound when finished generating"),
             Some(
                 "Hear a notification sound when the agent is done generating changes or needs your input.".into(),
             ),
@@ -470,7 +470,7 @@ impl AgentConfiguration {
 
         SwitchField::new(
             "modifier-send",
-            "Use modifier to submit a message",
+            Some("Use modifier to submit a message"),
             Some(
                 "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
             ),

crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs 🔗

@@ -619,10 +619,10 @@ mod tests {
         cx.update(|_window, cx| {
             LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
                 registry.register_provider(
-                    FakeLanguageModelProvider::new(
+                    Arc::new(FakeLanguageModelProvider::new(
                         LanguageModelProviderId::new("someprovider"),
                         LanguageModelProviderName::new("Some Provider"),
-                    ),
+                    )),
                     cx,
                 );
             });

crates/agent_ui/src/agent_diff.rs 🔗

@@ -1814,7 +1814,6 @@ mod tests {
     use serde_json::json;
     use settings::{Settings, SettingsStore};
     use std::{path::Path, rc::Rc};
-    use theme::ThemeSettings;
     use util::path;
 
     #[gpui::test]
@@ -1827,7 +1826,7 @@ mod tests {
             AgentSettings::register(cx);
             prompt_store::init(cx);
             workspace::init_settings(cx);
-            ThemeSettings::register(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             EditorSettings::register(cx);
             language_model::init_settings(cx);
         });
@@ -1979,7 +1978,7 @@ mod tests {
             AgentSettings::register(cx);
             prompt_store::init(cx);
             workspace::init_settings(cx);
-            ThemeSettings::register(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             EditorSettings::register(cx);
             language_model::init_settings(cx);
             workspace::register_project_item::<Editor>(cx);

crates/assistant_tools/src/terminal_tool.rs 🔗

@@ -704,7 +704,6 @@ mod tests {
     use serde_json::json;
     use settings::{Settings, SettingsStore};
     use terminal::terminal_settings::TerminalSettings;
-    use theme::ThemeSettings;
     use util::{ResultExt as _, test::TempTree};
 
     use super::*;
@@ -719,7 +718,7 @@ mod tests {
             language::init(cx);
             Project::init_settings(cx);
             workspace::init_settings(cx);
-            ThemeSettings::register(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             TerminalSettings::register(cx);
             EditorSettings::register(cx);
         });

crates/audio/src/audio.rs 🔗

@@ -1,12 +1,12 @@
 use anyhow::{Context as _, Result};
 use collections::HashMap;
 use gpui::{App, BackgroundExecutor, BorrowAppContext, Global};
+use log::info;
 
 #[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
 mod non_windows_and_freebsd_deps {
     pub(super) use gpui::AsyncApp;
     pub(super) use libwebrtc::native::apm;
-    pub(super) use log::info;
     pub(super) use parking_lot::Mutex;
     pub(super) use rodio::cpal::Sample;
     pub(super) use rodio::source::LimitSettings;

crates/audio/src/audio_settings.rs 🔗

@@ -42,7 +42,7 @@ pub struct AudioSettings {
 
 /// Configuration of audio in Zed
 impl Settings for AudioSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let audio = &content.audio.as_ref().unwrap();
         AudioSettings {
             rodio_audio: audio.rodio_audio.unwrap(),

crates/auto_update/src/auto_update.rs 🔗

@@ -127,7 +127,7 @@ struct AutoUpdateSetting(bool);
 ///
 /// Default: true
 impl Settings for AutoUpdateSetting {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         Self(content.auto_update.unwrap())
     }
 }

crates/call/src/call_settings.rs 🔗

@@ -1,4 +1,3 @@
-use gpui::App;
 use settings::Settings;
 
 #[derive(Debug)]
@@ -8,17 +7,11 @@ pub struct CallSettings {
 }
 
 impl Settings for CallSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let call = content.calls.clone().unwrap();
         CallSettings {
             mute_on_join: call.mute_on_join.unwrap(),
             share_on_join: call.share_on_join.unwrap(),
         }
     }
-
-    fn import_from_vscode(
-        _vscode: &settings::VsCodeSettings,
-        _current: &mut settings::SettingsContent,
-    ) {
-    }
 }

crates/client/src/client.rs 🔗

@@ -101,7 +101,7 @@ pub struct ClientSettings {
 }
 
 impl Settings for ClientSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         if let Some(server_url) = &*ZED_SERVER_URL {
             return Self {
                 server_url: server_url.clone(),
@@ -133,7 +133,7 @@ impl ProxySettings {
 }
 
 impl Settings for ProxySettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         Self {
             proxy: content.proxy.clone(),
         }
@@ -519,7 +519,7 @@ pub struct TelemetrySettings {
 }
 
 impl settings::Settings for TelemetrySettings {
-    fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &SettingsContent) -> Self {
         Self {
             diagnostics: content.telemetry.as_ref().unwrap().diagnostics.unwrap(),
             metrics: content.telemetry.as_ref().unwrap().metrics.unwrap(),

crates/cloud_llm_client/src/predict_edits_v3.rs 🔗

@@ -127,7 +127,6 @@ pub struct DeclarationScoreComponents {
     pub declaration_count: usize,
     pub reference_line_distance: u32,
     pub declaration_line_distance: u32,
-    pub declaration_line_distance_rank: usize,
     pub excerpt_vs_item_jaccard: f32,
     pub excerpt_vs_signature_jaccard: f32,
     pub adjacent_vs_item_jaccard: f32,
@@ -136,6 +135,13 @@ pub struct DeclarationScoreComponents {
     pub excerpt_vs_signature_weighted_overlap: f32,
     pub adjacent_vs_item_weighted_overlap: f32,
     pub adjacent_vs_signature_weighted_overlap: f32,
+    pub path_import_match_count: usize,
+    pub wildcard_path_import_match_count: usize,
+    pub import_similarity: f32,
+    pub max_import_similarity: f32,
+    pub normalized_import_similarity: f32,
+    pub wildcard_import_similarity: f32,
+    pub normalized_wildcard_import_similarity: f32,
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize)]

crates/codestral/Cargo.toml 🔗

@@ -0,0 +1,28 @@
+[package]
+name = "codestral"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lib]
+path = "src/codestral.rs"
+
+[dependencies]
+anyhow.workspace = true
+edit_prediction.workspace = true
+edit_prediction_context.workspace = true
+futures.workspace = true
+gpui.workspace = true
+http_client.workspace = true
+language.workspace = true
+language_models.workspace = true
+log.workspace = true
+mistral.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+smol.workspace = true
+text.workspace = true
+workspace-hack.workspace = true
+
+[dev-dependencies]

crates/codestral/src/codestral.rs 🔗

@@ -0,0 +1,381 @@
+use anyhow::{Context as _, Result};
+use edit_prediction::{Direction, EditPrediction, EditPredictionProvider};
+use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
+use futures::AsyncReadExt;
+use gpui::{App, Context, Entity, Task};
+use http_client::HttpClient;
+use language::{
+    language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview, ToPoint,
+};
+use language_models::MistralLanguageModelProvider;
+use mistral::CODESTRAL_API_URL;
+use serde::{Deserialize, Serialize};
+use std::{
+    ops::Range,
+    sync::Arc,
+    time::{Duration, Instant},
+};
+use text::ToOffset;
+
+pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(150);
+
+const EXCERPT_OPTIONS: EditPredictionExcerptOptions = EditPredictionExcerptOptions {
+    max_bytes: 1050,
+    min_bytes: 525,
+    target_before_cursor_over_total_bytes: 0.66,
+};
+
+/// Represents a completion that has been received and processed from Codestral.
+/// This struct maintains the state needed to interpolate the completion as the user types.
+#[derive(Clone)]
+struct CurrentCompletion {
+    /// The buffer snapshot at the time the completion was generated.
+    /// Used to detect changes and interpolate edits.
+    snapshot: BufferSnapshot,
+    /// The edits that should be applied to transform the original text into the predicted text.
+    /// Each edit is a range in the buffer and the text to replace it with.
+    edits: Arc<[(Range<Anchor>, String)]>,
+    /// Preview of how the buffer will look after applying the edits.
+    edit_preview: EditPreview,
+}
+
+impl CurrentCompletion {
+    /// Attempts to adjust the edits based on changes made to the buffer since the completion was generated.
+    /// Returns None if the user's edits conflict with the predicted edits.
+    fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
+        edit_prediction::interpolate_edits(&self.snapshot, new_snapshot, &self.edits)
+    }
+}
+
+pub struct CodestralCompletionProvider {
+    http_client: Arc<dyn HttpClient>,
+    pending_request: Option<Task<Result<()>>>,
+    current_completion: Option<CurrentCompletion>,
+}
+
+impl CodestralCompletionProvider {
+    pub fn new(http_client: Arc<dyn HttpClient>) -> Self {
+        Self {
+            http_client,
+            pending_request: None,
+            current_completion: None,
+        }
+    }
+
+    pub fn has_api_key(cx: &App) -> bool {
+        Self::api_key(cx).is_some()
+    }
+
+    fn api_key(cx: &App) -> Option<Arc<str>> {
+        MistralLanguageModelProvider::try_global(cx)
+            .and_then(|provider| provider.codestral_api_key(CODESTRAL_API_URL, cx))
+    }
+
+    /// Uses Codestral's Fill-in-the-Middle API for code completion.
+    async fn fetch_completion(
+        http_client: Arc<dyn HttpClient>,
+        api_key: &str,
+        prompt: String,
+        suffix: String,
+        model: String,
+        max_tokens: Option<u32>,
+    ) -> Result<String> {
+        let start_time = Instant::now();
+
+        log::debug!(
+            "Codestral: Requesting completion (model: {}, max_tokens: {:?})",
+            model,
+            max_tokens
+        );
+
+        let request = CodestralRequest {
+            model,
+            prompt,
+            suffix: if suffix.is_empty() {
+                None
+            } else {
+                Some(suffix)
+            },
+            max_tokens: max_tokens.or(Some(350)),
+            temperature: Some(0.2),
+            top_p: Some(1.0),
+            stream: Some(false),
+            stop: None,
+            random_seed: None,
+            min_tokens: None,
+        };
+
+        let request_body = serde_json::to_string(&request)?;
+
+        log::debug!("Codestral: Sending FIM request");
+
+        let http_request = http_client::Request::builder()
+            .method(http_client::Method::POST)
+            .uri(format!("{}/v1/fim/completions", CODESTRAL_API_URL))
+            .header("Content-Type", "application/json")
+            .header("Authorization", format!("Bearer {}", api_key))
+            .body(http_client::AsyncBody::from(request_body))?;
+
+        let mut response = http_client.send(http_request).await?;
+        let status = response.status();
+
+        log::debug!("Codestral: Response status: {}", status);
+
+        if !status.is_success() {
+            let mut body = String::new();
+            response.body_mut().read_to_string(&mut body).await?;
+            return Err(anyhow::anyhow!(
+                "Codestral API error: {} - {}",
+                status,
+                body
+            ));
+        }
+
+        let mut body = String::new();
+        response.body_mut().read_to_string(&mut body).await?;
+
+        let codestral_response: CodestralResponse = serde_json::from_str(&body)?;
+
+        let elapsed = start_time.elapsed();
+
+        if let Some(choice) = codestral_response.choices.first() {
+            let completion = &choice.message.content;
+
+            log::debug!(
+                "Codestral: Completion received ({} tokens, {:.2}s)",
+                codestral_response.usage.completion_tokens,
+                elapsed.as_secs_f64()
+            );
+
+            // Return just the completion text for insertion at cursor
+            Ok(completion.clone())
+        } else {
+            log::error!("Codestral: No completion returned in response");
+            Err(anyhow::anyhow!("No completion returned from Codestral"))
+        }
+    }
+}
+
+impl EditPredictionProvider for CodestralCompletionProvider {
+    fn name() -> &'static str {
+        "codestral"
+    }
+
+    fn display_name() -> &'static str {
+        "Codestral"
+    }
+
+    fn show_completions_in_menu() -> bool {
+        true
+    }
+
+    fn is_enabled(&self, _buffer: &Entity<Buffer>, _cursor_position: Anchor, cx: &App) -> bool {
+        Self::api_key(cx).is_some()
+    }
+
+    fn is_refreshing(&self) -> bool {
+        self.pending_request.is_some()
+    }
+
+    fn refresh(
+        &mut self,
+        buffer: Entity<Buffer>,
+        cursor_position: language::Anchor,
+        debounce: bool,
+        cx: &mut Context<Self>,
+    ) {
+        log::debug!("Codestral: Refresh called (debounce: {})", debounce);
+
+        let Some(api_key) = Self::api_key(cx) else {
+            log::warn!("Codestral: No API key configured, skipping refresh");
+            return;
+        };
+
+        let snapshot = buffer.read(cx).snapshot();
+
+        // Check if current completion is still valid
+        if let Some(current_completion) = self.current_completion.as_ref() {
+            if current_completion.interpolate(&snapshot).is_some() {
+                return;
+            }
+        }
+
+        let http_client = self.http_client.clone();
+
+        // Get settings
+        let settings = all_language_settings(None, cx);
+        let model = settings
+            .edit_predictions
+            .codestral
+            .model
+            .clone()
+            .unwrap_or_else(|| "codestral-latest".to_string());
+        let max_tokens = settings.edit_predictions.codestral.max_tokens;
+
+        self.pending_request = Some(cx.spawn(async move |this, cx| {
+            if debounce {
+                log::debug!("Codestral: Debouncing for {:?}", DEBOUNCE_TIMEOUT);
+                smol::Timer::after(DEBOUNCE_TIMEOUT).await;
+            }
+
+            let cursor_offset = cursor_position.to_offset(&snapshot);
+            let cursor_point = cursor_offset.to_point(&snapshot);
+            let excerpt = EditPredictionExcerpt::select_from_buffer(
+                cursor_point,
+                &snapshot,
+                &EXCERPT_OPTIONS,
+                None,
+            )
+            .context("Line containing cursor doesn't fit in excerpt max bytes")?;
+
+            let excerpt_text = excerpt.text(&snapshot);
+            let cursor_within_excerpt = cursor_offset
+                .saturating_sub(excerpt.range.start)
+                .min(excerpt_text.body.len());
+            let prompt = excerpt_text.body[..cursor_within_excerpt].to_string();
+            let suffix = excerpt_text.body[cursor_within_excerpt..].to_string();
+
+            let completion_text = match Self::fetch_completion(
+                http_client,
+                &api_key,
+                prompt,
+                suffix,
+                model,
+                max_tokens,
+            )
+            .await
+            {
+                Ok(completion) => completion,
+                Err(e) => {
+                    log::error!("Codestral: Failed to fetch completion: {}", e);
+                    this.update(cx, |this, cx| {
+                        this.pending_request = None;
+                        cx.notify();
+                    })?;
+                    return Err(e);
+                }
+            };
+
+            if completion_text.trim().is_empty() {
+                log::debug!("Codestral: Completion was empty after trimming; ignoring");
+                this.update(cx, |this, cx| {
+                    this.pending_request = None;
+                    cx.notify();
+                })?;
+                return Ok(());
+            }
+
+            let edits: Arc<[(Range<Anchor>, String)]> =
+                vec![(cursor_position..cursor_position, completion_text)].into();
+            let edit_preview = buffer
+                .read_with(cx, |buffer, cx| buffer.preview_edits(edits.clone(), cx))?
+                .await;
+
+            this.update(cx, |this, cx| {
+                this.current_completion = Some(CurrentCompletion {
+                    snapshot,
+                    edits,
+                    edit_preview,
+                });
+                this.pending_request = None;
+                cx.notify();
+            })?;
+
+            Ok(())
+        }));
+    }
+
+    fn cycle(
+        &mut self,
+        _buffer: Entity<Buffer>,
+        _cursor_position: Anchor,
+        _direction: Direction,
+        _cx: &mut Context<Self>,
+    ) {
+        // Codestral doesn't support multiple completions, so cycling does nothing
+    }
+
+    fn accept(&mut self, _cx: &mut Context<Self>) {
+        log::debug!("Codestral: Completion accepted");
+        self.pending_request = None;
+        self.current_completion = None;
+    }
+
+    fn discard(&mut self, _cx: &mut Context<Self>) {
+        log::debug!("Codestral: Completion discarded");
+        self.pending_request = None;
+        self.current_completion = None;
+    }
+
+    /// Returns the completion suggestion, adjusted or invalidated based on user edits
+    fn suggest(
+        &mut self,
+        buffer: &Entity<Buffer>,
+        _cursor_position: Anchor,
+        cx: &mut Context<Self>,
+    ) -> Option<EditPrediction> {
+        let current_completion = self.current_completion.as_ref()?;
+        let buffer = buffer.read(cx);
+        let edits = current_completion.interpolate(&buffer.snapshot())?;
+        if edits.is_empty() {
+            return None;
+        }
+        Some(EditPrediction::Local {
+            id: None,
+            edits,
+            edit_preview: Some(current_completion.edit_preview.clone()),
+        })
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CodestralRequest {
+    pub model: String,
+    pub prompt: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub suffix: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub max_tokens: Option<u32>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub temperature: Option<f32>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub top_p: Option<f32>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub stream: Option<bool>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub stop: Option<Vec<String>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub random_seed: Option<u32>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub min_tokens: Option<u32>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct CodestralResponse {
+    pub id: String,
+    pub object: String,
+    pub model: String,
+    pub usage: Usage,
+    pub created: u64,
+    pub choices: Vec<Choice>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Usage {
+    pub prompt_tokens: u32,
+    pub completion_tokens: u32,
+    pub total_tokens: u32,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Choice {
+    pub index: u32,
+    pub message: Message,
+    pub finish_reason: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Message {
+    pub content: String,
+    pub role: String,
+}

crates/collab/src/tests/editor_tests.rs 🔗

@@ -1272,7 +1272,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
     fake_language_server.start_progress("the-token").await;
 
     executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
-    fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
+    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
         token: lsp::NumberOrString::String("the-token".to_string()),
         value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
             lsp::WorkDoneProgressReport {
@@ -1306,7 +1306,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
     });
 
     executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
-    fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
+    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
         token: lsp::NumberOrString::String("the-token".to_string()),
         value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
             lsp::WorkDoneProgressReport {
@@ -2041,6 +2041,10 @@ async fn test_mutual_editor_inlay_hint_cache_update(
     });
 }
 
+// This test started hanging on seed 2 after the theme settings
+// PR. The hypothesis is that it's been buggy for a while, but got lucky
+// on seeds.
+#[ignore]
 #[gpui::test(iterations = 10)]
 async fn test_inlay_hint_refresh_is_forwarded(
     cx_a: &mut TestAppContext,
@@ -2844,7 +2848,7 @@ async fn test_lsp_pull_diagnostics(
     });
 
     fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
-        &lsp::PublishDiagnosticsParams {
+        lsp::PublishDiagnosticsParams {
             uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
             diagnostics: vec![lsp::Diagnostic {
                 range: lsp::Range {
@@ -2865,7 +2869,7 @@ async fn test_lsp_pull_diagnostics(
         },
     );
     fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
-        &lsp::PublishDiagnosticsParams {
+        lsp::PublishDiagnosticsParams {
             uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
             diagnostics: vec![lsp::Diagnostic {
                 range: lsp::Range {
@@ -2887,7 +2891,7 @@ async fn test_lsp_pull_diagnostics(
     );
 
     if should_stream_workspace_diagnostic {
-        fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
+        fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
             token: expected_workspace_diagnostic_token.clone(),
             value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
                 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
@@ -3069,7 +3073,7 @@ async fn test_lsp_pull_diagnostics(
     });
 
     if should_stream_workspace_diagnostic {
-        fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
+        fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
             token: expected_workspace_diagnostic_token.clone(),
             value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
                 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {

crates/collab/src/tests/integration_tests.rs 🔗

@@ -4077,7 +4077,7 @@ async fn test_collaborating_with_diagnostics(
         .receive_notification::<lsp::notification::DidOpenTextDocument>()
         .await;
     fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
-        &lsp::PublishDiagnosticsParams {
+        lsp::PublishDiagnosticsParams {
             uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
             version: None,
             diagnostics: vec![lsp::Diagnostic {
@@ -4097,7 +4097,7 @@ async fn test_collaborating_with_diagnostics(
         .await
         .unwrap();
     fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
-        &lsp::PublishDiagnosticsParams {
+        lsp::PublishDiagnosticsParams {
             uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
             version: None,
             diagnostics: vec![lsp::Diagnostic {
@@ -4171,7 +4171,7 @@ async fn test_collaborating_with_diagnostics(
 
     // Simulate a language server reporting more errors for a file.
     fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
-        &lsp::PublishDiagnosticsParams {
+        lsp::PublishDiagnosticsParams {
             uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
             version: None,
             diagnostics: vec![
@@ -4269,7 +4269,7 @@ async fn test_collaborating_with_diagnostics(
 
     // Simulate a language server reporting no errors for a file.
     fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
-        &lsp::PublishDiagnosticsParams {
+        lsp::PublishDiagnosticsParams {
             uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
             version: None,
             diagnostics: Vec::new(),
@@ -4365,7 +4365,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
         .await
         .into_response()
         .unwrap();
-    fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
+    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
         token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
         value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
             lsp::WorkDoneProgressBegin {
@@ -4376,7 +4376,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
     });
     for file_name in file_names {
         fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
-            &lsp::PublishDiagnosticsParams {
+            lsp::PublishDiagnosticsParams {
                 uri: lsp::Uri::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
                 version: None,
                 diagnostics: vec![lsp::Diagnostic {
@@ -4389,7 +4389,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
             },
         );
     }
-    fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
+    fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
         token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
         value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
             lsp::WorkDoneProgressEnd { message: None },

crates/collab/src/tests/randomized_test_helpers.rs 🔗

@@ -183,9 +183,10 @@ pub async fn run_randomized_test<T: RandomizedTest>(
 
     for (client, cx) in clients {
         cx.update(|cx| {
-            let store = cx.remove_global::<SettingsStore>();
+            let settings = cx.remove_global::<SettingsStore>();
             cx.clear_globals();
-            cx.set_global(store);
+            cx.set_global(settings);
+            theme::init(theme::LoadThemes::JustBase, cx);
             drop(client);
         });
     }

crates/collab/src/tests/test_server.rs 🔗

@@ -172,6 +172,7 @@ impl TestServer {
             }
             let settings = SettingsStore::test(cx);
             cx.set_global(settings);
+            theme::init(theme::LoadThemes::JustBase, cx);
             release_channel::init(SemanticVersion::default(), cx);
             client::init_settings(cx);
         });

crates/collab_ui/src/panel_settings.rs 🔗

@@ -18,7 +18,7 @@ pub struct NotificationPanelSettings {
 }
 
 impl Settings for CollaborationPanelSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let panel = content.collaboration_panel.as_ref().unwrap();
 
         Self {
@@ -30,7 +30,7 @@ impl Settings for CollaborationPanelSettings {
 }
 
 impl Settings for NotificationPanelSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let panel = content.notification_panel.as_ref().unwrap();
         return Self {
             button: panel.button.unwrap(),

crates/command_palette_hooks/src/command_palette_hooks.rs 🔗

@@ -97,11 +97,10 @@ impl CommandPaletteFilter {
 pub struct CommandInterceptResult {
     /// The action produced as a result of the interception.
     pub action: Box<dyn Action>,
-    // TODO: Document this field.
-    #[allow(missing_docs)]
+    /// The display string to show in the command palette for this result.
     pub string: String,
-    // TODO: Document this field.
-    #[allow(missing_docs)]
+    /// The character positions in the string that match the query.
+    /// Used for highlighting matched characters in the command palette UI.
     pub positions: Vec<usize>,
 }
 

crates/copilot/src/copilot.rs 🔗

@@ -270,7 +270,7 @@ impl RegisteredBuffer {
                             server
                                 .lsp
                                 .notify::<lsp::notification::DidChangeTextDocument>(
-                                    &lsp::DidChangeTextDocumentParams {
+                                    lsp::DidChangeTextDocumentParams {
                                         text_document: lsp::VersionedTextDocumentIdentifier::new(
                                             buffer.uri.clone(),
                                             buffer.snapshot_version,
@@ -744,7 +744,7 @@ impl Copilot {
                 let snapshot = buffer.read(cx).snapshot();
                 server
                     .notify::<lsp::notification::DidOpenTextDocument>(
-                        &lsp::DidOpenTextDocumentParams {
+                        lsp::DidOpenTextDocumentParams {
                             text_document: lsp::TextDocumentItem {
                                 uri: uri.clone(),
                                 language_id: language_id.clone(),
@@ -792,13 +792,14 @@ impl Copilot {
                     server
                         .lsp
                         .notify::<lsp::notification::DidSaveTextDocument>(
-                            &lsp::DidSaveTextDocumentParams {
+                            lsp::DidSaveTextDocumentParams {
                                 text_document: lsp::TextDocumentIdentifier::new(
                                     registered_buffer.uri.clone(),
                                 ),
                                 text: None,
                             },
-                        )?;
+                        )
+                        .ok();
                 }
                 language::BufferEvent::FileHandleChanged
                 | language::BufferEvent::LanguageChanged => {
@@ -814,14 +815,15 @@ impl Copilot {
                         server
                             .lsp
                             .notify::<lsp::notification::DidCloseTextDocument>(
-                                &lsp::DidCloseTextDocumentParams {
+                                lsp::DidCloseTextDocumentParams {
                                     text_document: lsp::TextDocumentIdentifier::new(old_uri),
                                 },
-                            )?;
+                            )
+                            .ok();
                         server
                             .lsp
                             .notify::<lsp::notification::DidOpenTextDocument>(
-                                &lsp::DidOpenTextDocumentParams {
+                                lsp::DidOpenTextDocumentParams {
                                     text_document: lsp::TextDocumentItem::new(
                                         registered_buffer.uri.clone(),
                                         registered_buffer.language_id.clone(),
@@ -829,7 +831,8 @@ impl Copilot {
                                         registered_buffer.snapshot.text(),
                                     ),
                                 },
-                            )?;
+                            )
+                            .ok();
                     }
                 }
                 _ => {}
@@ -846,7 +849,7 @@ impl Copilot {
             server
                 .lsp
                 .notify::<lsp::notification::DidCloseTextDocument>(
-                    &lsp::DidCloseTextDocumentParams {
+                    lsp::DidCloseTextDocumentParams {
                         text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
                     },
                 )
@@ -1151,9 +1154,12 @@ fn notify_did_change_config_to_server(
         }
     });
 
-    server.notify::<lsp::notification::DidChangeConfiguration>(&lsp::DidChangeConfigurationParams {
-        settings,
-    })
+    server
+        .notify::<lsp::notification::DidChangeConfiguration>(lsp::DidChangeConfigurationParams {
+            settings,
+        })
+        .ok();
+    Ok(())
 }
 
 async fn clear_copilot_dir() {

crates/dap/src/adapters.rs 🔗

@@ -46,6 +46,7 @@ pub trait DapDelegate: Send + Sync + 'static {
     async fn which(&self, command: &OsStr) -> Option<PathBuf>;
     async fn read_text_file(&self, path: &RelPath) -> Result<String>;
     async fn shell_env(&self) -> collections::HashMap<String, String>;
+    fn is_headless(&self) -> bool;
 }
 
 #[derive(

crates/dap/src/debugger_settings.rs 🔗

@@ -1,5 +1,4 @@
 use dap_types::SteppingGranularity;
-use gpui::App;
 use settings::{Settings, SettingsContent};
 
 pub struct DebuggerSettings {
@@ -34,7 +33,7 @@ pub struct DebuggerSettings {
 }
 
 impl Settings for DebuggerSettings {
-    fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &SettingsContent) -> Self {
         let content = content.debugger.clone().unwrap();
         Self {
             stepping_granularity: dap_granularity_from_settings(

crates/dap_adapters/src/javascript.rs 🔗

@@ -120,6 +120,13 @@ impl JsDebugAdapter {
             configuration
                 .entry("sourceMapRenames")
                 .or_insert(true.into());
+
+            // Set up remote browser debugging
+            if delegate.is_headless() {
+                configuration
+                    .entry("browserLaunchLocation")
+                    .or_insert("ui".into());
+            }
         }
 
         let adapter_path = if let Some(user_installed_path) = user_installed_path {

crates/edit_prediction/src/edit_prediction.rs 🔗

@@ -2,7 +2,7 @@ use std::ops::Range;
 
 use client::EditPredictionUsage;
 use gpui::{App, Context, Entity, SharedString};
-use language::Buffer;
+use language::{Anchor, Buffer, BufferSnapshot, OffsetRangeExt};
 
 // TODO: Find a better home for `Direction`.
 //
@@ -242,3 +242,51 @@ where
         self.update(cx, |this, cx| this.suggest(buffer, cursor_position, cx))
     }
 }
+
+/// Returns edits updated based on user edits since the old snapshot. None is returned if any user
+/// edit is not a prefix of a predicted insertion.
+pub fn interpolate_edits(
+    old_snapshot: &BufferSnapshot,
+    new_snapshot: &BufferSnapshot,
+    current_edits: &[(Range<Anchor>, String)],
+) -> Option<Vec<(Range<Anchor>, String)>> {
+    let mut edits = Vec::new();
+
+    let mut model_edits = current_edits.iter().peekable();
+    for user_edit in new_snapshot.edits_since::<usize>(&old_snapshot.version) {
+        while let Some((model_old_range, _)) = model_edits.peek() {
+            let model_old_range = model_old_range.to_offset(old_snapshot);
+            if model_old_range.end < user_edit.old.start {
+                let (model_old_range, model_new_text) = model_edits.next().unwrap();
+                edits.push((model_old_range.clone(), model_new_text.clone()));
+            } else {
+                break;
+            }
+        }
+
+        if let Some((model_old_range, model_new_text)) = model_edits.peek() {
+            let model_old_offset_range = model_old_range.to_offset(old_snapshot);
+            if user_edit.old == model_old_offset_range {
+                let user_new_text = new_snapshot
+                    .text_for_range(user_edit.new.clone())
+                    .collect::<String>();
+
+                if let Some(model_suffix) = model_new_text.strip_prefix(&user_new_text) {
+                    if !model_suffix.is_empty() {
+                        let anchor = old_snapshot.anchor_after(user_edit.old.end);
+                        edits.push((anchor..anchor, model_suffix.to_string()));
+                    }
+
+                    model_edits.next();
+                    continue;
+                }
+            }
+        }
+
+        return None;
+    }
+
+    edits.extend(model_edits.cloned());
+
+    if edits.is_empty() { None } else { Some(edits) }
+}

crates/edit_prediction_button/Cargo.toml 🔗

@@ -16,6 +16,7 @@ doctest = false
 anyhow.workspace = true
 client.workspace = true
 cloud_llm_client.workspace = true
+codestral.workspace = true
 copilot.workspace = true
 editor.workspace = true
 feature_flags.workspace = true

crates/edit_prediction_button/src/edit_prediction_button.rs 🔗

@@ -1,6 +1,7 @@
 use anyhow::Result;
 use client::{UserStore, zed_urls};
 use cloud_llm_client::UsageLimit;
+use codestral::CodestralCompletionProvider;
 use copilot::{Copilot, Status};
 use editor::{Editor, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll};
 use feature_flags::{FeatureFlagAppExt, PredictEditsRateCompletionsFeatureFlag};
@@ -234,6 +235,67 @@ impl Render for EditPredictionButton {
                 )
             }
 
+            EditPredictionProvider::Codestral => {
+                let enabled = self.editor_enabled.unwrap_or(true);
+                let has_api_key = CodestralCompletionProvider::has_api_key(cx);
+                let fs = self.fs.clone();
+                let this = cx.entity();
+
+                div().child(
+                    PopoverMenu::new("codestral")
+                        .menu(move |window, cx| {
+                            if has_api_key {
+                                Some(this.update(cx, |this, cx| {
+                                    this.build_codestral_context_menu(window, cx)
+                                }))
+                            } else {
+                                Some(ContextMenu::build(window, cx, |menu, _, _| {
+                                    let fs = fs.clone();
+                                    menu.entry("Use Zed AI instead", None, move |_, cx| {
+                                        set_completion_provider(
+                                            fs.clone(),
+                                            cx,
+                                            EditPredictionProvider::Zed,
+                                        )
+                                    })
+                                    .separator()
+                                    .entry(
+                                        "Configure Codestral API Key",
+                                        None,
+                                        move |window, cx| {
+                                            window.dispatch_action(
+                                                zed_actions::agent::OpenSettings.boxed_clone(),
+                                                cx,
+                                            );
+                                        },
+                                    )
+                                }))
+                            }
+                        })
+                        .anchor(Corner::BottomRight)
+                        .trigger_with_tooltip(
+                            IconButton::new("codestral-icon", IconName::AiMistral)
+                                .shape(IconButtonShape::Square)
+                                .when(!has_api_key, |this| {
+                                    this.indicator(Indicator::dot().color(Color::Error))
+                                        .indicator_border_color(Some(
+                                            cx.theme().colors().status_bar_background,
+                                        ))
+                                })
+                                .when(has_api_key && !enabled, |this| {
+                                    this.indicator(Indicator::dot().color(Color::Ignored))
+                                        .indicator_border_color(Some(
+                                            cx.theme().colors().status_bar_background,
+                                        ))
+                                }),
+                            move |window, cx| {
+                                Tooltip::for_action("Codestral", &ToggleMenu, window, cx)
+                            },
+                        )
+                        .with_handle(self.popover_menu_handle.clone()),
+                )
+            }
+
             EditPredictionProvider::Zed => {
                 let enabled = self.editor_enabled.unwrap_or(true);
 
@@ -493,6 +555,7 @@ impl EditPredictionButton {
             EditPredictionProvider::Zed
                 | EditPredictionProvider::Copilot
                 | EditPredictionProvider::Supermaven
+                | EditPredictionProvider::Codestral
         ) {
             menu = menu
                 .separator()
@@ -719,6 +782,25 @@ impl EditPredictionButton {
         })
     }
 
+    fn build_codestral_context_menu(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Entity<ContextMenu> {
+        let fs = self.fs.clone();
+        ContextMenu::build(window, cx, |menu, window, cx| {
+            self.build_language_settings_menu(menu, window, cx)
+                .separator()
+                .entry("Use Zed AI instead", None, move |_, cx| {
+                    set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
+                })
+                .separator()
+                .entry("Configure Codestral API Key", None, move |window, cx| {
+                    window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
+                })
+        })
+    }
+
     fn build_zeta_context_menu(
         &self,
         window: &mut Window,

crates/edit_prediction_context/Cargo.toml 🔗

@@ -19,6 +19,7 @@ collections.workspace = true
 futures.workspace = true
 gpui.workspace = true
 hashbrown.workspace = true
+indoc.workspace = true
 itertools.workspace = true
 language.workspace = true
 log.workspace = true
@@ -45,5 +46,8 @@ project = {workspace= true, features = ["test-support"]}
 serde_json.workspace = true
 settings = {workspace= true, features = ["test-support"]}
 text = { workspace = true, features = ["test-support"] }
+tree-sitter-c.workspace = true
+tree-sitter-cpp.workspace = true
+tree-sitter-go.workspace = true
 util = { workspace = true, features = ["test-support"] }
 zlog.workspace = true

crates/edit_prediction_context/src/declaration.rs 🔗

@@ -1,9 +1,11 @@
-use language::LanguageId;
+use language::{Language, LanguageId};
 use project::ProjectEntryId;
-use std::borrow::Cow;
 use std::ops::Range;
 use std::sync::Arc;
+use std::{borrow::Cow, path::Path};
 use text::{Bias, BufferId, Rope};
+use util::paths::{path_ends_with, strip_path_suffix};
+use util::rel_path::RelPath;
 
 use crate::outline::OutlineDeclaration;
 
@@ -22,12 +24,14 @@ pub enum Declaration {
     File {
         project_entry_id: ProjectEntryId,
         declaration: FileDeclaration,
+        cached_path: CachedDeclarationPath,
     },
     Buffer {
         project_entry_id: ProjectEntryId,
         buffer_id: BufferId,
         rope: Rope,
         declaration: BufferDeclaration,
+        cached_path: CachedDeclarationPath,
     },
 }
 
@@ -73,6 +77,13 @@ impl Declaration {
         }
     }
 
+    pub fn cached_path(&self) -> &CachedDeclarationPath {
+        match self {
+            Declaration::File { cached_path, .. } => cached_path,
+            Declaration::Buffer { cached_path, .. } => cached_path,
+        }
+    }
+
     pub fn item_range(&self) -> Range<usize> {
         match self {
             Declaration::File { declaration, .. } => declaration.item_range.clone(),
@@ -235,3 +246,69 @@ impl BufferDeclaration {
         }
     }
 }
+
+#[derive(Debug, Clone)]
+pub struct CachedDeclarationPath {
+    pub worktree_abs_path: Arc<Path>,
+    pub rel_path: Arc<RelPath>,
+    /// The relative path of the file, possibly stripped according to `import_path_strip_regex`.
+    pub rel_path_after_regex_stripping: Arc<RelPath>,
+}
+
+impl CachedDeclarationPath {
+    pub fn new(
+        worktree_abs_path: Arc<Path>,
+        path: &Arc<RelPath>,
+        language: Option<&Arc<Language>>,
+    ) -> Self {
+        let rel_path = path.clone();
+        let rel_path_after_regex_stripping = if let Some(language) = language
+            && let Some(strip_regex) = language.config().import_path_strip_regex.as_ref()
+            && let Ok(stripped) = RelPath::unix(&Path::new(
+                strip_regex.replace_all(rel_path.as_unix_str(), "").as_ref(),
+            )) {
+            Arc::from(stripped)
+        } else {
+            rel_path.clone()
+        };
+        CachedDeclarationPath {
+            worktree_abs_path,
+            rel_path,
+            rel_path_after_regex_stripping,
+        }
+    }
+
+    #[cfg(test)]
+    pub fn new_for_test(worktree_abs_path: &str, rel_path: &str) -> Self {
+        let rel_path: Arc<RelPath> = util::rel_path::rel_path(rel_path).into();
+        CachedDeclarationPath {
+            worktree_abs_path: std::path::PathBuf::from(worktree_abs_path).into(),
+            rel_path_after_regex_stripping: rel_path.clone(),
+            rel_path,
+        }
+    }
+
+    pub fn ends_with_posix_path(&self, path: &Path) -> bool {
+        if path.as_os_str().len() <= self.rel_path_after_regex_stripping.as_unix_str().len() {
+            path_ends_with(self.rel_path_after_regex_stripping.as_std_path(), path)
+        } else {
+            if let Some(remaining) =
+                strip_path_suffix(path, self.rel_path_after_regex_stripping.as_std_path())
+            {
+                path_ends_with(&self.worktree_abs_path, remaining)
+            } else {
+                false
+            }
+        }
+    }
+
+    pub fn equals_absolute_path(&self, path: &Path) -> bool {
+        if let Some(remaining) =
+            strip_path_suffix(path, &self.rel_path_after_regex_stripping.as_std_path())
+        {
+            self.worktree_abs_path.as_ref() == remaining
+        } else {
+            false
+        }
+    }
+}

crates/edit_prediction_context/src/declaration_scoring.rs 🔗

@@ -1,15 +1,15 @@
 use cloud_llm_client::predict_edits_v3::DeclarationScoreComponents;
 use collections::HashMap;
-use itertools::Itertools as _;
 use language::BufferSnapshot;
 use ordered_float::OrderedFloat;
 use serde::Serialize;
-use std::{cmp::Reverse, ops::Range};
+use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
 use strum::EnumIter;
 use text::{Point, ToPoint};
 
 use crate::{
-    Declaration, EditPredictionExcerpt, Identifier,
+    CachedDeclarationPath, Declaration, EditPredictionExcerpt, Identifier,
+    imports::{Import, Imports, Module},
     reference::{Reference, ReferenceRegion},
     syntax_index::SyntaxIndexState,
     text_similarity::{Occurrences, jaccard_similarity, weighted_overlap_coefficient},
@@ -17,12 +17,17 @@ use crate::{
 
 const MAX_IDENTIFIER_DECLARATION_COUNT: usize = 16;
 
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct EditPredictionScoreOptions {
+    pub omit_excerpt_overlaps: bool,
+}
+
 #[derive(Clone, Debug)]
 pub struct ScoredDeclaration {
+    /// identifier used by the local reference
     pub identifier: Identifier,
     pub declaration: Declaration,
-    pub score_components: DeclarationScoreComponents,
-    pub scores: DeclarationScores,
+    pub components: DeclarationScoreComponents,
 }
 
 #[derive(EnumIter, Clone, Copy, PartialEq, Eq, Hash, Debug)]
@@ -31,12 +36,55 @@ pub enum DeclarationStyle {
     Declaration,
 }
 
+#[derive(Clone, Debug, Serialize, Default)]
+pub struct DeclarationScores {
+    pub signature: f32,
+    pub declaration: f32,
+    pub retrieval: f32,
+}
+
 impl ScoredDeclaration {
     /// Returns the score for this declaration with the specified style.
     pub fn score(&self, style: DeclarationStyle) -> f32 {
+        // TODO: handle truncation
+
+        // Score related to how likely this is the correct declaration, range 0 to 1
+        let retrieval = self.retrieval_score();
+
+        // Score related to the distance between the reference and cursor, range 0 to 1
+        let distance_score = if self.components.is_referenced_nearby {
+            1.0 / (1.0 + self.components.reference_line_distance as f32 / 10.0).powf(2.0)
+        } else {
+            // same score as ~14 lines away, rationale is to not overly penalize references from parent signatures
+            0.5
+        };
+
+        // For now instead of linear combination, the scores are just multiplied together.
+        let combined_score = 10.0 * retrieval * distance_score;
+
         match style {
-            DeclarationStyle::Signature => self.scores.signature,
-            DeclarationStyle::Declaration => self.scores.declaration,
+            DeclarationStyle::Signature => {
+                combined_score * self.components.excerpt_vs_signature_weighted_overlap
+            }
+            DeclarationStyle::Declaration => {
+                2.0 * combined_score * self.components.excerpt_vs_item_weighted_overlap
+            }
+        }
+    }
+
+    pub fn retrieval_score(&self) -> f32 {
+        if self.components.is_same_file {
+            10.0 / self.components.same_file_declaration_count as f32
+        } else if self.components.path_import_match_count > 0 {
+            3.0
+        } else if self.components.wildcard_path_import_match_count > 0 {
+            1.0
+        } else if self.components.normalized_import_similarity > 0.0 {
+            self.components.normalized_import_similarity
+        } else if self.components.normalized_wildcard_import_similarity > 0.0 {
+            0.5 * self.components.normalized_wildcard_import_similarity
+        } else {
+            1.0 / self.components.declaration_count as f32
         }
     }
 
@@ -54,100 +102,215 @@ impl ScoredDeclaration {
     }
 
     pub fn score_density(&self, style: DeclarationStyle) -> f32 {
-        self.score(style) / (self.size(style)) as f32
+        self.score(style) / self.size(style) as f32
     }
 }
 
 pub fn scored_declarations(
+    options: &EditPredictionScoreOptions,
     index: &SyntaxIndexState,
     excerpt: &EditPredictionExcerpt,
     excerpt_occurrences: &Occurrences,
     adjacent_occurrences: &Occurrences,
+    imports: &Imports,
     identifier_to_references: HashMap<Identifier, Vec<Reference>>,
     cursor_offset: usize,
     current_buffer: &BufferSnapshot,
 ) -> Vec<ScoredDeclaration> {
     let cursor_point = cursor_offset.to_point(&current_buffer);
 
+    let mut wildcard_import_occurrences = Vec::new();
+    let mut wildcard_import_paths = Vec::new();
+    for wildcard_import in imports.wildcard_modules.iter() {
+        match wildcard_import {
+            Module::Namespace(namespace) => {
+                wildcard_import_occurrences.push(namespace.occurrences())
+            }
+            Module::SourceExact(path) => wildcard_import_paths.push(path),
+            Module::SourceFuzzy(path) => {
+                wildcard_import_occurrences.push(Occurrences::from_path(&path))
+            }
+        }
+    }
+
     let mut declarations = identifier_to_references
         .into_iter()
         .flat_map(|(identifier, references)| {
-            let declarations =
-                index.declarations_for_identifier::<MAX_IDENTIFIER_DECLARATION_COUNT>(&identifier);
+            let mut import_occurrences = Vec::new();
+            let mut import_paths = Vec::new();
+            let mut found_external_identifier: Option<&Identifier> = None;
+
+            if let Some(imports) = imports.identifier_to_imports.get(&identifier) {
+                // only use alias when it's the only import, could be generalized if some language
+                // has overlapping aliases
+                //
+                // TODO: when an aliased declaration is included in the prompt, should include the
+                // aliasing in the prompt.
+                //
+                // TODO: For SourceFuzzy consider having componentwise comparison that pays
+                // attention to ordering.
+                if let [
+                    Import::Alias {
+                        module,
+                        external_identifier,
+                    },
+                ] = imports.as_slice()
+                {
+                    match module {
+                        Module::Namespace(namespace) => {
+                            import_occurrences.push(namespace.occurrences())
+                        }
+                        Module::SourceExact(path) => import_paths.push(path),
+                        Module::SourceFuzzy(path) => {
+                            import_occurrences.push(Occurrences::from_path(&path))
+                        }
+                    }
+                    found_external_identifier = Some(&external_identifier);
+                } else {
+                    for import in imports {
+                        match import {
+                            Import::Direct { module } => match module {
+                                Module::Namespace(namespace) => {
+                                    import_occurrences.push(namespace.occurrences())
+                                }
+                                Module::SourceExact(path) => import_paths.push(path),
+                                Module::SourceFuzzy(path) => {
+                                    import_occurrences.push(Occurrences::from_path(&path))
+                                }
+                            },
+                            Import::Alias { .. } => {}
+                        }
+                    }
+                }
+            }
+
+            let identifier_to_lookup = found_external_identifier.unwrap_or(&identifier);
+            // TODO: update this to be able to return more declarations? Especially if there is the
+            // ability to quickly filter a large list (based on imports)
+            let declarations = index
+                .declarations_for_identifier::<MAX_IDENTIFIER_DECLARATION_COUNT>(
+                    &identifier_to_lookup,
+                );
             let declaration_count = declarations.len();
 
-            declarations
-                .into_iter()
-                .filter_map(|(declaration_id, declaration)| match declaration {
+            if declaration_count == 0 {
+                return Vec::new();
+            }
+
+            // TODO: option to filter out other candidates when same file / import match
+            let mut checked_declarations = Vec::new();
+            for (declaration_id, declaration) in declarations {
+                match declaration {
                     Declaration::Buffer {
                         buffer_id,
                         declaration: buffer_declaration,
                         ..
                     } => {
-                        let is_same_file = buffer_id == &current_buffer.remote_id();
-
-                        if is_same_file {
-                            let overlaps_excerpt =
+                        if buffer_id == &current_buffer.remote_id() {
+                            let already_included_in_prompt =
                                 range_intersection(&buffer_declaration.item_range, &excerpt.range)
-                                    .is_some();
-                            if overlaps_excerpt
-                                || excerpt
-                                    .parent_declarations
-                                    .iter()
-                                    .any(|(excerpt_parent, _)| excerpt_parent == &declaration_id)
-                            {
-                                None
-                            } else {
+                                    .is_some()
+                                    || excerpt.parent_declarations.iter().any(
+                                        |(excerpt_parent, _)| excerpt_parent == &declaration_id,
+                                    );
+                            if !options.omit_excerpt_overlaps || !already_included_in_prompt {
                                 let declaration_line = buffer_declaration
                                     .item_range
                                     .start
                                     .to_point(current_buffer)
                                     .row;
-                                Some((
-                                    true,
-                                    (cursor_point.row as i32 - declaration_line as i32)
-                                        .unsigned_abs(),
+                                let declaration_line_distance = (cursor_point.row as i32
+                                    - declaration_line as i32)
+                                    .unsigned_abs();
+                                checked_declarations.push(CheckedDeclaration {
                                     declaration,
-                                ))
+                                    same_file_line_distance: Some(declaration_line_distance),
+                                    path_import_match_count: 0,
+                                    wildcard_path_import_match_count: 0,
+                                });
                             }
+                            continue;
                         } else {
-                            Some((false, u32::MAX, declaration))
                         }
                     }
-                    Declaration::File { .. } => {
-                        // We can assume that a file declaration is in a different file,
-                        // because the current one must be open
-                        Some((false, u32::MAX, declaration))
+                    Declaration::File { .. } => {}
+                }
+                let declaration_path = declaration.cached_path();
+                let path_import_match_count = import_paths
+                    .iter()
+                    .filter(|import_path| {
+                        declaration_path_matches_import(&declaration_path, import_path)
+                    })
+                    .count();
+                let wildcard_path_import_match_count = wildcard_import_paths
+                    .iter()
+                    .filter(|import_path| {
+                        declaration_path_matches_import(&declaration_path, import_path)
+                    })
+                    .count();
+                checked_declarations.push(CheckedDeclaration {
+                    declaration,
+                    same_file_line_distance: None,
+                    path_import_match_count,
+                    wildcard_path_import_match_count,
+                });
+            }
+
+            let mut max_import_similarity = 0.0;
+            let mut max_wildcard_import_similarity = 0.0;
+
+            let mut scored_declarations_for_identifier = checked_declarations
+                .into_iter()
+                .map(|checked_declaration| {
+                    let same_file_declaration_count =
+                        index.file_declaration_count(checked_declaration.declaration);
+
+                    let declaration = score_declaration(
+                        &identifier,
+                        &references,
+                        checked_declaration,
+                        same_file_declaration_count,
+                        declaration_count,
+                        &excerpt_occurrences,
+                        &adjacent_occurrences,
+                        &import_occurrences,
+                        &wildcard_import_occurrences,
+                        cursor_point,
+                        current_buffer,
+                    );
+
+                    if declaration.components.import_similarity > max_import_similarity {
+                        max_import_similarity = declaration.components.import_similarity;
+                    }
+
+                    if declaration.components.wildcard_import_similarity
+                        > max_wildcard_import_similarity
+                    {
+                        max_wildcard_import_similarity =
+                            declaration.components.wildcard_import_similarity;
                     }
+
+                    declaration
                 })
-                .sorted_by_key(|&(_, distance, _)| distance)
-                .enumerate()
-                .map(
-                    |(
-                        declaration_line_distance_rank,
-                        (is_same_file, declaration_line_distance, declaration),
-                    )| {
-                        let same_file_declaration_count = index.file_declaration_count(declaration);
-
-                        score_declaration(
-                            &identifier,
-                            &references,
-                            declaration.clone(),
-                            is_same_file,
-                            declaration_line_distance,
-                            declaration_line_distance_rank,
-                            same_file_declaration_count,
-                            declaration_count,
-                            &excerpt_occurrences,
-                            &adjacent_occurrences,
-                            cursor_point,
-                            current_buffer,
-                        )
-                    },
-                )
-                .collect::<Vec<_>>()
+                .collect::<Vec<_>>();
+
+            if max_import_similarity > 0.0 || max_wildcard_import_similarity > 0.0 {
+                for declaration in scored_declarations_for_identifier.iter_mut() {
+                    if max_import_similarity > 0.0 {
+                        declaration.components.max_import_similarity = max_import_similarity;
+                        declaration.components.normalized_import_similarity =
+                            declaration.components.import_similarity / max_import_similarity;
+                    }
+                    if max_wildcard_import_similarity > 0.0 {
+                        declaration.components.normalized_wildcard_import_similarity =
+                            declaration.components.wildcard_import_similarity
+                                / max_wildcard_import_similarity;
+                    }
+                }
+            }
+
+            scored_declarations_for_identifier
         })
-        .flatten()
         .collect::<Vec<_>>();
 
     declarations.sort_unstable_by_key(|declaration| {
@@ -160,6 +323,24 @@ pub fn scored_declarations(
     declarations
 }
 
+struct CheckedDeclaration<'a> {
+    declaration: &'a Declaration,
+    same_file_line_distance: Option<u32>,
+    path_import_match_count: usize,
+    wildcard_path_import_match_count: usize,
+}
+
+fn declaration_path_matches_import(
+    declaration_path: &CachedDeclarationPath,
+    import_path: &Arc<Path>,
+) -> bool {
+    if import_path.is_absolute() {
+        declaration_path.equals_absolute_path(import_path)
+    } else {
+        declaration_path.ends_with_posix_path(import_path)
+    }
+}
+
 fn range_intersection<T: Ord + Clone>(a: &Range<T>, b: &Range<T>) -> Option<Range<T>> {
     let start = a.start.clone().max(b.start.clone());
     let end = a.end.clone().min(b.end.clone());
@@ -173,17 +354,23 @@ fn range_intersection<T: Ord + Clone>(a: &Range<T>, b: &Range<T>) -> Option<Rang
 fn score_declaration(
     identifier: &Identifier,
     references: &[Reference],
-    declaration: Declaration,
-    is_same_file: bool,
-    declaration_line_distance: u32,
-    declaration_line_distance_rank: usize,
+    checked_declaration: CheckedDeclaration,
     same_file_declaration_count: usize,
     declaration_count: usize,
     excerpt_occurrences: &Occurrences,
     adjacent_occurrences: &Occurrences,
+    import_occurrences: &[Occurrences],
+    wildcard_import_occurrences: &[Occurrences],
     cursor: Point,
     current_buffer: &BufferSnapshot,
-) -> Option<ScoredDeclaration> {
+) -> ScoredDeclaration {
+    let CheckedDeclaration {
+        declaration,
+        same_file_line_distance,
+        path_import_match_count,
+        wildcard_path_import_match_count,
+    } = checked_declaration;
+
     let is_referenced_nearby = references
         .iter()
         .any(|r| r.region == ReferenceRegion::Nearby);
@@ -200,6 +387,9 @@ fn score_declaration(
         .min()
         .unwrap();
 
+    let is_same_file = same_file_line_distance.is_some();
+    let declaration_line_distance = same_file_line_distance.unwrap_or(u32::MAX);
+
     let item_source_occurrences = Occurrences::within_string(&declaration.item_text().0);
     let item_signature_occurrences = Occurrences::within_string(&declaration.signature_text().0);
     let excerpt_vs_item_jaccard = jaccard_similarity(excerpt_occurrences, &item_source_occurrences);
@@ -219,6 +409,37 @@ fn score_declaration(
     let adjacent_vs_signature_weighted_overlap =
         weighted_overlap_coefficient(adjacent_occurrences, &item_signature_occurrences);
 
+    let mut import_similarity = 0f32;
+    let mut wildcard_import_similarity = 0f32;
+    if !import_occurrences.is_empty() || !wildcard_import_occurrences.is_empty() {
+        let cached_path = declaration.cached_path();
+        let path_occurrences = Occurrences::from_worktree_path(
+            cached_path
+                .worktree_abs_path
+                .file_name()
+                .map(|f| f.to_string_lossy()),
+            &cached_path.rel_path,
+        );
+        import_similarity = import_occurrences
+            .iter()
+            .map(|namespace_occurrences| {
+                OrderedFloat(jaccard_similarity(namespace_occurrences, &path_occurrences))
+            })
+            .max()
+            .map(|similarity| similarity.into_inner())
+            .unwrap_or_default();
+
+        // TODO: Consider something other than max
+        wildcard_import_similarity = wildcard_import_occurrences
+            .iter()
+            .map(|namespace_occurrences| {
+                OrderedFloat(jaccard_similarity(namespace_occurrences, &path_occurrences))
+            })
+            .max()
+            .map(|similarity| similarity.into_inner())
+            .unwrap_or_default();
+    }
+
     // TODO: Consider adding declaration_file_count
     let score_components = DeclarationScoreComponents {
         is_same_file,
@@ -226,7 +447,6 @@ fn score_declaration(
         is_referenced_in_breadcrumb,
         reference_line_distance,
         declaration_line_distance,
-        declaration_line_distance_rank,
         reference_count,
         same_file_declaration_count,
         declaration_count,
@@ -238,52 +458,59 @@ fn score_declaration(
         excerpt_vs_signature_weighted_overlap,
         adjacent_vs_item_weighted_overlap,
         adjacent_vs_signature_weighted_overlap,
+        path_import_match_count,
+        wildcard_path_import_match_count,
+        import_similarity,
+        max_import_similarity: 0.0,
+        normalized_import_similarity: 0.0,
+        wildcard_import_similarity,
+        normalized_wildcard_import_similarity: 0.0,
     };
 
-    Some(ScoredDeclaration {
+    ScoredDeclaration {
         identifier: identifier.clone(),
-        declaration: declaration,
-        scores: DeclarationScores::score(&score_components),
-        score_components,
-    })
+        declaration: declaration.clone(),
+        components: score_components,
+    }
 }
 
-#[derive(Clone, Debug, Serialize)]
-pub struct DeclarationScores {
-    pub signature: f32,
-    pub declaration: f32,
-    pub retrieval: f32,
-}
+#[cfg(test)]
+mod test {
+    use super::*;
 
-impl DeclarationScores {
-    fn score(components: &DeclarationScoreComponents) -> DeclarationScores {
-        // TODO: handle truncation
+    #[test]
+    fn test_declaration_path_matches() {
+        let declaration_path =
+            CachedDeclarationPath::new_for_test("/home/user/project", "src/maths.ts");
 
-        // Score related to how likely this is the correct declaration, range 0 to 1
-        let retrieval = if components.is_same_file {
-            // TODO: use declaration_line_distance_rank
-            1.0 / components.same_file_declaration_count as f32
-        } else {
-            1.0 / components.declaration_count as f32
-        };
+        assert!(declaration_path_matches_import(
+            &declaration_path,
+            &Path::new("maths.ts").into()
+        ));
 
-        // Score related to the distance between the reference and cursor, range 0 to 1
-        let distance_score = if components.is_referenced_nearby {
-            1.0 / (1.0 + components.reference_line_distance as f32 / 10.0).powf(2.0)
-        } else {
-            // same score as ~14 lines away, rationale is to not overly penalize references from parent signatures
-            0.5
-        };
+        assert!(declaration_path_matches_import(
+            &declaration_path,
+            &Path::new("project/src/maths.ts").into()
+        ));
 
-        // For now instead of linear combination, the scores are just multiplied together.
-        let combined_score = 10.0 * retrieval * distance_score;
+        assert!(declaration_path_matches_import(
+            &declaration_path,
+            &Path::new("user/project/src/maths.ts").into()
+        ));
 
-        DeclarationScores {
-            signature: combined_score * components.excerpt_vs_signature_weighted_overlap,
-            // declaration score gets boosted both by being multiplied by 2 and by there being more
-            // weighted overlap.
-            declaration: 2.0 * combined_score * components.excerpt_vs_item_weighted_overlap,
-            retrieval,
-        }
+        assert!(declaration_path_matches_import(
+            &declaration_path,
+            &Path::new("/home/user/project/src/maths.ts").into()
+        ));
+
+        assert!(!declaration_path_matches_import(
+            &declaration_path,
+            &Path::new("other.ts").into()
+        ));
+
+        assert!(!declaration_path_matches_import(
+            &declaration_path,
+            &Path::new("/home/user/project/src/other.ts").into()
+        ));
     }
 }

crates/edit_prediction_context/src/edit_prediction_context.rs 🔗

@@ -1,12 +1,13 @@
 mod declaration;
 mod declaration_scoring;
 mod excerpt;
+mod imports;
 mod outline;
 mod reference;
 mod syntax_index;
 pub mod text_similarity;
 
-use std::sync::Arc;
+use std::{path::Path, sync::Arc};
 
 use collections::HashMap;
 use gpui::{App, AppContext as _, Entity, Task};
@@ -16,9 +17,17 @@ use text::{Point, ToOffset as _};
 pub use declaration::*;
 pub use declaration_scoring::*;
 pub use excerpt::*;
+pub use imports::*;
 pub use reference::*;
 pub use syntax_index::*;
 
+#[derive(Clone, Debug, PartialEq)]
+pub struct EditPredictionContextOptions {
+    pub use_imports: bool,
+    pub excerpt: EditPredictionExcerptOptions,
+    pub score: EditPredictionScoreOptions,
+}
+
 #[derive(Clone, Debug)]
 pub struct EditPredictionContext {
     pub excerpt: EditPredictionExcerpt,
@@ -31,21 +40,34 @@ impl EditPredictionContext {
     pub fn gather_context_in_background(
         cursor_point: Point,
         buffer: BufferSnapshot,
-        excerpt_options: EditPredictionExcerptOptions,
+        options: EditPredictionContextOptions,
         syntax_index: Option<Entity<SyntaxIndex>>,
         cx: &mut App,
     ) -> Task<Option<Self>> {
+        let parent_abs_path = project::File::from_dyn(buffer.file()).and_then(|f| {
+            let mut path = f.worktree.read(cx).absolutize(&f.path);
+            if path.pop() { Some(path) } else { None }
+        });
+
         if let Some(syntax_index) = syntax_index {
             let index_state =
                 syntax_index.read_with(cx, |index, _cx| Arc::downgrade(index.state()));
             cx.background_spawn(async move {
+                let parent_abs_path = parent_abs_path.as_deref();
                 let index_state = index_state.upgrade()?;
                 let index_state = index_state.lock().await;
-                Self::gather_context(cursor_point, &buffer, &excerpt_options, Some(&index_state))
+                Self::gather_context(
+                    cursor_point,
+                    &buffer,
+                    parent_abs_path,
+                    &options,
+                    Some(&index_state),
+                )
             })
         } else {
             cx.background_spawn(async move {
-                Self::gather_context(cursor_point, &buffer, &excerpt_options, None)
+                let parent_abs_path = parent_abs_path.as_deref();
+                Self::gather_context(cursor_point, &buffer, parent_abs_path, &options, None)
             })
         }
     }
@@ -53,13 +75,20 @@ impl EditPredictionContext {
     pub fn gather_context(
         cursor_point: Point,
         buffer: &BufferSnapshot,
-        excerpt_options: &EditPredictionExcerptOptions,
+        parent_abs_path: Option<&Path>,
+        options: &EditPredictionContextOptions,
         index_state: Option<&SyntaxIndexState>,
     ) -> Option<Self> {
+        let imports = if options.use_imports {
+            Imports::gather(&buffer, parent_abs_path)
+        } else {
+            Imports::default()
+        };
         Self::gather_context_with_references_fn(
             cursor_point,
             buffer,
-            excerpt_options,
+            &imports,
+            options,
             index_state,
             references_in_excerpt,
         )
@@ -68,7 +97,8 @@ impl EditPredictionContext {
     pub fn gather_context_with_references_fn(
         cursor_point: Point,
         buffer: &BufferSnapshot,
-        excerpt_options: &EditPredictionExcerptOptions,
+        imports: &Imports,
+        options: &EditPredictionContextOptions,
         index_state: Option<&SyntaxIndexState>,
         get_references: impl FnOnce(
             &EditPredictionExcerpt,
@@ -79,7 +109,7 @@ impl EditPredictionContext {
         let excerpt = EditPredictionExcerpt::select_from_buffer(
             cursor_point,
             buffer,
-            excerpt_options,
+            &options.excerpt,
             index_state,
         )?;
         let excerpt_text = excerpt.text(buffer);
@@ -101,10 +131,12 @@ impl EditPredictionContext {
             let references = get_references(&excerpt, &excerpt_text, buffer);
 
             scored_declarations(
+                &options.score,
                 &index_state,
                 &excerpt,
                 &excerpt_occurrences,
                 &adjacent_occurrences,
+                &imports,
                 references,
                 cursor_offset_in_file,
                 buffer,
@@ -160,12 +192,18 @@ mod tests {
                 EditPredictionContext::gather_context_in_background(
                     cursor_point,
                     buffer_snapshot,
-                    EditPredictionExcerptOptions {
-                        max_bytes: 60,
-                        min_bytes: 10,
-                        target_before_cursor_over_total_bytes: 0.5,
+                    EditPredictionContextOptions {
+                        use_imports: true,
+                        excerpt: EditPredictionExcerptOptions {
+                            max_bytes: 60,
+                            min_bytes: 10,
+                            target_before_cursor_over_total_bytes: 0.5,
+                        },
+                        score: EditPredictionScoreOptions {
+                            omit_excerpt_overlaps: true,
+                        },
                     },
-                    Some(index),
+                    Some(index.clone()),
                     cx,
                 )
             })

crates/edit_prediction_context/src/imports.rs 🔗

@@ -0,0 +1,1319 @@
+use collections::HashMap;
+use language::BufferSnapshot;
+use language::ImportsConfig;
+use language::Language;
+use std::ops::Deref;
+use std::path::Path;
+use std::sync::Arc;
+use std::{borrow::Cow, ops::Range};
+use text::OffsetRangeExt as _;
+use util::RangeExt;
+use util::paths::PathStyle;
+
+use crate::Identifier;
+use crate::text_similarity::Occurrences;
+
+// TODO: Write documentation for extension authors. The @import capture must match before or in the
+// same pattern as all all captures it contains
+
+// Future improvements to consider:
+//
+// * Distinguish absolute vs relative paths in captures. `#include "maths.h"` is relative whereas
+// `#include <maths.h>` is not.
+//
+// * Provide the name used when importing whole modules (see tests with "named_module" in the name).
+// To be useful, will require parsing of identifier qualification.
+//
+// * Scoping for imports that aren't at the top level
+//
+// * Only scan a prefix of the file, when possible. This could look like having query matches that
+// indicate it reached a declaration that is not allowed in the import section.
+//
+// * Support directly parsing to occurrences instead of storing namespaces / paths. Types should be
+// generic on this, so that tests etc can still use strings. Could do similar in syntax index.
+//
+// * Distinguish different types of namespaces when known. E.g. "name.type" capture. Once capture
+// names are more open-ended like this may make sense to build and cache a jump table (direct
+// dispatch from capture index).
+//
+// * There are a few "Language specific:" comments on behavior that gets applied to all languages.
+// Would be cleaner to be conditional on the language or otherwise configured.
+
+#[derive(Debug, Clone, Default)]
+pub struct Imports {
+    pub identifier_to_imports: HashMap<Identifier, Vec<Import>>,
+    pub wildcard_modules: Vec<Module>,
+}
+
+#[derive(Debug, Clone)]
+pub enum Import {
+    Direct {
+        module: Module,
+    },
+    Alias {
+        module: Module,
+        external_identifier: Identifier,
+    },
+}
+
+#[derive(Debug, Clone)]
+pub enum Module {
+    SourceExact(Arc<Path>),
+    SourceFuzzy(Arc<Path>),
+    Namespace(Namespace),
+}
+
+impl Module {
+    fn empty() -> Self {
+        Module::Namespace(Namespace::default())
+    }
+
+    fn push_range(
+        &mut self,
+        range: &ModuleRange,
+        snapshot: &BufferSnapshot,
+        language: &Language,
+        parent_abs_path: Option<&Path>,
+    ) -> usize {
+        if range.is_empty() {
+            return 0;
+        }
+
+        match range {
+            ModuleRange::Source(range) => {
+                if let Self::Namespace(namespace) = self
+                    && namespace.0.is_empty()
+                {
+                    let path = snapshot.text_for_range(range.clone()).collect::<Cow<str>>();
+
+                    let path = if let Some(strip_regex) =
+                        language.config().import_path_strip_regex.as_ref()
+                    {
+                        strip_regex.replace_all(&path, "")
+                    } else {
+                        path
+                    };
+
+                    let path = Path::new(path.as_ref());
+                    if (path.starts_with(".") || path.starts_with(".."))
+                        && let Some(parent_abs_path) = parent_abs_path
+                        && let Ok(abs_path) =
+                            util::paths::normalize_lexically(&parent_abs_path.join(path))
+                    {
+                        *self = Self::SourceExact(abs_path.into());
+                    } else {
+                        *self = Self::SourceFuzzy(path.into());
+                    };
+                } else if matches!(self, Self::SourceExact(_))
+                    || matches!(self, Self::SourceFuzzy(_))
+                {
+                    log::warn!("bug in imports query: encountered multiple @source matches");
+                } else {
+                    log::warn!(
+                        "bug in imports query: encountered both @namespace and @source match"
+                    );
+                }
+            }
+            ModuleRange::Namespace(range) => {
+                if let Self::Namespace(namespace) = self {
+                    let segment = range_text(snapshot, range);
+                    if language.config().ignored_import_segments.contains(&segment) {
+                        return 0;
+                    } else {
+                        namespace.0.push(segment);
+                        return 1;
+                    }
+                } else {
+                    log::warn!(
+                        "bug in imports query: encountered both @namespace and @source match"
+                    );
+                }
+            }
+        }
+        0
+    }
+}
+
+#[derive(Debug, Clone)]
+enum ModuleRange {
+    Source(Range<usize>),
+    Namespace(Range<usize>),
+}
+
+impl Deref for ModuleRange {
+    type Target = Range<usize>;
+
+    fn deref(&self) -> &Self::Target {
+        match self {
+            ModuleRange::Source(range) => range,
+            ModuleRange::Namespace(range) => range,
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Default)]
+pub struct Namespace(pub Vec<Arc<str>>);
+
+impl Namespace {
+    pub fn occurrences(&self) -> Occurrences {
+        Occurrences::from_identifiers(&self.0)
+    }
+}
+
+impl Imports {
+    pub fn gather(snapshot: &BufferSnapshot, parent_abs_path: Option<&Path>) -> Self {
+        // Query to match different import patterns
+        let mut matches = snapshot
+            .syntax
+            .matches(0..snapshot.len(), &snapshot.text, |grammar| {
+                grammar.imports_config().map(|imports| &imports.query)
+            });
+
+        let mut detached_nodes: Vec<DetachedNode> = Vec::new();
+        let mut identifier_to_imports = HashMap::default();
+        let mut wildcard_modules = Vec::new();
+        let mut import_range = None;
+
+        while let Some(query_match) = matches.peek() {
+            let ImportsConfig {
+                query: _,
+                import_ix,
+                name_ix,
+                namespace_ix,
+                source_ix,
+                list_ix,
+                wildcard_ix,
+                alias_ix,
+            } = matches.grammars()[query_match.grammar_index]
+                .imports_config()
+                .unwrap();
+
+            let mut new_import_range = None;
+            let mut alias_range = None;
+            let mut modules = Vec::new();
+            let mut content: Option<(Range<usize>, ContentKind)> = None;
+            for capture in query_match.captures {
+                let capture_range = capture.node.byte_range();
+
+                if capture.index == *import_ix {
+                    new_import_range = Some(capture_range);
+                } else if Some(capture.index) == *namespace_ix {
+                    modules.push(ModuleRange::Namespace(capture_range));
+                } else if Some(capture.index) == *source_ix {
+                    modules.push(ModuleRange::Source(capture_range));
+                } else if Some(capture.index) == *alias_ix {
+                    alias_range = Some(capture_range);
+                } else {
+                    let mut found_content = None;
+                    if Some(capture.index) == *name_ix {
+                        found_content = Some((capture_range, ContentKind::Name));
+                    } else if Some(capture.index) == *list_ix {
+                        found_content = Some((capture_range, ContentKind::List));
+                    } else if Some(capture.index) == *wildcard_ix {
+                        found_content = Some((capture_range, ContentKind::Wildcard));
+                    }
+                    if let Some((found_content_range, found_kind)) = found_content {
+                        if let Some((_, old_kind)) = content {
+                            let point = found_content_range.to_point(snapshot);
+                            log::warn!(
+                                "bug in {} imports query: unexpected multiple captures of {} and {} ({}:{}:{})",
+                                query_match.language.name(),
+                                old_kind.capture_name(),
+                                found_kind.capture_name(),
+                                snapshot
+                                    .file()
+                                    .map(|p| p.path().display(PathStyle::Posix))
+                                    .unwrap_or_default(),
+                                point.start.row + 1,
+                                point.start.column + 1
+                            );
+                        }
+                        content = Some((found_content_range, found_kind));
+                    }
+                }
+            }
+
+            if let Some(new_import_range) = new_import_range {
+                log::trace!("starting new import {:?}", new_import_range);
+                Self::gather_from_import_statement(
+                    &detached_nodes,
+                    &snapshot,
+                    parent_abs_path,
+                    &mut identifier_to_imports,
+                    &mut wildcard_modules,
+                );
+                detached_nodes.clear();
+                import_range = Some(new_import_range.clone());
+            }
+
+            if let Some((content, content_kind)) = content {
+                if import_range
+                    .as_ref()
+                    .is_some_and(|import_range| import_range.contains_inclusive(&content))
+                {
+                    detached_nodes.push(DetachedNode {
+                        modules,
+                        content: content.clone(),
+                        content_kind,
+                        alias: alias_range.unwrap_or(0..0),
+                        language: query_match.language.clone(),
+                    });
+                } else {
+                    log::trace!(
+                        "filtered out match not inside import range: {content_kind:?} at {content:?}"
+                    );
+                }
+            }
+
+            matches.advance();
+        }
+
+        Self::gather_from_import_statement(
+            &detached_nodes,
+            &snapshot,
+            parent_abs_path,
+            &mut identifier_to_imports,
+            &mut wildcard_modules,
+        );
+
+        Imports {
+            identifier_to_imports,
+            wildcard_modules,
+        }
+    }
+
+    fn gather_from_import_statement(
+        detached_nodes: &[DetachedNode],
+        snapshot: &BufferSnapshot,
+        parent_abs_path: Option<&Path>,
+        identifier_to_imports: &mut HashMap<Identifier, Vec<Import>>,
+        wildcard_modules: &mut Vec<Module>,
+    ) {
+        let mut trees = Vec::new();
+
+        for detached_node in detached_nodes {
+            if let Some(node) = Self::attach_node(detached_node.into(), &mut trees) {
+                trees.push(node);
+            }
+            log::trace!(
+                "Attached node to tree\n{:#?}\nAttach result:\n{:#?}",
+                detached_node,
+                trees
+                    .iter()
+                    .map(|tree| tree.debug(snapshot))
+                    .collect::<Vec<_>>()
+            );
+        }
+
+        for tree in &trees {
+            let mut module = Module::empty();
+            Self::gather_from_tree(
+                tree,
+                snapshot,
+                parent_abs_path,
+                &mut module,
+                identifier_to_imports,
+                wildcard_modules,
+            );
+        }
+    }
+
+    fn attach_node(mut node: ImportTree, trees: &mut Vec<ImportTree>) -> Option<ImportTree> {
+        let mut tree_index = 0;
+        while tree_index < trees.len() {
+            let tree = &mut trees[tree_index];
+            if !node.content.is_empty() && node.content == tree.content {
+                // multiple matches can apply to the same name/list/wildcard. This keeps the queries
+                // simpler by combining info from these matches.
+                if tree.module.is_empty() {
+                    tree.module = node.module;
+                    tree.module_children = node.module_children;
+                }
+                if tree.alias.is_empty() {
+                    tree.alias = node.alias;
+                }
+                return None;
+            } else if !node.module.is_empty() && node.module.contains_inclusive(&tree.range()) {
+                node.module_children.push(trees.remove(tree_index));
+                continue;
+            } else if !node.content.is_empty() && node.content.contains_inclusive(&tree.content) {
+                node.content_children.push(trees.remove(tree_index));
+                continue;
+            } else if !tree.content.is_empty() && tree.content.contains_inclusive(&node.content) {
+                if let Some(node) = Self::attach_node(node, &mut tree.content_children) {
+                    tree.content_children.push(node);
+                }
+                return None;
+            }
+            tree_index += 1;
+        }
+        Some(node)
+    }
+
+    fn gather_from_tree(
+        tree: &ImportTree,
+        snapshot: &BufferSnapshot,
+        parent_abs_path: Option<&Path>,
+        current_module: &mut Module,
+        identifier_to_imports: &mut HashMap<Identifier, Vec<Import>>,
+        wildcard_modules: &mut Vec<Module>,
+    ) {
+        let mut pop_count = 0;
+
+        if tree.module_children.is_empty() {
+            pop_count +=
+                current_module.push_range(&tree.module, snapshot, &tree.language, parent_abs_path);
+        } else {
+            for child in &tree.module_children {
+                pop_count += Self::extend_namespace_from_tree(
+                    child,
+                    snapshot,
+                    parent_abs_path,
+                    current_module,
+                );
+            }
+        };
+
+        if tree.content_children.is_empty() && !tree.content.is_empty() {
+            match tree.content_kind {
+                ContentKind::Name | ContentKind::List => {
+                    if tree.alias.is_empty() {
+                        identifier_to_imports
+                            .entry(Identifier {
+                                language_id: tree.language.id(),
+                                name: range_text(snapshot, &tree.content),
+                            })
+                            .or_default()
+                            .push(Import::Direct {
+                                module: current_module.clone(),
+                            });
+                    } else {
+                        let alias_name: Arc<str> = range_text(snapshot, &tree.alias);
+                        let external_name = range_text(snapshot, &tree.content);
+                        // Language specific: skip "_" aliases for Rust
+                        if alias_name.as_ref() != "_" {
+                            identifier_to_imports
+                                .entry(Identifier {
+                                    language_id: tree.language.id(),
+                                    name: alias_name,
+                                })
+                                .or_default()
+                                .push(Import::Alias {
+                                    module: current_module.clone(),
+                                    external_identifier: Identifier {
+                                        language_id: tree.language.id(),
+                                        name: external_name,
+                                    },
+                                });
+                        }
+                    }
+                }
+                ContentKind::Wildcard => wildcard_modules.push(current_module.clone()),
+            }
+        } else {
+            for child in &tree.content_children {
+                Self::gather_from_tree(
+                    child,
+                    snapshot,
+                    parent_abs_path,
+                    current_module,
+                    identifier_to_imports,
+                    wildcard_modules,
+                );
+            }
+        }
+
+        if pop_count > 0 {
+            match current_module {
+                Module::SourceExact(_) | Module::SourceFuzzy(_) => {
+                    log::warn!(
+                        "bug in imports query: encountered both @namespace and @source match"
+                    );
+                }
+                Module::Namespace(namespace) => {
+                    namespace.0.drain(namespace.0.len() - pop_count..);
+                }
+            }
+        }
+    }
+
+    fn extend_namespace_from_tree(
+        tree: &ImportTree,
+        snapshot: &BufferSnapshot,
+        parent_abs_path: Option<&Path>,
+        module: &mut Module,
+    ) -> usize {
+        let mut pop_count = 0;
+        if tree.module_children.is_empty() {
+            pop_count += module.push_range(&tree.module, snapshot, &tree.language, parent_abs_path);
+        } else {
+            for child in &tree.module_children {
+                pop_count +=
+                    Self::extend_namespace_from_tree(child, snapshot, parent_abs_path, module);
+            }
+        }
+        if tree.content_children.is_empty() {
+            pop_count += module.push_range(
+                &ModuleRange::Namespace(tree.content.clone()),
+                snapshot,
+                &tree.language,
+                parent_abs_path,
+            );
+        } else {
+            for child in &tree.content_children {
+                pop_count +=
+                    Self::extend_namespace_from_tree(child, snapshot, parent_abs_path, module);
+            }
+        }
+        pop_count
+    }
+}
+
+fn range_text(snapshot: &BufferSnapshot, range: &Range<usize>) -> Arc<str> {
+    snapshot
+        .text_for_range(range.clone())
+        .collect::<Cow<str>>()
+        .into()
+}
+
+#[derive(Debug)]
+struct DetachedNode {
+    modules: Vec<ModuleRange>,
+    content: Range<usize>,
+    content_kind: ContentKind,
+    alias: Range<usize>,
+    language: Arc<Language>,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum ContentKind {
+    Name,
+    Wildcard,
+    List,
+}
+
+impl ContentKind {
+    fn capture_name(&self) -> &'static str {
+        match self {
+            ContentKind::Name => "name",
+            ContentKind::Wildcard => "wildcard",
+            ContentKind::List => "list",
+        }
+    }
+}
+
+#[derive(Debug)]
+struct ImportTree {
+    module: ModuleRange,
+    /// When non-empty, provides namespace / source info which should be used instead of `module`.
+    module_children: Vec<ImportTree>,
+    content: Range<usize>,
+    /// When non-empty, provides content which should be used instead of `content`.
+    content_children: Vec<ImportTree>,
+    content_kind: ContentKind,
+    alias: Range<usize>,
+    language: Arc<Language>,
+}
+
+impl ImportTree {
+    fn range(&self) -> Range<usize> {
+        self.module.start.min(self.content.start)..self.module.end.max(self.content.end)
+    }
+
+    #[allow(dead_code)]
+    fn debug<'a>(&'a self, snapshot: &'a BufferSnapshot) -> ImportTreeDebug<'a> {
+        ImportTreeDebug {
+            tree: self,
+            snapshot,
+        }
+    }
+
+    fn from_module_range(module: &ModuleRange, language: Arc<Language>) -> Self {
+        ImportTree {
+            module: module.clone(),
+            module_children: Vec::new(),
+            content: 0..0,
+            content_children: Vec::new(),
+            content_kind: ContentKind::Name,
+            alias: 0..0,
+            language,
+        }
+    }
+}
+
+impl From<&DetachedNode> for ImportTree {
+    fn from(value: &DetachedNode) -> Self {
+        let module;
+        let module_children;
+        match value.modules.len() {
+            0 => {
+                module = ModuleRange::Namespace(0..0);
+                module_children = Vec::new();
+            }
+            1 => {
+                module = value.modules[0].clone();
+                module_children = Vec::new();
+            }
+            _ => {
+                module = ModuleRange::Namespace(
+                    value.modules.first().unwrap().start..value.modules.last().unwrap().end,
+                );
+                module_children = value
+                    .modules
+                    .iter()
+                    .map(|module| ImportTree::from_module_range(module, value.language.clone()))
+                    .collect();
+            }
+        }
+
+        ImportTree {
+            module,
+            module_children,
+            content: value.content.clone(),
+            content_children: Vec::new(),
+            content_kind: value.content_kind,
+            alias: value.alias.clone(),
+            language: value.language.clone(),
+        }
+    }
+}
+
+struct ImportTreeDebug<'a> {
+    tree: &'a ImportTree,
+    snapshot: &'a BufferSnapshot,
+}
+
+impl std::fmt::Debug for ImportTreeDebug<'_> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ImportTree")
+            .field("module_range", &self.tree.module)
+            .field("module_text", &range_text(self.snapshot, &self.tree.module))
+            .field(
+                "module_children",
+                &self
+                    .tree
+                    .module_children
+                    .iter()
+                    .map(|child| child.debug(&self.snapshot))
+                    .collect::<Vec<Self>>(),
+            )
+            .field("content_range", &self.tree.content)
+            .field(
+                "content_text",
+                &range_text(self.snapshot, &self.tree.content),
+            )
+            .field(
+                "content_children",
+                &self
+                    .tree
+                    .content_children
+                    .iter()
+                    .map(|child| child.debug(&self.snapshot))
+                    .collect::<Vec<Self>>(),
+            )
+            .field("content_kind", &self.tree.content_kind)
+            .field("alias_range", &self.tree.alias)
+            .field("alias_text", &range_text(self.snapshot, &self.tree.alias))
+            .finish()
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use std::path::PathBuf;
+    use std::sync::{Arc, LazyLock};
+
+    use super::*;
+    use collections::HashSet;
+    use gpui::{TestAppContext, prelude::*};
+    use indoc::indoc;
+    use language::{
+        Buffer, Language, LanguageConfig, tree_sitter_python, tree_sitter_rust,
+        tree_sitter_typescript,
+    };
+    use regex::Regex;
+
+    #[gpui::test]
+    fn test_rust_simple(cx: &mut TestAppContext) {
+        check_imports(
+            &RUST,
+            "use std::collections::HashMap;",
+            &[&["std", "collections", "HashMap"]],
+            cx,
+        );
+
+        check_imports(
+            &RUST,
+            "pub use std::collections::HashMap;",
+            &[&["std", "collections", "HashMap"]],
+            cx,
+        );
+
+        check_imports(
+            &RUST,
+            "use std::collections::{HashMap, HashSet};",
+            &[
+                &["std", "collections", "HashMap"],
+                &["std", "collections", "HashSet"],
+            ],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_rust_nested(cx: &mut TestAppContext) {
+        check_imports(
+            &RUST,
+            "use std::{any::TypeId, collections::{HashMap, HashSet}};",
+            &[
+                &["std", "any", "TypeId"],
+                &["std", "collections", "HashMap"],
+                &["std", "collections", "HashSet"],
+            ],
+            cx,
+        );
+
+        check_imports(
+            &RUST,
+            "use a::b::c::{d::e::F, g::h::I};",
+            &[
+                &["a", "b", "c", "d", "e", "F"],
+                &["a", "b", "c", "g", "h", "I"],
+            ],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_rust_multiple_imports(cx: &mut TestAppContext) {
+        check_imports(
+            &RUST,
+            indoc! {"
+                use std::collections::HashMap;
+                use std::any::{TypeId, Any};
+            "},
+            &[
+                &["std", "collections", "HashMap"],
+                &["std", "any", "TypeId"],
+                &["std", "any", "Any"],
+            ],
+            cx,
+        );
+
+        check_imports(
+            &RUST,
+            indoc! {"
+                use std::collections::HashSet;
+
+                fn main() {
+                    let unqualified = HashSet::new();
+                    let qualified = std::collections::HashMap::new();
+                }
+
+                use std::any::TypeId;
+            "},
+            &[
+                &["std", "collections", "HashSet"],
+                &["std", "any", "TypeId"],
+            ],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_rust_wildcard(cx: &mut TestAppContext) {
+        check_imports(&RUST, "use prelude::*;", &[&["prelude", "WILDCARD"]], cx);
+
+        check_imports(
+            &RUST,
+            "use zed::prelude::*;",
+            &[&["zed", "prelude", "WILDCARD"]],
+            cx,
+        );
+
+        check_imports(&RUST, "use prelude::{*};", &[&["prelude", "WILDCARD"]], cx);
+
+        check_imports(
+            &RUST,
+            "use prelude::{File, *};",
+            &[&["prelude", "File"], &["prelude", "WILDCARD"]],
+            cx,
+        );
+
+        check_imports(
+            &RUST,
+            "use zed::{App, prelude::*};",
+            &[&["zed", "App"], &["zed", "prelude", "WILDCARD"]],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_rust_alias(cx: &mut TestAppContext) {
+        check_imports(
+            &RUST,
+            "use std::io::Result as IoResult;",
+            &[&["std", "io", "Result AS IoResult"]],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_rust_crate_and_super(cx: &mut TestAppContext) {
+        check_imports(&RUST, "use crate::a::b::c;", &[&["a", "b", "c"]], cx);
+        check_imports(&RUST, "use super::a::b::c;", &[&["a", "b", "c"]], cx);
+        // TODO: Consider stripping leading "::". Not done for now because for the text similarity matching usecase this
+        // is fine.
+        check_imports(&RUST, "use ::a::b::c;", &[&["::a", "b", "c"]], cx);
+    }
+
+    #[gpui::test]
+    fn test_typescript_imports(cx: &mut TestAppContext) {
+        let parent_abs_path = PathBuf::from("/home/user/project");
+
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &TYPESCRIPT,
+            r#"import "./maths.js";"#,
+            &[&["SOURCE /home/user/project/maths", "WILDCARD"]],
+            cx,
+        );
+
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &TYPESCRIPT,
+            r#"import "../maths.js";"#,
+            &[&["SOURCE /home/user/maths", "WILDCARD"]],
+            cx,
+        );
+
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &TYPESCRIPT,
+            r#"import RandomNumberGenerator, { pi as π } from "./maths.js";"#,
+            &[
+                &["SOURCE /home/user/project/maths", "RandomNumberGenerator"],
+                &["SOURCE /home/user/project/maths", "pi AS π"],
+            ],
+            cx,
+        );
+
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &TYPESCRIPT,
+            r#"import { pi, phi, absolute } from "./maths.js";"#,
+            &[
+                &["SOURCE /home/user/project/maths", "pi"],
+                &["SOURCE /home/user/project/maths", "phi"],
+                &["SOURCE /home/user/project/maths", "absolute"],
+            ],
+            cx,
+        );
+
+        // index.js is removed by import_path_strip_regex
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &TYPESCRIPT,
+            r#"import { pi, phi, absolute } from "./maths/index.js";"#,
+            &[
+                &["SOURCE /home/user/project/maths", "pi"],
+                &["SOURCE /home/user/project/maths", "phi"],
+                &["SOURCE /home/user/project/maths", "absolute"],
+            ],
+            cx,
+        );
+
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &TYPESCRIPT,
+            r#"import type { SomeThing } from "./some-module.js";"#,
+            &[&["SOURCE /home/user/project/some-module", "SomeThing"]],
+            cx,
+        );
+
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &TYPESCRIPT,
+            r#"import { type SomeThing, OtherThing } from "./some-module.js";"#,
+            &[
+                &["SOURCE /home/user/project/some-module", "SomeThing"],
+                &["SOURCE /home/user/project/some-module", "OtherThing"],
+            ],
+            cx,
+        );
+
+        // index.js is removed by import_path_strip_regex
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &TYPESCRIPT,
+            r#"import { type SomeThing, OtherThing } from "./some-module/index.js";"#,
+            &[
+                &["SOURCE /home/user/project/some-module", "SomeThing"],
+                &["SOURCE /home/user/project/some-module", "OtherThing"],
+            ],
+            cx,
+        );
+
+        // fuzzy paths
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &TYPESCRIPT,
+            r#"import { type SomeThing, OtherThing } from "@my-app/some-module.js";"#,
+            &[
+                &["SOURCE FUZZY @my-app/some-module", "SomeThing"],
+                &["SOURCE FUZZY @my-app/some-module", "OtherThing"],
+            ],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_typescript_named_module_imports(cx: &mut TestAppContext) {
+        let parent_abs_path = PathBuf::from("/home/user/project");
+
+        // TODO: These should provide the name that the module is bound to.
+        // For now instead these are treated as unqualified wildcard imports.
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &TYPESCRIPT,
+            r#"import * as math from "./maths.js";"#,
+            // &[&["/home/user/project/maths.js", "WILDCARD AS math"]],
+            &[&["SOURCE /home/user/project/maths", "WILDCARD"]],
+            cx,
+        );
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &TYPESCRIPT,
+            r#"import math = require("./maths");"#,
+            // &[&["/home/user/project/maths", "WILDCARD AS math"]],
+            &[&["SOURCE /home/user/project/maths", "WILDCARD"]],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_python_imports(cx: &mut TestAppContext) {
+        check_imports(&PYTHON, "from math import pi", &[&["math", "pi"]], cx);
+
+        check_imports(
+            &PYTHON,
+            "from math import pi, sin, cos",
+            &[&["math", "pi"], &["math", "sin"], &["math", "cos"]],
+            cx,
+        );
+
+        check_imports(&PYTHON, "from math import *", &[&["math", "WILDCARD"]], cx);
+
+        check_imports(
+            &PYTHON,
+            "from math import foo.bar.baz",
+            &[&["math", "foo", "bar", "baz"]],
+            cx,
+        );
+
+        check_imports(
+            &PYTHON,
+            "from math import pi as PI",
+            &[&["math", "pi AS PI"]],
+            cx,
+        );
+
+        check_imports(
+            &PYTHON,
+            "from serializers.json import JsonSerializer",
+            &[&["serializers", "json", "JsonSerializer"]],
+            cx,
+        );
+
+        check_imports(
+            &PYTHON,
+            "from custom.serializers import json, xml, yaml",
+            &[
+                &["custom", "serializers", "json"],
+                &["custom", "serializers", "xml"],
+                &["custom", "serializers", "yaml"],
+            ],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_python_named_module_imports(cx: &mut TestAppContext) {
+        // TODO: These should provide the name that the module is bound to.
+        // For now instead these are treated as unqualified wildcard imports.
+        //
+        // check_imports(&PYTHON, "import math", &[&["math", "WILDCARD as math"]], cx);
+        // check_imports(&PYTHON, "import math as maths", &[&["math", "WILDCARD AS maths"]], cx);
+        //
+        // Something like:
+        //
+        // (import_statement
+        //     name: [
+        //         (dotted_name
+        //             (identifier)* @namespace
+        //             (identifier) @name.module .)
+        //         (aliased_import
+        //             name: (dotted_name
+        //                 ((identifier) ".")* @namespace
+        //                 (identifier) @name.module .)
+        //             alias: (identifier) @alias)
+        //     ]) @import
+
+        check_imports(&PYTHON, "import math", &[&["math", "WILDCARD"]], cx);
+
+        check_imports(
+            &PYTHON,
+            "import math as maths",
+            &[&["math", "WILDCARD"]],
+            cx,
+        );
+
+        check_imports(&PYTHON, "import a.b.c", &[&["a", "b", "c", "WILDCARD"]], cx);
+
+        check_imports(
+            &PYTHON,
+            "import a.b.c as d",
+            &[&["a", "b", "c", "WILDCARD"]],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_python_package_relative_imports(cx: &mut TestAppContext) {
+        // TODO: These should provide info about the dir they are relative to, to provide more
+        // precise resolution. Instead, fuzzy matching is used as usual.
+
+        check_imports(&PYTHON, "from . import math", &[&["math"]], cx);
+
+        check_imports(&PYTHON, "from .a import math", &[&["a", "math"]], cx);
+
+        check_imports(
+            &PYTHON,
+            "from ..a.b import math",
+            &[&["a", "b", "math"]],
+            cx,
+        );
+
+        check_imports(
+            &PYTHON,
+            "from ..a.b import *",
+            &[&["a", "b", "WILDCARD"]],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_c_imports(cx: &mut TestAppContext) {
+        let parent_abs_path = PathBuf::from("/home/user/project");
+
+        // TODO: Distinguish that these are not relative to current path
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &C,
+            r#"#include <math.h>"#,
+            &[&["SOURCE FUZZY math.h", "WILDCARD"]],
+            cx,
+        );
+
+        // TODO: These should be treated as relative, but don't start with ./ or ../
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &C,
+            r#"#include "math.h""#,
+            &[&["SOURCE FUZZY math.h", "WILDCARD"]],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_cpp_imports(cx: &mut TestAppContext) {
+        let parent_abs_path = PathBuf::from("/home/user/project");
+
+        // TODO: Distinguish that these are not relative to current path
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &CPP,
+            r#"#include <math.h>"#,
+            &[&["SOURCE FUZZY math.h", "WILDCARD"]],
+            cx,
+        );
+
+        // TODO: These should be treated as relative, but don't start with ./ or ../
+        check_imports_with_file_abs_path(
+            Some(&parent_abs_path),
+            &CPP,
+            r#"#include "math.h""#,
+            &[&["SOURCE FUZZY math.h", "WILDCARD"]],
+            cx,
+        );
+    }
+
+    #[gpui::test]
+    fn test_go_imports(cx: &mut TestAppContext) {
+        check_imports(
+            &GO,
+            r#"import . "lib/math""#,
+            &[&["lib/math", "WILDCARD"]],
+            cx,
+        );
+
+        // not included, these are only for side-effects
+        check_imports(&GO, r#"import _ "lib/math""#, &[], cx);
+    }
+
+    #[gpui::test]
+    fn test_go_named_module_imports(cx: &mut TestAppContext) {
+        // TODO: These should provide the name that the module is bound to.
+        // For now instead these are treated as unqualified wildcard imports.
+
+        check_imports(
+            &GO,
+            r#"import "lib/math""#,
+            &[&["lib/math", "WILDCARD"]],
+            cx,
+        );
+        check_imports(
+            &GO,
+            r#"import m "lib/math""#,
+            &[&["lib/math", "WILDCARD"]],
+            cx,
+        );
+    }
+
+    #[track_caller]
+    fn check_imports(
+        language: &Arc<Language>,
+        source: &str,
+        expected: &[&[&str]],
+        cx: &mut TestAppContext,
+    ) {
+        check_imports_with_file_abs_path(None, language, source, expected, cx);
+    }
+
+    #[track_caller]
+    fn check_imports_with_file_abs_path(
+        parent_abs_path: Option<&Path>,
+        language: &Arc<Language>,
+        source: &str,
+        expected: &[&[&str]],
+        cx: &mut TestAppContext,
+    ) {
+        let buffer = cx.new(|cx| {
+            let mut buffer = Buffer::local(source, cx);
+            buffer.set_language(Some(language.clone()), cx);
+            buffer
+        });
+        cx.run_until_parked();
+
+        let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
+
+        let imports = Imports::gather(&snapshot, parent_abs_path);
+        let mut actual_symbols = imports
+            .identifier_to_imports
+            .iter()
+            .flat_map(|(identifier, imports)| {
+                imports
+                    .iter()
+                    .map(|import| import.to_identifier_parts(identifier.name.as_ref()))
+            })
+            .chain(
+                imports
+                    .wildcard_modules
+                    .iter()
+                    .map(|module| module.to_identifier_parts("WILDCARD")),
+            )
+            .collect::<Vec<_>>();
+        let mut expected_symbols = expected
+            .iter()
+            .map(|expected| expected.iter().map(|s| s.to_string()).collect::<Vec<_>>())
+            .collect::<Vec<_>>();
+        actual_symbols.sort();
+        expected_symbols.sort();
+        if actual_symbols != expected_symbols {
+            let top_layer = snapshot.syntax_layers().next().unwrap();
+            panic!(
+                "Expected imports: {:?}\n\
+                Actual imports: {:?}\n\
+                Tree:\n{}",
+                expected_symbols,
+                actual_symbols,
+                tree_to_string(&top_layer.node()),
+            );
+        }
+    }
+
+    fn tree_to_string(node: &tree_sitter::Node) -> String {
+        let mut cursor = node.walk();
+        let mut result = String::new();
+        let mut depth = 0;
+        'outer: loop {
+            result.push_str(&"  ".repeat(depth));
+            if let Some(field_name) = cursor.field_name() {
+                result.push_str(field_name);
+                result.push_str(": ");
+            }
+            if cursor.node().is_named() {
+                result.push_str(cursor.node().kind());
+            } else {
+                result.push('"');
+                result.push_str(cursor.node().kind());
+                result.push('"');
+            }
+            result.push('\n');
+
+            if cursor.goto_first_child() {
+                depth += 1;
+                continue;
+            }
+            if cursor.goto_next_sibling() {
+                continue;
+            }
+            while cursor.goto_parent() {
+                depth -= 1;
+                if cursor.goto_next_sibling() {
+                    continue 'outer;
+                }
+            }
+            break;
+        }
+        result
+    }
+
+    static RUST: LazyLock<Arc<Language>> = LazyLock::new(|| {
+        Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Rust".into(),
+                    ignored_import_segments: HashSet::from_iter(["crate".into(), "super".into()]),
+                    import_path_strip_regex: Some(Regex::new("/(lib|mod)\\.rs$").unwrap()),
+                    ..Default::default()
+                },
+                Some(tree_sitter_rust::LANGUAGE.into()),
+            )
+            .with_imports_query(include_str!("../../languages/src/rust/imports.scm"))
+            .unwrap(),
+        )
+    });
+
+    static TYPESCRIPT: LazyLock<Arc<Language>> = LazyLock::new(|| {
+        Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "TypeScript".into(),
+                    import_path_strip_regex: Some(Regex::new("(?:/index)?\\.[jt]s$").unwrap()),
+                    ..Default::default()
+                },
+                Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
+            )
+            .with_imports_query(include_str!("../../languages/src/typescript/imports.scm"))
+            .unwrap(),
+        )
+    });
+
+    static PYTHON: LazyLock<Arc<Language>> = LazyLock::new(|| {
+        Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Python".into(),
+                    import_path_strip_regex: Some(Regex::new("/__init__\\.py$").unwrap()),
+                    ..Default::default()
+                },
+                Some(tree_sitter_python::LANGUAGE.into()),
+            )
+            .with_imports_query(include_str!("../../languages/src/python/imports.scm"))
+            .unwrap(),
+        )
+    });
+
+    // TODO: Ideally should use actual language configurations
+    static C: LazyLock<Arc<Language>> = LazyLock::new(|| {
+        Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "C".into(),
+                    import_path_strip_regex: Some(Regex::new("^<|>$").unwrap()),
+                    ..Default::default()
+                },
+                Some(tree_sitter_c::LANGUAGE.into()),
+            )
+            .with_imports_query(include_str!("../../languages/src/c/imports.scm"))
+            .unwrap(),
+        )
+    });
+
+    static CPP: LazyLock<Arc<Language>> = LazyLock::new(|| {
+        Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "C++".into(),
+                    import_path_strip_regex: Some(Regex::new("^<|>$").unwrap()),
+                    ..Default::default()
+                },
+                Some(tree_sitter_cpp::LANGUAGE.into()),
+            )
+            .with_imports_query(include_str!("../../languages/src/cpp/imports.scm"))
+            .unwrap(),
+        )
+    });
+
+    static GO: LazyLock<Arc<Language>> = LazyLock::new(|| {
+        Arc::new(
+            Language::new(
+                LanguageConfig {
+                    name: "Go".into(),
+                    ..Default::default()
+                },
+                Some(tree_sitter_go::LANGUAGE.into()),
+            )
+            .with_imports_query(include_str!("../../languages/src/go/imports.scm"))
+            .unwrap(),
+        )
+    });
+
+    impl Import {
+        fn to_identifier_parts(&self, identifier: &str) -> Vec<String> {
+            match self {
+                Import::Direct { module } => module.to_identifier_parts(identifier),
+                Import::Alias {
+                    module,
+                    external_identifier: external_name,
+                } => {
+                    module.to_identifier_parts(&format!("{} AS {}", external_name.name, identifier))
+                }
+            }
+        }
+    }
+
+    impl Module {
+        fn to_identifier_parts(&self, identifier: &str) -> Vec<String> {
+            match self {
+                Self::Namespace(namespace) => namespace.to_identifier_parts(identifier),
+                Self::SourceExact(path) => {
+                    vec![
+                        format!("SOURCE {}", path.display().to_string().replace("\\", "/")),
+                        identifier.to_string(),
+                    ]
+                }
+                Self::SourceFuzzy(path) => {
+                    vec![
+                        format!(
+                            "SOURCE FUZZY {}",
+                            path.display().to_string().replace("\\", "/")
+                        ),
+                        identifier.to_string(),
+                    ]
+                }
+            }
+        }
+    }
+
+    impl Namespace {
+        fn to_identifier_parts(&self, identifier: &str) -> Vec<String> {
+            self.0
+                .iter()
+                .map(|chunk| chunk.to_string())
+                .chain(std::iter::once(identifier.to_string()))
+                .collect::<Vec<_>>()
+        }
+    }
+}

crates/edit_prediction_context/src/syntax_index.rs 🔗

@@ -5,6 +5,7 @@ use futures::lock::Mutex;
 use futures::{FutureExt as _, StreamExt, future};
 use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
 use itertools::Itertools;
+
 use language::{Buffer, BufferEvent};
 use postage::stream::Stream as _;
 use project::buffer_store::{BufferStore, BufferStoreEvent};
@@ -17,6 +18,7 @@ use std::sync::Arc;
 use text::BufferId;
 use util::{RangeExt as _, debug_panic, some_or_debug_panic};
 
+use crate::CachedDeclarationPath;
 use crate::declaration::{
     BufferDeclaration, Declaration, DeclarationId, FileDeclaration, Identifier,
 };
@@ -28,6 +30,8 @@ use crate::outline::declarations_in_buffer;
 // `buffer_declarations_containing_range` assumes that the index is always immediately up to date.
 //
 // * Add a per language configuration for skipping indexing.
+//
+// * Handle tsx / ts / js referencing each-other
 
 // Potential future improvements:
 //
@@ -61,6 +65,7 @@ pub struct SyntaxIndex {
     state: Arc<Mutex<SyntaxIndexState>>,
     project: WeakEntity<Project>,
     initial_file_indexing_done_rx: postage::watch::Receiver<bool>,
+    _file_indexing_task: Option<Task<()>>,
 }
 
 pub struct SyntaxIndexState {
@@ -70,7 +75,6 @@ pub struct SyntaxIndexState {
     buffers: HashMap<BufferId, BufferState>,
     dirty_files: HashMap<ProjectEntryId, ProjectPath>,
     dirty_files_tx: mpsc::Sender<()>,
-    _file_indexing_task: Option<Task<()>>,
 }
 
 #[derive(Debug, Default)]
@@ -102,12 +106,12 @@ impl SyntaxIndex {
             buffers: HashMap::default(),
             dirty_files: HashMap::default(),
             dirty_files_tx,
-            _file_indexing_task: None,
         };
-        let this = Self {
+        let mut this = Self {
             project: project.downgrade(),
             state: Arc::new(Mutex::new(initial_state)),
             initial_file_indexing_done_rx,
+            _file_indexing_task: None,
         };
 
         let worktree_store = project.read(cx).worktree_store();
@@ -116,75 +120,77 @@ impl SyntaxIndex {
             .worktrees()
             .map(|w| w.read(cx).snapshot())
             .collect::<Vec<_>>();
-        if !initial_worktree_snapshots.is_empty() {
-            this.state.try_lock().unwrap()._file_indexing_task =
-                Some(cx.spawn(async move |this, cx| {
-                    let snapshots_file_count = initial_worktree_snapshots
-                        .iter()
-                        .map(|worktree| worktree.file_count())
-                        .sum::<usize>();
-                    let chunk_size = snapshots_file_count.div_ceil(file_indexing_parallelism);
-                    let chunk_count = snapshots_file_count.div_ceil(chunk_size);
-                    let file_chunks = initial_worktree_snapshots
-                        .iter()
-                        .flat_map(|worktree| {
-                            let worktree_id = worktree.id();
-                            worktree.files(false, 0).map(move |entry| {
-                                (
-                                    entry.id,
-                                    ProjectPath {
-                                        worktree_id,
-                                        path: entry.path.clone(),
-                                    },
-                                )
-                            })
+        this._file_indexing_task = Some(cx.spawn(async move |this, cx| {
+            let snapshots_file_count = initial_worktree_snapshots
+                .iter()
+                .map(|worktree| worktree.file_count())
+                .sum::<usize>();
+            if snapshots_file_count > 0 {
+                let chunk_size = snapshots_file_count.div_ceil(file_indexing_parallelism);
+                let chunk_count = snapshots_file_count.div_ceil(chunk_size);
+                let file_chunks = initial_worktree_snapshots
+                    .iter()
+                    .flat_map(|worktree| {
+                        let worktree_id = worktree.id();
+                        worktree.files(false, 0).map(move |entry| {
+                            (
+                                entry.id,
+                                ProjectPath {
+                                    worktree_id,
+                                    path: entry.path.clone(),
+                                },
+                            )
                         })
-                        .chunks(chunk_size);
-
-                    let mut tasks = Vec::with_capacity(chunk_count);
-                    for chunk in file_chunks.into_iter() {
-                        tasks.push(Self::update_dirty_files(
-                            &this,
-                            chunk.into_iter().collect(),
-                            cx.clone(),
-                        ));
-                    }
-                    futures::future::join_all(tasks).await;
-
-                    log::info!("Finished initial file indexing");
-                    *initial_file_indexing_done_tx.borrow_mut() = true;
-
-                    let Ok(state) = this.read_with(cx, |this, _cx| this.state.clone()) else {
-                        return;
-                    };
-                    while dirty_files_rx.next().await.is_some() {
-                        let mut state = state.lock().await;
-                        let was_underused = state.dirty_files.capacity() > 255
-                            && state.dirty_files.len() * 8 < state.dirty_files.capacity();
-                        let dirty_files = state.dirty_files.drain().collect::<Vec<_>>();
-                        if was_underused {
-                            state.dirty_files.shrink_to_fit();
-                        }
-                        drop(state);
-                        if dirty_files.is_empty() {
-                            continue;
-                        }
+                    })
+                    .chunks(chunk_size);
+
+                let mut tasks = Vec::with_capacity(chunk_count);
+                for chunk in file_chunks.into_iter() {
+                    tasks.push(Self::update_dirty_files(
+                        &this,
+                        chunk.into_iter().collect(),
+                        cx.clone(),
+                    ));
+                }
+                futures::future::join_all(tasks).await;
+                log::info!("Finished initial file indexing");
+            }
 
-                        let chunk_size = dirty_files.len().div_ceil(file_indexing_parallelism);
-                        let chunk_count = dirty_files.len().div_ceil(chunk_size);
-                        let mut tasks = Vec::with_capacity(chunk_count);
-                        let chunks = dirty_files.into_iter().chunks(chunk_size);
-                        for chunk in chunks.into_iter() {
-                            tasks.push(Self::update_dirty_files(
-                                &this,
-                                chunk.into_iter().collect(),
-                                cx.clone(),
-                            ));
-                        }
-                        futures::future::join_all(tasks).await;
-                    }
-                }));
-        }
+            *initial_file_indexing_done_tx.borrow_mut() = true;
+
+            let Ok(state) = this.read_with(cx, |this, _cx| Arc::downgrade(&this.state)) else {
+                return;
+            };
+            while dirty_files_rx.next().await.is_some() {
+                let Some(state) = state.upgrade() else {
+                    return;
+                };
+                let mut state = state.lock().await;
+                let was_underused = state.dirty_files.capacity() > 255
+                    && state.dirty_files.len() * 8 < state.dirty_files.capacity();
+                let dirty_files = state.dirty_files.drain().collect::<Vec<_>>();
+                if was_underused {
+                    state.dirty_files.shrink_to_fit();
+                }
+                drop(state);
+                if dirty_files.is_empty() {
+                    continue;
+                }
+
+                let chunk_size = dirty_files.len().div_ceil(file_indexing_parallelism);
+                let chunk_count = dirty_files.len().div_ceil(chunk_size);
+                let mut tasks = Vec::with_capacity(chunk_count);
+                let chunks = dirty_files.into_iter().chunks(chunk_size);
+                for chunk in chunks.into_iter() {
+                    tasks.push(Self::update_dirty_files(
+                        &this,
+                        chunk.into_iter().collect(),
+                        cx.clone(),
+                    ));
+                }
+                futures::future::join_all(tasks).await;
+            }
+        }));
 
         cx.subscribe(&worktree_store, Self::handle_worktree_store_event)
             .detach();
@@ -364,7 +370,9 @@ impl SyntaxIndex {
         cx: &mut Context<Self>,
     ) {
         match event {
-            BufferEvent::Edited => self.update_buffer(buffer, cx),
+            BufferEvent::Edited |
+            // paths are cached and so should be updated
+            BufferEvent::FileHandleChanged => self.update_buffer(buffer, cx),
             _ => {}
         }
     }
@@ -375,8 +383,16 @@ impl SyntaxIndex {
             return;
         }
 
-        let Some(project_entry_id) =
-            project::File::from_dyn(buffer.file()).and_then(|f| f.project_entry_id(cx))
+        let Some((project_entry_id, cached_path)) = project::File::from_dyn(buffer.file())
+            .and_then(|f| {
+                let project_entry_id = f.project_entry_id()?;
+                let cached_path = CachedDeclarationPath::new(
+                    f.worktree.read(cx).abs_path(),
+                    &f.path,
+                    buffer.language(),
+                );
+                Some((project_entry_id, cached_path))
+            })
         else {
             return;
         };
@@ -440,6 +456,7 @@ impl SyntaxIndex {
                     buffer_id,
                     declaration,
                     project_entry_id,
+                    cached_path: cached_path.clone(),
                 });
                 new_ids.push(declaration_id);
 
@@ -507,13 +524,14 @@ impl SyntaxIndex {
 
         let snapshot_task = worktree.update(cx, |worktree, cx| {
             let load_task = worktree.load_file(&project_path.path, cx);
+            let worktree_abs_path = worktree.abs_path();
             cx.spawn(async move |_this, cx| {
                 let loaded_file = load_task.await?;
                 let language = language.await?;
 
                 let buffer = cx.new(|cx| {
                     let mut buffer = Buffer::local(loaded_file.text, cx);
-                    buffer.set_language(Some(language), cx);
+                    buffer.set_language(Some(language.clone()), cx);
                     buffer
                 })?;
 
@@ -522,14 +540,22 @@ impl SyntaxIndex {
                     parse_status.changed().await?;
                 }
 
-                buffer.read_with(cx, |buffer, _cx| buffer.snapshot())
+                let cached_path = CachedDeclarationPath::new(
+                    worktree_abs_path,
+                    &project_path.path,
+                    Some(&language),
+                );
+
+                let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
+
+                anyhow::Ok((snapshot, cached_path))
             })
         });
 
         let state = Arc::downgrade(&self.state);
         cx.background_spawn(async move {
             // TODO: How to handle errors?
-            let Ok(snapshot) = snapshot_task.await else {
+            let Ok((snapshot, cached_path)) = snapshot_task.await else {
                 return;
             };
             let rope = snapshot.as_rope();
@@ -567,6 +593,7 @@ impl SyntaxIndex {
                 let declaration_id = state.declarations.insert(Declaration::File {
                     project_entry_id: entry_id,
                     declaration,
+                    cached_path: cached_path.clone(),
                 });
                 new_ids.push(declaration_id);
 
@@ -921,6 +948,7 @@ mod tests {
         if let Declaration::File {
             declaration,
             project_entry_id: file,
+            ..
         } = declaration
         {
             assert_eq!(

crates/edit_prediction_context/src/text_similarity.rs 🔗

@@ -1,9 +1,12 @@
 use hashbrown::HashTable;
 use regex::Regex;
 use std::{
+    borrow::Cow,
     hash::{Hash, Hasher as _},
+    path::Path,
     sync::LazyLock,
 };
+use util::rel_path::RelPath;
 
 use crate::reference::Reference;
 
@@ -45,19 +48,34 @@ impl Occurrences {
         )
     }
 
-    pub fn from_identifiers<'a>(identifiers: impl IntoIterator<Item = &'a str>) -> Self {
+    pub fn from_identifiers(identifiers: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
         let mut this = Self::default();
         // TODO: Score matches that match case higher?
         //
         // TODO: Also include unsplit identifier?
         for identifier in identifiers {
-            for identifier_part in split_identifier(identifier) {
+            for identifier_part in split_identifier(identifier.as_ref()) {
                 this.add_hash(fx_hash(&identifier_part.to_lowercase()));
             }
         }
         this
     }
 
+    pub fn from_worktree_path(worktree_name: Option<Cow<'_, str>>, rel_path: &RelPath) -> Self {
+        if let Some(worktree_name) = worktree_name {
+            Self::from_identifiers(
+                std::iter::once(worktree_name)
+                    .chain(iter_path_without_extension(rel_path.as_std_path())),
+            )
+        } else {
+            Self::from_path(rel_path.as_std_path())
+        }
+    }
+
+    pub fn from_path(path: &Path) -> Self {
+        Self::from_identifiers(iter_path_without_extension(path))
+    }
+
     fn add_hash(&mut self, hash: u64) {
         self.table
             .entry(
@@ -82,6 +100,15 @@ impl Occurrences {
     }
 }
 
+fn iter_path_without_extension(path: &Path) -> impl Iterator<Item = Cow<'_, str>> {
+    let last_component: Option<Cow<'_, str>> = path.file_stem().map(|stem| stem.to_string_lossy());
+    let mut path_components = path.components();
+    path_components.next_back();
+    path_components
+        .map(|component| component.as_os_str().to_string_lossy())
+        .chain(last_component)
+}
+
 pub fn fx_hash<T: Hash + ?Sized>(data: &T) -> u64 {
     let mut hasher = collections::FxHasher::default();
     data.hash(&mut hasher);
@@ -269,4 +296,19 @@ mod test {
         // the smaller set, 10.
         assert_eq!(weighted_overlap_coefficient(&set_a, &set_b), 7.0 / 10.0);
     }
+
+    #[test]
+    fn test_iter_path_without_extension() {
+        let mut iter = iter_path_without_extension(Path::new(""));
+        assert_eq!(iter.next(), None);
+
+        let iter = iter_path_without_extension(Path::new("foo"));
+        assert_eq!(iter.collect::<Vec<_>>(), ["foo"]);
+
+        let iter = iter_path_without_extension(Path::new("foo/bar.txt"));
+        assert_eq!(iter.collect::<Vec<_>>(), ["foo", "bar"]);
+
+        let iter = iter_path_without_extension(Path::new("foo/bar/baz.txt"));
+        assert_eq!(iter.collect::<Vec<_>>(), ["foo", "bar", "baz"]);
+    }
 }

crates/editor/src/actions.rs 🔗

@@ -456,6 +456,33 @@ actions!(
         Fold,
         /// Folds all foldable regions in the editor.
         FoldAll,
+        /// Folds all code blocks at indentation level 1.
+        #[action(name = "FoldAtLevel_1")]
+        FoldAtLevel1,
+        /// Folds all code blocks at indentation level 2.
+        #[action(name = "FoldAtLevel_2")]
+        FoldAtLevel2,
+        /// Folds all code blocks at indentation level 3.
+        #[action(name = "FoldAtLevel_3")]
+        FoldAtLevel3,
+        /// Folds all code blocks at indentation level 4.
+        #[action(name = "FoldAtLevel_4")]
+        FoldAtLevel4,
+        /// Folds all code blocks at indentation level 5.
+        #[action(name = "FoldAtLevel_5")]
+        FoldAtLevel5,
+        /// Folds all code blocks at indentation level 6.
+        #[action(name = "FoldAtLevel_6")]
+        FoldAtLevel6,
+        /// Folds all code blocks at indentation level 7.
+        #[action(name = "FoldAtLevel_7")]
+        FoldAtLevel7,
+        /// Folds all code blocks at indentation level 8.
+        #[action(name = "FoldAtLevel_8")]
+        FoldAtLevel8,
+        /// Folds all code blocks at indentation level 9.
+        #[action(name = "FoldAtLevel_9")]
+        FoldAtLevel9,
         /// Folds all function bodies in the editor.
         FoldFunctionBodies,
         /// Folds the current code block and all its children.

crates/editor/src/editor.rs 🔗

@@ -3172,7 +3172,7 @@ impl Editor {
             self.refresh_code_actions(window, cx);
             self.refresh_document_highlights(cx);
             self.refresh_selected_text_highlights(false, window, cx);
-            refresh_matching_bracket_highlights(self, window, cx);
+            refresh_matching_bracket_highlights(self, cx);
             self.update_visible_edit_prediction(window, cx);
             self.edit_prediction_requires_modifier_in_indent_conflict = true;
             linked_editing_ranges::refresh_linked_ranges(self, window, cx);
@@ -5343,7 +5343,7 @@ impl Editor {
                 let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?;
                 let worktree_entry = buffer_worktree
                     .read(cx)
-                    .entry_for_id(buffer_file.project_entry_id(cx)?)?;
+                    .entry_for_id(buffer_file.project_entry_id()?)?;
                 if worktree_entry.is_ignored {
                     return None;
                 }
@@ -6607,26 +6607,32 @@ impl Editor {
         &self.context_menu
     }
 
-    fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<()> {
-        let newest_selection = self.selections.newest_anchor().clone();
-        let newest_selection_adjusted = self.selections.newest_adjusted(cx);
-        let buffer = self.buffer.read(cx);
-        if newest_selection.head().diff_base_anchor.is_some() {
-            return None;
-        }
-        let (start_buffer, start) =
-            buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?;
-        let (end_buffer, end) =
-            buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?;
-        if start_buffer != end_buffer {
-            return None;
-        }
-
+    fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         self.code_actions_task = Some(cx.spawn_in(window, async move |this, cx| {
             cx.background_executor()
                 .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT)
                 .await;
 
+            let (start_buffer, start, _, end, newest_selection) = this
+                .update(cx, |this, cx| {
+                    let newest_selection = this.selections.newest_anchor().clone();
+                    if newest_selection.head().diff_base_anchor.is_some() {
+                        return None;
+                    }
+                    let newest_selection_adjusted = this.selections.newest_adjusted(cx);
+                    let buffer = this.buffer.read(cx);
+
+                    let (start_buffer, start) =
+                        buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?;
+                    let (end_buffer, end) =
+                        buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?;
+
+                    Some((start_buffer, start, end_buffer, end, newest_selection))
+                })?
+                .filter(|(start_buffer, _, end_buffer, _, _)| start_buffer == end_buffer)
+                .context(
+                    "Expected selection to lie in a single buffer when refreshing code actions",
+                )?;
             let (providers, tasks) = this.update_in(cx, |this, window, cx| {
                 let providers = this.code_action_providers.clone();
                 let tasks = this
@@ -6667,7 +6673,6 @@ impl Editor {
                 cx.notify();
             })
         }));
-        None
     }
 
     fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -6917,19 +6922,24 @@ impl Editor {
         if self.selections.count() != 1 || self.selections.line_mode() {
             return None;
         }
-        let selection = self.selections.newest::<Point>(cx);
-        if selection.is_empty() || selection.start.row != selection.end.row {
+        let selection = self.selections.newest_anchor();
+        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
+        let selection_point_range = selection.start.to_point(&multi_buffer_snapshot)
+            ..selection.end.to_point(&multi_buffer_snapshot);
+        // If the selection spans multiple rows OR it is empty
+        if selection_point_range.start.row != selection_point_range.end.row
+            || selection_point_range.start.column == selection_point_range.end.column
+        {
             return None;
         }
-        let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
-        let selection_anchor_range = selection.range().to_anchors(&multi_buffer_snapshot);
+
         let query = multi_buffer_snapshot
-            .text_for_range(selection_anchor_range.clone())
+            .text_for_range(selection.range())
             .collect::<String>();
         if query.trim().is_empty() {
             return None;
         }
-        Some((query, selection_anchor_range))
+        Some((query, selection.range()))
     }
 
     fn update_selection_occurrence_highlights(
@@ -11687,13 +11697,26 @@ impl Editor {
                     rows.end.previous_row().0,
                     buffer.line_len(rows.end.previous_row()),
                 );
-                let text = buffer
-                    .text_for_range(start..end)
-                    .chain(Some("\n"))
-                    .collect::<String>();
+
+                let mut text = buffer.text_for_range(start..end).collect::<String>();
+
                 let insert_location = if upwards {
-                    Point::new(rows.end.0, 0)
+                    // When duplicating upward, we need to insert before the current line.
+                    // If we're on the last line and it doesn't end with a newline,
+                    // we need to add a newline before the duplicated content.
+                    let needs_leading_newline = rows.end.0 >= buffer.max_point().row
+                        && buffer.max_point().column > 0
+                        && !text.ends_with('\n');
+
+                    if needs_leading_newline {
+                        text.insert(0, '\n');
+                        end
+                    } else {
+                        text.push('\n');
+                        Point::new(rows.end.0, 0)
+                    }
                 } else {
+                    text.push('\n');
                     start
                 };
                 edits.push((insert_location..insert_location, text));
@@ -12503,9 +12526,18 @@ impl Editor {
                 let mut start = selection.start;
                 let mut end = selection.end;
                 let is_entire_line = selection.is_empty() || self.selections.line_mode();
+                let mut add_trailing_newline = false;
                 if is_entire_line {
                     start = Point::new(start.row, 0);
-                    end = cmp::min(max_point, Point::new(end.row + 1, 0));
+                    let next_line_start = Point::new(end.row + 1, 0);
+                    if next_line_start <= max_point {
+                        end = next_line_start;
+                    } else {
+                        // We're on the last line without a trailing newline.
+                        // Copy to the end of the line and add a newline afterwards.
+                        end = Point::new(end.row, buffer.line_len(MultiBufferRow(end.row)));
+                        add_trailing_newline = true;
+                    }
                 }
 
                 let mut trimmed_selections = Vec::new();
@@ -12556,6 +12588,10 @@ impl Editor {
                         text.push_str(chunk);
                         len += chunk.len();
                     }
+                    if add_trailing_newline {
+                        text.push('\n');
+                        len += 1;
+                    }
                     clipboard_selections.push(ClipboardSelection {
                         len,
                         is_entire_line,
@@ -18170,6 +18206,87 @@ impl Editor {
         self.fold_creases(to_fold, true, window, cx);
     }
 
+    pub fn fold_at_level_1(
+        &mut self,
+        _: &actions::FoldAtLevel1,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.fold_at_level(&actions::FoldAtLevel(1), window, cx);
+    }
+
+    pub fn fold_at_level_2(
+        &mut self,
+        _: &actions::FoldAtLevel2,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.fold_at_level(&actions::FoldAtLevel(2), window, cx);
+    }
+
+    pub fn fold_at_level_3(
+        &mut self,
+        _: &actions::FoldAtLevel3,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.fold_at_level(&actions::FoldAtLevel(3), window, cx);
+    }
+
+    pub fn fold_at_level_4(
+        &mut self,
+        _: &actions::FoldAtLevel4,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.fold_at_level(&actions::FoldAtLevel(4), window, cx);
+    }
+
+    pub fn fold_at_level_5(
+        &mut self,
+        _: &actions::FoldAtLevel5,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.fold_at_level(&actions::FoldAtLevel(5), window, cx);
+    }
+
+    pub fn fold_at_level_6(
+        &mut self,
+        _: &actions::FoldAtLevel6,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.fold_at_level(&actions::FoldAtLevel(6), window, cx);
+    }
+
+    pub fn fold_at_level_7(
+        &mut self,
+        _: &actions::FoldAtLevel7,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.fold_at_level(&actions::FoldAtLevel(7), window, cx);
+    }
+
+    pub fn fold_at_level_8(
+        &mut self,
+        _: &actions::FoldAtLevel8,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.fold_at_level(&actions::FoldAtLevel(8), window, cx);
+    }
+
+    pub fn fold_at_level_9(
+        &mut self,
+        _: &actions::FoldAtLevel9,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.fold_at_level(&actions::FoldAtLevel(9), window, cx);
+    }
+
     pub fn fold_all(&mut self, _: &actions::FoldAll, window: &mut Window, cx: &mut Context<Self>) {
         if self.buffer.read(cx).is_singleton() {
             let mut fold_ranges = Vec::new();
@@ -20698,7 +20815,7 @@ impl Editor {
                 self.refresh_code_actions(window, cx);
                 self.refresh_selected_text_highlights(true, window, cx);
                 self.refresh_single_line_folds(window, cx);
-                refresh_matching_bracket_highlights(self, window, cx);
+                refresh_matching_bracket_highlights(self, cx);
                 if self.has_active_edit_prediction() {
                     self.update_visible_edit_prediction(window, cx);
                 }

crates/editor/src/editor_settings.rs 🔗

@@ -176,7 +176,7 @@ impl ScrollbarVisibility for EditorSettings {
 }
 
 impl Settings for EditorSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let editor = content.editor.clone();
         let scrollbar = editor.scrollbar.unwrap();
         let minimap = editor.minimap.unwrap();

crates/editor/src/editor_tests.rs 🔗

@@ -12416,11 +12416,6 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
         .join("\n"),
     );
 
-    // Submit a format request.
-    let format = cx
-        .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
-        .unwrap();
-
     // Record which buffer changes have been sent to the language server
     let buffer_changes = Arc::new(Mutex::new(Vec::new()));
     cx.lsp
@@ -12441,28 +12436,29 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
         .set_request_handler::<lsp::request::Formatting, _, _>({
             let buffer_changes = buffer_changes.clone();
             move |_, _| {
-                // When formatting is requested, trailing whitespace has already been stripped,
-                // and the trailing newline has already been added.
-                assert_eq!(
-                    &buffer_changes.lock()[1..],
-                    &[
-                        (
-                            lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
-                            "".into()
-                        ),
-                        (
-                            lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
-                            "".into()
-                        ),
-                        (
-                            lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
-                            "\n".into()
-                        ),
-                    ]
-                );
-
+                let buffer_changes = buffer_changes.clone();
                 // Insert blank lines between each line of the buffer.
                 async move {
+                    // When formatting is requested, trailing whitespace has already been stripped,
+                    // and the trailing newline has already been added.
+                    assert_eq!(
+                        &buffer_changes.lock()[1..],
+                        &[
+                            (
+                                lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
+                                "".into()
+                            ),
+                            (
+                                lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
+                                "".into()
+                            ),
+                            (
+                                lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
+                                "\n".into()
+                            ),
+                        ]
+                    );
+
                     Ok(Some(vec![
                         lsp::TextEdit {
                             range: lsp::Range::new(
@@ -12483,10 +12479,17 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
             }
         });
 
+    // Submit a format request.
+    let format = cx
+        .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
+        .unwrap();
+
+    cx.run_until_parked();
     // After formatting the buffer, the trailing whitespace is stripped,
     // a newline is appended, and the edits provided by the language server
     // have been applied.
     format.await.unwrap();
+
     cx.assert_editor_state(
         &[
             "one",   //
@@ -16515,7 +16518,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
     leader.update(cx, |leader, cx| {
         leader.buffer.update(cx, |multibuffer, cx| {
             multibuffer.set_excerpts_for_path(
-                PathKey::namespaced(1, rel_path("b.txt").into_arc()),
+                PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
                 buffer_1.clone(),
                 vec![
                     Point::row_range(0..3),
@@ -16526,7 +16529,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
                 cx,
             );
             multibuffer.set_excerpts_for_path(
-                PathKey::namespaced(1, rel_path("a.txt").into_arc()),
+                PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
                 buffer_2.clone(),
                 vec![Point::row_range(0..6), Point::row_range(8..12)],
                 0,
@@ -21029,7 +21032,7 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
         for buffer in &buffers {
             let snapshot = buffer.read(cx).snapshot();
             multibuffer.set_excerpts_for_path(
-                PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
+                PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
                 buffer.clone(),
                 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
                 2,
@@ -26475,3 +26478,64 @@ fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
         .map(Rgba::from)
         .collect()
 }
+
+#[gpui::test]
+fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let editor = cx.add_window(|window, cx| {
+        let buffer = MultiBuffer::build_simple("line1\nline2", cx);
+        build_editor(buffer, window, cx)
+    });
+
+    editor
+        .update(cx, |editor, window, cx| {
+            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+                s.select_display_ranges([
+                    DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
+                ])
+            });
+
+            editor.duplicate_line_up(&DuplicateLineUp, window, cx);
+
+            assert_eq!(
+                editor.display_text(cx),
+                "line1\nline2\nline2",
+                "Duplicating last line upward should create duplicate above, not on same line"
+            );
+
+            assert_eq!(
+                editor.selections.display_ranges(cx),
+                vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)],
+                "Selection should remain on the original line"
+            );
+        })
+        .unwrap();
+}
+
+#[gpui::test]
+async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    cx.set_state("line1\nline2ˇ");
+
+    cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
+
+    let clipboard_text = cx
+        .read_from_clipboard()
+        .and_then(|item| item.text().as_deref().map(str::to_string));
+
+    assert_eq!(
+        clipboard_text,
+        Some("line2\n".to_string()),
+        "Copying a line without trailing newline should include a newline"
+    );
+
+    cx.set_state("line1\nˇ");
+
+    cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
+
+    cx.assert_editor_state("line1\nline2\nˇ");
+}

crates/editor/src/element.rs 🔗

@@ -432,6 +432,15 @@ impl EditorElement {
         register_action(editor, window, Editor::open_selected_filename);
         register_action(editor, window, Editor::fold);
         register_action(editor, window, Editor::fold_at_level);
+        register_action(editor, window, Editor::fold_at_level_1);
+        register_action(editor, window, Editor::fold_at_level_2);
+        register_action(editor, window, Editor::fold_at_level_3);
+        register_action(editor, window, Editor::fold_at_level_4);
+        register_action(editor, window, Editor::fold_at_level_5);
+        register_action(editor, window, Editor::fold_at_level_6);
+        register_action(editor, window, Editor::fold_at_level_7);
+        register_action(editor, window, Editor::fold_at_level_8);
+        register_action(editor, window, Editor::fold_at_level_9);
         register_action(editor, window, Editor::fold_all);
         register_action(editor, window, Editor::fold_function_bodies);
         register_action(editor, window, Editor::fold_recursive);

crates/editor/src/highlight_matching_bracket.rs 🔗

@@ -1,47 +1,46 @@
 use crate::{Editor, RangeToAnchorExt};
-use gpui::{Context, HighlightStyle, Window};
+use gpui::{Context, HighlightStyle};
 use language::CursorShape;
+use multi_buffer::ToOffset;
 use theme::ActiveTheme;
 
 enum MatchingBracketHighlight {}
 
-pub fn refresh_matching_bracket_highlights(
-    editor: &mut Editor,
-    window: &mut Window,
-    cx: &mut Context<Editor>,
-) {
+pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut Context<Editor>) {
     editor.clear_highlights::<MatchingBracketHighlight>(cx);
 
-    let newest_selection = editor.selections.newest::<usize>(cx);
+    let buffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+    let newest_selection = editor
+        .selections
+        .newest_anchor()
+        .map(|anchor| anchor.to_offset(&buffer_snapshot));
     // Don't highlight brackets if the selection isn't empty
     if !newest_selection.is_empty() {
         return;
     }
 
-    let snapshot = editor.snapshot(window, cx);
     let head = newest_selection.head();
-    if head > snapshot.buffer_snapshot().len() {
+    if head > buffer_snapshot.len() {
         log::error!("bug: cursor offset is out of range while refreshing bracket highlights");
         return;
     }
 
     let mut tail = head;
     if (editor.cursor_shape == CursorShape::Block || editor.cursor_shape == CursorShape::Hollow)
-        && head < snapshot.buffer_snapshot().len()
+        && head < buffer_snapshot.len()
     {
-        if let Some(tail_ch) = snapshot.buffer_snapshot().chars_at(tail).next() {
+        if let Some(tail_ch) = buffer_snapshot.chars_at(tail).next() {
             tail += tail_ch.len_utf8();
         }
     }
 
-    if let Some((opening_range, closing_range)) = snapshot
-        .buffer_snapshot()
-        .innermost_enclosing_bracket_ranges(head..tail, None)
+    if let Some((opening_range, closing_range)) =
+        buffer_snapshot.innermost_enclosing_bracket_ranges(head..tail, None)
     {
         editor.highlight_text::<MatchingBracketHighlight>(
             vec![
-                opening_range.to_anchors(&snapshot.buffer_snapshot()),
-                closing_range.to_anchors(&snapshot.buffer_snapshot()),
+                opening_range.to_anchors(&buffer_snapshot),
+                closing_range.to_anchors(&buffer_snapshot),
             ],
             HighlightStyle {
                 background_color: Some(

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -1495,7 +1495,7 @@ pub mod tests {
             .into_response()
             .expect("work done progress create request failed");
         cx.executor().run_until_parked();
-        fake_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
+        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
             token: lsp::ProgressToken::String(progress_token.to_string()),
             value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
                 lsp::WorkDoneProgressBegin::default(),
@@ -1515,7 +1515,7 @@ pub mod tests {
             })
             .unwrap();
 
-        fake_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
+        fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
             token: lsp::ProgressToken::String(progress_token.to_string()),
             value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
                 lsp::WorkDoneProgressEnd::default(),

crates/editor/src/selections_collection.rs 🔗

@@ -184,6 +184,27 @@ impl SelectionsCollection {
         selections
     }
 
+    /// Returns all of the selections, adjusted to take into account the selection line_mode. Uses a provided snapshot to resolve selections.
+    pub fn all_adjusted_with_snapshot(
+        &self,
+        snapshot: &MultiBufferSnapshot,
+    ) -> Vec<Selection<Point>> {
+        let mut selections = self
+            .disjoint
+            .iter()
+            .chain(self.pending_anchor())
+            .map(|anchor| anchor.map(|anchor| anchor.to_point(&snapshot)))
+            .collect::<Vec<_>>();
+        if self.line_mode {
+            for selection in &mut selections {
+                let new_range = snapshot.expand_to_line(selection.range());
+                selection.start = new_range.start;
+                selection.end = new_range.end;
+            }
+        }
+        selections
+    }
+
     /// Returns the newest selection, adjusted to take into account the selection line_mode
     pub fn newest_adjusted(&self, cx: &mut App) -> Selection<Point> {
         let mut selection = self.newest::<Point>(cx);

crates/editor/src/test/editor_lsp_test_context.rs 🔗

@@ -262,6 +262,77 @@ impl EditorLspTestContext {
         Self::new(language, capabilities, cx).await
     }
 
+    pub async fn new_tsx(
+        capabilities: lsp::ServerCapabilities,
+        cx: &mut gpui::TestAppContext,
+    ) -> EditorLspTestContext {
+        let mut word_characters: HashSet<char> = Default::default();
+        word_characters.insert('$');
+        word_characters.insert('#');
+        let language = Language::new(
+            LanguageConfig {
+                name: "TSX".into(),
+                matcher: LanguageMatcher {
+                    path_suffixes: vec!["tsx".to_string()],
+                    ..Default::default()
+                },
+                brackets: language::BracketPairConfig {
+                    pairs: vec![language::BracketPair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                        close: true,
+                        surround: true,
+                        newline: true,
+                    }],
+                    disabled_scopes_by_bracket_ix: Default::default(),
+                },
+                word_characters,
+                ..Default::default()
+            },
+            Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
+        )
+        .with_queries(LanguageQueries {
+            brackets: Some(Cow::from(indoc! {r#"
+                ("(" @open ")" @close)
+                ("[" @open "]" @close)
+                ("{" @open "}" @close)
+                ("<" @open ">" @close)
+                ("<" @open "/>" @close)
+                ("</" @open ">" @close)
+                ("\"" @open "\"" @close)
+                ("'" @open "'" @close)
+                ("`" @open "`" @close)
+                ((jsx_element (jsx_opening_element) @open (jsx_closing_element) @close) (#set! newline.only))"#})),
+            indents: Some(Cow::from(indoc! {r#"
+                [
+                    (call_expression)
+                    (assignment_expression)
+                    (member_expression)
+                    (lexical_declaration)
+                    (variable_declaration)
+                    (assignment_expression)
+                    (if_statement)
+                    (for_statement)
+                ] @indent
+
+                (_ "[" "]" @end) @indent
+                (_ "<" ">" @end) @indent
+                (_ "{" "}" @end) @indent
+                (_ "(" ")" @end) @indent
+
+                (jsx_opening_element ">" @end) @indent
+
+                (jsx_element
+                  (jsx_opening_element) @start
+                  (jsx_closing_element)? @end) @indent
+                "#})),
+            ..Default::default()
+        })
+        .expect("Could not parse queries");
+
+        Self::new(language, capabilities, cx).await
+    }
+
     pub async fn new_html(cx: &mut gpui::TestAppContext) -> Self {
         let language = Language::new(
             LanguageConfig {
@@ -369,7 +440,7 @@ impl EditorLspTestContext {
     }
 
     pub fn notify<T: notification::Notification>(&self, params: T::Params) {
-        self.lsp.notify::<T>(&params);
+        self.lsp.notify::<T>(params);
     }
 
     #[cfg(target_os = "windows")]

crates/extension_host/src/extension_settings.rs 🔗

@@ -2,7 +2,6 @@ use collections::HashMap;
 use extension::{
     DownloadFileCapability, ExtensionCapability, NpmInstallPackageCapability, ProcessExecCapability,
 };
-use gpui::App;
 use settings::Settings;
 use std::sync::Arc;
 
@@ -37,7 +36,7 @@ impl ExtensionSettings {
 }
 
 impl Settings for ExtensionSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         Self {
             auto_install_extensions: content.extension.auto_install_extensions.clone(),
             auto_update_extensions: content.extension.auto_update_extensions.clone(),

crates/file_finder/src/file_finder.rs 🔗

@@ -1172,18 +1172,25 @@ impl FileFinderDelegate {
         )
     }
 
+    /// Attempts to resolve an absolute file path and update the search matches if found.
+    ///
+    /// If the query path resolves to an absolute file that exists in the project,
+    /// this method will find the corresponding worktree and relative path, create a
+    /// match for it, and update the picker's search results.
+    ///
+    /// Returns `true` if the absolute path exists, otherwise returns `false`.
     fn lookup_absolute_path(
         &self,
         query: FileSearchQuery,
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
-    ) -> Task<()> {
+    ) -> Task<bool> {
         cx.spawn_in(window, async move |picker, cx| {
             let Some(project) = picker
                 .read_with(cx, |picker, _| picker.delegate.project.clone())
                 .log_err()
             else {
-                return;
+                return false;
             };
 
             let query_path = Path::new(query.path_query());
@@ -1216,7 +1223,7 @@ impl FileFinderDelegate {
                     })
                     .log_err();
                 if update_result.is_none() {
-                    return;
+                    return abs_file_exists;
                 }
             }
 
@@ -1229,6 +1236,7 @@ impl FileFinderDelegate {
                     anyhow::Ok(())
                 })
                 .log_err();
+            abs_file_exists
         })
     }
 
@@ -1377,13 +1385,14 @@ impl PickerDelegate for FileFinderDelegate {
         } else {
             let path_position = PathWithPosition::parse_str(raw_query);
             let raw_query = raw_query.trim().trim_end_matches(':').to_owned();
-            let path = path_position.path.to_str();
-            let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':');
+            let path = path_position.path.clone();
+            let path_str = path_position.path.to_str();
+            let path_trimmed = path_str.unwrap_or(&raw_query).trim_end_matches(':');
             let file_query_end = if path_trimmed == raw_query {
                 None
             } else {
                 // Safe to unwrap as we won't get here when the unwrap in if fails
-                Some(path.unwrap().len())
+                Some(path_str.unwrap().len())
             };
 
             let query = FileSearchQuery {
@@ -1392,11 +1401,29 @@ impl PickerDelegate for FileFinderDelegate {
                 path_position,
             };
 
-            if Path::new(query.path_query()).is_absolute() {
-                self.lookup_absolute_path(query, window, cx)
-            } else {
-                self.spawn_search(query, window, cx)
-            }
+            cx.spawn_in(window, async move |this, cx| {
+                let _ = maybe!(async move {
+                    let is_absolute_path = path.is_absolute();
+                    let did_resolve_abs_path = is_absolute_path
+                        && this
+                            .update_in(cx, |this, window, cx| {
+                                this.delegate
+                                    .lookup_absolute_path(query.clone(), window, cx)
+                            })?
+                            .await;
+
+                    // Only check for relative paths if no absolute paths were
+                    // found.
+                    if !did_resolve_abs_path {
+                        this.update_in(cx, |this, window, cx| {
+                            this.delegate.spawn_search(query, window, cx)
+                        })?
+                        .await;
+                    }
+                    anyhow::Ok(())
+                })
+                .await;
+            })
         }
     }
 

crates/file_finder/src/file_finder_settings.rs 🔗

@@ -11,7 +11,7 @@ pub struct FileFinderSettings {
 }
 
 impl Settings for FileFinderSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let file_finder = content.file_finder.as_ref().unwrap();
 
         Self {

crates/file_finder/src/file_finder_tests.rs 🔗

@@ -3069,3 +3069,49 @@ async fn test_filename_precedence(cx: &mut TestAppContext) {
         );
     });
 }
+
+#[gpui::test]
+async fn test_paths_with_starting_slash(cx: &mut TestAppContext) {
+    let app_state = init_test(cx);
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            path!("/root"),
+            json!({
+                "a": {
+                    "file1.txt": "",
+                    "b": {
+                        "file2.txt": "",
+                    },
+                }
+            }),
+        )
+        .await;
+
+    let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
+
+    let (picker, workspace, cx) = build_find_picker(project, cx);
+
+    let matching_abs_path = "/file1.txt".to_string();
+    picker
+        .update_in(cx, |picker, window, cx| {
+            picker
+                .delegate
+                .update_matches(matching_abs_path, window, cx)
+        })
+        .await;
+    picker.update(cx, |picker, _| {
+        assert_eq!(
+            collect_search_matches(picker).search_paths_only(),
+            vec![rel_path("a/file1.txt").into()],
+            "Relative path starting with slash should match"
+        )
+    });
+    cx.dispatch_action(SelectNext);
+    cx.dispatch_action(Confirm);
+    cx.read(|cx| {
+        let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
+        assert_eq!(active_editor.read(cx).title(cx), "file1.txt");
+    });
+}

crates/file_icons/Cargo.toml 🔗

@@ -15,7 +15,6 @@ doctest = false
 [dependencies]
 gpui.workspace = true
 serde.workspace = true
-settings.workspace = true
 theme.workspace = true
 util.workspace = true
 workspace-hack.workspace = true

crates/file_icons/src/file_icons.rs 🔗

@@ -2,8 +2,7 @@ use std::sync::Arc;
 use std::{path::Path, str};
 
 use gpui::{App, SharedString};
-use settings::Settings;
-use theme::{IconTheme, ThemeRegistry, ThemeSettings};
+use theme::{GlobalTheme, IconTheme, ThemeRegistry};
 use util::paths::PathExt;
 
 #[derive(Debug)]
@@ -13,10 +12,8 @@ pub struct FileIcons {
 
 impl FileIcons {
     pub fn get(cx: &App) -> Self {
-        let theme_settings = ThemeSettings::get_global(cx);
-
         Self {
-            icon_theme: theme_settings.active_icon_theme.clone(),
+            icon_theme: GlobalTheme::icon_theme(cx).clone(),
         }
     }
 
@@ -97,7 +94,7 @@ impl FileIcons {
                 .map(|icon_definition| icon_definition.path.clone())
         }
 
-        get_icon_for_type(&ThemeSettings::get_global(cx).active_icon_theme, typ).or_else(|| {
+        get_icon_for_type(GlobalTheme::icon_theme(cx), typ).or_else(|| {
             Self::default_icon_theme(cx).and_then(|icon_theme| get_icon_for_type(&icon_theme, typ))
         })
     }
@@ -122,20 +119,16 @@ impl FileIcons {
             }
         }
 
-        get_folder_icon(
-            &ThemeSettings::get_global(cx).active_icon_theme,
-            path,
-            expanded,
-        )
-        .or_else(|| {
-            Self::default_icon_theme(cx)
-                .and_then(|icon_theme| get_folder_icon(&icon_theme, path, expanded))
-        })
-        .or_else(|| {
-            // If we can't find a specific folder icon for the folder at the given path, fall back to the generic folder
-            // icon.
-            Self::get_generic_folder_icon(expanded, cx)
-        })
+        get_folder_icon(GlobalTheme::icon_theme(cx), path, expanded)
+            .or_else(|| {
+                Self::default_icon_theme(cx)
+                    .and_then(|icon_theme| get_folder_icon(&icon_theme, path, expanded))
+            })
+            .or_else(|| {
+                // If we can't find a specific folder icon for the folder at the given path, fall back to the generic folder
+                // icon.
+                Self::get_generic_folder_icon(expanded, cx)
+            })
     }
 
     fn get_generic_folder_icon(expanded: bool, cx: &App) -> Option<SharedString> {
@@ -150,12 +143,10 @@ impl FileIcons {
             }
         }
 
-        get_generic_folder_icon(&ThemeSettings::get_global(cx).active_icon_theme, expanded).or_else(
-            || {
-                Self::default_icon_theme(cx)
-                    .and_then(|icon_theme| get_generic_folder_icon(&icon_theme, expanded))
-            },
-        )
+        get_generic_folder_icon(GlobalTheme::icon_theme(cx), expanded).or_else(|| {
+            Self::default_icon_theme(cx)
+                .and_then(|icon_theme| get_generic_folder_icon(&icon_theme, expanded))
+        })
     }
 
     pub fn get_chevron_icon(expanded: bool, cx: &App) -> Option<SharedString> {
@@ -167,7 +158,7 @@ impl FileIcons {
             }
         }
 
-        get_chevron_icon(&ThemeSettings::get_global(cx).active_icon_theme, expanded).or_else(|| {
+        get_chevron_icon(GlobalTheme::icon_theme(cx), expanded).or_else(|| {
             Self::default_icon_theme(cx)
                 .and_then(|icon_theme| get_chevron_icon(&icon_theme, expanded))
         })

crates/git/src/git.rs 🔗

@@ -94,6 +94,8 @@ actions!(
         OpenModifiedFiles,
         /// Clones a repository.
         Clone,
+        /// Adds a file to .gitignore.
+        AddToGitignore,
     ]
 );
 

crates/git_hosting_providers/src/settings.rs 🔗

@@ -58,7 +58,7 @@ pub struct GitHostingProviderSettings {
 }
 
 impl Settings for GitHostingProviderSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         Self {
             git_hosting_providers: content
                 .project

crates/git_ui/src/commit_view.rs 🔗

@@ -43,8 +43,8 @@ struct CommitMetadataFile {
     worktree_id: WorktreeId,
 }
 
-const COMMIT_METADATA_NAMESPACE: u64 = 0;
-const FILE_NAMESPACE: u64 = 1;
+const COMMIT_METADATA_SORT_PREFIX: u64 = 0;
+const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
 
 impl CommitView {
     pub fn open(
@@ -145,7 +145,7 @@ impl CommitView {
             });
             multibuffer.update(cx, |multibuffer, cx| {
                 multibuffer.set_excerpts_for_path(
-                    PathKey::namespaced(COMMIT_METADATA_NAMESPACE, file.title.clone()),
+                    PathKey::with_sort_prefix(COMMIT_METADATA_SORT_PREFIX, file.title.clone()),
                     buffer.clone(),
                     vec![Point::zero()..buffer.read(cx).max_point()],
                     0,
@@ -193,7 +193,7 @@ impl CommitView {
                             .collect::<Vec<_>>();
                         let path = snapshot.file().unwrap().path().clone();
                         let _is_newly_added = multibuffer.set_excerpts_for_path(
-                            PathKey::namespaced(FILE_NAMESPACE, path),
+                            PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path),
                             buffer,
                             diff_hunk_ranges,
                             multibuffer_context_lines(cx),

crates/git_ui/src/file_diff_view.rs 🔗

@@ -360,7 +360,7 @@ mod tests {
     use editor::test::editor_test_context::assert_state_with_diff;
     use gpui::TestAppContext;
     use project::{FakeFs, Fs, Project};
-    use settings::{Settings, SettingsStore};
+    use settings::SettingsStore;
     use std::path::PathBuf;
     use unindent::unindent;
     use util::path;
@@ -374,7 +374,7 @@ mod tests {
             Project::init_settings(cx);
             workspace::init_settings(cx);
             editor::init_settings(cx);
-            theme::ThemeSettings::register(cx)
+            theme::init(theme::LoadThemes::JustBase, cx);
         });
     }
 

crates/git_ui/src/git_panel.rs 🔗

@@ -870,6 +870,77 @@ impl GitPanel {
         });
     }
 
+    fn add_to_gitignore(
+        &mut self,
+        _: &git::AddToGitignore,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        maybe!({
+            let list_entry = self.entries.get(self.selected_entry?)?.clone();
+            let entry = list_entry.status_entry()?.to_owned();
+
+            if !entry.status.is_created() {
+                return Some(());
+            }
+
+            let project = self.project.downgrade();
+            let repo_path = entry.repo_path;
+            let active_repository = self.active_repository.as_ref()?.downgrade();
+
+            cx.spawn(async move |_, cx| {
+                let file_path_str = repo_path.0.display(PathStyle::Posix);
+
+                let repo_root = active_repository.read_with(cx, |repository, _| {
+                    repository.snapshot().work_directory_abs_path
+                })?;
+
+                let gitignore_abs_path = repo_root.join(".gitignore");
+
+                let buffer = project
+                    .update(cx, |project, cx| {
+                        project.open_local_buffer(gitignore_abs_path, cx)
+                    })?
+                    .await?;
+
+                let mut should_save = false;
+                buffer.update(cx, |buffer, cx| {
+                    let existing_content = buffer.text();
+
+                    if existing_content
+                        .lines()
+                        .any(|line| line.trim() == file_path_str)
+                    {
+                        return;
+                    }
+
+                    let insert_position = existing_content.len();
+                    let new_entry = if existing_content.is_empty() {
+                        format!("{}\n", file_path_str)
+                    } else if existing_content.ends_with('\n') {
+                        format!("{}\n", file_path_str)
+                    } else {
+                        format!("\n{}\n", file_path_str)
+                    };
+
+                    buffer.edit([(insert_position..insert_position, new_entry)], None, cx);
+                    should_save = true;
+                })?;
+
+                if should_save {
+                    project
+                        .update(cx, |project, cx| project.save_buffer(buffer, cx))?
+                        .await?;
+                }
+
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+
+            Some(())
+        });
+    }
+
     fn revert_entry(
         &mut self,
         entry: &GitStatusEntry,
@@ -3817,10 +3888,17 @@ impl GitPanel {
             "Restore File"
         };
         let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
-            context_menu
+            let mut context_menu = context_menu
                 .context(self.focus_handle.clone())
                 .action(stage_title, ToggleStaged.boxed_clone())
-                .action(restore_title, git::RestoreFile::default().boxed_clone())
+                .action(restore_title, git::RestoreFile::default().boxed_clone());
+
+            if entry.status.is_created() {
+                context_menu =
+                    context_menu.action("Add to .gitignore", git::AddToGitignore.boxed_clone());
+            }
+
+            context_menu
                 .separator()
                 .action("Open Diff", Confirm.boxed_clone())
                 .action("Open File", SecondaryConfirm.boxed_clone())
@@ -4243,6 +4321,7 @@ impl Render for GitPanel {
                     .on_action(cx.listener(Self::unstage_selected))
                     .on_action(cx.listener(Self::restore_tracked_files))
                     .on_action(cx.listener(Self::revert_selected))
+                    .on_action(cx.listener(Self::add_to_gitignore))
                     .on_action(cx.listener(Self::clean_all))
                     .on_action(cx.listener(Self::generate_commit_message_action))
                     .on_action(cx.listener(Self::stash_all))
@@ -4894,6 +4973,7 @@ mod tests {
     use settings::SettingsStore;
     use theme::LoadThemes;
     use util::path;
+    use util::rel_path::rel_path;
 
     use super::*;
 
@@ -5516,6 +5596,68 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_open_diff(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.background_executor.clone());
+        fs.insert_tree(
+            path!("/project"),
+            json!({
+                ".git": {},
+                "tracked": "tracked\n",
+                "untracked": "\n",
+            }),
+        )
+        .await;
+
+        fs.set_head_and_index_for_repo(
+            path!("/project/.git").as_ref(),
+            &[("tracked", "old tracked\n".into())],
+        );
+
+        let project = Project::test(fs.clone(), [Path::new(path!("/project"))], 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, GitPanel::new).unwrap();
+
+        // Enable the `sort_by_path` setting and wait for entries to be updated,
+        // as there should no longer be separators between Tracked and Untracked
+        // files.
+        cx.update(|_window, cx| {
+            SettingsStore::update_global(cx, |store, cx| {
+                store.update_user_settings(cx, |settings| {
+                    settings.git_panel.get_or_insert_default().sort_by_path = Some(true);
+                })
+            });
+        });
+
+        cx.update_window_entity(&panel, |panel, _, _| {
+            std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
+        })
+        .await;
+
+        // Confirm that `Open Diff` still works for the untracked file, updating
+        // the Project Diff's active path.
+        panel.update_in(cx, |panel, window, cx| {
+            panel.selected_entry = Some(1);
+            panel.open_diff(&Confirm, window, cx);
+        });
+        cx.run_until_parked();
+
+        let _ = workspace.update(cx, |workspace, _window, cx| {
+            let active_path = workspace
+                .item_of_type::<ProjectDiff>(cx)
+                .expect("ProjectDiff should exist")
+                .read(cx)
+                .active_path(cx)
+                .expect("active_path should exist");
+
+            assert_eq!(active_path.path, rel_path("untracked").into_arc());
+        });
+    }
+
     fn assert_entry_paths(entries: &[GitListEntry], expected_paths: &[Option<&str>]) {
         assert_eq!(entries.len(), expected_paths.len());
         for (entry, expected_path) in entries.iter().zip(expected_paths) {

crates/git_ui/src/git_panel_settings.rs 🔗

@@ -43,7 +43,7 @@ impl ScrollbarVisibility for GitPanelSettings {
 }
 
 impl Settings for GitPanelSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let git_panel = content.git_panel.clone().unwrap();
         Self {
             button: git_panel.button.unwrap(),

crates/git_ui/src/project_diff.rs 🔗

@@ -16,7 +16,7 @@ use editor::{
 use futures::StreamExt;
 use git::{
     Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
-    repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
+    repository::{Branch, RepoPath, Upstream, UpstreamTracking, UpstreamTrackingStatus},
     status::FileStatus,
 };
 use gpui::{
@@ -27,7 +27,7 @@ use language::{Anchor, Buffer, Capability, OffsetRangeExt};
 use multi_buffer::{MultiBuffer, PathKey};
 use project::{
     Project, ProjectPath,
-    git_store::{GitStore, GitStoreEvent},
+    git_store::{GitStore, GitStoreEvent, Repository},
 };
 use settings::{Settings, SettingsStore};
 use std::any::{Any, TypeId};
@@ -73,9 +73,9 @@ struct DiffBuffer {
     file_status: FileStatus,
 }
 
-const CONFLICT_NAMESPACE: u64 = 1;
-const TRACKED_NAMESPACE: u64 = 2;
-const NEW_NAMESPACE: u64 = 3;
+const CONFLICT_SORT_PREFIX: u64 = 1;
+const TRACKED_SORT_PREFIX: u64 = 2;
+const NEW_SORT_PREFIX: u64 = 3;
 
 impl ProjectDiff {
     pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
@@ -234,16 +234,8 @@ impl ProjectDiff {
             return;
         };
         let repo = git_repo.read(cx);
-
-        let namespace = if repo.had_conflict_on_last_merge_head_change(&entry.repo_path) {
-            CONFLICT_NAMESPACE
-        } else if entry.status.is_created() {
-            NEW_NAMESPACE
-        } else {
-            TRACKED_NAMESPACE
-        };
-
-        let path_key = PathKey::namespaced(namespace, entry.repo_path.0);
+        let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.status, cx);
+        let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.0);
 
         self.move_to_path(path_key, window, cx)
     }
@@ -388,16 +380,8 @@ impl ProjectDiff {
                 else {
                     continue;
                 };
-                let namespace = if GitPanelSettings::get_global(cx).sort_by_path {
-                    TRACKED_NAMESPACE
-                } else if repo.had_conflict_on_last_merge_head_change(&entry.repo_path) {
-                    CONFLICT_NAMESPACE
-                } else if entry.status.is_created() {
-                    NEW_NAMESPACE
-                } else {
-                    TRACKED_NAMESPACE
-                };
-                let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone());
+                let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.status, cx);
+                let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.0.clone());
 
                 previous_paths.remove(&path_key);
                 let load_buffer = self
@@ -541,6 +525,18 @@ impl ProjectDiff {
     }
 }
 
+fn sort_prefix(repo: &Repository, repo_path: &RepoPath, status: FileStatus, cx: &App) -> u64 {
+    if GitPanelSettings::get_global(cx).sort_by_path {
+        TRACKED_SORT_PREFIX
+    } else if repo.had_conflict_on_last_merge_head_change(repo_path) {
+        CONFLICT_SORT_PREFIX
+    } else if status.is_created() {
+        NEW_SORT_PREFIX
+    } else {
+        TRACKED_SORT_PREFIX
+    }
+}
+
 impl EventEmitter<EditorEvent> for ProjectDiff {}
 
 impl Focusable for ProjectDiff {
@@ -1463,7 +1459,7 @@ mod tests {
 
         let editor = cx.update_window_entity(&diff, |diff, window, cx| {
             diff.move_to_path(
-                PathKey::namespaced(TRACKED_NAMESPACE, rel_path("foo").into_arc()),
+                PathKey::with_sort_prefix(TRACKED_SORT_PREFIX, rel_path("foo").into_arc()),
                 window,
                 cx,
             );
@@ -1484,7 +1480,7 @@ mod tests {
 
         let editor = cx.update_window_entity(&diff, |diff, window, cx| {
             diff.move_to_path(
-                PathKey::namespaced(TRACKED_NAMESPACE, rel_path("bar").into_arc()),
+                PathKey::with_sort_prefix(TRACKED_SORT_PREFIX, rel_path("bar").into_arc()),
                 window,
                 cx,
             );

crates/git_ui/src/repository_selector.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
 use itertools::Itertools;
-use picker::{Picker, PickerDelegate};
+use picker::{Picker, PickerDelegate, PickerEditorPosition};
 use project::{Project, git_store::Repository};
 use std::sync::Arc;
 use ui::{ListItem, ListItemSpacing, prelude::*};
@@ -36,11 +36,11 @@ impl RepositorySelector {
     ) -> Self {
         let git_store = project_handle.read(cx).git_store().clone();
         let repository_entries = git_store.update(cx, |git_store, _cx| {
-            git_store
-                .repositories()
-                .values()
-                .cloned()
-                .collect::<Vec<_>>()
+            let mut repos: Vec<_> = git_store.repositories().values().cloned().collect();
+
+            repos.sort_by_key(|a| a.read(_cx).display_name());
+
+            repos
         });
         let filtered_repositories = repository_entries.clone();
 
@@ -59,7 +59,7 @@ impl RepositorySelector {
         };
 
         let picker = cx.new(|cx| {
-            Picker::nonsearchable_uniform_list(delegate, window, cx)
+            Picker::uniform_list(delegate, window, cx)
                 .widest_item(widest_item_ix)
                 .max_height(Some(rems(20.).into()))
         });
@@ -158,6 +158,10 @@ impl PickerDelegate for RepositorySelectorDelegate {
         "Select a repository...".into()
     }
 
+    fn editor_position(&self) -> PickerEditorPosition {
+        PickerEditorPosition::End
+    }
+
     fn update_matches(
         &mut self,
         query: String,
@@ -166,25 +170,31 @@ impl PickerDelegate for RepositorySelectorDelegate {
     ) -> Task<()> {
         let all_repositories = self.repository_entries.clone();
 
+        let repo_names: Vec<(Entity<Repository>, String)> = all_repositories
+            .iter()
+            .map(|repo| (repo.clone(), repo.read(cx).display_name().to_lowercase()))
+            .collect();
+
         cx.spawn_in(window, async move |this, cx| {
             let filtered_repositories = cx
                 .background_spawn(async move {
                     if query.is_empty() {
                         all_repositories
                     } else {
-                        all_repositories
+                        let query_lower = query.to_lowercase();
+                        repo_names
                             .into_iter()
-                            .filter(|_repo_info| {
-                                // TODO: Implement repository filtering logic
-                                true
-                            })
+                            .filter(|(_, display_name)| display_name.contains(&query_lower))
+                            .map(|(repo, _)| repo)
                             .collect()
                     }
                 })
                 .await;
 
             this.update_in(cx, |this, window, cx| {
-                this.delegate.filtered_repositories = filtered_repositories;
+                let mut sorted_repositories = filtered_repositories;
+                sorted_repositories.sort_by_key(|a| a.read(cx).display_name());
+                this.delegate.filtered_repositories = sorted_repositories;
                 this.delegate.set_selected_index(0, window, cx);
                 cx.notify();
             })

crates/git_ui/src/text_diff_view.rs 🔗

@@ -450,7 +450,7 @@ mod tests {
     use gpui::{TestAppContext, VisualContext};
     use project::{FakeFs, Project};
     use serde_json::json;
-    use settings::{Settings, SettingsStore};
+    use settings::SettingsStore;
     use unindent::unindent;
     use util::{path, test::marked_text_ranges};
 
@@ -462,7 +462,7 @@ mod tests {
             Project::init_settings(cx);
             workspace::init_settings(cx);
             editor::init_settings(cx);
-            theme::ThemeSettings::register(cx)
+            theme::init(theme::LoadThemes::JustBase, cx);
         });
     }
 

crates/go_to_line/src/cursor_position.rs 🔗

@@ -113,7 +113,9 @@ impl CursorPosition {
                                 let mut last_selection = None::<Selection<Point>>;
                                 let snapshot = editor.buffer().read(cx).snapshot(cx);
                                 if snapshot.excerpts().count() > 0 {
-                                    for selection in editor.selections.all_adjusted(cx) {
+                                    for selection in
+                                        editor.selections.all_adjusted_with_snapshot(&snapshot)
+                                    {
                                         let selection_summary = snapshot
                                             .text_summary_for_range::<text::TextSummary, _>(
                                                 selection.start..selection.end,
@@ -304,7 +306,7 @@ impl From<settings::LineIndicatorFormat> for LineIndicatorFormat {
 }
 
 impl Settings for LineIndicatorFormat {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         content.line_indicator_format.unwrap().into()
     }
 }

crates/gpui/Cargo.toml 🔗

@@ -1,6 +1,6 @@
 [package]
 name = "gpui"
-version = "0.1.0"
+version = "0.2.0"
 edition.workspace = true
 authors = ["Nathan Sobo <nathan@zed.dev>"]
 description = "Zed's GPU-accelerated UI framework"

crates/gpui/examples/animation.rs 🔗

@@ -3,8 +3,8 @@ use std::time::Duration;
 use anyhow::Result;
 use gpui::{
     Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Context, SharedString,
-    Transformation, Window, WindowBounds, WindowOptions, black, bounce, div, ease_in_out,
-    percentage, prelude::*, px, rgb, size, svg,
+    Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, percentage,
+    prelude::*, px, size, svg,
 };
 
 struct Assets {}
@@ -37,37 +37,66 @@ struct AnimationExample {}
 
 impl Render for AnimationExample {
     fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        div().flex().flex_col().size_full().justify_around().child(
-            div().flex().flex_row().w_full().justify_around().child(
+        div()
+            .flex()
+            .flex_col()
+            .size_full()
+            .bg(gpui::white())
+            .text_color(gpui::black())
+            .justify_around()
+            .child(
                 div()
                     .flex()
-                    .bg(rgb(0x2e7d32))
-                    .size(px(300.0))
-                    .justify_center()
-                    .items_center()
-                    .shadow_lg()
-                    .text_xl()
-                    .text_color(black())
-                    .child("hello")
+                    .flex_col()
+                    .size_full()
+                    .justify_around()
                     .child(
-                        svg()
-                            .size_8()
-                            .path(ARROW_CIRCLE_SVG)
-                            .text_color(black())
-                            .with_animation(
-                                "image_circle",
-                                Animation::new(Duration::from_secs(2))
-                                    .repeat()
-                                    .with_easing(bounce(ease_in_out)),
-                                |svg, delta| {
-                                    svg.with_transformation(Transformation::rotate(percentage(
-                                        delta,
-                                    )))
-                                },
+                        div()
+                            .id("content")
+                            .flex()
+                            .flex_col()
+                            .h(px(150.))
+                            .overflow_y_scroll()
+                            .w_full()
+                            .flex_1()
+                            .justify_center()
+                            .items_center()
+                            .text_xl()
+                            .gap_4()
+                            .child("Hello Animation")
+                            .child(
+                                svg()
+                                    .size_20()
+                                    .overflow_hidden()
+                                    .path(ARROW_CIRCLE_SVG)
+                                    .text_color(gpui::black())
+                                    .with_animation(
+                                        "image_circle",
+                                        Animation::new(Duration::from_secs(2))
+                                            .repeat()
+                                            .with_easing(bounce(ease_in_out)),
+                                        |svg, delta| {
+                                            svg.with_transformation(Transformation::rotate(
+                                                percentage(delta),
+                                            ))
+                                        },
+                                    ),
                             ),
+                    )
+                    .child(
+                        div()
+                            .flex()
+                            .h(px(64.))
+                            .w_full()
+                            .p_2()
+                            .justify_center()
+                            .items_center()
+                            .border_t_1()
+                            .border_color(gpui::black().opacity(0.1))
+                            .bg(gpui::black().opacity(0.05))
+                            .child("Other Panel"),
                     ),
-            ),
-        )
+            )
     }
 }
 

crates/gpui/src/platform.rs 🔗

@@ -1213,6 +1213,11 @@ impl WindowBounds {
             WindowBounds::Fullscreen(bounds) => *bounds,
         }
     }
+
+    /// Creates a new window bounds that centers the window on the screen.
+    pub fn centered(size: Size<Pixels>, cx: &App) -> Self {
+        WindowBounds::Windowed(Bounds::centered(None, size, cx))
+    }
 }
 
 impl Default for WindowOptions {
@@ -1263,6 +1268,9 @@ pub enum WindowKind {
     /// A window that appears above all other windows, usually used for alerts or popups
     /// use sparingly!
     PopUp,
+
+    /// A floating window that appears on top of its parent window
+    Floating,
 }
 
 /// The appearance of the window, as defined by the operating system.

crates/gpui/src/platform/blade/shaders.wgsl 🔗

@@ -172,6 +172,12 @@ fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds:
     return distance_from_clip_rect_impl(position, clip_bounds);
 }
 
+fn distance_from_clip_rect_transformed(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds, transform: TransformationMatrix) -> vec4<f32> {
+    let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
+    let transformed = transpose(transform.rotation_scale) * position + transform.translation;
+    return distance_from_clip_rect_impl(transformed, clip_bounds);
+}
+
 // https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl
 fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
     let cutoff = srgb < vec3<f32>(0.04045);
@@ -1150,7 +1156,7 @@ fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index
 
     out.tile_position = to_tile_position(unit_vertex, sprite.tile);
     out.color = hsla_to_rgba(sprite.color);
-    out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
+    out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
     return out;
 }
 

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -695,6 +695,8 @@ impl LinuxClient for WaylandClient {
     ) -> anyhow::Result<Box<dyn PlatformWindow>> {
         let mut state = self.0.borrow_mut();
 
+        let parent = state.keyboard_focused_window.as_ref().map(|w| w.toplevel());
+
         let (window, surface_id) = WaylandWindow::new(
             handle,
             state.globals.clone(),
@@ -702,6 +704,7 @@ impl LinuxClient for WaylandClient {
             WaylandClientStatePtr(Rc::downgrade(&self.0)),
             params,
             state.common.appearance,
+            parent,
         )?;
         state.windows.insert(surface_id, window.0.clone());
 

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -14,14 +14,16 @@ use raw_window_handle as rwh;
 use wayland_backend::client::ObjectId;
 use wayland_client::WEnum;
 use wayland_client::{Proxy, protocol::wl_surface};
-use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1;
 use wayland_protocols::wp::viewporter::client::wp_viewport;
 use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1;
 use wayland_protocols::xdg::shell::client::xdg_surface;
 use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
+use wayland_protocols::{
+    wp::fractional_scale::v1::client::wp_fractional_scale_v1,
+    xdg::shell::client::xdg_toplevel::XdgToplevel,
+};
 use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
 
-use crate::scene::Scene;
 use crate::{
     AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
     PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
@@ -36,6 +38,7 @@ use crate::{
         linux::wayland::{display::WaylandDisplay, serial::SerialKind},
     },
 };
+use crate::{WindowKind, scene::Scene};
 
 #[derive(Default)]
 pub(crate) struct Callbacks {
@@ -276,6 +279,7 @@ impl WaylandWindow {
         client: WaylandClientStatePtr,
         params: WindowParams,
         appearance: WindowAppearance,
+        parent: Option<XdgToplevel>,
     ) -> anyhow::Result<(Self, ObjectId)> {
         let surface = globals.compositor.create_surface(&globals.qh, ());
         let xdg_surface = globals
@@ -283,6 +287,10 @@ impl WaylandWindow {
             .get_xdg_surface(&surface, &globals.qh, surface.id());
         let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
 
+        if params.kind == WindowKind::Floating {
+            toplevel.set_parent(parent.as_ref());
+        }
+
         if let Some(size) = params.window_min_size {
             toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
         }
@@ -337,6 +345,10 @@ impl WaylandWindowStatePtr {
         self.state.borrow().surface.clone()
     }
 
+    pub fn toplevel(&self) -> xdg_toplevel::XdgToplevel {
+        self.state.borrow().toplevel.clone()
+    }
+
     pub fn ptr_eq(&self, other: &Self) -> bool {
         Rc::ptr_eq(&self.state, &other.state)
     }

crates/gpui/src/platform/linux/x11/client.rs 🔗

@@ -1448,6 +1448,10 @@ impl LinuxClient for X11Client {
         params: WindowParams,
     ) -> anyhow::Result<Box<dyn PlatformWindow>> {
         let mut state = self.0.borrow_mut();
+        let parent_window = state
+            .keyboard_focused_window
+            .and_then(|focused_window| state.windows.get(&focused_window))
+            .map(|window| window.window.x_window);
         let x_window = state
             .xcb_connection
             .generate_id()
@@ -1466,6 +1470,7 @@ impl LinuxClient for X11Client {
             &state.atoms,
             state.scale_factor,
             state.common.appearance,
+            parent_window,
         )?;
         check_reply(
             || "Failed to set XdndAware property",

crates/gpui/src/platform/linux/x11/window.rs 🔗

@@ -57,6 +57,7 @@ x11rb::atom_manager! {
         WM_PROTOCOLS,
         WM_DELETE_WINDOW,
         WM_CHANGE_STATE,
+        WM_TRANSIENT_FOR,
         _NET_WM_PID,
         _NET_WM_NAME,
         _NET_WM_STATE,
@@ -72,6 +73,7 @@ x11rb::atom_manager! {
         _NET_WM_MOVERESIZE,
         _NET_WM_WINDOW_TYPE,
         _NET_WM_WINDOW_TYPE_NOTIFICATION,
+        _NET_WM_WINDOW_TYPE_DIALOG,
         _NET_WM_SYNC,
         _NET_SUPPORTED,
         _MOTIF_WM_HINTS,
@@ -392,6 +394,7 @@ impl X11WindowState {
         atoms: &XcbAtoms,
         scale_factor: f32,
         appearance: WindowAppearance,
+        parent_window: Option<xproto::Window>,
     ) -> anyhow::Result<Self> {
         let x_screen_index = params
             .display_id
@@ -529,6 +532,7 @@ impl X11WindowState {
                     ),
                 )?;
             }
+
             if params.kind == WindowKind::PopUp {
                 check_reply(
                     || "X11 ChangeProperty32 setting window type for pop-up failed.",
@@ -542,6 +546,38 @@ impl X11WindowState {
                 )?;
             }
 
+            if params.kind == WindowKind::Floating {
+                if let Some(parent_window) = parent_window {
+                    // WM_TRANSIENT_FOR hint indicating the main application window. For floating windows, we set
+                    // a parent window (WM_TRANSIENT_FOR) such that the window manager knows where to
+                    // place the floating window in relation to the main window.
+                    // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
+                    check_reply(
+                        || "X11 ChangeProperty32 setting WM_TRANSIENT_FOR for floating window failed.",
+                        xcb.change_property32(
+                            xproto::PropMode::REPLACE,
+                            x_window,
+                            atoms.WM_TRANSIENT_FOR,
+                            xproto::AtomEnum::WINDOW,
+                            &[parent_window],
+                        ),
+                    )?;
+                }
+
+                // _NET_WM_WINDOW_TYPE_DIALOG indicates that this is a dialog (floating) window
+                // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html
+                check_reply(
+                    || "X11 ChangeProperty32 setting window type for floating window failed.",
+                    xcb.change_property32(
+                        xproto::PropMode::REPLACE,
+                        x_window,
+                        atoms._NET_WM_WINDOW_TYPE,
+                        xproto::AtomEnum::ATOM,
+                        &[atoms._NET_WM_WINDOW_TYPE_DIALOG],
+                    ),
+                )?;
+            }
+
             check_reply(
                 || "X11 ChangeProperty32 setting protocols failed.",
                 xcb.change_property32(
@@ -737,6 +773,7 @@ impl X11Window {
         atoms: &XcbAtoms,
         scale_factor: f32,
         appearance: WindowAppearance,
+        parent_window: Option<xproto::Window>,
     ) -> anyhow::Result<Self> {
         let ptr = X11WindowStatePtr {
             state: Rc::new(RefCell::new(X11WindowState::new(
@@ -752,6 +789,7 @@ impl X11Window {
                 atoms,
                 scale_factor,
                 appearance,
+                parent_window,
             )?)),
             callbacks: Rc::new(RefCell::new(Callbacks::default())),
             xcb: xcb.clone(),

crates/gpui/src/platform/mac/shaders.metal 🔗

@@ -18,6 +18,8 @@ float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
                         constant Size_DevicePixels *atlas_size);
 float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds,
                                Bounds_ScaledPixels clip_bounds);
+float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds,
+                               Bounds_ScaledPixels clip_bounds, TransformationMatrix transformation);
 float corner_dash_velocity(float dv1, float dv2);
 float dash_alpha(float t, float period, float length, float dash_velocity,
                  float antialias_threshold);
@@ -599,13 +601,14 @@ struct MonochromeSpriteVertexOutput {
   float4 position [[position]];
   float2 tile_position;
   float4 color [[flat]];
-  float clip_distance [[clip_distance]][4];
+  float4 clip_distance;
 };
 
 struct MonochromeSpriteFragmentInput {
   float4 position [[position]];
   float2 tile_position;
   float4 color [[flat]];
+  float4 clip_distance;
 };
 
 vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex(
@@ -620,8 +623,8 @@ vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex(
   MonochromeSprite sprite = sprites[sprite_id];
   float4 device_position =
       to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation, viewport_size);
-  float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds,
-                                                 sprite.content_mask.bounds);
+  float4 clip_distance = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds,
+                                                 sprite.content_mask.bounds, sprite.transformation);
   float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
   float4 color = hsla_to_rgba(sprite.color);
   return MonochromeSpriteVertexOutput{
@@ -635,6 +638,10 @@ fragment float4 monochrome_sprite_fragment(
     MonochromeSpriteFragmentInput input [[stage_in]],
     constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
     texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) {
+  if (any(input.clip_distance < float4(0.0))) {
+    return float4(0.0);
+  }
+
   constexpr sampler atlas_texture_sampler(mag_filter::linear,
                                           min_filter::linear);
   float4 sample =
@@ -1096,6 +1103,23 @@ float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds,
                 clip_bounds.origin.y + clip_bounds.size.height - position.y);
 }
 
+float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds,
+                               Bounds_ScaledPixels clip_bounds, TransformationMatrix transformation) {
+  float2 position =
+      unit_vertex * float2(bounds.size.width, bounds.size.height) +
+      float2(bounds.origin.x, bounds.origin.y);
+  float2 transformed_position = float2(0, 0);
+  transformed_position[0] = position[0] * transformation.rotation_scale[0][0] + position[1] * transformation.rotation_scale[0][1];
+  transformed_position[1] = position[0] * transformation.rotation_scale[1][0] + position[1] * transformation.rotation_scale[1][1];
+  transformed_position[0] += transformation.translation[0];
+  transformed_position[1] += transformation.translation[1];
+
+  return float4(transformed_position.x - clip_bounds.origin.x,
+                clip_bounds.origin.x + clip_bounds.size.width - transformed_position.x,
+                transformed_position.y - clip_bounds.origin.y,
+                clip_bounds.origin.y + clip_bounds.size.height - transformed_position.y);
+}
+
 float4 over(float4 below, float4 above) {
   float4 result;
   float alpha = above.a + below.a * (1.0 - above.a);

crates/gpui/src/platform/mac/window.rs 🔗

@@ -618,7 +618,7 @@ impl MacWindow {
             }
 
             let native_window: id = match kind {
-                WindowKind::Normal => msg_send![WINDOW_CLASS, alloc],
+                WindowKind::Normal | WindowKind::Floating => msg_send![WINDOW_CLASS, alloc],
                 WindowKind::PopUp => {
                     style_mask |= NSWindowStyleMaskNonactivatingPanel;
                     msg_send![PANEL_CLASS, alloc]
@@ -776,7 +776,7 @@ impl MacWindow {
             native_window.makeFirstResponder_(native_view);
 
             match kind {
-                WindowKind::Normal => {
+                WindowKind::Normal | WindowKind::Floating => {
                     native_window.setLevel_(NSNormalWindowLevel);
                     native_window.setAcceptsMouseMovedEvents_(YES);
 

crates/gpui/src/platform/windows/shaders.hlsl 🔗

@@ -107,6 +107,12 @@ float4 distance_from_clip_rect(float2 unit_vertex, Bounds bounds, Bounds clip_bo
     return distance_from_clip_rect_impl(position, clip_bounds);
 }
 
+float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds bounds, Bounds clip_bounds, TransformationMatrix transformation) {
+    float2 position = unit_vertex * bounds.size + bounds.origin;
+    float2 transformed = mul(position, transformation.rotation_scale) + transformation.translation;
+    return distance_from_clip_rect_impl(transformed, clip_bounds);
+}
+
 // Convert linear RGB to sRGB
 float3 linear_to_srgb(float3 color) {
     return pow(color, float3(2.2, 2.2, 2.2));
@@ -1088,7 +1094,7 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI
     MonochromeSprite sprite = mono_sprites[sprite_id];
     float4 device_position =
         to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
-    float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
+    float4 clip_distance = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
     float2 tile_position = to_tile_position(unit_vertex, sprite.tile);
     float4 color = hsla_to_rgba(sprite.color);
 

crates/gpui/src/svg_renderer.rs 🔗

@@ -54,7 +54,10 @@ impl SvgRenderer {
         }
     }
 
-    pub(crate) fn render(&self, params: &RenderSvgParams) -> Result<Option<Vec<u8>>> {
+    pub(crate) fn render(
+        &self,
+        params: &RenderSvgParams,
+    ) -> Result<Option<(Size<DevicePixels>, Vec<u8>)>> {
         anyhow::ensure!(!params.size.is_zero(), "can't render at a zero size");
 
         // Load the tree.
@@ -65,30 +68,33 @@ impl SvgRenderer {
         let pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?;
 
         // Convert the pixmap's pixels into an alpha mask.
+        let size = Size::new(
+            DevicePixels(pixmap.width() as i32),
+            DevicePixels(pixmap.height() as i32),
+        );
         let alpha_mask = pixmap
             .pixels()
             .iter()
             .map(|p| p.alpha())
             .collect::<Vec<_>>();
-        Ok(Some(alpha_mask))
+        Ok(Some((size, alpha_mask)))
     }
 
     pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
         let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?;
-
-        let size = match size {
-            SvgSize::Size(size) => size,
-            SvgSize::ScaleFactor(scale) => crate::size(
-                DevicePixels((tree.size().width() * scale) as i32),
-                DevicePixels((tree.size().height() * scale) as i32),
-            ),
+        let svg_size = tree.size();
+        let scale = match size {
+            SvgSize::Size(size) => size.width.0 as f32 / svg_size.width(),
+            SvgSize::ScaleFactor(scale) => scale,
         };
 
         // Render the SVG to a pixmap with the specified width and height.
-        let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width.into(), size.height.into())
-            .ok_or(usvg::Error::InvalidSize)?;
+        let mut pixmap = resvg::tiny_skia::Pixmap::new(
+            (svg_size.width() * scale) as u32,
+            (svg_size.height() * scale) as u32,
+        )
+        .ok_or(usvg::Error::InvalidSize)?;
 
-        let scale = size.width.0 as f32 / tree.size().width();
         let transform = resvg::tiny_skia::Transform::from_scale(scale, scale);
 
         resvg::render(&tree, transform, &mut pixmap.as_mut());

crates/gpui/src/window.rs 🔗

@@ -58,7 +58,7 @@ mod prompts;
 use crate::util::atomic_incr_if_not_zero;
 pub use prompts::*;
 
-pub(crate) const DEFAULT_WINDOW_SIZE: Size<Pixels> = size(px(1024.), px(700.));
+pub(crate) const DEFAULT_WINDOW_SIZE: Size<Pixels> = size(px(1536.), px(864.));
 
 /// Represents the two different phases when dispatching events.
 #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
@@ -3082,22 +3082,31 @@ impl Window {
         let Some(tile) =
             self.sprite_atlas
                 .get_or_insert_with(&params.clone().into(), &mut || {
-                    let Some(bytes) = cx.svg_renderer.render(&params)? else {
+                    let Some((size, bytes)) = cx.svg_renderer.render(&params)? else {
                         return Ok(None);
                     };
-                    Ok(Some((params.size, Cow::Owned(bytes))))
+                    Ok(Some((size, Cow::Owned(bytes))))
                 })?
         else {
             return Ok(());
         };
         let content_mask = self.content_mask().scale(scale_factor);
+        let svg_bounds = Bounds {
+            origin: bounds.center()
+                - Point::new(
+                    ScaledPixels(tile.bounds.size.width.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.),
+                    ScaledPixels(tile.bounds.size.height.0 as f32 / SMOOTH_SVG_SCALE_FACTOR / 2.),
+                ),
+            size: tile
+                .bounds
+                .size
+                .map(|value| ScaledPixels(value.0 as f32 / SMOOTH_SVG_SCALE_FACTOR)),
+        };
 
         self.next_frame.scene.insert_primitive(MonochromeSprite {
             order: 0,
             pad: 0,
-            bounds: bounds
-                .map_origin(|origin| origin.floor())
-                .map_size(|size| size.ceil()),
+            bounds: svg_bounds,
             content_mask,
             color: color.opacity(element_opacity),
             tile,
@@ -4633,6 +4642,14 @@ pub struct WindowHandle<V> {
     state_type: PhantomData<V>,
 }
 
+impl<V> Debug for WindowHandle<V> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("WindowHandle")
+            .field("any_handle", &self.any_handle.id.as_u64())
+            .finish()
+    }
+}
+
 impl<V: 'static + Render> WindowHandle<V> {
     /// Creates a new handle from a window ID.
     /// This does not check if the root type of the window is `V`.

crates/image_viewer/src/image_viewer_settings.rs 🔗

@@ -1,4 +1,3 @@
-use gpui::App;
 pub use settings::ImageFileSizeUnit;
 use settings::Settings;
 
@@ -12,7 +11,7 @@ pub struct ImageViewerSettings {
 }
 
 impl Settings for ImageViewerSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         Self {
             unit: content.image_viewer.clone().unwrap().unit.unwrap(),
         }

crates/inspector_ui/Cargo.toml 🔗

@@ -22,6 +22,7 @@ project.workspace = true
 serde_json.workspace = true
 serde_json_lenient.workspace = true
 theme.workspace = true
+title_bar.workspace = true
 ui.workspace = true
 util.workspace = true
 util_macros.workspace = true

crates/inspector_ui/src/div_inspector.rs 🔗

@@ -576,7 +576,12 @@ fn render_layout_state(inspector_state: &DivInspectorState, cx: &App) -> Div {
         .child(
             div()
                 .text_ui(cx)
-                .child(format!("Bounds: {}", inspector_state.bounds)),
+                .child(format!(
+                    "Bounds: ⌜{} - {}⌟",
+                    inspector_state.bounds.origin,
+                    inspector_state.bounds.bottom_right()
+                ))
+                .child(format!("Size: {}", inspector_state.bounds.size)),
         )
         .child(
             div()

crates/inspector_ui/src/inspector.rs 🔗

@@ -1,6 +1,7 @@
 use anyhow::{Context as _, anyhow};
 use gpui::{App, DivInspectorState, Inspector, InspectorElementId, IntoElement, Window};
 use std::{cell::OnceCell, path::Path, sync::Arc};
+use title_bar::platform_title_bar::PlatformTitleBar;
 use ui::{Label, Tooltip, prelude::*};
 use util::{ResultExt as _, command::new_smol_command};
 use workspace::AppState;
@@ -56,6 +57,8 @@ fn render_inspector(
     let ui_font = theme::setup_ui_font(window, cx);
     let colors = cx.theme().colors();
     let inspector_id = inspector.active_element_id();
+    let toolbar_height = PlatformTitleBar::height(window);
+
     v_flex()
         .size_full()
         .bg(colors.panel_background)
@@ -65,7 +68,11 @@ fn render_inspector(
         .border_color(colors.border)
         .child(
             h_flex()
-                .p_2()
+                .justify_between()
+                .pr_2()
+                .pl_1()
+                .mt_px()
+                .h(toolbar_height)
                 .border_b_1()
                 .border_color(colors.border_variant)
                 .child(
@@ -78,18 +85,14 @@ fn render_inspector(
                             window.refresh();
                         })),
                 )
-                .child(
-                    h_flex()
-                        .w_full()
-                        .justify_end()
-                        .child(Label::new("GPUI Inspector").size(LabelSize::Large)),
-                ),
+                .child(h_flex().justify_end().child(Label::new("GPUI Inspector"))),
         )
         .child(
             v_flex()
                 .id("gpui-inspector-content")
                 .overflow_y_scroll()
-                .p_2()
+                .px_2()
+                .py_0p5()
                 .gap_2()
                 .when_some(inspector_id, |this, inspector_id| {
                     this.child(render_inspector_id(inspector_id, cx))
@@ -110,15 +113,19 @@ fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
         .unwrap_or(source_location_string);
 
     v_flex()
-        .child(Label::new("Element ID").size(LabelSize::Large))
         .child(
-            div()
-                .id("instance-id")
-                .text_ui(cx)
-                .tooltip(Tooltip::text(
-                    "Disambiguates elements from the same source location",
-                ))
-                .child(format!("Instance {}", inspector_id.instance_id)),
+            h_flex()
+                .justify_between()
+                .child(Label::new("Element ID").size(LabelSize::Large))
+                .child(
+                    div()
+                        .id("instance-id")
+                        .text_ui(cx)
+                        .tooltip(Tooltip::text(
+                            "Disambiguates elements from the same source location",
+                        ))
+                        .child(format!("Instance {}", inspector_id.instance_id)),
+                ),
         )
         .child(
             div()
@@ -126,8 +133,10 @@ fn render_inspector_id(inspector_id: &InspectorElementId, cx: &App) -> Div {
                 .text_ui(cx)
                 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
                 .underline()
+                .font_buffer(cx)
+                .text_xs()
                 .child(source_location_string)
-                .tooltip(Tooltip::text("Click to open by running zed cli"))
+                .tooltip(Tooltip::text("Click to open by running Zed CLI"))
                 .on_click(move |_, _window, cx| {
                     cx.background_spawn(open_zed_source_location(source_location))
                         .detach_and_log_err(cx);

crates/journal/src/journal.rs 🔗

@@ -33,7 +33,7 @@ pub struct JournalSettings {
 }
 
 impl settings::Settings for JournalSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let journal = content.journal.clone().unwrap();
 
         Self {

crates/keymap_editor/src/keymap_editor.rs 🔗

@@ -26,9 +26,9 @@ use project::{CompletionDisplayOptions, Project};
 use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets};
 use ui::{
     ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator,
-    Modal, ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString,
-    Styled as _, Table, TableColumnWidths, TableInteractionState, TableResizeBehavior, Tooltip,
-    Window, prelude::*, right_click_menu,
+    Modal, ModalFooter, ModalHeader, ParentElement as _, PopoverMenu, Render, Section,
+    SharedString, Styled as _, Table, TableColumnWidths, TableInteractionState,
+    TableResizeBehavior, Tooltip, Window, prelude::*,
 };
 use ui_input::SingleLineInput;
 use util::ResultExt;
@@ -1663,56 +1663,61 @@ impl Render for KeymapEditor {
                                             }),
                                     )
                                     .child(
-                                        div()
-                                            .ml_1()
+                                        h_flex()
+                                            .w_full()
                                             .pl_2()
-                                            .border_l_1()
-                                            .border_color(cx.theme().colors().border_variant)
+                                            .gap_1()
+                                            .justify_end()
                                             .child(
-                                                right_click_menu("open-keymap-menu")
-                                                    .menu(|window, cx| {
-                                                        ContextMenu::build(window, cx, |menu, _, _| {
-                                                            menu.header("Open Keymap JSON")
-                                                                .action(
-                                                                    "User",
-                                                                    zed_actions::OpenKeymap.boxed_clone(),
-                                                                )
+                                                PopoverMenu::new("open-keymap-menu")
+                                                    .menu(move |window, cx| {
+                                                        Some(ContextMenu::build(window, cx, |menu, _, _| {
+                                                            menu.header("View Default...")
                                                                 .action(
-                                                                    "Zed Default",
+                                                                    "Zed Key Bindings",
                                                                     zed_actions::OpenDefaultKeymap
                                                                         .boxed_clone(),
                                                                 )
                                                                 .action(
-                                                                    "Vim Default",
+                                                                    "Vim Bindings",
                                                                     vim::OpenDefaultKeymap.boxed_clone(),
                                                                 )
-                                                        })
+                                                        }))
+                                                    })
+                                                    .anchor(gpui::Corner::TopRight)
+                                                    .offset(gpui::Point {
+                                                        x: px(0.0),
+                                                        y: px(2.0),
                                                     })
-                                                    .anchor(gpui::Corner::TopLeft)
-                                                    .trigger(|open, _, _| {
+                                                    .trigger_with_tooltip(
                                                         IconButton::new(
                                                             "OpenKeymapJsonButton",
-                                                            IconName::Json,
+                                                            IconName::Ellipsis,
                                                         )
-                                                        .icon_size(IconSize::Small)
-                                                        .when(!open, |this| {
-                                                            this.tooltip(move |window, cx| {
-                                                                Tooltip::with_meta(
-                                                                    "Open keymap.json",
-                                                                    Some(&zed_actions::OpenKeymap),
-                                                                    "Right click to view more options",
+                                                        .icon_size(IconSize::Small),
+                                                        {
+                                                            let focus_handle = focus_handle.clone();
+                                                            move |window, cx| {
+                                                                Tooltip::for_action_in(
+                                                                    "View Default...",
+                                                                    &zed_actions::OpenKeymap,
+                                                                    &focus_handle,
                                                                     window,
                                                                     cx,
                                                                 )
-                                                            })
-                                                        })
-                                                        .on_click(|_, window, cx| {
-                                                            window.dispatch_action(
-                                                                zed_actions::OpenKeymap.boxed_clone(),
-                                                                cx,
-                                                            );
-                                                        })
-                                                    }),
+                                                            }
+                                                        },
+                                                    ),
+                                            )
+                                            .child(
+                                                Button::new("edit-in-json", "Edit in keymap.json")
+                                                    .style(ButtonStyle::Outlined)
+                                                    .on_click(|_, window, cx| {
+                                                        window.dispatch_action(
+                                                            zed_actions::OpenKeymap.boxed_clone(),
+                                                            cx,
+                                                        );
+                                                    })
                                             ),
                                     )
                             ),

crates/language/src/language.rs 🔗

@@ -777,6 +777,15 @@ pub struct LanguageConfig {
     /// A list of preferred debuggers for this language.
     #[serde(default)]
     pub debuggers: IndexSet<SharedString>,
+    /// A list of import namespace segments that aren't expected to appear in file paths. For
+    /// example, "super" and "crate" in Rust.
+    #[serde(default)]
+    pub ignored_import_segments: HashSet<Arc<str>>,
+    /// Regular expression that matches substrings to omit from import paths, to make the paths more
+    /// similar to how they are specified when imported. For example, "/mod\.rs$" or "/__init__\.py$".
+    #[serde(default, deserialize_with = "deserialize_regex")]
+    #[schemars(schema_with = "regex_json_schema")]
+    pub import_path_strip_regex: Option<Regex>,
 }
 
 #[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
@@ -973,6 +982,8 @@ impl Default for LanguageConfig {
             completion_query_characters: Default::default(),
             linked_edit_characters: Default::default(),
             debuggers: Default::default(),
+            ignored_import_segments: Default::default(),
+            import_path_strip_regex: None,
         }
     }
 }
@@ -1162,6 +1173,7 @@ pub struct Grammar {
     pub(crate) injection_config: Option<InjectionConfig>,
     pub(crate) override_config: Option<OverrideConfig>,
     pub(crate) debug_variables_config: Option<DebugVariablesConfig>,
+    pub(crate) imports_config: Option<ImportsConfig>,
     pub(crate) highlight_map: Mutex<HighlightMap>,
 }
 
@@ -1314,6 +1326,17 @@ pub struct DebugVariablesConfig {
     pub objects_by_capture_ix: Vec<(u32, DebuggerTextObject)>,
 }
 
+pub struct ImportsConfig {
+    pub query: Query,
+    pub import_ix: u32,
+    pub name_ix: Option<u32>,
+    pub namespace_ix: Option<u32>,
+    pub source_ix: Option<u32>,
+    pub list_ix: Option<u32>,
+    pub wildcard_ix: Option<u32>,
+    pub alias_ix: Option<u32>,
+}
+
 impl Language {
     pub fn new(config: LanguageConfig, ts_language: Option<tree_sitter::Language>) -> Self {
         Self::new_with_id(LanguageId::new(), config, ts_language)
@@ -1346,6 +1369,7 @@ impl Language {
                     runnable_config: None,
                     error_query: Query::new(&ts_language, "(ERROR) @error").ok(),
                     debug_variables_config: None,
+                    imports_config: None,
                     ts_language,
                     highlight_map: Default::default(),
                 })
@@ -1427,6 +1451,11 @@ impl Language {
                 .with_debug_variables_query(query.as_ref())
                 .context("Error loading debug variables query")?;
         }
+        if let Some(query) = queries.imports {
+            self = self
+                .with_imports_query(query.as_ref())
+                .context("Error loading imports query")?;
+        }
         Ok(self)
     }
 
@@ -1595,6 +1624,45 @@ impl Language {
         Ok(self)
     }
 
+    pub fn with_imports_query(mut self, source: &str) -> Result<Self> {
+        let query = Query::new(&self.expect_grammar()?.ts_language, source)?;
+
+        let mut import_ix = 0;
+        let mut name_ix = None;
+        let mut namespace_ix = None;
+        let mut source_ix = None;
+        let mut list_ix = None;
+        let mut wildcard_ix = None;
+        let mut alias_ix = None;
+        if populate_capture_indices(
+            &query,
+            &self.config.name,
+            "imports",
+            &[],
+            &mut [
+                Capture::Required("import", &mut import_ix),
+                Capture::Optional("name", &mut name_ix),
+                Capture::Optional("namespace", &mut namespace_ix),
+                Capture::Optional("source", &mut source_ix),
+                Capture::Optional("list", &mut list_ix),
+                Capture::Optional("wildcard", &mut wildcard_ix),
+                Capture::Optional("alias", &mut alias_ix),
+            ],
+        ) {
+            self.grammar_mut()?.imports_config = Some(ImportsConfig {
+                query,
+                import_ix,
+                name_ix,
+                namespace_ix,
+                source_ix,
+                list_ix,
+                wildcard_ix,
+                alias_ix,
+            });
+        }
+        return Ok(self);
+    }
+
     pub fn with_brackets_query(mut self, source: &str) -> Result<Self> {
         let query = Query::new(&self.expect_grammar()?.ts_language, source)?;
         let mut open_capture_ix = 0;
@@ -2149,6 +2217,10 @@ impl Grammar {
     pub fn debug_variables_config(&self) -> Option<&DebugVariablesConfig> {
         self.debug_variables_config.as_ref()
     }
+
+    pub fn imports_config(&self) -> Option<&ImportsConfig> {
+        self.imports_config.as_ref()
+    }
 }
 
 impl CodeLabel {

crates/language/src/language_registry.rs 🔗

@@ -229,6 +229,7 @@ pub const QUERY_FILENAME_PREFIXES: &[(
     ("runnables", |q| &mut q.runnables),
     ("debugger", |q| &mut q.debugger),
     ("textobjects", |q| &mut q.text_objects),
+    ("imports", |q| &mut q.imports),
 ];
 
 /// Tree-sitter language queries for a given language.
@@ -245,6 +246,7 @@ pub struct LanguageQueries {
     pub runnables: Option<Cow<'static, str>>,
     pub text_objects: Option<Cow<'static, str>>,
     pub debugger: Option<Cow<'static, str>>,
+    pub imports: Option<Cow<'static, str>>,
 }
 
 #[derive(Clone, Default)]

crates/language/src/language_settings.rs 🔗

@@ -377,6 +377,8 @@ pub struct EditPredictionSettings {
     pub mode: settings::EditPredictionsMode,
     /// Settings specific to GitHub Copilot.
     pub copilot: CopilotSettings,
+    /// Settings specific to Codestral.
+    pub codestral: CodestralSettings,
     /// Whether edit predictions are enabled in the assistant panel.
     /// This setting has no effect if globally disabled.
     pub enabled_in_text_threads: bool,
@@ -412,6 +414,14 @@ pub struct CopilotSettings {
     pub enterprise_uri: Option<String>,
 }
 
+#[derive(Clone, Debug, Default)]
+pub struct CodestralSettings {
+    /// Model to use for completions.
+    pub model: Option<String>,
+    /// Maximum tokens to generate.
+    pub max_tokens: Option<u32>,
+}
+
 impl AllLanguageSettings {
     /// Returns the [`LanguageSettings`] for the language with the specified name.
     pub fn language<'a>(
@@ -500,7 +510,7 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr
 }
 
 impl settings::Settings for AllLanguageSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let all_languages = &content.project.all_languages;
 
         fn load_from_content(settings: LanguageSettingsContent) -> LanguageSettings {
@@ -622,6 +632,12 @@ impl settings::Settings for AllLanguageSettings {
             enterprise_uri: copilot.enterprise_uri,
         };
 
+        let codestral = edit_predictions.codestral.unwrap();
+        let codestral_settings = CodestralSettings {
+            model: codestral.model,
+            max_tokens: codestral.max_tokens,
+        };
+
         let enabled_in_text_threads = edit_predictions.enabled_in_text_threads.unwrap();
 
         let mut file_types: FxHashMap<Arc<str>, GlobSet> = FxHashMap::default();
@@ -655,6 +671,7 @@ impl settings::Settings for AllLanguageSettings {
                     .collect(),
                 mode: edit_predictions_mode,
                 copilot: copilot_settings,
+                codestral: codestral_settings,
                 enabled_in_text_threads,
             },
             defaults: default_language_settings,

crates/language_model/src/registry.rs 🔗

@@ -118,14 +118,14 @@ impl LanguageModelRegistry {
     }
 
     #[cfg(any(test, feature = "test-support"))]
-    pub fn test(cx: &mut App) -> crate::fake_provider::FakeLanguageModelProvider {
-        let fake_provider = crate::fake_provider::FakeLanguageModelProvider::default();
+    pub fn test(cx: &mut App) -> Arc<crate::fake_provider::FakeLanguageModelProvider> {
+        let fake_provider = Arc::new(crate::fake_provider::FakeLanguageModelProvider::default());
         let registry = cx.new(|cx| {
             let mut registry = Self::default();
             registry.register_provider(fake_provider.clone(), cx);
             let model = fake_provider.provided_models(cx)[0].clone();
             let configured_model = ConfiguredModel {
-                provider: Arc::new(fake_provider.clone()),
+                provider: fake_provider.clone(),
                 model,
             };
             registry.set_default_model(Some(configured_model), cx);
@@ -137,7 +137,7 @@ impl LanguageModelRegistry {
 
     pub fn register_provider<T: LanguageModelProvider + LanguageModelProviderState>(
         &mut self,
-        provider: T,
+        provider: Arc<T>,
         cx: &mut Context<Self>,
     ) {
         let id = provider.id();
@@ -152,7 +152,7 @@ impl LanguageModelRegistry {
             subscription.detach();
         }
 
-        self.providers.insert(id.clone(), Arc::new(provider));
+        self.providers.insert(id.clone(), provider);
         cx.emit(Event::AddedProvider(id));
     }
 
@@ -395,7 +395,7 @@ mod tests {
     fn test_register_providers(cx: &mut App) {
         let registry = cx.new(|_| LanguageModelRegistry::default());
 
-        let provider = FakeLanguageModelProvider::default();
+        let provider = Arc::new(FakeLanguageModelProvider::default());
         registry.update(cx, |registry, cx| {
             registry.register_provider(provider.clone(), cx);
         });

crates/language_model/src/request.rs 🔗

@@ -99,6 +99,10 @@ impl LanguageModelImage {
                     .and_then(image::DynamicImage::from_decoder),
                 ImageFormat::Gif => image::codecs::gif::GifDecoder::new(image_bytes)
                     .and_then(image::DynamicImage::from_decoder),
+                ImageFormat::Bmp => image::codecs::bmp::BmpDecoder::new(image_bytes)
+                    .and_then(image::DynamicImage::from_decoder),
+                ImageFormat::Tiff => image::codecs::tiff::TiffDecoder::new(image_bytes)
+                    .and_then(image::DynamicImage::from_decoder),
                 _ => return None,
             }
             .log_err()?;

crates/language_models/src/language_models.rs 🔗

@@ -18,7 +18,7 @@ use crate::provider::cloud::CloudLanguageModelProvider;
 use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
 use crate::provider::google::GoogleLanguageModelProvider;
 use crate::provider::lmstudio::LmStudioLanguageModelProvider;
-use crate::provider::mistral::MistralLanguageModelProvider;
+pub use crate::provider::mistral::MistralLanguageModelProvider;
 use crate::provider::ollama::OllamaLanguageModelProvider;
 use crate::provider::open_ai::OpenAiLanguageModelProvider;
 use crate::provider::open_ai_compatible::OpenAiCompatibleLanguageModelProvider;
@@ -87,11 +87,11 @@ fn register_openai_compatible_providers(
     for provider_id in new {
         if !old.contains(provider_id) {
             registry.register_provider(
-                OpenAiCompatibleLanguageModelProvider::new(
+                Arc::new(OpenAiCompatibleLanguageModelProvider::new(
                     provider_id.clone(),
                     client.http_client(),
                     cx,
-                ),
+                )),
                 cx,
             );
         }
@@ -105,50 +105,62 @@ fn register_language_model_providers(
     cx: &mut Context<LanguageModelRegistry>,
 ) {
     registry.register_provider(
-        CloudLanguageModelProvider::new(user_store, client.clone(), cx),
+        Arc::new(CloudLanguageModelProvider::new(
+            user_store,
+            client.clone(),
+            cx,
+        )),
+        cx,
+    );
+    registry.register_provider(
+        Arc::new(AnthropicLanguageModelProvider::new(
+            client.http_client(),
+            cx,
+        )),
         cx,
     );
-
     registry.register_provider(
-        AnthropicLanguageModelProvider::new(client.http_client(), cx),
+        Arc::new(OpenAiLanguageModelProvider::new(client.http_client(), cx)),
         cx,
     );
     registry.register_provider(
-        OpenAiLanguageModelProvider::new(client.http_client(), cx),
+        Arc::new(OllamaLanguageModelProvider::new(client.http_client(), cx)),
         cx,
     );
     registry.register_provider(
-        OllamaLanguageModelProvider::new(client.http_client(), cx),
+        Arc::new(LmStudioLanguageModelProvider::new(client.http_client(), cx)),
         cx,
     );
     registry.register_provider(
-        LmStudioLanguageModelProvider::new(client.http_client(), cx),
+        Arc::new(DeepSeekLanguageModelProvider::new(client.http_client(), cx)),
         cx,
     );
     registry.register_provider(
-        DeepSeekLanguageModelProvider::new(client.http_client(), cx),
+        Arc::new(GoogleLanguageModelProvider::new(client.http_client(), cx)),
         cx,
     );
     registry.register_provider(
-        GoogleLanguageModelProvider::new(client.http_client(), cx),
+        MistralLanguageModelProvider::global(client.http_client(), cx),
         cx,
     );
     registry.register_provider(
-        MistralLanguageModelProvider::new(client.http_client(), cx),
+        Arc::new(BedrockLanguageModelProvider::new(client.http_client(), cx)),
         cx,
     );
     registry.register_provider(
-        BedrockLanguageModelProvider::new(client.http_client(), cx),
+        Arc::new(OpenRouterLanguageModelProvider::new(
+            client.http_client(),
+            cx,
+        )),
         cx,
     );
     registry.register_provider(
-        OpenRouterLanguageModelProvider::new(client.http_client(), cx),
+        Arc::new(VercelLanguageModelProvider::new(client.http_client(), cx)),
         cx,
     );
     registry.register_provider(
-        VercelLanguageModelProvider::new(client.http_client(), cx),
+        Arc::new(XAiLanguageModelProvider::new(client.http_client(), cx)),
         cx,
     );
-    registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx);
-    registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx);
+    registry.register_provider(Arc::new(CopilotChatLanguageModelProvider::new(cx)), cx);
 }

crates/language_models/src/provider/mistral.rs 🔗

@@ -1,7 +1,8 @@
 use anyhow::{Result, anyhow};
 use collections::BTreeMap;
+use fs::Fs;
 use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::BoxStream};
-use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
+use gpui::{AnyView, App, AsyncApp, Context, Entity, Global, SharedString, Task, Window};
 use http_client::HttpClient;
 use language_model::{
     AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
@@ -10,9 +11,9 @@ use language_model::{
     LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
     RateLimiter, Role, StopReason, TokenUsage,
 };
-use mistral::{MISTRAL_API_URL, StreamResponse};
+use mistral::{CODESTRAL_API_URL, MISTRAL_API_URL, StreamResponse};
 pub use settings::MistralAvailableModel as AvailableModel;
-use settings::{Settings, SettingsStore};
+use settings::{EditPredictionProvider, Settings, SettingsStore, update_settings_file};
 use std::collections::HashMap;
 use std::pin::Pin;
 use std::str::FromStr;
@@ -31,6 +32,9 @@ const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new(
 const API_KEY_ENV_VAR_NAME: &str = "MISTRAL_API_KEY";
 static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
 
+const CODESTRAL_API_KEY_ENV_VAR_NAME: &str = "CODESTRAL_API_KEY";
+static CODESTRAL_API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(CODESTRAL_API_KEY_ENV_VAR_NAME);
+
 #[derive(Default, Clone, Debug, PartialEq)]
 pub struct MistralSettings {
     pub api_url: String,
@@ -44,6 +48,7 @@ pub struct MistralLanguageModelProvider {
 
 pub struct State {
     api_key_state: ApiKeyState,
+    codestral_api_key_state: ApiKeyState,
 }
 
 impl State {
@@ -57,6 +62,19 @@ impl State {
             .store(api_url, api_key, |this| &mut this.api_key_state, cx)
     }
 
+    fn set_codestral_api_key(
+        &mut self,
+        api_key: Option<String>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        self.codestral_api_key_state.store(
+            CODESTRAL_API_URL.into(),
+            api_key,
+            |this| &mut this.codestral_api_key_state,
+            cx,
+        )
+    }
+
     fn authenticate(&mut self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
         let api_url = MistralLanguageModelProvider::api_url(cx);
         self.api_key_state.load_if_needed(
@@ -66,10 +84,34 @@ impl State {
             cx,
         )
     }
+
+    fn authenticate_codestral(
+        &mut self,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<(), AuthenticateError>> {
+        self.codestral_api_key_state.load_if_needed(
+            CODESTRAL_API_URL.into(),
+            &CODESTRAL_API_KEY_ENV_VAR,
+            |this| &mut this.codestral_api_key_state,
+            cx,
+        )
+    }
 }
 
+struct GlobalMistralLanguageModelProvider(Arc<MistralLanguageModelProvider>);
+
+impl Global for GlobalMistralLanguageModelProvider {}
+
 impl MistralLanguageModelProvider {
-    pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
+    pub fn try_global(cx: &App) -> Option<&Arc<MistralLanguageModelProvider>> {
+        cx.try_global::<GlobalMistralLanguageModelProvider>()
+            .map(|this| &this.0)
+    }
+
+    pub fn global(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Arc<Self> {
+        if let Some(this) = cx.try_global::<GlobalMistralLanguageModelProvider>() {
+            return this.0.clone();
+        }
         let state = cx.new(|cx| {
             cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
                 let api_url = Self::api_url(cx);
@@ -84,10 +126,22 @@ impl MistralLanguageModelProvider {
             .detach();
             State {
                 api_key_state: ApiKeyState::new(Self::api_url(cx)),
+                codestral_api_key_state: ApiKeyState::new(CODESTRAL_API_URL.into()),
             }
         });
 
-        Self { http_client, state }
+        let this = Arc::new(Self { http_client, state });
+        cx.set_global(GlobalMistralLanguageModelProvider(this));
+        cx.global::<GlobalMistralLanguageModelProvider>().0.clone()
+    }
+
+    pub fn load_codestral_api_key(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
+        self.state
+            .update(cx, |state, cx| state.authenticate_codestral(cx))
+    }
+
+    pub fn codestral_api_key(&self, url: &str, cx: &App) -> Option<Arc<str>> {
+        self.state.read(cx).codestral_api_key_state.key(url)
     }
 
     fn create_language_model(&self, model: mistral::Model) -> Arc<dyn LanguageModel> {
@@ -691,6 +745,7 @@ struct RawToolCall {
 
 struct ConfigurationView {
     api_key_editor: Entity<SingleLineInput>,
+    codestral_api_key_editor: Entity<SingleLineInput>,
     state: Entity<State>,
     load_credentials_task: Option<Task<()>>,
 }
@@ -699,6 +754,8 @@ impl ConfigurationView {
     fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
         let api_key_editor =
             cx.new(|cx| SingleLineInput::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
+        let codestral_api_key_editor =
+            cx.new(|cx| SingleLineInput::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2"));
 
         cx.observe(&state, |_, _, cx| {
             cx.notify();
@@ -715,6 +772,12 @@ impl ConfigurationView {
                     // We don't log an error, because "not signed in" is also an error.
                     let _ = task.await;
                 }
+                if let Some(task) = state
+                    .update(cx, |state, cx| state.authenticate_codestral(cx))
+                    .log_err()
+                {
+                    let _ = task.await;
+                }
 
                 this.update(cx, |this, cx| {
                     this.load_credentials_task = None;
@@ -726,6 +789,7 @@ impl ConfigurationView {
 
         Self {
             api_key_editor,
+            codestral_api_key_editor,
             state,
             load_credentials_task,
         }
@@ -763,47 +827,92 @@ impl ConfigurationView {
         .detach_and_log_err(cx);
     }
 
-    fn should_render_editor(&self, cx: &mut Context<Self>) -> bool {
-        !self.state.read(cx).is_authenticated()
+    fn save_codestral_api_key(
+        &mut self,
+        _: &menu::Confirm,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let api_key = self
+            .codestral_api_key_editor
+            .read(cx)
+            .text(cx)
+            .trim()
+            .to_string();
+        if api_key.is_empty() {
+            return;
+        }
+
+        // url changes can cause the editor to be displayed again
+        self.codestral_api_key_editor
+            .update(cx, |editor, cx| editor.set_text("", window, cx));
+
+        let state = self.state.clone();
+        cx.spawn_in(window, async move |_, cx| {
+            state
+                .update(cx, |state, cx| {
+                    state.set_codestral_api_key(Some(api_key), cx)
+                })?
+                .await?;
+            cx.update(|_window, cx| {
+                set_edit_prediction_provider(EditPredictionProvider::Codestral, cx)
+            })
+        })
+        .detach_and_log_err(cx);
     }
-}
 
-impl Render for ConfigurationView {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
+    fn reset_codestral_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.codestral_api_key_editor
+            .update(cx, |editor, cx| editor.set_text("", window, cx));
 
-        if self.load_credentials_task.is_some() {
-            div().child(Label::new("Loading credentials...")).into_any()
-        } else if self.should_render_editor(cx) {
+        let state = self.state.clone();
+        cx.spawn_in(window, async move |_, cx| {
+            state
+                .update(cx, |state, cx| state.set_codestral_api_key(None, cx))?
+                .await?;
+            cx.update(|_window, cx| set_edit_prediction_provider(EditPredictionProvider::Zed, cx))
+        })
+        .detach_and_log_err(cx);
+    }
+
+    fn should_render_api_key_editor(&self, cx: &mut Context<Self>) -> bool {
+        !self.state.read(cx).is_authenticated()
+    }
+
+    fn render_codestral_api_key_editor(&mut self, cx: &mut Context<Self>) -> AnyElement {
+        let key_state = &self.state.read(cx).codestral_api_key_state;
+        let should_show_editor = !key_state.has_key();
+        let env_var_set = key_state.is_from_env_var();
+        if should_show_editor {
             v_flex()
+                .id("codestral")
                 .size_full()
-                .on_action(cx.listener(Self::save_api_key))
-                .child(Label::new("To use Zed's agent with Mistral, you need to add an API key. Follow these steps:"))
+                .mt_2()
+                .on_action(cx.listener(Self::save_codestral_api_key))
+                .child(Label::new(
+                    "To use Codestral as an edit prediction provider, \
+                    you need to add a Codestral-specific API key. Follow these steps:",
+                ))
                 .child(
                     List::new()
                         .child(InstructionListItem::new(
                             "Create one by visiting",
-                            Some("Mistral's console"),
-                            Some("https://console.mistral.ai/api-keys"),
+                            Some("the Codestral section of Mistral's console"),
+                            Some("https://console.mistral.ai/codestral"),
                         ))
-                        .child(InstructionListItem::text_only(
-                            "Ensure your Mistral account has credits",
-                        ))
-                        .child(InstructionListItem::text_only(
-                            "Paste your API key below and hit enter to start using the assistant",
-                        )),
+                        .child(InstructionListItem::text_only("Paste your API key below and hit enter")),
                 )
-                .child(self.api_key_editor.clone())
+                .child(self.codestral_api_key_editor.clone())
                 .child(
                     Label::new(
-                        format!("You can also assign the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
+                        format!("You can also assign the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
                     )
                     .size(LabelSize::Small).color(Color::Muted),
-                )
-                .into_any()
+                ).into_any()
         } else {
             h_flex()
-                .mt_1()
+                .id("codestral")
+                .mt_2()
                 .p_1()
                 .justify_between()
                 .rounded_md()
@@ -815,14 +924,9 @@ impl Render for ConfigurationView {
                         .gap_1()
                         .child(Icon::new(IconName::Check).color(Color::Success))
                         .child(Label::new(if env_var_set {
-                            format!("API key set in {API_KEY_ENV_VAR_NAME} environment variable")
+                            format!("API key set in {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable")
                         } else {
-                            let api_url = MistralLanguageModelProvider::api_url(cx);
-                            if api_url == MISTRAL_API_URL {
-                                "API key configured".to_string()
-                            } else {
-                                format!("API key configured for {}", truncate_and_trailoff(&api_url, 32))
-                            }
+                            "Codestral API key configured".to_string()
                         })),
                 )
                 .child(
@@ -833,15 +937,121 @@ impl Render for ConfigurationView {
                         .icon_position(IconPosition::Start)
                         .disabled(env_var_set)
                         .when(env_var_set, |this| {
-                            this.tooltip(Tooltip::text(format!("To reset your API key, unset the {API_KEY_ENV_VAR_NAME} environment variable.")))
+                            this.tooltip(Tooltip::text(format!(
+                                "To reset your API key, \
+                                unset the {CODESTRAL_API_KEY_ENV_VAR_NAME} environment variable."
+                            )))
                         })
-                        .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
+                        .on_click(
+                            cx.listener(|this, _, window, cx| this.reset_codestral_api_key(window, cx)),
+                        ),
+                ).into_any()
+        }
+    }
+}
+
+impl Render for ConfigurationView {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
+
+        if self.load_credentials_task.is_some() {
+            div().child(Label::new("Loading credentials...")).into_any()
+        } else if self.should_render_api_key_editor(cx) {
+            v_flex()
+                .size_full()
+                .on_action(cx.listener(Self::save_api_key))
+                .child(Label::new("To use Zed's agent with Mistral, you need to add an API key. Follow these steps:"))
+                .child(
+                    List::new()
+                        .child(InstructionListItem::new(
+                            "Create one by visiting",
+                            Some("Mistral's console"),
+                            Some("https://console.mistral.ai/api-keys"),
+                        ))
+                        .child(InstructionListItem::text_only(
+                            "Ensure your Mistral account has credits",
+                        ))
+                        .child(InstructionListItem::text_only(
+                            "Paste your API key below and hit enter to start using the assistant",
+                        )),
                 )
+                .child(self.api_key_editor.clone())
+                .child(
+                    Label::new(
+                        format!("You can also assign the {API_KEY_ENV_VAR_NAME} environment variable and restart Zed."),
+                    )
+                    .size(LabelSize::Small).color(Color::Muted),
+                )
+                .child(self.render_codestral_api_key_editor(cx))
+                .into_any()
+        } else {
+            v_flex()
+                .size_full()
+                .child(
+                    h_flex()
+                        .mt_1()
+                        .p_1()
+                        .justify_between()
+                        .rounded_md()
+                        .border_1()
+                        .border_color(cx.theme().colors().border)
+                        .bg(cx.theme().colors().background)
+                        .child(
+                            h_flex()
+                                .gap_1()
+                                .child(Icon::new(IconName::Check).color(Color::Success))
+                                .child(Label::new(if env_var_set {
+                                    format!(
+                                        "API key set in {API_KEY_ENV_VAR_NAME} environment variable"
+                                    )
+                                } else {
+                                    let api_url = MistralLanguageModelProvider::api_url(cx);
+                                    if api_url == MISTRAL_API_URL {
+                                        "API key configured".to_string()
+                                    } else {
+                                        format!(
+                                            "API key configured for {}",
+                                            truncate_and_trailoff(&api_url, 32)
+                                        )
+                                    }
+                                })),
+                        )
+                        .child(
+                            Button::new("reset-key", "Reset Key")
+                                .label_size(LabelSize::Small)
+                                .icon(Some(IconName::Trash))
+                                .icon_size(IconSize::Small)
+                                .icon_position(IconPosition::Start)
+                                .disabled(env_var_set)
+                                .when(env_var_set, |this| {
+                                    this.tooltip(Tooltip::text(format!(
+                                        "To reset your API key, \
+                                        unset the {API_KEY_ENV_VAR_NAME} environment variable."
+                                    )))
+                                })
+                                .on_click(cx.listener(|this, _, window, cx| {
+                                    this.reset_api_key(window, cx)
+                                })),
+                        ),
+                )
+                .child(self.render_codestral_api_key_editor(cx))
                 .into_any()
         }
     }
 }
 
+fn set_edit_prediction_provider(provider: EditPredictionProvider, cx: &mut App) {
+    let fs = <dyn Fs>::global(cx);
+    update_settings_file(fs, cx, move |settings, _| {
+        settings
+            .project
+            .all_languages
+            .features
+            .get_or_insert_default()
+            .edit_prediction_provider = Some(provider);
+    });
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/language_models/src/settings.rs 🔗

@@ -36,7 +36,7 @@ pub struct AllLanguageModelSettings {
 impl settings::Settings for AllLanguageModelSettings {
     const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
 
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let language_models = content.language_models.clone().unwrap();
         let anthropic = language_models.anthropic.unwrap();
         let bedrock = language_models.bedrock.unwrap();

crates/language_tools/src/lsp_log_view_tests.rs 🔗

@@ -73,7 +73,7 @@ async fn test_lsp_log_view(cx: &mut TestAppContext) {
     let log_view = window.root(cx).unwrap();
     let mut cx = VisualTestContext::from_window(*window, cx);
 
-    language_server.notify::<lsp::notification::LogMessage>(&lsp::LogMessageParams {
+    language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams {
         message: "hello from the server".into(),
         typ: lsp::MessageType::INFO,
     });

crates/languages/Cargo.toml 🔗

@@ -91,7 +91,6 @@ tree-sitter-typescript = { workspace = true, optional = true }
 tree-sitter-yaml = { workspace = true, optional = true }
 util.workspace = true
 workspace-hack.workspace = true
-shlex.workspace = true
 
 [dev-dependencies]
 pretty_assertions.workspace = true

crates/languages/src/c/config.toml 🔗

@@ -17,3 +17,4 @@ brackets = [
 ]
 debuggers = ["CodeLLDB", "GDB"]
 documentation_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }
+import_path_strip_regex = "^<|>$"

crates/languages/src/c/imports.scm 🔗

@@ -0,0 +1,7 @@
+(preproc_include
+    path: [
+        (
+            (system_lib_string) @source @wildcard
+            (#strip! @source "[<>]"))
+        (string_literal (string_content) @source @wildcard)
+    ]) @import

crates/languages/src/cpp/config.toml 🔗

@@ -17,3 +17,4 @@ brackets = [
 ]
 debuggers = ["CodeLLDB", "GDB"]
 documentation_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }
+import_path_strip_regex = "^<|>$"

crates/languages/src/go.rs 🔗

@@ -635,6 +635,22 @@ impl ContextProvider for GoContextProvider {
                 cwd: package_cwd.clone(),
                 ..TaskTemplate::default()
             },
+            TaskTemplate {
+                label: format!(
+                    "go test {} -run {}",
+                    GO_PACKAGE_TASK_VARIABLE.template_value(),
+                    VariableName::Symbol.template_value(),
+                ),
+                command: "go".into(),
+                args: vec![
+                    "test".into(),
+                    "-run".into(),
+                    format!("\\^{}\\$", VariableName::Symbol.template_value(),),
+                ],
+                tags: vec!["go-example".to_owned()],
+                cwd: package_cwd.clone(),
+                ..TaskTemplate::default()
+            },
             TaskTemplate {
                 label: format!("go test {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
                 command: "go".into(),
@@ -992,6 +1008,43 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    fn test_go_example_test_detection(cx: &mut TestAppContext) {
+        let language = language("go", tree_sitter_go::LANGUAGE.into());
+
+        let example_test = r#"
+        package main
+
+        import "fmt"
+
+        func Example() {
+            fmt.Println("Hello, world!")
+            // Output: Hello, world!
+        }
+        "#;
+
+        let buffer =
+            cx.new(|cx| crate::Buffer::local(example_test, cx).with_language(language.clone(), cx));
+        cx.executor().run_until_parked();
+
+        let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
+            let snapshot = buffer.snapshot();
+            snapshot.runnable_ranges(0..example_test.len()).collect()
+        });
+
+        let tag_strings: Vec<String> = runnables
+            .iter()
+            .flat_map(|r| &r.runnable.tags)
+            .map(|tag| tag.0.to_string())
+            .collect();
+
+        assert!(
+            tag_strings.contains(&"go-example".to_string()),
+            "Should find go-example tag, found: {:?}",
+            tag_strings
+        );
+    }
+
     #[gpui::test]
     fn test_go_table_test_slice_detection(cx: &mut TestAppContext) {
         let language = language("go", tree_sitter_go::LANGUAGE.into());

crates/languages/src/go/imports.scm 🔗

@@ -0,0 +1,14 @@
+(import_spec
+    name: [
+        (dot)
+        (package_identifier)
+    ]
+    path: (interpreted_string_literal
+        (interpreted_string_literal_content) @namespace)
+) @wildcard @import
+
+(import_spec
+    !name
+    path: (interpreted_string_literal
+        (interpreted_string_literal_content) @namespace)
+) @wildcard @import

crates/languages/src/go/runnables.scm 🔗

@@ -71,6 +71,15 @@
   (#set! tag go-subtest)
 )
 
+; Functions names start with `Example`
+(
+  (
+    (function_declaration name: (_) @run @_name
+      (#match? @_name "^Example.*"))
+  ) @_
+  (#set! tag go-example)
+)
+
 ; Functions names start with `Benchmark`
 (
   (

crates/languages/src/javascript/config.toml 🔗

@@ -23,6 +23,7 @@ tab_size = 2
 scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
 prettier_parser_name = "babel"
 debuggers = ["JavaScript"]
+import_path_strip_regex = "(?:/index)?\\.[jt]s$"
 
 [jsx_tag_auto_close]
 open_tag_node_name = "jsx_opening_element"

crates/languages/src/javascript/imports.scm 🔗

@@ -0,0 +1,14 @@
+(import_statement
+    import_clause: (import_clause
+        [
+            (identifier) @name
+            (named_imports
+                (import_specifier
+                    name: (_) @name
+                    alias: (_)? @alias))
+        ])
+    source: (string (string_fragment) @source)) @import
+
+(import_statement
+    !import_clause
+    source: (string (string_fragment) @source @wildcard)) @import

crates/languages/src/python.rs 🔗

@@ -1180,15 +1180,7 @@ impl ToolchainLister for PythonToolchainProvider {
             }
             Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
                 if let Some(prefix) = &toolchain.prefix {
-                    let activate_keyword = match shell {
-                        ShellKind::Cmd => ".",
-                        ShellKind::Nushell => "overlay use",
-                        ShellKind::PowerShell => ".",
-                        ShellKind::Fish => "source",
-                        ShellKind::Csh => "source",
-                        ShellKind::Tcsh => "source",
-                        ShellKind::Posix | ShellKind::Rc => "source",
-                    };
+                    let activate_keyword = shell.activate_keyword();
                     let activate_script_name = match shell {
                         ShellKind::Posix | ShellKind::Rc => "activate",
                         ShellKind::Csh => "activate.csh",
@@ -1197,11 +1189,11 @@ impl ToolchainLister for PythonToolchainProvider {
                         ShellKind::Nushell => "activate.nu",
                         ShellKind::PowerShell => "activate.ps1",
                         ShellKind::Cmd => "activate.bat",
+                        ShellKind::Xonsh => "activate.xsh",
                     };
                     let path = prefix.join(BINARY_DIR).join(activate_script_name);
 
-                    if let Ok(quoted) =
-                        shlex::try_quote(&path.to_string_lossy()).map(Cow::into_owned)
+                    if let Some(quoted) = shell.try_quote(&path.to_string_lossy())
                         && fs.is_file(&path).await
                     {
                         activation_script.push(format!("{activate_keyword} {quoted}"));
@@ -1224,6 +1216,7 @@ impl ToolchainLister for PythonToolchainProvider {
                     ShellKind::Tcsh => None,
                     ShellKind::Cmd => None,
                     ShellKind::Rc => None,
+                    ShellKind::Xonsh => None,
                 })
             }
             _ => {}

crates/languages/src/python/config.toml 🔗

@@ -35,3 +35,4 @@ decrease_indent_patterns = [
   { pattern = "^\\s*except\\b.*:\\s*(#.*)?",  valid_after = ["try", "except"] },
   { pattern = "^\\s*finally\\b.*:\\s*(#.*)?", valid_after = ["try", "except", "else"] },
 ]
+import_path_strip_regex = "/__init__\\.py$"

crates/languages/src/python/imports.scm 🔗

@@ -0,0 +1,32 @@
+(import_statement
+    name: [
+        (dotted_name
+            ((identifier) @namespace ".")*
+            (identifier) @namespace .)
+        (aliased_import
+            name: (dotted_name
+                ((identifier) @namespace ".")*
+                (identifier) @namespace .))
+    ]) @wildcard @import
+
+(import_from_statement
+    module_name: [
+        (dotted_name
+            ((identifier) @namespace ".")*
+            (identifier) @namespace .)
+        (relative_import
+            (dotted_name
+                ((identifier) @namespace ".")*
+                (identifier) @namespace .)?)
+    ]
+    (wildcard_import)? @wildcard
+    name: [
+        (dotted_name
+            ((identifier) @namespace ".")*
+            (identifier) @name .)
+        (aliased_import
+            name: (dotted_name
+                ((identifier) @namespace ".")*
+                (identifier) @name .)
+            alias: (identifier) @alias)
+    ]?) @import

crates/languages/src/rust/config.toml 🔗

@@ -17,3 +17,5 @@ brackets = [
 collapsed_placeholder = " /* ... */ "
 debuggers = ["CodeLLDB", "GDB"]
 documentation_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }
+ignored_import_segments = ["crate", "super"]
+import_path_strip_regex = "/(lib|mod)\\.rs$"

crates/languages/src/rust/imports.scm 🔗

@@ -0,0 +1,27 @@
+(use_declaration) @import
+
+(scoped_use_list
+    path: (_) @namespace
+    list: (_) @list)
+
+(scoped_identifier
+    path: (_) @namespace
+    name: (identifier) @name)
+
+(use_list (identifier) @name)
+
+(use_declaration (identifier) @name)
+
+(use_as_clause
+    path: (scoped_identifier
+       path: (_) @namespace
+       name: (_) @name)
+    alias: (_) @alias)
+
+(use_as_clause
+    path: (identifier) @name
+    alias: (_) @alias)
+
+(use_wildcard
+    (_)? @namespace
+    "*" @wildcard)

crates/languages/src/tsx/imports.scm 🔗

@@ -0,0 +1,14 @@
+(import_statement
+    import_clause: (import_clause
+        [
+            (identifier) @name
+            (named_imports
+                (import_specifier
+                    name: (_) @name
+                    alias: (_)? @alias))
+        ])
+    source: (string (string_fragment) @source)) @import
+
+(import_statement
+    !import_clause
+    source: (string (string_fragment) @source @wildcard)) @import

crates/languages/src/typescript/config.toml 🔗

@@ -22,6 +22,7 @@ prettier_parser_name = "typescript"
 tab_size = 2
 debuggers = ["JavaScript"]
 scope_opt_in_language_servers = ["tailwindcss-language-server"]
+import_path_strip_regex = "(?:/index)?\\.[jt]s$"
 
 [overrides.string]
 completion_query_characters = ["-", "."]

crates/languages/src/typescript/imports.scm 🔗

@@ -0,0 +1,20 @@
+(import_statement
+    import_clause: (import_clause
+        [
+            (identifier) @name
+            (named_imports
+                (import_specifier
+                    name: (_) @name
+                    alias: (_)? @alias))
+            (namespace_import) @wildcard
+        ])
+    source: (string (string_fragment) @source)) @import
+
+(import_statement
+    !source
+    import_clause: (import_require_clause
+        source: (string (string_fragment) @source))) @wildcard @import
+
+(import_statement
+    !import_clause
+    source: (string (string_fragment) @source)) @wildcard @import

crates/lsp/src/lsp.rs 🔗

@@ -80,11 +80,14 @@ pub struct LanguageServerBinaryOptions {
     pub pre_release: bool,
 }
 
+struct NotificationSerializer(Box<dyn FnOnce() -> String + Send + Sync>);
+
 /// A running language server process.
 pub struct LanguageServer {
     server_id: LanguageServerId,
     next_id: AtomicI32,
     outbound_tx: channel::Sender<String>,
+    notification_tx: channel::Sender<NotificationSerializer>,
     name: LanguageServerName,
     process_name: Arc<str>,
     binary: LanguageServerBinary,
@@ -477,9 +480,24 @@ impl LanguageServer {
         }
         .into();
 
+        let (notification_tx, notification_rx) = channel::unbounded::<NotificationSerializer>();
+        cx.background_spawn({
+            let outbound_tx = outbound_tx.clone();
+            async move {
+                while let Ok(serializer) = notification_rx.recv().await {
+                    let serialized = (serializer.0)();
+                    let Ok(_) = outbound_tx.send(serialized).await else {
+                        return;
+                    };
+                }
+                outbound_tx.close();
+            }
+        })
+        .detach();
         Self {
             server_id,
             notification_handlers,
+            notification_tx,
             response_handlers,
             io_handlers,
             name: server_name,
@@ -906,7 +924,7 @@ impl LanguageServer {
             self.capabilities = RwLock::new(response.capabilities);
             self.configuration = configuration;
 
-            self.notify::<notification::Initialized>(&InitializedParams {})?;
+            self.notify::<notification::Initialized>(InitializedParams {})?;
             Ok(Arc::new(self))
         })
     }
@@ -918,11 +936,13 @@ impl LanguageServer {
             let next_id = AtomicI32::new(self.next_id.load(SeqCst));
             let outbound_tx = self.outbound_tx.clone();
             let executor = self.executor.clone();
+            let notification_serializers = self.notification_tx.clone();
             let mut output_done = self.output_done_rx.lock().take().unwrap();
             let shutdown_request = Self::request_internal::<request::Shutdown>(
                 &next_id,
                 &response_handlers,
                 &outbound_tx,
+                &notification_serializers,
                 &executor,
                 (),
             );
@@ -956,8 +976,8 @@ impl LanguageServer {
                 }
 
                 response_handlers.lock().take();
-                Self::notify_internal::<notification::Exit>(&outbound_tx, &()).ok();
-                outbound_tx.close();
+                Self::notify_internal::<notification::Exit>(&notification_serializers, ()).ok();
+                notification_serializers.close();
                 output_done.recv().await;
                 server.lock().take().map(|mut child| child.kill());
                 drop(tasks);
@@ -1179,6 +1199,7 @@ impl LanguageServer {
             &self.next_id,
             &self.response_handlers,
             &self.outbound_tx,
+            &self.notification_tx,
             &self.executor,
             params,
         )
@@ -1200,6 +1221,7 @@ impl LanguageServer {
             &self.next_id,
             &self.response_handlers,
             &self.outbound_tx,
+            &self.notification_tx,
             &self.executor,
             timer,
             params,
@@ -1210,6 +1232,7 @@ impl LanguageServer {
         next_id: &AtomicI32,
         response_handlers: &Mutex<Option<HashMap<RequestId, ResponseHandler>>>,
         outbound_tx: &channel::Sender<String>,
+        notification_serializers: &channel::Sender<NotificationSerializer>,
         executor: &BackgroundExecutor,
         timer: U,
         params: T::Params,
@@ -1261,7 +1284,7 @@ impl LanguageServer {
             .try_send(message)
             .context("failed to write to language server's stdin");
 
-        let outbound_tx = outbound_tx.downgrade();
+        let notification_serializers = notification_serializers.downgrade();
         let started = Instant::now();
         LspRequest::new(id, async move {
             if let Err(e) = handle_response {
@@ -1272,10 +1295,10 @@ impl LanguageServer {
             }
 
             let cancel_on_drop = util::defer(move || {
-                if let Some(outbound_tx) = outbound_tx.upgrade() {
+                if let Some(notification_serializers) = notification_serializers.upgrade() {
                     Self::notify_internal::<notification::Cancel>(
-                        &outbound_tx,
-                        &CancelParams {
+                        &notification_serializers,
+                        CancelParams {
                             id: NumberOrString::Number(id),
                         },
                     )
@@ -1310,6 +1333,7 @@ impl LanguageServer {
         next_id: &AtomicI32,
         response_handlers: &Mutex<Option<HashMap<RequestId, ResponseHandler>>>,
         outbound_tx: &channel::Sender<String>,
+        notification_serializers: &channel::Sender<NotificationSerializer>,
         executor: &BackgroundExecutor,
         params: T::Params,
     ) -> impl LspRequestFuture<T::Result> + use<T>
@@ -1321,6 +1345,7 @@ impl LanguageServer {
             next_id,
             response_handlers,
             outbound_tx,
+            notification_serializers,
             executor,
             Self::default_request_timer(executor.clone()),
             params,
@@ -1336,21 +1361,25 @@ impl LanguageServer {
     /// Sends a RPC notification to the language server.
     ///
     /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage)
-    pub fn notify<T: notification::Notification>(&self, params: &T::Params) -> Result<()> {
-        Self::notify_internal::<T>(&self.outbound_tx, params)
+    pub fn notify<T: notification::Notification>(&self, params: T::Params) -> Result<()> {
+        let outbound = self.notification_tx.clone();
+        Self::notify_internal::<T>(&outbound, params)
     }
 
     fn notify_internal<T: notification::Notification>(
-        outbound_tx: &channel::Sender<String>,
-        params: &T::Params,
+        outbound_tx: &channel::Sender<NotificationSerializer>,
+        params: T::Params,
     ) -> Result<()> {
-        let message = serde_json::to_string(&Notification {
-            jsonrpc: JSON_RPC_VERSION,
-            method: T::METHOD,
-            params,
-        })
-        .unwrap();
-        outbound_tx.try_send(message)?;
+        let serializer = NotificationSerializer(Box::new(move || {
+            serde_json::to_string(&Notification {
+                jsonrpc: JSON_RPC_VERSION,
+                method: T::METHOD,
+                params,
+            })
+            .unwrap()
+        }));
+
+        outbound_tx.send_blocking(serializer)?;
         Ok(())
     }
 
@@ -1385,7 +1414,7 @@ impl LanguageServer {
                     removed: vec![],
                 },
             };
-            self.notify::<DidChangeWorkspaceFolders>(&params).ok();
+            self.notify::<DidChangeWorkspaceFolders>(params).ok();
         }
     }
 
@@ -1419,7 +1448,7 @@ impl LanguageServer {
                     }],
                 },
             };
-            self.notify::<DidChangeWorkspaceFolders>(&params).ok();
+            self.notify::<DidChangeWorkspaceFolders>(params).ok();
         }
     }
     pub fn set_workspace_folders(&self, folders: BTreeSet<Uri>) {
@@ -1451,7 +1480,7 @@ impl LanguageServer {
             let params = DidChangeWorkspaceFoldersParams {
                 event: WorkspaceFoldersChangeEvent { added, removed },
             };
-            self.notify::<DidChangeWorkspaceFolders>(&params).ok();
+            self.notify::<DidChangeWorkspaceFolders>(params).ok();
         }
     }
 
@@ -1469,14 +1498,14 @@ impl LanguageServer {
         version: i32,
         initial_text: String,
     ) {
-        self.notify::<notification::DidOpenTextDocument>(&DidOpenTextDocumentParams {
+        self.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
             text_document: TextDocumentItem::new(uri, language_id, version, initial_text),
         })
         .ok();
     }
 
     pub fn unregister_buffer(&self, uri: Uri) {
-        self.notify::<notification::DidCloseTextDocument>(&DidCloseTextDocumentParams {
+        self.notify::<notification::DidCloseTextDocument>(DidCloseTextDocumentParams {
             text_document: TextDocumentIdentifier::new(uri),
         })
         .ok();
@@ -1692,7 +1721,7 @@ impl LanguageServer {
 #[cfg(any(test, feature = "test-support"))]
 impl FakeLanguageServer {
     /// See [`LanguageServer::notify`].
-    pub fn notify<T: notification::Notification>(&self, params: &T::Params) {
+    pub fn notify<T: notification::Notification>(&self, params: T::Params) {
         self.server.notify::<T>(params).ok();
     }
 
@@ -1801,7 +1830,7 @@ impl FakeLanguageServer {
         .await
         .into_response()
         .unwrap();
-        self.notify::<notification::Progress>(&ProgressParams {
+        self.notify::<notification::Progress>(ProgressParams {
             token: NumberOrString::String(token),
             value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(progress)),
         });
@@ -1809,7 +1838,7 @@ impl FakeLanguageServer {
 
     /// Simulate that the server has completed work and notifies about that with the specified token.
     pub fn end_progress(&self, token: impl Into<String>) {
-        self.notify::<notification::Progress>(&ProgressParams {
+        self.notify::<notification::Progress>(ProgressParams {
             token: NumberOrString::String(token.into()),
             value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())),
         });
@@ -1868,7 +1897,7 @@ mod tests {
             .await
             .unwrap();
         server
-            .notify::<notification::DidOpenTextDocument>(&DidOpenTextDocumentParams {
+            .notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
                 text_document: TextDocumentItem::new(
                     Uri::from_str("file://a/b").unwrap(),
                     "rust".to_string(),
@@ -1886,11 +1915,11 @@ mod tests {
             "file://a/b"
         );
 
-        fake.notify::<notification::ShowMessage>(&ShowMessageParams {
+        fake.notify::<notification::ShowMessage>(ShowMessageParams {
             typ: MessageType::ERROR,
             message: "ok".to_string(),
         });
-        fake.notify::<notification::PublishDiagnostics>(&PublishDiagnosticsParams {
+        fake.notify::<notification::PublishDiagnostics>(PublishDiagnosticsParams {
             uri: Uri::from_str("file://b/c").unwrap(),
             version: Some(5),
             diagnostics: vec![],
@@ -1904,6 +1933,7 @@ mod tests {
         fake.set_request_handler::<request::Shutdown, _, _>(|_, _| async move { Ok(()) });
 
         drop(server);
+        cx.run_until_parked();
         fake.receive_notification::<notification::Exit>().await;
     }
 

crates/markdown_preview/src/markdown_parser.rs 🔗

@@ -880,6 +880,10 @@ impl<'a> MarkdownParser<'a> {
                             contents: paragraph,
                         }));
                     }
+                } else if local_name!("blockquote") == name.local {
+                    if let Some(blockquote) = self.extract_html_blockquote(node, source_range) {
+                        elements.push(ParsedMarkdownElement::BlockQuote(blockquote));
+                    }
                 } else if local_name!("table") == name.local {
                     if let Some(table) = self.extract_html_table(node, source_range) {
                         elements.push(ParsedMarkdownElement::Table(table));
@@ -1002,6 +1006,24 @@ impl<'a> MarkdownParser<'a> {
         Some(image)
     }
 
+    fn extract_html_blockquote(
+        &self,
+        node: &Rc<markup5ever_rcdom::Node>,
+        source_range: Range<usize>,
+    ) -> Option<ParsedMarkdownBlockQuote> {
+        let mut children = Vec::new();
+        self.consume_children(source_range.clone(), node, &mut children);
+
+        if children.is_empty() {
+            None
+        } else {
+            Some(ParsedMarkdownBlockQuote {
+                children,
+                source_range,
+            })
+        }
+    }
+
     fn extract_html_table(
         &self,
         node: &Rc<markup5ever_rcdom::Node>,
@@ -1410,6 +1432,61 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_html_block_quote() {
+        let parsed = parse(
+            "<blockquote>
+              <p>some description</p>
+            </blockquote>",
+        )
+        .await;
+
+        assert_eq!(
+            ParsedMarkdown {
+                children: vec![block_quote(
+                    vec![ParsedMarkdownElement::Paragraph(text(
+                        "some description",
+                        0..76
+                    ))],
+                    0..76,
+                )]
+            },
+            parsed
+        );
+    }
+
+    #[gpui::test]
+    async fn test_html_nested_block_quote() {
+        let parsed = parse(
+            "<blockquote>
+              <p>some description</p>
+              <blockquote>
+                <p>second description</p>
+              </blockquote>
+            </blockquote>",
+        )
+        .await;
+
+        assert_eq!(
+            ParsedMarkdown {
+                children: vec![block_quote(
+                    vec![
+                        ParsedMarkdownElement::Paragraph(text("some description", 0..173)),
+                        block_quote(
+                            vec![ParsedMarkdownElement::Paragraph(text(
+                                "second description",
+                                0..173
+                            ))],
+                            0..173,
+                        )
+                    ],
+                    0..173,
+                )]
+            },
+            parsed
+        );
+    }
+
     #[gpui::test]
     async fn test_html_table() {
         let parsed = parse(

crates/migrator/src/migrations/m_2025_01_29/keymap.rs 🔗

@@ -156,6 +156,16 @@ static TRANSFORM_ARRAY: LazyLock<HashMap<(&str, &str), &str>> = LazyLock::new(||
         (("vim::ResizePane", "Narrow"), "vim::ResizePaneLeft"),
         (("vim::ResizePane", "Shorten"), "vim::ResizePaneDown"),
         (("vim::ResizePane", "Lengthen"), "vim::ResizePaneUp"),
+        // fold at level
+        (("editor::FoldAtLevel", "1"), "editor::FoldAtLevel1"),
+        (("editor::FoldAtLevel", "2"), "editor::FoldAtLevel2"),
+        (("editor::FoldAtLevel", "3"), "editor::FoldAtLevel3"),
+        (("editor::FoldAtLevel", "4"), "editor::FoldAtLevel4"),
+        (("editor::FoldAtLevel", "5"), "editor::FoldAtLevel5"),
+        (("editor::FoldAtLevel", "6"), "editor::FoldAtLevel6"),
+        (("editor::FoldAtLevel", "7"), "editor::FoldAtLevel7"),
+        (("editor::FoldAtLevel", "8"), "editor::FoldAtLevel8"),
+        (("editor::FoldAtLevel", "9"), "editor::FoldAtLevel9"),
     ])
 });
 

crates/migrator/src/migrator.rs 🔗

@@ -65,7 +65,13 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt
     }
 }
 
+/// Runs the provided migrations on the given text.
+/// Will automatically return `Ok(None)` if there's no content to migrate.
 fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result<Option<String>> {
+    if text.is_empty() {
+        return Ok(None);
+    }
+
     let mut current_text = text.to_string();
     let mut result: Option<String> = None;
     for migration in migrations.iter() {
@@ -371,6 +377,11 @@ mod tests {
         assert_migrated_correctly(migrated, output);
     }
 
+    #[test]
+    fn test_empty_content() {
+        assert_migrate_settings("", None)
+    }
+
     #[test]
     fn test_replace_array_with_single_string() {
         assert_migrate_keymap(

crates/mistral/src/mistral.rs 🔗

@@ -7,6 +7,7 @@ use std::convert::TryFrom;
 use strum::EnumIter;
 
 pub const MISTRAL_API_URL: &str = "https://api.mistral.ai/v1";
+pub const CODESTRAL_API_URL: &str = "https://codestral.mistral.ai";
 
 #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
 #[serde(rename_all = "lowercase")]

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -161,24 +161,25 @@ impl MultiBufferDiffHunk {
 
 #[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)]
 pub struct PathKey {
-    namespace: Option<u64>,
+    // Used by the derived PartialOrd & Ord
+    sort_prefix: Option<u64>,
     path: Arc<RelPath>,
 }
 
 impl PathKey {
-    pub fn namespaced(namespace: u64, path: Arc<RelPath>) -> Self {
+    pub fn with_sort_prefix(sort_prefix: u64, path: Arc<RelPath>) -> Self {
         Self {
-            namespace: Some(namespace),
+            sort_prefix: Some(sort_prefix),
             path,
         }
     }
 
     pub fn for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Self {
         if let Some(file) = buffer.read(cx).file() {
-            Self::namespaced(file.worktree_id(cx).to_proto(), file.path().clone())
+            Self::with_sort_prefix(file.worktree_id(cx).to_proto(), file.path().clone())
         } else {
             Self {
-                namespace: None,
+                sort_prefix: None,
                 path: RelPath::unix(&buffer.entity_id().to_string())
                     .unwrap()
                     .into_arc(),
@@ -6385,6 +6386,17 @@ impl MultiBufferSnapshot {
             debug_ranges.insert(key, text_ranges, format!("{value:?}").into())
         });
     }
+
+    // used by line_mode selections and tries to match vim behavior
+    pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
+        let new_start = MultiBufferPoint::new(range.start.row, 0);
+        let new_end = if range.end.column > 0 {
+            MultiBufferPoint::new(range.end.row, self.line_len(MultiBufferRow(range.end.row)))
+        } else {
+            range.end
+        };
+        new_start..new_end
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -1525,7 +1525,7 @@ fn test_set_excerpts_for_buffer_ordering(cx: &mut TestAppContext) {
             cx,
         )
     });
-    let path1: PathKey = PathKey::namespaced(0, rel_path("root").into_arc());
+    let path1: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc());
 
     let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
     multibuffer.update(cx, |multibuffer, cx| {
@@ -1620,7 +1620,7 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
             cx,
         )
     });
-    let path1: PathKey = PathKey::namespaced(0, rel_path("root").into_arc());
+    let path1: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc());
     let buf2 = cx.new(|cx| {
         Buffer::local(
             indoc! {
@@ -1639,7 +1639,7 @@ fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) {
             cx,
         )
     });
-    let path2 = PathKey::namespaced(1, rel_path("root").into_arc());
+    let path2 = PathKey::with_sort_prefix(1, rel_path("root").into_arc());
 
     let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
     multibuffer.update(cx, |multibuffer, cx| {
@@ -1816,7 +1816,7 @@ fn test_set_excerpts_for_buffer_rename(cx: &mut TestAppContext) {
             cx,
         )
     });
-    let path: PathKey = PathKey::namespaced(0, rel_path("root").into_arc());
+    let path: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc());
     let buf2 = cx.new(|cx| {
         Buffer::local(
             indoc! {

crates/onboarding/Cargo.toml 🔗

@@ -15,20 +15,15 @@ path = "src/onboarding.rs"
 default = []
 
 [dependencies]
-ai_onboarding.workspace = true
 anyhow.workspace = true
 client.workspace = true
 component.workspace = true
 db.workspace = true
 documented.workspace = true
-editor.workspace = true
 fs.workspace = true
 fuzzy.workspace = true
 git.workspace = true
 gpui.workspace = true
-itertools.workspace = true
-language.workspace = true
-language_model.workspace = true
 menu.workspace = true
 notifications.workspace = true
 picker.workspace = true

crates/onboarding/src/ai_setup_page.rs 🔗

@@ -1,427 +0,0 @@
-use std::sync::Arc;
-
-use ai_onboarding::AiUpsellCard;
-use client::{Client, UserStore, zed_urls};
-use fs::Fs;
-use gpui::{
-    Action, AnyView, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity,
-    Window, prelude::*,
-};
-use itertools;
-use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
-use project::DisableAiSettings;
-use settings::{Settings, update_settings_file};
-use ui::{
-    Badge, ButtonLike, Divider, KeyBinding, Modal, ModalFooter, ModalHeader, Section, SwitchField,
-    ToggleState, prelude::*, tooltip_container,
-};
-use util::ResultExt;
-use workspace::{ModalView, Workspace};
-use zed_actions::agent::OpenSettings;
-
-const FEATURED_PROVIDERS: [&str; 4] = ["anthropic", "google", "openai", "ollama"];
-
-fn render_llm_provider_section(
-    tab_index: &mut isize,
-    workspace: WeakEntity<Workspace>,
-    disabled: bool,
-    window: &mut Window,
-    cx: &mut App,
-) -> impl IntoElement {
-    v_flex()
-        .gap_4()
-        .child(
-            v_flex()
-                .child(Label::new("Or use other LLM providers").size(LabelSize::Large))
-                .child(
-                    Label::new("Bring your API keys to use the available providers with Zed's UI for free.")
-                        .color(Color::Muted),
-                ),
-        )
-        .child(render_llm_provider_card(tab_index, workspace, disabled, window, cx))
-}
-
-fn render_privacy_card(tab_index: &mut isize, disabled: bool, cx: &mut App) -> impl IntoElement {
-    let (title, description) = if disabled {
-        (
-            "AI is disabled across Zed",
-            "Re-enable it any time in Settings.",
-        )
-    } else {
-        (
-            "Privacy is the default for Zed",
-            "Any use or storage of your data is with your explicit, single-use, opt-in consent.",
-        )
-    };
-
-    v_flex()
-        .relative()
-        .pt_2()
-        .pb_2p5()
-        .pl_3()
-        .pr_2()
-        .border_1()
-        .border_dashed()
-        .border_color(cx.theme().colors().border.opacity(0.5))
-        .bg(cx.theme().colors().surface_background.opacity(0.3))
-        .rounded_lg()
-        .overflow_hidden()
-        .child(
-            h_flex()
-                .gap_2()
-                .justify_between()
-                .child(Label::new(title))
-                .child(
-                    h_flex()
-                        .gap_1()
-                        .child(
-                            Badge::new("Privacy")
-                                .icon(IconName::ShieldCheck)
-                                .tooltip(move |_, cx| cx.new(|_| AiPrivacyTooltip::new()).into()),
-                        )
-                        .child(
-                            Button::new("learn_more", "Learn More")
-                                .style(ButtonStyle::Outlined)
-                                .label_size(LabelSize::Small)
-                                .icon(IconName::ArrowUpRight)
-                                .icon_size(IconSize::XSmall)
-                                .icon_color(Color::Muted)
-                                .on_click(|_, _, cx| {
-                                    cx.open_url(&zed_urls::ai_privacy_and_security(cx))
-                                })
-                                .tab_index({
-                                    *tab_index += 1;
-                                    *tab_index - 1
-                                }),
-                        ),
-                ),
-        )
-        .child(
-            Label::new(description)
-                .size(LabelSize::Small)
-                .color(Color::Muted),
-        )
-}
-
-fn render_llm_provider_card(
-    tab_index: &mut isize,
-    workspace: WeakEntity<Workspace>,
-    disabled: bool,
-    _: &mut Window,
-    cx: &mut App,
-) -> impl IntoElement {
-    let registry = LanguageModelRegistry::read_global(cx);
-
-    v_flex()
-        .border_1()
-        .border_color(cx.theme().colors().border)
-        .bg(cx.theme().colors().surface_background.opacity(0.5))
-        .rounded_lg()
-        .overflow_hidden()
-        .children(itertools::intersperse_with(
-            FEATURED_PROVIDERS
-                .into_iter()
-                .flat_map(|provider_name| {
-                    registry.provider(&LanguageModelProviderId::new(provider_name))
-                })
-                .enumerate()
-                .map(|(index, provider)| {
-                    let group_name = SharedString::new(format!("onboarding-hover-group-{}", index));
-                    let is_authenticated = provider.is_authenticated(cx);
-
-                    ButtonLike::new(("onboarding-ai-setup-buttons", index))
-                        .size(ButtonSize::Large)
-                        .tab_index({
-                            *tab_index += 1;
-                            *tab_index - 1
-                        })
-                        .child(
-                            h_flex()
-                                .group(&group_name)
-                                .px_0p5()
-                                .w_full()
-                                .gap_2()
-                                .justify_between()
-                                .child(
-                                    h_flex()
-                                        .gap_1()
-                                        .child(
-                                            Icon::new(provider.icon())
-                                                .color(Color::Muted)
-                                                .size(IconSize::XSmall),
-                                        )
-                                        .child(Label::new(provider.name().0)),
-                                )
-                                .child(
-                                    h_flex()
-                                        .gap_1()
-                                        .when(!is_authenticated, |el| {
-                                            el.visible_on_hover(group_name.clone())
-                                                .child(
-                                                    Icon::new(IconName::Settings)
-                                                        .color(Color::Muted)
-                                                        .size(IconSize::XSmall),
-                                                )
-                                                .child(
-                                                    Label::new("Configure")
-                                                        .color(Color::Muted)
-                                                        .size(LabelSize::Small),
-                                                )
-                                        })
-                                        .when(is_authenticated && !disabled, |el| {
-                                            el.child(
-                                                Icon::new(IconName::Check)
-                                                    .color(Color::Success)
-                                                    .size(IconSize::XSmall),
-                                            )
-                                            .child(
-                                                Label::new("Configured")
-                                                    .color(Color::Muted)
-                                                    .size(LabelSize::Small),
-                                            )
-                                        }),
-                                ),
-                        )
-                        .on_click({
-                            let workspace = workspace.clone();
-                            move |_, window, cx| {
-                                workspace
-                                    .update(cx, |workspace, cx| {
-                                        workspace.toggle_modal(window, cx, |window, cx| {
-                                            telemetry::event!(
-                                                "Welcome AI Modal Opened",
-                                                provider = provider.name().0,
-                                            );
-
-                                            let modal = AiConfigurationModal::new(
-                                                provider.clone(),
-                                                window,
-                                                cx,
-                                            );
-                                            window.focus(&modal.focus_handle(cx));
-                                            modal
-                                        });
-                                    })
-                                    .log_err();
-                            }
-                        })
-                        .into_any_element()
-                }),
-            || Divider::horizontal().into_any_element(),
-        ))
-        .child(Divider::horizontal())
-        .child(
-            Button::new("agent_settings", "Add Many Others")
-                .size(ButtonSize::Large)
-                .icon(IconName::Plus)
-                .icon_position(IconPosition::Start)
-                .icon_color(Color::Muted)
-                .icon_size(IconSize::XSmall)
-                .on_click(|_event, window, cx| {
-                    window.dispatch_action(OpenSettings.boxed_clone(), cx)
-                })
-                .tab_index({
-                    *tab_index += 1;
-                    *tab_index - 1
-                }),
-        )
-}
-
-pub(crate) fn render_ai_setup_page(
-    workspace: WeakEntity<Workspace>,
-    user_store: Entity<UserStore>,
-    client: Arc<Client>,
-    window: &mut Window,
-    cx: &mut App,
-) -> impl IntoElement {
-    let mut tab_index = 0;
-    let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
-
-    v_flex()
-        .gap_2()
-        .child(
-            SwitchField::new(
-                "enable_ai",
-                "Enable AI features",
-                None,
-                if is_ai_disabled {
-                    ToggleState::Unselected
-                } else {
-                    ToggleState::Selected
-                },
-                |&toggle_state, _, cx| {
-                    let enabled = match toggle_state {
-                        ToggleState::Indeterminate => {
-                            return;
-                        }
-                        ToggleState::Unselected => true,
-                        ToggleState::Selected => false,
-                    };
-
-                    telemetry::event!(
-                        "Welcome AI Enabled",
-                        toggle = if enabled { "on" } else { "off" },
-                    );
-
-                    let fs = <dyn Fs>::global(cx);
-                    update_settings_file(fs, cx, move |settings, _| {
-                        settings.disable_ai = Some(enabled.into());
-                    });
-                },
-            )
-            .tab_index({
-                tab_index += 1;
-                tab_index - 1
-            }),
-        )
-        .child(render_privacy_card(&mut tab_index, is_ai_disabled, cx))
-        .child(
-            v_flex()
-                .mt_2()
-                .gap_6()
-                .child(
-                    AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx)
-                        .tab_index(Some({
-                            tab_index += 1;
-                            tab_index - 1
-                        })),
-                )
-                .child(render_llm_provider_section(
-                    &mut tab_index,
-                    workspace,
-                    is_ai_disabled,
-                    window,
-                    cx,
-                ))
-                .when(is_ai_disabled, |this| {
-                    this.child(
-                        div()
-                            .id("backdrop")
-                            .size_full()
-                            .absolute()
-                            .inset_0()
-                            .bg(cx.theme().colors().editor_background)
-                            .opacity(0.8)
-                            .block_mouse_except_scroll(),
-                    )
-                }),
-        )
-}
-
-struct AiConfigurationModal {
-    focus_handle: FocusHandle,
-    selected_provider: Arc<dyn LanguageModelProvider>,
-    configuration_view: AnyView,
-}
-
-impl AiConfigurationModal {
-    fn new(
-        selected_provider: Arc<dyn LanguageModelProvider>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let focus_handle = cx.focus_handle();
-        let configuration_view = selected_provider.configuration_view(
-            language_model::ConfigurationViewTargetAgent::ZedAgent,
-            window,
-            cx,
-        );
-
-        Self {
-            focus_handle,
-            configuration_view,
-            selected_provider,
-        }
-    }
-
-    fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
-        cx.emit(DismissEvent);
-    }
-}
-
-impl ModalView for AiConfigurationModal {}
-
-impl EventEmitter<DismissEvent> for AiConfigurationModal {}
-
-impl Focusable for AiConfigurationModal {
-    fn focus_handle(&self, _cx: &App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl Render for AiConfigurationModal {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        v_flex()
-            .key_context("OnboardingAiConfigurationModal")
-            .w(rems(34.))
-            .elevation_3(cx)
-            .track_focus(&self.focus_handle)
-            .on_action(
-                cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
-            )
-            .child(
-                Modal::new("onboarding-ai-setup-modal", None)
-                    .header(
-                        ModalHeader::new()
-                            .icon(
-                                Icon::new(self.selected_provider.icon())
-                                    .color(Color::Muted)
-                                    .size(IconSize::Small),
-                            )
-                            .headline(self.selected_provider.name().0),
-                    )
-                    .section(Section::new().child(self.configuration_view.clone()))
-                    .footer(
-                        ModalFooter::new().end_slot(
-                            Button::new("ai-onb-modal-Done", "Done")
-                                .key_binding(
-                                    KeyBinding::for_action_in(
-                                        &menu::Cancel,
-                                        &self.focus_handle.clone(),
-                                        window,
-                                        cx,
-                                    )
-                                    .map(|kb| kb.size(rems_from_px(12.))),
-                                )
-                                .on_click(cx.listener(|this, _event, _window, cx| {
-                                    this.cancel(&menu::Cancel, cx)
-                                })),
-                        ),
-                    ),
-            )
-    }
-}
-
-pub struct AiPrivacyTooltip {}
-
-impl AiPrivacyTooltip {
-    pub fn new() -> Self {
-        Self {}
-    }
-}
-
-impl Render for AiPrivacyTooltip {
-    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        const DESCRIPTION: &str = "We believe in opt-in data sharing as the default for building AI products, rather than opt-out. We'll only use or store your data if you affirmatively send it to us. ";
-
-        tooltip_container(cx, move |this, _| {
-            this.child(
-                h_flex()
-                    .gap_1()
-                    .child(
-                        Icon::new(IconName::ShieldCheck)
-                            .size(IconSize::Small)
-                            .color(Color::Muted),
-                    )
-                    .child(Label::new("Privacy First")),
-            )
-            .child(
-                div().max_w_64().child(
-                    Label::new(DESCRIPTION)
-                        .size(LabelSize::Small)
-                        .color(Color::Muted),
-                ),
-            )
-        })
-    }
-}

crates/onboarding/src/basics_page.rs 🔗

@@ -2,19 +2,23 @@ use std::sync::Arc;
 
 use client::TelemetrySettings;
 use fs::Fs;
-use gpui::{App, IntoElement};
+use gpui::{Action, App, IntoElement};
 use settings::{BaseKeymap, Settings, update_settings_file};
 use theme::{
     Appearance, SystemAppearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection,
     ThemeSettings,
 };
 use ui::{
-    ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup,
-    ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px,
+    ButtonLike, ParentElement as _, StatefulInteractiveElement, SwitchField, TintColor,
+    ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*,
+    rems_from_px,
 };
 use vim_mode_setting::VimModeSetting;
 
-use crate::theme_preview::{ThemePreviewStyle, ThemePreviewTile};
+use crate::{
+    ImportCursorSettings, ImportVsCodeSettings, SettingsImportState,
+    theme_preview::{ThemePreviewStyle, ThemePreviewTile},
+};
 
 const LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
 const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
@@ -34,16 +38,8 @@ fn get_theme_family_themes(theme_name: &str) -> Option<(&'static str, &'static s
 }
 
 fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
-    let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone();
+    let theme_selection = ThemeSettings::get_global(cx).theme.clone();
     let system_appearance = theme::SystemAppearance::global(cx);
-    let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic {
-        mode: match *system_appearance {
-            Appearance::Light => ThemeMode::Light,
-            Appearance::Dark => ThemeMode::Dark,
-        },
-        light: ThemeName("One Light".into()),
-        dark: ThemeName("One Dark".into()),
-    });
 
     let theme_mode = theme_selection
         .mode()
@@ -78,6 +74,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
                         )
                     }),
                 )
+                .size(ToggleButtonGroupSize::Medium)
                 .tab_index(tab_index)
                 .selected_index(theme_mode as usize)
                 .style(ui::ToggleButtonGroupStyle::Outlined)
@@ -111,7 +108,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
             ThemeMode::Dark => Appearance::Dark,
             ThemeMode::System => *system_appearance,
         };
-        let current_theme_name = SharedString::new(theme_selection.theme(appearance));
+        let current_theme_name: SharedString = theme_selection.name(appearance).0.into();
 
         let theme_names = match appearance {
             Appearance::Light => LIGHT_THEMES,
@@ -228,91 +225,87 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
         .gap_4()
         .border_t_1()
         .border_color(cx.theme().colors().border_variant.opacity(0.5))
-        .child(Label::new("Telemetry").size(LabelSize::Large))
-        .child(SwitchField::new(
-            "onboarding-telemetry-metrics",
-            "Help Improve Zed",
-            Some("Anonymous usage data helps us build the right features and improve your experience.".into()),
-            if TelemetrySettings::get_global(cx).metrics {
-                ui::ToggleState::Selected
-            } else {
-                ui::ToggleState::Unselected
-            },
-            {
-            let fs = fs.clone();
-            move |selection, _, cx| {
-                let enabled = match selection {
-                    ToggleState::Selected => true,
-                    ToggleState::Unselected => false,
-                    ToggleState::Indeterminate => { return; },
-                };
-
-                update_settings_file(
-                    fs.clone(),
-                    cx,
-                    move |setting, _| {
-                        setting.telemetry.get_or_insert_default().metrics = Some(enabled);
-                    }
-                    ,
-                );
-
-                // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
-                // and can fix it in a timely manner to respect a user's choice.
-                telemetry::event!("Welcome Page Telemetry Metrics Toggled",
-                    options = if enabled {
-                        "on"
-                    } else {
-                        "off"
+        .child(
+            SwitchField::new(
+                "onboarding-telemetry-metrics",
+                None::<&str>,
+                Some("Help improve Zed by sending anonymous usage data".into()),
+                if TelemetrySettings::get_global(cx).metrics {
+                    ui::ToggleState::Selected
+                } else {
+                    ui::ToggleState::Unselected
+                },
+                {
+                    let fs = fs.clone();
+                    move |selection, _, cx| {
+                        let enabled = match selection {
+                            ToggleState::Selected => true,
+                            ToggleState::Unselected => false,
+                            ToggleState::Indeterminate => {
+                                return;
+                            }
+                        };
+
+                        update_settings_file(fs.clone(), cx, move |setting, _| {
+                            setting.telemetry.get_or_insert_default().metrics = Some(enabled);
+                        });
+
+                        // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
+                        // and can fix it in a timely manner to respect a user's choice.
+                        telemetry::event!(
+                            "Welcome Page Telemetry Metrics Toggled",
+                            options = if enabled { "on" } else { "off" }
+                        );
                     }
-                );
+                },
+            )
+            .tab_index({
+                *tab_index += 1;
+                *tab_index
+            }),
+        )
+        .child(
+            SwitchField::new(
+                "onboarding-telemetry-crash-reports",
+                None::<&str>,
+                Some(
+                    "Help fix Zed by sending crash reports so we can fix critical issues fast"
+                        .into(),
+                ),
+                if TelemetrySettings::get_global(cx).diagnostics {
+                    ui::ToggleState::Selected
+                } else {
+                    ui::ToggleState::Unselected
+                },
+                {
+                    let fs = fs.clone();
+                    move |selection, _, cx| {
+                        let enabled = match selection {
+                            ToggleState::Selected => true,
+                            ToggleState::Unselected => false,
+                            ToggleState::Indeterminate => {
+                                return;
+                            }
+                        };
 
-            }},
-        ).tab_index({
-            *tab_index += 1;
-            *tab_index
-        }))
-        .child(SwitchField::new(
-            "onboarding-telemetry-crash-reports",
-            "Help Fix Zed",
-            Some("Send crash reports so we can fix critical issues fast.".into()),
-            if TelemetrySettings::get_global(cx).diagnostics {
-                ui::ToggleState::Selected
-            } else {
-                ui::ToggleState::Unselected
-            },
-            {
-                let fs = fs.clone();
-                move |selection, _, cx| {
-                    let enabled = match selection {
-                        ToggleState::Selected => true,
-                        ToggleState::Unselected => false,
-                        ToggleState::Indeterminate => { return; },
-                    };
-
-                    update_settings_file(
-                        fs.clone(),
-                        cx,
-                        move |setting, _| {
+                        update_settings_file(fs.clone(), cx, move |setting, _| {
                             setting.telemetry.get_or_insert_default().diagnostics = Some(enabled);
-                        },
-
-                    );
-
-                    // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
-                    // and can fix it in a timely manner to respect a user's choice.
-                    telemetry::event!("Welcome Page Telemetry Diagnostics Toggled",
-                        options = if enabled {
-                            "on"
-                        } else {
-                            "off"
-                        }
-                    );
-                }
-            }
-        ).tab_index({
-                    *tab_index += 1;
-                    *tab_index
-                }))
+                        });
+
+                        // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
+                        // and can fix it in a timely manner to respect a user's choice.
+                        telemetry::event!(
+                            "Welcome Page Telemetry Diagnostics Toggled",
+                            options = if enabled { "on" } else { "off" }
+                        );
+                    }
+                },
+            )
+            .tab_index({
+                *tab_index += 1;
+                *tab_index
+            }),
+        )
 }
 
 fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
@@ -380,8 +373,8 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
     };
     SwitchField::new(
         "onboarding-vim-mode",
-        "Vim Mode",
-        Some("Coming from Neovim? Use our first-class implementation of Vim Mode.".into()),
+        Some("Vim Mode"),
+        Some("Coming from Neovim? Use our first-class implementation of Vim Mode".into()),
         toggle_state,
         {
             let fs = <dyn Fs>::global(cx);
@@ -410,12 +403,79 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
     })
 }
 
+fn render_setting_import_button(
+    tab_index: isize,
+    label: SharedString,
+    action: &dyn Action,
+    imported: bool,
+) -> impl IntoElement + 'static {
+    let action = action.boxed_clone();
+    h_flex().w_full().child(
+        ButtonLike::new(label.clone())
+            .style(ButtonStyle::OutlinedTransparent)
+            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+            .toggle_state(imported)
+            .size(ButtonSize::Medium)
+            .tab_index(tab_index)
+            .child(
+                h_flex()
+                    .w_full()
+                    .justify_between()
+                    .when(imported, |this| {
+                        this.child(Icon::new(IconName::Check).color(Color::Success))
+                    })
+                    .child(Label::new(label.clone()).mx_2().size(LabelSize::Small)),
+            )
+            .on_click(move |_, window, cx| {
+                telemetry::event!("Welcome Import Settings", import_source = label,);
+                window.dispatch_action(action.boxed_clone(), cx);
+            }),
+    )
+}
+
+fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement {
+    let import_state = SettingsImportState::global(cx);
+    let imports: [(SharedString, &dyn Action, bool); 2] = [
+        (
+            "VS Code".into(),
+            &ImportVsCodeSettings { skip_prompt: false },
+            import_state.vscode,
+        ),
+        (
+            "Cursor".into(),
+            &ImportCursorSettings { skip_prompt: false },
+            import_state.cursor,
+        ),
+    ];
+
+    let [vscode, cursor] = imports.map(|(label, action, imported)| {
+        *tab_index += 1;
+        render_setting_import_button(*tab_index - 1, label, action, imported)
+    });
+
+    h_flex()
+        .child(
+            v_flex()
+                .gap_0p5()
+                .max_w_5_6()
+                .child(Label::new("Import Settings"))
+                .child(
+                    Label::new("Automatically pull your settings from other editors")
+                        .color(Color::Muted),
+                ),
+        )
+        .child(div().w_full())
+        .child(h_flex().gap_1().child(vscode).child(cursor))
+}
+
 pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement {
     let mut tab_index = 0;
     v_flex()
+        .id("basics-page")
         .gap_6()
         .child(render_theme_section(&mut tab_index, cx))
         .child(render_base_keymap_section(&mut tab_index, cx))
+        .child(render_import_settings_section(&mut tab_index, cx))
         .child(render_vim_mode_switch(&mut tab_index, cx))
         .child(render_telemetry_section(&mut tab_index, cx))
 }

crates/onboarding/src/editing_page.rs 🔗

@@ -1,611 +0,0 @@
-use std::sync::Arc;
-
-use editor::{EditorSettings, ShowMinimap};
-use fs::Fs;
-use gpui::{Action, App, FontFeatures, IntoElement, Pixels, SharedString, Window};
-use language::language_settings::{AllLanguageSettings, FormatOnSave};
-use project::project_settings::ProjectSettings;
-use settings::{Settings as _, update_settings_file};
-use theme::{FontFamilyName, ThemeSettings};
-use ui::{
-    ButtonLike, PopoverMenu, SwitchField, ToggleButtonGroup, ToggleButtonGroupStyle,
-    ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
-};
-use ui_input::{NumberField, font_picker};
-
-use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState};
-
-fn read_show_mini_map(cx: &App) -> ShowMinimap {
-    editor::EditorSettings::get_global(cx).minimap.show
-}
-
-fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    // This is used to speed up the UI
-    // the UI reads the current values to get what toggle state to show on buttons
-    // there's a slight delay if we just call update_settings_file so we manually set
-    // the value here then call update_settings file to get around the delay
-    let mut curr_settings = EditorSettings::get_global(cx).clone();
-    curr_settings.minimap.show = show;
-    EditorSettings::override_global(curr_settings, cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        telemetry::event!(
-            "Welcome Minimap Clicked",
-            from = settings.editor.minimap.clone().unwrap_or_default(),
-            to = show
-        );
-        settings.editor.minimap.get_or_insert_default().show = Some(show);
-    });
-}
-
-fn read_inlay_hints(cx: &App) -> bool {
-    AllLanguageSettings::get_global(cx)
-        .defaults
-        .inlay_hints
-        .enabled
-}
-
-fn write_inlay_hints(enabled: bool, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    let mut curr_settings = AllLanguageSettings::get_global(cx).clone();
-    curr_settings.defaults.inlay_hints.enabled = enabled;
-    AllLanguageSettings::override_global(curr_settings, cx);
-
-    update_settings_file(fs, cx, move |settings, _cx| {
-        settings
-            .project
-            .all_languages
-            .defaults
-            .inlay_hints
-            .get_or_insert_default()
-            .enabled = Some(enabled);
-    });
-}
-
-fn read_git_blame(cx: &App) -> bool {
-    ProjectSettings::get_global(cx).git.inline_blame.enabled
-}
-
-fn write_git_blame(enabled: bool, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    let mut curr_settings = ProjectSettings::get_global(cx).clone();
-    curr_settings.git.inline_blame.enabled = enabled;
-    ProjectSettings::override_global(curr_settings, cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        settings
-            .git
-            .get_or_insert_default()
-            .inline_blame
-            .get_or_insert_default()
-            .enabled = Some(enabled);
-    });
-}
-
-fn write_ui_font_family(font: SharedString, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        telemetry::event!(
-            "Welcome Font Changed",
-            type = "ui font",
-            old = settings.theme.ui_font_family,
-            new = font
-        );
-        settings.theme.ui_font_family = Some(FontFamilyName(font.into()));
-    });
-}
-
-fn write_ui_font_size(size: Pixels, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        settings.theme.ui_font_size = Some(size.into());
-    });
-}
-
-fn write_buffer_font_size(size: Pixels, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        settings.theme.buffer_font_size = Some(size.into());
-    });
-}
-
-fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        telemetry::event!(
-            "Welcome Font Changed",
-            type = "editor font",
-            old = settings.theme.buffer_font_family,
-            new = font_family
-        );
-
-        settings.theme.buffer_font_family = Some(FontFamilyName(font_family.into()));
-    });
-}
-
-fn read_font_ligatures(cx: &App) -> bool {
-    ThemeSettings::get_global(cx)
-        .buffer_font
-        .features
-        .is_calt_enabled()
-        .unwrap_or(true)
-}
-
-fn write_font_ligatures(enabled: bool, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-    let bit = if enabled { 1 } else { 0 };
-
-    update_settings_file(fs, cx, move |settings, _| {
-        let mut features = settings
-            .theme
-            .buffer_font_features
-            .as_mut()
-            .map(|features| features.tag_value_list().to_vec())
-            .unwrap_or_default();
-
-        if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
-            features[calt_index].1 = bit;
-        } else {
-            features.push(("calt".into(), bit));
-        }
-
-        settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features)));
-    });
-}
-
-fn read_format_on_save(cx: &App) -> bool {
-    match AllLanguageSettings::get_global(cx).defaults.format_on_save {
-        FormatOnSave::On => true,
-        FormatOnSave::Off => false,
-    }
-}
-
-fn write_format_on_save(format_on_save: bool, cx: &mut App) {
-    let fs = <dyn Fs>::global(cx);
-
-    update_settings_file(fs, cx, move |settings, _| {
-        settings.project.all_languages.defaults.format_on_save = Some(match format_on_save {
-            true => FormatOnSave::On,
-            false => FormatOnSave::Off,
-        });
-    });
-}
-
-fn render_setting_import_button(
-    tab_index: isize,
-    label: SharedString,
-    icon_name: IconName,
-    action: &dyn Action,
-    imported: bool,
-) -> impl IntoElement {
-    let action = action.boxed_clone();
-    h_flex().w_full().child(
-        ButtonLike::new(label.clone())
-            .full_width()
-            .style(ButtonStyle::Outlined)
-            .size(ButtonSize::Large)
-            .tab_index(tab_index)
-            .child(
-                h_flex()
-                    .w_full()
-                    .justify_between()
-                    .child(
-                        h_flex()
-                            .gap_1p5()
-                            .px_1()
-                            .child(
-                                Icon::new(icon_name)
-                                    .color(Color::Muted)
-                                    .size(IconSize::XSmall),
-                            )
-                            .child(Label::new(label.clone())),
-                    )
-                    .when(imported, |this| {
-                        this.child(
-                            h_flex()
-                                .gap_1p5()
-                                .child(
-                                    Icon::new(IconName::Check)
-                                        .color(Color::Success)
-                                        .size(IconSize::XSmall),
-                                )
-                                .child(Label::new("Imported").size(LabelSize::Small)),
-                        )
-                    }),
-            )
-            .on_click(move |_, window, cx| {
-                telemetry::event!("Welcome Import Settings", import_source = label,);
-                window.dispatch_action(action.boxed_clone(), cx);
-            }),
-    )
-}
-
-fn render_import_settings_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
-    let import_state = SettingsImportState::global(cx);
-    let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [
-        (
-            "VS Code".into(),
-            IconName::EditorVsCode,
-            &ImportVsCodeSettings { skip_prompt: false },
-            import_state.vscode,
-        ),
-        (
-            "Cursor".into(),
-            IconName::EditorCursor,
-            &ImportCursorSettings { skip_prompt: false },
-            import_state.cursor,
-        ),
-    ];
-
-    let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| {
-        *tab_index += 1;
-        render_setting_import_button(*tab_index - 1, label, icon_name, action, imported)
-    });
-
-    v_flex()
-        .gap_4()
-        .child(
-            v_flex()
-                .child(Label::new("Import Settings").size(LabelSize::Large))
-                .child(
-                    Label::new("Automatically pull your settings from other editors.")
-                        .color(Color::Muted),
-                ),
-        )
-        .child(h_flex().w_full().gap_4().child(vscode).child(cursor))
-}
-
-fn render_font_customization_section(
-    tab_index: &mut isize,
-    window: &mut Window,
-    cx: &mut App,
-) -> impl IntoElement {
-    let theme_settings = ThemeSettings::get_global(cx);
-    let ui_font_size = theme_settings.ui_font_size(cx);
-    let ui_font_family = theme_settings.ui_font.family.clone();
-    let buffer_font_family = theme_settings.buffer_font.family.clone();
-    let buffer_font_size = theme_settings.buffer_font_size(cx);
-
-    let ui_font_picker =
-        cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx));
-
-    let buffer_font_picker = cx.new(|cx| {
-        font_picker(
-            buffer_font_family.clone(),
-            write_buffer_font_family,
-            window,
-            cx,
-        )
-    });
-
-    let ui_font_handle = ui::PopoverMenuHandle::default();
-    let buffer_font_handle = ui::PopoverMenuHandle::default();
-
-    h_flex()
-        .w_full()
-        .gap_4()
-        .child(
-            v_flex()
-                .w_full()
-                .gap_1()
-                .child(Label::new("UI Font"))
-                .child(
-                    h_flex()
-                        .w_full()
-                        .justify_between()
-                        .gap_2()
-                        .child(
-                            PopoverMenu::new("ui-font-picker")
-                                .menu({
-                                    let ui_font_picker = ui_font_picker;
-                                    move |_window, _cx| Some(ui_font_picker.clone())
-                                })
-                                .trigger(
-                                    ButtonLike::new("ui-font-family-button")
-                                        .style(ButtonStyle::Outlined)
-                                        .size(ButtonSize::Medium)
-                                        .full_width()
-                                        .tab_index({
-                                            *tab_index += 1;
-                                            *tab_index - 1
-                                        })
-                                        .child(
-                                            h_flex()
-                                                .w_full()
-                                                .justify_between()
-                                                .child(Label::new(ui_font_family))
-                                                .child(
-                                                    Icon::new(IconName::ChevronUpDown)
-                                                        .color(Color::Muted)
-                                                        .size(IconSize::XSmall),
-                                                ),
-                                        ),
-                                )
-                                .full_width(true)
-                                .anchor(gpui::Corner::TopLeft)
-                                .offset(gpui::Point {
-                                    x: px(0.0),
-                                    y: px(4.0),
-                                })
-                                .with_handle(ui_font_handle),
-                        )
-                        .child(font_picker_stepper(
-                            "ui-font-size",
-                            &ui_font_size,
-                            tab_index,
-                            write_ui_font_size,
-                            window,
-                            cx,
-                        )),
-                ),
-        )
-        .child(
-            v_flex()
-                .w_full()
-                .gap_1()
-                .child(Label::new("Editor Font"))
-                .child(
-                    h_flex()
-                        .w_full()
-                        .justify_between()
-                        .gap_2()
-                        .child(
-                            PopoverMenu::new("buffer-font-picker")
-                                .menu({
-                                    let buffer_font_picker = buffer_font_picker;
-                                    move |_window, _cx| Some(buffer_font_picker.clone())
-                                })
-                                .trigger(
-                                    ButtonLike::new("buffer-font-family-button")
-                                        .style(ButtonStyle::Outlined)
-                                        .size(ButtonSize::Medium)
-                                        .full_width()
-                                        .tab_index({
-                                            *tab_index += 1;
-                                            *tab_index - 1
-                                        })
-                                        .child(
-                                            h_flex()
-                                                .w_full()
-                                                .justify_between()
-                                                .child(Label::new(buffer_font_family))
-                                                .child(
-                                                    Icon::new(IconName::ChevronUpDown)
-                                                        .color(Color::Muted)
-                                                        .size(IconSize::XSmall),
-                                                ),
-                                        ),
-                                )
-                                .full_width(true)
-                                .anchor(gpui::Corner::TopLeft)
-                                .offset(gpui::Point {
-                                    x: px(0.0),
-                                    y: px(4.0),
-                                })
-                                .with_handle(buffer_font_handle),
-                        )
-                        .child(font_picker_stepper(
-                            "buffer-font-size",
-                            &buffer_font_size,
-                            tab_index,
-                            write_buffer_font_size,
-                            window,
-                            cx,
-                        )),
-                ),
-        )
-}
-
-fn font_picker_stepper(
-    id: &'static str,
-    font_size: &Pixels,
-    tab_index: &mut isize,
-    write_font_size: fn(Pixels, &mut App),
-    window: &mut Window,
-    cx: &mut App,
-) -> NumberField<u32> {
-    window.with_id(id, |window| {
-        let optimistic_font_size: gpui::Entity<Option<u32>> = window.use_state(cx, |_, _| None);
-        optimistic_font_size.update(cx, |optimistic_font_size, _| {
-            if let Some(optimistic_font_size_val) = optimistic_font_size {
-                if *optimistic_font_size_val == u32::from(font_size) {
-                    *optimistic_font_size = None;
-                }
-            }
-        });
-
-        let stepper_font_size = optimistic_font_size
-            .read(cx)
-            .unwrap_or_else(|| font_size.into());
-
-        NumberField::new(
-            SharedString::new(format!("{}-stepper", id)),
-            stepper_font_size,
-            window,
-            cx,
-        )
-        .on_change(move |new_value, _, cx| {
-            optimistic_font_size.write(cx, Some(*new_value));
-            write_font_size(Pixels::from(*new_value), cx);
-        })
-        .format(|value| format!("{value}px"))
-        .tab_index({
-            *tab_index += 2;
-            *tab_index - 2
-        })
-        .min(6)
-        .max(32)
-    })
-}
-
-fn render_popular_settings_section(
-    tab_index: &mut isize,
-    window: &mut Window,
-    cx: &mut App,
-) -> impl IntoElement {
-    const LIGATURE_TOOLTIP: &str =
-        "Font ligatures combine two characters into one. For example, turning != into ≠.";
-
-    v_flex()
-        .pt_6()
-        .gap_4()
-        .border_t_1()
-        .border_color(cx.theme().colors().border_variant.opacity(0.5))
-        .child(Label::new("Popular Settings").size(LabelSize::Large))
-        .child(render_font_customization_section(tab_index, window, cx))
-        .child(
-            SwitchField::new(
-                "onboarding-font-ligatures",
-                "Font Ligatures",
-                Some("Combine text characters into their associated symbols.".into()),
-                if read_font_ligatures(cx) {
-                    ui::ToggleState::Selected
-                } else {
-                    ui::ToggleState::Unselected
-                },
-                |toggle_state, _, cx| {
-                    let enabled = toggle_state == &ToggleState::Selected;
-                    telemetry::event!(
-                        "Welcome Font Ligature",
-                        options = if enabled { "on" } else { "off" },
-                    );
-
-                    write_font_ligatures(enabled, cx);
-                },
-            )
-            .tab_index({
-                *tab_index += 1;
-                *tab_index - 1
-            })
-            .tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
-        )
-        .child(
-            SwitchField::new(
-                "onboarding-format-on-save",
-                "Format on Save",
-                Some("Format code automatically when saving.".into()),
-                if read_format_on_save(cx) {
-                    ui::ToggleState::Selected
-                } else {
-                    ui::ToggleState::Unselected
-                },
-                |toggle_state, _, cx| {
-                    let enabled = toggle_state == &ToggleState::Selected;
-                    telemetry::event!(
-                        "Welcome Format On Save Changed",
-                        options = if enabled { "on" } else { "off" },
-                    );
-
-                    write_format_on_save(enabled, cx);
-                },
-            )
-            .tab_index({
-                *tab_index += 1;
-                *tab_index - 1
-            }),
-        )
-        .child(
-            SwitchField::new(
-                "onboarding-enable-inlay-hints",
-                "Inlay Hints",
-                Some("See parameter names for function and method calls inline.".into()),
-                if read_inlay_hints(cx) {
-                    ui::ToggleState::Selected
-                } else {
-                    ui::ToggleState::Unselected
-                },
-                |toggle_state, _, cx| {
-                    let enabled = toggle_state == &ToggleState::Selected;
-                    telemetry::event!(
-                        "Welcome Inlay Hints Changed",
-                        options = if enabled { "on" } else { "off" },
-                    );
-
-                    write_inlay_hints(enabled, cx);
-                },
-            )
-            .tab_index({
-                *tab_index += 1;
-                *tab_index - 1
-            }),
-        )
-        .child(
-            SwitchField::new(
-                "onboarding-git-blame-switch",
-                "Inline Git Blame",
-                Some("See who committed each line on a given file.".into()),
-                if read_git_blame(cx) {
-                    ui::ToggleState::Selected
-                } else {
-                    ui::ToggleState::Unselected
-                },
-                |toggle_state, _, cx| {
-                    let enabled = toggle_state == &ToggleState::Selected;
-                    telemetry::event!(
-                        "Welcome Git Blame Changed",
-                        options = if enabled { "on" } else { "off" },
-                    );
-
-                    write_git_blame(enabled, cx);
-                },
-            )
-            .tab_index({
-                *tab_index += 1;
-                *tab_index - 1
-            }),
-        )
-        .child(
-            h_flex()
-                .items_start()
-                .justify_between()
-                .child(
-                    v_flex().child(Label::new("Minimap")).child(
-                        Label::new("See a high-level overview of your source code.")
-                            .color(Color::Muted),
-                    ),
-                )
-                .child(
-                    ToggleButtonGroup::single_row(
-                        "onboarding-show-mini-map",
-                        [
-                            ToggleButtonSimple::new("Auto", |_, _, cx| {
-                                write_show_mini_map(ShowMinimap::Auto, cx);
-                            })
-                            .tooltip(Tooltip::text(
-                                "Show the minimap if the editor's scrollbar is visible.",
-                            )),
-                            ToggleButtonSimple::new("Always", |_, _, cx| {
-                                write_show_mini_map(ShowMinimap::Always, cx);
-                            }),
-                            ToggleButtonSimple::new("Never", |_, _, cx| {
-                                write_show_mini_map(ShowMinimap::Never, cx);
-                            }),
-                        ],
-                    )
-                    .selected_index(match read_show_mini_map(cx) {
-                        ShowMinimap::Auto => 0,
-                        ShowMinimap::Always => 1,
-                        ShowMinimap::Never => 2,
-                    })
-                    .tab_index(tab_index)
-                    .style(ToggleButtonGroupStyle::Outlined)
-                    .width(ui::rems_from_px(3. * 64.)),
-                ),
-        )
-}
-
-pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
-    let mut tab_index = 0;
-    v_flex()
-        .gap_6()
-        .child(render_import_settings_section(&mut tab_index, cx))
-        .child(render_popular_settings_section(&mut tab_index, window, cx))
-}

crates/onboarding/src/onboarding.rs 🔗

@@ -14,8 +14,8 @@ use serde::Deserialize;
 use settings::{SettingsStore, VsCodeSettingsSource};
 use std::sync::Arc;
 use ui::{
-    Avatar, ButtonLike, FluentBuilder, Headline, KeyBinding, ParentElement as _,
-    StatefulInteractiveElement, Vector, VectorName, WithScrollbar, prelude::*, rems_from_px,
+    KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName,
+    WithScrollbar as _, prelude::*, rems_from_px,
 };
 pub use ui_input::font_picker;
 use workspace::{
@@ -26,10 +26,8 @@ use workspace::{
     open_new, register_serializable_item, with_active_or_new_workspace,
 };
 
-mod ai_setup_page;
 mod base_keymap_picker;
 mod basics_page;
-mod editing_page;
 pub mod multibuffer_hint;
 mod theme_preview;
 mod welcome;
@@ -66,12 +64,6 @@ actions!(
 actions!(
     onboarding,
     [
-        /// Activates the Basics page.
-        ActivateBasicsPage,
-        /// Activates the Editing page.
-        ActivateEditingPage,
-        /// Activates the AI Setup page.
-        ActivateAISetupPage,
         /// Finish the onboarding process.
         Finish,
         /// Sign in while in the onboarding flow.
@@ -216,27 +208,9 @@ pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyh
     )
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum SelectedPage {
-    Basics,
-    Editing,
-    AiSetup,
-}
-
-impl SelectedPage {
-    fn name(&self) -> &'static str {
-        match self {
-            SelectedPage::Basics => "Basics",
-            SelectedPage::Editing => "Editing",
-            SelectedPage::AiSetup => "AI Setup",
-        }
-    }
-}
-
 struct Onboarding {
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
-    selected_page: SelectedPage,
     user_store: Entity<UserStore>,
     scroll_handle: ScrollHandle,
     _settings_subscription: Subscription,
@@ -259,7 +233,6 @@ impl Onboarding {
                 workspace: workspace.weak_handle(),
                 focus_handle: cx.focus_handle(),
                 scroll_handle: ScrollHandle::new(),
-                selected_page: SelectedPage::Basics,
                 user_store: workspace.user_store().clone(),
                 _settings_subscription: cx
                     .observe_global::<SettingsStore>(move |_, cx| cx.notify()),
@@ -267,228 +240,8 @@ impl Onboarding {
         })
     }
 
-    fn set_page(
-        &mut self,
-        page: SelectedPage,
-        clicked: Option<&'static str>,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(click) = clicked {
-            telemetry::event!(
-                "Welcome Tab Clicked",
-                from = self.selected_page.name(),
-                to = page.name(),
-                clicked = click,
-            );
-        }
-
-        self.selected_page = page;
-        self.scroll_handle.set_offset(Default::default());
-        cx.notify();
-        cx.emit(ItemEvent::UpdateTab);
-    }
-
-    fn render_nav_buttons(
-        &mut self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> [impl IntoElement; 3] {
-        let pages = [
-            SelectedPage::Basics,
-            SelectedPage::Editing,
-            SelectedPage::AiSetup,
-        ];
-
-        let text = ["Basics", "Editing", "AI Setup"];
-
-        let actions: [&dyn Action; 3] = [
-            &ActivateBasicsPage,
-            &ActivateEditingPage,
-            &ActivateAISetupPage,
-        ];
-
-        let mut binding = actions.map(|action| {
-            KeyBinding::for_action_in(action, &self.focus_handle, window, cx)
-                .map(|kb| kb.size(rems_from_px(12.)))
-        });
-
-        pages.map(|page| {
-            let i = page as usize;
-            let selected = self.selected_page == page;
-            h_flex()
-                .id(text[i])
-                .relative()
-                .w_full()
-                .gap_2()
-                .px_2()
-                .py_0p5()
-                .justify_between()
-                .rounded_sm()
-                .when(selected, |this| {
-                    this.child(
-                        div()
-                            .h_4()
-                            .w_px()
-                            .bg(cx.theme().colors().text_accent)
-                            .absolute()
-                            .left_0(),
-                    )
-                })
-                .hover(|style| style.bg(cx.theme().colors().element_hover))
-                .child(Label::new(text[i]).map(|this| {
-                    if selected {
-                        this.color(Color::Default)
-                    } else {
-                        this.color(Color::Muted)
-                    }
-                }))
-                .child(binding[i].take().map_or(
-                    gpui::Empty.into_any_element(),
-                    IntoElement::into_any_element,
-                ))
-                .on_click(cx.listener(move |this, click_event, _, cx| {
-                    let click = match click_event {
-                        gpui::ClickEvent::Mouse(_) => "mouse",
-                        gpui::ClickEvent::Keyboard(_) => "keyboard",
-                    };
-
-                    this.set_page(page, Some(click), cx);
-                }))
-        })
-    }
-
-    fn render_nav(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        v_flex()
-            .h_full()
-            .w(rems_from_px(220.))
-            .flex_shrink_0()
-            .gap_4()
-            .justify_between()
-            .child(
-                v_flex()
-                    .gap_6()
-                    .child(
-                        h_flex()
-                            .px_2()
-                            .gap_4()
-                            .child(Vector::square(VectorName::ZedLogo, rems(2.5)))
-                            .child(
-                                v_flex()
-                                    .child(
-                                        Headline::new("Welcome to Zed").size(HeadlineSize::Small),
-                                    )
-                                    .child(
-                                        Label::new("The editor for what's next")
-                                            .color(Color::Muted)
-                                            .size(LabelSize::Small)
-                                            .italic(),
-                                    ),
-                            ),
-                    )
-                    .child(
-                        v_flex()
-                            .gap_4()
-                            .child(
-                                v_flex()
-                                    .py_4()
-                                    .border_y_1()
-                                    .border_color(cx.theme().colors().border_variant.opacity(0.5))
-                                    .gap_1()
-                                    .children(self.render_nav_buttons(window, cx)),
-                            )
-                            .map(|this| {
-                                if let Some(user) = self.user_store.read(cx).current_user() {
-                                    this.child(
-                                        v_flex()
-                                            .gap_1()
-                                            .child(
-                                                h_flex()
-                                                    .ml_2()
-                                                    .gap_2()
-                                                    .max_w_full()
-                                                    .w_full()
-                                                    .child(Avatar::new(user.avatar_uri.clone()))
-                                                    .child(
-                                                        Label::new(user.github_login.clone())
-                                                            .truncate(),
-                                                    ),
-                                            )
-                                            .child(
-                                                ButtonLike::new("open_account")
-                                                    .size(ButtonSize::Medium)
-                                                    .child(
-                                                        h_flex()
-                                                            .ml_1()
-                                                            .w_full()
-                                                            .justify_between()
-                                                            .child(Label::new("Open Account"))
-                                                            .children(
-                                                                KeyBinding::for_action_in(
-                                                                    &OpenAccount,
-                                                                    &self.focus_handle,
-                                                                    window,
-                                                                    cx,
-                                                                )
-                                                                .map(|kb| {
-                                                                    kb.size(rems_from_px(12.))
-                                                                }),
-                                                            ),
-                                                    )
-                                                    .on_click(|_, window, cx| {
-                                                        window.dispatch_action(
-                                                            OpenAccount.boxed_clone(),
-                                                            cx,
-                                                        );
-                                                    }),
-                                            ),
-                                    )
-                                } else {
-                                    this.child(
-                                        ButtonLike::new("sign_in")
-                                            .size(ButtonSize::Medium)
-                                            .child(
-                                                h_flex()
-                                                    .ml_1()
-                                                    .w_full()
-                                                    .justify_between()
-                                                    .child(Label::new("Sign In"))
-                                                    .children(
-                                                        KeyBinding::for_action_in(
-                                                            &SignIn,
-                                                            &self.focus_handle,
-                                                            window,
-                                                            cx,
-                                                        )
-                                                        .map(|kb| kb.size(rems_from_px(12.))),
-                                                    ),
-                                            )
-                                            .on_click(|_, window, cx| {
-                                                telemetry::event!("Welcome Sign In Clicked");
-                                                window.dispatch_action(SignIn.boxed_clone(), cx);
-                                            }),
-                                    )
-                                }
-                            }),
-                    ),
-            )
-            .child({
-                Button::new("start_building", "Start Building")
-                    .full_width()
-                    .style(ButtonStyle::Outlined)
-                    .size(ButtonSize::Medium)
-                    .key_binding(
-                        KeyBinding::for_action_in(&Finish, &self.focus_handle, window, cx)
-                            .map(|kb| kb.size(rems_from_px(12.))),
-                    )
-                    .on_click(|_, window, cx| {
-                        telemetry::event!("Welcome Start Building Clicked");
-                        window.dispatch_action(Finish.boxed_clone(), cx);
-                    })
-            })
-    }
-
     fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) {
-        telemetry::event!("Welcome Skip Clicked");
+        telemetry::event!("Finish Setup");
         go_to_welcome_page(cx);
     }
 
@@ -509,29 +262,14 @@ impl Onboarding {
         cx.open_url(&zed_urls::account_url(cx))
     }
 
-    fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
-        let client = Client::global(cx);
-
-        match self.selected_page {
-            SelectedPage::Basics => crate::basics_page::render_basics_page(cx).into_any_element(),
-            SelectedPage::Editing => {
-                crate::editing_page::render_editing_page(window, cx).into_any_element()
-            }
-            SelectedPage::AiSetup => crate::ai_setup_page::render_ai_setup_page(
-                self.workspace.clone(),
-                self.user_store.clone(),
-                client,
-                window,
-                cx,
-            )
-            .into_any_element(),
-        }
+    fn render_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
+        crate::basics_page::render_basics_page(cx).into_any_element()
     }
 }
 
 impl Render for Onboarding {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        h_flex()
+        div()
             .image_cache(gpui::retain_all("onboarding-page"))
             .key_context({
                 let mut ctx = KeyContext::new_with_defaults();
@@ -545,15 +283,6 @@ impl Render for Onboarding {
             .on_action(Self::on_finish)
             .on_action(Self::handle_sign_in)
             .on_action(Self::handle_open_account)
-            .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
-                this.set_page(SelectedPage::Basics, Some("action"), cx);
-            }))
-            .on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| {
-                this.set_page(SelectedPage::Editing, Some("action"), cx);
-            }))
-            .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| {
-                this.set_page(SelectedPage::AiSetup, Some("action"), cx);
-            }))
             .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| {
                 window.focus_next();
                 cx.notify();
@@ -563,35 +292,68 @@ impl Render for Onboarding {
                 cx.notify();
             }))
             .child(
-                h_flex()
-                    .max_w(rems_from_px(1100.))
-                    .max_h(rems_from_px(850.))
+                div()
+                    .max_w(Rems(48.0))
+                    .w_full()
+                    .mx_auto()
                     .size_full()
-                    .m_auto()
-                    .py_20()
-                    .px_12()
-                    .items_start()
-                    .gap_12()
-                    .child(self.render_nav(window, cx))
+                    .gap_6()
                     .child(
-                        div()
+                        v_flex()
+                            .m_auto()
+                            .id("page-content")
+                            .gap_6()
                             .size_full()
-                            .pr_6()
+                            .max_w_full()
+                            .min_w_0()
+                            .p_12()
+                            .border_color(cx.theme().colors().border_variant.opacity(0.5))
+                            .overflow_y_scroll()
                             .child(
-                                v_flex()
-                                    .id("page-content")
-                                    .size_full()
-                                    .max_w_full()
-                                    .min_w_0()
-                                    .pl_12()
-                                    .border_l_1()
-                                    .border_color(cx.theme().colors().border_variant.opacity(0.5))
-                                    .overflow_y_scroll()
-                                    .child(self.render_page(window, cx))
-                                    .track_scroll(&self.scroll_handle),
+                                h_flex()
+                                    .w_full()
+                                    .gap_4()
+                                    .child(Vector::square(VectorName::ZedLogo, rems(2.5)))
+                                    .child(
+                                        v_flex()
+                                            .child(
+                                                Headline::new("Welcome to Zed")
+                                                    .size(HeadlineSize::Small),
+                                            )
+                                            .child(
+                                                Label::new("The editor for what's next")
+                                                    .color(Color::Muted)
+                                                    .size(LabelSize::Small)
+                                                    .italic(),
+                                            ),
+                                    )
+                                    .child(div().w_full())
+                                    .child({
+                                        Button::new("finish_setup", "Finish Setup")
+                                            .style(ButtonStyle::Filled)
+                                            .size(ButtonSize::Large)
+                                            .width(Rems(12.0))
+                                            .key_binding(
+                                                KeyBinding::for_action_in(
+                                                    &Finish,
+                                                    &self.focus_handle,
+                                                    window,
+                                                    cx,
+                                                )
+                                                .map(|kb| kb.size(rems_from_px(12.))),
+                                            )
+                                            .on_click(|_, window, cx| {
+                                                window.dispatch_action(Finish.boxed_clone(), cx);
+                                            })
+                                    })
+                                    .pb_6()
+                                    .border_b_1()
+                                    .border_color(cx.theme().colors().border_variant.opacity(0.5)),
                             )
-                            .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
-                    ),
+                            .child(self.render_page(cx))
+                            .track_scroll(&self.scroll_handle),
+                    )
+                    .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
             )
     }
 }
@@ -628,7 +390,6 @@ impl Item for Onboarding {
         Some(cx.new(|cx| Onboarding {
             workspace: self.workspace.clone(),
             user_store: self.user_store.clone(),
-            selected_page: self.selected_page,
             scroll_handle: ScrollHandle::new(),
             focus_handle: cx.focus_handle(),
             _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
@@ -814,25 +575,10 @@ impl workspace::SerializableItem for Onboarding {
         cx: &mut App,
     ) -> gpui::Task<gpui::Result<Entity<Self>>> {
         window.spawn(cx, async move |cx| {
-            if let Some(page_number) =
+            if let Some(_) =
                 persistence::ONBOARDING_PAGES.get_onboarding_page(item_id, workspace_id)?
             {
-                let page = match page_number {
-                    0 => Some(SelectedPage::Basics),
-                    1 => Some(SelectedPage::Editing),
-                    2 => Some(SelectedPage::AiSetup),
-                    _ => None,
-                };
-                workspace.update(cx, |workspace, cx| {
-                    let onboarding_page = Onboarding::new(workspace, cx);
-                    if let Some(page) = page {
-                        zlog::info!("Onboarding page {page:?} loaded");
-                        onboarding_page.update(cx, |onboarding_page, cx| {
-                            onboarding_page.set_page(page, None, cx);
-                        })
-                    }
-                    onboarding_page
-                })
+                workspace.update(cx, |workspace, cx| Onboarding::new(workspace, cx))
             } else {
                 Err(anyhow::anyhow!("No onboarding page to deserialize"))
             }
@@ -848,10 +594,10 @@ impl workspace::SerializableItem for Onboarding {
         cx: &mut ui::Context<Self>,
     ) -> Option<gpui::Task<gpui::Result<()>>> {
         let workspace_id = workspace.database_id()?;
-        let page_number = self.selected_page as u16;
+
         Some(cx.background_spawn(async move {
             persistence::ONBOARDING_PAGES
-                .save_onboarding_page(item_id, workspace_id, page_number)
+                .save_onboarding_page(item_id, workspace_id)
                 .await
         }))
     }
@@ -874,17 +620,32 @@ mod persistence {
     impl Domain for OnboardingPagesDb {
         const NAME: &str = stringify!(OnboardingPagesDb);
 
-        const MIGRATIONS: &[&str] = &[sql!(
-                    CREATE TABLE onboarding_pages (
-                        workspace_id INTEGER,
-                        item_id INTEGER UNIQUE,
-                        page_number INTEGER,
-
-                        PRIMARY KEY(workspace_id, item_id),
-                        FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
-                        ON DELETE CASCADE
-                    ) STRICT;
-        )];
+        const MIGRATIONS: &[&str] = &[
+            sql!(
+                        CREATE TABLE onboarding_pages (
+                            workspace_id INTEGER,
+                            item_id INTEGER UNIQUE,
+                            page_number INTEGER,
+
+                            PRIMARY KEY(workspace_id, item_id),
+                            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                            ON DELETE CASCADE
+                        ) STRICT;
+            ),
+            sql!(
+                        CREATE TABLE onboarding_pages_2 (
+                            workspace_id INTEGER,
+                            item_id INTEGER UNIQUE,
+
+                            PRIMARY KEY(workspace_id, item_id),
+                            FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+                            ON DELETE CASCADE
+                        ) STRICT;
+                        INSERT INTO onboarding_pages_2 SELECT workspace_id, item_id FROM onboarding_pages;
+                        DROP TABLE onboarding_pages;
+                        ALTER TABLE onboarding_pages_2 RENAME TO onboarding_pages;
+            ),
+        ];
     }
 
     db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
@@ -893,11 +654,10 @@ mod persistence {
         query! {
             pub async fn save_onboarding_page(
                 item_id: workspace::ItemId,
-                workspace_id: workspace::WorkspaceId,
-                page_number: u16
+                workspace_id: workspace::WorkspaceId
             ) -> Result<()> {
-                INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id, page_number)
-                VALUES (?, ?, ?)
+                INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id)
+                VALUES (?, ?)
             }
         }
 
@@ -905,8 +665,8 @@ mod persistence {
             pub fn get_onboarding_page(
                 item_id: workspace::ItemId,
                 workspace_id: workspace::WorkspaceId
-            ) -> Result<Option<u16>> {
-                SELECT page_number
+            ) -> Result<Option<workspace::ItemId>> {
+                SELECT item_id
                 FROM onboarding_pages
                 WHERE item_id = ? AND workspace_id = ?
             }

crates/onboarding/src/welcome.rs 🔗

@@ -9,7 +9,7 @@ use workspace::{
     item::{Item, ItemEvent},
     with_active_or_new_workspace,
 };
-use zed_actions::{Extensions, OpenSettings, agent, command_palette};
+use zed_actions::{Extensions, OpenSettingsEditor, agent, command_palette};
 
 use crate::{Onboarding, OpenOnboarding};
 
@@ -53,7 +53,7 @@ const CONTENT: (Section<4>, Section<3>) = (
             SectionEntry {
                 icon: IconName::Settings,
                 title: "Open Settings",
-                action: &OpenSettings,
+                action: &OpenSettingsEditor,
             },
             SectionEntry {
                 icon: IconName::ZedAssistant,
@@ -151,6 +151,7 @@ impl SectionEntry {
 }
 
 pub struct WelcomePage {
+    first_paint: bool,
     focus_handle: FocusHandle,
 }
 
@@ -168,6 +169,10 @@ impl WelcomePage {
 
 impl Render for WelcomePage {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        if self.first_paint {
+            window.request_animation_frame();
+            self.first_paint = false;
+        }
         let (first_section, second_section) = CONTENT;
         let first_section_entries = first_section.entries.len();
         let last_index = first_section_entries + second_section.entries.len();
@@ -311,7 +316,10 @@ impl WelcomePage {
             cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
                 .detach();
 
-            WelcomePage { focus_handle }
+            WelcomePage {
+                first_paint: true,
+                focus_handle,
+            }
         })
     }
 }

crates/outline_panel/src/outline_panel.rs 🔗

@@ -2668,7 +2668,7 @@ impl OutlinePanel {
                     |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
                         let buffer_id = buffer_snapshot.remote_id();
                         let file = File::from_dyn(buffer_snapshot.file());
-                        let entry_id = file.and_then(|file| file.project_entry_id(cx));
+                        let entry_id = file.and_then(|file| file.project_entry_id());
                         let worktree = file.map(|file| file.worktree.read(cx).snapshot());
                         let is_new = new_entries.contains(&excerpt_id)
                             || !outline_panel.excerpts.contains_key(&buffer_id);

crates/outline_panel/src/outline_panel_settings.rs 🔗

@@ -41,7 +41,7 @@ impl ScrollbarVisibility for OutlinePanelSettings {
 }
 
 impl Settings for OutlinePanelSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let panel = content.outline_panel.as_ref().unwrap();
         Self {
             button: panel.button.unwrap(),

crates/project/src/agent_server_store.rs 🔗

@@ -8,21 +8,20 @@ use std::{
 };
 
 use anyhow::{Context as _, Result, bail};
-use client::Client;
 use collections::HashMap;
 use feature_flags::FeatureFlagAppExt as _;
 use fs::{Fs, RemoveOptions, RenameOptions};
 use futures::StreamExt as _;
 use gpui::{
-    App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
+    AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
 };
-use http_client::github::AssetKind;
+use http_client::{HttpClient, github::AssetKind};
 use node_runtime::NodeRuntime;
 use remote::RemoteClient;
 use rpc::{AnyProtoClient, TypedEnvelope, proto};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{SettingsContent, SettingsStore};
+use settings::SettingsStore;
 use util::{ResultExt as _, debug_panic};
 
 use crate::ProjectEnvironment;
@@ -114,6 +113,7 @@ enum AgentServerStoreState {
         project_environment: Entity<ProjectEnvironment>,
         downstream_client: Option<(u64, AnyProtoClient)>,
         settings: Option<AllAgentServersSettings>,
+        http_client: Arc<dyn HttpClient>,
         _subscriptions: [Subscription; 1],
     },
     Remote {
@@ -174,6 +174,7 @@ impl AgentServerStore {
             project_environment,
             downstream_client,
             settings: old_settings,
+            http_client,
             ..
         } = &mut self.state
         else {
@@ -227,6 +228,8 @@ impl AgentServerStore {
                         .codex
                         .clone()
                         .and_then(|settings| settings.custom_command()),
+                    http_client: http_client.clone(),
+                    is_remote: downstream_client.is_some(),
                 }),
             );
         }
@@ -253,7 +256,6 @@ impl AgentServerStore {
                     names: self
                         .external_agents
                         .keys()
-                        .filter(|name| name.0 != CODEX_NAME)
                         .map(|name| name.to_string())
                         .collect(),
                 })
@@ -266,6 +268,7 @@ impl AgentServerStore {
         node_runtime: NodeRuntime,
         fs: Arc<dyn Fs>,
         project_environment: Entity<ProjectEnvironment>,
+        http_client: Arc<dyn HttpClient>,
         cx: &mut Context<Self>,
     ) -> Self {
         let subscription = cx.observe_global::<SettingsStore>(|this, cx| {
@@ -283,6 +286,7 @@ impl AgentServerStore {
                 node_runtime,
                 fs,
                 project_environment,
+                http_client,
                 downstream_client: None,
                 settings: None,
                 _subscriptions: [subscription],
@@ -297,12 +301,12 @@ impl AgentServerStore {
     pub(crate) fn remote(
         project_id: u64,
         upstream_client: Entity<RemoteClient>,
-        _cx: &mut Context<Self>,
+        cx: &mut Context<Self>,
     ) -> Self {
         // Set up the builtin agents here so they're immediately available in
         // remote projects--we know that the HeadlessProject on the other end
         // will have them.
-        let external_agents = [
+        let mut external_agents = [
             (
                 GEMINI_NAME.into(),
                 Box::new(RemoteExternalAgentServer {
@@ -325,7 +329,21 @@ impl AgentServerStore {
             ),
         ]
         .into_iter()
-        .collect();
+        .collect::<HashMap<ExternalAgentServerName, Box<dyn ExternalAgentServer>>>();
+
+        use feature_flags::FeatureFlagAppExt as _;
+        if cx.has_flag::<feature_flags::CodexAcpFeatureFlag>() {
+            external_agents.insert(
+                CODEX_NAME.into(),
+                Box::new(RemoteExternalAgentServer {
+                    project_id,
+                    upstream_client: upstream_client.clone(),
+                    name: CODEX_NAME.into(),
+                    status_tx: None,
+                    new_version_available_tx: None,
+                }) as Box<dyn ExternalAgentServer>,
+            );
+        }
 
         Self {
             state: AgentServerStoreState::Remote {
@@ -1003,7 +1021,9 @@ impl ExternalAgentServer for LocalClaudeCode {
 struct LocalCodex {
     fs: Arc<dyn Fs>,
     project_environment: Entity<ProjectEnvironment>,
+    http_client: Arc<dyn HttpClient>,
     custom_command: Option<AgentServerCommand>,
+    is_remote: bool,
 }
 
 impl ExternalAgentServer for LocalCodex {
@@ -1017,11 +1037,13 @@ impl ExternalAgentServer for LocalCodex {
     ) -> Task<Result<(AgentServerCommand, String, Option<task::SpawnInTerminal>)>> {
         let fs = self.fs.clone();
         let project_environment = self.project_environment.downgrade();
+        let http = self.http_client.clone();
         let custom_command = self.custom_command.clone();
         let root_dir: Arc<Path> = root_dir
             .map(|root_dir| Path::new(root_dir))
             .unwrap_or(paths::home_dir())
             .into();
+        let is_remote = self.is_remote;
 
         cx.spawn(async move |cx| {
             let mut env = project_environment
@@ -1030,6 +1052,9 @@ impl ExternalAgentServer for LocalCodex {
                 })?
                 .await
                 .unwrap_or_default();
+            if is_remote {
+                env.insert("NO_BROWSER".to_owned(), "1".to_owned());
+            }
 
             let mut command = if let Some(mut custom_command) = custom_command {
                 env.extend(custom_command.env.unwrap_or_default());
@@ -1040,7 +1065,6 @@ impl ExternalAgentServer for LocalCodex {
                 fs.create_dir(&dir).await?;
 
                 // Find or install the latest Codex release (no update checks for now).
-                let http = cx.update(|cx| Client::global(cx).http_client())?;
                 let release = ::http_client::github::latest_github_release(
                     CODEX_ACP_REPO,
                     true,
@@ -1294,7 +1318,7 @@ impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
 }
 
 impl settings::Settings for AllAgentServersSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let agent_settings = content.agent_servers.clone().unwrap();
         Self {
             gemini: agent_settings.gemini.map(Into::into),
@@ -1307,6 +1331,4 @@ impl settings::Settings for AllAgentServersSettings {
                 .collect(),
         }
     }
-
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
 }

crates/project/src/debugger/breakpoint_store.rs 🔗

@@ -164,6 +164,7 @@ pub struct BreakpointStore {
 
 impl BreakpointStore {
     pub fn init(client: &AnyProtoClient) {
+        log::error!("breakpoint store init");
         client.add_entity_request_handler(Self::handle_toggle_breakpoint);
         client.add_entity_message_handler(Self::handle_breakpoints_for_file);
     }

crates/project/src/debugger/dap_store.rs 🔗

@@ -22,9 +22,9 @@ use dap::{
     inline_value::VariableLookupKind,
     messages::Message,
 };
-use fs::Fs;
+use fs::{Fs, RemoveOptions};
 use futures::{
-    StreamExt,
+    StreamExt, TryStreamExt as _,
     channel::mpsc::{self, UnboundedSender},
     future::{Shared, join_all},
 };
@@ -78,12 +78,15 @@ pub struct LocalDapStore {
     http_client: Arc<dyn HttpClient>,
     environment: Entity<ProjectEnvironment>,
     toolchain_store: Arc<dyn LanguageToolchainStore>,
+    is_headless: bool,
 }
 
 pub struct RemoteDapStore {
     remote_client: Entity<RemoteClient>,
     upstream_client: AnyProtoClient,
     upstream_project_id: u64,
+    node_runtime: NodeRuntime,
+    http_client: Arc<dyn HttpClient>,
 }
 
 pub struct DapStore {
@@ -134,17 +137,19 @@ impl DapStore {
         toolchain_store: Arc<dyn LanguageToolchainStore>,
         worktree_store: Entity<WorktreeStore>,
         breakpoint_store: Entity<BreakpointStore>,
+        is_headless: bool,
         cx: &mut Context<Self>,
     ) -> Self {
         let mode = DapStoreMode::Local(LocalDapStore {
-            fs,
+            fs: fs.clone(),
             environment,
             http_client,
             node_runtime,
             toolchain_store,
+            is_headless,
         });
 
-        Self::new(mode, breakpoint_store, worktree_store, cx)
+        Self::new(mode, breakpoint_store, worktree_store, fs, cx)
     }
 
     pub fn new_remote(
@@ -152,15 +157,20 @@ impl DapStore {
         remote_client: Entity<RemoteClient>,
         breakpoint_store: Entity<BreakpointStore>,
         worktree_store: Entity<WorktreeStore>,
+        node_runtime: NodeRuntime,
+        http_client: Arc<dyn HttpClient>,
+        fs: Arc<dyn Fs>,
         cx: &mut Context<Self>,
     ) -> Self {
         let mode = DapStoreMode::Remote(RemoteDapStore {
             upstream_client: remote_client.read(cx).proto_client(),
             remote_client,
             upstream_project_id: project_id,
+            node_runtime,
+            http_client,
         });
 
-        Self::new(mode, breakpoint_store, worktree_store, cx)
+        Self::new(mode, breakpoint_store, worktree_store, fs, cx)
     }
 
     pub fn new_collab(
@@ -168,17 +178,55 @@ impl DapStore {
         _upstream_client: AnyProtoClient,
         breakpoint_store: Entity<BreakpointStore>,
         worktree_store: Entity<WorktreeStore>,
+        fs: Arc<dyn Fs>,
         cx: &mut Context<Self>,
     ) -> Self {
-        Self::new(DapStoreMode::Collab, breakpoint_store, worktree_store, cx)
+        Self::new(
+            DapStoreMode::Collab,
+            breakpoint_store,
+            worktree_store,
+            fs,
+            cx,
+        )
     }
 
     fn new(
         mode: DapStoreMode,
         breakpoint_store: Entity<BreakpointStore>,
         worktree_store: Entity<WorktreeStore>,
-        _cx: &mut Context<Self>,
+        fs: Arc<dyn Fs>,
+        cx: &mut Context<Self>,
     ) -> Self {
+        cx.background_spawn(async move {
+            let dir = paths::debug_adapters_dir().join("js-debug-companion");
+
+            let mut children = fs.read_dir(&dir).await?.try_collect::<Vec<_>>().await?;
+            children.sort_by_key(|child| semver::Version::parse(child.file_name()?.to_str()?).ok());
+
+            if let Some(child) = children.last()
+                && let Some(name) = child.file_name()
+                && let Some(name) = name.to_str()
+                && semver::Version::parse(name).is_ok()
+            {
+                children.pop();
+            }
+
+            for child in children {
+                fs.remove_dir(
+                    &child,
+                    RemoveOptions {
+                        recursive: true,
+                        ignore_if_not_exists: true,
+                    },
+                )
+                .await
+                .ok();
+            }
+
+            anyhow::Ok(())
+        })
+        .detach();
+
         Self {
             mode,
             next_session_id: 0,
@@ -401,6 +449,15 @@ impl DapStore {
             });
         }
 
+        let (remote_client, node_runtime, http_client) = match &self.mode {
+            DapStoreMode::Local(_) => (None, None, None),
+            DapStoreMode::Remote(remote_dap_store) => (
+                Some(remote_dap_store.remote_client.clone()),
+                Some(remote_dap_store.node_runtime.clone()),
+                Some(remote_dap_store.http_client.clone()),
+            ),
+            DapStoreMode::Collab => (None, None, None),
+        };
         let session = Session::new(
             self.breakpoint_store.clone(),
             session_id,
@@ -409,6 +466,9 @@ impl DapStore {
             adapter,
             task_context,
             quirks,
+            remote_client,
+            node_runtime,
+            http_client,
             cx,
         );
 
@@ -538,6 +598,7 @@ impl DapStore {
             local_store.environment.update(cx, |env, cx| {
                 env.get_worktree_environment(worktree.clone(), cx)
             }),
+            local_store.is_headless,
         ))
     }
 
@@ -870,6 +931,7 @@ pub struct DapAdapterDelegate {
     http_client: Arc<dyn HttpClient>,
     toolchain_store: Arc<dyn LanguageToolchainStore>,
     load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
+    is_headless: bool,
 }
 
 impl DapAdapterDelegate {
@@ -881,6 +943,7 @@ impl DapAdapterDelegate {
         http_client: Arc<dyn HttpClient>,
         toolchain_store: Arc<dyn LanguageToolchainStore>,
         load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
+        is_headless: bool,
     ) -> Self {
         Self {
             fs,
@@ -890,6 +953,7 @@ impl DapAdapterDelegate {
             node_runtime,
             toolchain_store,
             load_shell_env_task,
+            is_headless,
         }
     }
 }
@@ -953,4 +1017,8 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate {
 
         self.fs.load(&abs_path).await
     }
+
+    fn is_headless(&self) -> bool {
+        self.is_headless
+    }
 }

crates/project/src/debugger/session.rs 🔗

@@ -31,21 +31,28 @@ use dap::{
     RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments,
     StartDebuggingRequestArgumentsRequest, VariablePresentationHint, WriteMemoryArguments,
 };
-use futures::SinkExt;
 use futures::channel::mpsc::UnboundedSender;
 use futures::channel::{mpsc, oneshot};
+use futures::io::BufReader;
+use futures::{AsyncBufReadExt as _, SinkExt, StreamExt, TryStreamExt};
 use futures::{FutureExt, future::Shared};
 use gpui::{
     App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString,
     Task, WeakEntity,
 };
+use http_client::HttpClient;
 
+use node_runtime::NodeRuntime;
+use remote::RemoteClient;
 use rpc::ErrorExt;
+use serde::{Deserialize, Serialize};
 use serde_json::Value;
-use smol::stream::StreamExt;
+use smol::net::TcpListener;
 use std::any::TypeId;
 use std::collections::BTreeMap;
 use std::ops::RangeInclusive;
+use std::path::PathBuf;
+use std::process::Stdio;
 use std::u64;
 use std::{
     any::Any,
@@ -56,6 +63,7 @@ use std::{
 };
 use task::TaskContext;
 use text::{PointUtf16, ToPointUtf16};
+use util::command::new_smol_command;
 use util::{ResultExt, debug_panic, maybe};
 use worktree::Worktree;
 
@@ -696,6 +704,10 @@ pub struct Session {
     task_context: TaskContext,
     memory: memory::Memory,
     quirks: SessionQuirks,
+    remote_client: Option<Entity<RemoteClient>>,
+    node_runtime: Option<NodeRuntime>,
+    http_client: Option<Arc<dyn HttpClient>>,
+    companion_port: Option<u16>,
 }
 
 trait CacheableCommand: Any + Send + Sync {
@@ -812,6 +824,9 @@ impl Session {
         adapter: DebugAdapterName,
         task_context: TaskContext,
         quirks: SessionQuirks,
+        remote_client: Option<Entity<RemoteClient>>,
+        node_runtime: Option<NodeRuntime>,
+        http_client: Option<Arc<dyn HttpClient>>,
         cx: &mut App,
     ) -> Entity<Self> {
         cx.new::<Self>(|cx| {
@@ -867,6 +882,10 @@ impl Session {
                 task_context,
                 memory: memory::Memory::new(),
                 quirks,
+                remote_client,
+                node_runtime,
+                http_client,
+                companion_port: None,
             }
         })
     }
@@ -1557,7 +1576,21 @@ impl Session {
             Events::ProgressStart(_) => {}
             Events::ProgressUpdate(_) => {}
             Events::Invalidated(_) => {}
-            Events::Other(_) => {}
+            Events::Other(event) => {
+                if event.event == "launchBrowserInCompanion" {
+                    let Some(request) = serde_json::from_value(event.body).ok() else {
+                        log::error!("failed to deserialize launchBrowserInCompanion event");
+                        return;
+                    };
+                    self.launch_browser_for_remote_server(request, cx);
+                } else if event.event == "killCompanionBrowser" {
+                    let Some(request) = serde_json::from_value(event.body).ok() else {
+                        log::error!("failed to deserialize killCompanionBrowser event");
+                        return;
+                    };
+                    self.kill_browser(request, cx);
+                }
+            }
         }
     }
 
@@ -2716,4 +2749,304 @@ impl Session {
     pub fn quirks(&self) -> SessionQuirks {
         self.quirks
     }
+
+    fn launch_browser_for_remote_server(
+        &mut self,
+        mut request: LaunchBrowserInCompanionParams,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(remote_client) = self.remote_client.clone() else {
+            log::error!("can't launch browser in companion for non-remote project");
+            return;
+        };
+        let Some(http_client) = self.http_client.clone() else {
+            return;
+        };
+        let Some(node_runtime) = self.node_runtime.clone() else {
+            return;
+        };
+
+        let mut console_output = self.console_output(cx);
+        let task = cx.spawn(async move |this, cx| {
+            let (dap_port, _child) =
+                if remote_client.read_with(cx, |client, _| client.shares_network_interface())? {
+                    (request.server_port, None)
+                } else {
+                    let port = {
+                        let listener = TcpListener::bind("127.0.0.1:0")
+                            .await
+                            .context("getting port for DAP")?;
+                        listener.local_addr()?.port()
+                    };
+                    let child = remote_client.update(cx, |client, _| {
+                        let command = client.build_forward_port_command(
+                            port,
+                            "localhost".into(),
+                            request.server_port,
+                        )?;
+                        let child = new_smol_command(command.program)
+                            .args(command.args)
+                            .envs(command.env)
+                            .spawn()
+                            .context("spawning port forwarding process")?;
+                        anyhow::Ok(child)
+                    })??;
+                    (port, Some(child))
+                };
+
+            let mut companion_process = None;
+            let companion_port =
+                if let Some(companion_port) = this.read_with(cx, |this, _| this.companion_port)? {
+                    companion_port
+                } else {
+                    let task = cx.spawn(async move |cx| spawn_companion(node_runtime, cx).await);
+                    match task.await {
+                        Ok((port, child)) => {
+                            companion_process = Some(child);
+                            port
+                        }
+                        Err(e) => {
+                            console_output
+                                .send(format!("Failed to launch browser companion process: {e}"))
+                                .await
+                                .ok();
+                            return Err(e);
+                        }
+                    }
+                };
+            this.update(cx, |this, cx| {
+                this.companion_port = Some(companion_port);
+                let Some(mut child) = companion_process else {
+                    return;
+                };
+                if let Some(stderr) = child.stderr.take() {
+                    let mut console_output = console_output.clone();
+                    this.background_tasks.push(cx.spawn(async move |_, _| {
+                        let mut stderr = BufReader::new(stderr);
+                        let mut line = String::new();
+                        while let Ok(n) = stderr.read_line(&mut line).await
+                            && n > 0
+                        {
+                            console_output
+                                .send(format!("companion stderr: {line}"))
+                                .await
+                                .ok();
+                            line.clear();
+                        }
+                    }));
+                }
+                this.background_tasks.push(cx.spawn({
+                    let mut console_output = console_output.clone();
+                    async move |_, _| match child.status().await {
+                        Ok(status) => {
+                            if status.success() {
+                                console_output
+                                    .send("Companion process exited normally".into())
+                                    .await
+                                    .ok();
+                            } else {
+                                console_output
+                                    .send(format!(
+                                        "Companion process exited abnormally with {status:?}"
+                                    ))
+                                    .await
+                                    .ok();
+                            }
+                        }
+                        Err(e) => {
+                            console_output
+                                .send(format!("Failed to join companion process: {e}"))
+                                .await
+                                .ok();
+                        }
+                    }
+                }))
+            })?;
+
+            request
+                .other
+                .insert("proxyUri".into(), format!("127.0.0.1:{dap_port}").into());
+            // TODO pass wslInfo as needed
+
+            let response = http_client
+                .post_json(
+                    &format!("http://127.0.0.1:{companion_port}/launch-and-attach"),
+                    serde_json::to_string(&request)
+                        .context("serializing request")?
+                        .into(),
+                )
+                .await;
+            match response {
+                Ok(response) => {
+                    if !response.status().is_success() {
+                        console_output
+                            .send("Launch request to companion failed".into())
+                            .await
+                            .ok();
+                        return Err(anyhow!("launch request failed"));
+                    }
+                }
+                Err(e) => {
+                    console_output
+                        .send("Failed to read response from companion".into())
+                        .await
+                        .ok();
+                    return Err(e);
+                }
+            }
+
+            anyhow::Ok(())
+        });
+        self.background_tasks.push(cx.spawn(async move |_, _| {
+            task.await.log_err();
+        }));
+    }
+
+    fn kill_browser(&self, request: KillCompanionBrowserParams, cx: &mut App) {
+        let Some(companion_port) = self.companion_port else {
+            log::error!("received killCompanionBrowser but js-debug-companion is not running");
+            return;
+        };
+        let Some(http_client) = self.http_client.clone() else {
+            return;
+        };
+
+        cx.spawn(async move |_| {
+            http_client
+                .post_json(
+                    &format!("http://127.0.0.1:{companion_port}/kill"),
+                    serde_json::to_string(&request)
+                        .context("serializing request")?
+                        .into(),
+                )
+                .await?;
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx)
+    }
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct LaunchBrowserInCompanionParams {
+    server_port: u16,
+    #[serde(flatten)]
+    other: HashMap<String, serde_json::Value>,
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct KillCompanionBrowserParams {
+    launch_id: u64,
+}
+
+async fn spawn_companion(
+    node_runtime: NodeRuntime,
+    cx: &mut AsyncApp,
+) -> Result<(u16, smol::process::Child)> {
+    let binary_path = node_runtime
+        .binary_path()
+        .await
+        .context("getting node path")?;
+    let path = cx
+        .spawn(async move |cx| get_or_install_companion(node_runtime, cx).await)
+        .await?;
+    log::info!("will launch js-debug-companion version {path:?}");
+
+    let port = {
+        let listener = TcpListener::bind("127.0.0.1:0")
+            .await
+            .context("getting port for companion")?;
+        listener.local_addr()?.port()
+    };
+
+    let dir = paths::data_dir()
+        .join("js_debug_companion_state")
+        .to_string_lossy()
+        .to_string();
+
+    let child = new_smol_command(binary_path)
+        .arg(path)
+        .args([
+            format!("--listen=127.0.0.1:{port}"),
+            format!("--state={dir}"),
+        ])
+        .stdin(Stdio::piped())
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .spawn()
+        .context("spawning companion child process")?;
+
+    Ok((port, child))
+}
+
+async fn get_or_install_companion(node: NodeRuntime, cx: &mut AsyncApp) -> Result<PathBuf> {
+    const PACKAGE_NAME: &str = "@zed-industries/js-debug-companion-cli";
+
+    async fn install_latest_version(dir: PathBuf, node: NodeRuntime) -> Result<PathBuf> {
+        let temp_dir = tempfile::tempdir().context("creating temporary directory")?;
+        node.npm_install_packages(temp_dir.path(), &[(PACKAGE_NAME, "latest")])
+            .await
+            .context("installing latest companion package")?;
+        let version = node
+            .npm_package_installed_version(temp_dir.path(), PACKAGE_NAME)
+            .await
+            .context("getting installed companion version")?
+            .context("companion was not installed")?;
+        smol::fs::rename(temp_dir.path(), dir.join(&version))
+            .await
+            .context("moving companion package into place")?;
+        Ok(dir.join(version))
+    }
+
+    let dir = paths::debug_adapters_dir().join("js-debug-companion");
+    let (latest_installed_version, latest_version) = cx
+        .background_spawn({
+            let dir = dir.clone();
+            let node = node.clone();
+            async move {
+                smol::fs::create_dir_all(&dir)
+                    .await
+                    .context("creating companion installation directory")?;
+
+                let mut children = smol::fs::read_dir(&dir)
+                    .await
+                    .context("reading companion installation directory")?
+                    .try_collect::<Vec<_>>()
+                    .await
+                    .context("reading companion installation directory entries")?;
+                children
+                    .sort_by_key(|child| semver::Version::parse(child.file_name().to_str()?).ok());
+
+                let latest_installed_version = children.last().and_then(|child| {
+                    let version = child.file_name().into_string().ok()?;
+                    Some((child.path(), version))
+                });
+                let latest_version = node
+                    .npm_package_latest_version(PACKAGE_NAME)
+                    .await
+                    .log_err();
+                anyhow::Ok((latest_installed_version, latest_version))
+            }
+        })
+        .await?;
+
+    let path = if let Some((installed_path, installed_version)) = latest_installed_version {
+        if let Some(latest_version) = latest_version
+            && latest_version != installed_version
+        {
+            cx.background_spawn(install_latest_version(dir.clone(), node.clone()))
+                .detach();
+        }
+        Ok(installed_path)
+    } else {
+        cx.background_spawn(install_latest_version(dir.clone(), node.clone()))
+            .await
+    };
+
+    Ok(path?
+        .join("node_modules")
+        .join(PACKAGE_NAME)
+        .join("out")
+        .join("cli.js"))
 }

crates/project/src/lsp_store.rs 🔗

@@ -406,15 +406,14 @@ impl LocalLspStore {
                         adapter.clone(),
                     );
 
-                    let did_change_configuration_params =
-                        Arc::new(lsp::DidChangeConfigurationParams {
-                            settings: workspace_config,
-                        });
+                    let did_change_configuration_params = lsp::DidChangeConfigurationParams {
+                        settings: workspace_config,
+                    };
                     let language_server = cx
                         .update(|cx| {
                             language_server.initialize(
                                 initialization_params,
-                                did_change_configuration_params.clone(),
+                                Arc::new(did_change_configuration_params.clone()),
                                 cx,
                             )
                         })?
@@ -430,11 +429,9 @@ impl LocalLspStore {
                             }
                         })?;
 
-                    language_server
-                        .notify::<lsp::notification::DidChangeConfiguration>(
-                            &did_change_configuration_params,
-                        )
-                        .ok();
+                    language_server.notify::<lsp::notification::DidChangeConfiguration>(
+                        did_change_configuration_params,
+                    )?;
 
                     anyhow::Ok(language_server)
                 }
@@ -7206,7 +7203,7 @@ impl LspStore {
 
             language_server
                 .notify::<lsp::notification::DidChangeTextDocument>(
-                    &lsp::DidChangeTextDocumentParams {
+                    lsp::DidChangeTextDocumentParams {
                         text_document: lsp::VersionedTextDocumentIdentifier::new(
                             uri.clone(),
                             next_version,
@@ -7243,7 +7240,7 @@ impl LspStore {
                 };
                 server
                     .notify::<lsp::notification::DidSaveTextDocument>(
-                        &lsp::DidSaveTextDocumentParams {
+                        lsp::DidSaveTextDocumentParams {
                             text_document: text_document.clone(),
                             text,
                         },
@@ -7314,7 +7311,7 @@ impl LspStore {
                                             .ok()?;
                                         server
                                             .notify::<lsp::notification::DidChangeConfiguration>(
-                                                &lsp::DidChangeConfigurationParams { settings },
+                                                lsp::DidChangeConfigurationParams { settings },
                                             )
                                             .ok()?;
                                         Some(())
@@ -8536,15 +8533,16 @@ impl LspStore {
         cx: AsyncApp,
     ) -> Result<proto::Ack> {
         let server_id = LanguageServerId(envelope.payload.language_server_id as usize);
-        lsp_store.read_with(&cx, |lsp_store, _| {
+        let task = lsp_store.read_with(&cx, |lsp_store, _| {
             if let Some(server) = lsp_store.language_server_for_id(server_id) {
-                server
-                    .notify::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(&())
-                    .context("handling lsp ext cancel flycheck")
+                Some(server.notify::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(()))
             } else {
-                anyhow::Ok(())
+                None
             }
-        })??;
+        })?;
+        if let Some(task) = task {
+            task.context("handling lsp ext cancel flycheck")?;
+        }
 
         Ok(proto::Ack {})
     }
@@ -8578,14 +8576,11 @@ impl LspStore {
                 } else {
                     None
                 };
-                server
-                    .notify::<lsp_store::lsp_ext_command::LspExtRunFlycheck>(
-                        &lsp_store::lsp_ext_command::RunFlycheckParams { text_document },
-                    )
-                    .context("handling lsp ext run flycheck")
-            } else {
-                anyhow::Ok(())
+                server.notify::<lsp_store::lsp_ext_command::LspExtRunFlycheck>(
+                    lsp_store::lsp_ext_command::RunFlycheckParams { text_document },
+                )?;
             }
+            anyhow::Ok(())
         })??;
 
         Ok(proto::Ack {})
@@ -8597,15 +8592,15 @@ impl LspStore {
         cx: AsyncApp,
     ) -> Result<proto::Ack> {
         let server_id = LanguageServerId(envelope.payload.language_server_id as usize);
-        lsp_store.read_with(&cx, |lsp_store, _| {
-            if let Some(server) = lsp_store.language_server_for_id(server_id) {
-                server
-                    .notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(&())
-                    .context("handling lsp ext clear flycheck")
-            } else {
-                anyhow::Ok(())
-            }
-        })??;
+        lsp_store
+            .read_with(&cx, |lsp_store, _| {
+                if let Some(server) = lsp_store.language_server_for_id(server_id) {
+                    Some(server.notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(()))
+                } else {
+                    None
+                }
+            })
+            .context("handling lsp ext clear flycheck")?;
 
         Ok(proto::Ack {})
     }
@@ -8744,7 +8739,7 @@ impl LspStore {
 
                 if filter.should_send_did_rename(&old_uri, is_dir) {
                     language_server
-                        .notify::<DidRenameFiles>(&RenameFilesParams {
+                        .notify::<DidRenameFiles>(RenameFilesParams {
                             files: vec![FileRename {
                                 old_uri: old_uri.clone(),
                                 new_uri: new_uri.clone(),
@@ -8858,7 +8853,7 @@ impl LspStore {
             if !changes.is_empty() {
                 server
                     .notify::<lsp::notification::DidChangeWatchedFiles>(
-                        &lsp::DidChangeWatchedFilesParams { changes },
+                        lsp::DidChangeWatchedFilesParams { changes },
                     )
                     .ok();
             }
@@ -10668,7 +10663,7 @@ impl LspStore {
                     if progress.is_cancellable {
                         server
                             .notify::<lsp::notification::WorkDoneProgressCancel>(
-                                &WorkDoneProgressCancelParams {
+                                WorkDoneProgressCancelParams {
                                     token: lsp::NumberOrString::String(token.clone()),
                                 },
                             )
@@ -10799,7 +10794,7 @@ impl LspStore {
                 };
                 if !params.changes.is_empty() {
                     server
-                        .notify::<lsp::notification::DidChangeWatchedFiles>(&params)
+                        .notify::<lsp::notification::DidChangeWatchedFiles>(params)
                         .ok();
                 }
             }

crates/project/src/lsp_store/json_language_server_ext.rs 🔗

@@ -42,7 +42,7 @@ impl lsp::notification::Notification for SchemaContentsChanged {
     type Params = String;
 }
 
-pub fn notify_schema_changed(lsp_store: Entity<LspStore>, uri: &String, cx: &App) {
+pub fn notify_schema_changed(lsp_store: Entity<LspStore>, uri: String, cx: &App) {
     zlog::trace!(LOGGER => "Notifying schema changed for URI: {:?}", uri);
     let servers = lsp_store.read_with(cx, |lsp_store, _| {
         let mut servers = Vec::new();
@@ -65,7 +65,7 @@ pub fn notify_schema_changed(lsp_store: Entity<LspStore>, uri: &String, cx: &App
     for server in servers {
         zlog::trace!(LOGGER => "Notifying server {:?} of schema change for URI: {:?}", server.server_id(), &uri);
         // TODO: handle errors
-        server.notify::<SchemaContentsChanged>(uri).ok();
+        server.notify::<SchemaContentsChanged>(uri.clone()).ok();
     }
 }
 

crates/project/src/lsp_store/rust_analyzer_ext.rs 🔗

@@ -119,11 +119,12 @@ pub fn cancel_flycheck(
             lsp_store
                 .read_with(cx, |lsp_store, _| {
                     if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
-                        server.notify::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(&())?;
+                        server.notify::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(())
+                    } else {
+                        Ok(())
                     }
-                    anyhow::Ok(())
-                })?
-                .context("lsp ext cancel flycheck")?;
+                })
+                .context("lsp ext cancel flycheck")??;
         };
         anyhow::Ok(())
     })
@@ -173,14 +174,15 @@ pub fn run_flycheck(
                 .read_with(cx, |lsp_store, _| {
                     if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
                         server.notify::<lsp_store::lsp_ext_command::LspExtRunFlycheck>(
-                            &lsp_store::lsp_ext_command::RunFlycheckParams {
+                            lsp_store::lsp_ext_command::RunFlycheckParams {
                                 text_document: None,
                             },
-                        )?;
+                        )
+                    } else {
+                        Ok(())
                     }
-                    anyhow::Ok(())
-                })?
-                .context("lsp ext run flycheck")?;
+                })
+                .context("lsp ext run flycheck")??;
         };
         anyhow::Ok(())
     })
@@ -224,11 +226,12 @@ pub fn clear_flycheck(
             lsp_store
                 .read_with(cx, |lsp_store, _| {
                     if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) {
-                        server.notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(&())?;
+                        server.notify::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(())
+                    } else {
+                        Ok(())
                     }
-                    anyhow::Ok(())
-                })?
-                .context("lsp ext clear flycheck")?;
+                })
+                .context("lsp ext clear flycheck")??;
         };
         anyhow::Ok(())
     })

crates/project/src/project.rs 🔗

@@ -980,7 +980,7 @@ pub struct DisableAiSettings {
 }
 
 impl settings::Settings for DisableAiSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         Self {
             disable_ai: content.disable_ai.unwrap().0,
         }
@@ -1084,6 +1084,7 @@ impl Project {
                     toolchain_store.read(cx).as_language_toolchain_store(),
                     worktree_store.clone(),
                     breakpoint_store.clone(),
+                    false,
                     cx,
                 )
             });
@@ -1154,7 +1155,13 @@ impl Project {
             });
 
             let agent_server_store = cx.new(|cx| {
-                AgentServerStore::local(node.clone(), fs.clone(), environment.clone(), cx)
+                AgentServerStore::local(
+                    node.clone(),
+                    fs.clone(),
+                    environment.clone(),
+                    client.http_client(),
+                    cx,
+                )
             });
 
             cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
@@ -1306,6 +1313,9 @@ impl Project {
                     remote.clone(),
                     breakpoint_store.clone(),
                     worktree_store.clone(),
+                    node.clone(),
+                    client.http_client(),
+                    fs.clone(),
                     cx,
                 )
             });
@@ -1503,6 +1513,7 @@ impl Project {
                 client.clone().into(),
                 breakpoint_store.clone(),
                 worktree_store.clone(),
+                fs.clone(),
                 cx,
             )
         })?;
@@ -2571,8 +2582,8 @@ impl Project {
         let task = self.open_buffer(path, cx);
         cx.spawn(async move |_project, cx| {
             let buffer = task.await?;
-            let project_entry_id = buffer.read_with(cx, |buffer, cx| {
-                File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
+            let project_entry_id = buffer.read_with(cx, |buffer, _cx| {
+                File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id())
             })?;
 
             Ok((project_entry_id, buffer))
@@ -5515,8 +5526,8 @@ impl ProjectItem for Buffer {
         Some(project.update(cx, |project, cx| project.open_buffer(path.clone(), cx)))
     }
 
-    fn entry_id(&self, cx: &App) -> Option<ProjectEntryId> {
-        File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx))
+    fn entry_id(&self, _cx: &App) -> Option<ProjectEntryId> {
+        File::from_dyn(self.file()).and_then(|file| file.project_entry_id())
     }
 
     fn project_path(&self, cx: &App) -> Option<ProjectPath> {

crates/project/src/project_settings.rs 🔗

@@ -4,7 +4,7 @@ use context_server::ContextServerCommand;
 use dap::adapters::DebugAdapterName;
 use fs::Fs;
 use futures::StreamExt as _;
-use gpui::{App, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Subscription, Task};
+use gpui::{AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Subscription, Task};
 use lsp::LanguageServerName;
 use paths::{
     EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
@@ -437,7 +437,7 @@ pub struct LspPullDiagnosticsSettings {
 }
 
 impl Settings for ProjectSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let project = &content.project.clone();
         let diagnostics = content.diagnostics.as_ref().unwrap();
         let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap();

crates/project/src/project_tests.rs 🔗

@@ -1820,7 +1820,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
         }
     );
 
-    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
+    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
         uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
         version: None,
         diagnostics: vec![lsp::Diagnostic {
@@ -1873,7 +1873,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
     });
 
     // Ensure publishing empty diagnostics twice only results in one update event.
-    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
+    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
         uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
         version: None,
         diagnostics: Default::default(),
@@ -1886,7 +1886,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
         }
     );
 
-    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
+    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
         uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
         version: None,
         diagnostics: Default::default(),
@@ -2018,7 +2018,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
 
     // Publish diagnostics
     let fake_server = fake_servers.next().await.unwrap();
-    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
+    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
         uri: Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
         version: None,
         diagnostics: vec![lsp::Diagnostic {
@@ -2099,7 +2099,7 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
 
     // Before restarting the server, report diagnostics with an unknown buffer version.
     let fake_server = fake_servers.next().await.unwrap();
-    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
+    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
         uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
         version: Some(10000),
         diagnostics: Vec::new(),
@@ -2350,7 +2350,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
     assert!(change_notification_1.text_document.version > open_notification.text_document.version);
 
     // Report some diagnostics for the initial version of the buffer
-    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
+    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
         uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
         version: Some(open_notification.text_document.version),
         diagnostics: vec![
@@ -2438,7 +2438,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
     });
 
     // Ensure overlapping diagnostics are highlighted correctly.
-    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
+    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
         uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
         version: Some(open_notification.text_document.version),
         diagnostics: vec![
@@ -2532,7 +2532,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
     );
 
     // Handle out-of-order diagnostics
-    fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
+    fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
         uri: lsp::Uri::from_file_path(path!("/dir/a.rs")).unwrap(),
         version: Some(change_notification_2.text_document.version),
         diagnostics: vec![

crates/project/src/terminals.rs 🔗

@@ -201,15 +201,24 @@ impl Project {
                         },
                         None => match activation_script.clone() {
                             activation_script if !activation_script.is_empty() => {
-                                let activation_script = activation_script.join("; ");
+                                let separator = shell_kind.sequential_commands_separator();
+                                let activation_script =
+                                    activation_script.join(&format!("{separator} "));
                                 let to_run = format_to_run();
 
-                                let arg = format!("{activation_script}; {to_run}");
+                                let mut arg = format!("{activation_script}{separator} {to_run}");
+                                if shell_kind == ShellKind::Cmd {
+                                    // We need to put the entire command in quotes since otherwise CMD tries to execute them
+                                    // as separate commands rather than chaining one after another.
+                                    arg = format!("\"{arg}\"");
+                                }
+
+                                let args = shell_kind.args_for_shell(false, arg);
 
                                 (
                                     Shell::WithArguments {
                                         program: shell,
-                                        args: vec!["-c".to_owned(), arg],
+                                        args,
                                         title_override: None,
                                     },
                                     env,

crates/project_panel/src/project_panel.rs 🔗

@@ -3426,17 +3426,20 @@ impl ProjectPanel {
                             new_state.max_width_item_index = Some(visited_worktrees_length + index);
                         }
                     }
-                    if let Some((worktree_id, entry_id)) = new_selected_entry {
-                        new_state.selection = Some(SelectedEntry {
-                            worktree_id,
-                            entry_id,
-                        });
-                    }
                     new_state
                 })
                 .await;
             this.update_in(cx, |this, window, cx| {
+                let current_selection = this.state.selection;
                 this.state = new_state;
+                if let Some((worktree_id, entry_id)) = new_selected_entry {
+                    this.state.selection = Some(SelectedEntry {
+                        worktree_id,
+                        entry_id,
+                    });
+                } else {
+                    this.state.selection = current_selection;
+                }
                 let elapsed = now.elapsed();
                 if this.last_reported_update.elapsed() > Duration::from_secs(3600) {
                     telemetry::event!(

crates/project_panel/src/project_panel_settings.rs 🔗

@@ -55,7 +55,7 @@ impl ScrollbarVisibility for ProjectPanelSettings {
 }
 
 impl Settings for ProjectPanelSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let project_panel = content.project_panel.clone().unwrap();
         Self {
             button: project_panel.button.unwrap(),

crates/recent_projects/src/remote_connections.rs 🔗

@@ -104,7 +104,7 @@ impl From<WslConnection> for Connection {
 }
 
 impl Settings for SshSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let remote = &content.remote;
         Self {
             ssh_connections: remote.ssh_connections.clone().unwrap_or_default().into(),

crates/remote/src/remote_client.rs 🔗

@@ -836,6 +836,18 @@ impl RemoteClient {
         connection.build_command(program, args, env, working_dir, port_forward)
     }
 
+    pub fn build_forward_port_command(
+        &self,
+        local_port: u16,
+        host: String,
+        remote_port: u16,
+    ) -> Result<CommandTemplate> {
+        let Some(connection) = self.remote_connection() else {
+            return Err(anyhow!("no ssh connection"));
+        };
+        connection.build_forward_port_command(local_port, host, remote_port)
+    }
+
     pub fn upload_directory(
         &self,
         src_path: PathBuf,
@@ -1104,6 +1116,12 @@ pub(crate) trait RemoteConnection: Send + Sync {
         working_dir: Option<String>,
         port_forward: Option<(u16, String, u16)>,
     ) -> Result<CommandTemplate>;
+    fn build_forward_port_command(
+        &self,
+        local_port: u16,
+        remote: String,
+        remote_port: u16,
+    ) -> Result<CommandTemplate>;
     fn connection_options(&self) -> RemoteConnectionOptions;
     fn path_style(&self) -> PathStyle;
     fn shell(&self) -> String;
@@ -1533,6 +1551,23 @@ mod fake {
             })
         }
 
+        fn build_forward_port_command(
+            &self,
+            local_port: u16,
+            host: String,
+            remote_port: u16,
+        ) -> anyhow::Result<CommandTemplate> {
+            Ok(CommandTemplate {
+                program: "ssh".into(),
+                args: vec![
+                    "-N".into(),
+                    "-L".into(),
+                    format!("{local_port}:{host}:{remote_port}"),
+                ],
+                env: Default::default(),
+            })
+        }
+
         fn upload_directory(
             &self,
             _src_path: PathBuf,

crates/remote/src/transport/ssh.rs 🔗

@@ -145,6 +145,23 @@ impl RemoteConnection for SshRemoteConnection {
         )
     }
 
+    fn build_forward_port_command(
+        &self,
+        local_port: u16,
+        host: String,
+        remote_port: u16,
+    ) -> Result<CommandTemplate> {
+        Ok(CommandTemplate {
+            program: "ssh".into(),
+            args: vec![
+                "-N".into(),
+                "-L".into(),
+                format!("{local_port}:{host}:{remote_port}"),
+            ],
+            env: Default::default(),
+        })
+    }
+
     fn upload_directory(
         &self,
         src_path: PathBuf,

crates/remote/src/transport/wsl.rs 🔗

@@ -433,6 +433,15 @@ impl RemoteConnection for WslRemoteConnection {
         })
     }
 
+    fn build_forward_port_command(
+        &self,
+        _: u16,
+        _: String,
+        _: u16,
+    ) -> anyhow::Result<CommandTemplate> {
+        Err(anyhow!("WSL shares a network interface with the host"))
+    }
+
     fn connection_options(&self) -> RemoteConnectionOptions {
         RemoteConnectionOptions::Wsl(self.connection_options.clone())
     }

crates/remote_server/src/headless_project.rs 🔗

@@ -123,6 +123,7 @@ impl HeadlessProject {
                 toolchain_store.read(cx).as_language_toolchain_store(),
                 worktree_store.clone(),
                 breakpoint_store.clone(),
+                true,
                 cx,
             );
             dap_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
@@ -195,8 +196,13 @@ impl HeadlessProject {
         });
 
         let agent_server_store = cx.new(|cx| {
-            let mut agent_server_store =
-                AgentServerStore::local(node_runtime.clone(), fs.clone(), environment, cx);
+            let mut agent_server_store = AgentServerStore::local(
+                node_runtime.clone(),
+                fs.clone(),
+                environment,
+                http_client.clone(),
+                cx,
+            );
             agent_server_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
             agent_server_store
         });

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -1792,7 +1792,7 @@ async fn test_remote_external_agent_server(
             .map(|name| name.to_string())
             .collect::<Vec<_>>()
     });
-    pretty_assertions::assert_eq!(names, ["gemini", "claude"]);
+    pretty_assertions::assert_eq!(names, ["gemini", "codex", "claude"]);
     server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
         settings_store
             .set_server_settings(
@@ -1822,7 +1822,7 @@ async fn test_remote_external_agent_server(
             .map(|name| name.to_string())
             .collect::<Vec<_>>()
     });
-    pretty_assertions::assert_eq!(names, ["gemini", "foo", "claude"]);
+    pretty_assertions::assert_eq!(names, ["gemini", "codex", "claude", "foo"]);
     let (command, root, login) = project
         .update(cx, |project, cx| {
             project.agent_server_store().update(cx, |store, cx| {

crates/repl/src/jupyter_settings.rs 🔗

@@ -19,7 +19,7 @@ impl JupyterSettings {
 }
 
 impl Settings for JupyterSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let jupyter = content.editor.jupyter.clone().unwrap();
         Self {
             kernel_selections: jupyter.kernel_selections.unwrap_or_default(),

crates/repl/src/repl_settings.rs 🔗

@@ -1,4 +1,3 @@
-use gpui::App;
 use settings::Settings;
 
 /// Settings for configuring REPL display and behavior.
@@ -17,7 +16,7 @@ pub struct ReplSettings {
 }
 
 impl Settings for ReplSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let repl = content.repl.as_ref().unwrap();
 
         Self {

crates/rules_library/src/rules_library.rs 🔗

@@ -136,6 +136,7 @@ pub fn open_rules_library(
                     window_background: cx.theme().window_background_appearance(),
                     window_decorations: Some(window_decorations),
                     window_min_size: Some(size(px(800.), px(600.))), // 4:3 Aspect Ratio
+                    kind: gpui::WindowKind::Floating,
                     ..Default::default()
                 },
                 |window, cx| {

crates/settings/src/base_keymap_setting.rs 🔗

@@ -4,7 +4,6 @@ use crate::{
     self as settings,
     settings_content::{BaseKeymapContent, SettingsContent},
 };
-use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, VsCodeSettings};
@@ -131,7 +130,7 @@ impl BaseKeymap {
 }
 
 impl Settings for BaseKeymap {
-    fn from_settings(s: &crate::settings_content::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(s: &crate::settings_content::SettingsContent) -> Self {
         s.base_keymap.unwrap().into()
     }
 

crates/settings/src/settings_content.rs 🔗

@@ -787,7 +787,9 @@ pub enum ShowIndentGuides {
 }
 
 #[skip_serializing_none]
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
+#[derive(
+    Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
+)]
 pub struct IndentGuidesSettingsContent {
     /// When to show the scrollbar in the outline panel.
     pub show: Option<ShowIndentGuides>,

crates/settings/src/settings_content/language.rs 🔗

@@ -82,6 +82,7 @@ pub enum EditPredictionProvider {
     Copilot,
     Supermaven,
     Zed,
+    Codestral,
 }
 
 impl EditPredictionProvider {
@@ -90,7 +91,8 @@ impl EditPredictionProvider {
             EditPredictionProvider::Zed => true,
             EditPredictionProvider::None
             | EditPredictionProvider::Copilot
-            | EditPredictionProvider::Supermaven => false,
+            | EditPredictionProvider::Supermaven
+            | EditPredictionProvider::Codestral => false,
         }
     }
 }
@@ -108,6 +110,8 @@ pub struct EditPredictionSettingsContent {
     pub mode: Option<EditPredictionsMode>,
     /// Settings specific to GitHub Copilot.
     pub copilot: Option<CopilotSettingsContent>,
+    /// Settings specific to Codestral.
+    pub codestral: Option<CodestralSettingsContent>,
     /// Whether edit predictions are enabled in the assistant prompt editor.
     /// This has no effect if globally disabled.
     pub enabled_in_text_threads: Option<bool>,
@@ -130,6 +134,20 @@ pub struct CopilotSettingsContent {
     pub enterprise_uri: Option<String>,
 }
 
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
+pub struct CodestralSettingsContent {
+    /// Model to use for completions.
+    ///
+    /// Default: "codestral-latest"
+    #[serde(default)]
+    pub model: Option<String>,
+    /// Maximum tokens to generate.
+    ///
+    /// Default: 150
+    #[serde(default)]
+    pub max_tokens: Option<u32>,
+}
+
 /// The mode in which edit predictions should be displayed.
 #[derive(
     Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom,

crates/settings/src/settings_content/project.rs 🔗

@@ -7,7 +7,9 @@ use serde_with::skip_serializing_none;
 use settings_macros::MergeFrom;
 use util::serde::default_true;
 
-use crate::{AllLanguageSettingsContent, ExtendingVec, SlashCommandSettings};
+use crate::{
+    AllLanguageSettingsContent, ExtendingVec, ProjectTerminalSettingsContent, SlashCommandSettings,
+};
 
 #[skip_serializing_none]
 #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
@@ -29,6 +31,9 @@ pub struct ProjectSettingsContent {
     #[serde(default)]
     pub lsp: HashMap<Arc<str>, LspSettings>,
 
+    #[serde(default)]
+    pub terminal: Option<ProjectTerminalSettingsContent>,
+
     /// Configuration for Debugger-related features
     #[serde(default)]
     pub dap: HashMap<Arc<str>, DapSettingsContent>,

crates/settings/src/settings_content/terminal.rs 🔗

@@ -9,9 +9,8 @@ use settings_macros::MergeFrom;
 
 use crate::FontFamilyName;
 
-#[skip_serializing_none]
 #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
-pub struct TerminalSettingsContent {
+pub struct ProjectTerminalSettingsContent {
     /// What shell to use when opening a terminal.
     ///
     /// Default: system
@@ -20,6 +19,24 @@ pub struct TerminalSettingsContent {
     ///
     /// Default: current_project_directory
     pub working_directory: Option<WorkingDirectory>,
+    /// Any key-value pairs added to this list will be added to the terminal's
+    /// environment. Use `:` to separate multiple values.
+    ///
+    /// Default: {}
+    pub env: Option<HashMap<String, String>>,
+    /// Activates the python virtual environment, if one is found, in the
+    /// terminal's working directory (as resolved by the working_directory
+    /// setting). Set this to "off" to disable this behavior.
+    ///
+    /// Default: on
+    pub detect_venv: Option<VenvSettings>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
+pub struct TerminalSettingsContent {
+    #[serde(flatten)]
+    pub project: ProjectTerminalSettingsContent,
     /// Sets the terminal's font size.
     ///
     /// If this option is not included,
@@ -45,11 +62,6 @@ pub struct TerminalSettingsContent {
     pub font_features: Option<FontFeatures>,
     /// Sets the terminal's font weight in CSS weight units 0-900.
     pub font_weight: Option<f32>,
-    /// Any key-value pairs added to this list will be added to the terminal's
-    /// environment. Use `:` to separate multiple values.
-    ///
-    /// Default: {}
-    pub env: Option<HashMap<String, String>>,
     /// Default cursor shape for the terminal.
     /// Can be "bar", "block", "underline", or "hollow".
     ///
@@ -77,7 +89,7 @@ pub struct TerminalSettingsContent {
     pub copy_on_select: Option<bool>,
     /// Whether to keep the text selection after copying it to the clipboard.
     ///
-    /// Default: false
+    /// Default: true
     pub keep_selection_on_copy: Option<bool>,
     /// Whether to show the terminal button in the status bar.
     ///
@@ -92,12 +104,6 @@ pub struct TerminalSettingsContent {
     ///
     /// Default: 320
     pub default_height: Option<f32>,
-    /// Activates the python virtual environment, if one is found, in the
-    /// terminal's working directory (as resolved by the working_directory
-    /// setting). Set this to "off" to disable this behavior.
-    ///
-    /// Default: on
-    pub detect_venv: Option<VenvSettings>,
     /// The maximum number of lines to keep in the scrollback history.
     /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
     /// 0 disables the scrolling.
@@ -164,7 +170,9 @@ pub enum WorkingDirectory {
 }
 
 #[skip_serializing_none]
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
+#[derive(
+    Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
+)]
 pub struct ScrollbarSettingsContent {
     /// When to show the scrollbar in the terminal.
     ///
@@ -203,6 +211,7 @@ impl TerminalLineHeight {
     Copy,
     Clone,
     Debug,
+    Default,
     Serialize,
     Deserialize,
     JsonSchema,
@@ -216,6 +225,7 @@ impl TerminalLineHeight {
 pub enum ShowScrollbar {
     /// Show the scrollbar if there's important information or
     /// follow the system's configured behavior.
+    #[default]
     Auto,
     /// Match the system's configured behavior.
     System,
@@ -379,3 +389,33 @@ pub enum ActivateScript {
     PowerShell,
     Pyenv,
 }
+
+#[cfg(test)]
+mod test {
+    use serde_json::json;
+
+    use crate::{ProjectSettingsContent, Shell, UserSettingsContent};
+
+    #[test]
+    fn test_project_settings() {
+        let project_content =
+            json!({"terminal": {"shell": {"program": "/bin/project"}}, "option_as_meta": true});
+
+        let user_content =
+            json!({"terminal": {"shell": {"program": "/bin/user"}}, "option_as_meta": false});
+
+        let user_settings = serde_json::from_value::<UserSettingsContent>(user_content).unwrap();
+        let project_settings =
+            serde_json::from_value::<ProjectSettingsContent>(project_content).unwrap();
+
+        assert_eq!(
+            user_settings.content.terminal.unwrap().project.shell,
+            Some(Shell::Program("/bin/user".to_owned()))
+        );
+        assert_eq!(user_settings.content.project.terminal, None);
+        assert_eq!(
+            project_settings.terminal.unwrap().shell,
+            Some(Shell::Program("/bin/project".to_owned()))
+        );
+    }
+}

crates/settings/src/settings_content/theme.rs 🔗

@@ -57,7 +57,7 @@ pub struct ThemeSettingsContent {
     /// The font size for agent responses in the agent panel. Falls back to the UI font size if unset.
     #[serde(default)]
     pub agent_ui_font_size: Option<f32>,
-    /// The font size for user messages in the agent panel. Falls back to the buffer font size if unset.
+    /// The font size for user messages in the agent panel.
     #[serde(default)]
     pub agent_buffer_font_size: Option<f32>,
     /// The name of the Zed theme to use.

crates/settings/src/settings_content/workspace.rs 🔗

@@ -418,7 +418,7 @@ pub enum PaneSplitDirectionVertical {
 }
 
 #[skip_serializing_none]
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)]
 #[serde(rename_all = "snake_case")]
 pub struct CenteredLayoutSettings {
     /// The relative width of the left padding of the central pane from the
@@ -564,7 +564,9 @@ pub enum ProjectPanelEntrySpacing {
 }
 
 #[skip_serializing_none]
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
+#[derive(
+    Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
+)]
 pub struct ProjectPanelIndentGuidesSettings {
     pub show: Option<ShowIndentGuides>,
 }

crates/settings/src/settings_file.rs 🔗

@@ -22,7 +22,7 @@ pub fn test_settings() -> String {
             "buffer_font_family": "Courier",
             "buffer_font_features": {},
             "buffer_font_size": 14,
-            "buffer_font_fallback": [],
+            "buffer_font_fallbacks": [],
             "theme": EMPTY_THEME_NAME,
         }),
         &mut value,
@@ -37,7 +37,7 @@ pub fn test_settings() -> String {
             "buffer_font_family": "Courier New",
             "buffer_font_features": {},
             "buffer_font_size": 14,
-            "buffer_font_fallback": [],
+            "buffer_font_fallbacks": [],
             "theme": EMPTY_THEME_NAME,
         }),
         &mut value,

crates/settings/src/settings_store.rs 🔗

@@ -67,11 +67,7 @@ pub trait Settings: 'static + Send + Sync + Sized {
     ///
     /// This function *should* panic if default values are missing,
     /// and you should add a default to default.json for documentation.
-    fn from_settings(content: &SettingsContent, cx: &mut App) -> Self;
-
-    fn missing_default() -> anyhow::Error {
-        anyhow::anyhow!("missing default for: {}", std::any::type_name::<Self>())
-    }
+    fn from_settings(content: &SettingsContent) -> Self;
 
     /// Use [the helpers in the vscode_import module](crate::vscode_import) to apply known
     /// equivalent settings from a vscode config to our config
@@ -82,8 +78,8 @@ pub trait Settings: 'static + Send + Sync + Sized {
     where
         Self: Sized,
     {
-        SettingsStore::update_global(cx, |store, cx| {
-            store.register_setting::<Self>(cx);
+        SettingsStore::update_global(cx, |store, _| {
+            store.register_setting::<Self>();
         });
     }
 
@@ -162,8 +158,8 @@ pub enum SettingsFile {
     User,
     Server,
     Default,
-    /// Local also represents project settings in ssh projects as well as local projects
-    Local((WorktreeId, Arc<RelPath>)),
+    /// Represents project settings in ssh projects as well as local projects
+    Project((WorktreeId, Arc<RelPath>)),
 }
 
 #[derive(Clone)]
@@ -205,7 +201,7 @@ struct SettingValue<T> {
 trait AnySettingValue: 'static + Send + Sync {
     fn setting_type_name(&self) -> &'static str;
 
-    fn from_settings(&self, s: &SettingsContent, cx: &mut App) -> Box<dyn Any>;
+    fn from_settings(&self, s: &SettingsContent) -> Box<dyn Any>;
 
     fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
     fn all_local_values(&self) -> Vec<(WorktreeId, Arc<RelPath>, &dyn Any)>;
@@ -259,7 +255,7 @@ impl SettingsStore {
     }
 
     /// Add a new type of setting to the store.
-    pub fn register_setting<T: Settings>(&mut self, cx: &mut App) {
+    pub fn register_setting<T: Settings>(&mut self) {
         let setting_type_id = TypeId::of::<T>();
         let entry = self.setting_values.entry(setting_type_id);
 
@@ -271,7 +267,7 @@ impl SettingsStore {
             global_value: None,
             local_values: Vec::new(),
         }));
-        let value = T::from_settings(&self.merged_settings, cx);
+        let value = T::from_settings(&self.merged_settings);
         setting_value.set_global_value(Box::new(value));
     }
 
@@ -469,7 +465,7 @@ impl SettingsStore {
                 // rev because these are sorted by path, so highest precedence is last
                 .rev()
                 .cloned()
-                .map(SettingsFile::Local),
+                .map(SettingsFile::Project),
         );
 
         if self.server_settings.is_some() {
@@ -496,7 +492,7 @@ impl SettingsStore {
                 .map(|settings| settings.content.as_ref()),
             SettingsFile::Default => Some(self.default_settings.as_ref()),
             SettingsFile::Server => self.server_settings.as_deref(),
-            SettingsFile::Local(ref key) => self.local_settings.get(key),
+            SettingsFile::Project(ref key) => self.local_settings.get(key),
         }
     }
 
@@ -515,8 +511,8 @@ impl SettingsStore {
                 continue;
             }
 
-            if let SettingsFile::Local((wt_id, ref path)) = file
-                && let SettingsFile::Local((target_wt_id, ref target_path)) = target_file
+            if let SettingsFile::Project((wt_id, ref path)) = file
+                && let SettingsFile::Project((target_wt_id, ref target_path)) = target_file
                 && (wt_id != target_wt_id || !target_path.starts_with(path))
             {
                 // if requesting value from a local file, don't return values from local files in different worktrees
@@ -543,7 +539,7 @@ impl SettingsStore {
         target_file: SettingsFile,
         pick: fn(&SettingsContent) -> &Option<T>,
     ) -> (SettingsFile, Option<&T>) {
-        // TODO: Add a metadata field for overriding the "overrides" tag, for contextually different settings
+        // todo(settings_ui): Add a metadata field for overriding the "overrides" tag, for contextually different settings
         //  e.g. disable AI isn't overridden, or a vec that gets extended instead or some such
 
         // todo(settings_ui) cache all files
@@ -556,9 +552,9 @@ impl SettingsStore {
             }
             found_file = true;
 
-            if let SettingsFile::Local((wt_id, ref path)) = file
-                && let SettingsFile::Local((target_wt_id, ref target_path)) = target_file
-                && (wt_id != target_wt_id || !target_path.starts_with(&path))
+            if let SettingsFile::Project((worktree_id, ref path)) = file
+                && let SettingsFile::Project((target_worktree_id, ref target_path)) = target_file
+                && (worktree_id != target_worktree_id || !target_path.starts_with(&path))
             {
                 // if requesting value from a local file, don't return values from local files in different worktrees
                 continue;
@@ -948,7 +944,7 @@ impl SettingsStore {
             self.merged_settings = Rc::new(merged);
 
             for setting_value in self.setting_values.values_mut() {
-                let value = setting_value.from_settings(&self.merged_settings, cx);
+                let value = setting_value.from_settings(&self.merged_settings);
                 setting_value.set_global_value(value);
             }
         }
@@ -985,8 +981,7 @@ impl SettingsStore {
             }
 
             for setting_value in self.setting_values.values_mut() {
-                let value =
-                    setting_value.from_settings(&project_settings_stack.last().unwrap(), cx);
+                let value = setting_value.from_settings(&project_settings_stack.last().unwrap());
                 setting_value.set_local_value(*root_id, directory_path.clone(), value);
             }
         }
@@ -1070,8 +1065,8 @@ impl Debug for SettingsStore {
 }
 
 impl<T: Settings> AnySettingValue for SettingValue<T> {
-    fn from_settings(&self, s: &SettingsContent, cx: &mut App) -> Box<dyn Any> {
-        Box::new(T::from_settings(s, cx)) as _
+    fn from_settings(&self, s: &SettingsContent) -> Box<dyn Any> {
+        Box::new(T::from_settings(s)) as _
     }
 
     fn setting_type_name(&self) -> &'static str {
@@ -1142,7 +1137,7 @@ mod tests {
     }
 
     impl Settings for AutoUpdateSetting {
-        fn from_settings(content: &SettingsContent, _: &mut App) -> Self {
+        fn from_settings(content: &SettingsContent) -> Self {
             AutoUpdateSetting {
                 auto_update: content.auto_update.unwrap(),
             }
@@ -1156,7 +1151,7 @@ mod tests {
     }
 
     impl Settings for ItemSettings {
-        fn from_settings(content: &SettingsContent, _: &mut App) -> Self {
+        fn from_settings(content: &SettingsContent) -> Self {
             let content = content.tabs.clone().unwrap();
             ItemSettings {
                 close_position: content.close_position.unwrap(),
@@ -1185,7 +1180,7 @@ mod tests {
     }
 
     impl Settings for DefaultLanguageSettings {
-        fn from_settings(content: &SettingsContent, _: &mut App) -> Self {
+        fn from_settings(content: &SettingsContent) -> Self {
             let content = &content.project.all_languages.defaults;
             DefaultLanguageSettings {
                 tab_size: content.tab_size.unwrap(),
@@ -1206,12 +1201,38 @@ mod tests {
         }
     }
 
+    #[derive(Debug, PartialEq)]
+    struct ThemeSettings {
+        buffer_font_family: FontFamilyName,
+        buffer_font_fallbacks: Vec<FontFamilyName>,
+    }
+
+    impl Settings for ThemeSettings {
+        fn from_settings(content: &SettingsContent) -> Self {
+            let content = content.theme.clone();
+            ThemeSettings {
+                buffer_font_family: content.buffer_font_family.unwrap(),
+                buffer_font_fallbacks: content.buffer_font_fallbacks.unwrap(),
+            }
+        }
+
+        fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) {
+            let content = &mut content.theme;
+
+            vscode.font_family_setting(
+                "editor.fontFamily",
+                &mut content.buffer_font_family,
+                &mut content.buffer_font_fallbacks,
+            );
+        }
+    }
+
     #[gpui::test]
     fn test_settings_store_basic(cx: &mut App) {
         let mut store = SettingsStore::new(cx, &default_settings());
-        store.register_setting::<AutoUpdateSetting>(cx);
-        store.register_setting::<ItemSettings>(cx);
-        store.register_setting::<DefaultLanguageSettings>(cx);
+        store.register_setting::<AutoUpdateSetting>();
+        store.register_setting::<ItemSettings>();
+        store.register_setting::<DefaultLanguageSettings>();
 
         assert_eq!(
             store.get::<AutoUpdateSetting>(None),
@@ -1317,7 +1338,7 @@ mod tests {
         store
             .set_user_settings(r#"{ "auto_update": false }"#, cx)
             .unwrap();
-        store.register_setting::<AutoUpdateSetting>(cx);
+        store.register_setting::<AutoUpdateSetting>();
 
         assert_eq!(
             store.get::<AutoUpdateSetting>(None),
@@ -1525,9 +1546,10 @@ mod tests {
     #[gpui::test]
     fn test_vscode_import(cx: &mut App) {
         let mut store = SettingsStore::new(cx, &test_settings());
-        store.register_setting::<DefaultLanguageSettings>(cx);
-        store.register_setting::<ItemSettings>(cx);
-        store.register_setting::<AutoUpdateSetting>(cx);
+        store.register_setting::<DefaultLanguageSettings>();
+        store.register_setting::<ItemSettings>();
+        store.register_setting::<AutoUpdateSetting>();
+        store.register_setting::<ThemeSettings>();
 
         // create settings that werent present
         check_vscode_import(
@@ -1599,6 +1621,26 @@ mod tests {
             .unindent(),
             cx,
         );
+
+        // font-family
+        check_vscode_import(
+            &mut store,
+            r#"{
+            }
+            "#
+            .unindent(),
+            r#"{ "editor.fontFamily": "Cascadia Code, 'Consolas', Courier New" }"#.to_owned(),
+            r#"{
+                "buffer_font_fallbacks": [
+                    "Consolas",
+                    "Courier New"
+                ],
+                "buffer_font_family": "Cascadia Code"
+            }
+            "#
+            .unindent(),
+            cx,
+        );
     }
 
     #[track_caller]
@@ -1646,7 +1688,7 @@ mod tests {
     #[gpui::test]
     fn test_global_settings(cx: &mut App) {
         let mut store = SettingsStore::new(cx, &test_settings());
-        store.register_setting::<ItemSettings>(cx);
+        store.register_setting::<ItemSettings>();
 
         // Set global settings - these should override defaults but not user settings
         store
@@ -1695,7 +1737,7 @@ mod tests {
     #[gpui::test]
     fn test_get_value_for_field_basic(cx: &mut App) {
         let mut store = SettingsStore::new(cx, &test_settings());
-        store.register_setting::<DefaultLanguageSettings>(cx);
+        store.register_setting::<DefaultLanguageSettings>();
 
         store
             .set_user_settings(r#"{"preferred_line_length": 0}"#, cx)
@@ -1718,7 +1760,7 @@ mod tests {
         let default_value = get(&store.default_settings).unwrap();
 
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local.clone()), get),
+            store.get_value_from_file(SettingsFile::Project(local.clone()), get),
             (SettingsFile::User, Some(&0))
         );
         assert_eq!(
@@ -1727,7 +1769,7 @@ mod tests {
         );
         store.set_user_settings(r#"{}"#, cx).unwrap();
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local.clone()), get),
+            store.get_value_from_file(SettingsFile::Project(local.clone()), get),
             (SettingsFile::Default, Some(&default_value))
         );
         store
@@ -1740,8 +1782,8 @@ mod tests {
             )
             .unwrap();
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local.clone()), get),
-            (SettingsFile::Local(local), Some(&80))
+            store.get_value_from_file(SettingsFile::Project(local.clone()), get),
+            (SettingsFile::Project(local), Some(&80))
         );
         assert_eq!(
             store.get_value_from_file(SettingsFile::User, get),
@@ -1752,8 +1794,8 @@ mod tests {
     #[gpui::test]
     fn test_get_value_for_field_local_worktrees_dont_interfere(cx: &mut App) {
         let mut store = SettingsStore::new(cx, &test_settings());
-        store.register_setting::<DefaultLanguageSettings>(cx);
-        store.register_setting::<AutoUpdateSetting>(cx);
+        store.register_setting::<DefaultLanguageSettings>();
+        store.register_setting::<AutoUpdateSetting>();
 
         let local_1 = (WorktreeId::from_usize(0), RelPath::empty().into_arc());
 
@@ -1821,12 +1863,12 @@ mod tests {
 
         // each local child should only inherit from it's parent
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local_2_child), get),
-            (SettingsFile::Local(local_2), Some(&2))
+            store.get_value_from_file(SettingsFile::Project(local_2_child), get),
+            (SettingsFile::Project(local_2), Some(&2))
         );
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local_1_child.clone()), get),
-            (SettingsFile::Local(local_1.clone()), Some(&1))
+            store.get_value_from_file(SettingsFile::Project(local_1_child.clone()), get),
+            (SettingsFile::Project(local_1.clone()), Some(&1))
         );
 
         // adjacent children should be treated as siblings not inherit from each other
@@ -1851,8 +1893,8 @@ mod tests {
             .unwrap();
 
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local_1_adjacent_child.clone()), get),
-            (SettingsFile::Local(local_1.clone()), Some(&1))
+            store.get_value_from_file(SettingsFile::Project(local_1_adjacent_child.clone()), get),
+            (SettingsFile::Project(local_1.clone()), Some(&1))
         );
         store
             .set_local_settings(
@@ -1873,15 +1915,15 @@ mod tests {
             )
             .unwrap();
         assert_eq!(
-            store.get_value_from_file(SettingsFile::Local(local_1_child), get),
-            (SettingsFile::Local(local_1), Some(&1))
+            store.get_value_from_file(SettingsFile::Project(local_1_child), get),
+            (SettingsFile::Project(local_1), Some(&1))
         );
     }
 
     #[gpui::test]
     fn test_get_overrides_for_field(cx: &mut App) {
         let mut store = SettingsStore::new(cx, &test_settings());
-        store.register_setting::<DefaultLanguageSettings>(cx);
+        store.register_setting::<DefaultLanguageSettings>();
 
         let wt0_root = (WorktreeId::from_usize(0), RelPath::empty().into_arc());
         let wt0_child1 = (WorktreeId::from_usize(0), rel_path("child1").into_arc());
@@ -1950,9 +1992,9 @@ mod tests {
             overrides,
             vec![
                 SettingsFile::User,
-                SettingsFile::Local(wt0_root.clone()),
-                SettingsFile::Local(wt0_child1.clone()),
-                SettingsFile::Local(wt1_root.clone()),
+                SettingsFile::Project(wt0_root.clone()),
+                SettingsFile::Project(wt0_child1.clone()),
+                SettingsFile::Project(wt1_root.clone()),
             ]
         );
 
@@ -1960,25 +2002,26 @@ mod tests {
         assert_eq!(
             overrides,
             vec![
-                SettingsFile::Local(wt0_root.clone()),
-                SettingsFile::Local(wt0_child1.clone()),
-                SettingsFile::Local(wt1_root.clone()),
+                SettingsFile::Project(wt0_root.clone()),
+                SettingsFile::Project(wt0_child1.clone()),
+                SettingsFile::Project(wt1_root.clone()),
             ]
         );
 
-        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_root), get);
+        let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_root), get);
         assert_eq!(overrides, vec![]);
 
-        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_child1.clone()), get);
+        let overrides =
+            store.get_overrides_for_field(SettingsFile::Project(wt0_child1.clone()), get);
         assert_eq!(overrides, vec![]);
 
-        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_child2), get);
+        let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_child2), get);
         assert_eq!(overrides, vec![]);
 
-        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt1_root), get);
+        let overrides = store.get_overrides_for_field(SettingsFile::Project(wt1_root), get);
         assert_eq!(overrides, vec![]);
 
-        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt1_subdir), get);
+        let overrides = store.get_overrides_for_field(SettingsFile::Project(wt1_subdir), get);
         assert_eq!(overrides, vec![]);
 
         let wt0_deep_child = (
@@ -1995,10 +2038,10 @@ mod tests {
             )
             .unwrap();
 
-        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_deep_child), get);
+        let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_deep_child), get);
         assert_eq!(overrides, vec![]);
 
-        let overrides = store.get_overrides_for_field(SettingsFile::Local(wt0_child1), get);
+        let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_child1), get);
         assert_eq!(overrides, vec![]);
     }
 }

crates/settings/src/vscode_import.rs 🔗

@@ -4,6 +4,8 @@ use paths::{cursor_settings_file_paths, vscode_settings_file_paths};
 use serde_json::{Map, Value};
 use std::{path::Path, sync::Arc};
 
+use crate::FontFamilyName;
+
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
 pub enum VsCodeSettingsSource {
     VsCode,
@@ -145,4 +147,53 @@ impl VsCodeSettings {
     pub fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
         self.content.get(key).and_then(Value::as_str).and_then(f)
     }
+
+    pub fn font_family_setting(
+        &self,
+        key: &str,
+        font_family: &mut Option<FontFamilyName>,
+        font_fallbacks: &mut Option<Vec<FontFamilyName>>,
+    ) {
+        let Some(css_name) = self.content.get(key).and_then(Value::as_str) else {
+            return;
+        };
+
+        let mut name_buffer = String::new();
+        let mut quote_char: Option<char> = None;
+        let mut fonts = Vec::new();
+        let mut add_font = |buffer: &mut String| {
+            let trimmed = buffer.trim();
+            if !trimmed.is_empty() {
+                fonts.push(trimmed.to_string().into());
+            }
+
+            buffer.clear();
+        };
+
+        for ch in css_name.chars() {
+            match (ch, quote_char) {
+                ('"' | '\'', None) => {
+                    quote_char = Some(ch);
+                }
+                (_, Some(q)) if ch == q => {
+                    quote_char = None;
+                }
+                (',', None) => {
+                    add_font(&mut name_buffer);
+                }
+                _ => {
+                    name_buffer.push(ch);
+                }
+            }
+        }
+
+        add_font(&mut name_buffer);
+
+        let mut iter = fonts.into_iter();
+        *font_family = iter.next();
+        let fallbacks: Vec<_> = iter.collect();
+        if !fallbacks.is_empty() {
+            *font_fallbacks = Some(fallbacks);
+        }
+    }
 }

crates/settings_ui/Cargo.toml 🔗

@@ -17,7 +17,6 @@ test-support = []
 
 [dependencies]
 anyhow.workspace = true
-command_palette_hooks.workspace = true
 heck.workspace = true
 editor.workspace = true
 feature_flags.workspace = true
@@ -39,6 +38,7 @@ util.workspace = true
 workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
+log.workspace = true
 
 [dev-dependencies]
 assets.workspace = true
@@ -52,7 +52,3 @@ session.workspace = true
 settings.workspace = true
 zlog.workspace = true
 pretty_assertions.workspace = true
-
-[[example]]
-name = "ui"
-path = "examples/ui.rs"

crates/settings_ui/examples/ui.rs 🔗

@@ -1,113 +0,0 @@
-use std::sync::Arc;
-
-use futures::StreamExt;
-use gpui::AppContext as _;
-use settings::{DEFAULT_KEYMAP_PATH, KeymapFile, SettingsStore, watch_config_file};
-use settings_ui::open_settings_editor;
-use ui::BorrowAppContext;
-
-fn merge_paths(a: &std::path::Path, b: &std::path::Path) -> std::path::PathBuf {
-    let a_parts: Vec<_> = a.components().collect();
-    let b_parts: Vec<_> = b.components().collect();
-
-    let mut overlap = 0;
-    for i in 0..=a_parts.len().min(b_parts.len()) {
-        if a_parts[a_parts.len() - i..] == b_parts[..i] {
-            overlap = i;
-        }
-    }
-
-    let mut result = std::path::PathBuf::new();
-    for part in &a_parts {
-        result.push(part.as_os_str());
-    }
-    for part in &b_parts[overlap..] {
-        result.push(part.as_os_str());
-    }
-    result
-}
-
-fn main() {
-    zlog::init();
-    zlog::init_output_stderr();
-
-    let [crate_path, file_path] = [env!("CARGO_MANIFEST_DIR"), file!()].map(std::path::Path::new);
-    let example_dir_abs_path = merge_paths(crate_path, file_path)
-        .parent()
-        .unwrap()
-        .to_path_buf();
-
-    let app = gpui::Application::new().with_assets(assets::Assets);
-
-    let fs = Arc::new(fs::RealFs::new(None, app.background_executor()));
-    let mut user_settings_file_rx = watch_config_file(
-        &app.background_executor(),
-        fs.clone(),
-        paths::settings_file().clone(),
-    );
-
-    app.run(move |cx| {
-        <dyn fs::Fs>::set_global(fs.clone(), cx);
-        settings::init(cx);
-        settings_ui::init(cx);
-        theme::init(theme::LoadThemes::JustBase, cx);
-        client::init_settings(cx);
-        workspace::init_settings(cx);
-        // production client because fake client requires gpui/test-support
-        // and that causes issues with the real stuff we want to do
-        let client = client::Client::production(cx);
-        let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
-        let languages = Arc::new(language::LanguageRegistry::new(
-            cx.background_executor().clone(),
-        ));
-
-        client::init(&client, cx);
-
-        project::Project::init(&client, cx);
-
-        zlog::info!(
-            "Creating fake worktree in {}",
-            example_dir_abs_path.display(),
-        );
-        let project = project::Project::local(
-            client.clone(),
-            node_runtime::NodeRuntime::unavailable(),
-            user_store,
-            languages,
-            fs.clone(),
-            Some(Default::default()), // WARN: if None is passed here, prepare to be process bombed
-            cx,
-        );
-        let worktree_task = project.update(cx, |project, cx| {
-            project.create_worktree(example_dir_abs_path, true, cx)
-        });
-        cx.spawn(async move |_| {
-            let worktree = worktree_task.await.unwrap();
-            std::mem::forget(worktree);
-        })
-        .detach();
-        std::mem::forget(project);
-
-        language::init(cx);
-        editor::init(cx);
-        menu::init();
-
-        let keybindings =
-            KeymapFile::load_asset_allow_partial_failure(DEFAULT_KEYMAP_PATH, cx).unwrap();
-        cx.bind_keys(keybindings);
-        cx.spawn(async move |cx| {
-            while let Some(content) = user_settings_file_rx.next().await {
-                cx.update(|cx| {
-                    cx.update_global(|store: &mut SettingsStore, cx| {
-                        store.set_user_settings(&content, cx).unwrap()
-                    })
-                })
-                .ok();
-            }
-        })
-        .detach();
-
-        open_settings_editor(cx).unwrap();
-        cx.activate(true);
-    });
-}

crates/settings_ui/src/page_data.rs 🔗

@@ -111,32 +111,38 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
-                // SettingsPageItem::SectionHeader("Scoped Settings"),
-                // todo(settings_ui): Implement another setting item type that just shows an edit in settings.json
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Preview Channel",
-                //     description: "Which settings should be activated only in Preview build of Zed",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| &settings_content.workspace.use_system_prompts,
-                //         pick_mut: |settings_content| {
-                //             &mut settings_content.workspace.use_system_prompts
-                //         },
-                //     }),
-                //     metadata: None,
-                // }),
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Settings Profiles",
-                //     description: "Any number of settings profiles that are temporarily applied on top of your existing user settings.",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| &settings_content.workspace.use_system_prompts,
-                //         pick_mut: |settings_content| {
-                //             &mut settings_content.workspace.use_system_prompts
-                //         },
-                //     }),
-                //     metadata: None,
-                // }),
+                SettingsPageItem::SectionHeader("Scoped Settings"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    // todo(settings_ui): Implement another setting item type that just shows an edit in settings.json
+                    files: USER,
+                    title: "Preview Channel",
+                    description: "Which settings should be activated only in Preview build of Zed",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| &settings_content.workspace.use_system_prompts,
+                            pick_mut: |settings_content| {
+                                &mut settings_content.workspace.use_system_prompts
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    files: USER,
+                    title: "Settings Profiles",
+                    description: "Any number of settings profiles that are temporarily applied on top of your existing user settings",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| &settings_content.workspace.use_system_prompts,
+                            pick_mut: |settings_content| {
+                                &mut settings_content.workspace.use_system_prompts
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                }),
                 SettingsPageItem::SectionHeader("Privacy"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Telemetry Diagnostics",
@@ -182,30 +188,36 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
         SettingsPage {
             title: "Appearance & Behavior",
             items: vec![
-                // SettingsPageItem::SectionHeader("Theme"),
+                SettingsPageItem::SectionHeader("Theme"),
                 // todo(settings_ui): Figure out how we want to add these
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Theme Mode",
-                //     description: "How to select the theme",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| &settings_content.theme.theme,
-                //         pick_mut: |settings_content| &mut settings_content.theme.theme,
-                //     }),
-                //     metadata: None,
-                // }),
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Icon Theme",
-                //     // todo(settings_ui)
-                //     // This description is misleading because the icon theme is used in more places than the file explorer)
-                //     description: "Choose the icon theme for file explorer",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| &settings_content.theme.icon_theme,
-                //         pick_mut: |settings_content| &mut settings_content.theme.icon_theme,
-                //     }),
-                //     metadata: None,
-                // }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    files: USER,
+                    title: "Theme Mode",
+                    description: "How to select the theme",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| &settings_content.theme.theme,
+                            pick_mut: |settings_content| &mut settings_content.theme.theme,
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    files: USER,
+                    title: "Icon Theme",
+                    // todo(settings_ui)
+                    // This description is misleading because the icon theme is used in more places than the file explorer)
+                    description: "Choose the icon theme for file explorer",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| &settings_content.theme.icon_theme,
+                            pick_mut: |settings_content| &mut settings_content.theme.icon_theme,
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                }),
                 SettingsPageItem::SectionHeader("Fonts"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Buffer Font Family",
@@ -238,16 +250,21 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     files: USER,
                 }),
                 // todo(settings_ui): This needs custom ui
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Buffer Line Height",
-                //     description: "Line height for editor text",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| &settings_content.theme.buffer_line_height,
-                //         pick_mut: |settings_content| &mut settings_content.theme.buffer_line_height,
-                //     }),
-                //     metadata: None,
-                // }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    files: USER,
+                    title: "Buffer Line Height",
+                    description: "Line height for editor text",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| &settings_content.theme.buffer_line_height,
+                            pick_mut: |settings_content| {
+                                &mut settings_content.theme.buffer_line_height
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "UI Font Family",
                     description: "Font family for UI elements",
@@ -278,6 +295,34 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Agent Panel UI Font Size",
+                    description: "Font size for agent response text in the agent panel. Falls back to the regular UI font size.",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if settings_content.theme.agent_ui_font_size.is_some() {
+                                &settings_content.theme.agent_ui_font_size
+                            } else {
+                                &settings_content.theme.ui_font_size
+                            }
+                        },
+                        pick_mut: |settings_content| &mut settings_content.theme.agent_ui_font_size,
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Agent Panel Buffer Font Size",
+                    description: "Font size for user messages text in the agent panel",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| &settings_content.theme.agent_buffer_font_size,
+                        pick_mut: |settings_content| {
+                            &mut settings_content.theme.agent_buffer_font_size
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
                 SettingsPageItem::SectionHeader("Keymap"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Base Keymap",
@@ -478,78 +523,62 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Centered Layout Left Padding",
-                //     description: "Left padding for centered layout",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| {
-                //             if let Some(centered_layout) =
-                //                 &settings_content.workspace.centered_layout
-                //             {
-                //                 &centered_layout.left_padding
-                //             } else {
-                //                 &None
-                //             }
-                //         },
-                //         pick_mut: |settings_content| {
-                //             if let Some(mut centered_layout) =
-                //                 settings_content.workspace.centered_layout
-                //             {
-                //                 &mut centered_layout.left_padding
-                //             } else {
-                //                 &mut None
-                //             }
-                //         },
-                //     }),
-                //     metadata: None,
-                // }),
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Centered Layout Right Padding",
-                //     description: "Right padding for centered layout",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| {
-                //             if let Some(centered_layout) =
-                //                 &settings_content.workspace.centered_layout
-                //             {
-                //                 &centered_layout.right_padding
-                //             } else {
-                //                 &None
-                //             }
-                //         },
-                //         pick_mut: |settings_content| {
-                //             if let Some(mut centered_layout) =
-                //                 settings_content.workspace.centered_layout
-                //             {
-                //                 &mut centered_layout.right_padding
-                //             } else {
-                //                 &mut None
-                //             }
-                //         },
-                //     }),
-                //     metadata: None,
-                // }),
                 SettingsPageItem::SettingItem(SettingItem {
-                    title: "Zoomed Padding",
-                    description: "Whether to show padding for zoomed panels",
+                    files: USER,
+                    title: "Centered Layout Left Padding",
+                    description: "Left padding for centered layout",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.workspace.zoomed_padding,
-                        pick_mut: |settings_content| &mut settings_content.workspace.zoomed_padding,
+                        pick: |settings_content| {
+                            if let Some(centered_layout) =
+                                &settings_content.workspace.centered_layout
+                            {
+                                &centered_layout.left_padding
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .workspace
+                                .centered_layout
+                                .get_or_insert_default()
+                                .left_padding
+                        },
                     }),
                     metadata: None,
-                    files: USER,
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
-                    title: "Use System Window Tabs",
-                    description: "(macOS-only) Whether to allow windows to merge based on the user's tabbing preference",
+                    files: USER,
+                    title: "Centered Layout Right Padding",
+                    description: "Right padding for centered layout",
                     field: Box::new(SettingField {
-                        pick: |settings_content| &settings_content.workspace.use_system_window_tabs,
+                        pick: |settings_content| {
+                            if let Some(centered_layout) =
+                                &settings_content.workspace.centered_layout
+                            {
+                                &centered_layout.right_padding
+                            } else {
+                                &None
+                            }
+                        },
                         pick_mut: |settings_content| {
-                            &mut settings_content.workspace.use_system_window_tabs
+                            &mut settings_content
+                                .workspace
+                                .centered_layout
+                                .get_or_insert_default()
+                                .right_padding
                         },
                     }),
                     metadata: None,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Zoomed Padding",
+                    description: "Whether to show padding for zoomed panels",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| &settings_content.workspace.zoomed_padding,
+                        pick_mut: |settings_content| &mut settings_content.workspace.zoomed_padding,
+                    }),
+                    metadata: None,
                     files: USER,
                 }),
                 SettingsPageItem::SectionHeader("Window"),
@@ -1007,7 +1036,7 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     }),
                     SettingsPageItem::SettingItem(SettingItem {
                         title: "Min Line Number Digits",
-                        description: "Minimum number of characters to reserve space for in the gutter.",
+                        description: "Minimum number of characters to reserve space for in the gutter",
                         field: Box::new(SettingField {
                             pick: |settings_content| {
                                 if let Some(gutter) = &settings_content.editor.gutter {
@@ -1442,18 +1471,23 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                         metadata: None,
                         files: USER,
                     }),
-                    // files: USER,
-                    // SettingsPageItem::SettingItem(SettingItem {
-                    //     title: "Maximum Tabs",
-                    //     description: "Maximum open tabs in a pane. Will not close an unsaved tab",
-                    //     // todo(settings_ui): The default for this value is null and it's use in code
-                    //     // is complex, so I'm going to come back to this later
-                    //     field: Box::new(SettingField {
-                    //         pick: |settings_content| &settings_content.workspace.max_tabs,
-                    //         pick_mut: |settings_content| &mut settings_content.workspace.max_tabs,
-                    //     }),
-                    //     metadata: None,
-                    // }),
+                    SettingsPageItem::SettingItem(SettingItem {
+                        files: USER,
+                        title: "Maximum Tabs",
+                        description: "Maximum open tabs in a pane. Will not close an unsaved tab",
+                        // todo(settings_ui): The default for this value is null and it's use in code
+                        // is complex, so I'm going to come back to this later
+                        field: Box::new(
+                            SettingField {
+                                pick: |settings_content| &settings_content.workspace.max_tabs,
+                                pick_mut: |settings_content| {
+                                    &mut settings_content.workspace.max_tabs
+                                },
+                            }
+                            .unimplemented(),
+                        ),
+                        metadata: None,
+                    }),
                     SettingsPageItem::SectionHeader("Toolbar"),
                     SettingsPageItem::SettingItem(SettingItem {
                         title: "Breadcrumbs",
@@ -1578,40 +1612,65 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     title: "JSON",
                     files: USER | LOCAL,
                     render: Arc::new(|this, window, cx| {
-                        this.render_page_items(language_settings_data().iter(), window, cx)
-                            .into_any_element()
+                        this.render_page_items(
+                            language_settings_data().iter().enumerate(),
+                            None,
+                            window,
+                            cx,
+                        )
+                        .into_any_element()
                     }),
                 }),
                 SettingsPageItem::SubPageLink(SubPageLink {
                     title: "JSONC",
                     files: USER | LOCAL,
                     render: Arc::new(|this, window, cx| {
-                        this.render_page_items(language_settings_data().iter(), window, cx)
-                            .into_any_element()
+                        this.render_page_items(
+                            language_settings_data().iter().enumerate(),
+                            None,
+                            window,
+                            cx,
+                        )
+                        .into_any_element()
                     }),
                 }),
                 SettingsPageItem::SubPageLink(SubPageLink {
                     title: "Rust",
                     files: USER | LOCAL,
                     render: Arc::new(|this, window, cx| {
-                        this.render_page_items(language_settings_data().iter(), window, cx)
-                            .into_any_element()
+                        this.render_page_items(
+                            language_settings_data().iter().enumerate(),
+                            None,
+                            window,
+                            cx,
+                        )
+                        .into_any_element()
                     }),
                 }),
                 SettingsPageItem::SubPageLink(SubPageLink {
                     title: "Python",
                     files: USER | LOCAL,
                     render: Arc::new(|this, window, cx| {
-                        this.render_page_items(language_settings_data().iter(), window, cx)
-                            .into_any_element()
+                        this.render_page_items(
+                            language_settings_data().iter().enumerate(),
+                            None,
+                            window,
+                            cx,
+                        )
+                        .into_any_element()
                     }),
                 }),
                 SettingsPageItem::SubPageLink(SubPageLink {
                     title: "TSX",
                     files: USER | LOCAL,
                     render: Arc::new(|this, window, cx| {
-                        this.render_page_items(language_settings_data().iter(), window, cx)
-                            .into_any_element()
+                        this.render_page_items(
+                            language_settings_data().iter().enumerate(),
+                            None,
+                            window,
+                            cx,
+                        )
+                        .into_any_element()
                     }),
                 }),
             ],
@@ -1620,6 +1679,27 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
             title: "Workbench & Window",
             items: vec![
                 SettingsPageItem::SectionHeader("Status Bar"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Project Panel Button",
+                    description: "Whether to show the project panel button in the status bar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(project_panel) = &settings_content.project_panel {
+                                &project_panel.button
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .project_panel
+                                .get_or_insert_default()
+                                .button
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Active Language Button",
                     description: "Whether to show the active language button in the status bar",
@@ -1720,6 +1800,24 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Debugger Button",
+                    description: "Whether to show the debugger button in the status bar",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(debugger) = &settings_content.debugger {
+                                &debugger.button
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.debugger.get_or_insert_default().button
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
                 SettingsPageItem::SectionHeader("Tab Bar"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Editor Tabs",
@@ -1826,7 +1924,7 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Show Onboarding Banner",
-                    description: "Whether to show onboarding banners in the titlebar",
+                    description: "Whether to show banners announcing new features in the titlebar",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
                             if let Some(title_bar) = &settings_content.title_bar {
@@ -2207,54 +2305,36 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     files: USER,
                 }),
                 // todo: null by default
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Include Ignored",
-                //     description: "Whether to use gitignored files when searching",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| {
-                //             if let Some(file_finder) = &settings_content.file_finder {
-                //                 &file_finder.include_ignored
-                //             } else {
-                //                 &None
-                //             }
-                //         },
-                //         pick_mut: |settings_content| {
-                //             &mut settings_content
-                //                 .file_finder
-                //                 .get_or_insert_default()
-                //                 .include_ignored
-                //         },
-                //     }),
-                //     metadata: None,
-                // }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Include Ignored",
+                    description: "Whether to use gitignored files when searching",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| {
+                                if let Some(file_finder) = &settings_content.file_finder {
+                                    &file_finder.include_ignored
+                                } else {
+                                    &None
+                                }
+                            },
+                            pick_mut: |settings_content| {
+                                &mut settings_content
+                                    .file_finder
+                                    .get_or_insert_default()
+                                    .include_ignored
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                    files: USER,
+                }),
             ],
         },
         SettingsPage {
             title: "Panels",
             items: vec![
                 SettingsPageItem::SectionHeader("Project Panel"),
-                SettingsPageItem::SettingItem(SettingItem {
-                    title: "Project Panel Button",
-                    description: "Whether to show the project panel button in the status bar",
-                    field: Box::new(SettingField {
-                        pick: |settings_content| {
-                            if let Some(project_panel) = &settings_content.project_panel {
-                                &project_panel.button
-                            } else {
-                                &None
-                            }
-                        },
-                        pick_mut: |settings_content| {
-                            &mut settings_content
-                                .project_panel
-                                .get_or_insert_default()
-                                .button
-                        },
-                    }),
-                    metadata: None,
-                    files: USER,
-                }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Project Panel Dock",
                     description: "Where to dock the project panel",
@@ -2338,7 +2418,7 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "File Icons",
-                    description: "Whether to show folder icons or chevrons for directories in the project panel",
+                    description: "Whether to show file icons in the project panel",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
                             if let Some(project_panel) = &settings_content.project_panel {
@@ -2483,31 +2563,34 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Scrollbar Show",
-                //     description: "When to show the scrollbar in the project panel",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| {
-                //             if let Some(project_panel) = &settings_content.project_panel {
-                //                 if let Some(scrollbar) = &project_panel.scrollbar {
-                //                     &scrollbar.show
-                //                 } else {
-                //                     &None
-                //                 }
-                //             } else {
-                //                 &None
-                //             }
-                //         },
-                //         pick_mut: |settings_content| {
-                //             &mut settings_content
-                //                 .project_panel
-                //                 .get_or_insert_default()
-                //                 .scrollbar
-                //         },
-                //     }),
-                //     metadata: None,
-                // }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Scrollbar Show",
+                    description: "When to show the scrollbar in the project panel",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(project_panel) = &settings_content.project_panel
+                                && let Some(scrollbar) = &project_panel.scrollbar
+                                && scrollbar.show.is_some()
+                            {
+                                &scrollbar.show
+                            } else if let Some(scrollbar) = &settings_content.editor.scrollbar {
+                                &scrollbar.show
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .project_panel
+                                .get_or_insert_default()
+                                .scrollbar
+                                .get_or_insert_default()
+                                .show
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Show Diagnostics",
                     description: "Which files containing diagnostic errors/warnings to mark in the project panel",
@@ -2550,33 +2633,36 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Indent Guides Show",
-                //     description: "When to show indent guides in the project panel",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| {
-                //             if let Some(project_panel) = &settings_content.project_panel {
-                //                 if let Some(indent_guides) = &project_panel.indent_guides {
-                //                     &indent_guides.show
-                //                 } else {
-                //                     &None
-                //                 }
-                //             } else {
-                //                 &None
-                //             }
-                //         },
-                //         pick_mut: |settings_content| {
-                //             &mut settings_content
-                //                 .project_panel
-                //                 .get_or_insert_default()
-                //                 .indent_guides
-                //                 .get_or_insert_default()
-                //                 .show
-                //         },
-                //     }),
-                //     metadata: None,
-                // }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    files: USER,
+                    title: "Indent Guides Show",
+                    description: "When to show indent guides in the project panel",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| {
+                                if let Some(project_panel) = &settings_content.project_panel {
+                                    if let Some(indent_guides) = &project_panel.indent_guides {
+                                        &indent_guides.show
+                                    } else {
+                                        &None
+                                    }
+                                } else {
+                                    &None
+                                }
+                            },
+                            pick_mut: |settings_content| {
+                                &mut settings_content
+                                    .project_panel
+                                    .get_or_insert_default()
+                                    .indent_guides
+                                    .get_or_insert_default()
+                                    .show
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Drag and Drop",
                     description: "Whether to enable drag-and-drop operations in the project panel",
@@ -3011,37 +3097,40 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Indent Guides Show",
-                //     description: "When to show indent guides in the outline panel",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| {
-                //             if let Some(outline_panel) = &settings_content.outline_panel {
-                //                 if let Some(indent_guides) = &outline_panel.indent_guides {
-                //                     &indent_guides.show
-                //                 } else {
-                //                     &None
-                //                 }
-                //             } else {
-                //                 &None
-                //             }
-                //         },
-                //         pick_mut: |settings_content| {
-                //             &mut settings_content
-                //                 .outline_panel
-                //                 .get_or_insert_default()
-                //                 .indent_guides
-                //                 .get_or_insert_default()
-                //                 .show
-                //         },
-                //     }),
-                //     metadata: None,
-                // }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    files: USER,
+                    title: "Indent Guides Show",
+                    description: "When to show indent guides in the outline panel",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| {
+                                if let Some(outline_panel) = &settings_content.outline_panel {
+                                    if let Some(indent_guides) = &outline_panel.indent_guides {
+                                        &indent_guides.show
+                                    } else {
+                                        &None
+                                    }
+                                } else {
+                                    &None
+                                }
+                            },
+                            pick_mut: |settings_content| {
+                                &mut settings_content
+                                    .outline_panel
+                                    .get_or_insert_default()
+                                    .indent_guides
+                                    .get_or_insert_default()
+                                    .show
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                }),
                 SettingsPageItem::SectionHeader("Git Panel"),
                 SettingsPageItem::SettingItem(SettingItem {
-                    title: "Button",
-                    description: "Whether to show the git panel button in the status bar",
+                    title: "Git Panel Button",
+                    description: "Whether to show the Git panel button in the status bar",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
                             if let Some(git_panel) = &settings_content.git_panel {
@@ -3058,8 +3147,8 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     files: USER,
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
-                    title: "Dock",
-                    description: "Where to dock the git panel",
+                    title: "Git Panel Dock",
+                    description: "Where to dock the Git panel",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
                             if let Some(git_panel) = &settings_content.git_panel {
@@ -3076,8 +3165,8 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     files: USER,
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
-                    title: "Default Width",
-                    description: "Default width of the git panel in pixels",
+                    title: "Git Panel Default Width",
+                    description: "Default width of the Git panel in pixels",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
                             if let Some(git_panel) = &settings_content.git_panel {
@@ -3096,9 +3185,28 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
-                SettingsPageItem::SectionHeader("Notification Panel"),
+                SettingsPageItem::SectionHeader("Debugger Panel"),
                 SettingsPageItem::SettingItem(SettingItem {
-                    title: "Notification Panel Button",
+                    title: "Debugger Panel Dock",
+                    description: "The dock position of the debug panel",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(debugger) = &settings_content.debugger {
+                                &debugger.dock
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.debugger.get_or_insert_default().dock
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SectionHeader("Notification Panel"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Notification Panel Button",
                     description: "Whether to show the notification panel button in the status bar",
                     field: Box::new(SettingField {
                         pick: |settings_content| {
@@ -3484,18 +3592,21 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
             items: vec![
                 SettingsPageItem::SectionHeader("Network"),
                 // todo(settings_ui): Proxy needs a default
-                // files: USER,
-                // SettingsPageItem::SettingItem(SettingItem {
-                //     title: "Proxy",
-                //     description: "The proxy to use for network requests",
-                //     field: Box::new(SettingField {
-                //         pick: |settings_content| &settings_content.proxy,
-                //         pick_mut: |settings_content| &mut settings_content.proxy,
-                //     }),
-                //     metadata: Some(Box::new(SettingsFieldMetadata {
-                //         placeholder: Some("socks5h://localhost:10808"),
-                //     })),
-                // }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Proxy",
+                    description: "The proxy to use for network requests",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| &settings_content.proxy,
+                            pick_mut: |settings_content| &mut settings_content.proxy,
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: Some(Box::new(SettingsFieldMetadata {
+                        placeholder: Some("socks5h://localhost:10808"),
+                    })),
+                    files: USER,
+                }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Server URL",
                     description: "The URL of the Zed server to connect to",
@@ -3789,24 +3900,6 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
-                SettingsPageItem::SettingItem(SettingItem {
-                    title: "Dock",
-                    description: "The dock position of the debug panel",
-                    field: Box::new(SettingField {
-                        pick: |settings_content| {
-                            if let Some(debugger) = &settings_content.debugger {
-                                &debugger.dock
-                            } else {
-                                &None
-                            }
-                        },
-                        pick_mut: |settings_content| {
-                            &mut settings_content.debugger.get_or_insert_default().dock
-                        },
-                    }),
-                    metadata: None,
-                    files: USER,
-                }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Log DAP Communications",
                     description: "Whether to log messages between active debug adapters and Zed",
@@ -3849,24 +3942,6 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
-                SettingsPageItem::SettingItem(SettingItem {
-                    title: "Button",
-                    description: "Whether to show the debug button in the status bar",
-                    field: Box::new(SettingField {
-                        pick: |settings_content| {
-                            if let Some(debugger) = &settings_content.debugger {
-                                &debugger.button
-                            } else {
-                                &None
-                            }
-                        },
-                        pick_mut: |settings_content| {
-                            &mut settings_content.debugger.get_or_insert_default().button
-                        },
-                    }),
-                    metadata: None,
-                    files: USER,
-                }),
             ],
         },
         SettingsPage {
@@ -4027,6 +4102,519 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
                 }),
             ],
         },
+        SettingsPage {
+            title: "Terminal",
+            items: vec![
+                SettingsPageItem::SectionHeader("Environment"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Shell",
+                    description: "What shell to use when opening a terminal",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| {
+                                if let Some(terminal) = &settings_content.terminal {
+                                    &terminal.project.shell
+                                } else {
+                                    &None
+                                }
+                            },
+                            pick_mut: |settings_content| {
+                                &mut settings_content
+                                    .terminal
+                                    .get_or_insert_default()
+                                    .project
+                                    .shell
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                    files: USER | LOCAL,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Working Directory",
+                    description: "What working directory to use when launching the terminal",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| {
+                                if let Some(terminal) = &settings_content.terminal {
+                                    &terminal.project.working_directory
+                                } else {
+                                    &None
+                                }
+                            },
+                            pick_mut: |settings_content| {
+                                &mut settings_content
+                                    .terminal
+                                    .get_or_insert_default()
+                                    .project
+                                    .working_directory
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                    files: USER | LOCAL,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Environment Variables",
+                    description: "Key-value pairs to add to the terminal's environment",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| {
+                                if let Some(terminal) = &settings_content.terminal {
+                                    &terminal.project.env
+                                } else {
+                                    &None
+                                }
+                            },
+                            pick_mut: |settings_content| {
+                                &mut settings_content
+                                    .terminal
+                                    .get_or_insert_default()
+                                    .project
+                                    .env
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                    files: USER | LOCAL,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Detect Virtual Environment",
+                    description: "Activates the python virtual environment, if one is found, in the terminal's working directory",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| {
+                                if let Some(terminal) = &settings_content.terminal {
+                                    &terminal.project.detect_venv
+                                } else {
+                                    &None
+                                }
+                            },
+                            pick_mut: |settings_content| {
+                                &mut settings_content
+                                    .terminal
+                                    .get_or_insert_default()
+                                    .project
+                                    .detect_venv
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                    files: USER | LOCAL,
+                }),
+                SettingsPageItem::SectionHeader("Font"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Font Size",
+                    description: "Font size for terminal text. If not set, defaults to buffer font size",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.font_size
+                            } else if settings_content.theme.buffer_font_size.is_some() {
+                                &settings_content.theme.buffer_font_size
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.terminal.get_or_insert_default().font_size
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Font Family",
+                    description: "Font family for terminal text. If not set, defaults to buffer font family",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal
+                                && terminal.font_family.is_some()
+                            {
+                                &terminal.font_family
+                            } else if settings_content.theme.buffer_font_family.is_some() {
+                                &settings_content.theme.buffer_font_family
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .font_family
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Font Fallbacks",
+                    description: "Font fallbacks for terminal text. If not set, defaults to buffer font fallbacks",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| {
+                                if let Some(terminal) = &settings_content.terminal {
+                                    &terminal.font_fallbacks
+                                } else {
+                                    &None
+                                }
+                            },
+                            pick_mut: |settings_content| {
+                                &mut settings_content
+                                    .terminal
+                                    .get_or_insert_default()
+                                    .font_fallbacks
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Font Weight",
+                    description: "Font weight for terminal text in CSS weight units (100-900)",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.font_weight
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .font_weight
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Font Features",
+                    description: "Font features for terminal text",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| {
+                                if let Some(terminal) = &settings_content.terminal {
+                                    &terminal.font_features
+                                } else {
+                                    &None
+                                }
+                            },
+                            pick_mut: |settings_content| {
+                                &mut settings_content
+                                    .terminal
+                                    .get_or_insert_default()
+                                    .font_features
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SectionHeader("Display Settings"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Line Height",
+                    description: "Line height for terminal text",
+                    field: Box::new(
+                        SettingField {
+                            pick: |settings_content| {
+                                if let Some(terminal) = &settings_content.terminal {
+                                    &terminal.line_height
+                                } else {
+                                    &None
+                                }
+                            },
+                            pick_mut: |settings_content| {
+                                &mut settings_content
+                                    .terminal
+                                    .get_or_insert_default()
+                                    .line_height
+                            },
+                        }
+                        .unimplemented(),
+                    ),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Cursor Shape",
+                    description: "Default cursor shape for the terminal (bar, block, underline, or hollow)",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.cursor_shape
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .cursor_shape
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Cursor Blinking",
+                    description: "Sets the cursor blinking behavior in the terminal",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.blinking
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content.terminal.get_or_insert_default().blinking
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Alternate Scroll",
+                    description: "Whether Alternate Scroll mode is active by default (converts mouse scroll to arrow keys in apps like vim)",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.alternate_scroll
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .alternate_scroll
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Minimum Contrast",
+                    description: "The minimum APCA perceptual contrast between foreground and background colors (0-106)",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.minimum_contrast
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .minimum_contrast
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SectionHeader("Behavior Settings"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Option As Meta",
+                    description: "Whether the option key behaves as the meta key",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.option_as_meta
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .option_as_meta
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Copy On Select",
+                    description: "Whether selecting text in the terminal automatically copies to the system clipboard",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.copy_on_select
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .copy_on_select
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Keep Selection On Copy",
+                    description: "Whether to keep the text selection after copying it to the clipboard",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.keep_selection_on_copy
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .keep_selection_on_copy
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SectionHeader("Layout Settings"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Default Width",
+                    description: "Default width when the terminal is docked to the left or right (in pixels)",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.default_width
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .default_width
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Default Height",
+                    description: "Default height when the terminal is docked to the bottom (in pixels)",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.default_height
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .default_height
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SectionHeader("Advanced Settings"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Max Scroll History Lines",
+                    description: "Maximum number of lines to keep in scrollback history (max: 100,000; 0 disables scrolling)",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                &terminal.max_scroll_history_lines
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .max_scroll_history_lines
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SectionHeader("Toolbar"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Breadcrumbs",
+                    description: "Whether to display the terminal title in breadcrumbs inside the terminal pane",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal {
+                                if let Some(toolbar) = &terminal.toolbar {
+                                    &toolbar.breadcrumbs
+                                } else {
+                                    &None
+                                }
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .toolbar
+                                .get_or_insert_default()
+                                .breadcrumbs
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SectionHeader("Scrollbar"),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Show Scrollbar",
+                    description: "When to show the scrollbar in the terminal",
+                    field: Box::new(SettingField {
+                        pick: |settings_content| {
+                            if let Some(terminal) = &settings_content.terminal
+                                && let Some(scrollbar) = &terminal.scrollbar
+                                && scrollbar.show.is_some()
+                            {
+                                &scrollbar.show
+                            } else if let Some(scrollbar) = &settings_content.editor.scrollbar {
+                                &scrollbar.show
+                            } else {
+                                &None
+                            }
+                        },
+                        pick_mut: |settings_content| {
+                            &mut settings_content
+                                .terminal
+                                .get_or_insert_default()
+                                .scrollbar
+                                .get_or_insert_default()
+                                .show
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+            ],
+        },
     ]
 }
 

crates/settings_ui/src/settings_ui.rs 🔗

@@ -4,12 +4,13 @@ mod page_data;
 
 use anyhow::Result;
 use editor::{Editor, EditorEvent};
-use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
+use feature_flags::FeatureFlag;
 use fuzzy::StringMatchCandidate;
 use gpui::{
     Action, App, Div, Entity, FocusHandle, Focusable, FontWeight, Global, ReadGlobal as _,
-    ScrollHandle, Task, TitlebarOptions, UniformListScrollHandle, Window, WindowHandle,
-    WindowOptions, actions, div, point, prelude::*, px, size, uniform_list,
+    ScrollHandle, Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window,
+    WindowBounds, WindowHandle, WindowOptions, actions, div, point, prelude::*, px, size,
+    uniform_list,
 };
 use heck::ToTitleCase as _;
 use project::WorktreeId;
@@ -29,19 +30,24 @@ use std::{
     sync::{Arc, LazyLock, RwLock, atomic::AtomicBool},
 };
 use ui::{
-    ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, KeyBinding, KeybindingHint,
-    PopoverMenu, Switch, SwitchColor, TreeViewItem, WithScrollbar, prelude::*,
+    ContextMenu, Divider, DividerColor, DropdownMenu, DropdownStyle, IconButtonShape, PopoverMenu,
+    Switch, SwitchColor, Tooltip, TreeViewItem, WithScrollbar, prelude::*,
 };
 use ui_input::{NumberField, NumberFieldType};
 use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
+use workspace::{OpenOptions, OpenVisible, Workspace};
 use zed_actions::OpenSettingsEditor;
 
 use crate::components::SettingsEditor;
 
 const NAVBAR_CONTAINER_TAB_INDEX: isize = 0;
 const NAVBAR_GROUP_TAB_INDEX: isize = 1;
-const CONTENT_CONTAINER_TAB_INDEX: isize = 2;
-const CONTENT_GROUP_TAB_INDEX: isize = 3;
+
+const HEADER_CONTAINER_TAB_INDEX: isize = 2;
+const HEADER_GROUP_TAB_INDEX: isize = 3;
+
+const CONTENT_CONTAINER_TAB_INDEX: isize = 4;
+const CONTENT_GROUP_TAB_INDEX: isize = 5;
 
 actions!(
     settings_editor,
@@ -50,10 +56,24 @@ actions!(
         Minimize,
         /// Toggles focus between the navbar and the main content.
         ToggleFocusNav,
+        /// Expands the navigation entry.
+        ExpandNavEntry,
+        /// Collapses the navigation entry.
+        CollapseNavEntry,
         /// Focuses the next file in the file list.
         FocusNextFile,
         /// Focuses the previous file in the file list.
-        FocusPreviousFile
+        FocusPreviousFile,
+        /// Opens an editor for the current file
+        OpenCurrentFile,
+        /// Focuses the previous root navigation entry.
+        FocusPreviousRootNavEntry,
+        /// Focuses the next root navigation entry.
+        FocusNextRootNavEntry,
+        /// Focuses the first navigation entry.
+        FocusFirstNavEntry,
+        /// Focuses the last navigation entry.
+        FocusLastNavEntry
     ]
 );
 
@@ -180,14 +200,55 @@ impl SettingFieldRenderer {
         if let Some(renderer) = self.renderers.borrow().get(&key) {
             renderer(any_setting_field, settings_file, metadata, window, cx)
         } else {
-            panic!(
-                "No renderer found for type: {}",
-                any_setting_field.type_name()
-            )
+            Button::new("no-renderer", "NO RENDERER")
+                .style(ButtonStyle::Outlined)
+                .size(ButtonSize::Medium)
+                .icon(Some(IconName::XCircle))
+                .icon_position(IconPosition::Start)
+                .icon_color(Color::Error)
+                .tab_index(0_isize)
+                .tooltip(Tooltip::text(any_setting_field.type_name()))
+                .into_any_element()
+            // panic!(
+            //     "No renderer found for type: {}",
+            //     any_setting_field.type_name()
+            // )
         }
     }
 }
 
+struct NonFocusableHandle {
+    handle: FocusHandle,
+    _subscription: Subscription,
+}
+
+impl NonFocusableHandle {
+    fn new(tab_index: isize, tab_stop: bool, window: &mut Window, cx: &mut App) -> Entity<Self> {
+        let handle = cx.focus_handle().tab_index(tab_index).tab_stop(tab_stop);
+        Self::from_handle(handle, window, cx)
+    }
+
+    fn from_handle(handle: FocusHandle, window: &mut Window, cx: &mut App) -> Entity<Self> {
+        cx.new(|cx| {
+            let _subscription = cx.on_focus(&handle, window, {
+                move |_, window, _| {
+                    window.focus_next();
+                }
+            });
+            Self {
+                handle,
+                _subscription,
+            }
+        })
+    }
+}
+
+impl Focusable for NonFocusableHandle {
+    fn focus_handle(&self, _: &App) -> FocusHandle {
+        self.handle.clone()
+    }
+}
+
 struct SettingsFieldMetadata {
     placeholder: Option<&'static str>,
 }
@@ -202,46 +263,27 @@ pub fn init(cx: &mut App) {
     init_renderers(cx);
 
     cx.observe_new(|workspace: &mut workspace::Workspace, _, _| {
-        workspace.register_action_renderer(|div, _, _, cx| {
-            let settings_ui_actions = [
-                TypeId::of::<OpenSettingsEditor>(),
-                TypeId::of::<ToggleFocusNav>(),
-                TypeId::of::<FocusFile>(),
-                TypeId::of::<FocusNextFile>(),
-                TypeId::of::<FocusPreviousFile>(),
-            ];
-            let has_flag = cx.has_flag::<SettingsUiFeatureFlag>();
-            command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| {
-                if has_flag {
-                    filter.show_action_types(&settings_ui_actions);
-                } else {
-                    filter.hide_action_types(&settings_ui_actions);
-                }
-            });
-            if has_flag {
-                div.on_action(cx.listener(|_, _: &OpenSettingsEditor, _, cx| {
-                    open_settings_editor(cx).ok();
-                }))
-            } else {
-                div
-            }
+        workspace.register_action(|workspace, _: &OpenSettingsEditor, window, cx| {
+            let window_handle = window
+                .window_handle()
+                .downcast::<Workspace>()
+                .expect("Workspaces are root Windows");
+            open_settings_editor(workspace, window_handle, cx);
         });
     })
     .detach();
 }
 
 fn init_renderers(cx: &mut App) {
-    // fn (field: SettingsField, current_file: SettingsFile, cx) -> (currently_set_in: SettingsFile, overridden_in: Vec<SettingsFile>)
     cx.default_global::<SettingFieldRenderer>()
         .add_renderer::<UnimplementedSettingField>(|_, _, _, _, _| {
-            // TODO(settings_ui): In non-dev builds (`#[cfg(not(debug_assertions))]`) make this render as edit-in-json
-            Button::new("unimplemented-field", "UNIMPLEMENTED")
-                .size(ButtonSize::Medium)
-                .icon(IconName::XCircle)
-                .icon_position(IconPosition::Start)
-                .icon_color(Color::Error)
-                .icon_size(IconSize::Small)
+            Button::new("open-in-settings-file", "Edit in settings.json")
                 .style(ButtonStyle::Outlined)
+                .size(ButtonSize::Medium)
+                .tab_index(0_isize)
+                .on_click(|_, window, cx| {
+                    window.dispatch_action(Box::new(OpenCurrentFile), cx);
+                })
                 .into_any_element()
         })
         .add_renderer::<bool>(|settings_field, file, _, _, cx| {
@@ -376,6 +418,15 @@ fn init_renderers(cx: &mut App) {
         .add_renderer::<settings::LspInsertMode>(|settings_field, file, _, window, cx| {
             render_dropdown(*settings_field, file, window, cx)
         })
+        .add_renderer::<settings::AlternateScroll>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::TerminalBlink>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
+        .add_renderer::<settings::CursorShapeContent>(|settings_field, file, _, window, cx| {
+            render_dropdown(*settings_field, file, window, cx)
+        })
         .add_renderer::<f32>(|settings_field, file, _, window, cx| {
             render_number_field(*settings_field, file, window, cx)
         })
@@ -440,7 +491,11 @@ fn init_renderers(cx: &mut App) {
     // });
 }
 
-pub fn open_settings_editor(cx: &mut App) -> anyhow::Result<WindowHandle<SettingsWindow>> {
+pub fn open_settings_editor(
+    _workspace: &mut Workspace,
+    workspace_handle: WindowHandle<Workspace>,
+    cx: &mut App,
+) {
     let existing_window = cx
         .windows()
         .into_iter()
@@ -448,29 +503,36 @@ pub fn open_settings_editor(cx: &mut App) -> anyhow::Result<WindowHandle<Setting
 
     if let Some(existing_window) = existing_window {
         existing_window
-            .update(cx, |_, window, _| {
+            .update(cx, |settings_window, window, _| {
+                settings_window.original_window = Some(workspace_handle);
                 window.activate_window();
             })
             .ok();
-        return Ok(existing_window);
-    }
-
-    cx.open_window(
-        WindowOptions {
-            titlebar: Some(TitlebarOptions {
-                title: Some("Settings Window".into()),
-                appears_transparent: true,
-                traffic_light_position: Some(point(px(12.0), px(12.0))),
-            }),
-            focus: true,
-            show: true,
-            kind: gpui::WindowKind::Normal,
-            window_background: cx.theme().window_background_appearance(),
-            window_min_size: Some(size(px(800.), px(600.))), // 4:3 Aspect Ratio
-            ..Default::default()
-        },
-        |window, cx| cx.new(|cx| SettingsWindow::new(window, cx)),
-    )
+        return;
+    }
+
+    // We have to defer this to get the workspace off the stack.
+
+    cx.defer(move |cx| {
+        cx.open_window(
+            WindowOptions {
+                titlebar: Some(TitlebarOptions {
+                    title: Some("Settings Window".into()),
+                    appears_transparent: true,
+                    traffic_light_position: Some(point(px(12.0), px(12.0))),
+                }),
+                focus: true,
+                show: true,
+                kind: gpui::WindowKind::Floating,
+                window_background: cx.theme().window_background_appearance(),
+                window_min_size: Some(size(px(900.), px(750.))), // 4:3 Aspect Ratio
+                window_bounds: Some(WindowBounds::centered(size(px(900.), px(750.)), cx)),
+                ..Default::default()
+            },
+            |window, cx| cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx)),
+        )
+        .log_err();
+    });
 }
 
 /// The current sub page path that is selected.
@@ -494,19 +556,23 @@ fn sub_page_stack_mut() -> std::sync::RwLockWriteGuard<'static, Vec<SubPage>> {
 }
 
 pub struct SettingsWindow {
+    original_window: Option<WindowHandle<Workspace>>,
     files: Vec<(SettingsUiFile, FocusHandle)>,
+    worktree_root_dirs: HashMap<WorktreeId, String>,
     current_file: SettingsUiFile,
     pages: Vec<SettingsPage>,
     search_bar: Entity<Editor>,
     search_task: Option<Task<()>>,
-    navbar_entry: usize, // Index into pages - should probably be (usize, Option<usize>) for section + page
+    /// Index into navbar_entries
+    navbar_entry: usize,
     navbar_entries: Vec<NavBarEntry>,
     list_handle: UniformListScrollHandle,
     search_matches: Vec<Vec<bool>>,
+    content_handles: Vec<Vec<Entity<NonFocusableHandle>>>,
     scroll_handle: ScrollHandle,
     focus_handle: FocusHandle,
-    navbar_focus_handle: FocusHandle,
-    content_focus_handle: FocusHandle,
+    navbar_focus_handle: Entity<NonFocusableHandle>,
+    content_focus_handle: Entity<NonFocusableHandle>,
     files_focus_handle: FocusHandle,
 }
 
@@ -515,13 +581,14 @@ struct SubPage {
     section_header: &'static str,
 }
 
-#[derive(PartialEq, Debug)]
+#[derive(Debug)]
 struct NavBarEntry {
     title: &'static str,
     is_root: bool,
     expanded: bool,
     page_index: usize,
     item_index: Option<usize>,
+    focus_handle: FocusHandle,
 }
 
 struct SettingsPage {
@@ -553,23 +620,24 @@ impl std::fmt::Debug for SettingsPageItem {
 impl SettingsPageItem {
     fn render(
         &self,
-        file: SettingsUiFile,
+        settings_window: &SettingsWindow,
         section_header: &'static str,
         is_last: bool,
         window: &mut Window,
         cx: &mut Context<SettingsWindow>,
     ) -> AnyElement {
+        let file = settings_window.current_file.clone();
         match self {
             SettingsPageItem::SectionHeader(header) => v_flex()
                 .w_full()
-                .gap_1()
+                .gap_1p5()
                 .child(
                     Label::new(SharedString::new_static(header))
-                        .size(LabelSize::XSmall)
+                        .size(LabelSize::Small)
                         .color(Color::Muted)
                         .buffer_font(cx),
                 )
-                .child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
+                .child(Divider::horizontal().color(DividerColor::BorderFaded))
                 .into_any_element(),
             SettingsPageItem::SettingItem(setting_item) => {
                 let renderer = cx.default_global::<SettingFieldRenderer>().clone();
@@ -578,13 +646,13 @@ impl SettingsPageItem {
 
                 h_flex()
                     .id(setting_item.title)
-                    .w_full()
+                    .min_w_0()
                     .gap_2()
-                    .flex_wrap()
                     .justify_between()
+                    .pt_4()
                     .map(|this| {
                         if is_last {
-                            this.pb_6()
+                            this.pb_10()
                         } else {
                             this.pb_4()
                                 .border_b_1()
@@ -593,8 +661,8 @@ impl SettingsPageItem {
                     })
                     .child(
                         v_flex()
+                            .w_full()
                             .max_w_1_2()
-                            .flex_shrink()
                             .child(
                                 h_flex()
                                     .w_full()
@@ -606,7 +674,9 @@ impl SettingsPageItem {
                                             this.child(
                                                 Label::new(format!(
                                                     "— set in {}",
-                                                    file_set_in.name()
+                                                    settings_window
+                                                        .display_name(&file_set_in)
+                                                        .expect("File name should exist")
                                                 ))
                                                 .color(Color::Muted)
                                                 .size(LabelSize::Small),
@@ -628,6 +698,9 @@ impl SettingsPageItem {
                             .icon_color(Color::Error)
                             .icon_size(IconSize::Small)
                             .style(ButtonStyle::Outlined)
+                            .tooltip(Tooltip::text(
+                                "This warning is only displayed in dev builds.",
+                            ))
                             .into_any_element()
                     } else {
                         renderer.render(
@@ -643,9 +716,10 @@ impl SettingsPageItem {
             SettingsPageItem::SubPageLink(sub_page_link) => h_flex()
                 .id(sub_page_link.title)
                 .w_full()
+                .min_w_0()
                 .gap_2()
-                .flex_wrap()
                 .justify_between()
+                .pt_4()
                 .when(!is_last, |this| {
                     this.pb_4()
                         .border_b_1()
@@ -653,18 +727,18 @@ impl SettingsPageItem {
                 })
                 .child(
                     v_flex()
+                        .w_full()
                         .max_w_1_2()
-                        .flex_shrink()
                         .child(Label::new(SharedString::new_static(sub_page_link.title))),
                 )
                 .child(
                     Button::new(("sub-page".into(), sub_page_link.title), "Configure")
-                        .size(ButtonSize::Medium)
                         .icon(IconName::ChevronRight)
                         .icon_position(IconPosition::End)
                         .icon_color(Color::Muted)
                         .icon_size(IconSize::Small)
-                        .style(ButtonStyle::Outlined),
+                        .style(ButtonStyle::Outlined)
+                        .size(ButtonSize::Medium),
                 )
                 .on_click({
                     let sub_page_link = sub_page_link.clone();
@@ -766,27 +840,28 @@ impl PartialEq for SubPageLink {
 #[allow(unused)]
 #[derive(Clone, PartialEq)]
 enum SettingsUiFile {
-    User,                              // Uses all settings.
-    Local((WorktreeId, Arc<RelPath>)), // Has a special name, and special set of settings
-    Server(&'static str),              // Uses a special name, and the user settings
+    User,                                // Uses all settings.
+    Project((WorktreeId, Arc<RelPath>)), // Has a special name, and special set of settings
+    Server(&'static str),                // Uses a special name, and the user settings
 }
 
 impl SettingsUiFile {
-    fn name(&self) -> SharedString {
+    fn is_server(&self) -> bool {
+        matches!(self, SettingsUiFile::Server(_))
+    }
+
+    fn worktree_id(&self) -> Option<WorktreeId> {
         match self {
-            SettingsUiFile::User => SharedString::new_static("User"),
-            // TODO is PathStyle::local() ever not appropriate?
-            SettingsUiFile::Local((_, path)) => {
-                format!("Local ({})", path.display(PathStyle::local())).into()
-            }
-            SettingsUiFile::Server(file) => format!("Server ({})", file).into(),
+            SettingsUiFile::User => None,
+            SettingsUiFile::Project((worktree_id, _)) => Some(*worktree_id),
+            SettingsUiFile::Server(_) => None,
         }
     }
 
     fn from_settings(file: settings::SettingsFile) -> Option<Self> {
         Some(match file {
             settings::SettingsFile::User => SettingsUiFile::User,
-            settings::SettingsFile::Local(location) => SettingsUiFile::Local(location),
+            settings::SettingsFile::Project(location) => SettingsUiFile::Project(location),
             settings::SettingsFile::Server => SettingsUiFile::Server("todo: server name"),
             settings::SettingsFile::Default => return None,
         })
@@ -795,7 +870,7 @@ impl SettingsUiFile {
     fn to_settings(&self) -> settings::SettingsFile {
         match self {
             SettingsUiFile::User => settings::SettingsFile::User,
-            SettingsUiFile::Local(location) => settings::SettingsFile::Local(location.clone()),
+            SettingsUiFile::Project(location) => settings::SettingsFile::Project(location.clone()),
             SettingsUiFile::Server(_) => settings::SettingsFile::Server,
         }
     }
@@ -803,14 +878,18 @@ impl SettingsUiFile {
     fn mask(&self) -> FileMask {
         match self {
             SettingsUiFile::User => USER,
-            SettingsUiFile::Local(_) => LOCAL,
+            SettingsUiFile::Project(_) => LOCAL,
             SettingsUiFile::Server(_) => SERVER,
         }
     }
 }
 
 impl SettingsWindow {
-    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
+    pub fn new(
+        original_window: Option<WindowHandle<Workspace>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
         let font_family_cache = theme::FontFamilyCache::global(cx);
 
         cx.spawn(async move |this, cx| {
@@ -837,13 +916,15 @@ impl SettingsWindow {
         })
         .detach();
 
-        cx.observe_global_in::<SettingsStore>(window, move |this, _, cx| {
-            this.fetch_files(cx);
+        cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
+            this.fetch_files(window, cx);
             cx.notify();
         })
         .detach();
 
         let mut this = Self {
+            original_window,
+            worktree_root_dirs: HashMap::default(),
             files: vec![],
             current_file: current_file,
             pages: vec![],
@@ -853,21 +934,29 @@ impl SettingsWindow {
             search_bar,
             search_task: None,
             search_matches: vec![],
+            content_handles: vec![],
             scroll_handle: ScrollHandle::new(),
             focus_handle: cx.focus_handle(),
-            navbar_focus_handle: cx
-                .focus_handle()
-                .tab_index(NAVBAR_CONTAINER_TAB_INDEX)
-                .tab_stop(false),
-            content_focus_handle: cx
+            navbar_focus_handle: NonFocusableHandle::new(
+                NAVBAR_CONTAINER_TAB_INDEX,
+                false,
+                window,
+                cx,
+            ),
+            content_focus_handle: NonFocusableHandle::new(
+                CONTENT_CONTAINER_TAB_INDEX,
+                false,
+                window,
+                cx,
+            ),
+            files_focus_handle: cx
                 .focus_handle()
-                .tab_index(CONTENT_CONTAINER_TAB_INDEX)
+                .tab_index(HEADER_CONTAINER_TAB_INDEX)
                 .tab_stop(false),
-            files_focus_handle: cx.focus_handle().tab_stop(false),
         };
 
-        this.fetch_files(cx);
-        this.build_ui(cx);
+        this.fetch_files(window, cx);
+        this.build_ui(window, cx);
 
         this.search_bar.update(cx, |editor, cx| {
             editor.focus_handle(cx).focus(window);
@@ -876,25 +965,27 @@ impl SettingsWindow {
         this
     }
 
-    fn toggle_navbar_entry(&mut self, ix: usize) {
+    fn toggle_navbar_entry(&mut self, nav_entry_index: usize) {
         // We can only toggle root entries
-        if !self.navbar_entries[ix].is_root {
+        if !self.navbar_entries[nav_entry_index].is_root {
             return;
         }
 
-        let toggle_page_index = self.page_index_from_navbar_index(ix);
-        let selected_page_index = self.page_index_from_navbar_index(self.navbar_entry);
-
-        let expanded = &mut self.navbar_entries[ix].expanded;
+        let expanded = &mut self.navbar_entries[nav_entry_index].expanded;
         *expanded = !*expanded;
+        let expanded = *expanded;
+
+        let toggle_page_index = self.page_index_from_navbar_index(nav_entry_index);
+        let selected_page_index = self.page_index_from_navbar_index(self.navbar_entry);
         // if currently selected page is a child of the parent page we are folding,
         // set the current page to the parent page
-        if !*expanded && selected_page_index == toggle_page_index {
-            self.navbar_entry = ix;
+        if !expanded && selected_page_index == toggle_page_index {
+            self.navbar_entry = nav_entry_index;
+            // note: not opening page. Toggling does not change content just selected page
         }
     }
 
-    fn build_navbar(&mut self) {
+    fn build_navbar(&mut self, cx: &App) {
         let mut prev_navbar_state = HashMap::new();
         let mut root_entry = "";
         let mut prev_selected_entry = None;
@@ -921,6 +1012,7 @@ impl SettingsWindow {
                 expanded: false,
                 page_index,
                 item_index: None,
+                focus_handle: cx.focus_handle().tab_index(0).tab_stop(true),
             });
 
             for (item_index, item) in page.items.iter().enumerate() {
@@ -933,6 +1025,7 @@ impl SettingsWindow {
                     expanded: false,
                     page_index,
                     item_index: Some(item_index),
+                    focus_handle: cx.focus_handle().tab_index(0).tab_stop(true),
                 });
             }
         }
@@ -949,13 +1042,13 @@ impl SettingsWindow {
             };
             let key = (root_entry, sub_entry_title);
             if Some(key) == prev_selected_entry {
-                self.navbar_entry = index;
+                self.open_navbar_entry_page(index);
                 found_nav_entry = true;
             }
             entry.expanded = *prev_navbar_state.get(&key).unwrap_or(&false);
         }
         if !found_nav_entry {
-            self.navbar_entry = 0;
+            self.open_first_nav_page();
         }
         self.navbar_entries = navbar_entries;
     }
@@ -1111,12 +1204,7 @@ impl SettingsWindow {
                     page[item_index] = true;
                 }
                 this.filter_matches_to_file();
-                let first_navbar_entry_index = this
-                    .visible_navbar_entries()
-                    .next()
-                    .map(|e| e.0)
-                    .unwrap_or(0);
-                this.navbar_entry = first_navbar_entry_index;
+                this.open_first_nav_page();
                 cx.notify();
             })
             .ok();
@@ -1131,42 +1219,34 @@ impl SettingsWindow {
             .collect::<Vec<_>>();
     }
 
-    fn build_ui(&mut self, cx: &mut Context<SettingsWindow>) {
+    fn build_content_handles(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
+        self.content_handles = self
+            .pages
+            .iter()
+            .map(|page| {
+                std::iter::repeat_with(|| NonFocusableHandle::new(0, false, window, cx))
+                    .take(page.items.len())
+                    .collect()
+            })
+            .collect::<Vec<_>>();
+    }
+
+    fn build_ui(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
         if self.pages.is_empty() {
             self.pages = page_data::settings_data();
         }
+        sub_page_stack_mut().clear();
+        self.build_content_handles(window, cx);
         self.build_search_matches();
-        self.build_navbar();
+        self.build_navbar(cx);
 
         self.update_matches(cx);
 
         cx.notify();
     }
 
-    fn calculate_navbar_entry_from_scroll_position(&mut self) {
-        let top = self.scroll_handle.top_item();
-        let bottom = self.scroll_handle.bottom_item();
-
-        let scroll_index = (top + bottom) / 2;
-        let scroll_index = scroll_index.clamp(top, bottom);
-        let mut page_index = self.navbar_entry;
-
-        while !self.navbar_entries[page_index].is_root {
-            page_index -= 1;
-        }
-
-        if self.navbar_entries[page_index].expanded {
-            let section_index = self
-                .page_items()
-                .take(scroll_index + 1)
-                .filter(|item| matches!(item, SettingsPageItem::SectionHeader(_)))
-                .count();
-
-            self.navbar_entry = section_index + page_index;
-        }
-    }
-
-    fn fetch_files(&mut self, cx: &mut Context<SettingsWindow>) {
+    fn fetch_files(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) {
+        self.worktree_root_dirs.clear();
         let prev_files = self.files.clone();
         let settings_store = cx.global::<SettingsStore>();
         let mut ui_files = vec![];
@@ -1175,12 +1255,37 @@ impl SettingsWindow {
             let Some(settings_ui_file) = SettingsUiFile::from_settings(file) else {
                 continue;
             };
+            if settings_ui_file.is_server() {
+                continue;
+            }
+
+            if let Some(worktree_id) = settings_ui_file.worktree_id() {
+                let directory_name = all_projects(cx)
+                    .find_map(|project| project.read(cx).worktree_for_id(worktree_id, cx))
+                    .and_then(|worktree| worktree.read(cx).root_dir())
+                    .and_then(|root_dir| {
+                        root_dir
+                            .file_name()
+                            .map(|os_string| os_string.to_string_lossy().to_string())
+                    });
+
+                let Some(directory_name) = directory_name else {
+                    log::error!(
+                        "No directory name found for settings file at worktree ID: {}",
+                        worktree_id
+                    );
+                    continue;
+                };
+
+                self.worktree_root_dirs.insert(worktree_id, directory_name);
+            }
+
             let focus_handle = prev_files
                 .iter()
                 .find_map(|(prev_file, handle)| {
                     (prev_file == &settings_ui_file).then(|| handle.clone())
                 })
-                .unwrap_or_else(|| cx.focus_handle());
+                .unwrap_or_else(|| cx.focus_handle().tab_index(0).tab_stop(true));
             ui_files.push((settings_ui_file, focus_handle));
         }
         ui_files.reverse();
@@ -1190,32 +1295,53 @@ impl SettingsWindow {
             .iter()
             .any(|(file, _)| file == &self.current_file);
         if !current_file_still_exists {
-            self.change_file(0, cx);
+            self.change_file(0, window, cx);
         }
     }
 
-    fn change_file(&mut self, ix: usize, cx: &mut Context<SettingsWindow>) {
+    fn open_navbar_entry_page(&mut self, navbar_entry: usize) {
+        self.navbar_entry = navbar_entry;
+        sub_page_stack_mut().clear();
+    }
+
+    fn open_first_nav_page(&mut self) {
+        let first_navbar_entry_index = self
+            .visible_navbar_entries()
+            .next()
+            .map(|e| e.0)
+            .unwrap_or(0);
+        self.open_navbar_entry_page(first_navbar_entry_index);
+    }
+
+    fn change_file(&mut self, ix: usize, window: &mut Window, cx: &mut Context<SettingsWindow>) {
         if ix >= self.files.len() {
             self.current_file = SettingsUiFile::User;
+            self.build_ui(window, cx);
             return;
         }
         if self.files[ix].0 == self.current_file {
             return;
         }
         self.current_file = self.files[ix].0.clone();
-        // self.navbar_entry = 0;
-        self.build_ui(cx);
+        self.open_navbar_entry_page(0);
+        self.build_ui(window, cx);
+
+        self.open_first_nav_page();
     }
 
-    fn render_files(
+    fn render_files_header(
         &self,
         _window: &mut Window,
         cx: &mut Context<SettingsWindow>,
     ) -> impl IntoElement {
         h_flex()
             .w_full()
+            .pb_4()
             .gap_1()
             .justify_between()
+            .tab_group()
+            .track_focus(&self.files_focus_handle)
+            .tab_index(HEADER_GROUP_TAB_INDEX)
             .child(
                 h_flex()
                     .id("file_buttons_container")
@@ -1227,28 +1353,85 @@ impl SettingsWindow {
                             .iter()
                             .enumerate()
                             .map(|(ix, (file, focus_handle))| {
-                                Button::new(ix, file.name())
-                                    .toggle_state(file == &self.current_file)
-                                    .selected_style(ButtonStyle::Tinted(ui::TintColor::Accent))
-                                    .track_focus(focus_handle)
-                                    .on_click(cx.listener(
-                                        move |this, evt: &gpui::ClickEvent, window, cx| {
-                                            this.change_file(ix, cx);
-                                            if evt.is_keyboard() {
-                                                this.focus_first_nav_item(window, cx);
-                                            }
-                                        },
-                                    ))
+                                Button::new(
+                                    ix,
+                                    self.display_name(&file)
+                                        .expect("Files should always have a name"),
+                                )
+                                .toggle_state(file == &self.current_file)
+                                .selected_style(ButtonStyle::Tinted(ui::TintColor::Accent))
+                                .track_focus(focus_handle)
+                                .on_click(cx.listener({
+                                    let focus_handle = focus_handle.clone();
+                                    move |this, _: &gpui::ClickEvent, window, cx| {
+                                        this.change_file(ix, window, cx);
+                                        focus_handle.focus(window);
+                                    }
+                                }))
                             }),
                     ),
             )
-            .child(Button::new("temp", "Edit in settings.json").style(ButtonStyle::Outlined)) // This should be replaced by the actual, functioning button
+            .child(
+                Button::new("edit-in-json", "Edit in settings.json")
+                    .tab_index(0_isize)
+                    .style(ButtonStyle::OutlinedGhost)
+                    .on_click(cx.listener(|this, _, _, cx| {
+                        this.open_current_settings_file(cx);
+                    })),
+            )
     }
 
+    pub(crate) fn display_name(&self, file: &SettingsUiFile) -> Option<String> {
+        match file {
+            SettingsUiFile::User => Some("User".to_string()),
+            SettingsUiFile::Project((worktree_id, path)) => self
+                .worktree_root_dirs
+                .get(&worktree_id)
+                .map(|directory_name| {
+                    let path_style = PathStyle::local();
+                    if path.is_empty() {
+                        directory_name.clone()
+                    } else {
+                        format!(
+                            "{}{}{}",
+                            directory_name,
+                            path_style.separator(),
+                            path.display(path_style)
+                        )
+                    }
+                }),
+            SettingsUiFile::Server(file) => Some(file.to_string()),
+        }
+    }
+
+    // TODO:
+    //  Reconsider this after preview launch
+    // fn file_location_str(&self) -> String {
+    //     match &self.current_file {
+    //         SettingsUiFile::User => "settings.json".to_string(),
+    //         SettingsUiFile::Project((worktree_id, path)) => self
+    //             .worktree_root_dirs
+    //             .get(&worktree_id)
+    //             .map(|directory_name| {
+    //                 let path_style = PathStyle::local();
+    //                 let file_path = path.join(paths::local_settings_file_relative_path());
+    //                 format!(
+    //                     "{}{}{}",
+    //                     directory_name,
+    //                     path_style.separator(),
+    //                     file_path.display(path_style)
+    //                 )
+    //             })
+    //             .expect("Current file should always be present in root dir map"),
+    //         SettingsUiFile::Server(file) => file.to_string(),
+    //     }
+    // }
+
     fn render_search(&self, _window: &mut Window, cx: &mut App) -> Div {
         h_flex()
             .py_1()
             .px_1p5()
+            .mb_3()
             .gap_1p5()
             .rounded_sm()
             .bg(cx.theme().colors().editor_background)
@@ -1264,27 +1447,95 @@ impl SettingsWindow {
         cx: &mut Context<SettingsWindow>,
     ) -> impl IntoElement {
         let visible_count = self.visible_navbar_entries().count();
-        let nav_background = cx.theme().colors().panel_background;
-        let focus_keybind_label = if self.navbar_focus_handle.contains_focused(window, cx) {
-            "Focus Content"
-        } else {
-            "Focus Navbar"
-        };
+
+        // let focus_keybind_label = if self.navbar_focus_handle.contains_focused(window, cx) {
+        //     "Focus Content"
+        // } else {
+        //     "Focus Navbar"
+        // };
 
         v_flex()
             .w_64()
             .p_2p5()
             .pt_10()
-            .gap_3()
             .flex_none()
             .border_r_1()
+            .key_context("NavigationMenu")
+            .on_action(cx.listener(|this, _: &CollapseNavEntry, window, cx| {
+                let Some(focused_entry) = this.focused_nav_entry(window) else {
+                    return;
+                };
+                let focused_entry_parent = this.root_entry_containing(focused_entry);
+                if this.navbar_entries[focused_entry_parent].expanded {
+                    this.toggle_navbar_entry(focused_entry_parent);
+                    window.focus(&this.navbar_entries[focused_entry_parent].focus_handle);
+                }
+                cx.notify();
+            }))
+            .on_action(cx.listener(|this, _: &ExpandNavEntry, window, cx| {
+                let Some(focused_entry) = this.focused_nav_entry(window) else {
+                    return;
+                };
+                if !this.navbar_entries[focused_entry].is_root {
+                    return;
+                }
+                if !this.navbar_entries[focused_entry].expanded {
+                    this.toggle_navbar_entry(focused_entry);
+                }
+                cx.notify();
+            }))
+            .on_action(
+                cx.listener(|this, _: &FocusPreviousRootNavEntry, window, _| {
+                    let entry_index = this.focused_nav_entry(window).unwrap_or(this.navbar_entry);
+                    let mut root_index = None;
+                    for (index, entry) in this.visible_navbar_entries() {
+                        if index >= entry_index {
+                            break;
+                        }
+                        if entry.is_root {
+                            root_index = Some(index);
+                        }
+                    }
+                    let Some(previous_root_index) = root_index else {
+                        return;
+                    };
+                    this.focus_and_scroll_to_nav_entry(previous_root_index, window);
+                }),
+            )
+            .on_action(cx.listener(|this, _: &FocusNextRootNavEntry, window, _| {
+                let entry_index = this.focused_nav_entry(window).unwrap_or(this.navbar_entry);
+                let mut root_index = None;
+                for (index, entry) in this.visible_navbar_entries() {
+                    if index <= entry_index {
+                        continue;
+                    }
+                    if entry.is_root {
+                        root_index = Some(index);
+                        break;
+                    }
+                }
+                let Some(next_root_index) = root_index else {
+                    return;
+                };
+                this.focus_and_scroll_to_nav_entry(next_root_index, window);
+            }))
+            .on_action(cx.listener(|this, _: &FocusFirstNavEntry, window, _| {
+                if let Some((first_entry_index, _)) = this.visible_navbar_entries().next() {
+                    this.focus_and_scroll_to_nav_entry(first_entry_index, window);
+                }
+            }))
+            .on_action(cx.listener(|this, _: &FocusLastNavEntry, window, _| {
+                if let Some((last_entry_index, _)) = this.visible_navbar_entries().last() {
+                    this.focus_and_scroll_to_nav_entry(last_entry_index, window);
+                }
+            }))
             .border_color(cx.theme().colors().border)
-            .bg(nav_background)
+            .bg(cx.theme().colors().panel_background)
             .child(self.render_search(window, cx))
             .child(
                 v_flex()
-                    .flex_grow()
-                    .track_focus(&self.navbar_focus_handle)
+                    .size_full()
+                    .track_focus(&self.navbar_focus_handle.focus_handle(cx))
                     .tab_group()
                     .tab_index(NAVBAR_GROUP_TAB_INDEX)
                     .child(

crates/storybook/src/storybook.rs 🔗

@@ -19,7 +19,7 @@ use reqwest_client::ReqwestClient;
 use settings::{KeymapFile, Settings};
 use simplelog::SimpleLogger;
 use strum::IntoEnumIterator;
-use theme::{ThemeRegistry, ThemeSettings};
+use theme::ThemeSettings;
 use ui::prelude::*;
 use workspace;
 
@@ -80,9 +80,9 @@ fn main() {
 
         let selector = story_selector;
 
-        let theme_registry = ThemeRegistry::global(cx);
         let mut theme_settings = ThemeSettings::get_global(cx).clone();
-        theme_settings.active_theme = theme_registry.get(&theme_name).unwrap();
+        theme_settings.theme =
+            theme::ThemeSelection::Static(settings::ThemeName(theme_name.into()));
         ThemeSettings::override_global(theme_settings, cx);
 
         language::init(cx);

crates/task/src/shell_builder.rs 🔗

@@ -49,14 +49,15 @@ impl ShellBuilder {
                     format!("{} -C '{}'", self.program, command_to_use_in_label)
                 }
                 ShellKind::Cmd => {
-                    format!("{} /C '{}'", self.program, command_to_use_in_label)
+                    format!("{} /C \"{}\"", self.program, command_to_use_in_label)
                 }
                 ShellKind::Posix
                 | ShellKind::Nushell
                 | ShellKind::Fish
                 | ShellKind::Csh
                 | ShellKind::Tcsh
-                | ShellKind::Rc => {
+                | ShellKind::Rc
+                | ShellKind::Xonsh => {
                     let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
                     format!(
                         "{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
@@ -91,7 +92,8 @@ impl ShellBuilder {
                     | ShellKind::Fish
                     | ShellKind::Csh
                     | ShellKind::Tcsh
-                    | ShellKind::Rc => {
+                    | ShellKind::Rc
+                    | ShellKind::Xonsh => {
                         combined_command.insert(0, '(');
                         combined_command.push_str(") </dev/null");
                     }

crates/task/src/task.rs 🔗

@@ -345,6 +345,7 @@ impl Shell {
             Shell::System => get_system_shell(),
         }
     }
+
     pub fn program_and_args(&self) -> (String, &[String]) {
         match self {
             Shell::Program(program) => (program.clone(), &[]),
@@ -352,6 +353,14 @@ impl Shell {
             Shell::System => (get_system_shell(), &[]),
         }
     }
+
+    pub fn shell_kind(&self) -> ShellKind {
+        match self {
+            Shell::Program(program) => ShellKind::new(program),
+            Shell::WithArguments { program, .. } => ShellKind::new(program),
+            Shell::System => ShellKind::system(),
+        }
+    }
 }
 
 type VsCodeEnvVariable = String;

crates/terminal/src/terminal.rs 🔗

@@ -409,6 +409,7 @@ impl TerminalBuilder {
             events_rx,
         })
     }
+
     pub fn new(
         working_directory: Option<PathBuf>,
         task: Option<TaskState>,
@@ -494,6 +495,8 @@ impl TerminalBuilder {
                 .unwrap_or(params.program.clone())
         });
 
+        let shell_kind = shell.shell_kind();
+
         let pty_options = {
             let alac_shell = shell_params.as_ref().map(|params| {
                 alacritty_terminal::tty::Shell::new(
@@ -507,8 +510,10 @@ impl TerminalBuilder {
                 working_directory: working_directory.clone(),
                 drain_on_exit: true,
                 env: env.clone().into_iter().collect(),
+                // We do not want to escape arguments if we are using CMD as our shell.
+                // If we do we end up with too many quotes/escaped quotes for CMD to handle.
                 #[cfg(windows)]
-                escape_args: true,
+                escape_args: shell_kind != util::shell::ShellKind::Cmd,
             }
         };
 
@@ -578,7 +583,7 @@ impl TerminalBuilder {
 
         let no_task = task.is_none();
 
-        let mut terminal = Terminal {
+        let terminal = Terminal {
             task,
             terminal_type: TerminalType::Pty {
                 pty_tx: Notifier(pty_tx),
@@ -618,14 +623,23 @@ impl TerminalBuilder {
 
         if !activation_script.is_empty() && no_task {
             for activation_script in activation_script {
-                terminal.input(activation_script.into_bytes());
-                terminal.write_to_pty(if cfg!(windows) {
-                    b"\r\n" as &[_]
-                } else {
-                    b"\n"
-                });
-            }
-            terminal.clear();
+                terminal.write_to_pty(activation_script.into_bytes());
+                // Simulate enter key press
+                // NOTE(PowerShell): using `\r\n` will put PowerShell in a continuation mode (infamous >> character)
+                // and generally mess up the rendering.
+                terminal.write_to_pty(b"\x0d");
+            }
+            // In order to clear the screen at this point, we have two options:
+            // 1. We can send a shell-specific command such as "clear" or "cls"
+            // 2. We can "echo" a marker message that we will then catch when handling a Wakeup event
+            //    and clear the screen using `terminal.clear()` method
+            // We cannot issue a `terminal.clear()` command at this point as alacritty is evented
+            // and while we have sent the activation script to the pty, it will be executed asynchronously.
+            // Therefore, we somehow need to wait for the activation script to finish executing before we
+            // can proceed with clearing the screen.
+            terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes());
+            // Simulate enter key press
+            terminal.write_to_pty(b"\x0d");
         }
 
         Ok(TerminalBuilder {

crates/terminal/src/terminal_settings.rs 🔗

@@ -2,7 +2,7 @@ use alacritty_terminal::vte::ansi::{
     CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
 };
 use collections::HashMap;
-use gpui::{App, FontFallbacks, FontFeatures, FontWeight, Pixels, px};
+use gpui::{FontFallbacks, FontFeatures, FontWeight, Pixels, px};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
@@ -10,6 +10,7 @@ pub use settings::AlternateScroll;
 use settings::{
     CursorShapeContent, SettingsContent, ShowScrollbar, TerminalBlink, TerminalDockPosition,
     TerminalLineHeight, TerminalSettingsContent, VenvSettings, WorkingDirectory,
+    merge_from::MergeFrom,
 };
 use task::Shell;
 use theme::FontFamilyName;
@@ -72,14 +73,17 @@ fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell {
 }
 
 impl settings::Settings for TerminalSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
-        let content = content.terminal.clone().unwrap();
+    fn from_settings(content: &settings::SettingsContent) -> Self {
+        let user_content = content.terminal.clone().unwrap();
+        // Note: we allow a subset of "terminal" settings in the project files.
+        let mut project_content = user_content.project.clone();
+        project_content.merge_from_option(content.project.terminal.as_ref());
         TerminalSettings {
-            shell: settings_shell_to_task_shell(content.shell.unwrap()),
-            working_directory: content.working_directory.unwrap(),
-            font_size: content.font_size.map(px),
-            font_family: content.font_family,
-            font_fallbacks: content.font_fallbacks.map(|fallbacks| {
+            shell: settings_shell_to_task_shell(project_content.shell.unwrap()),
+            working_directory: project_content.working_directory.unwrap(),
+            font_size: user_content.font_size.map(px),
+            font_family: user_content.font_family,
+            font_fallbacks: user_content.font_fallbacks.map(|fallbacks| {
                 FontFallbacks::from_fonts(
                     fallbacks
                         .into_iter()
@@ -87,29 +91,29 @@ impl settings::Settings for TerminalSettings {
                         .collect(),
                 )
             }),
-            font_features: content.font_features,
-            font_weight: content.font_weight.map(FontWeight),
-            line_height: content.line_height.unwrap(),
-            env: content.env.unwrap(),
-            cursor_shape: content.cursor_shape.map(Into::into).unwrap_or_default(),
-            blinking: content.blinking.unwrap(),
-            alternate_scroll: content.alternate_scroll.unwrap(),
-            option_as_meta: content.option_as_meta.unwrap(),
-            copy_on_select: content.copy_on_select.unwrap(),
-            keep_selection_on_copy: content.keep_selection_on_copy.unwrap(),
-            button: content.button.unwrap(),
-            dock: content.dock.unwrap(),
-            default_width: px(content.default_width.unwrap()),
-            default_height: px(content.default_height.unwrap()),
-            detect_venv: content.detect_venv.unwrap(),
-            max_scroll_history_lines: content.max_scroll_history_lines,
+            font_features: user_content.font_features,
+            font_weight: user_content.font_weight.map(FontWeight),
+            line_height: user_content.line_height.unwrap(),
+            env: project_content.env.unwrap(),
+            cursor_shape: user_content.cursor_shape.unwrap().into(),
+            blinking: user_content.blinking.unwrap(),
+            alternate_scroll: user_content.alternate_scroll.unwrap(),
+            option_as_meta: user_content.option_as_meta.unwrap(),
+            copy_on_select: user_content.copy_on_select.unwrap(),
+            keep_selection_on_copy: user_content.keep_selection_on_copy.unwrap(),
+            button: user_content.button.unwrap(),
+            dock: user_content.dock.unwrap(),
+            default_width: px(user_content.default_width.unwrap()),
+            default_height: px(user_content.default_height.unwrap()),
+            detect_venv: project_content.detect_venv.unwrap(),
+            max_scroll_history_lines: user_content.max_scroll_history_lines,
             toolbar: Toolbar {
-                breadcrumbs: content.toolbar.unwrap().breadcrumbs.unwrap(),
+                breadcrumbs: user_content.toolbar.unwrap().breadcrumbs.unwrap(),
             },
             scrollbar: ScrollbarSettings {
-                show: content.scrollbar.unwrap().show,
+                show: user_content.scrollbar.unwrap().show,
             },
-            minimum_contrast: content.minimum_contrast.unwrap(),
+            minimum_contrast: user_content.minimum_contrast.unwrap(),
         }
     }
 
@@ -119,9 +123,11 @@ impl settings::Settings for TerminalSettings {
         let name = |s| format!("terminal.integrated.{s}");
 
         vscode.f32_setting(&name("fontSize"), &mut current.font_size);
-        if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
-            current.font_family = Some(FontFamilyName(font_family.into()));
-        }
+        vscode.font_family_setting(
+            &name("fontFamily"),
+            &mut current.font_family,
+            &mut current.font_fallbacks,
+        );
         vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
         vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
         vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
@@ -160,7 +166,7 @@ impl settings::Settings for TerminalSettings {
         // TODO: handle arguments
         let shell_name = format!("{platform}Exec");
         if let Some(s) = vscode.read_string(&name(&shell_name)) {
-            current.shell = Some(settings::Shell::Program(s.to_owned()))
+            current.project.shell = Some(settings::Shell::Program(s.to_owned()))
         }
 
         if let Some(env) = vscode
@@ -169,15 +175,15 @@ impl settings::Settings for TerminalSettings {
         {
             for (k, v) in env {
                 if v.is_null()
-                    && let Some(zed_env) = current.env.as_mut()
+                    && let Some(zed_env) = current.project.env.as_mut()
                 {
                     zed_env.remove(k);
                 }
                 let Some(v) = v.as_str() else { continue };
-                if let Some(zed_env) = current.env.as_mut() {
+                if let Some(zed_env) = current.project.env.as_mut() {
                     zed_env.insert(k.clone(), v.to_owned());
                 } else {
-                    current.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
+                    current.project.env = Some([(k.clone(), v.to_owned())].into_iter().collect())
                 }
             }
         }

crates/theme/src/fallback_themes.rs 🔗

@@ -3,9 +3,9 @@ use std::sync::Arc;
 use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance, hsla};
 
 use crate::{
-    AccentColors, Appearance, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme,
-    SystemColors, Theme, ThemeColors, ThemeColorsRefinement, ThemeFamily, ThemeStyles,
-    default_color_scales,
+    AccentColors, Appearance, DEFAULT_DARK_THEME, PlayerColors, StatusColors,
+    StatusColorsRefinement, SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeColorsRefinement,
+    ThemeFamily, ThemeStyles, default_color_scales,
 };
 
 /// The default theme family for Zed.
@@ -92,7 +92,7 @@ pub(crate) fn zed_default_dark() -> Theme {
     let player = PlayerColors::dark();
     Theme {
         id: "one_dark".to_string(),
-        name: "One Dark".into(),
+        name: DEFAULT_DARK_THEME.into(),
         appearance: Appearance::Dark,
         styles: ThemeStyles {
             window_background_appearance: WindowBackgroundAppearance::Opaque,

crates/theme/src/settings.rs 🔗

@@ -1,8 +1,6 @@
-use crate::fallback_themes::zed_default_dark;
 use crate::{
-    Appearance, DEFAULT_ICON_THEME_NAME, IconTheme, IconThemeNotFoundError, SyntaxTheme, Theme,
-    ThemeNotFoundError, ThemeRegistry, status_colors_refinement, syntax_overrides,
-    theme_colors_refinement,
+    Appearance, DEFAULT_ICON_THEME_NAME, SyntaxTheme, Theme, status_colors_refinement,
+    syntax_overrides, theme_colors_refinement,
 };
 use collections::HashMap;
 use derive_more::{Deref, DerefMut};
@@ -16,7 +14,6 @@ use serde::{Deserialize, Serialize};
 pub use settings::{FontFamilyName, IconThemeName, ThemeMode, ThemeName};
 use settings::{Settings, SettingsContent};
 use std::sync::Arc;
-use util::ResultExt as _;
 
 const MIN_FONT_SIZE: Pixels = px(6.0);
 const MAX_FONT_SIZE: Pixels = px(100.0);
@@ -116,7 +113,7 @@ pub struct ThemeSettings {
     pub buffer_font: Font,
     /// The agent font size. Determines the size of text in the agent panel. Falls back to the UI font size if unset.
     agent_ui_font_size: Option<Pixels>,
-    /// The agent buffer font size. Determines the size of user messages in the agent panel. Falls back to the buffer font size if unset.
+    /// The agent buffer font size. Determines the size of user messages in the agent panel.
     agent_buffer_font_size: Option<Pixels>,
     /// The line height for buffers, and the terminal.
     ///
@@ -125,9 +122,7 @@ pub struct ThemeSettings {
     /// The terminal font family can be overridden using it's own setting.
     pub buffer_line_height: BufferLineHeight,
     /// The current theme selection.
-    pub theme_selection: Option<ThemeSelection>,
-    /// The active theme.
-    pub active_theme: Arc<Theme>,
+    pub theme: ThemeSelection,
     /// Manual overrides for the active theme.
     ///
     /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)
@@ -135,9 +130,7 @@ pub struct ThemeSettings {
     /// Manual overrides per theme
     pub theme_overrides: HashMap<String, settings::ThemeStyleContent>,
     /// The current icon theme selection.
-    pub icon_theme_selection: Option<IconThemeSelection>,
-    /// The active icon theme.
-    pub active_icon_theme: Arc<IconTheme>,
+    pub icon_theme: IconThemeSelection,
     /// The density of the UI.
     /// Note: This setting is still experimental. See [this tracking issue](
     pub ui_density: UiDensity,
@@ -145,73 +138,14 @@ pub struct ThemeSettings {
     pub unnecessary_code_fade: f32,
 }
 
-impl ThemeSettings {
-    const DEFAULT_LIGHT_THEME: &'static str = "One Light";
-    const DEFAULT_DARK_THEME: &'static str = "One Dark";
-
-    /// Returns the name of the default theme for the given [`Appearance`].
-    pub fn default_theme(appearance: Appearance) -> &'static str {
-        match appearance {
-            Appearance::Light => Self::DEFAULT_LIGHT_THEME,
-            Appearance::Dark => Self::DEFAULT_DARK_THEME,
-        }
-    }
+pub(crate) const DEFAULT_LIGHT_THEME: &'static str = "One Light";
+pub(crate) const DEFAULT_DARK_THEME: &'static str = "One Dark";
 
-    /// Reloads the current theme.
-    ///
-    /// Reads the [`ThemeSettings`] to know which theme should be loaded,
-    /// taking into account the current [`SystemAppearance`].
-    pub fn reload_current_theme(cx: &mut App) {
-        let mut theme_settings = ThemeSettings::get_global(cx).clone();
-        let system_appearance = SystemAppearance::global(cx);
-
-        if let Some(theme_selection) = theme_settings.theme_selection.clone() {
-            let mut theme_name = theme_selection.theme(*system_appearance);
-
-            // If the selected theme doesn't exist, fall back to a default theme
-            // based on the system appearance.
-            let theme_registry = ThemeRegistry::global(cx);
-            if let Err(err @ ThemeNotFoundError(_)) = theme_registry.get(theme_name) {
-                if theme_registry.extensions_loaded() {
-                    log::error!("{err}");
-                }
-
-                theme_name = Self::default_theme(*system_appearance);
-            };
-
-            if let Some(_theme) = theme_settings.switch_theme(theme_name, cx) {
-                ThemeSettings::override_global(theme_settings, cx);
-            }
-        }
-    }
-
-    /// Reloads the current icon theme.
-    ///
-    /// Reads the [`ThemeSettings`] to know which icon theme should be loaded,
-    /// taking into account the current [`SystemAppearance`].
-    pub fn reload_current_icon_theme(cx: &mut App) {
-        let mut theme_settings = ThemeSettings::get_global(cx).clone();
-        let system_appearance = SystemAppearance::global(cx);
-
-        if let Some(icon_theme_selection) = theme_settings.icon_theme_selection.clone() {
-            let mut icon_theme_name = icon_theme_selection.icon_theme(*system_appearance);
-
-            // If the selected icon theme doesn't exist, fall back to the default theme.
-            let theme_registry = ThemeRegistry::global(cx);
-            if let Err(err @ IconThemeNotFoundError(_)) =
-                theme_registry.get_icon_theme(icon_theme_name)
-            {
-                if theme_registry.extensions_loaded() {
-                    log::error!("{err}");
-                }
-
-                icon_theme_name = DEFAULT_ICON_THEME_NAME;
-            };
-
-            if let Some(_theme) = theme_settings.switch_icon_theme(icon_theme_name, cx) {
-                ThemeSettings::override_global(theme_settings, cx);
-            }
-        }
+/// Returns the name of the default theme for the given [`Appearance`].
+pub fn default_theme(appearance: Appearance) -> &'static str {
+    match appearance {
+        Appearance::Light => DEFAULT_LIGHT_THEME,
+        Appearance::Dark => DEFAULT_DARK_THEME,
     }
 }
 
@@ -237,13 +171,6 @@ impl SystemAppearance {
             GlobalSystemAppearance(SystemAppearance(cx.window_appearance().into()));
     }
 
-    /// Returns the global [`SystemAppearance`].
-    ///
-    /// Inserts a default [`SystemAppearance`] if one does not yet exist.
-    pub(crate) fn default_global(cx: &mut App) -> Self {
-        cx.default_global::<GlobalSystemAppearance>().0
-    }
-
     /// Returns the global [`SystemAppearance`].
     pub fn global(cx: &App) -> Self {
         cx.global::<GlobalSystemAppearance>().0
@@ -302,15 +229,15 @@ impl From<settings::ThemeSelection> for ThemeSelection {
 
 impl ThemeSelection {
     /// Returns the theme name for the selected [ThemeMode].
-    pub fn theme(&self, system_appearance: Appearance) -> &str {
+    pub fn name(&self, system_appearance: Appearance) -> ThemeName {
         match self {
-            Self::Static(theme) => &theme.0,
+            Self::Static(theme) => theme.clone(),
             Self::Dynamic { mode, light, dark } => match mode {
-                ThemeMode::Light => &light.0,
-                ThemeMode::Dark => &dark.0,
+                ThemeMode::Light => light.clone(),
+                ThemeMode::Dark => dark.clone(),
                 ThemeMode::System => match system_appearance {
-                    Appearance::Light => &light.0,
-                    Appearance::Dark => &dark.0,
+                    Appearance::Light => light.clone(),
+                    Appearance::Dark => dark.clone(),
                 },
             },
         }
@@ -354,15 +281,15 @@ impl From<settings::IconThemeSelection> for IconThemeSelection {
 
 impl IconThemeSelection {
     /// Returns the icon theme name based on the given [`Appearance`].
-    pub fn icon_theme(&self, system_appearance: Appearance) -> &str {
+    pub fn name(&self, system_appearance: Appearance) -> IconThemeName {
         match self {
-            Self::Static(theme) => &theme.0,
+            Self::Static(theme) => theme.clone(),
             Self::Dynamic { mode, light, dark } => match mode {
-                ThemeMode::Light => &light.0,
-                ThemeMode::Dark => &dark.0,
+                ThemeMode::Light => light.clone(),
+                ThemeMode::Dark => dark.clone(),
                 ThemeMode::System => match system_appearance {
-                    Appearance::Light => &light.0,
-                    Appearance::Dark => &dark.0,
+                    Appearance::Light => light.clone(),
+                    Appearance::Dark => dark.clone(),
                 },
             },
         }
@@ -408,7 +335,7 @@ pub fn set_theme(
 /// Sets the icon theme for the given appearance to the icon theme with the specified name.
 pub fn set_icon_theme(
     current: &mut SettingsContent,
-    icon_theme_name: String,
+    icon_theme_name: IconThemeName,
     appearance: Appearance,
 ) {
     if let Some(selection) = current.theme.icon_theme.as_mut() {
@@ -424,11 +351,9 @@ pub fn set_icon_theme(
             },
         };
 
-        *icon_theme_to_update = IconThemeName(icon_theme_name.into());
+        *icon_theme_to_update = icon_theme_name;
     } else {
-        current.theme.icon_theme = Some(settings::IconThemeSelection::Static(IconThemeName(
-            icon_theme_name.into(),
-        )));
+        current.theme.icon_theme = Some(settings::IconThemeSelection::Static(icon_theme_name));
     }
 }
 
@@ -456,8 +381,8 @@ pub fn set_mode(content: &mut SettingsContent, mode: ThemeMode) {
     } else {
         theme.theme = Some(settings::ThemeSelection::Dynamic {
             mode,
-            light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()),
-            dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()),
+            light: ThemeName(DEFAULT_LIGHT_THEME.into()),
+            dark: ThemeName(DEFAULT_DARK_THEME.into()),
         });
     }
 
@@ -549,7 +474,7 @@ impl ThemeSettings {
             .unwrap_or_else(|| self.ui_font_size(cx))
     }
 
-    /// Returns the agent panel buffer font size. Falls back to the buffer font size if unset.
+    /// Returns the agent panel buffer font size.
     pub fn agent_buffer_font_size(&self, cx: &App) -> Pixels {
         cx.try_global::<AgentFontSize>()
             .map(|size| size.0)
@@ -596,44 +521,22 @@ impl ThemeSettings {
         f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
     }
 
-    /// Switches to the theme with the given name, if it exists.
-    ///
-    /// Returns a `Some` containing the new theme if it was successful.
-    /// Returns `None` otherwise.
-    pub fn switch_theme(&mut self, theme: &str, cx: &mut App) -> Option<Arc<Theme>> {
-        let themes = ThemeRegistry::default_global(cx);
-
-        let mut new_theme = None;
-
-        match themes.get(theme) {
-            Ok(theme) => {
-                self.active_theme = theme.clone();
-                new_theme = Some(theme);
-            }
-            Err(err @ ThemeNotFoundError(_)) => {
-                log::error!("{err}");
-            }
-        }
-
-        self.apply_theme_overrides();
-
-        new_theme
-    }
-
     /// Applies the theme overrides, if there are any, to the current theme.
-    pub fn apply_theme_overrides(&mut self) {
+    pub fn apply_theme_overrides(&self, mut arc_theme: Arc<Theme>) -> Arc<Theme> {
         // Apply the old overrides setting first, so that the new setting can override those.
         if let Some(experimental_theme_overrides) = &self.experimental_theme_overrides {
-            let mut theme = (*self.active_theme).clone();
+            let mut theme = (*arc_theme).clone();
             ThemeSettings::modify_theme(&mut theme, experimental_theme_overrides);
-            self.active_theme = Arc::new(theme);
+            arc_theme = Arc::new(theme);
         }
 
-        if let Some(theme_overrides) = self.theme_overrides.get(self.active_theme.name.as_ref()) {
-            let mut theme = (*self.active_theme).clone();
+        if let Some(theme_overrides) = self.theme_overrides.get(arc_theme.name.as_ref()) {
+            let mut theme = (*arc_theme).clone();
             ThemeSettings::modify_theme(&mut theme, theme_overrides);
-            self.active_theme = Arc::new(theme);
+            arc_theme = Arc::new(theme);
         }
+
+        arc_theme
     }
 
     fn modify_theme(base_theme: &mut Theme, theme_overrides: &settings::ThemeStyleContent) {
@@ -654,24 +557,6 @@ impl ThemeSettings {
             syntax_overrides(&theme_overrides),
         );
     }
-
-    /// Switches to the icon theme with the given name, if it exists.
-    ///
-    /// Returns a `Some` containing the new icon theme if it was successful.
-    /// Returns `None` otherwise.
-    pub fn switch_icon_theme(&mut self, icon_theme: &str, cx: &mut App) -> Option<Arc<IconTheme>> {
-        let themes = ThemeRegistry::default_global(cx);
-
-        let mut new_icon_theme = None;
-
-        if let Some(icon_theme) = themes.get_icon_theme(icon_theme).log_err() {
-            self.active_icon_theme = icon_theme.clone();
-            new_icon_theme = Some(icon_theme);
-            cx.refresh_windows();
-        }
-
-        new_icon_theme
-    }
 }
 
 /// Observe changes to the adjusted buffer font size.
@@ -804,14 +689,11 @@ pub fn font_fallbacks_from_settings(
 }
 
 impl settings::Settings for ThemeSettings {
-    fn from_settings(content: &settings::SettingsContent, cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let content = &content.theme;
-        // todo(settings_refactor). This should *not* require cx...
-        let themes = ThemeRegistry::default_global(cx);
-        let system_appearance = SystemAppearance::default_global(cx);
         let theme_selection: ThemeSelection = content.theme.clone().unwrap().into();
         let icon_theme_selection: IconThemeSelection = content.icon_theme.clone().unwrap().into();
-        let mut this = Self {
+        Self {
             ui_font_size: clamp_font_size(content.ui_font_size.unwrap().into()),
             ui_font: Font {
                 family: content.ui_font_family.as_ref().unwrap().0.clone().into(),
@@ -837,31 +719,23 @@ impl settings::Settings for ThemeSettings {
             buffer_line_height: content.buffer_line_height.unwrap().into(),
             agent_ui_font_size: content.agent_ui_font_size.map(Into::into),
             agent_buffer_font_size: content.agent_buffer_font_size.map(Into::into),
-            active_theme: themes
-                .get(theme_selection.theme(*system_appearance))
-                .or(themes.get(&zed_default_dark().name))
-                .unwrap(),
-            theme_selection: Some(theme_selection),
+            theme: theme_selection,
             experimental_theme_overrides: content.experimental_theme_overrides.clone(),
             theme_overrides: content.theme_overrides.clone(),
-            active_icon_theme: themes
-                .get_icon_theme(icon_theme_selection.icon_theme(*system_appearance))
-                .or_else(|_| themes.default_icon_theme())
-                .unwrap(),
-            icon_theme_selection: Some(icon_theme_selection),
+            icon_theme: icon_theme_selection,
             ui_density: content.ui_density.unwrap_or_default().into(),
             unnecessary_code_fade: content.unnecessary_code_fade.unwrap().0.clamp(0.0, 0.9),
-        };
-        this.apply_theme_overrides();
-        this
+        }
     }
 
     fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
         vscode.from_f32_setting("editor.fontWeight", &mut current.theme.buffer_font_weight);
         vscode.from_f32_setting("editor.fontSize", &mut current.theme.buffer_font_size);
-        if let Some(font) = vscode.read_string("editor.font") {
-            current.theme.buffer_font_family = Some(FontFamilyName(font.into()));
-        }
+        vscode.font_family_setting(
+            "editor.fontFamily",
+            &mut current.theme.buffer_font_family,
+            &mut current.theme.buffer_font_fallbacks,
+        )
         // TODO: possibly map editor.fontLigatures to buffer_font_features?
     }
 }

crates/theme/src/theme.rs 🔗

@@ -27,6 +27,8 @@ use ::settings::SettingsStore;
 use anyhow::Result;
 use fallback_themes::apply_status_color_defaults;
 use fs::Fs;
+use gpui::BorrowAppContext;
+use gpui::Global;
 use gpui::{
     App, AssetSource, HighlightStyle, Hsla, Pixels, Refineable, SharedString, WindowAppearance,
     WindowBackgroundAppearance, px,
@@ -95,6 +97,7 @@ pub enum LoadThemes {
 
 /// Initialize the theme system.
 pub fn init(themes_to_load: LoadThemes, cx: &mut App) {
+    SystemAppearance::init(cx);
     let (assets, load_user_themes) = match themes_to_load {
         LoadThemes::JustBase => (Box::new(()) as Box<dyn AssetSource>, false),
         LoadThemes::All(assets) => (assets, true),
@@ -108,40 +111,67 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut App) {
     ThemeSettings::register(cx);
     FontFamilyCache::init_global(cx);
 
-    let mut prev_buffer_font_size_settings =
-        ThemeSettings::get_global(cx).buffer_font_size_settings();
-    let mut prev_ui_font_size_settings = ThemeSettings::get_global(cx).ui_font_size_settings();
-    let mut prev_agent_ui_font_size_settings =
-        ThemeSettings::get_global(cx).agent_ui_font_size_settings();
-    let mut prev_agent_buffer_font_size_settings =
-        ThemeSettings::get_global(cx).agent_buffer_font_size_settings();
+    let theme = GlobalTheme::configured_theme(cx);
+    let icon_theme = GlobalTheme::configured_icon_theme(cx);
+    cx.set_global(GlobalTheme { theme, icon_theme });
+
+    let settings = ThemeSettings::get_global(cx);
+
+    let mut prev_buffer_font_size_settings = settings.buffer_font_size_settings();
+    let mut prev_ui_font_size_settings = settings.ui_font_size_settings();
+    let mut prev_agent_ui_font_size_settings = settings.agent_ui_font_size_settings();
+    let mut prev_agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
+    let mut prev_theme_name = settings.theme.name(SystemAppearance::global(cx).0);
+    let mut prev_icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
+    let mut prev_theme_overrides = (
+        settings.experimental_theme_overrides.clone(),
+        settings.theme_overrides.clone(),
+    );
 
     cx.observe_global::<SettingsStore>(move |cx| {
-        let buffer_font_size_settings = ThemeSettings::get_global(cx).buffer_font_size_settings();
+        let settings = ThemeSettings::get_global(cx);
+
+        let buffer_font_size_settings = settings.buffer_font_size_settings();
+        let ui_font_size_settings = settings.ui_font_size_settings();
+        let agent_ui_font_size_settings = settings.agent_ui_font_size_settings();
+        let agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings();
+        let theme_name = settings.theme.name(SystemAppearance::global(cx).0);
+        let icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0);
+        let theme_overrides = (
+            settings.experimental_theme_overrides.clone(),
+            settings.theme_overrides.clone(),
+        );
+
         if buffer_font_size_settings != prev_buffer_font_size_settings {
             prev_buffer_font_size_settings = buffer_font_size_settings;
             reset_buffer_font_size(cx);
         }
 
-        let ui_font_size_settings = ThemeSettings::get_global(cx).ui_font_size_settings();
         if ui_font_size_settings != prev_ui_font_size_settings {
             prev_ui_font_size_settings = ui_font_size_settings;
             reset_ui_font_size(cx);
         }
 
-        let agent_ui_font_size_settings =
-            ThemeSettings::get_global(cx).agent_ui_font_size_settings();
         if agent_ui_font_size_settings != prev_agent_ui_font_size_settings {
             prev_agent_ui_font_size_settings = agent_ui_font_size_settings;
             reset_agent_ui_font_size(cx);
         }
 
-        let agent_buffer_font_size_settings =
-            ThemeSettings::get_global(cx).agent_buffer_font_size_settings();
         if agent_buffer_font_size_settings != prev_agent_buffer_font_size_settings {
             prev_agent_buffer_font_size_settings = agent_buffer_font_size_settings;
             reset_agent_buffer_font_size(cx);
         }
+
+        if theme_name != prev_theme_name || theme_overrides != prev_theme_overrides {
+            prev_theme_name = theme_name;
+            prev_theme_overrides = theme_overrides;
+            GlobalTheme::reload_theme(cx);
+        }
+
+        if icon_theme_name != prev_icon_theme_name {
+            prev_icon_theme_name = icon_theme_name;
+            GlobalTheme::reload_icon_theme(cx);
+        }
     })
     .detach();
 }
@@ -154,7 +184,7 @@ pub trait ActiveTheme {
 
 impl ActiveTheme for App {
     fn theme(&self) -> &Arc<Theme> {
-        &ThemeSettings::get_global(self).active_theme
+        GlobalTheme::theme(self)
     }
 }
 
@@ -408,3 +438,82 @@ pub async fn read_icon_theme(
 
     Ok(icon_theme_family)
 }
+
+/// The active theme
+pub struct GlobalTheme {
+    theme: Arc<Theme>,
+    icon_theme: Arc<IconTheme>,
+}
+impl Global for GlobalTheme {}
+
+impl GlobalTheme {
+    fn configured_theme(cx: &mut App) -> Arc<Theme> {
+        let themes = ThemeRegistry::default_global(cx);
+        let theme_settings = ThemeSettings::get_global(cx);
+        let system_appearance = SystemAppearance::global(cx);
+
+        let theme_name = theme_settings.theme.name(*system_appearance);
+
+        let theme = match themes.get(&theme_name.0) {
+            Ok(theme) => theme,
+            Err(err) => {
+                if themes.extensions_loaded() {
+                    log::error!("{err}");
+                }
+                themes
+                    .get(default_theme(*system_appearance))
+                    // fallback for tests.
+                    .unwrap_or_else(|_| themes.get(DEFAULT_DARK_THEME).unwrap())
+            }
+        };
+        theme_settings.apply_theme_overrides(theme)
+    }
+
+    /// Reloads the current theme.
+    ///
+    /// Reads the [`ThemeSettings`] to know which theme should be loaded,
+    /// taking into account the current [`SystemAppearance`].
+    pub fn reload_theme(cx: &mut App) {
+        let theme = Self::configured_theme(cx);
+        cx.update_global::<Self, _>(|this, _| this.theme = theme);
+        cx.refresh_windows();
+    }
+
+    fn configured_icon_theme(cx: &mut App) -> Arc<IconTheme> {
+        let themes = ThemeRegistry::default_global(cx);
+        let theme_settings = ThemeSettings::get_global(cx);
+        let system_appearance = SystemAppearance::global(cx);
+
+        let icon_theme_name = theme_settings.icon_theme.name(*system_appearance);
+
+        match themes.get_icon_theme(&icon_theme_name.0) {
+            Ok(theme) => theme,
+            Err(err) => {
+                if themes.extensions_loaded() {
+                    log::error!("{err}");
+                }
+                themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()
+            }
+        }
+    }
+
+    /// Reloads the current icon theme.
+    ///
+    /// Reads the [`ThemeSettings`] to know which icon theme should be loaded,
+    /// taking into account the current [`SystemAppearance`].
+    pub fn reload_icon_theme(cx: &mut App) {
+        let icon_theme = Self::configured_icon_theme(cx);
+        cx.update_global::<Self, _>(|this, _| this.icon_theme = icon_theme);
+        cx.refresh_windows();
+    }
+
+    /// the active theme
+    pub fn theme(cx: &App) -> &Arc<Theme> {
+        &cx.global::<Self>().theme
+    }
+
+    /// the active icon theme
+    pub fn icon_theme(cx: &App) -> &Arc<IconTheme> {
+        &cx.global::<Self>().icon_theme
+    }
+}

crates/theme_extension/src/theme_extension.rs 🔗

@@ -5,7 +5,7 @@ use anyhow::Result;
 use extension::{ExtensionHostProxy, ExtensionThemeProxy};
 use fs::Fs;
 use gpui::{App, BackgroundExecutor, SharedString, Task};
-use theme::{ThemeRegistry, ThemeSettings};
+use theme::{GlobalTheme, ThemeRegistry};
 
 pub fn init(
     extension_host_proxy: Arc<ExtensionHostProxy>,
@@ -46,7 +46,7 @@ impl ExtensionThemeProxy for ThemeRegistryProxy {
     }
 
     fn reload_current_theme(&self, cx: &mut App) {
-        ThemeSettings::reload_current_theme(cx)
+        GlobalTheme::reload_theme(cx)
     }
 
     fn list_icon_theme_names(
@@ -83,6 +83,6 @@ impl ExtensionThemeProxy for ThemeRegistryProxy {
     }
 
     fn reload_current_icon_theme(&self, cx: &mut App) {
-        ThemeSettings::reload_current_icon_theme(cx)
+        GlobalTheme::reload_icon_theme(cx)
     }
 }

crates/theme_selector/src/icon_theme_selector.rs 🔗

@@ -7,7 +7,10 @@ use gpui::{
 use picker::{Picker, PickerDelegate};
 use settings::{Settings as _, SettingsStore, update_settings_file};
 use std::sync::Arc;
-use theme::{Appearance, IconTheme, ThemeMeta, ThemeRegistry, ThemeSettings};
+use theme::{
+    Appearance, IconThemeName, IconThemeSelection, SystemAppearance, ThemeMeta, ThemeRegistry,
+    ThemeSettings,
+};
 use ui::{ListItem, ListItemSpacing, prelude::*, v_flex};
 use util::ResultExt;
 use workspace::{ModalView, ui::HighlightedLabel};
@@ -51,9 +54,9 @@ pub(crate) struct IconThemeSelectorDelegate {
     fs: Arc<dyn Fs>,
     themes: Vec<ThemeMeta>,
     matches: Vec<StringMatch>,
-    original_theme: Arc<IconTheme>,
+    original_theme: IconThemeName,
     selection_completed: bool,
-    selected_theme: Option<Arc<IconTheme>>,
+    selected_theme: Option<IconThemeName>,
     selected_index: usize,
     selector: WeakEntity<IconThemeSelector>,
 }
@@ -66,7 +69,9 @@ impl IconThemeSelectorDelegate {
         cx: &mut Context<IconThemeSelector>,
     ) -> Self {
         let theme_settings = ThemeSettings::get_global(cx);
-        let original_theme = theme_settings.active_icon_theme.clone();
+        let original_theme = theme_settings
+            .icon_theme
+            .name(SystemAppearance::global(cx).0);
 
         let registry = ThemeRegistry::global(cx);
         let mut themes = registry
@@ -107,29 +112,18 @@ impl IconThemeSelectorDelegate {
             selector,
         };
 
-        this.select_if_matching(&original_theme.name);
+        this.select_if_matching(&original_theme.0);
         this
     }
 
     fn show_selected_theme(
         &mut self,
         cx: &mut Context<Picker<IconThemeSelectorDelegate>>,
-    ) -> Option<Arc<IconTheme>> {
-        if let Some(mat) = self.matches.get(self.selected_index) {
-            let registry = ThemeRegistry::global(cx);
-            match registry.get_icon_theme(&mat.string) {
-                Ok(theme) => {
-                    Self::set_icon_theme(theme.clone(), cx);
-                    Some(theme)
-                }
-                Err(err) => {
-                    log::error!("error loading icon theme {}: {err}", mat.string);
-                    None
-                }
-            }
-        } else {
-            None
-        }
+    ) -> Option<IconThemeName> {
+        let mat = self.matches.get(self.selected_index)?;
+        let name = IconThemeName(mat.string.clone().into());
+        Self::set_icon_theme(name.clone(), cx);
+        Some(name)
     }
 
     fn select_if_matching(&mut self, theme_name: &str) {
@@ -140,12 +134,11 @@ impl IconThemeSelectorDelegate {
             .unwrap_or(self.selected_index);
     }
 
-    fn set_icon_theme(theme: Arc<IconTheme>, cx: &mut App) {
-        SettingsStore::update_global(cx, |store, cx| {
+    fn set_icon_theme(name: IconThemeName, cx: &mut App) {
+        SettingsStore::update_global(cx, |store, _| {
             let mut theme_settings = store.get::<ThemeSettings>(None).clone();
-            theme_settings.active_icon_theme = theme;
+            theme_settings.icon_theme = IconThemeSelection::Static(name);
             store.override_global(theme_settings);
-            cx.refresh_windows();
         });
     }
 }
@@ -170,7 +163,9 @@ impl PickerDelegate for IconThemeSelectorDelegate {
         self.selection_completed = true;
 
         let theme_settings = ThemeSettings::get_global(cx);
-        let theme_name = theme_settings.active_icon_theme.name.clone();
+        let theme_name = theme_settings
+            .icon_theme
+            .name(SystemAppearance::global(cx).0);
 
         telemetry::event!(
             "Settings Changed",
@@ -181,7 +176,7 @@ impl PickerDelegate for IconThemeSelectorDelegate {
         let appearance = Appearance::from(window.appearance());
 
         update_settings_file(self.fs.clone(), cx, move |settings, _| {
-            theme::set_icon_theme(settings, theme_name.to_string(), appearance);
+            theme::set_icon_theme(settings, theme_name, appearance);
         });
 
         self.selector
@@ -268,7 +263,7 @@ impl PickerDelegate for IconThemeSelectorDelegate {
                         .matches
                         .iter()
                         .enumerate()
-                        .find(|(_, mtch)| mtch.string == selected.name)
+                        .find(|(_, mtch)| mtch.string.as_str() == selected.0.as_ref())
                         .map(|(ix, _)| ix)
                         .unwrap_or_default();
                 } else {

crates/theme_selector/src/theme_selector.rs 🔗

@@ -203,12 +203,11 @@ impl ThemeSelectorDelegate {
     }
 
     fn set_theme(theme: Arc<Theme>, cx: &mut App) {
-        SettingsStore::update_global(cx, |store, cx| {
+        SettingsStore::update_global(cx, |store, _| {
             let mut theme_settings = store.get::<ThemeSettings>(None).clone();
-            theme_settings.active_theme = theme;
-            theme_settings.apply_theme_overrides();
+            let name = theme.as_ref().name.clone().into();
+            theme_settings.theme = theme::ThemeSelection::Static(theme::ThemeName(name));
             store.override_global(theme_settings);
-            cx.refresh_windows();
         });
     }
 }

crates/title_bar/src/title_bar_settings.rs 🔗

@@ -1,5 +1,4 @@
 use settings::{Settings, SettingsContent};
-use ui::App;
 
 #[derive(Copy, Clone, Debug)]
 pub struct TitleBarSettings {
@@ -13,7 +12,7 @@ pub struct TitleBarSettings {
 }
 
 impl Settings for TitleBarSettings {
-    fn from_settings(s: &SettingsContent, _: &mut App) -> Self {
+    fn from_settings(s: &SettingsContent) -> Self {
         let content = s.title_bar.clone().unwrap();
         TitleBarSettings {
             show_branch_icon: content.show_branch_icon.unwrap(),

crates/ui/src/components/button/button_like.rs 🔗

@@ -135,6 +135,12 @@ pub enum ButtonStyle {
     /// a fully transparent button.
     Outlined,
 
+    /// Transparent button that always has an outline.
+    OutlinedTransparent,
+
+    /// A more de-emphasized version of the outlined button.
+    OutlinedGhost,
+
     /// The default button style, used for most buttons. Has a transparent background,
     /// but has a background color to indicate states like hover and active.
     #[default]
@@ -146,11 +152,38 @@ pub enum ButtonStyle {
     Transparent,
 }
 
+/// Rounding for a button that may have straight edges.
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub(crate) enum ButtonLikeRounding {
-    All,
-    Left,
-    Right,
+pub(crate) struct ButtonLikeRounding {
+    /// Top-left corner rounding
+    pub top_left: bool,
+    /// Top-right corner rounding
+    pub top_right: bool,
+    /// Bottom-right corner rounding
+    pub bottom_right: bool,
+    /// Bottom-left corner rounding
+    pub bottom_left: bool,
+}
+
+impl ButtonLikeRounding {
+    pub const ALL: Self = Self {
+        top_left: true,
+        top_right: true,
+        bottom_right: true,
+        bottom_left: true,
+    };
+    pub const LEFT: Self = Self {
+        top_left: true,
+        top_right: false,
+        bottom_right: false,
+        bottom_left: true,
+    };
+    pub const RIGHT: Self = Self {
+        top_left: false,
+        top_right: true,
+        bottom_right: true,
+        bottom_left: false,
+    };
 }
 
 #[derive(Debug, Clone)]
@@ -195,6 +228,18 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
+                background: cx.theme().colors().ghost_element_background,
+                border_color: cx.theme().colors().border_variant,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle::OutlinedGhost => ButtonLikeStyles {
+                background: transparent_black(),
+                border_color: cx.theme().colors().border_variant,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_background,
                 border_color: transparent_black(),
@@ -240,6 +285,18 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
+                background: cx.theme().colors().ghost_element_hover,
+                border_color: cx.theme().colors().border,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle::OutlinedGhost => ButtonLikeStyles {
+                background: transparent_black(),
+                border_color: cx.theme().colors().border,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
             ButtonStyle::Subtle => ButtonLikeStyles {
                 background: cx.theme().colors().ghost_element_hover,
                 border_color: transparent_black(),
@@ -278,6 +335,18 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
+                background: cx.theme().colors().ghost_element_active,
+                border_color: cx.theme().colors().border_variant,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle::OutlinedGhost => ButtonLikeStyles {
+                background: transparent_black(),
+                border_color: cx.theme().colors().border_variant,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
             ButtonStyle::Transparent => ButtonLikeStyles {
                 background: transparent_black(),
                 border_color: transparent_black(),
@@ -311,6 +380,18 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
+                background: cx.theme().colors().ghost_element_background,
+                border_color: cx.theme().colors().border,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle::OutlinedGhost => ButtonLikeStyles {
+                background: transparent_black(),
+                border_color: cx.theme().colors().border,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
             ButtonStyle::Transparent => ButtonLikeStyles {
                 background: transparent_black(),
                 border_color: cx.theme().colors().border_focused,
@@ -347,6 +428,18 @@ impl ButtonStyle {
                 label_color: Color::Default.color(cx),
                 icon_color: Color::Default.color(cx),
             },
+            ButtonStyle::OutlinedTransparent => ButtonLikeStyles {
+                background: cx.theme().colors().ghost_element_disabled,
+                border_color: cx.theme().colors().border_disabled,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
+            ButtonStyle::OutlinedGhost => ButtonLikeStyles {
+                background: transparent_black(),
+                border_color: cx.theme().colors().border_disabled,
+                label_color: Color::Default.color(cx),
+                icon_color: Color::Default.color(cx),
+            },
             ButtonStyle::Transparent => ButtonLikeStyles {
                 background: transparent_black(),
                 border_color: transparent_black(),
@@ -422,7 +515,7 @@ impl ButtonLike {
             width: None,
             height: None,
             size: ButtonSize::Default,
-            rounding: Some(ButtonLikeRounding::All),
+            rounding: Some(ButtonLikeRounding::ALL),
             tooltip: None,
             hoverable_tooltip: None,
             children: SmallVec::new(),
@@ -436,15 +529,15 @@ impl ButtonLike {
     }
 
     pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
-        Self::new(id).rounding(ButtonLikeRounding::Left)
+        Self::new(id).rounding(ButtonLikeRounding::LEFT)
     }
 
     pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
-        Self::new(id).rounding(ButtonLikeRounding::Right)
+        Self::new(id).rounding(ButtonLikeRounding::RIGHT)
     }
 
     pub fn new_rounded_all(id: impl Into<ElementId>) -> Self {
-        Self::new(id).rounding(ButtonLikeRounding::All)
+        Self::new(id).rounding(ButtonLikeRounding::ALL)
     }
 
     pub fn opacity(mut self, opacity: f32) -> Self {
@@ -594,13 +687,20 @@ impl RenderOnce for ButtonLike {
             .when_some(self.width, |this, width| {
                 this.w(width).justify_center().text_center()
             })
-            .when(matches!(self.style, ButtonStyle::Outlined), |this| {
-                this.border_1()
-            })
-            .when_some(self.rounding, |this, rounding| match rounding {
-                ButtonLikeRounding::All => this.rounded_sm(),
-                ButtonLikeRounding::Left => this.rounded_l_sm(),
-                ButtonLikeRounding::Right => this.rounded_r_sm(),
+            .when(
+                matches!(
+                    self.style,
+                    ButtonStyle::Outlined
+                        | ButtonStyle::OutlinedTransparent
+                        | ButtonStyle::OutlinedGhost
+                ),
+                |this| this.border_1(),
+            )
+            .when_some(self.rounding, |this, rounding| {
+                this.when(rounding.top_left, |this| this.rounded_tl_sm())
+                    .when(rounding.top_right, |this| this.rounded_tr_sm())
+                    .when(rounding.bottom_right, |this| this.rounded_br_sm())
+                    .when(rounding.bottom_left, |this| this.rounded_bl_sm())
             })
             .gap(DynamicSpacing::Base04.rems(cx))
             .map(|this| match self.size {

crates/ui/src/components/button/toggle_button.rs 🔗

@@ -6,15 +6,41 @@ use crate::{ButtonLike, ButtonLikeRounding, ElevationIndex, TintColor, Tooltip,
 
 /// The position of a [`ToggleButton`] within a group of buttons.
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum ToggleButtonPosition {
-    /// The toggle button is first in the group.
-    First,
-
-    /// The toggle button is in the middle of the group (i.e., it is not the first or last toggle button).
-    Middle,
+pub struct ToggleButtonPosition {
+    /// The toggle button is one of the leftmost of the group.
+    leftmost: bool,
+    /// The toggle button is one of the rightmost of the group.
+    rightmost: bool,
+    /// The toggle button is one of the topmost of the group.
+    topmost: bool,
+    /// The toggle button is one of the bottommost of the group.
+    bottommost: bool,
+}
 
-    /// The toggle button is last in the group.
-    Last,
+impl ToggleButtonPosition {
+    pub const HORIZONTAL_FIRST: Self = Self {
+        leftmost: true,
+        ..Self::HORIZONTAL_MIDDLE
+    };
+    pub const HORIZONTAL_MIDDLE: Self = Self {
+        leftmost: false,
+        rightmost: false,
+        topmost: true,
+        bottommost: true,
+    };
+    pub const HORIZONTAL_LAST: Self = Self {
+        rightmost: true,
+        ..Self::HORIZONTAL_MIDDLE
+    };
+
+    pub(crate) fn to_rounding(self) -> ButtonLikeRounding {
+        ButtonLikeRounding {
+            top_left: self.topmost && self.leftmost,
+            top_right: self.topmost && self.rightmost,
+            bottom_right: self.bottommost && self.rightmost,
+            bottom_left: self.bottommost && self.leftmost,
+        }
+    }
 }
 
 #[derive(IntoElement, RegisterComponent)]
@@ -46,15 +72,15 @@ impl ToggleButton {
     }
 
     pub fn first(self) -> Self {
-        self.position_in_group(ToggleButtonPosition::First)
+        self.position_in_group(ToggleButtonPosition::HORIZONTAL_FIRST)
     }
 
     pub fn middle(self) -> Self {
-        self.position_in_group(ToggleButtonPosition::Middle)
+        self.position_in_group(ToggleButtonPosition::HORIZONTAL_MIDDLE)
     }
 
     pub fn last(self) -> Self {
-        self.position_in_group(ToggleButtonPosition::Last)
+        self.position_in_group(ToggleButtonPosition::HORIZONTAL_LAST)
     }
 }
 
@@ -153,10 +179,8 @@ impl RenderOnce for ToggleButton {
         };
 
         self.base
-            .when_some(self.position_in_group, |this, position| match position {
-                ToggleButtonPosition::First => this.rounding(ButtonLikeRounding::Left),
-                ToggleButtonPosition::Middle => this.rounding(None),
-                ToggleButtonPosition::Last => this.rounding(ButtonLikeRounding::Right),
+            .when_some(self.position_in_group, |this, position| {
+                this.rounding(position.to_rounding())
             })
             .child(
                 Label::new(self.label)
@@ -535,7 +559,15 @@ impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
 
                     ButtonLike::new((group_name.clone(), entry_index))
                         .full_width()
-                        .rounding(None)
+                        .rounding(Some(
+                            ToggleButtonPosition {
+                                leftmost: col_index == 0,
+                                rightmost: col_index == COLS - 1,
+                                topmost: row_index == 0,
+                                bottommost: row_index == ROWS - 1,
+                            }
+                            .to_rounding(),
+                        ))
                         .when_some(self.tab_index, |this, tab_index| {
                             this.tab_index(tab_index + entry_index as isize)
                         })

crates/ui/src/components/toggle.rs 🔗

@@ -585,7 +585,7 @@ impl RenderOnce for Switch {
 ///
 /// let switch_field = SwitchField::new(
 ///     "feature-toggle",
-///     "Enable feature",
+///     Some("Enable feature"),
 ///     Some("This feature adds new functionality to the app.".into()),
 ///     ToggleState::Unselected,
 ///     |state, window, cx| {
@@ -596,7 +596,7 @@ impl RenderOnce for Switch {
 #[derive(IntoElement, RegisterComponent)]
 pub struct SwitchField {
     id: ElementId,
-    label: SharedString,
+    label: Option<SharedString>,
     description: Option<SharedString>,
     toggle_state: ToggleState,
     on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
@@ -609,14 +609,14 @@ pub struct SwitchField {
 impl SwitchField {
     pub fn new(
         id: impl Into<ElementId>,
-        label: impl Into<SharedString>,
+        label: Option<impl Into<SharedString>>,
         description: Option<SharedString>,
         toggle_state: impl Into<ToggleState>,
         on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static,
     ) -> Self {
         Self {
             id: id.into(),
-            label: label.into(),
+            label: label.map(Into::into),
             description,
             toggle_state: toggle_state.into(),
             on_click: Arc::new(on_click),
@@ -657,11 +657,11 @@ impl SwitchField {
 
 impl RenderOnce for SwitchField {
     fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
-        let tooltip = self.tooltip.map(|tooltip_fn| {
-            h_flex()
-                .gap_0p5()
-                .child(Label::new(self.label.clone()))
-                .child(
+        let tooltip = self
+            .tooltip
+            .zip(self.label.clone())
+            .map(|(tooltip_fn, label)| {
+                h_flex().gap_0p5().child(Label::new(label)).child(
                     IconButton::new("tooltip_button", IconName::Info)
                         .icon_size(IconSize::XSmall)
                         .icon_color(Color::Muted)
@@ -673,7 +673,7 @@ impl RenderOnce for SwitchField {
                         })
                         .on_click(|_, _, _| {}), // Intentional empty on click handler so that clicking on the info tooltip icon doesn't trigger the switch toggle
                 )
-        });
+            });
 
         h_flex()
             .id((self.id.clone(), "container"))
@@ -694,11 +694,17 @@ impl RenderOnce for SwitchField {
                 (Some(description), None) => v_flex()
                     .gap_0p5()
                     .max_w_5_6()
-                    .child(Label::new(self.label.clone()))
+                    .when_some(self.label, |this, label| this.child(Label::new(label)))
                     .child(Label::new(description.clone()).color(Color::Muted))
                     .into_any_element(),
                 (None, Some(tooltip)) => tooltip.into_any_element(),
-                (None, None) => Label::new(self.label.clone()).into_any_element(),
+                (None, None) => {
+                    if let Some(label) = self.label.clone() {
+                        Label::new(label).into_any_element()
+                    } else {
+                        gpui::Empty.into_any_element()
+                    }
+                }
             })
             .child(
                 Switch::new((self.id.clone(), "switch"), self.toggle_state)
@@ -748,7 +754,7 @@ impl Component for SwitchField {
                                 "Unselected",
                                 SwitchField::new(
                                     "switch_field_unselected",
-                                    "Enable notifications",
+                                    Some("Enable notifications"),
                                     Some("Receive notifications when new messages arrive.".into()),
                                     ToggleState::Unselected,
                                     |_, _, _| {},
@@ -759,7 +765,7 @@ impl Component for SwitchField {
                                 "Selected",
                                 SwitchField::new(
                                     "switch_field_selected",
-                                    "Enable notifications",
+                                    Some("Enable notifications"),
                                     Some("Receive notifications when new messages arrive.".into()),
                                     ToggleState::Selected,
                                     |_, _, _| {},
@@ -775,7 +781,7 @@ impl Component for SwitchField {
                                 "Default",
                                 SwitchField::new(
                                     "switch_field_default",
-                                    "Default color",
+                                    Some("Default color"),
                                     Some("This uses the default switch color.".into()),
                                     ToggleState::Selected,
                                     |_, _, _| {},
@@ -786,7 +792,7 @@ impl Component for SwitchField {
                                 "Accent",
                                 SwitchField::new(
                                     "switch_field_accent",
-                                    "Accent color",
+                                    Some("Accent color"),
                                     Some("This uses the accent color scheme.".into()),
                                     ToggleState::Selected,
                                     |_, _, _| {},
@@ -802,7 +808,7 @@ impl Component for SwitchField {
                             "Disabled",
                             SwitchField::new(
                                 "switch_field_disabled",
-                                "Disabled field",
+                                Some("Disabled field"),
                                 Some("This field is disabled and cannot be toggled.".into()),
                                 ToggleState::Selected,
                                 |_, _, _| {},
@@ -817,7 +823,7 @@ impl Component for SwitchField {
                             "No Description",
                             SwitchField::new(
                                 "switch_field_disabled",
-                                "Disabled field",
+                                Some("Disabled field"),
                                 None,
                                 ToggleState::Selected,
                                 |_, _, _| {},
@@ -832,7 +838,7 @@ impl Component for SwitchField {
                                 "Tooltip with Description",
                                 SwitchField::new(
                                     "switch_field_tooltip_with_desc",
-                                    "Nice Feature",
+                                    Some("Nice Feature"),
                                     Some("Enable advanced configuration options.".into()),
                                     ToggleState::Unselected,
                                     |_, _, _| {},
@@ -844,7 +850,7 @@ impl Component for SwitchField {
                                 "Tooltip without Description",
                                 SwitchField::new(
                                     "switch_field_tooltip_no_desc",
-                                    "Nice Feature",
+                                    Some("Nice Feature"),
                                     None,
                                     ToggleState::Selected,
                                     |_, _, _| {},

crates/ui/src/components/tree_view_item.rs 🔗

@@ -21,6 +21,7 @@ pub struct TreeViewItem {
     on_toggle: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
     on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut Window, &mut App) + 'static>>,
     tab_index: Option<isize>,
+    focus_handle: Option<gpui::FocusHandle>,
 }
 
 impl TreeViewItem {
@@ -41,6 +42,7 @@ impl TreeViewItem {
             on_toggle: None,
             on_secondary_mouse_down: None,
             tab_index: None,
+            focus_handle: None,
         }
     }
 
@@ -107,6 +109,11 @@ impl TreeViewItem {
         self.focused = focused;
         self
     }
+
+    pub fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
+        self.focus_handle = Some(focus_handle.clone());
+        self
+    }
 }
 
 impl Disableable for TreeViewItem {
@@ -126,11 +133,12 @@ impl Toggleable for TreeViewItem {
 impl RenderOnce for TreeViewItem {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
         let selected_bg = cx.theme().colors().element_active.opacity(0.5);
+
+        let transparent_border = cx.theme().colors().border.opacity(0.);
         let selected_border = cx.theme().colors().border.opacity(0.6);
         let focused_border = cx.theme().colors().border_focused;
-        let transparent_border = cx.theme().colors().border_transparent;
-        let item_size = rems_from_px(28.);
 
+        let item_size = rems_from_px(28.);
         let indentation_line = h_flex().size(item_size).flex_none().justify_center().child(
             div()
                 .w_px()
@@ -145,18 +153,14 @@ impl RenderOnce for TreeViewItem {
             .child(
                 h_flex()
                     .id("inner_tree_view_item")
-                    .group("tree_view_item")
                     .cursor_pointer()
                     .size_full()
-                    .relative()
-                    .when_some(self.tab_index, |this, index| this.tab_index(index))
                     .map(|this| {
                         let label = self.label;
 
                         if self.root_item {
                             this.h(item_size)
                                 .px_1()
-                                .mb_1()
                                 .gap_2p5()
                                 .rounded_sm()
                                 .border_1()
@@ -166,6 +170,10 @@ impl RenderOnce for TreeViewItem {
                                 })
                                 .focus(|s| s.border_color(focused_border))
                                 .hover(|s| s.bg(cx.theme().colors().element_hover))
+                                .when_some(self.focus_handle, |this, handle| {
+                                    this.track_focus(&handle)
+                                })
+                                .when_some(self.tab_index, |this, index| this.tab_index(index))
                                 .child(
                                     Disclosure::new("toggle", self.expanded)
                                         .when_some(
@@ -181,6 +189,18 @@ impl RenderOnce for TreeViewItem {
                                     Label::new(label)
                                         .when(!self.selected, |this| this.color(Color::Muted)),
                                 )
+                                .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover))
+                                .when_some(
+                                    self.on_click.filter(|_| !self.disabled),
+                                    |this, on_click| this.on_click(on_click),
+                                )
+                                .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
+                                    this.on_mouse_down(
+                                        MouseButton::Right,
+                                        move |event, window, cx| (on_mouse_down)(event, window, cx),
+                                    )
+                                })
+                                .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
                         } else {
                             this.child(indentation_line).child(
                                 h_flex()
@@ -190,46 +210,42 @@ impl RenderOnce for TreeViewItem {
                                     .px_1()
                                     .rounded_sm()
                                     .border_1()
-                                    .focusable()
                                     .border_color(transparent_border)
                                     .when(self.selected, |this| {
                                         this.border_color(selected_border).bg(selected_bg)
                                     })
-                                    .in_focus(|s| s.border_color(focused_border))
+                                    .focus(|s| s.border_color(focused_border))
                                     .hover(|s| s.bg(cx.theme().colors().element_hover))
+                                    .when_some(self.focus_handle, |this, handle| {
+                                        this.track_focus(&handle)
+                                    })
+                                    .when_some(self.tab_index, |this, index| this.tab_index(index))
                                     .child(
                                         Label::new(label)
                                             .when(!self.selected, |this| this.color(Color::Muted)),
-                                    ),
+                                    )
+                                    .when_some(self.on_hover, |this, on_hover| {
+                                        this.on_hover(on_hover)
+                                    })
+                                    .when_some(
+                                        self.on_click.filter(|_| !self.disabled),
+                                        |this, on_click| this.on_click(on_click),
+                                    )
+                                    .when_some(
+                                        self.on_secondary_mouse_down,
+                                        |this, on_mouse_down| {
+                                            this.on_mouse_down(
+                                                MouseButton::Right,
+                                                move |event, window, cx| {
+                                                    (on_mouse_down)(event, window, cx)
+                                                },
+                                            )
+                                        },
+                                    )
+                                    .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)),
                             )
                         }
-                    })
-                    .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover))
-                    .when_some(
-                        self.on_click.filter(|_| !self.disabled),
-                        |this, on_click| {
-                            if self.root_item
-                                && let Some(on_toggle) = self.on_toggle.clone()
-                            {
-                                this.on_click(move |event, window, cx| {
-                                    if event.is_keyboard() {
-                                        on_click(event, window, cx);
-                                        on_toggle(event, window, cx);
-                                    } else {
-                                        on_click(event, window, cx);
-                                    }
-                                })
-                            } else {
-                                this.on_click(on_click)
-                            }
-                        },
-                    )
-                    .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
-                        this.on_mouse_down(MouseButton::Right, move |event, window, cx| {
-                            (on_mouse_down)(event, window, cx)
-                        })
-                    })
-                    .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)),
+                    }),
             )
     }
 }

crates/ui_input/src/number_field.rs 🔗

@@ -344,6 +344,27 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
             }
         };
 
+        let bg_color = cx.theme().colors().surface_background;
+        let hover_bg_color = cx.theme().colors().element_hover;
+
+        let border_color = cx.theme().colors().border_variant;
+        let focus_border_color = cx.theme().colors().border_focused;
+
+        let base_button = |icon: IconName| {
+            h_flex()
+                .cursor_pointer()
+                .p_1p5()
+                .size_full()
+                .justify_center()
+                .overflow_hidden()
+                .border_1()
+                .border_color(border_color)
+                .bg(bg_color)
+                .hover(|s| s.bg(hover_bg_color))
+                .focus(|s| s.border_color(focus_border_color).bg(hover_bg_color))
+                .child(Icon::new(icon).size(IconSize::Small))
+        };
+
         h_flex()
             .id(self.id.clone())
             .track_focus(&self.focus_handle)
@@ -376,28 +397,19 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
                         };
 
                         decrement.child(
-                            h_flex()
+                            base_button(IconName::Dash)
                                 .id("decrement_button")
-                                .cursor(gpui::CursorStyle::PointingHand)
-                                .p_1p5()
-                                .size_full()
-                                .justify_center()
-                                .overflow_hidden()
                                 .rounded_tl_sm()
                                 .rounded_bl_sm()
-                                .border_1()
-                                .border_color(cx.theme().colors().border_variant)
-                                .bg(cx.theme().colors().surface_background)
-                                .hover(|s| s.bg(cx.theme().colors().element_hover))
-                                .child(Icon::new(IconName::Dash).size(IconSize::Small))
-                                .when_some(tab_index.as_mut(), |this, tab_index| {
-                                    *tab_index += 1;
-                                    this.tab_index(*tab_index - 1).focus(|style| {
-                                        style
-                                            .border_color(cx.theme().colors().border_focused)
-                                            .bg(cx.theme().colors().element_hover)
-                                    })
-                                })
+                                .tab_index(
+                                    tab_index
+                                        .as_mut()
+                                        .map(|tab_index| {
+                                            *tab_index += 1;
+                                            *tab_index - 1
+                                        })
+                                        .unwrap_or(0),
+                                )
                                 .on_click(decrement_handler),
                         )
                     })
@@ -406,34 +418,23 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
                             .min_w_16()
                             .size_full()
                             .border_y_1()
-                            .border_color(cx.theme().colors().border_variant)
-                            .bg(cx.theme().colors().surface_background)
-                            .in_focus(|this| this.border_color(cx.theme().colors().border_focused))
+                            .border_color(border_color)
+                            .bg(bg_color)
+                            .in_focus(|this| this.border_color(focus_border_color))
                             .child(match *self.mode.read(cx) {
                                 NumberFieldMode::Read => h_flex()
-                                    .id("numeric_stepper_label")
                                     .px_1()
                                     .flex_1()
                                     .justify_center()
                                     .child(Label::new((self.format)(&self.value)))
-                                    .when_some(tab_index.as_mut(), |this, tab_index| {
-                                        *tab_index += 1;
-                                        this.tab_index(*tab_index - 1).focus(|style| {
-                                            style
-                                                .border_color(cx.theme().colors().border_focused)
-                                                .bg(cx.theme().colors().element_hover)
-                                        })
-                                    })
-                                    .on_click({
-                                        let _mode = self.mode.clone();
-                                        move |click, _, _cx| {
-                                            if click.click_count() == 2 || click.is_keyboard() {
-                                                // Edit mode is disabled until we implement center text alignment for editor
-                                                // mode.write(cx, NumberFieldMode::Edit);
-                                            }
-                                        }
-                                    })
                                     .into_any_element(),
+                                // Edit mode is disabled until we implement center text alignment for editor
+                                // mode.write(cx, NumberFieldMode::Edit);
+                                //
+                                // When we get to making Edit mode work, we shouldn't even focus the decrement/increment buttons.
+                                // Focus should go instead straight to the editor, avoiding any double-step focus.
+                                // In this world, the buttons become a mouse-only interaction, given users should be able
+                                // to do everything they'd do with the buttons straight in the editor anyway.
                                 NumberFieldMode::Edit => h_flex()
                                     .flex_1()
                                     .child(window.use_state(cx, {
@@ -501,28 +502,19 @@ impl<T: NumberFieldType> RenderOnce for NumberField<T> {
                         };
 
                         increment.child(
-                            h_flex()
+                            base_button(IconName::Plus)
                                 .id("increment_button")
-                                .cursor(gpui::CursorStyle::PointingHand)
-                                .p_1p5()
-                                .size_full()
-                                .justify_center()
-                                .overflow_hidden()
                                 .rounded_tr_sm()
                                 .rounded_br_sm()
-                                .border_1()
-                                .border_color(cx.theme().colors().border_variant)
-                                .bg(cx.theme().colors().surface_background)
-                                .hover(|s| s.bg(cx.theme().colors().element_hover))
-                                .child(Icon::new(IconName::Plus).size(IconSize::Small))
-                                .when_some(tab_index.as_mut(), |this, tab_index| {
-                                    *tab_index += 1;
-                                    this.tab_index(*tab_index - 1).focus(|style| {
-                                        style
-                                            .border_color(cx.theme().colors().border_focused)
-                                            .bg(cx.theme().colors().element_hover)
-                                    })
-                                })
+                                .tab_index(
+                                    tab_index
+                                        .as_mut()
+                                        .map(|tab_index| {
+                                            *tab_index += 1;
+                                            *tab_index - 1
+                                        })
+                                        .unwrap_or(0),
+                                )
                                 .on_click(increment_handler),
                         )
                     }),

crates/util/src/paths.rs 🔗

@@ -4,6 +4,7 @@ use itertools::Itertools;
 use regex::Regex;
 use serde::{Deserialize, Serialize};
 use std::cmp::Ordering;
+use std::error::Error;
 use std::fmt::{Display, Formatter};
 use std::mem;
 use std::path::StripPrefixError;
@@ -184,6 +185,31 @@ impl<T: AsRef<Path>> PathExt for T {
     }
 }
 
+pub fn path_ends_with(base: &Path, suffix: &Path) -> bool {
+    strip_path_suffix(base, suffix).is_some()
+}
+
+pub fn strip_path_suffix<'a>(base: &'a Path, suffix: &Path) -> Option<&'a Path> {
+    if let Some(remainder) = base
+        .as_os_str()
+        .as_encoded_bytes()
+        .strip_suffix(suffix.as_os_str().as_encoded_bytes())
+    {
+        if remainder
+            .last()
+            .is_none_or(|last_byte| std::path::is_separator(*last_byte as char))
+        {
+            let os_str = unsafe {
+                OsStr::from_encoded_bytes_unchecked(
+                    &remainder[0..remainder.len().saturating_sub(1)],
+                )
+            };
+            return Some(Path::new(os_str));
+        }
+    }
+    None
+}
+
 /// In memory, this is identical to `Path`. On non-Windows conversions to this type are no-ops. On
 /// windows, these conversions sanitize UNC paths by removing the `\\\\?\\` prefix.
 #[derive(Eq, PartialEq, Hash, Ord, PartialOrd)]
@@ -401,6 +427,82 @@ pub fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
                         .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
 }
 
+#[derive(Debug, PartialEq)]
+#[non_exhaustive]
+pub struct NormalizeError;
+
+impl Error for NormalizeError {}
+
+impl std::fmt::Display for NormalizeError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("parent reference `..` points outside of base directory")
+    }
+}
+
+/// Copied from stdlib where it's unstable.
+///
+/// Normalize a path, including `..` without traversing the filesystem.
+///
+/// Returns an error if normalization would leave leading `..` components.
+///
+/// <div class="warning">
+///
+/// This function always resolves `..` to the "lexical" parent.
+/// That is "a/b/../c" will always resolve to `a/c` which can change the meaning of the path.
+/// In particular, `a/c` and `a/b/../c` are distinct on many systems because `b` may be a symbolic link, so its parent isn't `a`.
+///
+/// </div>
+///
+/// [`path::absolute`](absolute) is an alternative that preserves `..`.
+/// Or [`Path::canonicalize`] can be used to resolve any `..` by querying the filesystem.
+pub fn normalize_lexically(path: &Path) -> Result<PathBuf, NormalizeError> {
+    use std::path::Component;
+
+    let mut lexical = PathBuf::new();
+    let mut iter = path.components().peekable();
+
+    // Find the root, if any, and add it to the lexical path.
+    // Here we treat the Windows path "C:\" as a single "root" even though
+    // `components` splits it into two: (Prefix, RootDir).
+    let root = match iter.peek() {
+        Some(Component::ParentDir) => return Err(NormalizeError),
+        Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => {
+            lexical.push(p);
+            iter.next();
+            lexical.as_os_str().len()
+        }
+        Some(Component::Prefix(prefix)) => {
+            lexical.push(prefix.as_os_str());
+            iter.next();
+            if let Some(p @ Component::RootDir) = iter.peek() {
+                lexical.push(p);
+                iter.next();
+            }
+            lexical.as_os_str().len()
+        }
+        None => return Ok(PathBuf::new()),
+        Some(Component::Normal(_)) => 0,
+    };
+
+    for component in iter {
+        match component {
+            Component::RootDir => unreachable!(),
+            Component::Prefix(_) => return Err(NormalizeError),
+            Component::CurDir => continue,
+            Component::ParentDir => {
+                // It's an error if ParentDir causes us to go above the "root".
+                if lexical.as_os_str().len() == root {
+                    return Err(NormalizeError);
+                } else {
+                    lexical.pop();
+                }
+            }
+            Component::Normal(path) => lexical.push(path),
+        }
+    }
+    Ok(lexical)
+}
+
 /// A delimiter to use in `path_query:row_number:column_number` strings parsing.
 pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
 
@@ -1798,4 +1900,35 @@ mod tests {
         let path = Path::new("/a/b/c/long.app.tar.gz");
         assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string()));
     }
+
+    #[test]
+    fn test_strip_path_suffix() {
+        let base = Path::new("/a/b/c/file_name");
+        let suffix = Path::new("file_name");
+        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
+
+        let base = Path::new("/a/b/c/file_name.tsx");
+        let suffix = Path::new("file_name.tsx");
+        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c")));
+
+        let base = Path::new("/a/b/c/file_name.stories.tsx");
+        let suffix = Path::new("c/file_name.stories.tsx");
+        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b")));
+
+        let base = Path::new("/a/b/c/long.app.tar.gz");
+        let suffix = Path::new("b/c/long.app.tar.gz");
+        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a")));
+
+        let base = Path::new("/a/b/c/long.app.tar.gz");
+        let suffix = Path::new("/a/b/c/long.app.tar.gz");
+        assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("")));
+
+        let base = Path::new("/a/b/c/long.app.tar.gz");
+        let suffix = Path::new("/a/b/c/no_match.app.tar.gz");
+        assert_eq!(strip_path_suffix(base, suffix), None);
+
+        let base = Path::new("/a/b/c/long.app.tar.gz");
+        let suffix = Path::new("app.tar.gz");
+        assert_eq!(strip_path_suffix(base, suffix), None);
+    }
 }

crates/util/src/shell.rs 🔗

@@ -11,6 +11,7 @@ pub enum ShellKind {
     PowerShell,
     Nushell,
     Cmd,
+    Xonsh,
 }
 
 pub fn get_system_shell() -> String {
@@ -165,6 +166,7 @@ impl fmt::Display for ShellKind {
             ShellKind::Nushell => write!(f, "nu"),
             ShellKind::Cmd => write!(f, "cmd"),
             ShellKind::Rc => write!(f, "rc"),
+            ShellKind::Xonsh => write!(f, "xonsh"),
         }
     }
 }
@@ -197,6 +199,8 @@ impl ShellKind {
             ShellKind::Tcsh
         } else if program == "rc" {
             ShellKind::Rc
+        } else if program == "xonsh" {
+            ShellKind::Xonsh
         } else if program == "sh" || program == "bash" {
             ShellKind::Posix
         } else {
@@ -220,6 +224,7 @@ impl ShellKind {
             Self::Tcsh => input.to_owned(),
             Self::Rc => input.to_owned(),
             Self::Nushell => Self::to_nushell_variable(input),
+            Self::Xonsh => input.to_owned(),
         }
     }
 
@@ -345,7 +350,8 @@ impl ShellKind {
             | ShellKind::Fish
             | ShellKind::Csh
             | ShellKind::Tcsh
-            | ShellKind::Rc => interactive
+            | ShellKind::Rc
+            | ShellKind::Xonsh => interactive
                 .then(|| "-i".to_owned())
                 .into_iter()
                 .chain(["-c".to_owned(), combined_command])
@@ -353,7 +359,7 @@ impl ShellKind {
         }
     }
 
-    pub fn command_prefix(&self) -> Option<char> {
+    pub const fn command_prefix(&self) -> Option<char> {
         match self {
             ShellKind::PowerShell => Some('&'),
             ShellKind::Nushell => Some('^'),
@@ -361,6 +367,13 @@ impl ShellKind {
         }
     }
 
+    pub const fn sequential_commands_separator(&self) -> char {
+        match self {
+            ShellKind::Cmd => '&',
+            _ => ';',
+        }
+    }
+
     pub fn try_quote<'a>(&self, arg: &'a str) -> Option<Cow<'a, str>> {
         shlex::try_quote(arg).ok().map(|arg| match self {
             // If we are running in PowerShell, we want to take extra care when escaping strings.
@@ -370,4 +383,24 @@ impl ShellKind {
             _ => arg,
         })
     }
+
+    pub const fn activate_keyword(&self) -> &'static str {
+        match self {
+            ShellKind::Cmd => "",
+            ShellKind::Nushell => "overlay use",
+            ShellKind::PowerShell => ".",
+            ShellKind::Fish => "source",
+            ShellKind::Csh => "source",
+            ShellKind::Tcsh => "source",
+            ShellKind::Posix | ShellKind::Rc => "source",
+            ShellKind::Xonsh => "source",
+        }
+    }
+
+    pub const fn clear_screen_command(&self) -> &'static str {
+        match self {
+            ShellKind::Cmd => "cls",
+            _ => "clear",
+        }
+    }
 }

crates/util/src/shell_env.rs 🔗

@@ -46,10 +46,14 @@ async fn capture_unix(
     // See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482
     const FD_STDIN: std::os::fd::RawFd = 0;
     const FD_STDOUT: std::os::fd::RawFd = 1;
+    const FD_STDERR: std::os::fd::RawFd = 2;
 
     let (fd_num, redir) = match shell_kind {
         ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
         ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
+        // xonsh doesn't support redirecting to stdin, and control sequences are printed to
+        // stdout on startup
+        ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()),
         _ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
     };
     command.stdin(Stdio::null());
@@ -133,7 +137,12 @@ async fn capture_windows(
 
     let shell_kind = ShellKind::new(shell_path);
     let env_output = match shell_kind {
-        ShellKind::Posix | ShellKind::Csh | ShellKind::Tcsh | ShellKind::Rc | ShellKind::Fish => {
+        ShellKind::Posix
+        | ShellKind::Csh
+        | ShellKind::Tcsh
+        | ShellKind::Rc
+        | ShellKind::Fish
+        | ShellKind::Xonsh => {
             return Err(anyhow::anyhow!("unsupported shell kind"));
         }
         ShellKind::PowerShell => {

crates/vim/src/motion.rs 🔗

@@ -2388,6 +2388,7 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
     let display_point = map.clip_at_line_end(display_point);
     let point = display_point.to_point(map);
     let offset = point.to_offset(&map.buffer_snapshot());
+    let snapshot = map.buffer_snapshot();
 
     // Ensure the range is contained by the current line.
     let mut line_end = map.next_line_boundary(point).0;
@@ -2395,10 +2396,19 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
         line_end = map.max_point().to_point(map);
     }
 
-    if let Some((opening_range, closing_range)) = map
-        .buffer_snapshot()
-        .innermost_enclosing_bracket_ranges(offset..offset, None)
-    {
+    // Attempt to find the smallest enclosing bracket range that also contains
+    // the offset, which only happens if the cursor is currently in a bracket.
+    let range_filter = |_buffer: &language::BufferSnapshot,
+                        opening_range: Range<usize>,
+                        closing_range: Range<usize>| {
+        opening_range.contains(&offset) || closing_range.contains(&offset)
+    };
+
+    let bracket_ranges = snapshot
+        .innermost_enclosing_bracket_ranges(offset..offset, Some(&range_filter))
+        .or_else(|| snapshot.innermost_enclosing_bracket_ranges(offset..offset, None));
+
+    if let Some((opening_range, closing_range)) = bracket_ranges {
         if opening_range.contains(&offset) {
             return closing_range.start.to_display_point(map);
         } else if closing_range.contains(&offset) {
@@ -2440,7 +2450,6 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
                 if distance < closest_distance {
                     closest_pair_destination = Some(close_range.start);
                     closest_distance = distance;
-                    continue;
                 }
             }
 
@@ -2451,7 +2460,6 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
                 if distance < closest_distance {
                     closest_pair_destination = Some(open_range.start);
                     closest_distance = distance;
-                    continue;
                 }
             }
 
@@ -3391,6 +3399,22 @@ mod test {
         }"});
     }
 
+    #[gpui::test]
+    async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new_tsx(cx).await;
+
+        cx.set_shared_state(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"})
+            .await;
+        cx.simulate_shared_keystrokes("%").await;
+        cx.shared_state()
+            .await
+            .assert_eq(indoc! {r"<Button onClick={() => {}ˇ}></Button>"});
+        cx.simulate_shared_keystrokes("%").await;
+        cx.shared_state()
+            .await
+            .assert_eq(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"});
+    }
+
     #[gpui::test]
     async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/src/normal.rs 🔗

@@ -450,6 +450,7 @@ impl Vim {
         &mut self,
         object: Object,
         times: Option<usize>,
+        opening: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -520,10 +521,11 @@ impl Vim {
             Some(Operator::DeleteSurrounds) => {
                 waiting_operator = Some(Operator::DeleteSurrounds);
             }
-            Some(Operator::ChangeSurrounds { target: None }) => {
+            Some(Operator::ChangeSurrounds { target: None, .. }) => {
                 if self.check_and_move_to_valid_bracket_pair(object, window, cx) {
                     waiting_operator = Some(Operator::ChangeSurrounds {
                         target: Some(object),
+                        opening,
                     });
                 }
             }

crates/vim/src/object.rs 🔗

@@ -85,6 +85,41 @@ pub struct CandidateWithRanges {
     close_range: Range<usize>,
 }
 
+/// Selects text at the same indentation level.
+#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
+#[action(namespace = vim)]
+#[serde(deny_unknown_fields)]
+struct Parentheses {
+    #[serde(default)]
+    opening: bool,
+}
+
+/// Selects text at the same indentation level.
+#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
+#[action(namespace = vim)]
+#[serde(deny_unknown_fields)]
+struct SquareBrackets {
+    #[serde(default)]
+    opening: bool,
+}
+
+/// Selects text at the same indentation level.
+#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
+#[action(namespace = vim)]
+#[serde(deny_unknown_fields)]
+struct AngleBrackets {
+    #[serde(default)]
+    opening: bool,
+}
+/// Selects text at the same indentation level.
+#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
+#[action(namespace = vim)]
+#[serde(deny_unknown_fields)]
+struct CurlyBrackets {
+    #[serde(default)]
+    opening: bool,
+}
+
 fn cover_or_next<I: Iterator<Item = (Range<usize>, Range<usize>)>>(
     candidates: Option<I>,
     caret: DisplayPoint,
@@ -275,18 +310,10 @@ actions!(
         DoubleQuotes,
         /// Selects text within vertical bars (pipes).
         VerticalBars,
-        /// Selects text within parentheses.
-        Parentheses,
         /// Selects text within the nearest brackets.
         MiniBrackets,
         /// Selects text within any type of brackets.
         AnyBrackets,
-        /// Selects text within square brackets.
-        SquareBrackets,
-        /// Selects text within curly brackets.
-        CurlyBrackets,
-        /// Selects text within angle brackets.
-        AngleBrackets,
         /// Selects a function argument.
         Argument,
         /// Selects an HTML/XML tag.
@@ -350,17 +377,17 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
         vim.object(Object::DoubleQuotes, window, cx)
     });
-    Vim::action(editor, cx, |vim, _: &Parentheses, window, cx| {
-        vim.object(Object::Parentheses, window, cx)
+    Vim::action(editor, cx, |vim, action: &Parentheses, window, cx| {
+        vim.object_impl(Object::Parentheses, action.opening, window, cx)
     });
-    Vim::action(editor, cx, |vim, _: &SquareBrackets, window, cx| {
-        vim.object(Object::SquareBrackets, window, cx)
+    Vim::action(editor, cx, |vim, action: &SquareBrackets, window, cx| {
+        vim.object_impl(Object::SquareBrackets, action.opening, window, cx)
     });
-    Vim::action(editor, cx, |vim, _: &CurlyBrackets, window, cx| {
-        vim.object(Object::CurlyBrackets, window, cx)
+    Vim::action(editor, cx, |vim, action: &CurlyBrackets, window, cx| {
+        vim.object_impl(Object::CurlyBrackets, action.opening, window, cx)
     });
-    Vim::action(editor, cx, |vim, _: &AngleBrackets, window, cx| {
-        vim.object(Object::AngleBrackets, window, cx)
+    Vim::action(editor, cx, |vim, action: &AngleBrackets, window, cx| {
+        vim.object_impl(Object::AngleBrackets, action.opening, window, cx)
     });
     Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| {
         vim.object(Object::VerticalBars, window, cx)
@@ -394,10 +421,22 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 
 impl Vim {
     fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
+        self.object_impl(object, false, window, cx);
+    }
+
+    fn object_impl(
+        &mut self,
+        object: Object,
+        opening: bool,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
         let count = Self::take_count(cx);
 
         match self.mode {
-            Mode::Normal | Mode::HelixNormal => self.normal_object(object, count, window, cx),
+            Mode::Normal | Mode::HelixNormal => {
+                self.normal_object(object, count, opening, window, cx)
+            }
             Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => {
                 self.visual_object(object, count, window, cx)
             }

crates/vim/src/state.rs 🔗

@@ -109,6 +109,9 @@ pub enum Operator {
     },
     ChangeSurrounds {
         target: Option<Object>,
+        /// Represents whether the opening bracket was used for the target
+        /// object.
+        opening: bool,
     },
     DeleteSurrounds,
     Mark,
@@ -1077,7 +1080,9 @@ impl Operator {
             | Operator::Replace
             | Operator::Digraph { .. }
             | Operator::Literal { .. }
-            | Operator::ChangeSurrounds { target: Some(_) }
+            | Operator::ChangeSurrounds {
+                target: Some(_), ..
+            }
             | Operator::DeleteSurrounds => true,
             Operator::Change
             | Operator::Delete
@@ -1094,7 +1099,7 @@ impl Operator {
             | Operator::ReplaceWithRegister
             | Operator::Exchange
             | Operator::Object { .. }
-            | Operator::ChangeSurrounds { target: None }
+            | Operator::ChangeSurrounds { target: None, .. }
             | Operator::OppositeCase
             | Operator::ToggleComments
             | Operator::HelixMatch
@@ -1121,7 +1126,7 @@ impl Operator {
             | Operator::Rewrap
             | Operator::ShellCommand
             | Operator::AddSurrounds { target: None }
-            | Operator::ChangeSurrounds { target: None }
+            | Operator::ChangeSurrounds { target: None, .. }
             | Operator::DeleteSurrounds
             | Operator::Exchange
             | Operator::HelixNext { .. }

crates/vim/src/surrounds.rs 🔗

@@ -221,6 +221,7 @@ impl Vim {
         &mut self,
         text: Arc<str>,
         target: Object,
+        opening: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -241,16 +242,19 @@ impl Vim {
                         },
                     };
 
-                    // Determines whether space should be added after
-                    // and before the surround pairs.
-                    // Space is only added in the following cases:
-                    // - new surround is not quote and is opening bracket (({[<)
-                    // - new surround is quote and original was also quote
-                    let surround = if pair.start != pair.end {
-                        pair.end != surround_alias((*text).as_ref())
-                    } else {
-                        will_replace_pair.start == will_replace_pair.end
-                    };
+                    // A single space should be added if the new surround is a
+                    // bracket and not a quote (pair.start != pair.end) and if
+                    // the bracket used is the opening bracket.
+                    let add_space =
+                        !(pair.start == pair.end) && (pair.end != surround_alias((*text).as_ref()));
+
+                    // Space should be preserved if either the surrounding
+                    // characters being updated are quotes
+                    // (will_replace_pair.start == will_replace_pair.end) or if
+                    // the bracket used in the command is not an opening
+                    // bracket.
+                    let preserve_space =
+                        will_replace_pair.start == will_replace_pair.end || !opening;
 
                     let (display_map, selections) = editor.selections.all_adjusted_display(cx);
                     let mut edits = Vec::new();
@@ -269,23 +273,36 @@ impl Vim {
                                     continue;
                                 }
                             }
+
+                            // Keeps track of the length of the string that is
+                            // going to be edited on the start so we can ensure
+                            // that the end replacement string does not exceed
+                            // this value. Helpful when dealing with newlines.
+                            let mut edit_len = 0;
                             let mut chars_and_offset = display_map
                                 .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left))
                                 .peekable();
+
                             while let Some((ch, offset)) = chars_and_offset.next() {
                                 if ch.to_string() == will_replace_pair.start {
                                     let mut open_str = pair.start.clone();
                                     let start = offset;
                                     let mut end = start + 1;
-                                    if let Some((next_ch, _)) = chars_and_offset.peek() {
-                                        // If the next position is already a space or line break,
-                                        // we don't need to splice another space even under around
-                                        if surround && !next_ch.is_whitespace() {
-                                            open_str.push(' ');
-                                        } else if !surround && next_ch.to_string() == " " {
-                                            end += 1;
+                                    while let Some((next_ch, _)) = chars_and_offset.next()
+                                        && next_ch.to_string() == " "
+                                    {
+                                        end += 1;
+
+                                        if preserve_space {
+                                            open_str.push(next_ch);
                                         }
                                     }
+
+                                    if add_space {
+                                        open_str.push(' ');
+                                    };
+
+                                    edit_len = end - start;
                                     edits.push((start..end, open_str));
                                     anchors.push(start..start);
                                     break;
@@ -299,16 +316,25 @@ impl Vim {
                                 .peekable();
                             while let Some((ch, offset)) = reverse_chars_and_offsets.next() {
                                 if ch.to_string() == will_replace_pair.end {
-                                    let mut close_str = pair.end.clone();
+                                    let mut close_str = String::new();
                                     let mut start = offset;
                                     let end = start + 1;
-                                    if let Some((next_ch, _)) = reverse_chars_and_offsets.peek() {
-                                        if surround && !next_ch.is_whitespace() {
-                                            close_str.insert(0, ' ')
-                                        } else if !surround && next_ch.to_string() == " " {
-                                            start -= 1;
+                                    while let Some((next_ch, _)) = reverse_chars_and_offsets.next()
+                                        && next_ch.to_string() == " "
+                                        && close_str.len() < edit_len - 1
+                                    {
+                                        start -= 1;
+
+                                        if preserve_space {
+                                            close_str.push(next_ch);
                                         }
                                     }
+
+                                    if add_space {
+                                        close_str.push(' ');
+                                    };
+
+                                    close_str.push_str(&pair.end);
                                     edits.push((start..end, close_str));
                                     break;
                                 }
@@ -448,7 +474,7 @@ impl Vim {
                 surround: true,
                 newline: false,
             }),
-            Object::CurlyBrackets => Some(BracketPair {
+            Object::CurlyBrackets { .. } => Some(BracketPair {
                 start: "{".to_string(),
                 end: "}".to_string(),
                 close: true,
@@ -1194,7 +1220,30 @@ mod test {
             };"},
             Mode::Normal,
         );
-        cx.simulate_keystrokes("c s { [");
+        cx.simulate_keystrokes("c s } ]");
+        cx.assert_state(
+            indoc! {"
+            fn test_surround() ˇ[
+                if 2 > 1 ˇ[
+                    println!(\"it is fine\");
+                ]
+            ];"},
+            Mode::Normal,
+        );
+
+        // Currently, the same test case but using the closing bracket `]`
+        // actually removes a whitespace before the closing bracket, something
+        // that might need to be fixed?
+        cx.set_state(
+            indoc! {"
+            fn test_surround() {
+                ifˇ 2 > 1 {
+                    ˇprintln!(\"it is fine\");
+                }
+            };"},
+            Mode::Normal,
+        );
+        cx.simulate_keystrokes("c s { ]");
         cx.assert_state(
             indoc! {"
             fn test_surround() ˇ[
@@ -1270,7 +1319,7 @@ mod test {
         cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal);
 
         cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal);
-        cx.simulate_keystrokes("c s b {");
+        cx.simulate_keystrokes("c s b }");
         cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal);
 
         cx.set_state(
@@ -1290,6 +1339,66 @@ mod test {
         );
     }
 
+    // The following test cases all follow tpope/vim-surround's behaviour
+    // and are more focused on how whitespace is handled.
+    #[gpui::test]
+    async fn test_change_surrounds_vim(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        // Changing quote to quote should never change the surrounding
+        // whitespace.
+        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
+        cx.simulate_keystrokes("c s ' \"");
+        cx.assert_state(indoc! {"ˇ\"  a  \""}, Mode::Normal);
+
+        cx.set_state(indoc! {"\"  ˇa  \""}, Mode::Normal);
+        cx.simulate_keystrokes("c s \" '");
+        cx.assert_state(indoc! {"ˇ'  a  '"}, Mode::Normal);
+
+        // Changing quote to bracket adds one more space when the opening
+        // bracket is used, does not affect whitespace when the closing bracket
+        // is used.
+        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
+        cx.simulate_keystrokes("c s ' {");
+        cx.assert_state(indoc! {"ˇ{   a   }"}, Mode::Normal);
+
+        cx.set_state(indoc! {"'  ˇa  '"}, Mode::Normal);
+        cx.simulate_keystrokes("c s ' }");
+        cx.assert_state(indoc! {"ˇ{  a  }"}, Mode::Normal);
+
+        // Changing bracket to quote should remove all space when the
+        // opening bracket is used and preserve all space when the
+        // closing one is used.
+        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
+        cx.simulate_keystrokes("c s { '");
+        cx.assert_state(indoc! {"ˇ'a'"}, Mode::Normal);
+
+        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
+        cx.simulate_keystrokes("c s } '");
+        cx.assert_state(indoc! {"ˇ'  a  '"}, Mode::Normal);
+
+        // Changing bracket to bracket follows these rules:
+        // * opening → opening – keeps only one space.
+        // * opening → closing – removes all space.
+        // * closing → opening – adds one space.
+        // * closing → closing – does not change space.
+        cx.set_state(indoc! {"{   ˇa   }"}, Mode::Normal);
+        cx.simulate_keystrokes("c s { [");
+        cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal);
+
+        cx.set_state(indoc! {"{   ˇa   }"}, Mode::Normal);
+        cx.simulate_keystrokes("c s { ]");
+        cx.assert_state(indoc! {"ˇ[a]"}, Mode::Normal);
+
+        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
+        cx.simulate_keystrokes("c s } [");
+        cx.assert_state(indoc! {"ˇ[   a   ]"}, Mode::Normal);
+
+        cx.set_state(indoc! {"{  ˇa  }"}, Mode::Normal);
+        cx.simulate_keystrokes("c s } ]");
+        cx.assert_state(indoc! {"ˇ[  a  ]"}, Mode::Normal);
+    }
+
     #[gpui::test]
     async fn test_surrounds(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;

crates/vim/src/test/neovim_backed_test_context.rs 🔗

@@ -207,6 +207,26 @@ impl NeovimBackedTestContext {
         }
     }
 
+    pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext {
+        #[cfg(feature = "neovim")]
+        cx.executor().allow_parking();
+        let thread = thread::current();
+        let test_name = thread
+            .name()
+            .expect("thread is not named")
+            .split(':')
+            .next_back()
+            .unwrap()
+            .to_string();
+        Self {
+            cx: VimTestContext::new_tsx(cx).await,
+            neovim: NeovimConnection::new(test_name).await,
+
+            last_set_state: None,
+            recent_keystrokes: Default::default(),
+        }
+    }
+
     pub async fn set_shared_state(&mut self, marked_text: &str) {
         let mode = if marked_text.contains('»') {
             Mode::Visual

crates/vim/src/test/vim_test_context.rs 🔗

@@ -66,6 +66,28 @@ impl VimTestContext {
         )
     }
 
+    pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> VimTestContext {
+        Self::init(cx);
+        Self::new_with_lsp(
+            EditorLspTestContext::new_tsx(
+                lsp::ServerCapabilities {
+                    completion_provider: Some(lsp::CompletionOptions {
+                        trigger_characters: Some(vec![".".to_string()]),
+                        ..Default::default()
+                    }),
+                    rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
+                        prepare_provider: Some(true),
+                        work_done_progress_options: Default::default(),
+                    })),
+                    ..Default::default()
+                },
+                cx,
+            )
+            .await,
+            true,
+        )
+    }
+
     pub fn init_keybindings(enabled: bool, cx: &mut App) {
         SettingsStore::update_global(cx, |store, cx| {
             store.update_user_settings(cx, |s| s.vim_mode = Some(enabled));

crates/vim/src/vim.rs 🔗

@@ -678,6 +678,7 @@ impl Vim {
                     vim.push_operator(
                         Operator::ChangeSurrounds {
                             target: action.target,
+                            opening: false,
                         },
                         window,
                         cx,
@@ -945,6 +946,7 @@ impl Vim {
                 self.update_editor(cx, |_, editor, cx| {
                     editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx)
                 });
+
                 return;
             }
         } else if window.has_pending_keystrokes() || keystroke_event.keystroke.is_ime_in_progress()
@@ -1780,10 +1782,10 @@ impl Vim {
                 }
                 _ => self.clear_operator(window, cx),
             },
-            Some(Operator::ChangeSurrounds { target }) => match self.mode {
+            Some(Operator::ChangeSurrounds { target, opening }) => match self.mode {
                 Mode::Normal => {
                     if let Some(target) = target {
-                        self.change_surrounds(text, target, window, cx);
+                        self.change_surrounds(text, target, opening, window, cx);
                         self.clear_operator(window, cx);
                     }
                 }
@@ -1913,7 +1915,7 @@ impl From<settings::ModeContent> for Mode {
 }
 
 impl Settings for VimSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let vim = content.vim.clone().unwrap();
         Self {
             default_mode: vim.default_mode.unwrap().into(),

crates/vim/test_data/test_matching_nested_brackets.json 🔗

@@ -0,0 +1,5 @@
+{"Put":{"state":"<Button onClick=ˇ{() => {}}></Button>"}}
+{"Key":"%"}
+{"Get":{"state":"<Button onClick={() => {}ˇ}></Button>","mode":"Normal"}}
+{"Key":"%"}
+{"Get":{"state":"<Button onClick=ˇ{() => {}}></Button>","mode":"Normal"}}

crates/vim_mode_setting/src/vim_mode_setting.rs 🔗

@@ -16,7 +16,7 @@ pub fn init(cx: &mut App) {
 pub struct VimModeSetting(pub bool);
 
 impl Settings for VimModeSetting {
-    fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &SettingsContent) -> Self {
         Self(content.vim_mode.unwrap())
     }
 
@@ -28,7 +28,7 @@ impl Settings for VimModeSetting {
 pub struct HelixModeSetting(pub bool);
 
 impl Settings for HelixModeSetting {
-    fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &SettingsContent) -> Self {
         Self(content.helix_mode.unwrap())
     }
 

crates/workspace/src/item.rs 🔗

@@ -65,7 +65,7 @@ pub struct PreviewTabsSettings {
 }
 
 impl Settings for ItemSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let tabs = content.tabs.as_ref().unwrap();
         Self {
             git_status: tabs.git_status.unwrap(),
@@ -113,7 +113,7 @@ impl Settings for ItemSettings {
 }
 
 impl Settings for PreviewTabsSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let preview_tabs = content.preview_tabs.as_ref().unwrap();
         Self {
             enabled: preview_tabs.enabled.unwrap(),

crates/workspace/src/workspace.rs 🔗

@@ -103,7 +103,7 @@ use std::{
     time::Duration,
 };
 use task::{DebugScenario, SpawnInTerminal, TaskContext};
-use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
+use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeSettings};
 pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
 use ui::{Window, prelude::*};
@@ -1435,8 +1435,8 @@ impl Workspace {
 
                 *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
 
-                ThemeSettings::reload_current_theme(cx);
-                ThemeSettings::reload_current_icon_theme(cx);
+                GlobalTheme::reload_theme(cx);
+                GlobalTheme::reload_icon_theme(cx);
             }),
             cx.on_release(move |this, cx| {
                 this.app_state.workspace_store.update(cx, move |store, _| {

crates/workspace/src/workspace_settings.rs 🔗

@@ -2,7 +2,6 @@ use std::num::NonZeroUsize;
 
 use crate::DockPosition;
 use collections::HashMap;
-use gpui::App;
 use serde::Deserialize;
 pub use settings::AutosaveSetting;
 use settings::Settings;
@@ -62,7 +61,7 @@ pub struct TabBarSettings {
 }
 
 impl Settings for WorkspaceSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let workspace = &content.workspace;
         Self {
             active_pane_modifiers: ActivePanelModifiers {
@@ -197,7 +196,7 @@ impl Settings for WorkspaceSettings {
 }
 
 impl Settings for TabBarSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let tab_bar = content.tab_bar.clone().unwrap();
         TabBarSettings {
             show: tab_bar.show.unwrap(),
@@ -231,7 +230,7 @@ pub struct StatusBarSettings {
 }
 
 impl Settings for StatusBarSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let status_bar = content.status_bar.clone().unwrap();
         StatusBarSettings {
             show: status_bar.show.unwrap(),

crates/worktree/src/worktree.rs 🔗

@@ -3154,7 +3154,7 @@ impl File {
         self.worktree.read(cx).id()
     }
 
-    pub fn project_entry_id(&self, _: &App) -> Option<ProjectEntryId> {
+    pub fn project_entry_id(&self) -> Option<ProjectEntryId> {
         match self.disk_state {
             DiskState::Deleted => None,
             _ => self.entry_id,

crates/worktree/src/worktree_settings.rs 🔗

@@ -1,7 +1,6 @@
 use std::path::Path;
 
 use anyhow::Context as _;
-use gpui::App;
 use settings::{Settings, SettingsContent};
 use util::{
     ResultExt,
@@ -35,7 +34,7 @@ impl WorktreeSettings {
 }
 
 impl Settings for WorktreeSettings {
-    fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         let worktree = content.project.worktree.clone();
         let file_scan_exclusions = worktree.file_scan_exclusions.unwrap();
         let file_scan_inclusions = worktree.file_scan_inclusions.unwrap();

crates/zed/Cargo.toml 🔗

@@ -2,7 +2,7 @@
 description = "The fast, collaborative code editor."
 edition.workspace = true
 name = "zed"
-version = "0.208.0"
+version = "0.209.0"
 publish.workspace = true
 license = "GPL-3.0-or-later"
 authors = ["Zed Team <hi@zed.dev>"]
@@ -39,6 +39,7 @@ channel.workspace = true
 clap.workspace = true
 cli.workspace = true
 client.workspace = true
+codestral.workspace = true
 collab_ui.workspace = true
 collections.workspace = true
 command_palette.workspace = true

crates/zed/src/main.rs 🔗

@@ -12,7 +12,6 @@ use crashes::InitCrashHandler;
 use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
 use editor::Editor;
 use extension::ExtensionHostProxy;
-use extension_host::ExtensionStore;
 use fs::{Fs, RealFs};
 use futures::{StreamExt, channel::oneshot, future};
 use git::GitHostingProviderRegistry;
@@ -40,10 +39,7 @@ use std::{
     process,
     sync::Arc,
 };
-use theme::{
-    ActiveTheme, IconThemeNotFoundError, SystemAppearance, ThemeNotFoundError, ThemeRegistry,
-    ThemeSettings,
-};
+use theme::{ActiveTheme, GlobalTheme, ThemeRegistry};
 use util::{ResultExt, TryFutureExt, maybe};
 use uuid::Uuid;
 use workspace::{
@@ -57,7 +53,7 @@ use zed::{
     initialize_workspace, open_paths_with_positions,
 };
 
-use crate::zed::OpenRequestKind;
+use crate::zed::{OpenRequestKind, eager_load_active_theme_and_icon_theme};
 
 #[cfg(feature = "mimalloc")]
 #[global_allocator]
@@ -224,7 +220,9 @@ pub fn main() {
         Ok(path) => askpass::set_askpass_program(path),
         Err(err) => {
             eprintln!("Error: {}", err);
-            process::exit(1);
+            if std::option_env!("ZED_BUNDLE").is_some() {
+                process::exit(1);
+            }
         }
     }
 
@@ -541,11 +539,18 @@ pub fn main() {
             system_id.as_ref().map(|id| id.to_string()),
             cx,
         );
+        extension_host::init(
+            extension_host_proxy.clone(),
+            app_state.fs.clone(),
+            app_state.client.clone(),
+            app_state.node_runtime.clone(),
+            cx,
+        );
 
-        SystemAppearance::init(cx);
         theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
+        eager_load_active_theme_and_icon_theme(fs.clone(), cx);
         theme_extension::init(
-            extension_host_proxy.clone(),
+            extension_host_proxy,
             ThemeRegistry::global(cx),
             cx.background_executor().clone(),
         );
@@ -579,18 +584,10 @@ pub fn main() {
         );
         assistant_tools::init(app_state.client.http_client(), cx);
         repl::init(app_state.fs.clone(), cx);
-        extension_host::init(
-            extension_host_proxy,
-            app_state.fs.clone(),
-            app_state.client.clone(),
-            app_state.node_runtime.clone(),
-            cx,
-        );
         recent_projects::init(cx);
 
         load_embedded_fonts(cx);
 
-        app_state.languages.set_theme(cx.theme().clone());
         editor::init(cx);
         image_viewer::init(cx);
         repl::notebook::init(cx);
@@ -636,8 +633,6 @@ pub fn main() {
         json_schema_store::init(cx);
 
         cx.observe_global::<SettingsStore>({
-            let fs = fs.clone();
-            let languages = app_state.languages.clone();
             let http = app_state.client.http_client();
             let client = app_state.client.clone();
             move |cx| {
@@ -650,9 +645,6 @@ pub fn main() {
                         .ok();
                 }
 
-                eager_load_active_theme_and_icon_theme(fs.clone(), cx);
-
-                languages.set_theme(cx.theme().clone());
                 let new_host = &client::ClientSettings::get_global(cx).server_url;
                 if &http.base_url() != new_host {
                     http.set_base_url(new_host);
@@ -663,6 +655,14 @@ pub fn main() {
             }
         })
         .detach();
+        app_state.languages.set_theme(cx.theme().clone());
+        cx.observe_global::<GlobalTheme>({
+            let languages = app_state.languages.clone();
+            move |cx| {
+                languages.set_theme(cx.theme().clone());
+            }
+        })
+        .detach();
         telemetry::event!(
             "Settings Changed",
             setting = "theme",
@@ -1352,63 +1352,6 @@ fn load_embedded_fonts(cx: &App) {
         .unwrap();
 }
 
-/// Eagerly loads the active theme and icon theme based on the selections in the
-/// theme settings.
-///
-/// This fast path exists to load these themes as soon as possible so the user
-/// doesn't see the default themes while waiting on extensions to load.
-fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &App) {
-    let extension_store = ExtensionStore::global(cx);
-    let theme_registry = ThemeRegistry::global(cx);
-    let theme_settings = ThemeSettings::get_global(cx);
-    let appearance = SystemAppearance::global(cx).0;
-
-    if let Some(theme_selection) = theme_settings.theme_selection.as_ref() {
-        let theme_name = theme_selection.theme(appearance);
-        if matches!(theme_registry.get(theme_name), Err(ThemeNotFoundError(_)))
-            && let Some(theme_path) = extension_store.read(cx).path_to_extension_theme(theme_name)
-        {
-            cx.spawn({
-                let theme_registry = theme_registry.clone();
-                let fs = fs.clone();
-                async move |cx| {
-                    theme_registry.load_user_theme(&theme_path, fs).await?;
-
-                    cx.update(|cx| {
-                        ThemeSettings::reload_current_theme(cx);
-                    })
-                }
-            })
-            .detach_and_log_err(cx);
-        }
-    }
-
-    if let Some(icon_theme_selection) = theme_settings.icon_theme_selection.as_ref() {
-        let icon_theme_name = icon_theme_selection.icon_theme(appearance);
-        if matches!(
-            theme_registry.get_icon_theme(icon_theme_name),
-            Err(IconThemeNotFoundError(_))
-        ) && let Some((icon_theme_path, icons_root_path)) = extension_store
-            .read(cx)
-            .path_to_extension_icon_theme(icon_theme_name)
-        {
-            cx.spawn({
-                let fs = fs.clone();
-                async move |cx| {
-                    theme_registry
-                        .load_icon_theme(&icon_theme_path, &icons_root_path, fs)
-                        .await?;
-
-                    cx.update(|cx| {
-                        ThemeSettings::reload_current_icon_theme(cx);
-                    })
-                }
-            })
-            .detach_and_log_err(cx);
-        }
-    }
-}
-
 /// Spawns a background task to load the user themes from the themes directory.
 fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut App) {
     cx.spawn({
@@ -1433,7 +1376,7 @@ fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut App) {
                     }
                 }
                 theme_registry.load_user_themes(themes_dir, fs).await?;
-                cx.update(ThemeSettings::reload_current_theme)?;
+                cx.update(GlobalTheme::reload_theme)?;
             }
             anyhow::Ok(())
         }
@@ -1459,7 +1402,7 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut App) {
                         .await
                         .log_err()
                 {
-                    cx.update(ThemeSettings::reload_current_theme).log_err();
+                    cx.update(GlobalTheme::reload_theme).log_err();
                 }
             }
         }

crates/zed/src/zed.rs 🔗

@@ -20,7 +20,9 @@ use collections::VecDeque;
 use debugger_ui::debugger_panel::DebugPanel;
 use editor::ProposedChangesEditorToolbar;
 use editor::{Editor, MultiBuffer};
+use extension_host::ExtensionStore;
 use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag};
+use fs::Fs;
 use futures::future::Either;
 use futures::{StreamExt, channel::mpsc, select_biased};
 use git_ui::git_panel::GitPanel;
@@ -68,7 +70,10 @@ use std::{
     sync::atomic::{self, AtomicBool},
 };
 use terminal_view::terminal_panel::{self, TerminalPanel};
-use theme::{ActiveTheme, ThemeSettings};
+use theme::{
+    ActiveTheme, GlobalTheme, IconThemeNotFoundError, SystemAppearance, ThemeNotFoundError,
+    ThemeRegistry, ThemeSettings,
+};
 use ui::{PopoverMenuHandle, prelude::*};
 use util::markdown::MarkdownString;
 use util::rel_path::RelPath;
@@ -2012,6 +2017,55 @@ fn capture_recent_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Cont
     );
 }
 
+/// Eagerly loads the active theme and icon theme based on the selections in the
+/// theme settings.
+///
+/// This fast path exists to load these themes as soon as possible so the user
+/// doesn't see the default themes while waiting on extensions to load.
+pub(crate) fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &mut App) {
+    let extension_store = ExtensionStore::global(cx);
+    let theme_registry = ThemeRegistry::global(cx);
+    let theme_settings = ThemeSettings::get_global(cx);
+    let appearance = SystemAppearance::global(cx).0;
+
+    let theme_name = theme_settings.theme.name(appearance);
+    if matches!(
+        theme_registry.get(&theme_name.0),
+        Err(ThemeNotFoundError(_))
+    ) && let Some(theme_path) = extension_store
+        .read(cx)
+        .path_to_extension_theme(&theme_name.0)
+    {
+        if cx
+            .background_executor()
+            .block(theme_registry.load_user_theme(&theme_path, fs.clone()))
+            .log_err()
+            .is_some()
+        {
+            GlobalTheme::reload_theme(cx);
+        }
+    }
+
+    let theme_settings = ThemeSettings::get_global(cx);
+    let icon_theme_name = theme_settings.icon_theme.name(appearance);
+    if matches!(
+        theme_registry.get_icon_theme(&icon_theme_name.0),
+        Err(IconThemeNotFoundError(_))
+    ) && let Some((icon_theme_path, icons_root_path)) = extension_store
+        .read(cx)
+        .path_to_extension_icon_theme(&icon_theme_name.0)
+    {
+        if cx
+            .background_executor()
+            .block(theme_registry.load_icon_theme(&icon_theme_path, &icons_root_path, fs))
+            .log_err()
+            .is_some()
+        {
+            GlobalTheme::reload_icon_theme(cx);
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -2031,8 +2085,11 @@ mod tests {
         path::{Path, PathBuf},
         time::Duration,
     };
-    use theme::{ThemeRegistry, ThemeSettings};
-    use util::{path, rel_path::rel_path};
+    use theme::ThemeRegistry;
+    use util::{
+        path,
+        rel_path::{RelPath, rel_path},
+    };
     use workspace::{
         NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
         WorkspaceHandle,
@@ -4632,7 +4689,7 @@ mod tests {
         for theme_name in themes.list().into_iter().map(|meta| meta.name) {
             let theme = themes.get(&theme_name).unwrap();
             assert_eq!(theme.name, theme_name);
-            if theme.name == ThemeSettings::get(None, cx).active_theme.name {
+            if theme.name.as_ref() == "One Dark" {
                 has_default_theme = true;
             }
         }

crates/zed/src/zed/edit_prediction_registry.rs 🔗

@@ -1,9 +1,11 @@
 use client::{Client, UserStore};
+use codestral::CodestralCompletionProvider;
 use collections::HashMap;
 use copilot::{Copilot, CopilotCompletionProvider};
 use editor::Editor;
 use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity};
 use language::language_settings::{EditPredictionProvider, all_language_settings};
+use language_models::MistralLanguageModelProvider;
 use settings::SettingsStore;
 use std::{cell::RefCell, rc::Rc, sync::Arc};
 use supermaven::{Supermaven, SupermavenCompletionProvider};
@@ -109,6 +111,10 @@ fn assign_edit_prediction_providers(
     user_store: Entity<UserStore>,
     cx: &mut App,
 ) {
+    if provider == EditPredictionProvider::Codestral {
+        let mistral = MistralLanguageModelProvider::global(client.http_client(), cx);
+        mistral.load_codestral_api_key(cx).detach();
+    }
     for (editor, window) in editors.borrow().iter() {
         _ = window.update(cx, |_window, window, cx| {
             _ = editor.update(cx, |editor, cx| {
@@ -189,6 +195,11 @@ fn assign_edit_prediction_provider(
                 editor.set_edit_prediction_provider(Some(provider), window, cx);
             }
         }
+        EditPredictionProvider::Codestral => {
+            let http_client = client.http_client();
+            let provider = cx.new(|_| CodestralCompletionProvider::new(http_client));
+            editor.set_edit_prediction_provider(Some(provider), window, cx);
+        }
         EditPredictionProvider::Zed => {
             if user_store.read(cx).current_user().is_some() {
                 let mut worktree = None;

crates/zeta/src/zeta.rs 🔗

@@ -151,56 +151,10 @@ impl EditPrediction {
     }
 
     fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
-        interpolate(&self.snapshot, new_snapshot, self.edits.clone())
+        edit_prediction::interpolate_edits(&self.snapshot, new_snapshot, &self.edits)
     }
 }
 
-fn interpolate(
-    old_snapshot: &BufferSnapshot,
-    new_snapshot: &BufferSnapshot,
-    current_edits: Arc<[(Range<Anchor>, String)]>,
-) -> Option<Vec<(Range<Anchor>, String)>> {
-    let mut edits = Vec::new();
-
-    let mut model_edits = current_edits.iter().peekable();
-    for user_edit in new_snapshot.edits_since::<usize>(&old_snapshot.version) {
-        while let Some((model_old_range, _)) = model_edits.peek() {
-            let model_old_range = model_old_range.to_offset(old_snapshot);
-            if model_old_range.end < user_edit.old.start {
-                let (model_old_range, model_new_text) = model_edits.next().unwrap();
-                edits.push((model_old_range.clone(), model_new_text.clone()));
-            } else {
-                break;
-            }
-        }
-
-        if let Some((model_old_range, model_new_text)) = model_edits.peek() {
-            let model_old_offset_range = model_old_range.to_offset(old_snapshot);
-            if user_edit.old == model_old_offset_range {
-                let user_new_text = new_snapshot
-                    .text_for_range(user_edit.new.clone())
-                    .collect::<String>();
-
-                if let Some(model_suffix) = model_new_text.strip_prefix(&user_new_text) {
-                    if !model_suffix.is_empty() {
-                        let anchor = old_snapshot.anchor_after(user_edit.old.end);
-                        edits.push((anchor..anchor, model_suffix.to_string()));
-                    }
-
-                    model_edits.next();
-                    continue;
-                }
-            }
-        }
-
-        return None;
-    }
-
-    edits.extend(model_edits.cloned());
-
-    if edits.is_empty() { None } else { Some(edits) }
-}
-
 impl std::fmt::Debug for EditPrediction {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("EditPrediction")
@@ -769,10 +723,11 @@ impl Zeta {
 
             let Some((edits, snapshot, edit_preview)) = buffer.read_with(cx, {
                 let edits = edits.clone();
-                |buffer, cx| {
+                move |buffer, cx| {
                     let new_snapshot = buffer.snapshot();
                     let edits: Arc<[(Range<Anchor>, String)]> =
-                        interpolate(&snapshot, &new_snapshot, edits)?.into();
+                        edit_prediction::interpolate_edits(&snapshot, &new_snapshot, &edits)?
+                            .into();
                     Some((edits.clone(), new_snapshot, buffer.preview_edits(edits, cx)))
                 }
             })?

crates/zeta2/src/zeta2.rs 🔗

@@ -5,13 +5,13 @@ use cloud_llm_client::predict_edits_v3::{self, PromptFormat, Signature};
 use cloud_llm_client::{
     EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME, ZED_VERSION_HEADER_NAME,
 };
-use cloud_zeta2_prompt::DEFAULT_MAX_PROMPT_BYTES;
+use cloud_zeta2_prompt::{DEFAULT_MAX_PROMPT_BYTES, PlannedPrompt};
 use edit_prediction_context::{
-    DeclarationId, EditPredictionContext, EditPredictionExcerptOptions, SyntaxIndex,
-    SyntaxIndexState,
+    DeclarationId, DeclarationStyle, EditPredictionContext, EditPredictionContextOptions,
+    EditPredictionExcerptOptions, EditPredictionScoreOptions, SyntaxIndex, SyntaxIndexState,
 };
 use futures::AsyncReadExt as _;
-use futures::channel::mpsc;
+use futures::channel::{mpsc, oneshot};
 use gpui::http_client::Method;
 use gpui::{
     App, Entity, EntityId, Global, SemanticVersion, SharedString, Subscription, Task, WeakEntity,
@@ -43,14 +43,20 @@ const BUFFER_CHANGE_GROUPING_INTERVAL: Duration = Duration::from_secs(1);
 /// Maximum number of events to track.
 const MAX_EVENT_COUNT: usize = 16;
 
-pub const DEFAULT_EXCERPT_OPTIONS: EditPredictionExcerptOptions = EditPredictionExcerptOptions {
-    max_bytes: 512,
-    min_bytes: 128,
-    target_before_cursor_over_total_bytes: 0.5,
+pub const DEFAULT_CONTEXT_OPTIONS: EditPredictionContextOptions = EditPredictionContextOptions {
+    use_imports: true,
+    excerpt: EditPredictionExcerptOptions {
+        max_bytes: 512,
+        min_bytes: 128,
+        target_before_cursor_over_total_bytes: 0.5,
+    },
+    score: EditPredictionScoreOptions {
+        omit_excerpt_overlaps: true,
+    },
 };
 
 pub const DEFAULT_OPTIONS: ZetaOptions = ZetaOptions {
-    excerpt: DEFAULT_EXCERPT_OPTIONS,
+    context: DEFAULT_CONTEXT_OPTIONS,
     max_prompt_bytes: DEFAULT_MAX_PROMPT_BYTES,
     max_diagnostic_bytes: 2048,
     prompt_format: PromptFormat::DEFAULT,
@@ -70,12 +76,12 @@ pub struct Zeta {
     projects: HashMap<EntityId, ZetaProject>,
     options: ZetaOptions,
     update_required: bool,
-    debug_tx: Option<mpsc::UnboundedSender<Result<PredictionDebugInfo, String>>>,
+    debug_tx: Option<mpsc::UnboundedSender<PredictionDebugInfo>>,
 }
 
 #[derive(Debug, Clone, PartialEq)]
 pub struct ZetaOptions {
-    pub excerpt: EditPredictionExcerptOptions,
+    pub context: EditPredictionContextOptions,
     pub max_prompt_bytes: usize,
     pub max_diagnostic_bytes: usize,
     pub prompt_format: predict_edits_v3::PromptFormat,
@@ -85,9 +91,10 @@ pub struct ZetaOptions {
 pub struct PredictionDebugInfo {
     pub context: EditPredictionContext,
     pub retrieval_time: TimeDelta,
-    pub request: RequestDebugInfo,
     pub buffer: WeakEntity<Buffer>,
     pub position: language::Anchor,
+    pub local_prompt: Result<String, String>,
+    pub response_rx: oneshot::Receiver<Result<RequestDebugInfo, String>>,
 }
 
 pub type RequestDebugInfo = predict_edits_v3::DebugInfo;
@@ -198,7 +205,7 @@ impl Zeta {
         }
     }
 
-    pub fn debug_info(&mut self) -> mpsc::UnboundedReceiver<Result<PredictionDebugInfo, String>> {
+    pub fn debug_info(&mut self) -> mpsc::UnboundedReceiver<PredictionDebugInfo> {
         let (debug_watch_tx, debug_watch_rx) = mpsc::unbounded();
         self.debug_tx = Some(debug_watch_tx);
         debug_watch_rx
@@ -501,6 +508,11 @@ impl Zeta {
 
         let diagnostics = snapshot.diagnostic_sets().clone();
 
+        let parent_abs_path = project::File::from_dyn(buffer.read(cx).file()).and_then(|f| {
+            let mut path = f.worktree.read(cx).absolutize(&f.path);
+            if path.pop() { Some(path) } else { None }
+        });
+
         let request_task = cx.background_spawn({
             let snapshot = snapshot.clone();
             let buffer = buffer.clone();
@@ -519,17 +531,14 @@ impl Zeta {
                 let Some(context) = EditPredictionContext::gather_context(
                     cursor_point,
                     &snapshot,
-                    &options.excerpt,
+                    parent_abs_path.as_deref(),
+                    &options.context,
                     index_state.as_deref(),
                 ) else {
                     return Ok(None);
                 };
 
-                let debug_context = if let Some(debug_tx) = debug_tx {
-                    Some((debug_tx, context.clone()))
-                } else {
-                    None
-                };
+                let retrieval_time = chrono::Utc::now() - before_retrieval;
 
                 let (diagnostic_groups, diagnostic_groups_truncated) =
                     Self::gather_nearby_diagnostics(
@@ -539,6 +548,8 @@ impl Zeta {
                         options.max_diagnostic_bytes,
                     );
 
+                let debug_context = debug_tx.map(|tx| (tx, context.clone()));
+
                 let request = make_cloud_request(
                     excerpt_path,
                     context,
@@ -555,25 +566,45 @@ impl Zeta {
                     options.prompt_format,
                 );
 
-                let retrieval_time = chrono::Utc::now() - before_retrieval;
-                let response = Self::perform_request(client, llm_token, app_version, request).await;
+                let debug_response_tx = if let Some((debug_tx, context)) = debug_context {
+                    let (response_tx, response_rx) = oneshot::channel();
+
+                    let local_prompt = PlannedPrompt::populate(&request)
+                        .and_then(|p| p.to_prompt_string().map(|p| p.0))
+                        .map_err(|err| err.to_string());
 
-                if let Some((debug_tx, context)) = debug_context {
                     debug_tx
-                        .unbounded_send(response.as_ref().map_err(|err| err.to_string()).and_then(
-                            |response| {
-                                let Some(request) =
-                                    some_or_debug_panic(response.0.debug_info.clone())
-                                else {
-                                    return Err("Missing debug info".to_string());
-                                };
-                                Ok(PredictionDebugInfo {
-                                    context,
-                                    request,
-                                    retrieval_time,
-                                    buffer: buffer.downgrade(),
-                                    position,
-                                })
+                        .unbounded_send(PredictionDebugInfo {
+                            context,
+                            retrieval_time,
+                            buffer: buffer.downgrade(),
+                            local_prompt,
+                            position,
+                            response_rx,
+                        })
+                        .ok();
+                    Some(response_tx)
+                } else {
+                    None
+                };
+
+                if cfg!(debug_assertions) && std::env::var("ZED_ZETA2_SKIP_REQUEST").is_ok() {
+                    if let Some(debug_response_tx) = debug_response_tx {
+                        debug_response_tx
+                            .send(Err("Request skipped".to_string()))
+                            .ok();
+                    }
+                    anyhow::bail!("Skipping request because ZED_ZETA2_SKIP_REQUEST is set")
+                }
+
+                let response = Self::perform_request(client, llm_token, app_version, request).await;
+
+                if let Some(debug_response_tx) = debug_response_tx {
+                    debug_response_tx
+                        .send(response.as_ref().map_err(|err| err.to_string()).and_then(
+                            |response| match some_or_debug_panic(response.0.debug_info.clone()) {
+                                Some(debug_info) => Ok(debug_info),
+                                None => Err("Missing debug info".to_string()),
                             },
                         ))
                         .ok();
@@ -785,6 +816,11 @@ impl Zeta {
             .map(|worktree| worktree.read(cx).snapshot())
             .collect::<Vec<_>>();
 
+        let parent_abs_path = project::File::from_dyn(buffer.read(cx).file()).and_then(|f| {
+            let mut path = f.worktree.read(cx).absolutize(&f.path);
+            if path.pop() { Some(path) } else { None }
+        });
+
         cx.background_spawn(async move {
             let index_state = if let Some(index_state) = index_state {
                 Some(index_state.lock_owned().await)
@@ -798,7 +834,8 @@ impl Zeta {
             EditPredictionContext::gather_context(
                 cursor_point,
                 &snapshot,
-                &options.excerpt,
+                parent_abs_path.as_deref(),
+                &options.context,
                 index_state.as_deref(),
             )
             .context("Failed to select excerpt")
@@ -893,9 +930,9 @@ fn make_cloud_request(
             text_is_truncated,
             signature_range: snippet.declaration.signature_range_in_item_text(),
             parent_index,
-            score_components: snippet.score_components,
-            signature_score: snippet.scores.signature,
-            declaration_score: snippet.scores.declaration,
+            signature_score: snippet.score(DeclarationStyle::Signature),
+            declaration_score: snippet.score(DeclarationStyle::Declaration),
+            score_components: snippet.components,
         });
     }
 
@@ -1350,7 +1387,7 @@ mod tests {
 
                                 let (res_tx, res_rx) = oneshot::channel();
                                 req_tx.unbounded_send((req, res_tx)).unwrap();
-                                serde_json::to_string(&res_rx.await.unwrap()).unwrap()
+                                serde_json::to_string(&res_rx.await?).unwrap()
                             }
                             _ => {
                                 panic!("Unexpected path: {}", uri)

crates/zeta2_tools/Cargo.toml 🔗

@@ -22,6 +22,8 @@ futures.workspace = true
 gpui.workspace = true
 language.workspace = true
 log.workspace = true
+multi_buffer.workspace = true
+ordered-float.workspace = true
 project.workspace = true
 serde.workspace = true
 text.workspace = true

crates/zeta2_tools/src/zeta2_tools.rs 🔗

@@ -1,22 +1,26 @@
-use std::{collections::hash_map::Entry, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
+use std::{
+    cmp::Reverse, collections::hash_map::Entry, path::PathBuf, str::FromStr, sync::Arc,
+    time::Duration,
+};
 
 use chrono::TimeDelta;
 use client::{Client, UserStore};
-use cloud_llm_client::predict_edits_v3::PromptFormat;
+use cloud_llm_client::predict_edits_v3::{DeclarationScoreComponents, PromptFormat};
 use collections::HashMap;
 use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer};
-use futures::StreamExt as _;
+use futures::{StreamExt as _, channel::oneshot};
 use gpui::{
-    Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, actions,
-    prelude::*,
+    CursorStyle, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
+    actions, prelude::*,
 };
 use language::{Buffer, DiskState};
+use ordered_float::OrderedFloat;
 use project::{Project, WorktreeId};
 use ui::{ContextMenu, ContextMenuEntry, DropdownMenu, prelude::*};
 use ui_input::SingleLineInput;
 use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
 use workspace::{Item, SplitDirection, Workspace};
-use zeta2::{Zeta, ZetaOptions};
+use zeta2::{DEFAULT_CONTEXT_OPTIONS, PredictionDebugInfo, Zeta, ZetaOptions};
 
 use edit_prediction_context::{DeclarationStyle, EditPredictionExcerptOptions};
 
@@ -56,7 +60,7 @@ pub fn init(cx: &mut App) {
 pub struct Zeta2Inspector {
     focus_handle: FocusHandle,
     project: Entity<Project>,
-    last_prediction: Option<LastPredictionState>,
+    last_prediction: Option<LastPrediction>,
     max_excerpt_bytes_input: Entity<SingleLineInput>,
     min_excerpt_bytes_input: Entity<SingleLineInput>,
     cursor_context_ratio_input: Entity<SingleLineInput>,
@@ -74,25 +78,27 @@ enum ActiveView {
     Inference,
 }
 
-enum LastPredictionState {
-    Failed(SharedString),
-    Success(LastPrediction),
-    Replaying {
-        prediction: LastPrediction,
-        _task: Task<()>,
-    },
-}
-
 struct LastPrediction {
     context_editor: Entity<Editor>,
-    retrieval_time: TimeDelta,
-    prompt_planning_time: TimeDelta,
-    inference_time: TimeDelta,
-    parsing_time: TimeDelta,
     prompt_editor: Entity<Editor>,
-    model_response_editor: Entity<Editor>,
+    retrieval_time: TimeDelta,
     buffer: WeakEntity<Buffer>,
     position: language::Anchor,
+    state: LastPredictionState,
+    _task: Option<Task<()>>,
+}
+
+enum LastPredictionState {
+    Requested,
+    Success {
+        inference_time: TimeDelta,
+        parsing_time: TimeDelta,
+        prompt_planning_time: TimeDelta,
+        model_response_editor: Entity<Editor>,
+    },
+    Failed {
+        message: String,
+    },
 }
 
 impl Zeta2Inspector {
@@ -107,15 +113,9 @@ impl Zeta2Inspector {
         let mut request_rx = zeta.update(cx, |zeta, _cx| zeta.debug_info());
 
         let receive_task = cx.spawn_in(window, async move |this, cx| {
-            while let Some(prediction_result) = request_rx.next().await {
-                this.update_in(cx, |this, window, cx| match prediction_result {
-                    Ok(prediction) => {
-                        this.update_last_prediction(prediction, window, cx);
-                    }
-                    Err(err) => {
-                        this.last_prediction = Some(LastPredictionState::Failed(err.into()));
-                        cx.notify();
-                    }
+            while let Some(prediction) = request_rx.next().await {
+                this.update_in(cx, |this, window, cx| {
+                    this.update_last_prediction(prediction, window, cx)
                 })
                 .ok();
             }
@@ -146,16 +146,19 @@ impl Zeta2Inspector {
         cx: &mut Context<Self>,
     ) {
         self.max_excerpt_bytes_input.update(cx, |input, cx| {
-            input.set_text(options.excerpt.max_bytes.to_string(), window, cx);
+            input.set_text(options.context.excerpt.max_bytes.to_string(), window, cx);
         });
         self.min_excerpt_bytes_input.update(cx, |input, cx| {
-            input.set_text(options.excerpt.min_bytes.to_string(), window, cx);
+            input.set_text(options.context.excerpt.min_bytes.to_string(), window, cx);
         });
         self.cursor_context_ratio_input.update(cx, |input, cx| {
             input.set_text(
                 format!(
                     "{:.2}",
-                    options.excerpt.target_before_cursor_over_total_bytes
+                    options
+                        .context
+                        .excerpt
+                        .target_before_cursor_over_total_bytes
                 ),
                 window,
                 cx,
@@ -172,16 +175,12 @@ impl Zeta2Inspector {
 
         const THROTTLE_TIME: Duration = Duration::from_millis(100);
 
-        if let Some(
-            LastPredictionState::Success(prediction)
-            | LastPredictionState::Replaying { prediction, .. },
-        ) = self.last_prediction.take()
-        {
+        if let Some(prediction) = self.last_prediction.as_mut() {
             if let Some(buffer) = prediction.buffer.upgrade() {
                 let position = prediction.position;
                 let zeta = self.zeta.clone();
                 let project = self.project.clone();
-                let task = cx.spawn(async move |_this, cx| {
+                prediction._task = Some(cx.spawn(async move |_this, cx| {
                     cx.background_executor().timer(THROTTLE_TIME).await;
                     if let Some(task) = zeta
                         .update(cx, |zeta, cx| {
@@ -191,13 +190,10 @@ impl Zeta2Inspector {
                     {
                         task.await.log_err();
                     }
-                });
-                self.last_prediction = Some(LastPredictionState::Replaying {
-                    prediction,
-                    _task: task,
-                });
+                }));
+                prediction.state = LastPredictionState::Requested;
             } else {
-                self.last_prediction = Some(LastPredictionState::Failed("Buffer dropped".into()));
+                self.last_prediction.take();
             }
         }
 
@@ -236,7 +232,8 @@ impl Zeta2Inspector {
                         .unwrap_or_default()
                 }
 
-                let excerpt_options = EditPredictionExcerptOptions {
+                let mut context_options = DEFAULT_CONTEXT_OPTIONS.clone();
+                context_options.excerpt = EditPredictionExcerptOptions {
                     max_bytes: number_input_value(&this.max_excerpt_bytes_input, cx),
                     min_bytes: number_input_value(&this.min_excerpt_bytes_input, cx),
                     target_before_cursor_over_total_bytes: number_input_value(
@@ -248,7 +245,7 @@ impl Zeta2Inspector {
                 let zeta_options = this.zeta.read(cx).options();
                 this.set_options(
                     ZetaOptions {
-                        excerpt: excerpt_options,
+                        context: context_options,
                         max_prompt_bytes: number_input_value(&this.max_prompt_bytes_input, cx),
                         max_diagnostic_bytes: zeta_options.max_diagnostic_bytes,
                         prompt_format: zeta_options.prompt_format,
@@ -305,6 +302,8 @@ impl Zeta2Inspector {
 
                 this.update_in(cx, |this, window, cx| {
                     let context_editor = cx.new(|cx| {
+                        let mut excerpt_score_components = HashMap::default();
+
                         let multibuffer = cx.new(|cx| {
                             let mut multibuffer = MultiBuffer::new(language::Capability::ReadOnly);
                             let excerpt_file = Arc::new(ExcerptMetadataFile {
@@ -335,7 +334,14 @@ impl Zeta2Inspector {
                                 cx,
                             );
 
-                            for snippet in &prediction.context.declarations {
+                            let mut declarations = prediction.context.declarations.clone();
+                            declarations.sort_unstable_by_key(|declaration| {
+                                Reverse(OrderedFloat(
+                                    declaration.score(DeclarationStyle::Declaration),
+                                ))
+                            });
+
+                            for snippet in &declarations {
                                 let path = this
                                     .project
                                     .read(cx)
@@ -343,10 +349,10 @@ impl Zeta2Inspector {
 
                                 let snippet_file = Arc::new(ExcerptMetadataFile {
                                     title: RelPath::unix(&format!(
-                                        "{} (Score density: {})",
+                                        "{} (Score: {})",
                                         path.map(|p| p.path.display(path_style).to_string())
                                             .unwrap_or_else(|| "".to_string()),
-                                        snippet.score_density(DeclarationStyle::Declaration)
+                                        snippet.score(DeclarationStyle::Declaration)
                                     ))
                                     .unwrap()
                                     .into(),
@@ -366,41 +372,108 @@ impl Zeta2Inspector {
                                     buffer
                                 });
 
-                                multibuffer.push_excerpts(
+                                let excerpt_ids = multibuffer.push_excerpts(
                                     excerpt_buffer,
                                     [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
                                     cx,
                                 );
+                                let excerpt_id = excerpt_ids.first().unwrap();
+
+                                excerpt_score_components
+                                    .insert(*excerpt_id, snippet.components.clone());
                             }
 
                             multibuffer
                         });
 
-                        Editor::new(EditorMode::full(), multibuffer, None, window, cx)
+                        let mut editor =
+                            Editor::new(EditorMode::full(), multibuffer, None, window, cx);
+                        editor.register_addon(ZetaContextAddon {
+                            excerpt_score_components,
+                        });
+                        editor
+                    });
+
+                    let PredictionDebugInfo {
+                        response_rx,
+                        position,
+                        buffer,
+                        retrieval_time,
+                        local_prompt,
+                        ..
+                    } = prediction;
+
+                    let task = cx.spawn_in(window, {
+                        let markdown_language = markdown_language.clone();
+                        async move |this, cx| {
+                            let response = response_rx.await;
+
+                            this.update_in(cx, |this, window, cx| {
+                                if let Some(prediction) = this.last_prediction.as_mut() {
+                                    prediction.state = match response {
+                                        Ok(Ok(response)) => {
+                                            prediction.prompt_editor.update(
+                                                cx,
+                                                |prompt_editor, cx| {
+                                                    prompt_editor.set_text(
+                                                        response.prompt,
+                                                        window,
+                                                        cx,
+                                                    );
+                                                },
+                                            );
+
+                                            LastPredictionState::Success {
+                                                prompt_planning_time: response.prompt_planning_time,
+                                                inference_time: response.inference_time,
+                                                parsing_time: response.parsing_time,
+                                                model_response_editor: cx.new(|cx| {
+                                                    let buffer = cx.new(|cx| {
+                                                        let mut buffer = Buffer::local(
+                                                            response.model_response,
+                                                            cx,
+                                                        );
+                                                        buffer.set_language(markdown_language, cx);
+                                                        buffer
+                                                    });
+                                                    let buffer = cx.new(|cx| {
+                                                        MultiBuffer::singleton(buffer, cx)
+                                                    });
+                                                    let mut editor = Editor::new(
+                                                        EditorMode::full(),
+                                                        buffer,
+                                                        None,
+                                                        window,
+                                                        cx,
+                                                    );
+                                                    editor.set_read_only(true);
+                                                    editor.set_show_line_numbers(false, cx);
+                                                    editor.set_show_gutter(false, cx);
+                                                    editor.set_show_scrollbars(false, cx);
+                                                    editor
+                                                }),
+                                            }
+                                        }
+                                        Ok(Err(err)) => {
+                                            LastPredictionState::Failed { message: err }
+                                        }
+                                        Err(oneshot::Canceled) => LastPredictionState::Failed {
+                                            message: "Canceled".to_string(),
+                                        },
+                                    };
+                                }
+                            })
+                            .ok();
+                        }
                     });
 
-                    let last_prediction = LastPrediction {
+                    this.last_prediction = Some(LastPrediction {
                         context_editor,
                         prompt_editor: cx.new(|cx| {
-                            let buffer = cx.new(|cx| {
-                                let mut buffer = Buffer::local(prediction.request.prompt, cx);
-                                buffer.set_language(markdown_language.clone(), cx);
-                                buffer
-                            });
-                            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
-                            let mut editor =
-                                Editor::new(EditorMode::full(), buffer, None, window, cx);
-                            editor.set_read_only(true);
-                            editor.set_show_line_numbers(false, cx);
-                            editor.set_show_gutter(false, cx);
-                            editor.set_show_scrollbars(false, cx);
-                            editor
-                        }),
-                        model_response_editor: cx.new(|cx| {
                             let buffer = cx.new(|cx| {
                                 let mut buffer =
-                                    Buffer::local(prediction.request.model_response, cx);
-                                buffer.set_language(markdown_language, cx);
+                                    Buffer::local(local_prompt.unwrap_or_else(|err| err), cx);
+                                buffer.set_language(markdown_language.clone(), cx);
                                 buffer
                             });
                             let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
@@ -412,14 +485,12 @@ impl Zeta2Inspector {
                             editor.set_show_scrollbars(false, cx);
                             editor
                         }),
-                        retrieval_time: prediction.retrieval_time,
-                        prompt_planning_time: prediction.request.prompt_planning_time,
-                        inference_time: prediction.request.inference_time,
-                        parsing_time: prediction.request.parsing_time,
-                        buffer: prediction.buffer,
-                        position: prediction.position,
-                    };
-                    this.last_prediction = Some(LastPredictionState::Success(last_prediction));
+                        retrieval_time,
+                        buffer,
+                        position,
+                        state: LastPredictionState::Requested,
+                        _task: Some(task),
+                    });
                     cx.notify();
                 })
                 .ok();
@@ -510,9 +581,7 @@ impl Zeta2Inspector {
     }
 
     fn render_tabs(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
-        let Some(LastPredictionState::Success { .. } | LastPredictionState::Replaying { .. }) =
-            self.last_prediction.as_ref()
-        else {
+        if self.last_prediction.is_none() {
             return None;
         };
 
@@ -547,14 +616,26 @@ impl Zeta2Inspector {
     }
 
     fn render_stats(&self) -> Option<Div> {
-        let Some(
-            LastPredictionState::Success(prediction)
-            | LastPredictionState::Replaying { prediction, .. },
-        ) = self.last_prediction.as_ref()
-        else {
+        let Some(prediction) = self.last_prediction.as_ref() else {
             return None;
         };
 
+        let (prompt_planning_time, inference_time, parsing_time) = match &prediction.state {
+            LastPredictionState::Success {
+                inference_time,
+                parsing_time,
+                prompt_planning_time,
+                ..
+            } => (
+                Some(*prompt_planning_time),
+                Some(*inference_time),
+                Some(*parsing_time),
+            ),
+            LastPredictionState::Requested | LastPredictionState::Failed { .. } => {
+                (None, None, None)
+            }
+        };
+
         Some(
             v_flex()
                 .p_4()
@@ -563,32 +644,30 @@ impl Zeta2Inspector {
                 .child(Headline::new("Stats").size(HeadlineSize::Small))
                 .child(Self::render_duration(
                     "Context retrieval",
-                    prediction.retrieval_time,
+                    Some(prediction.retrieval_time),
                 ))
                 .child(Self::render_duration(
                     "Prompt planning",
-                    prediction.prompt_planning_time,
-                ))
-                .child(Self::render_duration(
-                    "Inference",
-                    prediction.inference_time,
+                    prompt_planning_time,
                 ))
-                .child(Self::render_duration("Parsing", prediction.parsing_time)),
+                .child(Self::render_duration("Inference", inference_time))
+                .child(Self::render_duration("Parsing", parsing_time)),
         )
     }
 
-    fn render_duration(name: &'static str, time: chrono::TimeDelta) -> Div {
+    fn render_duration(name: &'static str, time: Option<chrono::TimeDelta>) -> Div {
         h_flex()
             .gap_1()
             .child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
-            .child(
-                Label::new(if time.num_microseconds().unwrap_or(0) >= 1000 {
+            .child(match time {
+                Some(time) => Label::new(if time.num_microseconds().unwrap_or(0) >= 1000 {
                     format!("{} ms", time.num_milliseconds())
                 } else {
                     format!("{} µs", time.num_microseconds().unwrap_or(0))
                 })
                 .size(LabelSize::Small),
-            )
+                None => Label::new("...").size(LabelSize::Small),
+            })
     }
 
     fn render_content(&self, cx: &mut Context<Self>) -> AnyElement {
@@ -599,18 +678,7 @@ impl Zeta2Inspector {
                 .items_center()
                 .child(Label::new("No prediction").size(LabelSize::Large))
                 .into_any(),
-            Some(LastPredictionState::Success(prediction)) => {
-                self.render_last_prediction(prediction, cx).into_any()
-            }
-            Some(LastPredictionState::Replaying { prediction, _task }) => self
-                .render_last_prediction(prediction, cx)
-                .opacity(0.6)
-                .into_any(),
-            Some(LastPredictionState::Failed(err)) => v_flex()
-                .p_4()
-                .gap_2()
-                .child(Label::new(err.clone()).buffer_font(cx))
-                .into_any(),
+            Some(prediction) => self.render_last_prediction(prediction, cx).into_any(),
         }
     }
 
@@ -630,7 +698,20 @@ impl Zeta2Inspector {
                         .gap_2()
                         .p_4()
                         .h_full()
-                        .child(ui::Headline::new("Prompt").size(ui::HeadlineSize::XSmall))
+                        .child(
+                            h_flex()
+                                .justify_between()
+                                .child(ui::Headline::new("Prompt").size(ui::HeadlineSize::XSmall))
+                                .child(match prediction.state {
+                                    LastPredictionState::Requested
+                                    | LastPredictionState::Failed { .. } => ui::Chip::new("Local")
+                                        .bg_color(cx.theme().status().warning_background)
+                                        .label_color(Color::Success),
+                                    LastPredictionState::Success { .. } => ui::Chip::new("Cloud")
+                                        .bg_color(cx.theme().status().success_background)
+                                        .label_color(Color::Success),
+                                }),
+                        )
                         .child(prediction.prompt_editor.clone()),
                 )
                 .child(ui::vertical_divider())
@@ -641,7 +722,22 @@ impl Zeta2Inspector {
                         .h_full()
                         .p_4()
                         .child(ui::Headline::new("Model Response").size(ui::HeadlineSize::XSmall))
-                        .child(prediction.model_response_editor.clone()),
+                        .child(match &prediction.state {
+                            LastPredictionState::Success {
+                                model_response_editor,
+                                ..
+                            } => model_response_editor.clone().into_any_element(),
+                            LastPredictionState::Requested => v_flex()
+                                .p_4()
+                                .gap_2()
+                                .child(Label::new("Loading...").buffer_font(cx))
+                                .into_any(),
+                            LastPredictionState::Failed { message } => v_flex()
+                                .p_4()
+                                .gap_2()
+                                .child(Label::new(message.clone()).buffer_font(cx))
+                                .into_any(),
+                        }),
                 ),
         }
     }
@@ -733,3 +829,58 @@ impl language::File for ExcerptMetadataFile {
         false
     }
 }
+
+struct ZetaContextAddon {
+    excerpt_score_components: HashMap<editor::ExcerptId, DeclarationScoreComponents>,
+}
+
+impl editor::Addon for ZetaContextAddon {
+    fn to_any(&self) -> &dyn std::any::Any {
+        self
+    }
+
+    fn render_buffer_header_controls(
+        &self,
+        excerpt_info: &multi_buffer::ExcerptInfo,
+        _window: &Window,
+        _cx: &App,
+    ) -> Option<AnyElement> {
+        let score_components = self.excerpt_score_components.get(&excerpt_info.id)?.clone();
+
+        Some(
+            div()
+                .id(excerpt_info.id.to_proto() as usize)
+                .child(ui::Icon::new(IconName::Info))
+                .cursor(CursorStyle::PointingHand)
+                .tooltip(move |_, cx| {
+                    cx.new(|_| ScoreComponentsTooltip::new(&score_components))
+                        .into()
+                })
+                .into_any(),
+        )
+    }
+}
+
+struct ScoreComponentsTooltip {
+    text: SharedString,
+}
+
+impl ScoreComponentsTooltip {
+    fn new(components: &DeclarationScoreComponents) -> Self {
+        Self {
+            text: format!("{:#?}", components).into(),
+        }
+    }
+}
+
+impl Render for ScoreComponentsTooltip {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        div().pl_2().pt_2p5().child(
+            div()
+                .elevation_2(cx)
+                .py_1()
+                .px_2()
+                .child(ui::Label::new(self.text.clone()).buffer_font(cx)),
+        )
+    }
+}

crates/zeta_cli/Cargo.toml 🔗

@@ -18,6 +18,7 @@ clap.workspace = true
 client.workspace = true
 cloud_llm_client.workspace= true
 cloud_zeta2_prompt.workspace= true
+collections.workspace = true
 debug_adapter_extension.workspace = true
 edit_prediction_context.workspace = true
 extension.workspace = true
@@ -32,6 +33,7 @@ language_models.workspace = true
 languages = { workspace = true, features = ["load-grammars"] }
 log.workspace = true
 node_runtime.workspace = true
+ordered-float.workspace = true
 paths.workspace = true
 project.workspace = true
 prompt_store.workspace = true
@@ -49,4 +51,3 @@ workspace-hack.workspace = true
 zeta.workspace = true
 zeta2.workspace = true
 zlog.workspace = true
-ordered-float.workspace = true

crates/zeta_cli/src/main.rs 🔗

@@ -1,33 +1,40 @@
 mod headless;
 
-use anyhow::{Result, anyhow};
+use anyhow::{Context as _, Result, anyhow};
 use clap::{Args, Parser, Subcommand};
-use cloud_llm_client::predict_edits_v3;
+use cloud_llm_client::predict_edits_v3::{self, DeclarationScoreComponents};
 use edit_prediction_context::{
-    Declaration, EditPredictionContext, EditPredictionExcerptOptions, Identifier, ReferenceRegion,
-    SyntaxIndex, references_in_range,
+    Declaration, DeclarationStyle, EditPredictionContext, EditPredictionContextOptions,
+    EditPredictionExcerptOptions, EditPredictionScoreOptions, Identifier, Imports, Reference,
+    ReferenceRegion, SyntaxIndex, SyntaxIndexState, references_in_range,
 };
 use futures::channel::mpsc;
 use futures::{FutureExt as _, StreamExt as _};
 use gpui::{AppContext, Application, AsyncApp};
 use gpui::{Entity, Task};
-use language::{Bias, LanguageServerId};
+use language::{Bias, BufferSnapshot, LanguageServerId, Point};
 use language::{Buffer, OffsetRangeExt};
-use language::{LanguageId, Point};
+use language::{LanguageId, ParseStatus};
 use language_model::LlmApiToken;
 use ordered_float::OrderedFloat;
-use project::{Project, ProjectPath, Worktree};
+use project::{Project, ProjectEntryId, ProjectPath, Worktree};
 use release_channel::AppVersion;
 use reqwest_client::ReqwestClient;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use serde_json::json;
 use std::cmp::Reverse;
 use std::collections::{HashMap, HashSet};
+use std::fmt::{self, Display};
+use std::fs::File;
+use std::hash::Hash;
+use std::hash::Hasher;
 use std::io::Write as _;
 use std::ops::Range;
 use std::path::{Path, PathBuf};
 use std::process::exit;
 use std::str::FromStr;
-use std::sync::Arc;
+use std::sync::atomic::AtomicUsize;
+use std::sync::{Arc, atomic};
 use std::time::Duration;
 use util::paths::PathStyle;
 use util::rel_path::RelPath;
@@ -59,10 +66,16 @@ enum Commands {
         context_args: Option<ContextArgs>,
     },
     RetrievalStats {
+        #[clap(flatten)]
+        zeta2_args: Zeta2Args,
         #[arg(long)]
         worktree: PathBuf,
-        #[arg(long, default_value_t = 42)]
-        file_indexing_parallelism: usize,
+        #[arg(long)]
+        extension: Option<String>,
+        #[arg(long)]
+        limit: Option<usize>,
+        #[arg(long)]
+        skip: Option<usize>,
     },
 }
 
@@ -72,7 +85,7 @@ struct ContextArgs {
     #[arg(long)]
     worktree: PathBuf,
     #[arg(long)]
-    cursor: CursorPosition,
+    cursor: SourceLocation,
     #[arg(long)]
     use_language_server: bool,
     #[arg(long)]
@@ -97,6 +110,8 @@ struct Zeta2Args {
     output_format: OutputFormat,
     #[arg(long, default_value_t = 42)]
     file_indexing_parallelism: usize,
+    #[arg(long, default_value_t = false)]
+    disable_imports_gathering: bool,
 }
 
 #[derive(clap::ValueEnum, Default, Debug, Clone)]
@@ -151,20 +166,51 @@ impl FromStr for FileOrStdin {
     }
 }
 
-#[derive(Debug, Clone)]
-struct CursorPosition {
+#[derive(Debug, Clone, Hash, Eq, PartialEq)]
+struct SourceLocation {
     path: Arc<RelPath>,
     point: Point,
 }
 
-impl FromStr for CursorPosition {
+impl Serialize for SourceLocation {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        serializer.serialize_str(&self.to_string())
+    }
+}
+
+impl<'de> Deserialize<'de> for SourceLocation {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        s.parse().map_err(serde::de::Error::custom)
+    }
+}
+
+impl Display for SourceLocation {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "{}:{}:{}",
+            self.path.display(PathStyle::Posix),
+            self.point.row + 1,
+            self.point.column + 1
+        )
+    }
+}
+
+impl FromStr for SourceLocation {
     type Err = anyhow::Error;
 
     fn from_str(s: &str) -> Result<Self> {
         let parts: Vec<&str> = s.split(':').collect();
         if parts.len() != 3 {
             return Err(anyhow!(
-                "Invalid cursor format. Expected 'file.rs:line:column', got '{}'",
+                "Invalid source location. Expected 'file.rs:line:column', got '{}'",
                 s
             ));
         }
@@ -180,7 +226,7 @@ impl FromStr for CursorPosition {
         // Convert from 1-based to 0-based indexing
         let point = Point::new(line.saturating_sub(1), column.saturating_sub(1));
 
-        Ok(CursorPosition { path, point })
+        Ok(SourceLocation { path, point })
     }
 }
 
@@ -225,16 +271,17 @@ async fn get_context(
     let mut ready_languages = HashSet::default();
     let (_lsp_open_handle, buffer) = if use_language_server {
         let (lsp_open_handle, _, buffer) = open_buffer_with_language_server(
-            &project,
-            &worktree,
-            &cursor.path,
+            project.clone(),
+            worktree.clone(),
+            cursor.path.clone(),
             &mut ready_languages,
             cx,
         )
         .await?;
         (Some(lsp_open_handle), buffer)
     } else {
-        let buffer = open_buffer(&project, &worktree, &cursor.path, cx).await?;
+        let buffer =
+            open_buffer(project.clone(), worktree.clone(), cursor.path.clone(), cx).await?;
         (None, buffer)
     };
 
@@ -281,18 +328,7 @@ async fn get_context(
                     zeta2::Zeta::new(app_state.client.clone(), app_state.user_store.clone(), cx)
                 });
                 let indexing_done_task = zeta.update(cx, |zeta, cx| {
-                    zeta.set_options(zeta2::ZetaOptions {
-                        excerpt: EditPredictionExcerptOptions {
-                            max_bytes: zeta2_args.max_excerpt_bytes,
-                            min_bytes: zeta2_args.min_excerpt_bytes,
-                            target_before_cursor_over_total_bytes: zeta2_args
-                                .target_before_cursor_over_total_bytes,
-                        },
-                        max_diagnostic_bytes: zeta2_args.max_diagnostic_bytes,
-                        max_prompt_bytes: zeta2_args.max_prompt_bytes,
-                        prompt_format: zeta2_args.prompt_format.into(),
-                        file_indexing_parallelism: zeta2_args.file_indexing_parallelism,
-                    });
+                    zeta.set_options(zeta2_args.to_options(true));
                     zeta.register_buffer(&buffer, &project, cx);
                     zeta.wait_for_initial_indexing(&project, cx)
                 });
@@ -340,12 +376,39 @@ async fn get_context(
     }
 }
 
+impl Zeta2Args {
+    fn to_options(&self, omit_excerpt_overlaps: bool) -> zeta2::ZetaOptions {
+        zeta2::ZetaOptions {
+            context: EditPredictionContextOptions {
+                use_imports: !self.disable_imports_gathering,
+                excerpt: EditPredictionExcerptOptions {
+                    max_bytes: self.max_excerpt_bytes,
+                    min_bytes: self.min_excerpt_bytes,
+                    target_before_cursor_over_total_bytes: self
+                        .target_before_cursor_over_total_bytes,
+                },
+                score: EditPredictionScoreOptions {
+                    omit_excerpt_overlaps,
+                },
+            },
+            max_diagnostic_bytes: self.max_diagnostic_bytes,
+            max_prompt_bytes: self.max_prompt_bytes,
+            prompt_format: self.prompt_format.clone().into(),
+            file_indexing_parallelism: self.file_indexing_parallelism,
+        }
+    }
+}
+
 pub async fn retrieval_stats(
     worktree: PathBuf,
-    file_indexing_parallelism: usize,
     app_state: Arc<ZetaCliAppState>,
+    only_extension: Option<String>,
+    file_limit: Option<usize>,
+    skip_files: Option<usize>,
+    options: zeta2::ZetaOptions,
     cx: &mut AsyncApp,
 ) -> Result<String> {
+    let options = Arc::new(options);
     let worktree_path = worktree.canonicalize()?;
 
     let project = cx.update(|cx| {
@@ -365,7 +428,6 @@ pub async fn retrieval_stats(
             project.create_worktree(&worktree_path, true, cx)
         })?
         .await?;
-    let worktree_id = worktree.read_with(cx, |worktree, _cx| worktree.id())?;
 
     // wait for worktree scan so that wait_for_initial_file_indexing waits for the whole worktree.
     worktree
@@ -374,21 +436,492 @@ pub async fn retrieval_stats(
         })?
         .await;
 
-    let index = cx.new(|cx| SyntaxIndex::new(&project, file_indexing_parallelism, cx))?;
+    let index = cx.new(|cx| SyntaxIndex::new(&project, options.file_indexing_parallelism, cx))?;
     index
         .read_with(cx, |index, cx| index.wait_for_initial_file_indexing(cx))?
         .await?;
-    let files = index
+    let indexed_files = index
         .read_with(cx, |index, cx| index.indexed_file_paths(cx))?
-        .await
+        .await;
+    let mut filtered_files = indexed_files
         .into_iter()
         .filter(|project_path| {
-            project_path
-                .path
-                .extension()
-                .is_some_and(|extension| !["md", "json", "sh", "diff"].contains(&extension))
+            let file_extension = project_path.path.extension();
+            if let Some(only_extension) = only_extension.as_ref() {
+                file_extension.is_some_and(|extension| extension == only_extension)
+            } else {
+                file_extension
+                    .is_some_and(|extension| !["md", "json", "sh", "diff"].contains(&extension))
+            }
         })
         .collect::<Vec<_>>();
+    filtered_files.sort_by(|a, b| a.path.cmp(&b.path));
+
+    let index_state = index.read_with(cx, |index, _cx| index.state().clone())?;
+    cx.update(|_| {
+        drop(index);
+    })?;
+    let index_state = Arc::new(
+        Arc::into_inner(index_state)
+            .context("Index state had more than 1 reference")?
+            .into_inner(),
+    );
+
+    struct FileSnapshot {
+        project_entry_id: ProjectEntryId,
+        snapshot: BufferSnapshot,
+        hash: u64,
+        parent_abs_path: Arc<Path>,
+    }
+
+    let files: Vec<FileSnapshot> = futures::future::try_join_all({
+        filtered_files
+            .iter()
+            .map(|file| {
+                let buffer_task =
+                    open_buffer(project.clone(), worktree.clone(), file.path.clone(), cx);
+                cx.spawn(async move |cx| {
+                    let buffer = buffer_task.await?;
+                    let (project_entry_id, parent_abs_path, snapshot) =
+                        buffer.read_with(cx, |buffer, cx| {
+                            let file = project::File::from_dyn(buffer.file()).unwrap();
+                            let project_entry_id = file.project_entry_id().unwrap();
+                            let mut parent_abs_path = file.worktree.read(cx).absolutize(&file.path);
+                            if !parent_abs_path.pop() {
+                                panic!("Invalid worktree path");
+                            }
+
+                            (project_entry_id, parent_abs_path, buffer.snapshot())
+                        })?;
+
+                    anyhow::Ok(
+                        cx.background_spawn(async move {
+                            let mut hasher = collections::FxHasher::default();
+                            snapshot.text().hash(&mut hasher);
+                            FileSnapshot {
+                                project_entry_id,
+                                snapshot,
+                                hash: hasher.finish(),
+                                parent_abs_path: parent_abs_path.into(),
+                            }
+                        })
+                        .await,
+                    )
+                })
+            })
+            .collect::<Vec<_>>()
+    })
+    .await?;
+
+    let mut file_snapshots = HashMap::default();
+    let mut hasher = collections::FxHasher::default();
+    for FileSnapshot {
+        project_entry_id,
+        snapshot,
+        hash,
+        ..
+    } in &files
+    {
+        file_snapshots.insert(*project_entry_id, snapshot.clone());
+        hash.hash(&mut hasher);
+    }
+    let files_hash = hasher.finish();
+    let file_snapshots = Arc::new(file_snapshots);
+
+    let lsp_definitions_path = std::env::current_dir()?.join(format!(
+        "target/zeta2-lsp-definitions-{:x}.json",
+        files_hash
+    ));
+
+    let lsp_definitions: Arc<_> = if std::fs::exists(&lsp_definitions_path)? {
+        log::info!(
+            "Using cached LSP definitions from {}",
+            lsp_definitions_path.display()
+        );
+        serde_json::from_reader(File::open(&lsp_definitions_path)?)?
+    } else {
+        log::warn!(
+            "No LSP definitions found populating {}",
+            lsp_definitions_path.display()
+        );
+        let lsp_definitions =
+            gather_lsp_definitions(&filtered_files, &worktree, &project, cx).await?;
+        serde_json::to_writer_pretty(File::create(&lsp_definitions_path)?, &lsp_definitions)?;
+        lsp_definitions
+    }
+    .into();
+
+    let files_len = files.len().min(file_limit.unwrap_or(usize::MAX));
+    let done_count = Arc::new(AtomicUsize::new(0));
+
+    let (output_tx, mut output_rx) = mpsc::unbounded::<RetrievalStatsResult>();
+    let mut output = std::fs::File::create("target/zeta-retrieval-stats.txt")?;
+
+    let tasks = files
+        .into_iter()
+        .skip(skip_files.unwrap_or(0))
+        .take(file_limit.unwrap_or(usize::MAX))
+        .map(|project_file| {
+            let index_state = index_state.clone();
+            let lsp_definitions = lsp_definitions.clone();
+            let options = options.clone();
+            let output_tx = output_tx.clone();
+            let done_count = done_count.clone();
+            let file_snapshots = file_snapshots.clone();
+            cx.background_spawn(async move {
+                let snapshot = project_file.snapshot;
+
+                let full_range = 0..snapshot.len();
+                let references = references_in_range(
+                    full_range,
+                    &snapshot.text(),
+                    ReferenceRegion::Nearby,
+                    &snapshot,
+                );
+
+                println!("references: {}", references.len(),);
+
+                let imports = if options.context.use_imports {
+                    Imports::gather(&snapshot, Some(&project_file.parent_abs_path))
+                } else {
+                    Imports::default()
+                };
+
+                let path = snapshot.file().unwrap().path();
+
+                for reference in references {
+                    let query_point = snapshot.offset_to_point(reference.range.start);
+                    let source_location = SourceLocation {
+                        path: path.clone(),
+                        point: query_point,
+                    };
+                    let lsp_definitions = lsp_definitions
+                        .definitions
+                        .get(&source_location)
+                        .cloned()
+                        .unwrap_or_else(|| {
+                            log::warn!(
+                                "No definitions found for source location: {:?}",
+                                source_location
+                            );
+                            Vec::new()
+                        });
+
+                    let retrieve_result = retrieve_definitions(
+                        &reference,
+                        &imports,
+                        query_point,
+                        &snapshot,
+                        &index_state,
+                        &file_snapshots,
+                        &options,
+                    )
+                    .await?;
+
+                    // TODO: LSP returns things like locals, this filters out some of those, but potentially
+                    // hides some retrieval issues.
+                    if retrieve_result.definitions.is_empty() {
+                        continue;
+                    }
+
+                    let mut best_match = None;
+                    let mut has_external_definition = false;
+                    let mut in_excerpt = false;
+                    for (index, retrieved_definition) in
+                        retrieve_result.definitions.iter().enumerate()
+                    {
+                        for lsp_definition in &lsp_definitions {
+                            let SourceRange {
+                                path,
+                                point_range,
+                                offset_range,
+                            } = lsp_definition;
+                            let lsp_point_range =
+                                SerializablePoint::into_language_point_range(point_range.clone());
+                            has_external_definition = has_external_definition
+                                || path.is_absolute()
+                                || path
+                                    .components()
+                                    .any(|component| component.as_os_str() == "node_modules");
+                            let is_match = path.as_path()
+                                == retrieved_definition.path.as_std_path()
+                                && retrieved_definition
+                                    .range
+                                    .contains_inclusive(&lsp_point_range);
+                            if is_match {
+                                if best_match.is_none() {
+                                    best_match = Some(index);
+                                }
+                            }
+                            in_excerpt = in_excerpt
+                                || retrieve_result.excerpt_range.as_ref().is_some_and(
+                                    |excerpt_range| excerpt_range.contains_inclusive(&offset_range),
+                                );
+                        }
+                    }
+
+                    let outcome = if let Some(best_match) = best_match {
+                        RetrievalOutcome::Match { best_match }
+                    } else if has_external_definition {
+                        RetrievalOutcome::NoMatchDueToExternalLspDefinitions
+                    } else if in_excerpt {
+                        RetrievalOutcome::ProbablyLocal
+                    } else {
+                        RetrievalOutcome::NoMatch
+                    };
+
+                    let result = RetrievalStatsResult {
+                        outcome,
+                        path: path.clone(),
+                        identifier: reference.identifier,
+                        point: query_point,
+                        lsp_definitions,
+                        retrieved_definitions: retrieve_result.definitions,
+                    };
+
+                    output_tx.unbounded_send(result).ok();
+                }
+
+                println!(
+                    "{:02}/{:02} done",
+                    done_count.fetch_add(1, atomic::Ordering::Relaxed) + 1,
+                    files_len,
+                );
+
+                anyhow::Ok(())
+            })
+        })
+        .collect::<Vec<_>>();
+
+    drop(output_tx);
+
+    let results_task = cx.background_spawn(async move {
+        let mut results = Vec::new();
+        while let Some(result) = output_rx.next().await {
+            output
+                .write_all(format!("{:#?}\n", result).as_bytes())
+                .log_err();
+            results.push(result)
+        }
+        results
+    });
+
+    futures::future::try_join_all(tasks).await?;
+    println!("Tasks completed");
+    let results = results_task.await;
+    println!("Results received");
+
+    let mut references_count = 0;
+
+    let mut included_count = 0;
+    let mut both_absent_count = 0;
+
+    let mut retrieved_count = 0;
+    let mut top_match_count = 0;
+    let mut non_top_match_count = 0;
+    let mut ranking_involved_top_match_count = 0;
+
+    let mut no_match_count = 0;
+    let mut no_match_none_retrieved = 0;
+    let mut no_match_wrong_retrieval = 0;
+
+    let mut expected_no_match_count = 0;
+    let mut in_excerpt_count = 0;
+    let mut external_definition_count = 0;
+
+    for result in results {
+        references_count += 1;
+        match &result.outcome {
+            RetrievalOutcome::Match { best_match } => {
+                included_count += 1;
+                retrieved_count += 1;
+                let multiple = result.retrieved_definitions.len() > 1;
+                if *best_match == 0 {
+                    top_match_count += 1;
+                    if multiple {
+                        ranking_involved_top_match_count += 1;
+                    }
+                } else {
+                    non_top_match_count += 1;
+                }
+            }
+            RetrievalOutcome::NoMatch => {
+                if result.lsp_definitions.is_empty() {
+                    included_count += 1;
+                    both_absent_count += 1;
+                } else {
+                    no_match_count += 1;
+                    if result.retrieved_definitions.is_empty() {
+                        no_match_none_retrieved += 1;
+                    } else {
+                        no_match_wrong_retrieval += 1;
+                    }
+                }
+            }
+            RetrievalOutcome::NoMatchDueToExternalLspDefinitions => {
+                expected_no_match_count += 1;
+                external_definition_count += 1;
+            }
+            RetrievalOutcome::ProbablyLocal => {
+                included_count += 1;
+                in_excerpt_count += 1;
+            }
+        }
+    }
+
+    fn count_and_percentage(part: usize, total: usize) -> String {
+        format!("{} ({:.2}%)", part, (part as f64 / total as f64) * 100.0)
+    }
+
+    println!("");
+    println!("╮ references: {}", references_count);
+    println!(
+        "├─╮ included: {}",
+        count_and_percentage(included_count, references_count),
+    );
+    println!(
+        "│ ├─╮ retrieved: {}",
+        count_and_percentage(retrieved_count, references_count)
+    );
+    println!(
+        "│ │ ├─╮ top match : {}",
+        count_and_percentage(top_match_count, retrieved_count)
+    );
+    println!(
+        "│ │ │ ╰─╴ involving ranking: {}",
+        count_and_percentage(ranking_involved_top_match_count, top_match_count)
+    );
+    println!(
+        "│ │ ╰─╴ non-top match: {}",
+        count_and_percentage(non_top_match_count, retrieved_count)
+    );
+    println!(
+        "│ ├─╴ both absent: {}",
+        count_and_percentage(both_absent_count, included_count)
+    );
+    println!(
+        "│ ╰─╴ in excerpt: {}",
+        count_and_percentage(in_excerpt_count, included_count)
+    );
+    println!(
+        "├─╮ no match: {}",
+        count_and_percentage(no_match_count, references_count)
+    );
+    println!(
+        "│ ├─╴ none retrieved: {}",
+        count_and_percentage(no_match_none_retrieved, no_match_count)
+    );
+    println!(
+        "│ ╰─╴ wrong retrieval: {}",
+        count_and_percentage(no_match_wrong_retrieval, no_match_count)
+    );
+    println!(
+        "╰─╮ expected no match: {}",
+        count_and_percentage(expected_no_match_count, references_count)
+    );
+    println!(
+        "  ╰─╴ external definition: {}",
+        count_and_percentage(external_definition_count, expected_no_match_count)
+    );
+
+    println!("");
+    println!("LSP definition cache at {}", lsp_definitions_path.display());
+
+    Ok("".to_string())
+}
+
+struct RetrieveResult {
+    definitions: Vec<RetrievedDefinition>,
+    excerpt_range: Option<Range<usize>>,
+}
+
+async fn retrieve_definitions(
+    reference: &Reference,
+    imports: &Imports,
+    query_point: Point,
+    snapshot: &BufferSnapshot,
+    index: &Arc<SyntaxIndexState>,
+    file_snapshots: &Arc<HashMap<ProjectEntryId, BufferSnapshot>>,
+    options: &Arc<zeta2::ZetaOptions>,
+) -> Result<RetrieveResult> {
+    let mut single_reference_map = HashMap::default();
+    single_reference_map.insert(reference.identifier.clone(), vec![reference.clone()]);
+    let edit_prediction_context = EditPredictionContext::gather_context_with_references_fn(
+        query_point,
+        snapshot,
+        imports,
+        &options.context,
+        Some(&index),
+        |_, _, _| single_reference_map,
+    );
+
+    let Some(edit_prediction_context) = edit_prediction_context else {
+        return Ok(RetrieveResult {
+            definitions: Vec::new(),
+            excerpt_range: None,
+        });
+    };
+
+    let mut retrieved_definitions = Vec::new();
+    for scored_declaration in edit_prediction_context.declarations {
+        match &scored_declaration.declaration {
+            Declaration::File {
+                project_entry_id,
+                declaration,
+                ..
+            } => {
+                let Some(snapshot) = file_snapshots.get(&project_entry_id) else {
+                    log::error!("bug: file project entry not found");
+                    continue;
+                };
+                let path = snapshot.file().unwrap().path().clone();
+                retrieved_definitions.push(RetrievedDefinition {
+                    path,
+                    range: snapshot.offset_to_point(declaration.item_range.start)
+                        ..snapshot.offset_to_point(declaration.item_range.end),
+                    score: scored_declaration.score(DeclarationStyle::Declaration),
+                    retrieval_score: scored_declaration.retrieval_score(),
+                    components: scored_declaration.components,
+                });
+            }
+            Declaration::Buffer {
+                project_entry_id,
+                rope,
+                declaration,
+                ..
+            } => {
+                let Some(snapshot) = file_snapshots.get(&project_entry_id) else {
+                    // This case happens when dependency buffers have been opened by
+                    // go-to-definition, resulting in single-file worktrees.
+                    continue;
+                };
+                let path = snapshot.file().unwrap().path().clone();
+                retrieved_definitions.push(RetrievedDefinition {
+                    path,
+                    range: rope.offset_to_point(declaration.item_range.start)
+                        ..rope.offset_to_point(declaration.item_range.end),
+                    score: scored_declaration.score(DeclarationStyle::Declaration),
+                    retrieval_score: scored_declaration.retrieval_score(),
+                    components: scored_declaration.components,
+                });
+            }
+        }
+    }
+    retrieved_definitions.sort_by_key(|definition| Reverse(OrderedFloat(definition.score)));
+
+    Ok(RetrieveResult {
+        definitions: retrieved_definitions,
+        excerpt_range: Some(edit_prediction_context.excerpt.range),
+    })
+}
+
+async fn gather_lsp_definitions(
+    files: &[ProjectPath],
+    worktree: &Entity<Worktree>,
+    project: &Entity<Project>,
+    cx: &mut AsyncApp,
+) -> Result<LspResults> {
+    let worktree_id = worktree.read_with(cx, |worktree, _cx| worktree.id())?;
 
     let lsp_store = project.read_with(cx, |project, _cx| project.lsp_store())?;
     cx.subscribe(&lsp_store, {
@@ -410,24 +943,22 @@ pub async fn retrieval_stats(
     })?
     .detach();
 
+    let mut definitions = HashMap::default();
+    let mut error_count = 0;
     let mut lsp_open_handles = Vec::new();
-    let mut output = std::fs::File::create("retrieval-stats.txt")?;
-    let mut results = Vec::new();
     let mut ready_languages = HashSet::default();
     for (file_index, project_path) in files.iter().enumerate() {
-        let processing_file_message = format!(
+        println!(
             "Processing file {} of {}: {}",
             file_index + 1,
             files.len(),
             project_path.path.display(PathStyle::Posix)
         );
-        println!("{}", processing_file_message);
-        write!(output, "{processing_file_message}\n\n").ok();
 
         let Some((lsp_open_handle, language_server_id, buffer)) = open_buffer_with_language_server(
-            &project,
-            &worktree,
-            &project_path.path,
+            project.clone(),
+            worktree.clone(),
+            project_path.path.clone(),
             &mut ready_languages,
             cx,
         )
@@ -463,273 +994,182 @@ pub async fn retrieval_stats(
                 .await;
         }
 
-        let index = index.read_with(cx, |index, _cx| index.state().clone())?;
-        let index = index.lock().await;
         for reference in references {
-            let query_point = snapshot.offset_to_point(reference.range.start);
-            let mut single_reference_map = HashMap::default();
-            single_reference_map.insert(reference.identifier.clone(), vec![reference.clone()]);
-            let edit_prediction_context = EditPredictionContext::gather_context_with_references_fn(
-                query_point,
-                &snapshot,
-                &zeta2::DEFAULT_EXCERPT_OPTIONS,
-                Some(&index),
-                |_, _, _| single_reference_map,
-            );
-
-            let Some(edit_prediction_context) = edit_prediction_context else {
-                let result = RetrievalStatsResult {
-                    identifier: reference.identifier,
-                    point: query_point,
-                    outcome: RetrievalStatsOutcome::NoExcerpt,
-                };
-                write!(output, "{:?}\n\n", result)?;
-                results.push(result);
-                continue;
-            };
-
-            let mut retrieved_definitions = Vec::new();
-            for scored_declaration in edit_prediction_context.declarations {
-                match &scored_declaration.declaration {
-                    Declaration::File {
-                        project_entry_id,
-                        declaration,
-                    } => {
-                        let Some(path) = worktree.read_with(cx, |worktree, _cx| {
-                            worktree
-                                .entry_for_id(*project_entry_id)
-                                .map(|entry| entry.path.clone())
-                        })?
-                        else {
-                            log::error!("bug: file project entry not found");
-                            continue;
-                        };
-                        let project_path = ProjectPath {
-                            worktree_id,
-                            path: path.clone(),
-                        };
-                        let buffer = project
-                            .update(cx, |project, cx| project.open_buffer(project_path, cx))?
-                            .await?;
-                        let rope = buffer.read_with(cx, |buffer, _cx| buffer.as_rope().clone())?;
-                        retrieved_definitions.push((
-                            path,
-                            rope.offset_to_point(declaration.item_range.start)
-                                ..rope.offset_to_point(declaration.item_range.end),
-                            scored_declaration.scores.declaration,
-                            scored_declaration.scores.retrieval,
-                        ));
-                    }
-                    Declaration::Buffer {
-                        project_entry_id,
-                        rope,
-                        declaration,
-                        ..
-                    } => {
-                        let Some(path) = worktree.read_with(cx, |worktree, _cx| {
-                            worktree
-                                .entry_for_id(*project_entry_id)
-                                .map(|entry| entry.path.clone())
-                        })?
-                        else {
-                            // This case happens when dependency buffers have been opened by
-                            // go-to-definition, resulting in single-file worktrees.
-                            continue;
-                        };
-                        retrieved_definitions.push((
-                            path,
-                            rope.offset_to_point(declaration.item_range.start)
-                                ..rope.offset_to_point(declaration.item_range.end),
-                            scored_declaration.scores.declaration,
-                            scored_declaration.scores.retrieval,
-                        ));
-                    }
-                }
-            }
-            retrieved_definitions
-                .sort_by_key(|(_, _, _, retrieval_score)| Reverse(OrderedFloat(*retrieval_score)));
-
-            // TODO: Consider still checking language server in this case, or having a mode for
-            // this. For now assuming that the purpose of this is to refine the ranking rather than
-            // refining whether the definition is present at all.
-            if retrieved_definitions.is_empty() {
-                continue;
-            }
-
             // TODO: Rename declaration to definition in edit_prediction_context?
             let lsp_result = project
                 .update(cx, |project, cx| {
                     project.definitions(&buffer, reference.range.start, cx)
                 })?
                 .await;
+
             match lsp_result {
                 Ok(lsp_definitions) => {
-                    let lsp_definitions = lsp_definitions
-                        .unwrap_or_default()
-                        .into_iter()
-                        .filter_map(|definition| {
-                            definition
-                                .target
-                                .buffer
-                                .read_with(cx, |buffer, _cx| {
-                                    let path = buffer.file()?.path();
-                                    // filter out definitions from single-file worktrees
-                                    if path.is_empty() {
-                                        None
-                                    } else {
-                                        Some((
-                                            path.clone(),
-                                            definition.target.range.to_point(&buffer),
-                                        ))
-                                    }
-                                })
-                                .ok()?
-                        })
-                        .collect::<Vec<_>>();
+                    let mut targets = Vec::new();
+                    for target in lsp_definitions.unwrap_or_default() {
+                        let buffer = target.target.buffer;
+                        let anchor_range = target.target.range;
+                        buffer.read_with(cx, |buffer, cx| {
+                            let Some(file) = project::File::from_dyn(buffer.file()) else {
+                                return;
+                            };
+                            let file_worktree = file.worktree.read(cx);
+                            let file_worktree_id = file_worktree.id();
+                            // Relative paths for worktree files, absolute for all others
+                            let path = if worktree_id != file_worktree_id {
+                                file.worktree.read(cx).absolutize(&file.path)
+                            } else {
+                                file.path.as_std_path().to_path_buf()
+                            };
+                            let offset_range = anchor_range.to_offset(&buffer);
+                            let point_range = SerializablePoint::from_language_point_range(
+                                offset_range.to_point(&buffer),
+                            );
+                            targets.push(SourceRange {
+                                path,
+                                offset_range,
+                                point_range,
+                            });
+                        })?;
+                    }
 
-                    let result = RetrievalStatsResult {
-                        identifier: reference.identifier,
-                        point: query_point,
-                        outcome: RetrievalStatsOutcome::Success {
-                            matches: lsp_definitions
-                                .iter()
-                                .map(|(path, range)| {
-                                    retrieved_definitions.iter().position(
-                                        |(retrieved_path, retrieved_range, _, _)| {
-                                            path == retrieved_path
-                                                && retrieved_range.contains_inclusive(&range)
-                                        },
-                                    )
-                                })
-                                .collect(),
-                            lsp_definitions,
-                            retrieved_definitions,
+                    definitions.insert(
+                        SourceLocation {
+                            path: project_path.path.clone(),
+                            point: snapshot.offset_to_point(reference.range.start),
                         },
-                    };
-                    write!(output, "{:?}\n\n", result)?;
-                    results.push(result);
+                        targets,
+                    );
                 }
                 Err(err) => {
-                    let result = RetrievalStatsResult {
-                        identifier: reference.identifier,
-                        point: query_point,
-                        outcome: RetrievalStatsOutcome::LanguageServerError {
-                            message: err.to_string(),
-                        },
-                    };
-                    write!(output, "{:?}\n\n", result)?;
-                    results.push(result);
+                    log::error!("Language server error: {err}");
+                    error_count += 1;
                 }
             }
         }
     }
 
-    let mut no_excerpt_count = 0;
-    let mut error_count = 0;
-    let mut definitions_count = 0;
-    let mut top_match_count = 0;
-    let mut non_top_match_count = 0;
-    let mut ranking_involved_count = 0;
-    let mut ranking_involved_top_match_count = 0;
-    let mut ranking_involved_non_top_match_count = 0;
-    for result in &results {
-        match &result.outcome {
-            RetrievalStatsOutcome::NoExcerpt => no_excerpt_count += 1,
-            RetrievalStatsOutcome::LanguageServerError { .. } => error_count += 1,
-            RetrievalStatsOutcome::Success {
-                matches,
-                retrieved_definitions,
-                ..
-            } => {
-                definitions_count += 1;
-                let top_matches = matches.contains(&Some(0));
-                if top_matches {
-                    top_match_count += 1;
-                }
-                let non_top_matches = !top_matches && matches.iter().any(|index| *index != Some(0));
-                if non_top_matches {
-                    non_top_match_count += 1;
-                }
-                if retrieved_definitions.len() > 1 {
-                    ranking_involved_count += 1;
-                    if top_matches {
-                        ranking_involved_top_match_count += 1;
-                    }
-                    if non_top_matches {
-                        ranking_involved_non_top_match_count += 1;
-                    }
-                }
-            }
-        }
+    log::error!("Encountered {} language server errors", error_count);
+
+    Ok(LspResults { definitions })
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(transparent)]
+struct LspResults {
+    definitions: HashMap<SourceLocation, Vec<SourceRange>>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct SourceRange {
+    path: PathBuf,
+    point_range: Range<SerializablePoint>,
+    offset_range: Range<usize>,
+}
+
+/// Serializes to 1-based row and column indices.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SerializablePoint {
+    pub row: u32,
+    pub column: u32,
+}
+
+impl SerializablePoint {
+    pub fn into_language_point_range(range: Range<Self>) -> Range<Point> {
+        range.start.into()..range.end.into()
     }
 
-    println!("\nStats:\n");
-    println!("No Excerpt: {}", no_excerpt_count);
-    println!("Language Server Error: {}", error_count);
-    println!("Definitions: {}", definitions_count);
-    println!("Top Match: {}", top_match_count);
-    println!("Non-Top Match: {}", non_top_match_count);
-    println!("Ranking Involved: {}", ranking_involved_count);
-    println!(
-        "Ranking Involved Top Match: {}",
-        ranking_involved_top_match_count
-    );
-    println!(
-        "Ranking Involved Non-Top Match: {}",
-        ranking_involved_non_top_match_count
-    );
+    pub fn from_language_point_range(range: Range<Point>) -> Range<Self> {
+        range.start.into()..range.end.into()
+    }
+}
 
-    Ok("".to_string())
+impl From<Point> for SerializablePoint {
+    fn from(point: Point) -> Self {
+        SerializablePoint {
+            row: point.row + 1,
+            column: point.column + 1,
+        }
+    }
+}
+
+impl From<SerializablePoint> for Point {
+    fn from(serializable: SerializablePoint) -> Self {
+        Point {
+            row: serializable.row.saturating_sub(1),
+            column: serializable.column.saturating_sub(1),
+        }
+    }
 }
 
 #[derive(Debug)]
 struct RetrievalStatsResult {
+    outcome: RetrievalOutcome,
+    #[allow(dead_code)]
+    path: Arc<RelPath>,
     #[allow(dead_code)]
     identifier: Identifier,
     #[allow(dead_code)]
     point: Point,
-    outcome: RetrievalStatsOutcome,
+    #[allow(dead_code)]
+    lsp_definitions: Vec<SourceRange>,
+    retrieved_definitions: Vec<RetrievedDefinition>,
 }
 
 #[derive(Debug)]
-enum RetrievalStatsOutcome {
-    NoExcerpt,
-    LanguageServerError {
-        #[allow(dead_code)]
-        message: String,
-    },
-    Success {
-        matches: Vec<Option<usize>>,
-        #[allow(dead_code)]
-        lsp_definitions: Vec<(Arc<RelPath>, Range<Point>)>,
-        retrieved_definitions: Vec<(Arc<RelPath>, Range<Point>, f32, f32)>,
+enum RetrievalOutcome {
+    Match {
+        /// Lowest index within retrieved_definitions that matches an LSP definition.
+        best_match: usize,
     },
+    ProbablyLocal,
+    NoMatch,
+    NoMatchDueToExternalLspDefinitions,
 }
 
-pub async fn open_buffer(
-    project: &Entity<Project>,
-    worktree: &Entity<Worktree>,
-    path: &RelPath,
-    cx: &mut AsyncApp,
-) -> Result<Entity<Buffer>> {
-    let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
-        worktree_id: worktree.id(),
-        path: path.into(),
-    })?;
+#[derive(Debug)]
+struct RetrievedDefinition {
+    path: Arc<RelPath>,
+    range: Range<Point>,
+    score: f32,
+    #[allow(dead_code)]
+    retrieval_score: f32,
+    #[allow(dead_code)]
+    components: DeclarationScoreComponents,
+}
 
-    project
-        .update(cx, |project, cx| project.open_buffer(project_path, cx))?
-        .await
+pub fn open_buffer(
+    project: Entity<Project>,
+    worktree: Entity<Worktree>,
+    path: Arc<RelPath>,
+    cx: &AsyncApp,
+) -> Task<Result<Entity<Buffer>>> {
+    cx.spawn(async move |cx| {
+        let project_path = worktree.read_with(cx, |worktree, _cx| ProjectPath {
+            worktree_id: worktree.id(),
+            path,
+        })?;
+
+        let buffer = project
+            .update(cx, |project, cx| project.open_buffer(project_path, cx))?
+            .await?;
+
+        let mut parse_status = buffer.read_with(cx, |buffer, _cx| buffer.parse_status())?;
+        while *parse_status.borrow() != ParseStatus::Idle {
+            parse_status.changed().await?;
+        }
+
+        Ok(buffer)
+    })
 }
 
 pub async fn open_buffer_with_language_server(
-    project: &Entity<Project>,
-    worktree: &Entity<Worktree>,
-    path: &RelPath,
+    project: Entity<Project>,
+    worktree: Entity<Worktree>,
+    path: Arc<RelPath>,
     ready_languages: &mut HashSet<LanguageId>,
     cx: &mut AsyncApp,
 ) -> Result<(Entity<Entity<Buffer>>, LanguageServerId, Entity<Buffer>)> {
-    let buffer = open_buffer(project, worktree, path, cx).await?;
+    let buffer = open_buffer(project.clone(), worktree, path.clone(), cx).await?;
 
     let (lsp_open_handle, path_style) = project.update(cx, |project, cx| {
         (

crates/zlog_settings/src/zlog_settings.rs 🔗

@@ -24,7 +24,7 @@ pub struct ZlogSettings {
 }
 
 impl Settings for ZlogSettings {
-    fn from_settings(content: &settings::SettingsContent, _: &mut App) -> Self {
+    fn from_settings(content: &settings::SettingsContent) -> Self {
         ZlogSettings {
             scopes: content.log.clone().unwrap(),
         }

docs/src/configuring-zed.md 🔗

@@ -3512,7 +3512,7 @@ List of `integer` column numbers
     "alternate_scroll": "off",
     "blinking": "terminal_controlled",
     "copy_on_select": false,
-    "keep_selection_on_copy": false,
+    "keep_selection_on_copy": true,
     "dock": "bottom",
     "default_width": 640,
     "default_height": 320,
@@ -3690,7 +3690,7 @@ List of `integer` column numbers
 
 - Description: Whether or not to keep the selection in the terminal after copying text.
 - Setting: `keep_selection_on_copy`
-- Default: `false`
+- Default: `true`
 
 **Options**
 
@@ -3701,7 +3701,7 @@ List of `integer` column numbers
 ```json
 {
   "terminal": {
-    "keep_selection_on_copy": true
+    "keep_selection_on_copy": false
   }
 }
 ```

script/bump-gpui-minor-version 🔗

@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+
+# Ensure we're in a clean state on an up-to-date `main` branch.
+if [[ -n $(git status --short --untracked-files=no) ]]; then
+  echo "can't bump versions with uncommitted changes"
+  exit 1
+fi
+if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then
+  echo "this command must be run on main"
+  exit 1
+fi
+git pull -q --ff-only origin main
+
+
+# Parse the current version
+version=$(script/get-crate-version gpui)
+major=$(echo $version | cut -d. -f1)
+minor=$(echo $version | cut -d. -f2)
+next_minor=$(expr $minor + 1)
+
+next_minor_branch_name="bump-gpui-to-v${major}.${next_minor}.0"
+
+git checkout -b ${next_minor_branch_name}
+
+script/lib/bump-version.sh gpui gpui-v "" minor true
+
+git checkout -q main

script/lib/bump-version.sh 🔗

@@ -6,6 +6,7 @@ package=$1
 tag_prefix=$2
 tag_suffix=$3
 version_increment=$4
+gpui_release=${5:-false}
 
 if [[ -n $(git status --short --untracked-files=no) ]]; then
   echo "can't bump version with uncommitted changes"
@@ -25,6 +26,20 @@ tag_name=${tag_prefix}${new_version}${tag_suffix}
 git commit --quiet --all --message "${package} ${new_version}"
 git tag ${tag_name}
 
+if [[ "$gpui_release" == "true" ]]; then
+cat <<MESSAGE
+Locally committed and tagged ${package} version ${new_version}
+
+To push this:
+
+    git push origin ${tag_name} ${branch_name}; gh pr create -H ${branch_name}
+
+To undo this:
+
+    git branch -D ${branch_name} && git tag -d ${tag_name}
+
+MESSAGE
+else
 cat <<MESSAGE
 Locally committed and tagged ${package} version ${new_version}
 
@@ -37,3 +52,4 @@ To undo this:
     git reset --hard ${old_sha} && git tag -d ${tag_name}
 
 MESSAGE
+fi

tooling/xtask/src/tasks/publish_gpui.rs 🔗

@@ -27,14 +27,10 @@ pub fn run_publish_gpui(args: PublishGpuiArgs) -> Result<()> {
     ensure_cargo_set_version()?;
     check_git_clean()?;
 
-    let current_version = read_gpui_version()?;
-    let new_version = bump_version(&current_version, args.pre_release.as_deref())?;
-    println!(
-        "Updating GPUI version: {} -> {}",
-        current_version, new_version
-    );
-    publish_dependencies(&new_version, args.dry_run)?;
-    publish_gpui(&new_version, args.dry_run)?;
+    let version = read_gpui_version()?;
+    println!("Updating GPUI to version: {}", version);
+    publish_dependencies(&version, args.dry_run)?;
+    publish_gpui(&version, args.dry_run)?;
     println!("GPUI published in {}s", start_time.elapsed().as_secs_f32());
     Ok(())
 }
@@ -56,31 +52,6 @@ fn read_gpui_version() -> Result<String> {
     Ok(version.to_string())
 }
 
-fn bump_version(current_version: &str, pre_release: Option<&str>) -> Result<String> {
-    // Strip any existing metadata and pre-release
-    let without_metadata = current_version.split('+').next().unwrap();
-    let base_version = without_metadata.split('-').next().unwrap();
-
-    // Parse major.minor.patch
-    let parts: Vec<&str> = base_version.split('.').collect();
-    if parts.len() != 3 {
-        bail!("Invalid version format: {}", current_version);
-    }
-
-    let major: u32 = parts[0].parse().context("Failed to parse major version")?;
-    let minor: u32 = parts[1].parse().context("Failed to parse minor version")?;
-
-    // Always bump minor version
-    let new_version = format!("{}.{}.0", major, minor + 1);
-
-    // Add pre-release if specified
-    if let Some(pre) = pre_release {
-        Ok(format!("{}-{}", new_version, pre))
-    } else {
-        Ok(new_version)
-    }
-}
-
 fn publish_dependencies(new_version: &str, dry_run: bool) -> Result<()> {
     let gpui_dependencies = vec![
         ("zed-collections", "collections"),
@@ -347,40 +318,4 @@ mod tests {
 
         assert_eq!(result, output);
     }
-
-    #[test]
-    fn test_bump_version() {
-        // Test bumping minor version (default behavior)
-        assert_eq!(bump_version("0.1.0", None).unwrap(), "0.2.0");
-        assert_eq!(bump_version("0.1.5", None).unwrap(), "0.2.0");
-        assert_eq!(bump_version("1.42.7", None).unwrap(), "1.43.0");
-
-        // Test stripping pre-release and bumping minor
-        assert_eq!(bump_version("0.1.0-alpha.1", None).unwrap(), "0.2.0");
-        assert_eq!(bump_version("0.1.0-beta", None).unwrap(), "0.2.0");
-
-        // Test stripping existing metadata and bumping
-        assert_eq!(bump_version("0.1.0+old.metadata", None).unwrap(), "0.2.0");
-
-        // Test bumping minor with pre-release
-        assert_eq!(bump_version("0.1.0", Some("alpha")).unwrap(), "0.2.0-alpha");
-
-        // Test bumping minor with complex pre-release identifier
-        assert_eq!(
-            bump_version("0.1.0", Some("test.1")).unwrap(),
-            "0.2.0-test.1"
-        );
-
-        // Test bumping from existing pre-release adds new pre-release
-        assert_eq!(
-            bump_version("0.1.0-alpha", Some("beta")).unwrap(),
-            "0.2.0-beta"
-        );
-
-        // Test bumping and stripping metadata while adding pre-release
-        assert_eq!(
-            bump_version("0.1.0+metadata", Some("alpha")).unwrap(),
-            "0.2.0-alpha"
-        );
-    }
 }