Merge remote-tracking branch 'origin/main' into provider-extensions

Richard Feldman created

Change summary

.github/workflows/autofix_pr.yml                             |   2 
.github/workflows/cherry_pick.yml                            |   2 
.github/workflows/extension_tests.yml                        |   3 
.github/workflows/release.yml                                |  28 
.github/workflows/release_nightly.yml                        |   6 
.github/workflows/run_tests.yml                              |  55 
assets/prompts/content_prompt_v2.hbs                         |   3 
assets/settings/default.json                                 |   7 
crates/acp_thread/src/acp_thread.rs                          |   9 
crates/agent_ui/src/acp/model_selector.rs                    |  30 
crates/agent_ui/src/acp/thread_view.rs                       | 342 ++--
crates/agent_ui/src/buffer_codegen.rs                        |  96 +
crates/agent_ui/src/inline_assistant.rs                      |  30 
crates/codestral/src/codestral.rs                            |  12 
crates/copilot/src/copilot.rs                                | 170 -
crates/copilot/src/copilot_edit_prediction_delegate.rs       | 417 ++---
crates/copilot/src/request.rs                                | 100 
crates/deepseek/src/deepseek.rs                              |   5 
crates/edit_prediction/src/mercury.rs                        |  19 
crates/edit_prediction/src/sweep_ai.rs                       |  19 
crates/edit_prediction/src/zed_edit_prediction_delegate.rs   |  11 
crates/edit_prediction_types/src/edit_prediction_types.rs    |  26 
crates/editor/src/bracket_colorization.rs                    |  55 
crates/editor/src/edit_prediction_tests.rs                   |  18 
crates/editor/src/editor.rs                                  |  62 
crates/editor/src/element.rs                                 |   8 
crates/editor/src/git/blame.rs                               |  82 
crates/extension_api/src/extension_api.rs                    |  13 
crates/git/src/blame.rs                                      |  11 
crates/git/src/commit.rs                                     |  49 
crates/git_ui/src/blame_ui.rs                                |   5 
crates/git_ui/src/commit_tooltip.rs                          |   2 
crates/git_ui/src/git_panel.rs                               | 336 ++--
crates/google_ai/src/google_ai.rs                            |  20 
crates/gpui/src/platform/linux/wayland/window.rs             |  23 
crates/gpui/src/platform/windows/events.rs                   |   5 
crates/gpui/src/platform/windows/platform.rs                 |   2 
crates/gpui/src/text_system/line_wrapper.rs                  |  14 
crates/gpui/src/window.rs                                    |   2 
crates/language/src/buffer.rs                                |   4 
crates/language_tools/src/lsp_log_view.rs                    |   2 
crates/languages/src/python.rs                               |  17 
crates/languages/src/rust.rs                                 |  40 
crates/mistral/src/mistral.rs                                |  10 
crates/project/src/agent_server_store.rs                     |   2 
crates/project/src/git_store.rs                              |  86 -
crates/prompt_store/src/prompts.rs                           |  28 
crates/supermaven/src/supermaven_edit_prediction_delegate.rs |  11 
crates/terminal/src/terminal_hyperlinks.rs                   | 238 ++
crates/terminal_view/src/terminal_panel.rs                   |   6 
crates/terminal_view/src/terminal_view.rs                    |  28 
crates/ui/src/components/callout.rs                          |   2 
crates/workspace/src/item.rs                                 |   6 
crates/workspace/src/modal_layer.rs                          |   6 
crates/workspace/src/persistence.rs                          |  49 
crates/workspace/src/workspace.rs                            |  48 
crates/zed/resources/Document.icns                           |   0 
crates/zed/src/zed/edit_prediction_registry.rs               |  17 
docs/src/completions.md                                      |   4 
script/bundle-mac                                            |  11 
script/verify-macos-document-icon                            |  81 +
tooling/xtask/src/tasks/workflows/autofix_pr.rs              |  15 
tooling/xtask/src/tasks/workflows/cherry_pick.rs             |  17 
tooling/xtask/src/tasks/workflows/release.rs                 |   5 
tooling/xtask/src/tasks/workflows/run_tests.rs               |  79 +
tooling/xtask/src/tasks/workflows/steps.rs                   |  54 
66 files changed, 1,598 insertions(+), 1,367 deletions(-)

Detailed changes

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

@@ -90,7 +90,7 @@ jobs:
     runs-on: namespace-profile-2x4-ubuntu-2404
     steps:
     - id: get-app-token
-      name: autofix_pr::commit_changes::authenticate_as_zippy
+      name: steps::authenticate_as_zippy
       uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
       with:
         app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}

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

@@ -30,7 +30,7 @@ jobs:
       with:
         clean: false
     - id: get-app-token
-      name: cherry_pick::run_cherry_pick::authenticate_as_zippy
+      name: steps::authenticate_as_zippy
       uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
       with:
         app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}

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

@@ -61,7 +61,8 @@ jobs:
       uses: namespacelabs/nscloud-cache-action@v1
       with:
         cache: rust
-    - name: steps::cargo_fmt
+    - id: cargo_fmt
+      name: steps::cargo_fmt
       run: cargo fmt --all -- --check
       shell: bash -euxo pipefail {0}
     - name: extension_tests::run_clippy

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

@@ -26,7 +26,8 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy
       shell: bash -euxo pipefail {0}
     - name: steps::clear_target_dir_if_large
@@ -71,15 +72,15 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy
       shell: bash -euxo pipefail {0}
-    - name: steps::trigger_autofix
-      if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
-      run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
+    - id: record_clippy_failure
+      name: steps::record_clippy_failure
+      if: always()
+      run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
       shell: bash -euxo pipefail {0}
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     - name: steps::cargo_install_nextest
       uses: taiki-e/install-action@nextest
     - name: steps::clear_target_dir_if_large
@@ -93,6 +94,8 @@ jobs:
       run: |
         rm -rf ./../.cargo
       shell: bash -euxo pipefail {0}
+    outputs:
+      clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
     timeout-minutes: 60
   run_tests_windows:
     if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@@ -111,7 +114,8 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy.ps1
       shell: pwsh
     - name: steps::clear_target_dir_if_large
@@ -478,11 +482,17 @@ jobs:
     if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
     runs-on: namespace-profile-2x4-ubuntu-2404
     steps:
+    - id: get-app-token
+      name: steps::authenticate_as_zippy
+      uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
+      with:
+        app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
+        private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
     - name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
       run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
       shell: bash -euxo pipefail {0}
       env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
   notify_on_failure:
     needs:
     - upload_release_assets

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

@@ -20,7 +20,8 @@ jobs:
       with:
         clean: false
         fetch-depth: 0
-    - name: steps::cargo_fmt
+    - id: cargo_fmt
+      name: steps::cargo_fmt
       run: cargo fmt --all -- --check
       shell: bash -euxo pipefail {0}
     - name: ./script/clippy
@@ -44,7 +45,8 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy.ps1
       shell: pwsh
     - name: steps::clear_target_dir_if_large

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

@@ -74,18 +74,19 @@ jobs:
       uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
       with:
         version: '9'
-    - name: ./script/prettier
+    - id: prettier
+      name: steps::prettier
       run: ./script/prettier
       shell: bash -euxo pipefail {0}
-    - name: steps::cargo_fmt
+    - id: cargo_fmt
+      name: steps::cargo_fmt
       run: cargo fmt --all -- --check
       shell: bash -euxo pipefail {0}
-    - name: steps::trigger_autofix
-      if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
-      run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=false
+    - id: record_style_failure
+      name: steps::record_style_failure
+      if: always()
+      run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
       shell: bash -euxo pipefail {0}
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     - name: ./script/check-todos
       run: ./script/check-todos
       shell: bash -euxo pipefail {0}
@@ -96,6 +97,8 @@ jobs:
       uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
       with:
         config: ./typos.toml
+    outputs:
+      style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
     timeout-minutes: 60
   run_tests_windows:
     needs:
@@ -116,7 +119,8 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy.ps1
       shell: pwsh
     - name: steps::clear_target_dir_if_large
@@ -163,15 +167,15 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy
       shell: bash -euxo pipefail {0}
-    - name: steps::trigger_autofix
-      if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
-      run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
+    - id: record_clippy_failure
+      name: steps::record_clippy_failure
+      if: always()
+      run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
       shell: bash -euxo pipefail {0}
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
     - name: steps::cargo_install_nextest
       uses: taiki-e/install-action@nextest
     - name: steps::clear_target_dir_if_large
@@ -185,6 +189,8 @@ jobs:
       run: |
         rm -rf ./../.cargo
       shell: bash -euxo pipefail {0}
+    outputs:
+      clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
     timeout-minutes: 60
   run_tests_mac:
     needs:
@@ -205,7 +211,8 @@ jobs:
       uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
       with:
         node-version: '20'
-    - name: steps::clippy
+    - id: clippy
+      name: steps::clippy
       run: ./script/clippy
       shell: bash -euxo pipefail {0}
     - name: steps::clear_target_dir_if_large
@@ -585,6 +592,24 @@ jobs:
 
         exit $EXIT_CODE
       shell: bash -euxo pipefail {0}
+  call_autofix:
+    needs:
+    - check_style
+    - run_tests_linux
+    if: always() && (needs.check_style.outputs.style_failed == 'true' || needs.run_tests_linux.outputs.clippy_failed == 'true') && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
+    runs-on: namespace-profile-2x4-ubuntu-2404
+    steps:
+    - id: get-app-token
+      name: steps::authenticate_as_zippy
+      uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
+      with:
+        app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
+        private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
+    - name: run_tests::call_autofix::dispatch_autofix
+      run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=${{ needs.run_tests_linux.outputs.clippy_failed == 'true' }}
+      shell: bash -euxo pipefail {0}
+      env:
+        GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
   cancel-in-progress: true

assets/prompts/content_prompt_v2.hbs πŸ”—

@@ -14,7 +14,6 @@ The section you'll need to rewrite is marked with <rewrite_this></rewrite_this>
 The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
 {{/if}}
 
-{{#if rewrite_section}}
 And here's the section to rewrite based on that prompt again for reference:
 
 <rewrite_this>
@@ -33,8 +32,6 @@ Below are the diagnostic errors visible to the user.  If the user requests probl
 {{/each}}
 {{/if}}
 
-{{/if}}
-
 Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
 
 Start at the indentation level in the original file in the rewritten {{content_type}}.

assets/settings/default.json πŸ”—

@@ -1705,7 +1705,12 @@
   // }
   //
   "file_types": {
-    "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
+    "JSONC": [
+      "**/.zed/*.json",
+      "**/.vscode/**/*.json",
+      "**/{zed,Zed}/{settings,keymap,tasks,debug}.json",
+      "tsconfig*.json",
+    ],
     "Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
     "Shell Script": [".env.*"],
   },

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

@@ -192,6 +192,7 @@ pub struct ToolCall {
     pub locations: Vec<acp::ToolCallLocation>,
     pub resolved_locations: Vec<Option<AgentLocation>>,
     pub raw_input: Option<serde_json::Value>,
+    pub raw_input_markdown: Option<Entity<Markdown>>,
     pub raw_output: Option<serde_json::Value>,
 }
 
@@ -222,6 +223,11 @@ impl ToolCall {
             }
         }
 
+        let raw_input_markdown = tool_call
+            .raw_input
+            .as_ref()
+            .and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
+
         let result = Self {
             id: tool_call.tool_call_id,
             label: cx
@@ -232,6 +238,7 @@ impl ToolCall {
             resolved_locations: Vec::default(),
             status,
             raw_input: tool_call.raw_input,
+            raw_input_markdown,
             raw_output: tool_call.raw_output,
         };
         Ok(result)
@@ -307,6 +314,7 @@ impl ToolCall {
         }
 
         if let Some(raw_input) = raw_input {
+            self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
             self.raw_input = Some(raw_input);
         }
 
@@ -1355,6 +1363,7 @@ impl AcpThread {
                     locations: Vec::new(),
                     resolved_locations: Vec::new(),
                     raw_input: None,
+                    raw_input_markdown: None,
                     raw_output: None,
                 };
                 self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);

crates/agent_ui/src/acp/model_selector.rs πŸ”—

@@ -221,7 +221,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
         cx: &mut Context<Picker<Self>>,
     ) -> Task<()> {
         let favorites = if self.selector.supports_favorites() {
-            Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
+            AgentSettings::get_global(cx).favorite_model_ids()
         } else {
             Default::default()
         };
@@ -242,7 +242,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
 
             this.update_in(cx, |this, window, cx| {
                 this.delegate.filtered_entries =
-                    info_list_to_picker_entries(filtered_models, favorites);
+                    info_list_to_picker_entries(filtered_models, &favorites);
                 // Finds the currently selected model in the list
                 let new_index = this
                     .delegate
@@ -410,7 +410,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
 
 fn info_list_to_picker_entries(
     model_list: AgentModelList,
-    favorites: Arc<HashSet<ModelId>>,
+    favorites: &HashSet<ModelId>,
 ) -> Vec<AcpModelPickerEntry> {
     let mut entries = Vec::new();
 
@@ -576,13 +576,11 @@ mod tests {
         }
     }
 
-    fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
-        Arc::new(
-            models
-                .into_iter()
-                .map(|m| ModelId::new(m.to_string()))
-                .collect(),
-        )
+    fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
+        models
+            .into_iter()
+            .map(|m| ModelId::new(m.to_string()))
+            .collect()
     }
 
     fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
@@ -613,7 +611,7 @@ mod tests {
         ]);
         let favorites = create_favorites(vec!["zed/gemini"]);
 
-        let entries = info_list_to_picker_entries(models, favorites);
+        let entries = info_list_to_picker_entries(models, &favorites);
 
         assert!(matches!(
             entries.first(),
@@ -629,7 +627,7 @@ mod tests {
         let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
         let favorites = create_favorites(vec![]);
 
-        let entries = info_list_to_picker_entries(models, favorites);
+        let entries = info_list_to_picker_entries(models, &favorites);
 
         assert!(matches!(
             entries.first(),
@@ -645,7 +643,7 @@ mod tests {
         ]);
         let favorites = create_favorites(vec!["zed/claude"]);
 
-        let entries = info_list_to_picker_entries(models, favorites);
+        let entries = info_list_to_picker_entries(models, &favorites);
 
         for entry in &entries {
             if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
@@ -666,7 +664,7 @@ mod tests {
         ]);
         let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
 
-        let entries = info_list_to_picker_entries(models, favorites);
+        let entries = info_list_to_picker_entries(models, &favorites);
         let model_ids = get_entry_model_ids(&entries);
 
         assert_eq!(model_ids[0], "zed/gemini");
@@ -687,7 +685,7 @@ mod tests {
 
         let favorites = create_favorites(vec!["zed/claude"]);
 
-        let entries = info_list_to_picker_entries(models, favorites);
+        let entries = info_list_to_picker_entries(models, &favorites);
         let labels = get_entry_labels(&entries);
 
         assert_eq!(
@@ -727,7 +725,7 @@ mod tests {
         ]);
         let favorites = create_favorites(vec!["zed/gemini"]);
 
-        let entries = info_list_to_picker_entries(models, favorites);
+        let entries = info_list_to_picker_entries(models, &favorites);
 
         assert!(matches!(
             entries.first(),

crates/agent_ui/src/acp/thread_view.rs πŸ”—

@@ -34,7 +34,7 @@ use language::Buffer;
 
 use language_model::LanguageModelRegistry;
 use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
-use project::{Project, ProjectEntryId};
+use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
 use prompt_store::{PromptId, PromptStore};
 use rope::Point;
 use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
@@ -260,6 +260,7 @@ impl ThreadFeedbackState {
 
 pub struct AcpThreadView {
     agent: Rc<dyn AgentServer>,
+    agent_server_store: Entity<AgentServerStore>,
     workspace: WeakEntity<Workspace>,
     project: Entity<Project>,
     thread_state: ThreadState,
@@ -406,6 +407,7 @@ impl AcpThreadView {
 
         Self {
             agent: agent.clone(),
+            agent_server_store,
             workspace: workspace.clone(),
             project: project.clone(),
             entry_view_state,
@@ -737,7 +739,7 @@ impl AcpThreadView {
         cx: &mut App,
     ) {
         let agent_name = agent.name();
-        let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
+        let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
             let registry = LanguageModelRegistry::global(cx);
 
             let sub = window.subscribe(&registry, cx, {
@@ -779,7 +781,6 @@ impl AcpThreadView {
                 configuration_view,
                 description: err
                     .description
-                    .clone()
                     .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
                 _subscription: subscription,
             };
@@ -1088,10 +1089,7 @@ impl AcpThreadView {
                 window.defer(cx, |window, cx| {
                     Self::handle_auth_required(
                         this,
-                        AuthRequired {
-                            description: None,
-                            provider_id: None,
-                        },
+                        AuthRequired::new(),
                         agent,
                         connection,
                         window,
@@ -1663,44 +1661,6 @@ impl AcpThreadView {
                 });
                 return;
             }
-        } else if method.0.as_ref() == "anthropic-api-key" {
-            let registry = LanguageModelRegistry::global(cx);
-            let provider = registry
-                .read(cx)
-                .provider(&language_model::ANTHROPIC_PROVIDER_ID)
-                .unwrap();
-            let this = cx.weak_entity();
-            let agent = self.agent.clone();
-            let connection = connection.clone();
-            window.defer(cx, move |window, cx| {
-                if !provider.is_authenticated(cx) {
-                    Self::handle_auth_required(
-                        this,
-                        AuthRequired {
-                            description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
-                            provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
-                        },
-                        agent,
-                        connection,
-                        window,
-                        cx,
-                    );
-                } else {
-                    this.update(cx, |this, cx| {
-                        this.thread_state = Self::initial_state(
-                            agent,
-                            None,
-                            this.workspace.clone(),
-                            this.project.clone(),
-                            true,
-                            window,
-                            cx,
-                        )
-                    })
-                    .ok();
-                }
-            });
-            return;
         } else if method.0.as_ref() == "vertex-ai"
             && std::env::var("GOOGLE_API_KEY").is_err()
             && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
@@ -2440,6 +2400,12 @@ impl AcpThreadView {
         let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
 
         let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
+        let input_output_header = |label: SharedString| {
+            Label::new(label)
+                .size(LabelSize::XSmall)
+                .color(Color::Muted)
+                .buffer_font(cx)
+        };
 
         let tool_output_display =
             if is_open {
@@ -2481,7 +2447,25 @@ impl AcpThreadView {
                     | ToolCallStatus::Completed
                     | ToolCallStatus::Failed
                     | ToolCallStatus::Canceled => v_flex()
+                        .mt_1p5()
                         .w_full()
+                        .child(
+                            v_flex()
+                                .ml(rems(0.4))
+                                .px_3p5()
+                                .pb_1()
+                                .gap_1()
+                                .border_l_1()
+                                .border_color(self.tool_card_border_color(cx))
+                                .child(input_output_header("Raw Input".into()))
+                                .children(tool_call.raw_input_markdown.clone().map(|input| {
+                                    self.render_markdown(
+                                        input,
+                                        default_markdown_style(false, false, window, cx),
+                                    )
+                                }))
+                                .child(input_output_header("Output:".into())),
+                        )
                         .children(tool_call.content.iter().enumerate().map(
                             |(content_ix, content)| {
                                 div().child(self.render_tool_call_content(
@@ -2580,7 +2564,7 @@ impl AcpThreadView {
                                         .gap_px()
                                         .when(is_collapsible, |this| {
                                             this.child(
-                                            Disclosure::new(("expand", entry_ix), is_open)
+                                            Disclosure::new(("expand-output", entry_ix), is_open)
                                                 .opened_icon(IconName::ChevronUp)
                                                 .closed_icon(IconName::ChevronDown)
                                                 .visible_on_hover(&card_header_id)
@@ -2766,7 +2750,6 @@ impl AcpThreadView {
         let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
 
         v_flex()
-            .mt_1p5()
             .gap_2()
             .when(!card_layout, |this| {
                 this.ml(rems(0.4))
@@ -3500,138 +3483,119 @@ impl AcpThreadView {
         pending_auth_method: Option<&acp::AuthMethodId>,
         window: &mut Window,
         cx: &Context<Self>,
-    ) -> Div {
-        let show_description =
-            configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
-
+    ) -> impl IntoElement {
         let auth_methods = connection.auth_methods();
 
-        v_flex().flex_1().size_full().justify_end().child(
-            v_flex()
-                .p_2()
-                .pr_3()
-                .w_full()
-                .gap_1()
-                .border_t_1()
-                .border_color(cx.theme().colors().border)
-                .bg(cx.theme().status().warning.opacity(0.04))
-                .child(
-                    h_flex()
-                        .gap_1p5()
-                        .child(
-                            Icon::new(IconName::Warning)
-                                .color(Color::Warning)
-                                .size(IconSize::Small),
-                        )
-                        .child(Label::new("Authentication Required").size(LabelSize::Small)),
-                )
-                .children(description.map(|desc| {
-                    div().text_ui(cx).child(self.render_markdown(
-                        desc.clone(),
-                        default_markdown_style(false, false, window, cx),
-                    ))
-                }))
-                .children(
-                    configuration_view
-                        .cloned()
-                        .map(|view| div().w_full().child(view)),
-                )
-                .when(show_description, |el| {
-                    el.child(
-                        Label::new(format!(
-                            "You are not currently authenticated with {}.{}",
-                            self.agent.name(),
-                            if auth_methods.len() > 1 {
-                                " Please choose one of the following options:"
-                            } else {
-                                ""
-                            }
-                        ))
-                        .size(LabelSize::Small)
-                        .color(Color::Muted)
-                        .mb_1()
-                        .ml_5(),
-                    )
-                })
-                .when_some(pending_auth_method, |el, _| {
-                    el.child(
-                        h_flex()
-                            .py_4()
-                            .w_full()
-                            .justify_center()
-                            .gap_1()
-                            .child(
-                                Icon::new(IconName::ArrowCircle)
-                                    .size(IconSize::Small)
-                                    .color(Color::Muted)
-                                    .with_rotate_animation(2),
-                            )
-                            .child(Label::new("Authenticating…").size(LabelSize::Small)),
-                    )
-                })
-                .when(!auth_methods.is_empty(), |this| {
-                    this.child(
-                        h_flex()
-                            .justify_end()
-                            .flex_wrap()
-                            .gap_1()
-                            .when(!show_description, |this| {
-                                this.border_t_1()
-                                    .mt_1()
-                                    .pt_2()
-                                    .border_color(cx.theme().colors().border.opacity(0.8))
+        let agent_display_name = self
+            .agent_server_store
+            .read(cx)
+            .agent_display_name(&ExternalAgentServerName(self.agent.name()))
+            .unwrap_or_else(|| self.agent.name());
+
+        let show_fallback_description = auth_methods.len() > 1
+            && configuration_view.is_none()
+            && description.is_none()
+            && pending_auth_method.is_none();
+
+        let auth_buttons = || {
+            h_flex().justify_end().flex_wrap().gap_1().children(
+                connection
+                    .auth_methods()
+                    .iter()
+                    .enumerate()
+                    .rev()
+                    .map(|(ix, method)| {
+                        let (method_id, name) = if self.project.read(cx).is_via_remote_server()
+                            && method.id.0.as_ref() == "oauth-personal"
+                            && method.name == "Log in with Google"
+                        {
+                            ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
+                        } else {
+                            (method.id.0.clone(), method.name.clone())
+                        };
+
+                        let agent_telemetry_id = connection.telemetry_id();
+
+                        Button::new(method_id.clone(), name)
+                            .label_size(LabelSize::Small)
+                            .map(|this| {
+                                if ix == 0 {
+                                    this.style(ButtonStyle::Tinted(TintColor::Accent))
+                                } else {
+                                    this.style(ButtonStyle::Outlined)
+                                }
                             })
-                            .children(connection.auth_methods().iter().enumerate().rev().map(
-                                |(ix, method)| {
-                                    let (method_id, name) = if self
-                                        .project
-                                        .read(cx)
-                                        .is_via_remote_server()
-                                        && method.id.0.as_ref() == "oauth-personal"
-                                        && method.name == "Log in with Google"
-                                    {
-                                        ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
-                                    } else {
-                                        (method.id.0.clone(), method.name.clone())
-                                    };
+                            .when_some(method.description.clone(), |this, description| {
+                                this.tooltip(Tooltip::text(description))
+                            })
+                            .on_click({
+                                cx.listener(move |this, _, window, cx| {
+                                    telemetry::event!(
+                                        "Authenticate Agent Started",
+                                        agent = agent_telemetry_id,
+                                        method = method_id
+                                    );
 
-                                    let agent_telemetry_id = connection.telemetry_id();
+                                    this.authenticate(
+                                        acp::AuthMethodId::new(method_id.clone()),
+                                        window,
+                                        cx,
+                                    )
+                                })
+                            })
+                    }),
+            )
+        };
 
-                                    Button::new(method_id.clone(), name)
-                                        .label_size(LabelSize::Small)
-                                        .map(|this| {
-                                            if ix == 0 {
-                                                this.style(ButtonStyle::Tinted(TintColor::Warning))
-                                            } else {
-                                                this.style(ButtonStyle::Outlined)
-                                            }
-                                        })
-                                        .when_some(
-                                            method.description.clone(),
-                                            |this, description| {
-                                                this.tooltip(Tooltip::text(description))
-                                            },
-                                        )
-                                        .on_click({
-                                            cx.listener(move |this, _, window, cx| {
-                                                telemetry::event!(
-                                                    "Authenticate Agent Started",
-                                                    agent = agent_telemetry_id,
-                                                    method = method_id
-                                                );
-
-                                                this.authenticate(
-                                                    acp::AuthMethodId::new(method_id.clone()),
-                                                    window,
-                                                    cx,
-                                                )
-                                            })
-                                        })
-                                },
-                            )),
-                    )
-                }),
-        )
+        if pending_auth_method.is_some() {
+            return Callout::new()
+                .icon(IconName::Info)
+                .title(format!("Authenticating to {}…", agent_display_name))
+                .actions_slot(
+                    Icon::new(IconName::ArrowCircle)
+                        .size(IconSize::Small)
+                        .color(Color::Muted)
+                        .with_rotate_animation(2)
+                        .into_any_element(),
+                )
+                .into_any_element();
+        }
+
+        Callout::new()
+            .icon(IconName::Info)
+            .title(format!("Authenticate to {}", agent_display_name))
+            .when(auth_methods.len() == 1, |this| {
+                this.actions_slot(auth_buttons())
+            })
+            .description_slot(
+                v_flex()
+                    .text_ui(cx)
+                    .map(|this| {
+                        if show_fallback_description {
+                            this.child(
+                                Label::new("Choose one of the following authentication options:")
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted),
+                            )
+                        } else {
+                            this.children(
+                                configuration_view
+                                    .cloned()
+                                    .map(|view| div().w_full().child(view)),
+                            )
+                            .children(description.map(|desc| {
+                                self.render_markdown(
+                                    desc.clone(),
+                                    default_markdown_style(false, false, window, cx),
+                                )
+                            }))
+                        }
+                    })
+                    .when(auth_methods.len() > 1, |this| {
+                        this.gap_1().child(auth_buttons())
+                    }),
+            )
+            .into_any_element()
     }
 
     fn render_load_error(
@@ -5880,10 +5844,6 @@ impl AcpThreadView {
                     };
 
                     let connection = thread.read(cx).connection().clone();
-                    let err = AuthRequired {
-                        description: None,
-                        provider_id: None,
-                    };
                     this.clear_thread_error(cx);
                     if let Some(message) = this.in_flight_prompt.take() {
                         this.message_editor.update(cx, |editor, cx| {
@@ -5892,7 +5852,14 @@ impl AcpThreadView {
                     }
                     let this = cx.weak_entity();
                     window.defer(cx, |window, cx| {
-                        Self::handle_auth_required(this, err, agent, connection, window, cx);
+                        Self::handle_auth_required(
+                            this,
+                            AuthRequired::new(),
+                            agent,
+                            connection,
+                            window,
+                            cx,
+                        );
                     })
                 }
             }))
@@ -5905,14 +5872,10 @@ impl AcpThreadView {
         };
 
         let connection = thread.read(cx).connection().clone();
-        let err = AuthRequired {
-            description: None,
-            provider_id: None,
-        };
         self.clear_thread_error(cx);
         let this = cx.weak_entity();
         window.defer(cx, |window, cx| {
-            Self::handle_auth_required(this, err, agent, connection, window, cx);
+            Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx);
         })
     }
 
@@ -6015,16 +5978,19 @@ impl Render for AcpThreadView {
                     configuration_view,
                     pending_auth_method,
                     ..
-                } => self
-                    .render_auth_required_state(
+                } => v_flex()
+                    .flex_1()
+                    .size_full()
+                    .justify_end()
+                    .child(self.render_auth_required_state(
                         connection,
                         description.as_ref(),
                         configuration_view.as_ref(),
                         pending_auth_method.as_ref(),
                         window,
                         cx,
-                    )
-                    .into_any(),
+                    ))
+                    .into_any_element(),
                 ThreadState::Loading { .. } => v_flex()
                     .flex_1()
                     .child(self.render_recent_history(cx))

crates/agent_ui/src/buffer_codegen.rs πŸ”—

@@ -75,6 +75,9 @@ pub struct BufferCodegen {
     session_id: Uuid,
 }
 
+pub const REWRITE_SECTION_TOOL_NAME: &str = "rewrite_section";
+pub const FAILURE_MESSAGE_TOOL_NAME: &str = "failure_message";
+
 impl BufferCodegen {
     pub fn new(
         buffer: Entity<MultiBuffer>,
@@ -522,12 +525,12 @@ impl CodegenAlternative {
 
             let tools = vec![
                 LanguageModelRequestTool {
-                    name: "rewrite_section".to_string(),
+                    name: REWRITE_SECTION_TOOL_NAME.to_string(),
                     description: "Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
                     input_schema: language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
                 },
                 LanguageModelRequestTool {
-                    name: "failure_message".to_string(),
+                    name: FAILURE_MESSAGE_TOOL_NAME.to_string(),
                     description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
                     input_schema: language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
                 },
@@ -1167,7 +1170,7 @@ impl CodegenAlternative {
             let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option<ToolUseOutput> {
                 let mut chars_read_so_far = chars_read_so_far.lock();
                 match tool_use.name.as_ref() {
-                    "rewrite_section" => {
+                    REWRITE_SECTION_TOOL_NAME => {
                         let Ok(input) =
                             serde_json::from_value::<RewriteSectionInput>(tool_use.input)
                         else {
@@ -1180,7 +1183,7 @@ impl CodegenAlternative {
                             description: None,
                         })
                     }
-                    "failure_message" => {
+                    FAILURE_MESSAGE_TOOL_NAME => {
                         let Ok(mut input) =
                             serde_json::from_value::<FailureMessageInput>(tool_use.input)
                         else {
@@ -1493,7 +1496,10 @@ mod tests {
     use indoc::indoc;
     use language::{Buffer, Point};
     use language_model::fake_provider::FakeLanguageModel;
-    use language_model::{LanguageModelRegistry, TokenUsage};
+    use language_model::{
+        LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRegistry,
+        LanguageModelToolUse, StopReason, TokenUsage,
+    };
     use languages::rust_lang;
     use rand::prelude::*;
     use settings::SettingsStore;
@@ -1805,6 +1811,51 @@ mod tests {
         );
     }
 
+    // When not streaming tool calls, we strip backticks as part of parsing the model's
+    // plain text response. This is a regression test for a bug where we stripped
+    // backticks incorrectly.
+    #[gpui::test]
+    async fn test_allows_model_to_output_backticks(cx: &mut TestAppContext) {
+        init_test(cx);
+        let text = "- Improved; `cmd+click` behavior. Now requires `cmd` to be pressed before the click starts or it doesn't run. ([#44579](https://github.com/zed-industries/zed/pull/44579); thanks [Zachiah](https://github.com/Zachiah))";
+        let buffer = cx.new(|cx| Buffer::local("", cx));
+        let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+        let range = buffer.read_with(cx, |buffer, cx| {
+            let snapshot = buffer.snapshot(cx);
+            snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(0, 0))
+        });
+        let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
+        let codegen = cx.new(|cx| {
+            CodegenAlternative::new(
+                buffer.clone(),
+                range.clone(),
+                true,
+                prompt_builder,
+                Uuid::new_v4(),
+                cx,
+            )
+        });
+
+        let events_tx = simulate_tool_based_completion(&codegen, cx);
+        let chunk_len = text.find('`').unwrap();
+        events_tx
+            .unbounded_send(rewrite_tool_use("tool_1", &text[..chunk_len], false))
+            .unwrap();
+        events_tx
+            .unbounded_send(rewrite_tool_use("tool_2", &text, true))
+            .unwrap();
+        events_tx
+            .unbounded_send(LanguageModelCompletionEvent::Stop(StopReason::EndTurn))
+            .unwrap();
+        drop(events_tx);
+        cx.run_until_parked();
+
+        assert_eq!(
+            buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
+            text
+        );
+    }
+
     #[gpui::test]
     async fn test_strip_invalid_spans_from_codeblock() {
         assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await;
@@ -1870,4 +1921,39 @@ mod tests {
         });
         chunks_tx
     }
+
+    fn simulate_tool_based_completion(
+        codegen: &Entity<CodegenAlternative>,
+        cx: &mut TestAppContext,
+    ) -> mpsc::UnboundedSender<LanguageModelCompletionEvent> {
+        let (events_tx, events_rx) = mpsc::unbounded();
+        let model = Arc::new(FakeLanguageModel::default());
+        codegen.update(cx, |codegen, cx| {
+            let completion_stream = Task::ready(Ok(events_rx.map(Ok).boxed()
+                as BoxStream<
+                    'static,
+                    Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
+                >));
+            codegen.generation = codegen.handle_completion(model, completion_stream, cx);
+        });
+        events_tx
+    }
+
+    fn rewrite_tool_use(
+        id: &str,
+        replacement_text: &str,
+        is_complete: bool,
+    ) -> LanguageModelCompletionEvent {
+        let input = RewriteSectionInput {
+            replacement_text: replacement_text.into(),
+        };
+        LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
+            id: id.into(),
+            name: REWRITE_SECTION_TOOL_NAME.into(),
+            raw_input: serde_json::to_string(&input).unwrap(),
+            input: serde_json::to_value(&input).unwrap(),
+            is_input_complete: is_complete,
+            thought_signature: None,
+        })
+    }
 }

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

@@ -2271,6 +2271,36 @@ pub mod evals {
         );
     }
 
+    #[test]
+    #[cfg_attr(not(feature = "unit-eval"), ignore)]
+    fn eval_empty_buffer() {
+        run_eval(
+            20,
+            1.0,
+            "Write a Python hello, world program".to_string(),
+            "Λ‡".to_string(),
+            |output| match output {
+                InlineAssistantOutput::Success {
+                    full_buffer_text, ..
+                } => {
+                    if full_buffer_text.is_empty() {
+                        EvalOutput::failed("expected some output".to_string())
+                    } else {
+                        EvalOutput::passed(format!("Produced {full_buffer_text}"))
+                    }
+                }
+                o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!(
+                    "Assistant output does not match expected output: {:?}",
+                    o
+                )),
+                o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!(
+                    "Assistant output does not match expected output: {:?}",
+                    o
+                )),
+            },
+        );
+    }
+
     fn run_eval(
         iterations: usize,
         expected_pass_ratio: f32,

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

@@ -1,6 +1,6 @@
 use anyhow::{Context as _, Result};
 use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
-use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
+use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
 use futures::AsyncReadExt;
 use gpui::{App, Context, Entity, Task};
 use http_client::HttpClient;
@@ -300,16 +300,6 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate {
         }));
     }
 
-    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;

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

@@ -4,6 +4,7 @@ pub mod copilot_responses;
 pub mod request;
 mod sign_in;
 
+use crate::request::NextEditSuggestions;
 use crate::sign_in::initiate_sign_out;
 use ::fs::Fs;
 use anyhow::{Context as _, Result, anyhow};
@@ -18,7 +19,7 @@ use http_client::HttpClient;
 use language::language_settings::CopilotSettings;
 use language::{
     Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,
-    language_settings::{EditPredictionProvider, all_language_settings, language_settings},
+    language_settings::{EditPredictionProvider, all_language_settings},
     point_from_lsp, point_to_lsp,
 };
 use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
@@ -40,7 +41,7 @@ use std::{
     sync::Arc,
 };
 use sum_tree::Dimensions;
-use util::{ResultExt, fs::remove_matching, rel_path::RelPath};
+use util::{ResultExt, fs::remove_matching};
 use workspace::Workspace;
 
 pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;
@@ -315,6 +316,15 @@ struct GlobalCopilot(Entity<Copilot>);
 
 impl Global for GlobalCopilot {}
 
+/// Copilot's NextEditSuggestion response, with coordinates converted to Anchors.
+struct CopilotEditPrediction {
+    buffer: Entity<Buffer>,
+    range: Range<Anchor>,
+    text: String,
+    command: Option<lsp::Command>,
+    snapshot: BufferSnapshot,
+}
+
 impl Copilot {
     pub fn global(cx: &App) -> Option<Entity<Self>> {
         cx.try_global::<GlobalCopilot>()
@@ -873,101 +883,19 @@ impl Copilot {
         }
     }
 
-    pub fn completions<T>(
-        &mut self,
-        buffer: &Entity<Buffer>,
-        position: T,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<Completion>>>
-    where
-        T: ToPointUtf16,
-    {
-        self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
-    }
-
-    pub fn completions_cycling<T>(
+    pub(crate) fn completions(
         &mut self,
         buffer: &Entity<Buffer>,
-        position: T,
+        position: Anchor,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<Completion>>>
-    where
-        T: ToPointUtf16,
-    {
-        self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
-    }
-
-    pub fn accept_completion(
-        &mut self,
-        completion: &Completion,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        let server = match self.server.as_authenticated() {
-            Ok(server) => server,
-            Err(error) => return Task::ready(Err(error)),
-        };
-        let request =
-            server
-                .lsp
-                .request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
-                    uuid: completion.uuid.clone(),
-                });
-        cx.background_spawn(async move {
-            request
-                .await
-                .into_response()
-                .context("copilot: notify accepted")?;
-            Ok(())
-        })
-    }
-
-    pub fn discard_completions(
-        &mut self,
-        completions: &[Completion],
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        let server = match self.server.as_authenticated() {
-            Ok(server) => server,
-            Err(_) => return Task::ready(Ok(())),
-        };
-        let request =
-            server
-                .lsp
-                .request::<request::NotifyRejected>(request::NotifyRejectedParams {
-                    uuids: completions
-                        .iter()
-                        .map(|completion| completion.uuid.clone())
-                        .collect(),
-                });
-        cx.background_spawn(async move {
-            request
-                .await
-                .into_response()
-                .context("copilot: notify rejected")?;
-            Ok(())
-        })
-    }
-
-    fn request_completions<R, T>(
-        &mut self,
-        buffer: &Entity<Buffer>,
-        position: T,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<Completion>>>
-    where
-        R: 'static
-            + lsp::request::Request<
-                Params = request::GetCompletionsParams,
-                Result = request::GetCompletionsResult,
-            >,
-        T: ToPointUtf16,
-    {
+    ) -> Task<Result<Vec<CopilotEditPrediction>>> {
         self.register_buffer(buffer, cx);
 
         let server = match self.server.as_authenticated() {
             Ok(server) => server,
             Err(error) => return Task::ready(Err(error)),
         };
+        let buffer_entity = buffer.clone();
         let lsp = server.lsp.clone();
         let registered_buffer = server
             .registered_buffers
@@ -977,46 +905,31 @@ impl Copilot {
         let buffer = buffer.read(cx);
         let uri = registered_buffer.uri.clone();
         let position = position.to_point_utf16(buffer);
-        let settings = language_settings(
-            buffer.language_at(position).map(|l| l.name()),
-            buffer.file(),
-            cx,
-        );
-        let tab_size = settings.tab_size;
-        let hard_tabs = settings.hard_tabs;
-        let relative_path = buffer
-            .file()
-            .map_or(RelPath::empty().into(), |file| file.path().clone());
 
         cx.background_spawn(async move {
             let (version, snapshot) = snapshot.await?;
             let result = lsp
-                .request::<R>(request::GetCompletionsParams {
-                    doc: request::GetCompletionsDocument {
-                        uri,
-                        tab_size: tab_size.into(),
-                        indent_size: 1,
-                        insert_spaces: !hard_tabs,
-                        relative_path: relative_path.to_proto(),
-                        position: point_to_lsp(position),
-                        version: version.try_into().unwrap(),
-                    },
+                .request::<NextEditSuggestions>(request::NextEditSuggestionsParams {
+                    text_document: lsp::VersionedTextDocumentIdentifier { uri, version },
+                    position: point_to_lsp(position),
                 })
                 .await
                 .into_response()
                 .context("copilot: get completions")?;
             let completions = result
-                .completions
+                .edits
                 .into_iter()
                 .map(|completion| {
                     let start = snapshot
                         .clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
                     let end =
                         snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
-                    Completion {
-                        uuid: completion.uuid,
+                    CopilotEditPrediction {
+                        buffer: buffer_entity.clone(),
                         range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
                         text: completion.text,
+                        command: completion.command,
+                        snapshot: snapshot.clone(),
                     }
                 })
                 .collect();
@@ -1024,6 +937,35 @@ impl Copilot {
         })
     }
 
+    pub(crate) fn accept_completion(
+        &mut self,
+        completion: &CopilotEditPrediction,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<()>> {
+        let server = match self.server.as_authenticated() {
+            Ok(server) => server,
+            Err(error) => return Task::ready(Err(error)),
+        };
+        if let Some(command) = &completion.command {
+            let request = server
+                .lsp
+                .request::<lsp::ExecuteCommand>(lsp::ExecuteCommandParams {
+                    command: command.command.clone(),
+                    arguments: command.arguments.clone().unwrap_or_default(),
+                    ..Default::default()
+                });
+            cx.background_spawn(async move {
+                request
+                    .await
+                    .into_response()
+                    .context("copilot: notify accepted")?;
+                Ok(())
+            })
+        } else {
+            Task::ready(Ok(()))
+        }
+    }
+
     pub fn status(&self) -> Status {
         match &self.server {
             CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
@@ -1260,7 +1202,11 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
 mod tests {
     use super::*;
     use gpui::TestAppContext;
-    use util::{path, paths::PathStyle, rel_path::rel_path};
+    use util::{
+        path,
+        paths::PathStyle,
+        rel_path::{RelPath, rel_path},
+    };
 
     #[gpui::test(iterations = 10)]
     async fn test_buffer_management(cx: &mut TestAppContext) {

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

@@ -1,49 +1,29 @@
-use crate::{Completion, Copilot};
+use crate::{Copilot, CopilotEditPrediction};
 use anyhow::Result;
-use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
-use gpui::{App, Context, Entity, EntityId, Task};
-use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings};
-use settings::Settings;
-use std::{path::Path, time::Duration};
+use edit_prediction_types::{EditPrediction, EditPredictionDelegate, interpolate_edits};
+use gpui::{App, Context, Entity, Task};
+use language::{Anchor, Buffer, EditPreview, OffsetRangeExt};
+use std::{ops::Range, sync::Arc, time::Duration};
 
 pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
 
 pub struct CopilotEditPredictionDelegate {
-    cycled: bool,
-    buffer_id: Option<EntityId>,
-    completions: Vec<Completion>,
-    active_completion_index: usize,
-    file_extension: Option<String>,
+    completion: Option<(CopilotEditPrediction, EditPreview)>,
     pending_refresh: Option<Task<Result<()>>>,
-    pending_cycling_refresh: Option<Task<Result<()>>>,
     copilot: Entity<Copilot>,
 }
 
 impl CopilotEditPredictionDelegate {
     pub fn new(copilot: Entity<Copilot>) -> Self {
         Self {
-            cycled: false,
-            buffer_id: None,
-            completions: Vec::new(),
-            active_completion_index: 0,
-            file_extension: None,
+            completion: None,
             pending_refresh: None,
-            pending_cycling_refresh: None,
             copilot,
         }
     }
 
-    fn active_completion(&self) -> Option<&Completion> {
-        self.completions.get(self.active_completion_index)
-    }
-
-    fn push_completion(&mut self, new_completion: Completion) {
-        for completion in &self.completions {
-            if completion.text == new_completion.text && completion.range == new_completion.range {
-                return;
-            }
-        }
-        self.completions.push(new_completion);
+    fn active_completion(&self) -> Option<&(CopilotEditPrediction, EditPreview)> {
+        self.completion.as_ref()
     }
 }
 
@@ -64,12 +44,8 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
         true
     }
 
-    fn supports_jump_to_edit() -> bool {
-        false
-    }
-
     fn is_refreshing(&self, _cx: &App) -> bool {
-        self.pending_refresh.is_some() && self.completions.is_empty()
+        self.pending_refresh.is_some() && self.completion.is_none()
     }
 
     fn is_enabled(
@@ -102,160 +78,96 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
                 })?
                 .await?;
 
-            this.update(cx, |this, cx| {
-                if !completions.is_empty() {
-                    this.cycled = false;
+            if let Some(mut completion) = completions.into_iter().next()
+                && let Some(trimmed_completion) = cx
+                    .update(|cx| trim_completion(&completion, cx))
+                    .ok()
+                    .flatten()
+            {
+                let preview = buffer
+                    .update(cx, |this, cx| {
+                        this.preview_edits(Arc::from(std::slice::from_ref(&trimmed_completion)), cx)
+                    })?
+                    .await;
+                this.update(cx, |this, cx| {
                     this.pending_refresh = None;
-                    this.pending_cycling_refresh = None;
-                    this.completions.clear();
-                    this.active_completion_index = 0;
-                    this.buffer_id = Some(buffer.entity_id());
-                    this.file_extension = buffer.read(cx).file().and_then(|file| {
-                        Some(
-                            Path::new(file.file_name(cx))
-                                .extension()?
-                                .to_str()?
-                                .to_string(),
-                        )
-                    });
-
-                    for completion in completions {
-                        this.push_completion(completion);
-                    }
+                    completion.range = trimmed_completion.0;
+                    completion.text = trimmed_completion.1.to_string();
+                    this.completion = Some((completion, preview));
+
                     cx.notify();
-                }
-            })?;
+                })?;
+            }
 
             Ok(())
         }));
     }
 
-    fn cycle(
-        &mut self,
-        buffer: Entity<Buffer>,
-        cursor_position: language::Anchor,
-        direction: Direction,
-        cx: &mut Context<Self>,
-    ) {
-        if self.cycled {
-            match direction {
-                Direction::Prev => {
-                    self.active_completion_index = if self.active_completion_index == 0 {
-                        self.completions.len().saturating_sub(1)
-                    } else {
-                        self.active_completion_index - 1
-                    };
-                }
-                Direction::Next => {
-                    if self.completions.is_empty() {
-                        self.active_completion_index = 0
-                    } else {
-                        self.active_completion_index =
-                            (self.active_completion_index + 1) % self.completions.len();
-                    }
-                }
-            }
-
-            cx.notify();
-        } else {
-            let copilot = self.copilot.clone();
-            self.pending_cycling_refresh = Some(cx.spawn(async move |this, cx| {
-                let completions = copilot
-                    .update(cx, |copilot, cx| {
-                        copilot.completions_cycling(&buffer, cursor_position, cx)
-                    })?
-                    .await?;
-
-                this.update(cx, |this, cx| {
-                    this.cycled = true;
-                    this.file_extension = buffer.read(cx).file().and_then(|file| {
-                        Some(
-                            Path::new(file.file_name(cx))
-                                .extension()?
-                                .to_str()?
-                                .to_string(),
-                        )
-                    });
-                    for completion in completions {
-                        this.push_completion(completion);
-                    }
-                    this.cycle(buffer, cursor_position, direction, cx);
-                })?;
-
-                Ok(())
-            }));
-        }
-    }
-
     fn accept(&mut self, cx: &mut Context<Self>) {
-        if let Some(completion) = self.active_completion() {
+        if let Some((completion, _)) = self.active_completion() {
             self.copilot
                 .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
                 .detach_and_log_err(cx);
         }
     }
 
-    fn discard(&mut self, cx: &mut Context<Self>) {
-        let settings = AllLanguageSettings::get_global(cx);
-
-        let copilot_enabled = settings.show_edit_predictions(None, cx);
-
-        if !copilot_enabled {
-            return;
-        }
-
-        self.copilot
-            .update(cx, |copilot, cx| {
-                copilot.discard_completions(&self.completions, cx)
-            })
-            .detach_and_log_err(cx);
-    }
+    fn discard(&mut self, _: &mut Context<Self>) {}
 
     fn suggest(
         &mut self,
         buffer: &Entity<Buffer>,
-        cursor_position: language::Anchor,
+        _: language::Anchor,
         cx: &mut Context<Self>,
     ) -> Option<EditPrediction> {
         let buffer_id = buffer.entity_id();
         let buffer = buffer.read(cx);
-        let completion = self.active_completion()?;
-        if Some(buffer_id) != self.buffer_id
+        let (completion, edit_preview) = self.active_completion()?;
+
+        if Some(buffer_id) != Some(completion.buffer.entity_id())
             || !completion.range.start.is_valid(buffer)
             || !completion.range.end.is_valid(buffer)
         {
             return None;
         }
+        let edits = vec![(
+            completion.range.clone(),
+            Arc::from(completion.text.as_ref()),
+        )];
+        let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits)
+            .filter(|edits| !edits.is_empty())?;
+
+        Some(EditPrediction::Local {
+            id: None,
+            edits,
+            edit_preview: Some(edit_preview.clone()),
+        })
+    }
+}
 
-        let mut completion_range = completion.range.to_offset(buffer);
-        let prefix_len = common_prefix(
-            buffer.chars_for_range(completion_range.clone()),
-            completion.text.chars(),
-        );
-        completion_range.start += prefix_len;
-        let suffix_len = common_prefix(
-            buffer.reversed_chars_for_range(completion_range.clone()),
-            completion.text[prefix_len..].chars().rev(),
-        );
-        completion_range.end = completion_range.end.saturating_sub(suffix_len);
-
-        if completion_range.is_empty()
-            && completion_range.start == cursor_position.to_offset(buffer)
-        {
-            let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
-            if completion_text.trim().is_empty() {
-                None
-            } else {
-                let position = cursor_position.bias_right(buffer);
-                Some(EditPrediction::Local {
-                    id: None,
-                    edits: vec![(position..position, completion_text.into())],
-                    edit_preview: None,
-                })
-            }
-        } else {
-            None
-        }
+fn trim_completion(
+    completion: &CopilotEditPrediction,
+    cx: &mut App,
+) -> Option<(Range<Anchor>, Arc<str>)> {
+    let buffer = completion.buffer.read(cx);
+    let mut completion_range = completion.range.to_offset(buffer);
+    let prefix_len = common_prefix(
+        buffer.chars_for_range(completion_range.clone()),
+        completion.text.chars(),
+    );
+    completion_range.start += prefix_len;
+    let suffix_len = common_prefix(
+        buffer.reversed_chars_for_range(completion_range.clone()),
+        completion.text[prefix_len..].chars().rev(),
+    );
+    completion_range.end = completion_range.end.saturating_sub(suffix_len);
+    let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
+    if completion_text.trim().is_empty() {
+        None
+    } else {
+        let completion_range =
+            buffer.anchor_after(completion_range.start)..buffer.anchor_after(completion_range.end);
+
+        Some((completion_range, Arc::from(completion_text)))
     }
 }
 
@@ -282,6 +194,7 @@ mod tests {
         Point,
         language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode},
     };
+    use lsp::Uri;
     use project::Project;
     use serde_json::json;
     use settings::{AllLanguageSettingsContent, SettingsStore};
@@ -337,12 +250,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "one.copilot1".into(),
                 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, window, cx| {
@@ -383,12 +299,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "one.copilot1".into(),
                 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, _, cx| {
@@ -412,12 +331,15 @@ mod tests {
         // After debouncing, new Copilot completions should be requested.
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "one.copilot2".into(),
                 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, window, cx| {
@@ -479,45 +401,6 @@ mod tests {
             assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
             assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
         });
-
-        // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
-        cx.update_editor(|editor, window, cx| {
-            editor.set_text("fn foo() {\n  \n}", window, cx);
-            editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
-                s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
-            });
-        });
-        handle_copilot_completion_request(
-            &copilot_lsp,
-            vec![crate::request::Completion {
-                text: "    let x = 4;".into(),
-                range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
-                ..Default::default()
-            }],
-            vec![],
-        );
-
-        cx.update_editor(|editor, window, cx| {
-            editor.next_edit_prediction(&Default::default(), window, cx)
-        });
-        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
-        cx.update_editor(|editor, window, cx| {
-            assert!(editor.has_active_edit_prediction());
-            assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
-            assert_eq!(editor.text(cx), "fn foo() {\n  \n}");
-
-            // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
-            editor.tab(&Default::default(), window, cx);
-            assert!(editor.has_active_edit_prediction());
-            assert_eq!(editor.text(cx), "fn foo() {\n    \n}");
-            assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
-
-            // Using AcceptEditPrediction again accepts the suggestion.
-            editor.accept_edit_prediction(&Default::default(), window, cx);
-            assert!(!editor.has_active_edit_prediction());
-            assert_eq!(editor.text(cx), "fn foo() {\n    let x = 4;\n}");
-            assert_eq!(editor.display_text(cx), "fn foo() {\n    let x = 4;\n}");
-        });
     }
 
     #[gpui::test(iterations = 10)]
@@ -570,12 +453,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "one.copilot1".into(),
                 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, window, cx| {
@@ -614,12 +500,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "one.123. copilot\n 456".into(),
                 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, window, cx| {
@@ -686,15 +575,18 @@ mod tests {
 
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "two.foo()".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         cx.update_editor(|editor, window, cx| {
-            editor.next_edit_prediction(&Default::default(), window, cx)
+            editor.show_edit_prediction(&Default::default(), window, cx)
         });
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, window, cx| {
@@ -703,15 +595,22 @@ mod tests {
             assert_eq!(editor.text(cx), "one\ntw\nthree\n");
 
             editor.backspace(&Default::default(), window, cx);
+        });
+        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+        cx.run_until_parked();
+        cx.update_editor(|editor, window, cx| {
             assert!(editor.has_active_edit_prediction());
             assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
             assert_eq!(editor.text(cx), "one\nt\nthree\n");
 
             editor.backspace(&Default::default(), window, cx);
+        });
+        executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+        cx.run_until_parked();
+        cx.update_editor(|editor, window, cx| {
             assert!(editor.has_active_edit_prediction());
             assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
             assert_eq!(editor.text(cx), "one\n\nthree\n");
-
             // Deleting across the original suggestion range invalidates it.
             editor.backspace(&Default::default(), window, cx);
             assert!(!editor.has_active_edit_prediction());
@@ -765,19 +664,22 @@ mod tests {
 
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "b = 2 + a".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         _ = editor.update(cx, |editor, window, cx| {
             // Ensure copilot suggestions are shown for the first excerpt.
             editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                 s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
             });
-            editor.next_edit_prediction(&Default::default(), window, cx);
+            editor.show_edit_prediction(&Default::default(), window, cx);
         });
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         _ = editor.update(cx, |editor, _, cx| {
@@ -791,12 +693,15 @@ mod tests {
 
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "d = 4 + c".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         _ = editor.update(cx, |editor, window, cx| {
             // Move to another excerpt, ensuring the suggestion gets cleared.
@@ -873,15 +778,18 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "two.foo()".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         cx.update_editor(|editor, window, cx| {
-            editor.next_edit_prediction(&Default::default(), window, cx)
+            editor.show_edit_prediction(&Default::default(), window, cx)
         });
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, _, cx| {
@@ -903,12 +811,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "two.foo()".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, _, cx| {
@@ -930,12 +841,15 @@ mod tests {
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
-            vec![crate::request::Completion {
+            vec![crate::request::NextEditSuggestion {
                 text: "two.foo()".into(),
                 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
-                ..Default::default()
+                command: None,
+                text_document: lsp::VersionedTextDocumentIdentifier {
+                    uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                    version: 0,
+                },
             }],
-            vec![],
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, _, cx| {
@@ -1011,16 +925,20 @@ mod tests {
             .unwrap();
 
         let mut copilot_requests = copilot_lsp
-            .set_request_handler::<crate::request::GetCompletions, _, _>(
+            .set_request_handler::<crate::request::NextEditSuggestions, _, _>(
                 move |_params, _cx| async move {
-                    Ok(crate::request::GetCompletionsResult {
-                        completions: vec![crate::request::Completion {
+                    Ok(crate::request::NextEditSuggestionsResult {
+                        edits: vec![crate::request::NextEditSuggestion {
                             text: "next line".into(),
                             range: lsp::Range::new(
                                 lsp::Position::new(1, 0),
                                 lsp::Position::new(1, 0),
                             ),
-                            ..Default::default()
+                            command: None,
+                            text_document: lsp::VersionedTextDocumentIdentifier {
+                                uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+                                version: 0,
+                            },
                         }],
                     })
                 },
@@ -1049,23 +967,14 @@ mod tests {
 
     fn handle_copilot_completion_request(
         lsp: &lsp::FakeLanguageServer,
-        completions: Vec<crate::request::Completion>,
-        completions_cycling: Vec<crate::request::Completion>,
+        completions: Vec<crate::request::NextEditSuggestion>,
     ) {
-        lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
-            let completions = completions.clone();
-            async move {
-                Ok(crate::request::GetCompletionsResult {
-                    completions: completions.clone(),
-                })
-            }
-        });
-        lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
+        lsp.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
             move |_params, _cx| {
-                let completions_cycling = completions_cycling.clone();
+                let completions = completions.clone();
                 async move {
-                    Ok(crate::request::GetCompletionsResult {
-                        completions: completions_cycling.clone(),
+                    Ok(crate::request::NextEditSuggestionsResult {
+                        edits: completions.clone(),
                     })
                 }
             },

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

@@ -1,3 +1,4 @@
+use lsp::VersionedTextDocumentIdentifier;
 use serde::{Deserialize, Serialize};
 
 pub enum CheckStatus {}
@@ -88,72 +89,6 @@ impl lsp::request::Request for SignOut {
     const METHOD: &'static str = "signOut";
 }
 
-pub enum GetCompletions {}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct GetCompletionsParams {
-    pub doc: GetCompletionsDocument,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct GetCompletionsDocument {
-    pub tab_size: u32,
-    pub indent_size: u32,
-    pub insert_spaces: bool,
-    pub uri: lsp::Uri,
-    pub relative_path: String,
-    pub position: lsp::Position,
-    pub version: usize,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct GetCompletionsResult {
-    pub completions: Vec<Completion>,
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Completion {
-    pub text: String,
-    pub position: lsp::Position,
-    pub uuid: String,
-    pub range: lsp::Range,
-    pub display_text: String,
-}
-
-impl lsp::request::Request for GetCompletions {
-    type Params = GetCompletionsParams;
-    type Result = GetCompletionsResult;
-    const METHOD: &'static str = "getCompletions";
-}
-
-pub enum GetCompletionsCycling {}
-
-impl lsp::request::Request for GetCompletionsCycling {
-    type Params = GetCompletionsParams;
-    type Result = GetCompletionsResult;
-    const METHOD: &'static str = "getCompletionsCycling";
-}
-
-pub enum LogMessage {}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LogMessageParams {
-    pub level: u8,
-    pub message: String,
-    pub metadata_str: String,
-    pub extra: Vec<String>,
-}
-
-impl lsp::notification::Notification for LogMessage {
-    type Params = LogMessageParams;
-    const METHOD: &'static str = "LogMessage";
-}
-
 pub enum StatusNotification {}
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -223,3 +158,36 @@ impl lsp::request::Request for NotifyRejected {
     type Result = String;
     const METHOD: &'static str = "notifyRejected";
 }
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NextEditSuggestions;
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NextEditSuggestionsParams {
+    pub(crate) text_document: VersionedTextDocumentIdentifier,
+    pub(crate) position: lsp::Position,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NextEditSuggestion {
+    pub text: String,
+    pub text_document: VersionedTextDocumentIdentifier,
+    pub range: lsp::Range,
+    pub command: Option<lsp::Command>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NextEditSuggestionsResult {
+    pub edits: Vec<NextEditSuggestion>,
+}
+
+impl lsp::request::Request for NextEditSuggestions {
+    type Params = NextEditSuggestionsParams;
+    type Result = NextEditSuggestionsResult;
+
+    const METHOD: &'static str = "textDocument/copilotInlineEdit";
+}

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

@@ -103,8 +103,9 @@ impl Model {
 
     pub fn max_output_tokens(&self) -> Option<u64> {
         match self {
-            Self::Chat => Some(8_192),
-            Self::Reasoner => Some(64_000),
+            // Their API treats this max against the context window, which means we hit the limit a lot
+            // Using the default value of None in the API instead
+            Self::Chat | Self::Reasoner => None,
             Self::Custom {
                 max_output_tokens, ..
             } => *max_output_tokens,

crates/edit_prediction/src/mercury.rs πŸ”—

@@ -6,7 +6,7 @@ use crate::{
 use anyhow::{Context as _, Result};
 use futures::AsyncReadExt as _;
 use gpui::{
-    App, AppContext as _, Entity, SharedString, Task,
+    App, AppContext as _, Entity, Global, SharedString, Task,
     http_client::{self, AsyncBody, Method},
 };
 use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
@@ -300,14 +300,19 @@ pub const MERCURY_CREDENTIALS_URL: SharedString =
     SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
 pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
 pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
-pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
+
+struct GlobalMercuryApiKey(Entity<ApiKeyState>);
+
+impl Global for GlobalMercuryApiKey {}
 
 pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
-    MERCURY_API_KEY
-        .get_or_init(|| {
-            cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
-        })
-        .clone()
+    if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
+        return global.0.clone();
+    }
+    let entity =
+        cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
+    cx.set_global(GlobalMercuryApiKey(entity.clone()));
+    entity
 }
 
 pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {

crates/edit_prediction/src/sweep_ai.rs πŸ”—

@@ -1,7 +1,7 @@
 use anyhow::Result;
 use futures::AsyncReadExt as _;
 use gpui::{
-    App, AppContext as _, Entity, SharedString, Task,
+    App, AppContext as _, Entity, Global, SharedString, Task,
     http_client::{self, AsyncBody, Method},
 };
 use language::{Point, ToOffset as _};
@@ -272,14 +272,19 @@ pub const SWEEP_CREDENTIALS_URL: SharedString =
     SharedString::new_static("https://autocomplete.sweep.dev");
 pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
 pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
-pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
+
+struct GlobalSweepApiKey(Entity<ApiKeyState>);
+
+impl Global for GlobalSweepApiKey {}
 
 pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
-    SWEEP_API_KEY
-        .get_or_init(|| {
-            cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
-        })
-        .clone()
+    if let Some(global) = cx.try_global::<GlobalSweepApiKey>() {
+        return global.0.clone();
+    }
+    let entity =
+        cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
+    cx.set_global(GlobalSweepApiKey(entity.clone()));
+    entity
 }
 
 pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {

crates/edit_prediction/src/zed_edit_prediction_delegate.rs πŸ”—

@@ -2,7 +2,7 @@ use std::{cmp, sync::Arc};
 
 use client::{Client, UserStore};
 use cloud_llm_client::EditPredictionRejectReason;
-use edit_prediction_types::{DataCollectionState, Direction, EditPredictionDelegate};
+use edit_prediction_types::{DataCollectionState, EditPredictionDelegate};
 use gpui::{App, Entity, prelude::*};
 use language::{Buffer, ToPoint as _};
 use project::Project;
@@ -139,15 +139,6 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
         });
     }
 
-    fn cycle(
-        &mut self,
-        _buffer: Entity<language::Buffer>,
-        _cursor_position: language::Anchor,
-        _direction: Direction,
-        _cx: &mut Context<Self>,
-    ) {
-    }
-
     fn accept(&mut self, cx: &mut Context<Self>) {
         self.store.update(cx, |store, cx| {
             store.accept_current_prediction(&self.project, cx);

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

@@ -95,13 +95,6 @@ pub trait EditPredictionDelegate: 'static + Sized {
         debounce: bool,
         cx: &mut Context<Self>,
     );
-    fn cycle(
-        &mut self,
-        buffer: Entity<Buffer>,
-        cursor_position: language::Anchor,
-        direction: Direction,
-        cx: &mut Context<Self>,
-    );
     fn accept(&mut self, cx: &mut Context<Self>);
     fn discard(&mut self, cx: &mut Context<Self>);
     fn did_show(&mut self, _cx: &mut Context<Self>) {}
@@ -136,13 +129,6 @@ pub trait EditPredictionDelegateHandle {
         debounce: bool,
         cx: &mut App,
     );
-    fn cycle(
-        &self,
-        buffer: Entity<Buffer>,
-        cursor_position: language::Anchor,
-        direction: Direction,
-        cx: &mut App,
-    );
     fn did_show(&self, cx: &mut App);
     fn accept(&self, cx: &mut App);
     fn discard(&self, cx: &mut App);
@@ -215,18 +201,6 @@ where
         })
     }
 
-    fn cycle(
-        &self,
-        buffer: Entity<Buffer>,
-        cursor_position: language::Anchor,
-        direction: Direction,
-        cx: &mut App,
-    ) {
-        self.update(cx, |this, cx| {
-            this.cycle(buffer, cursor_position, direction, cx)
-        })
-    }
-
     fn accept(&self, cx: &mut App) {
         self.update(cx, |this, cx| this.accept(cx))
     }

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

@@ -348,6 +348,61 @@ where
         );
     }
 
+    #[gpui::test]
+    async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |language_settings| {
+            language_settings.defaults.colorize_brackets = Some(true);
+        });
+
+        let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
+        language_registry.add(markdown_lang());
+        language_registry.add(rust_lang());
+
+        let mut cx = EditorTestContext::new(cx).await;
+        cx.update_buffer(|buffer, cx| {
+            buffer.set_language_registry(language_registry.clone());
+            buffer.set_language(Some(markdown_lang()), cx);
+        });
+
+        cx.set_state(indoc! {r#"
+            fn main() {
+                let v: Vec<Stringˇ> = vec![];
+            }
+        "#});
+        cx.executor().advance_clock(Duration::from_millis(100));
+        cx.executor().run_until_parked();
+
+        assert_eq!(
+            r#"fn mainΒ«1()1Β» Β«1{
+    let v: Vec<String> = vec!Β«2[]2Β»;
+}1Β»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+"#,
+            &bracket_colors_markup(&mut cx),
+            "Markdown does not colorize <> brackets"
+        );
+
+        cx.update_buffer(|buffer, cx| {
+            buffer.set_language(Some(rust_lang()), cx);
+        });
+        cx.executor().advance_clock(Duration::from_millis(100));
+        cx.executor().run_until_parked();
+
+        assert_eq!(
+            r#"fn mainΒ«1()1Β» Β«1{
+    let v: VecΒ«2<String>2Β» = vec!Β«2[]2Β»;
+}1Β»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+"#,
+            &bracket_colors_markup(&mut cx),
+            "After switching to Rust, <> brackets are now colorized"
+        );
+    }
+
     #[gpui::test]
     async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
         init_test(cx, |language_settings| {

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

@@ -485,15 +485,6 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate {
     ) {
     }
 
-    fn cycle(
-        &mut self,
-        _buffer: gpui::Entity<language::Buffer>,
-        _cursor_position: language::Anchor,
-        _direction: edit_prediction_types::Direction,
-        _cx: &mut gpui::Context<Self>,
-    ) {
-    }
-
     fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
 
     fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
@@ -561,15 +552,6 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate {
     ) {
     }
 
-    fn cycle(
-        &mut self,
-        _buffer: gpui::Entity<language::Buffer>,
-        _cursor_position: language::Anchor,
-        _direction: edit_prediction_types::Direction,
-        _cx: &mut gpui::Context<Self>,
-    ) {
-    }
-
     fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
 
     fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}

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

@@ -73,11 +73,7 @@ pub use multi_buffer::{
 pub use split::SplittableEditor;
 pub use text::Bias;
 
-use ::git::{
-    Restore,
-    blame::{BlameEntry, ParsedCommitMessage},
-    status::FileStatus,
-};
+use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
 use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError};
 use anyhow::{Context as _, Result, anyhow, bail};
 use blink_manager::BlinkManager;
@@ -7468,26 +7464,6 @@ impl Editor {
         .unwrap_or(false)
     }
 
-    fn cycle_edit_prediction(
-        &mut self,
-        direction: Direction,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<()> {
-        let provider = self.edit_prediction_provider()?;
-        let cursor = self.selections.newest_anchor().head();
-        let (buffer, cursor_buffer_position) =
-            self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
-        if self.edit_predictions_hidden_for_vim_mode || !self.should_show_edit_predictions() {
-            return None;
-        }
-
-        provider.cycle(buffer, cursor_buffer_position, direction, cx);
-        self.update_visible_edit_prediction(window, cx);
-
-        Some(())
-    }
-
     pub fn show_edit_prediction(
         &mut self,
         _: &ShowEditPrediction,
@@ -7525,42 +7501,6 @@ impl Editor {
         .detach();
     }
 
-    pub fn next_edit_prediction(
-        &mut self,
-        _: &NextEditPrediction,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if self.has_active_edit_prediction() {
-            self.cycle_edit_prediction(Direction::Next, window, cx);
-        } else {
-            let is_copilot_disabled = self
-                .refresh_edit_prediction(false, true, window, cx)
-                .is_none();
-            if is_copilot_disabled {
-                cx.propagate();
-            }
-        }
-    }
-
-    pub fn previous_edit_prediction(
-        &mut self,
-        _: &PreviousEditPrediction,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if self.has_active_edit_prediction() {
-            self.cycle_edit_prediction(Direction::Prev, window, cx);
-        } else {
-            let is_copilot_disabled = self
-                .refresh_edit_prediction(false, true, window, cx)
-                .is_none();
-            if is_copilot_disabled {
-                cx.propagate();
-            }
-        }
-    }
-
     pub fn accept_partial_edit_prediction(
         &mut self,
         granularity: EditPredictionGranularity,

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

@@ -37,11 +37,7 @@ use crate::{
 use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
 use collections::{BTreeMap, HashMap};
 use file_icons::FileIcons;
-use git::{
-    Oid,
-    blame::{BlameEntry, ParsedCommitMessage},
-    status::FileStatus,
-};
+use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
 use gpui::{
     Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
     Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
@@ -594,8 +590,6 @@ impl EditorElement {
         register_action(editor, window, Editor::show_signature_help);
         register_action(editor, window, Editor::signature_help_prev);
         register_action(editor, window, Editor::signature_help_next);
-        register_action(editor, window, Editor::next_edit_prediction);
-        register_action(editor, window, Editor::previous_edit_prediction);
         register_action(editor, window, Editor::show_edit_prediction);
         register_action(editor, window, Editor::context_menu_first);
         register_action(editor, window, Editor::context_menu_prev);

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

@@ -3,9 +3,9 @@ use anyhow::{Context as _, Result};
 use collections::HashMap;
 
 use git::{
-    GitHostingProviderRegistry, GitRemote, Oid,
-    blame::{Blame, BlameEntry, ParsedCommitMessage},
-    parse_git_remote_url,
+    GitHostingProviderRegistry, Oid,
+    blame::{Blame, BlameEntry},
+    commit::ParsedCommitMessage,
 };
 use gpui::{
     AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task,
@@ -525,12 +525,7 @@ impl GitBlame {
                                 .git_store()
                                 .read(cx)
                                 .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
-                                .and_then(|(repo, _)| {
-                                    repo.read(cx)
-                                        .remote_upstream_url
-                                        .clone()
-                                        .or(repo.read(cx).remote_origin_url.clone())
-                                });
+                                .and_then(|(repo, _)| repo.read(cx).default_remote_url());
                             let blame_buffer = project
                                 .update(cx, |project, cx| project.blame_buffer(&buffer, None, cx));
                             Ok(async move {
@@ -554,13 +549,19 @@ impl GitBlame {
                                             entries,
                                             snapshot.max_point().row,
                                         );
-                                        let commit_details = parse_commit_messages(
-                                            messages,
-                                            remote_url,
-                                            provider_registry.clone(),
-                                        )
-                                        .await;
-
+                                        let commit_details = messages
+                                            .into_iter()
+                                            .map(|(oid, message)| {
+                                                let parsed_commit_message =
+                                                    ParsedCommitMessage::parse(
+                                                        oid.to_string(),
+                                                        message,
+                                                        remote_url.as_deref(),
+                                                        Some(provider_registry.clone()),
+                                                    );
+                                                (oid, parsed_commit_message)
+                                            })
+                                            .collect();
                                         res.push((
                                             id,
                                             snapshot,
@@ -680,55 +681,6 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
     entries
 }
 
-async fn parse_commit_messages(
-    messages: impl IntoIterator<Item = (Oid, String)>,
-    remote_url: Option<String>,
-    provider_registry: Arc<GitHostingProviderRegistry>,
-) -> HashMap<Oid, ParsedCommitMessage> {
-    let mut commit_details = HashMap::default();
-
-    let parsed_remote_url = remote_url
-        .as_deref()
-        .and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
-
-    for (oid, message) in messages {
-        let permalink = if let Some((provider, git_remote)) = parsed_remote_url.as_ref() {
-            Some(provider.build_commit_permalink(
-                git_remote,
-                git::BuildCommitPermalinkParams {
-                    sha: oid.to_string().as_str(),
-                },
-            ))
-        } else {
-            None
-        };
-
-        let remote = parsed_remote_url
-            .as_ref()
-            .map(|(provider, remote)| GitRemote {
-                host: provider.clone(),
-                owner: remote.owner.clone().into(),
-                repo: remote.repo.clone().into(),
-            });
-
-        let pull_request = parsed_remote_url
-            .as_ref()
-            .and_then(|(provider, remote)| provider.extract_pull_request(remote, &message));
-
-        commit_details.insert(
-            oid,
-            ParsedCommitMessage {
-                message: message.into(),
-                permalink,
-                remote,
-                pull_request,
-            },
-        );
-    }
-
-    commit_details
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;

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

@@ -438,7 +438,6 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
 pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
 
 mod wit {
-
     wit_bindgen::generate!({
         skip: ["init-extension"],
         path: "./wit/since_v0.8.0",
@@ -692,6 +691,12 @@ impl wit::Guest for Component {
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
 pub struct LanguageServerId(String);
 
+impl LanguageServerId {
+    pub fn new(value: String) -> Self {
+        Self(value)
+    }
+}
+
 impl AsRef<str> for LanguageServerId {
     fn as_ref(&self) -> &str {
         &self.0
@@ -708,6 +713,12 @@ impl fmt::Display for LanguageServerId {
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
 pub struct ContextServerId(String);
 
+impl ContextServerId {
+    pub fn new(value: String) -> Self {
+        Self(value)
+    }
+}
+
 impl AsRef<str> for ContextServerId {
     fn as_ref(&self) -> &str {
         &self.0

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

@@ -1,10 +1,9 @@
+use crate::Oid;
 use crate::commit::get_messages;
 use crate::repository::RepoPath;
-use crate::{GitRemote, Oid};
 use anyhow::{Context as _, Result};
 use collections::{HashMap, HashSet};
 use futures::AsyncWriteExt;
-use gpui::SharedString;
 use serde::{Deserialize, Serialize};
 use std::process::Stdio;
 use std::{ops::Range, path::Path};
@@ -21,14 +20,6 @@ pub struct Blame {
     pub messages: HashMap<Oid, String>,
 }
 
-#[derive(Clone, Debug, Default)]
-pub struct ParsedCommitMessage {
-    pub message: SharedString,
-    pub permalink: Option<url::Url>,
-    pub pull_request: Option<crate::hosting_provider::PullRequest>,
-    pub remote: Option<GitRemote>,
-}
-
 impl Blame {
     pub async fn for_path(
         git_binary: &Path,

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

@@ -1,7 +1,52 @@
-use crate::{Oid, status::StatusCode};
+use crate::{
+    BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, parse_git_remote_url,
+    status::StatusCode,
+};
 use anyhow::{Context as _, Result};
 use collections::HashMap;
-use std::path::Path;
+use gpui::SharedString;
+use std::{path::Path, sync::Arc};
+
+#[derive(Clone, Debug, Default)]
+pub struct ParsedCommitMessage {
+    pub message: SharedString,
+    pub permalink: Option<url::Url>,
+    pub pull_request: Option<crate::hosting_provider::PullRequest>,
+    pub remote: Option<GitRemote>,
+}
+
+impl ParsedCommitMessage {
+    pub fn parse(
+        sha: String,
+        message: String,
+        remote_url: Option<&str>,
+        provider_registry: Option<Arc<GitHostingProviderRegistry>>,
+    ) -> Self {
+        if let Some((hosting_provider, remote)) = provider_registry
+            .and_then(|reg| remote_url.and_then(|url| parse_git_remote_url(reg, url)))
+        {
+            let pull_request = hosting_provider.extract_pull_request(&remote, &message);
+            Self {
+                message: message.into(),
+                permalink: Some(
+                    hosting_provider
+                        .build_commit_permalink(&remote, BuildCommitPermalinkParams { sha: &sha }),
+                ),
+                pull_request,
+                remote: Some(GitRemote {
+                    host: hosting_provider,
+                    owner: remote.owner.into(),
+                    repo: remote.repo.into(),
+                }),
+            }
+        } else {
+            Self {
+                message: message.into(),
+                ..Default::default()
+            }
+        }
+    }
+}
 
 pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
     if shas.is_empty() {

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

@@ -3,10 +3,7 @@ use crate::{
     commit_view::CommitView,
 };
 use editor::{BlameRenderer, Editor, hover_markdown_style};
-use git::{
-    blame::{BlameEntry, ParsedCommitMessage},
-    repository::CommitSummary,
-};
+use git::{blame::BlameEntry, commit::ParsedCommitMessage, repository::CommitSummary};
 use gpui::{
     ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle,
     TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,

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

@@ -3,7 +3,7 @@ use editor::hover_markdown_style;
 use futures::Future;
 use git::blame::BlameEntry;
 use git::repository::CommitSummary;
-use git::{GitRemote, blame::ParsedCommitMessage};
+use git::{GitRemote, commit::ParsedCommitMessage};
 use gpui::{
     App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
     StatefulInteractiveElement, WeakEntity, prelude::*,

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

@@ -15,12 +15,13 @@ use askpass::AskPassDelegate;
 use cloud_llm_client::CompletionIntent;
 use collections::{BTreeMap, HashMap, HashSet};
 use db::kvp::KEY_VALUE_STORE;
+use editor::RewrapOptions;
 use editor::{
     Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset,
     actions::ExpandAllDiffHunks,
 };
 use futures::StreamExt as _;
-use git::blame::ParsedCommitMessage;
+use git::commit::ParsedCommitMessage;
 use git::repository::{
     Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter,
     PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,
@@ -30,15 +31,14 @@ use git::stash::GitStash;
 use git::status::StageStatus;
 use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus};
 use git::{
-    ExpandCommitEditor, RestoreTrackedFiles, StageAll, StashAll, StashApply, StashPop,
-    TrashUntrackedFiles, UnstageAll,
+    ExpandCommitEditor, GitHostingProviderRegistry, RestoreTrackedFiles, StageAll, StashAll,
+    StashApply, StashPop, TrashUntrackedFiles, UnstageAll,
 };
 use gpui::{
     Action, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, Corner, DismissEvent, Entity,
-    EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
-    ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy,
-    Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point,
-    size, uniform_list,
+    EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent, Point,
+    PromptLevel, ScrollStrategy, Subscription, Task, UniformListScrollHandle, WeakEntity, actions,
+    anchored, deferred, point, size, uniform_list,
 };
 use itertools::Itertools;
 use language::{Buffer, File};
@@ -212,8 +212,7 @@ const GIT_PANEL_KEY: &str = "GitPanel";
 
 const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
 // TODO: We should revise this part. It seems the indentation width is not aligned with the one in project panel
-const TREE_INDENT: f32 = 12.0;
-const TREE_INDENT_GUIDE_OFFSET: f32 = 16.0;
+const TREE_INDENT: f32 = 16.0;
 
 pub fn register(workspace: &mut Workspace) {
     workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
@@ -2182,7 +2181,13 @@ impl GitPanel {
         let editor = cx.new(|cx| Editor::for_buffer(buffer, None, window, cx));
         let wrapped_message = editor.update(cx, |editor, cx| {
             editor.select_all(&Default::default(), window, cx);
-            editor.rewrap(&Default::default(), window, cx);
+            editor.rewrap_impl(
+                RewrapOptions {
+                    override_language_settings: false,
+                    preserve_existing_whitespace: true,
+                },
+                cx,
+            );
             editor.text(cx)
         });
         if wrapped_message.trim().is_empty() {
@@ -4697,7 +4702,10 @@ impl GitPanel {
                                         },
                                     )
                                     .with_render_fn(cx.entity(), |_, params, _, _| {
-                                        let left_offset = px(TREE_INDENT_GUIDE_OFFSET);
+                                        // Magic number to align the tree item is 3 here
+                                        // because we're using 12px as the left-side padding
+                                        // and 3 makes the alignment work with the bounding box of the icon
+                                        let left_offset = px(TREE_INDENT + 3_f32);
                                         let indent_size = params.indent_size;
                                         let item_height = params.item_height;
 
@@ -4725,10 +4733,6 @@ impl GitPanel {
                         })
                         .size_full()
                         .flex_grow()
-                        .with_sizing_behavior(ListSizingBehavior::Auto)
-                        .with_horizontal_sizing_behavior(
-                            ListHorizontalSizingBehavior::Unconstrained,
-                        )
                         .with_width_from_item(self.max_width_item_index)
                         .track_scroll(&self.scroll_handle),
                     )
@@ -4752,7 +4756,7 @@ impl GitPanel {
     }
 
     fn entry_label(&self, label: impl Into<SharedString>, color: Color) -> Label {
-        Label::new(label.into()).color(color).single_line()
+        Label::new(label.into()).color(color)
     }
 
     fn list_item_height(&self) -> Rems {
@@ -4774,8 +4778,8 @@ impl GitPanel {
             .h(self.list_item_height())
             .w_full()
             .items_end()
-            .px(rems(0.75)) // ~12px
-            .pb(rems(0.3125)) // ~ 5px
+            .px_3()
+            .pb_1()
             .child(
                 Label::new(header.title())
                     .color(Color::Muted)
@@ -4963,113 +4967,68 @@ impl GitPanel {
         let marked_bg_alpha = 0.12;
         let state_opacity_step = 0.04;
 
+        let info_color = cx.theme().status().info;
+
         let base_bg = match (selected, marked) {
-            (true, true) => cx
-                .theme()
-                .status()
-                .info
-                .alpha(selected_bg_alpha + marked_bg_alpha),
-            (true, false) => cx.theme().status().info.alpha(selected_bg_alpha),
-            (false, true) => cx.theme().status().info.alpha(marked_bg_alpha),
+            (true, true) => info_color.alpha(selected_bg_alpha + marked_bg_alpha),
+            (true, false) => info_color.alpha(selected_bg_alpha),
+            (false, true) => info_color.alpha(marked_bg_alpha),
             _ => cx.theme().colors().ghost_element_background,
         };
 
-        let hover_bg = if selected {
-            cx.theme()
-                .status()
-                .info
-                .alpha(selected_bg_alpha + state_opacity_step)
-        } else {
-            cx.theme().colors().ghost_element_hover
-        };
-
-        let active_bg = if selected {
-            cx.theme()
-                .status()
-                .info
-                .alpha(selected_bg_alpha + state_opacity_step * 2.0)
+        let (hover_bg, active_bg) = if selected {
+            (
+                info_color.alpha(selected_bg_alpha + state_opacity_step),
+                info_color.alpha(selected_bg_alpha + state_opacity_step * 2.0),
+            )
         } else {
-            cx.theme().colors().ghost_element_active
+            (
+                cx.theme().colors().ghost_element_hover,
+                cx.theme().colors().ghost_element_active,
+            )
         };
 
-        let mut name_row = h_flex()
-            .items_center()
-            .gap_1()
+        let name_row = h_flex()
+            .min_w_0()
             .flex_1()
-            .pl(if tree_view {
-                px(depth as f32 * TREE_INDENT)
-            } else {
-                px(0.)
-            })
-            .child(git_status_icon(status));
-
-        name_row = if tree_view {
-            name_row.child(
-                self.entry_label(display_name, label_color)
-                    .when(status.is_deleted(), Label::strikethrough)
-                    .truncate(),
-            )
-        } else {
-            name_row.child(h_flex().items_center().flex_1().map(|this| {
-                self.path_formatted(
-                    this,
-                    entry.parent_dir(path_style),
-                    path_color,
-                    display_name,
-                    label_color,
-                    path_style,
-                    git_path_style,
-                    status.is_deleted(),
-                )
-            }))
-        };
+            .gap_1()
+            .child(git_status_icon(status))
+            .map(|this| {
+                if tree_view {
+                    this.pl(px(depth as f32 * TREE_INDENT)).child(
+                        self.entry_label(display_name, label_color)
+                            .when(status.is_deleted(), Label::strikethrough)
+                            .truncate(),
+                    )
+                } else {
+                    this.child(self.path_formatted(
+                        entry.parent_dir(path_style),
+                        path_color,
+                        display_name,
+                        label_color,
+                        path_style,
+                        git_path_style,
+                        status.is_deleted(),
+                    ))
+                }
+            });
 
         h_flex()
             .id(id)
             .h(self.list_item_height())
             .w_full()
+            .pl_3()
+            .pr_1()
+            .gap_1p5()
             .border_1()
             .border_r_2()
             .when(selected && self.focus_handle.is_focused(window), |el| {
                 el.border_color(cx.theme().colors().panel_focused_border)
             })
-            .px(rems(0.75)) // ~12px
-            .overflow_hidden()
-            .flex_none()
-            .gap_1p5()
             .bg(base_bg)
-            .hover(|this| this.bg(hover_bg))
-            .active(|this| this.bg(active_bg))
-            .on_click({
-                cx.listener(move |this, event: &ClickEvent, window, cx| {
-                    this.selected_entry = Some(ix);
-                    cx.notify();
-                    if event.modifiers().secondary() {
-                        this.open_file(&Default::default(), window, cx)
-                    } else {
-                        this.open_diff(&Default::default(), window, cx);
-                        this.focus_handle.focus(window, cx);
-                    }
-                })
-            })
-            .on_mouse_down(
-                MouseButton::Right,
-                move |event: &MouseDownEvent, window, cx| {
-                    // why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`?
-                    if event.button != MouseButton::Right {
-                        return;
-                    }
-
-                    let Some(this) = handle.upgrade() else {
-                        return;
-                    };
-                    this.update(cx, |this, cx| {
-                        this.deploy_entry_context_menu(event.position, ix, window, cx);
-                    });
-                    cx.stop_propagation();
-                },
-            )
-            .child(name_row.overflow_x_hidden())
+            .hover(|s| s.bg(hover_bg))
+            .active(|s| s.bg(active_bg))
+            .child(name_row)
             .child(
                 div()
                     .id(checkbox_wrapper_id)
@@ -5119,6 +5078,35 @@ impl GitPanel {
                             }),
                     ),
             )
+            .on_click({
+                cx.listener(move |this, event: &ClickEvent, window, cx| {
+                    this.selected_entry = Some(ix);
+                    cx.notify();
+                    if event.modifiers().secondary() {
+                        this.open_file(&Default::default(), window, cx)
+                    } else {
+                        this.open_diff(&Default::default(), window, cx);
+                        this.focus_handle.focus(window, cx);
+                    }
+                })
+            })
+            .on_mouse_down(
+                MouseButton::Right,
+                move |event: &MouseDownEvent, window, cx| {
+                    // why isn't this happening automatically? we are passing MouseButton::Right to `on_mouse_down`?
+                    if event.button != MouseButton::Right {
+                        return;
+                    }
+
+                    let Some(this) = handle.upgrade() else {
+                        return;
+                    };
+                    this.update(cx, |this, cx| {
+                        this.deploy_entry_context_menu(event.position, ix, window, cx);
+                    });
+                    cx.stop_propagation();
+                },
+            )
             .into_any_element()
     }
 
@@ -5143,29 +5131,23 @@ impl GitPanel {
         let selected_bg_alpha = 0.08;
         let state_opacity_step = 0.04;
 
-        let base_bg = if selected {
-            cx.theme().status().info.alpha(selected_bg_alpha)
-        } else {
-            cx.theme().colors().ghost_element_background
-        };
+        let info_color = cx.theme().status().info;
+        let colors = cx.theme().colors();
 
-        let hover_bg = if selected {
-            cx.theme()
-                .status()
-                .info
-                .alpha(selected_bg_alpha + state_opacity_step)
+        let (base_bg, hover_bg, active_bg) = if selected {
+            (
+                info_color.alpha(selected_bg_alpha),
+                info_color.alpha(selected_bg_alpha + state_opacity_step),
+                info_color.alpha(selected_bg_alpha + state_opacity_step * 2.0),
+            )
         } else {
-            cx.theme().colors().ghost_element_hover
+            (
+                colors.ghost_element_background,
+                colors.ghost_element_hover,
+                colors.ghost_element_active,
+            )
         };
 
-        let active_bg = if selected {
-            cx.theme()
-                .status()
-                .info
-                .alpha(selected_bg_alpha + state_opacity_step * 2.0)
-        } else {
-            cx.theme().colors().ghost_element_active
-        };
         let folder_icon = if entry.expanded {
             IconName::FolderOpen
         } else {
@@ -5188,9 +5170,8 @@ impl GitPanel {
         };
 
         let name_row = h_flex()
-            .items_center()
+            .min_w_0()
             .gap_1()
-            .flex_1()
             .pl(px(entry.depth as f32 * TREE_INDENT))
             .child(
                 Icon::new(folder_icon)
@@ -5202,28 +5183,21 @@ impl GitPanel {
         h_flex()
             .id(id)
             .h(self.list_item_height())
+            .min_w_0()
             .w_full()
-            .items_center()
+            .pl_3()
+            .pr_1()
+            .gap_1p5()
+            .justify_between()
             .border_1()
             .border_r_2()
             .when(selected && self.focus_handle.is_focused(window), |el| {
                 el.border_color(cx.theme().colors().panel_focused_border)
             })
-            .px(rems(0.75))
-            .overflow_hidden()
-            .flex_none()
-            .gap_1p5()
             .bg(base_bg)
-            .hover(|this| this.bg(hover_bg))
-            .active(|this| this.bg(active_bg))
-            .on_click({
-                let key = entry.key.clone();
-                cx.listener(move |this, _event: &ClickEvent, window, cx| {
-                    this.selected_entry = Some(ix);
-                    this.toggle_directory(&key, window, cx);
-                })
-            })
-            .child(name_row.overflow_x_hidden())
+            .hover(|s| s.bg(hover_bg))
+            .active(|s| s.bg(active_bg))
+            .child(name_row)
             .child(
                 div()
                     .id(checkbox_wrapper_id)
@@ -5262,12 +5236,18 @@ impl GitPanel {
                             }),
                     ),
             )
+            .on_click({
+                let key = entry.key.clone();
+                cx.listener(move |this, _event: &ClickEvent, window, cx| {
+                    this.selected_entry = Some(ix);
+                    this.toggle_directory(&key, window, cx);
+                })
+            })
             .into_any_element()
     }
 
     fn path_formatted(
         &self,
-        parent: Div,
         directory: Option<String>,
         path_color: Color,
         file_name: String,
@@ -5276,41 +5256,31 @@ impl GitPanel {
         git_path_style: GitPathStyle,
         strikethrough: bool,
     ) -> Div {
-        parent
-            .when(git_path_style == GitPathStyle::FileNameFirst, |this| {
-                this.child(
-                    self.entry_label(
-                        match directory.as_ref().is_none_or(|d| d.is_empty()) {
-                            true => file_name.clone(),
-                            false => format!("{file_name} "),
-                        },
-                        label_color,
-                    )
-                    .when(strikethrough, Label::strikethrough),
-                )
-            })
-            .when_some(directory, |this, dir| {
-                match (
-                    !dir.is_empty(),
-                    git_path_style == GitPathStyle::FileNameFirst,
-                ) {
-                    (true, true) => this.child(
-                        self.entry_label(dir, path_color)
-                            .when(strikethrough, Label::strikethrough),
-                    ),
-                    (true, false) => this.child(
-                        self.entry_label(
-                            format!("{dir}{}", path_style.primary_separator()),
-                            path_color,
-                        )
+        let file_name_first = git_path_style == GitPathStyle::FileNameFirst;
+        let file_path_first = git_path_style == GitPathStyle::FilePathFirst;
+
+        let file_name = format!("{} ", file_name);
+
+        h_flex()
+            .min_w_0()
+            .overflow_hidden()
+            .when(file_path_first, |this| this.flex_row_reverse())
+            .child(
+                div().flex_none().child(
+                    self.entry_label(file_name, label_color)
                         .when(strikethrough, Label::strikethrough),
-                    ),
-                    _ => this,
-                }
-            })
-            .when(git_path_style == GitPathStyle::FilePathFirst, |this| {
+                ),
+            )
+            .when_some(directory, |this, dir| {
+                let path_name = if file_name_first {
+                    dir
+                } else {
+                    format!("{dir}{}", path_style.primary_separator())
+                };
+
                 this.child(
-                    self.entry_label(file_name, label_color)
+                    self.entry_label(path_name, path_color)
+                        .truncate()
                         .when(strikethrough, Label::strikethrough),
                 )
             })
@@ -5650,6 +5620,7 @@ impl GitPanelMessageTooltip {
         window: &mut Window,
         cx: &mut App,
     ) -> Entity<Self> {
+        let remote_url = repository.read(cx).default_remote_url();
         cx.new(|cx| {
             cx.spawn_in(window, async move |this, cx| {
                 let (details, workspace) = git_panel.update(cx, |git_panel, cx| {
@@ -5659,16 +5630,21 @@ impl GitPanelMessageTooltip {
                     )
                 })?;
                 let details = details.await?;
+                let provider_registry = cx
+                    .update(|_, app| GitHostingProviderRegistry::default_global(app))
+                    .ok();
 
                 let commit_details = crate::commit_tooltip::CommitDetails {
                     sha: details.sha.clone(),
                     author_name: details.author_name.clone(),
                     author_email: details.author_email.clone(),
                     commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?,
-                    message: Some(ParsedCommitMessage {
-                        message: details.message,
-                        ..Default::default()
-                    }),
+                    message: Some(ParsedCommitMessage::parse(
+                        details.sha.to_string(),
+                        details.message.to_string(),
+                        remote_url.as_deref(),
+                        provider_registry,
+                    )),
                 };
 
                 this.update(cx, |this: &mut GitPanelMessageTooltip, cx| {

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

@@ -566,22 +566,22 @@ impl Model {
 
     pub fn max_token_count(&self) -> u64 {
         match self {
-            Self::Gemini25FlashLite => 1_048_576,
-            Self::Gemini25Flash => 1_048_576,
-            Self::Gemini25Pro => 1_048_576,
-            Self::Gemini3Pro => 1_048_576,
-            Self::Gemini3Flash => 1_048_576,
+            Self::Gemini25FlashLite
+            | Self::Gemini25Flash
+            | Self::Gemini25Pro
+            | Self::Gemini3Pro
+            | Self::Gemini3Flash => 1_048_576,
             Self::Custom { max_tokens, .. } => *max_tokens,
         }
     }
 
     pub fn max_output_tokens(&self) -> Option<u64> {
         match self {
-            Model::Gemini25FlashLite => Some(65_536),
-            Model::Gemini25Flash => Some(65_536),
-            Model::Gemini25Pro => Some(65_536),
-            Model::Gemini3Pro => Some(65_536),
-            Model::Gemini3Flash => Some(65_536),
+            Model::Gemini25FlashLite
+            | Model::Gemini25Flash
+            | Model::Gemini25Pro
+            | Model::Gemini3Pro
+            | Model::Gemini3Flash => Some(65_536),
             Model::Custom { .. } => None,
         }
     }

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

@@ -1025,13 +1025,26 @@ impl PlatformWindow for WaylandWindow {
     fn resize(&mut self, size: Size<Pixels>) {
         let state = self.borrow();
         let state_ptr = self.0.clone();
-        let dp_size = size.to_device_pixels(self.scale_factor());
+
+        // Keep window geometry consistent with configure handling. On Wayland, window geometry is
+        // surface-local: resizing should not attempt to translate the window; the compositor
+        // controls placement. We also account for client-side decoration insets and tiling.
+        let window_geometry = inset_by_tiling(
+            Bounds {
+                origin: Point::default(),
+                size,
+            },
+            state.inset(),
+            state.tiling,
+        )
+        .map(|v| v.0 as i32)
+        .map_size(|v| if v <= 0 { 1 } else { v });
 
         state.surface_state.set_geometry(
-            state.bounds.origin.x.0 as i32,
-            state.bounds.origin.y.0 as i32,
-            dp_size.width.0,
-            dp_size.height.0,
+            window_geometry.origin.x,
+            window_geometry.origin.y,
+            window_geometry.size.width,
+            window_geometry.size.height,
         );
 
         state

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

@@ -40,6 +40,11 @@ impl WindowsWindowInner {
         lparam: LPARAM,
     ) -> LRESULT {
         let handled = match msg {
+            // eagerly activate the window, so calls to `active_window` will work correctly
+            WM_MOUSEACTIVATE => {
+                unsafe { SetActiveWindow(handle).log_err() };
+                None
+            }
             WM_ACTIVATE => self.handle_activate_msg(wparam),
             WM_CREATE => self.handle_create_msg(handle),
             WM_MOVE => self.handle_move_msg(handle, lparam),

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

@@ -659,7 +659,7 @@ impl Platform for WindowsPlatform {
             if let Err(err) = result {
                 // ERROR_NOT_FOUND means the credential doesn't exist.
                 // Return Ok(None) to match macOS and Linux behavior.
-                if err.code().0 == ERROR_NOT_FOUND.0 as i32 {
+                if err.code() == ERROR_NOT_FOUND.to_hresult() {
                     return Ok(None);
                 }
                 return Err(err.into());

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

@@ -182,6 +182,11 @@ impl LineWrapper {
         // Cyrillic for Russian, Ukrainian, etc.
         // https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
         matches!(c, '\u{0400}'..='\u{04FF}') ||
+
+        // Vietnamese (https://vietunicode.sourceforge.net/charset/)
+        matches!(c, '\u{1E00}'..='\u{1EFF}') || // Latin Extended Additional
+        matches!(c, '\u{0300}'..='\u{036F}') || // Combining Diacritical Marks
+
         // Some other known special characters that should be treated as word characters,
         // e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`,
         // `2^3`, `a~b`, `a=1`, `Self::new`, etc.
@@ -618,7 +623,12 @@ mod tests {
         #[track_caller]
         fn assert_word(word: &str) {
             for c in word.chars() {
-                assert!(LineWrapper::is_word_char(c), "assertion failed for '{}'", c);
+                assert!(
+                    LineWrapper::is_word_char(c),
+                    "assertion failed for '{}' (unicode 0x{:x})",
+                    c,
+                    c as u32
+                );
             }
         }
 
@@ -661,6 +671,8 @@ mod tests {
         assert_word("Ζ€ΖΖ‚ΖƒΖ„Ζ…Ζ†Ζ‡ΖˆΖ‰ΖŠΖ‹ΖŒΖΖŽΖ");
         // Cyrillic
         assert_word("ΠΠ‘Π’Π“Π”Π•Π–Π—Π˜Π™ΠšΠ›ΠœΠΠžΠŸ");
+        // Vietnamese (https://github.com/zed-industries/zed/issues/23245)
+        assert_word("ThαΊ­mchΓ­Δ‘αΊΏnkhithuachαΊ‘ychΓΊngcΓ²nnhαΊ«ntΓ’mgiαΊΏtnα»‘tsα»‘Δ‘Γ΄ngtΓΉchΓ­nhtrα»‹α»ŸYΓͺnBΓ‘ivΓ CaoBαΊ±ng");
 
         // non-word characters
         assert_not_word("δ½ ε₯½");

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

@@ -4966,7 +4966,7 @@ impl<V: 'static> From<WindowHandle<V>> for AnyWindowHandle {
 }
 
 /// A handle to a window with any root view type, which can be downcast to a window with a specific root view type.
-#[derive(Copy, Clone, PartialEq, Eq, Hash)]
+#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
 pub struct AnyWindowHandle {
     pub(crate) id: WindowId,
     state_type: TypeId,

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

@@ -1801,9 +1801,7 @@ impl Buffer {
         self.syntax_map.lock().did_parse(syntax_snapshot);
         self.request_autoindent(cx);
         self.parse_status.0.send(ParseStatus::Idle).unwrap();
-        if self.text.version() != *self.tree_sitter_data.version() {
-            self.invalidate_tree_sitter_data(self.text.snapshot());
-        }
+        self.invalidate_tree_sitter_data(self.text.snapshot());
         cx.emit(BufferEvent::Reparsed);
         cx.notify();
     }

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

@@ -125,7 +125,7 @@ pub fn init(on_headless_host: bool, cx: &mut App) {
                     let server_id = server.server_id();
                     let weak_lsp_store = cx.weak_entity();
                     log_store.copilot_log_subscription =
-                        Some(server.on_notification::<copilot::request::LogMessage, _>(
+                        Some(server.on_notification::<lsp::notification::LogMessage, _>(
                             move |params, cx| {
                                 weak_lsp_store
                                     .update(cx, |lsp_store, cx| {

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

@@ -295,6 +295,23 @@ impl LspInstaller for TyLspAdapter {
         })
     }
 
+    async fn check_if_user_installed(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+        _: Option<Toolchain>,
+        _: &AsyncApp,
+    ) -> Option<LanguageServerBinary> {
+        let Some(ty_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await else {
+            return None;
+        };
+        let env = delegate.shell_env().await;
+        Some(LanguageServerBinary {
+            path: ty_bin,
+            env: Some(env),
+            arguments: vec!["server".into()],
+        })
+    }
+
     async fn fetch_server_binary(
         &self,
         latest_version: Self::BinaryVersion,

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

@@ -355,7 +355,7 @@ impl LspAdapter for RustLspAdapter {
                         | lsp::CompletionTextEdit::Edit(lsp::TextEdit { new_text, .. }),
                     ) = completion.text_edit.as_ref()
                     && let Ok(mut snippet) = snippet::Snippet::parse(new_text)
-                    && !snippet.tabstops.is_empty()
+                    && snippet.tabstops.len() > 1
                 {
                     label = String::new();
 
@@ -421,7 +421,9 @@ impl LspAdapter for RustLspAdapter {
                             0..label.rfind('(').unwrap_or(completion.label.len()),
                             highlight_id,
                         ));
-                    } else if detail_left.is_none() {
+                    } else if detail_left.is_none()
+                        && kind != Some(lsp::CompletionItemKind::SNIPPET)
+                    {
                         return None;
                     }
                 }
@@ -1597,6 +1599,40 @@ mod tests {
             ))
         );
 
+        // Postfix completion without actual tabstops (only implicit final $0)
+        // The label should use completion.label so it can be filtered by "ref"
+        let ref_completion = adapter
+            .label_for_completion(
+                &lsp::CompletionItem {
+                    kind: Some(lsp::CompletionItemKind::SNIPPET),
+                    label: "ref".to_string(),
+                    filter_text: Some("ref".to_string()),
+                    label_details: Some(CompletionItemLabelDetails {
+                        detail: None,
+                        description: Some("&expr".to_string()),
+                    }),
+                    detail: Some("&expr".to_string()),
+                    insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                        range: lsp::Range::default(),
+                        new_text: "&String::new()".to_string(),
+                    })),
+                    ..Default::default()
+                },
+                &language,
+            )
+            .await;
+        assert!(
+            ref_completion.is_some(),
+            "ref postfix completion should have a label"
+        );
+        let ref_label = ref_completion.unwrap();
+        let filter_text = &ref_label.text[ref_label.filter_range.clone()];
+        assert!(
+            filter_text.contains("ref"),
+            "filter range text '{filter_text}' should contain 'ref' for filtering to work",
+        );
+
         // Test for correct range calculation with mixed empty and non-empty tabstops.(See https://github.com/zed-industries/zed/issues/44825)
         let res = adapter
             .label_for_completion(

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

@@ -155,15 +155,15 @@ impl Model {
     pub fn max_token_count(&self) -> u64 {
         match self {
             Self::CodestralLatest => 256000,
-            Self::MistralLargeLatest => 131000,
+            Self::MistralLargeLatest => 256000,
             Self::MistralMediumLatest => 128000,
             Self::MistralSmallLatest => 32000,
-            Self::MagistralMediumLatest => 40000,
-            Self::MagistralSmallLatest => 40000,
+            Self::MagistralMediumLatest => 128000,
+            Self::MagistralSmallLatest => 128000,
             Self::OpenMistralNemo => 131000,
             Self::OpenCodestralMamba => 256000,
-            Self::DevstralMediumLatest => 128000,
-            Self::DevstralSmallLatest => 262144,
+            Self::DevstralMediumLatest => 256000,
+            Self::DevstralSmallLatest => 256000,
             Self::Pixtral12BLatest => 128000,
             Self::PixtralLargeLatest => 128000,
             Self::Custom { max_tokens, .. } => *max_tokens,

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

@@ -460,7 +460,7 @@ impl AgentServerStore {
                     .gemini
                     .as_ref()
                     .and_then(|settings| settings.ignore_system_version)
-                    .unwrap_or(false),
+                    .unwrap_or(true),
             }),
         );
         self.external_agents.insert(

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

@@ -1672,59 +1672,6 @@ impl GitStore {
         }
     }
 
-    fn mark_entries_pending_by_project_paths(
-        &mut self,
-        project_paths: &[ProjectPath],
-        stage: bool,
-        cx: &mut Context<Self>,
-    ) {
-        let buffer_store = &self.buffer_store;
-
-        for project_path in project_paths {
-            let Some(buffer) = buffer_store.read(cx).get_by_path(project_path) else {
-                continue;
-            };
-
-            let buffer_id = buffer.read(cx).remote_id();
-            let Some(diff_state) = self.diffs.get(&buffer_id) else {
-                continue;
-            };
-
-            diff_state.update(cx, |diff_state, cx| {
-                let Some(uncommitted_diff) = diff_state.uncommitted_diff() else {
-                    return;
-                };
-
-                let buffer_snapshot = buffer.read(cx).text_snapshot();
-                let file_exists = buffer
-                    .read(cx)
-                    .file()
-                    .is_some_and(|file| file.disk_state().exists());
-
-                let all_hunks: Vec<_> = uncommitted_diff
-                    .read(cx)
-                    .hunks_intersecting_range(
-                        text::Anchor::MIN..text::Anchor::MAX,
-                        &buffer_snapshot,
-                        cx,
-                    )
-                    .collect();
-
-                if !all_hunks.is_empty() {
-                    uncommitted_diff.update(cx, |diff, cx| {
-                        diff.stage_or_unstage_hunks(
-                            stage,
-                            &all_hunks,
-                            &buffer_snapshot,
-                            file_exists,
-                            cx,
-                        );
-                    });
-                }
-            });
-        }
-    }
-
     pub fn git_clone(
         &self,
         repo: String,
@@ -4253,28 +4200,6 @@ impl Repository {
         save_futures
     }
 
-    fn mark_entries_pending_for_stage(
-        &self,
-        entries: &[RepoPath],
-        stage: bool,
-        cx: &mut Context<Self>,
-    ) {
-        let Some(git_store) = self.git_store() else {
-            return;
-        };
-
-        let mut project_paths = Vec::new();
-        for repo_path in entries {
-            if let Some(project_path) = self.repo_path_to_project_path(repo_path, cx) {
-                project_paths.push(project_path);
-            }
-        }
-
-        git_store.update(cx, move |git_store, cx| {
-            git_store.mark_entries_pending_by_project_paths(&project_paths, stage, cx);
-        });
-    }
-
     pub fn stage_entries(
         &mut self,
         entries: Vec<RepoPath>,
@@ -4283,9 +4208,6 @@ impl Repository {
         if entries.is_empty() {
             return Task::ready(Ok(()));
         }
-
-        self.mark_entries_pending_for_stage(&entries, true, cx);
-
         let id = self.id;
         let save_tasks = self.save_buffers(&entries, cx);
         let paths = entries
@@ -4351,9 +4273,6 @@ impl Repository {
         if entries.is_empty() {
             return Task::ready(Ok(()));
         }
-
-        self.mark_entries_pending_for_stage(&entries, false, cx);
-
         let id = self.id;
         let save_tasks = self.save_buffers(&entries, cx);
         let paths = entries
@@ -5948,6 +5867,11 @@ impl Repository {
         self.pending_ops.edit(edits, ());
         ids
     }
+    pub fn default_remote_url(&self) -> Option<String> {
+        self.remote_upstream_url
+            .clone()
+            .or(self.remote_origin_url.clone())
+    }
 }
 
 fn get_permalink_in_rust_registry_src(

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

@@ -112,7 +112,7 @@ pub struct ContentPromptContextV2 {
     pub language_name: Option<String>,
     pub is_truncated: bool,
     pub document_content: String,
-    pub rewrite_section: Option<String>,
+    pub rewrite_section: String,
     pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
 }
 
@@ -310,7 +310,6 @@ impl PromptBuilder {
         };
 
         const MAX_CTX: usize = 50000;
-        let is_insert = range.is_empty();
         let mut is_truncated = false;
 
         let before_range = 0..range.start;
@@ -335,28 +334,19 @@ impl PromptBuilder {
         for chunk in buffer.text_for_range(truncated_before) {
             document_content.push_str(chunk);
         }
-        if is_insert {
-            document_content.push_str("<insert_here></insert_here>");
-        } else {
-            document_content.push_str("<rewrite_this>\n");
-            for chunk in buffer.text_for_range(range.clone()) {
-                document_content.push_str(chunk);
-            }
-            document_content.push_str("\n</rewrite_this>");
+
+        document_content.push_str("<rewrite_this>\n");
+        for chunk in buffer.text_for_range(range.clone()) {
+            document_content.push_str(chunk);
         }
+        document_content.push_str("\n</rewrite_this>");
+
         for chunk in buffer.text_for_range(truncated_after) {
             document_content.push_str(chunk);
         }
 
-        let rewrite_section = if !is_insert {
-            let mut section = String::new();
-            for chunk in buffer.text_for_range(range.clone()) {
-                section.push_str(chunk);
-            }
-            Some(section)
-        } else {
-            None
-        };
+        let rewrite_section: String = buffer.text_for_range(range.clone()).collect();
+
         let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
         let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
             .map(|entry| {

crates/supermaven/src/supermaven_edit_prediction_delegate.rs πŸ”—

@@ -1,6 +1,6 @@
 use crate::{Supermaven, SupermavenCompletionStateId};
 use anyhow::Result;
-use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
+use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
 use futures::StreamExt as _;
 use gpui::{App, Context, Entity, EntityId, Task};
 use language::{Anchor, Buffer, BufferSnapshot};
@@ -189,15 +189,6 @@ impl EditPredictionDelegate for SupermavenEditPredictionDelegate {
         }));
     }
 
-    fn cycle(
-        &mut self,
-        _buffer: Entity<Buffer>,
-        _cursor_position: Anchor,
-        _direction: Direction,
-        _cx: &mut Context<Self>,
-    ) {
-    }
-
     fn accept(&mut self, _cx: &mut Context<Self>) {
         reset_completion_cache(self, _cx);
     }

crates/terminal/src/terminal_hyperlinks.rs πŸ”—

@@ -11,6 +11,7 @@ use alacritty_terminal::{
 use log::{info, warn};
 use regex::Regex;
 use std::{
+    iter::{once, once_with},
     ops::{Index, Range},
     time::{Duration, Instant},
 };
@@ -232,14 +233,17 @@ fn path_match<T>(
         (line_end.line.0 - line_start.line.0 + 1) as usize * term.grid().columns(),
     );
     let first_cell = &term.grid()[line_start];
+    let mut prev_len = 0;
     line.push(first_cell.c);
-    let mut start_offset = 0;
+    let mut prev_char_is_space = first_cell.c == ' ';
     let mut hovered_point_byte_offset = None;
+    let mut hovered_word_start_offset = None;
+    let mut hovered_word_end_offset = None;
 
-    if !first_cell.flags.intersects(WIDE_CHAR_SPACERS) {
-        start_offset += first_cell.c.len_utf8();
-        if line_start == hovered {
-            hovered_point_byte_offset = Some(0);
+    if line_start == hovered {
+        hovered_point_byte_offset = Some(0);
+        if first_cell.c != ' ' {
+            hovered_word_start_offset = Some(0);
         }
     }
 
@@ -247,27 +251,44 @@ fn path_match<T>(
         if cell.point > line_end {
             break;
         }
-        let is_spacer = cell.flags.intersects(WIDE_CHAR_SPACERS);
-        if cell.point == hovered {
-            debug_assert!(hovered_point_byte_offset.is_none());
-            if start_offset > 0 && cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
-                // If we hovered on a trailing spacer, back up to the end of the previous char's bytes.
-                start_offset -= 1;
+
+        if !cell.flags.intersects(WIDE_CHAR_SPACERS) {
+            prev_len = line.len();
+            match cell.c {
+                ' ' | '\t' => {
+                    if hovered_point_byte_offset.is_some() && !prev_char_is_space {
+                        if hovered_word_end_offset.is_none() {
+                            hovered_word_end_offset = Some(line.len());
+                        }
+                    }
+                    line.push(' ');
+                    prev_char_is_space = true;
+                }
+                c @ _ => {
+                    if hovered_point_byte_offset.is_none() && prev_char_is_space {
+                        hovered_word_start_offset = Some(line.len());
+                    }
+                    line.push(c);
+                    prev_char_is_space = false;
+                }
             }
-            hovered_point_byte_offset = Some(start_offset);
-        } else if cell.point < hovered && !is_spacer {
-            start_offset += cell.c.len_utf8();
         }
 
-        if !is_spacer {
-            line.push(match cell.c {
-                '\t' => ' ',
-                c @ _ => c,
-            });
+        if cell.point == hovered {
+            debug_assert!(hovered_point_byte_offset.is_none());
+            hovered_point_byte_offset = Some(prev_len);
         }
     }
     let line = line.trim_ascii_end();
     let hovered_point_byte_offset = hovered_point_byte_offset?;
+    let hovered_word_range = {
+        let word_start_offset = hovered_word_start_offset.unwrap_or(0);
+        (word_start_offset != 0)
+            .then_some(word_start_offset..hovered_word_end_offset.unwrap_or(line.len()))
+    };
+    if line.len() <= hovered_point_byte_offset {
+        return None;
+    }
     let found_from_range = |path_range: Range<usize>,
                             link_range: Range<usize>,
                             position: Option<(u32, Option<u32>)>| {
@@ -313,10 +334,27 @@ fn path_match<T>(
     for regex in path_hyperlink_regexes {
         let mut path_found = false;
 
-        for captures in regex.captures_iter(&line) {
+        for (line_start_offset, captures) in once(
+            regex
+                .captures_iter(&line)
+                .next()
+                .map(|captures| (0, captures)),
+        )
+        .chain(once_with(|| {
+            if let Some(hovered_word_range) = &hovered_word_range {
+                regex
+                    .captures_iter(&line[hovered_word_range.clone()])
+                    .next()
+                    .map(|captures| (hovered_word_range.start, captures))
+            } else {
+                None
+            }
+        }))
+        .flatten()
+        {
             path_found = true;
             let match_range = captures.get(0).unwrap().range();
-            let (path_range, line_column) = if let Some(path) = captures.name("path") {
+            let (mut path_range, line_column) = if let Some(path) = captures.name("path") {
                 let parse = |name: &str| {
                     captures
                         .name(name)
@@ -330,10 +368,15 @@ fn path_match<T>(
             } else {
                 (match_range.clone(), None)
             };
-            let link_range = captures
+            let mut link_range = captures
                 .name("link")
                 .map_or_else(|| match_range.clone(), |link| link.range());
 
+            path_range.start += line_start_offset;
+            path_range.end += line_start_offset;
+            link_range.start += line_start_offset;
+            link_range.end += line_start_offset;
+
             if !link_range.contains(&hovered_point_byte_offset) {
                 // No match, just skip.
                 continue;
@@ -638,9 +681,6 @@ mod tests {
             test_path!(
                 "β€ΉΒ«πŸ¦€ multiple_πŸ‘‰same_line πŸ¦€Β» 🚣«4Β» πŸ›οΈΒ«2Β»β€Ί: πŸ¦€ multiple_same_line πŸ¦€ 🚣4 πŸ›οΈ2:"
             );
-            test_path!(
-                "πŸ¦€ multiple_same_line πŸ¦€ 🚣4 πŸ›οΈ2 β€ΉΒ«πŸ¦€ multiple_πŸ‘‰same_line πŸ¦€Β» 🚣«4Β» πŸ›οΈΒ«2Β»β€Ί:"
-            );
 
             // ls output (tab separated)
             test_path!(
@@ -977,7 +1017,7 @@ mod tests {
             use crate::TerminalSettings;
             use alacritty_terminal::{
                 event::VoidListener,
-                grid::Dimensions,
+                grid::Scroll,
                 index::{Column, Point as AlacPoint},
                 term::test::mock_term,
                 term::{Term, search::Match},
@@ -986,14 +1026,20 @@ mod tests {
             use std::{cell::RefCell, rc::Rc};
             use util_macros::perf;
 
-            fn build_test_term(line: &str) -> (Term<VoidListener>, AlacPoint) {
-                let content = line.repeat(500);
-                let term = mock_term(&content);
-                let point = AlacPoint::new(
-                    term.grid().bottommost_line() - 1,
-                    Column(term.grid().last_column().0 / 2),
-                );
-
+            fn build_test_term(
+                line: &str,
+                repeat: usize,
+                hover_offset_column: usize,
+            ) -> (Term<VoidListener>, AlacPoint) {
+                let content = line.repeat(repeat);
+                let mut term = mock_term(&content);
+                term.resize(TermSize {
+                    columns: 1024,
+                    screen_lines: 10,
+                });
+                term.scroll_display(Scroll::Top);
+                let point =
+                    AlacPoint::new(Line(term.topmost_line().0 + 3), Column(hover_offset_column));
                 (term, point)
             }
 
@@ -1002,11 +1048,14 @@ mod tests {
                 const LINE: &str = "    Compiling terminal v0.1.0 (/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal)\r\n";
                 thread_local! {
                     static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
-                        build_test_term(LINE);
+                        build_test_term(LINE, 500, 50);
                 }
                 TEST_TERM_AND_POINT.with(|(term, point)| {
-                    assert!(
-                        find_from_grid_point_bench(term, *point).is_some(),
+                    assert_eq!(
+                        find_from_grid_point_bench(term, *point)
+                            .map(|(path, ..)| path)
+                            .unwrap_or_default(),
+                        "/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal",
                         "Hyperlink should have been found"
                     );
                 });
@@ -1017,11 +1066,14 @@ mod tests {
                 const LINE: &str = "    --> /Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42\r\n";
                 thread_local! {
                     static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
-                        build_test_term(LINE);
+                        build_test_term(LINE, 500, 50);
                 }
                 TEST_TERM_AND_POINT.with(|(term, point)| {
-                    assert!(
-                        find_from_grid_point_bench(term, *point).is_some(),
+                    assert_eq!(
+                        find_from_grid_point_bench(term, *point)
+                            .map(|(path, ..)| path)
+                            .unwrap_or_default(),
+                        "/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42",
                         "Hyperlink should have been found"
                     );
                 });
@@ -1032,11 +1084,111 @@ mod tests {
                 const LINE: &str = "Cargo.toml        experiments        notebooks        rust-toolchain.toml    tooling\r\n";
                 thread_local! {
                     static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
-                        build_test_term(LINE);
+                        build_test_term(LINE, 500, 60);
                 }
                 TEST_TERM_AND_POINT.with(|(term, point)| {
-                    assert!(
-                        find_from_grid_point_bench(term, *point).is_some(),
+                    assert_eq!(
+                        find_from_grid_point_bench(term, *point)
+                            .map(|(path, ..)| path)
+                            .unwrap_or_default(),
+                        "rust-toolchain.toml",
+                        "Hyperlink should have been found"
+                    );
+                });
+            }
+
+            #[perf]
+            // https://github.com/zed-industries/zed/pull/44407
+            pub fn pr_44407_hyperlink_benchmark() {
+                const LINE: &str = "-748, 706, 163, 222, -980, 949, 381, -568, 199, 501, 760, -821, 90, -451, 183, 867, -351, -810, -762, -109, 423, 84, 14, -77, -820, -345, 74, -791, 930, -618, -900, 862, -959, 289, -19, 471, -757, 793, 155, -554, 249, 830, 402, 732, -731, -866, -720, -703, -257, -439, 731, 872, -489, 676, -167, 613, -698, 415, -80, -453, -896, 333, -511, 621, -450, 624, -309, -575, 177, 141, 891, -104, -97, -367, -599, -675, 607, -225, -760, 552, -465, 804, 55, 282, 104, -929, -252,\
+-311, 900, 550, 599, -80, 774, 553, 837, -395, 541, 953, 154, -396, -596, -111, -802, -221, -337, -633, -73, -527, -82, -658, -264, 222, 375, 434, 204, -756, -703, 303, 239, -257, -365, -351, 904, 364, -743, -484, 655, -542, 446, 888, 632, -167, -260, 716, 150, 806, 723, 513, -118, -323, -683, 983, -564, 358, -16, -287, 277, -607, 87, 365, -1, 164, 401, 257, 369, -893, 145, -969, 375, -53, 541, -408, -865, 753, 258, 337, -886, 593, -378, -528, 191, 204, 566, -61, -621, 769, 524, -628, 6,\
+249, 896, -785, -776, 321, -681, 604, -740, 886, 426, -480, -983, 23, -247, 125, -666, 913, 842, -460, -797, -483, -58, -565, -587, -206, 197, 715, 764, -97, 457, -149, -226, 261, 194, -390, 431, 180, -778, 829, -657, -668, 397, 859, 152, -178, 677, -18, 687, -247, 96, 466, -572, 478, 622, -143, -25, -471, 265, 335, 957, 152, -951, -647, 670, 57, 152, -115, 206, 87, 629, -798, -125, -725, -31, 844, 398, -876, 44, 963, -211, 518, -8, -103, -999, 948, 823, 149, -803, 769, -236, -683, 527,\
+-108, -36, 18, -437, 687, -305, -526, 972, -965, 276, 420, -259, -379, -142, -747, 600, -578, 197, 673, 890, 324, -931, 755, -765, -422, 785, -369, -110, -505, 532, -208, -438, 713, 110, 853, 996, -360, 823, 289, -699, 629, -661, 560, -329, -323, 439, 571, -537, 644, -84, 25, -536, -161, 112, 169, -922, -537, -734, -423, 37, 451, -149, 408, 18, -672, 206, -784, 444, 593, -241, 502, -259, -798, -352, -658, 712, -675, -734, 627, -620, 64, -554, 999, -537, -160, -641, 464, 894, 29, 322, 566,\
+-510, -749, 982, 204, 967, -261, -986, -136, 251, -598, 995, -831, 891, 22, 761, -783, -415, 125, 470, -919, -97, -668, 85, 205, -175, -550, 502, 652, -468, 798, 775, -216, 89, -433, -24, -621, 877, -126, 951, 809, 782, 156, -618, -841, -463, 19, -723, -904, 550, 263, 991, -758, -114, 446, -731, -623, -634, 462, 48, 851, 333, -846, 480, 892, -966, -910, -436, 317, -711, -341, -294, 124, 238, -214, -281, 467, -950, -342, 913, -90, -388, -573, 740, -883, -451, 493, -500, 863, 930, 127, 530,\
+-810, 540, 541, -664, -951, -227, -420, -476, -581, -534, 549, 253, 984, -985, -84, -521, 538, 484, -440, 371, 784, -306, -850, 530, -133, 251, -799, 446, -170, -243, -674, 769, 646, 778, -680, -714, -442, 804, 901, -774, 69, 307, -293, 755, 443, 224, -918, -771, 723, 40, 132, 568, -847, -47, 844, 69, 986, -293, -459, 313, 155, 331, 69, 280, -637, 569, 104, -119, -988, 252, 857, -590, 810, -891, 484, 566, -934, -587, -290, 566, 587, 489, 870, 280, 454, -252, 613, -701, -278, 195, -198,\
+683, 533, -372, 707, -152, 371, 866, 609, -5, -372, -30, -694, 552, 192, 452, -663, 350, -985, 10, 884, 813, -592, -331, -470, 711, -941, 928, 379, -339, 220, 999, 376, 507, 179, 916, 84, 104, 392, 192, 299, -860, 218, -698, -919, -452, 37, 850, 5, -874, 287, 123, -746, -575, 776, -909, 118, 903, -275, 450, -996, -591, -920, -850, 453, -896, 73, 83, -535, -20, 287, -765, 442, 808, 45, 445, 202, 917, -208, 783, 790, -534, 373, -129, 556, -757, -69, 459, -163, -59, 265, -563, -889, 635,\
+-583, -261, -790, 799, 826, 953, 85, 619, 334, 842, 672, -869, -4, -833, 315, 942, -524, 579, 926, 628, -404, 128, -629, 161, 568, -117, -526, 223, -876, 906, 176, -549, -317, 381, 375, -801, -416, 647, 335, 253, -386, -375, -254, 635, 352, 317, 398, -422, 111, 201, 220, 554, -972, 853, 378, 956, 942, -857, -289, -333, -180, 488, -814, -42, -595, 721, 39, 644, 721, -242, -44, 643, -457, -419, 560, -863, 974, 458, 222, -882, 526, -243, -318, -343, -707, -401, 117, 677, -489, 546, -903,\
+-960, -881, -684, 125, -928, -995, -692, -773, 647, -718, -862, -814, 671, 664, -130, -856, -674, 653, 711, 194, -685, -160, 138, -27, -128, -671, -242, 526, 494, -674, 424, -921, -778, 313, -237, 332, 913, 252, 808, -936, 289, 755, 52, -139, 57, -19, -827, -775, -561, -14, 107, -84, 622, -303, -747, 258, -942, 290, 211, -919, -207, 797, 95, 794, -830, -181, -788, 757, 75, -946, -949, -988, 152, 340, 732, 886, -891, -642, -666, 321, -910, 841, 632, 298, 55, -349, 498, 287, -711, 97, 305,\
+-974, -987, 790, -64, 605, -583, -821, 345, 887, -861, 548, 894, 288, 452, 556, -448, 813, 420, 545, 967, 127, -947, 19, -314, -607, -513, -851, 254, -290, -938, -783, -93, 474, 368, -485, -935, -539, 81, 404, -283, 779, 345, -164, 53, 563, -771, 911, -323, 522, -998, 315, 415, 460, 58, -541, -878, -152, -886, 201, -446, -810, 549, -142, -575, -632, 521, 549, 209, -681, 998, 798, -611, -919, -708, -4, 677, -172, 588, 750, -435, 508, 609, 498, -535, -691, -738, 85, 615, 705, 169, 425,\
+-669, -491, -783, 73, -847, 228, -981, -812, -229, 950, -904, 175, -438, 632, -556, 910, 173, 576, -751, -53, -169, 635, 607, -944, -13, -84, 105, -644, 984, 935, 259, -445, 620, -405, 832, 167, 114, 209, -181, -944, -496, 693, -473, 137, 38, -873, -334, -353, -57, 397, 944, 698, 811, -401, 712, -667, 905, 276, -653, 368, -543, -349, 414, 287, 894, 935, 461, 55, 741, -623, -660, -773, 617, 834, 278, -121, 52, 495, -855, -440, -210, -99, 279, -661, 540, 934, 540, 784, 895, 268, -503, 513,\
+-484, -352, 528, 341, -451, 885, -71, 799, -195, -885, -585, -233, 92, 453, 994, 464, 694, 190, -561, -116, 675, -775, -236, 556, -110, -465, 77, -781, 507, -960, -410, 229, -632, 717, 597, 429, 358, -430, -692, -825, 576, 571, 758, -891, 528, -267, 190, -869, 132, -811, 796, 750, -596, -681, 870, 360, 969, 860, -412, -567, 694, -86, -498, 38, -178, -583, -778, 412, 842, -586, 722, -192, 350, 363, 81, -677, -163, 564, 543, 671, 110, 314, 739, -552, -224, -644, 922, 685, 134, 613, 793,\
+-363, -244, -284, -257, -561, 418, 988, 333, 110, -966, 790, 927, 536, -620, -309, -358, 895, -867, -796, -357, 308, -740, 287, -732, -363, -969, 658, 711, 511, 256, 590, -574, 815, -845, -84, 546, -581, -71, -334, -890, 652, -959, 320, -236, 445, -851, 825, -756, -4, 877, 308, 573, -117, 293, 686, -483, 391, 342, -550, -982, 713, 886, 552, 474, -673, 283, -591, -383, 988, 435, -131, 708, -326, -884, 87, 680, -818, -408, -486, 813, -307, -799, 23, -497, 802, -146, -100, 541, 7, -493, 577,\
+50, -270, 672, 834, 111, -788, 247, 337, 628, -33, -964, -519, 683, 54, -703, 633, -127, -448, 759, -975, 696, 2, -870, -760, 67, 696, 306, 750, 615, 155, -933, -568, 399, 795, 164, -460, 205, 439, -526, -691, 35, -136, -481, -63, 73, -598, 748, 133, 874, -29, 4, -73, 472, 389, 962, 231, -328, 240, 149, 959, 46, -207, 72, -514, -608, 0, -14, 32, 374, -478, -806, 919, -729, -286, 652, 109, 509, -879, -979, -865, 584, -92, -346, -992, 781, 401, 575, 993, -746, -33, 684, -683, 750, -105,\
+-425, -508, -627, 27, 770, -45, 338, 921, -139, -392, -933, 634, 563, 224, -780, 921, 991, 737, 22, 64, 414, -249, -687, 869, 50, 759, -97, 515, 20, -775, -332, 957, 138, -542, -835, 591, -819, 363, -715, -146, -950, -641, -35, -435, -407, -548, -984, 383, -216, -559, 853, 4, -410, -319, -831, -459, -628, -819, -324, 755, 696, -192, 238, -234, -724, -445, 915, 302, -708, 484, 224, -641, 25, -771, 528, -106, -744, -588, 913, -554, -515, -239, -843, -812, -171, 721, 543, -269, 440, 151,\
+996, -723, -557, -522, -280, -514, -593, 208, 715, 404, 353, 270, -483, -785, 318, -313, 798, 638, 764, 748, -929, -827, -318, -56, 389, -546, -958, -398, 463, -700, 461, 311, -787, -488, 877, 456, 166, 535, -995, -189, -715, 244, 40, 484, 212, -329, -351, 638, -69, -446, -292, 801, -822, 490, -486, -185, 790, 370, -340, 401, -656, 584, 561, -749, 269, -19, -294, -111, 975, 874, -73, 851, 231, -331, -684, 460, 765, -654, -76, 10, 733, 520, 521, 416, -958, -202, -186, -167, 175, 343, -50,\
+673, -763, -854, -977, -17, -853, -122, -25, 180, 149, 268, 874, -816, -745, 747, -303, -959, 390, 509, 18, -66, 275, -277, 9, 837, -124, 989, -542, -649, -845, 894, 926, 997, -847, -809, -579, -96, -372, 766, 238, -251, 503, 559, 276, -281, -102, -735, 815, 109, 175, -10, 128, 543, -558, -707, 949, 996, -422, -506, 252, 702, -930, 552, -961, 584, -79, -177, 341, -275, 503, -21, 677, -545, 8, -956, -795, -870, -254, 170, -502, -880, 106, 174, 459, 603, -600, -963, 164, -136, -641, -309,\
+-380, -707, -727, -10, 727, 952, 997, -731, -133, 269, 287, 855, 716, -650, 479, 299, -839, -308, -782, 769, 545, 663, -536, -115, 904, -986, -258, -562, 582, 664, 408, -525, -889, 471, -370, -534, -220, 310, 766, 931, -193, -897, -192, -74, -365, -256, -359, -328, 658, -691, -431, 406, 699, 425, 713, -584, -45, -588, 289, 658, -290, -880, -987, -444, 371, 904, -155, 81, -278, -708, -189, -78, 655, 342, -998, -647, -734, -218, 726, 619, 663, 744, 518, 60, -409, 561, -727, -961, -306,\
+-147, -550, 240, -218, -393, 267, 724, 791, -548, 480, 180, -631, 825, -170, 107, 227, -691, 905, -909, 359, 227, 287, 909, 632, -89, -522, 80, -429, 37, 561, -732, -474, 565, -798, -460, 188, 507, -511, -654, 212, -314, -376, -997, -114, -708, 512, -848, 781, 126, -956, -298, 354, -400, -121, 510, 445, 926, 27, -708, 676, 248, 834, 542, 236, -105, -153, 102, 128, 96, -348, -626, 598, 8, 978, -589, -461, -38, 381, -232, -817, 467, 356, -151, -460, 429, -408, 425, 618, -611, -247, 819,\
+963, -160, 1000, 141, -647, -875, 108, 790, -127, 463, -37, -195, -542, 12, 845, -384, 770, -129, 315, 826, -942, 430, 146, -170, -583, -903, -489, 497, -559, -401, -29, -129, -411, 166, 942, -646, -862, -404, 785, 777, -111, -481, -738, 490, 741, -398, 846, -178, -509, -661, 748, 297, -658, -567, 531, 427, -201, -41, -808, -668, 782, -860, -324, 249, 835, -234, 116, 542, -201, 328, 675, 480, -906, 188, 445, 63, -525, 811, 277, 133, 779, -680, 950, -477, -306, -64, 552, -890, -956, 169,\
+442, 44, -169, -243, -242, 423, -884, -757, -403, 739, -350, 383, 429, 153, -702, -725, 51, 310, 857, -56, 538, 46, -311, 132, -620, -297, -124, 534, 884, -629, -117, 506, -837, -100, -27, -381, -735, 262, 843, 703, 260, -457, 834, 469, 9, 950, 59, 127, -820, 518, 64, -783, 659, -608, -676, 802, 30, 589, 246, -369, 361, 347, 534, -376, 68, 941, 709, 264, 384, 481, 628, 199, -568, -342, -337, 853, -804, -858, -169, -270, 641, -344, 112, 530, -773, -349, -135, -367, -350, -756, -911, 180,\
+-660, 116, -478, -265, -581, 510, 520, -986, 935, 219, 522, 744, 47, -145, 917, 638, 301, 296, 858, -721, 511, -816, 328, 473, 441, 697, -260, -673, -379, 893, 458, 154, 86, 905, 590, 231, -717, -179, 79, 272, -439, -192, 178, -200, 51, 717, -256, -358, -626, -518, -314, -825, -325, 588, 675, -892, -798, 448, -518, 603, -23, 668, -655, 845, -314, 783, -347, -496, 921, 893, -163, -748, -906, 11, -143, -64, 300, 336, 882, 646, 533, 676, -98, -148, -607, -952, -481, -959, -874, 764, 537,\
+736, -347, 646, -843, 966, -916, -718, -391, -648, 740, 755, 919, -608, 388, -655, 68, 201, 675, -855, 7, -503, 881, 760, 669, 831, 721, -564, -445, 217, 331, 970, 521, 486, -254, 25, -259, 336, -831, 252, -995, 908, -412, -240, 123, -478, 366, 264, -504, -843, 632, -288, 896, 301, 423, 185, 318, 380, 457, -450, -162, -313, 673, -963, 570, 433, -548, 107, -39, -142, -98, -884, -3, 599, -486, -926, 923, -82, 686, 290, 99, -382, -789, 16, 495, 570, 284, 474, -504, -201, -178, -1, 592, 52,\
+827, -540, -151, -991, 130, 353, -420, -467, -661, 417, -690, 942, 936, 814, -566, -251, -298, 341, -139, 786, 129, 525, -861, 680, 955, -245, -50, 331, 412, -38, -66, 611, -558, 392, -629, -471, -68, -535, 744, 495, 87, 558, 695, 260, -308, 215, -464, 239, -50, 193, -540, 184, -8, -194, 148, 898, -557, -21, 884, 644, -785, -689, -281, -737, 267, 50, 206, 292, 265, 380, -511, 310, 53, 375, -497, -40, 312, -606, -395, 142, 422, 662, -584, 72, 144, 40, -679, -593, 581, 689, -829, 442, 822,\
+977, -832, -134, -248, -207, 248, 29, 259, 189, 592, -834, -866, 102, 0, 340, 25, -354, -239, 420, -730, -992, -925, -314, 420, 914, 607, -296, -415, -30, 813, 866, 153, -90, 150, -81, 636, -392, -222, -835, 482, -631, -962, -413, -727, 280, 686, -382, 157, -404, -511, -432, 455, 58, 108, -408, 290, -829, -252, 113, 550, -935, 925, 422, 38, 789, 361, 487, -460, -769, -963, -285, 206, -799, -488, -233, 416, 143, -456, 753, 520, 599, 621, -168, 178, -841, 51, 952, 374, 166, -300, -576, 844,\
+-656, 90, 780, 371, 730, -896, -895, -386, -662, 467, -61, 130, -362, -675, -113, 135, -761, -55, 408, 822, 675, -347, 725, 114, 952, -510, -972, 390, -413, -277, -52, 315, -80, 401, -712, 147, -202, 84, 214, -178, 970, -571, -210, 525, -887, -863, 504, 192, 837, -594, 203, -876, -209, 305, -826, 377, 103, -928, -803, -956, 949, -868, -547, 824, -994, 516, 93, -524, -866, -890, -988, -501, 15, -6, 413, -825, 304, -818, -223, 525, 176, 610, 828, 391, 940, 540, -831, 650, 438, 589, 941, 57,\
+523, 126, 221, 860, -282, -262, -226, 764, 743, -640, 390, 384, -434, 608, -983, 566, -446, 618, 456, -176, -278, 215, 871, -180, 444, -931, -200, -781, 404, 881, 780, -782, 517, -739, -548, -811, 201, -95, -249, -228, 491, -299, 700, 964, -550, 108, 334, -653, 245, -293, -552, 350, -685, -415, -818, 216, -194, -255, 295, 249, 408, 351, 287, 379, 682, 231, -693, 902, -902, 574, 937, -708, -402, -460, 827, -268, 791, 343, -780, -150, -738, 920, -430, -88, -361, -588, -727, -47, -297, 662,\
+-840, -637, -635, 916, -857, 938, 132, -553, 391, -522, 640, 626, 690, 833, 867, -555, 577, 226, 686, -44, 0, -965, 651, -1, 909, 595, -646, 740, -821, -648, -962, 927, -193, 159, 490, 594, -189, 707, -884, 759, -278, -160, -566, -340, 19, 862, -440, 445, -598, 341, 664, -311, 309, -159, 19, -672, 705, -646, 976, 247, 686, -830, -27, -667, 81, 399, -423, -567, 945, 38, 51, 740, 621, 204, -199, -908, -593, 424, 250, -561, 695, 9, 520, 878, 120, -109, 42, -375, -635, -711, -687, 383, -278,\
+36, 970, 925, 864, 836, 309, 117, 89, 654, -387, 346, -53, 617, -164, -624, 184, -45, 852, 498, -513, 794, -682, -576, 13, -147, 285, -776, -886, -96, 483, 994, -188, 346, -629, -848, 738, 51, 128, -898, -753, -906, 270, -203, -577, 48, -243, -210, 666, 353, 636, -954, 862, 560, -944, -877, -137, 440, -945, -316, 274, -211, -435, 615, -635, -468, 744, 948, -589, 525, 757, -191, -431, 42, 451, -160, -827, -991, 324, 697, 342, -610, 894, -787, -384, 872, 734, 878, 70, -260, 57, 397, -518,\
+629, -510, -94, 207, 214, -625, 106, -882, -575, 908, -650, 723, -154, 45, 108, -69, -565, 927, -68, -351, 707, -282, 429, -889, -596, 848, 578, -492, 41, -822, -992, 168, -286, -780, 970, 597, -293, -12, 367, 708, -415, 194, -86, -390, 224, 69, -368, -674, 1000, -672, 356, -202, -169, 826, 476, -285, 29, -448, 545, 186, 319, 67, 705, 412, 225, -212, -351, -391, -783, -9, 875, -59, -159, -123, -151, -296, 871, -638, 359, 909, -945, 345, -16, -562, -363, -183, -625, -115, -571, -329, 514,\
+99, 263, 463, -39, 597, -652, -349, 246, 77, -127, -563, -879, -30, 756, 777, -865, 675, -813, -501, 871, -406, -627, 834, -609, -205, -812, 643, -204, 291, -251, -184, -584, -541, 410, -573, -600, 908, -871, -687, 296, -713, -139, -778, -790, 347, -52, -400, 407, -653, 670, 39, -856, 904, 433, 392, 590, -271, -144, -863, 443, 353, 468, -544, 486, -930, 458, -596, -890, 163, 822, 768, 980, -783, -792, 126, 386, 367, -264, 603, -61, 728, 160, -4, -837, 832, 591, 436, 518, 796, -622, -867,\
+-669, -947, 253, 100, -792, 841, 413, 833, -249, -550, 282, -825, 936, -348, 898, -451, -283, 818, -237, 630, 216, -499, -637, -511, 767, -396, 221, 958, -586, -920, 401, -313, -580, -145, -270, 118, 497, 426, -975, 480, -445, -150, -721, -929, 439, -893, 902, 960, -525, -793, 924, 563, 683, -727, -86, 309, 432, -762, -345, 371, -617, 149, -215, -228, 505, 593, -20, -292, 704, -999, 149, -104, 819, -414, -443, 517, -599, -5, 145, -24, -993, -283, 904, 174, -112, -276, -860, 44, -257,\
+-931, -821, -667, 540, 421, 485, 531, 407, 833, 431, -415, 878, 503, -901, 639, -608, 896, 860, 927, 424, 113, -808, -323, 729, 382, -922, 548, -791, -379, 207, 203, 559, 537, 137, 999, -913, -240, 942, 249, 616, 775, -4, 915, 855, -987, -234, -384, 948, -310, -542, 125, -289, -599, 967, -492, -349, -552, 562, -926, 632, -164, 217, -165, -496, 847, 684, -884, 457, -748, -745, -38, 93, 961, 934, 588, 366, -130, 851, -803, -811, -211, 428, 183, -469, 888, 596, -475, -899, -681, 508, 184,\
+921, 863, -610, -416, -119, -966, -686, 210, 733, 715, -889, -925, -434, -566, -455, 596, -514, 983, 755, -194, -802, -313, 91, -541, 808, -834, 243, -377, 256, 966, -402, -773, -308, -605, 266, 866, 118, -425, -531, 498, 666, 813, -267, 830, 69, -869, -496, 735, 28, 488, -645, -493, -689, 170, -940, 532, 844, -658, -617, 408, -200, 764, -665, 568, 342, 621, 908, 471, 280, 859, 709, 898, 81, -547, 406, 514, -595, 43, -824, -696, -746, -429, -59, -263, -813, 233, 279, -125, 687, -418,\
+-530, 409, 614, 803, -407, 78, -676, -39, -887, -141, -292, 270, -343, 400, 907, 588, 668, 899, 973, 103, -101, -11, 397, -16, 165, 705, -410, -585, 316, 391, -346, -336, 957, -118, -538, -441, -845, 121, 591, -359, -188, -362, -208, 27, -925, -157, -495, -177, -580, 9, 531, -752, 94, 107, 820, 769, -500, 852, 617, 145, 355, 34, -463, -265, -709, -111, -855, -405, 560, 470, 3, -177, -164, -249, 450, 662, 841, -689, -509, 987, -33, 769, 234, -2, 203, 780, 744, -895, 497, -432, -406, -264,\
+-71, 124, 778, -897, 495, 127, -76, 52, -768, 205, 464, -992, 801, -83, -806, 545, -316, 146, 772, 786, 289, -936, 145, -30, -722, -455, 270, 444, 427, -482, 383, -861, 36, 630, -404, 83, 864, 743, -351, -846, 315, -837, 357, -195, 450, -715, 227, -942, 740, -519, 476, 716, 713, 169, 492, -112, -49, -931, 866, 95, -725, 198, -50, -17, -660, 356, -142, -781, 53, 431, 720, 143, -416, 446, -497, 490, -96, 157, 239, 487, -337, -224, -445, 813, 92, -22, 603, 424, 952, -632, -367, 898, -927,\
+884, -277, -187, -777, 537, -575, -313, 347, -33, 800, 672, -919, -541, 5, -270, -94, -265, -793, -183, -761, -516, -608, -218, 57, -889, -912, 508, 93, -90, 34, 530, 201, 999, -37, -186, -62, -980, 239, 902, 983, -287, -634, 524, -772, 470, -961, 32, 162, 315, -411, 400, -235, -283, -787, -703, 869, 792, 543, -274, 239, 733, -439, 306, 349, 579, -200, -201, -824, 384, -246, 133, -508, 770, -102, 957, -825, 740, 748, -376, 183, -426, 46, 668, -886, -43, -174, 672, -419, 390, 927, 1000,\
+318, 886, 47, 908, -540, -825, -5, 314, -999, 354, -603, 966, -633, -689, 985, 534, -290, 167, -652, -797, -612, -79, 488, 622, -464, -950, 595, 897, 704, -238, -395, 125, 831, -180, 226, -379, 310, 564, 56, -978, 895, -61, 686, -251, 434, -417, 161, -512, 752, 528, -589, -425, 66, -925, -157, 1000, 96, 256, -239, -784, -882, -464, -909, 663, -177, -678, -441, 669, -564, -201, -121, -743, 187, -107, -768, -682, 355, 161, 411, 984, -954, 166, -842, -755, 267, -709, 372, -699, -272, -850,\
+403, -839, 949, 622, -62, 51, 917, 70, 528, -558, -632, 832, 276, 61, -445, -195, 960, 846, -474, 764, 879, -411, 948, -62, -592, -123, -96, -551, -555, -724, 849, 250, -808, -732, 797, -839, -554, 306, -919, 888, 484, -728, 152, -122, -287, 16, -345, -396, -268, -963, -500, 433, 343, 418, -480, 828, 594, 821, -9, 933, -230, 707, -847, -610, -748, -234, 688, 935, 713, 865, -743, 293, -143, -20, 928, -906, -762, 528, 722, 412, -70, 622, -245, 539, -686, 730, -866, -705, 28, -916, -623,\
+-768, -614, -915, -123, -183, 680, -223, 515, -37, -235, -5, 260, 347, -239, -322, -861, -848, -936, 945, 721, -580, -639, 780, -153, -26, 685, 177, 587, 307, -915, 435, 658, 539, -229, -719, -171, -858, 162, 734, -539, -437, 246, 639, 765, -477, -342, -209, -284, -779, -414, -452, 914, 338, -83, 759, 567, 266, -485, 14, 225, 347, -432, -242, 997, -365, -764, 119, -641, -416, -388, -436, -388, -54, -649, -571, -920, -477, 714, -363, 836, 369, 702, 869, 503, -287, -679, 46, -666, -202,\
+-602, 71, -259, 967, 601, -571, -830, -993, -271, 281, -494, 482, -180, 572, 587, -651, -566, -448, -228, 511, -924, 832, -52, -712, 402, -644, -533, -865, 269, 965, 56, 675, 179, -338, -272, 614, 602, -283, 303, -70, 909, -942, 117, 839, 468, 813, -765, 884, -697, -813, 352, 374, -705, -295, 633, 211, -754, 597, -941, -142, -393, -469, -653, 688, 996, 911, 214, 431, 453, -141, 874, -81, -258, -735, -3, -110, -338, -929, -182, -306, -104, -840, -588, -759, -157, -801, 848, -698, 627, 914,\
+-33, -353, 425, 150, -798, 553, 934, -778, -196, -132, 808, 745, -894, 144, 213, 662, 273, -79, 454, -60, -467, 48, -15, -807, 69, -930, 749, 559, -867, -103, 258, -677, 750, -303, 846, -227, -936, 744, -770, 770, -434, 594, -477, 589, -612, 535, 357, -623, 683, 369, 905, 980, -410, -663, 762, -888, -563, -845, 843, 353, -491, 996, -255, -336, -132, 695, -823, 289, -143, 365, 916, 877, 245, -530, -848, -804, -118, -108, 847, 620, -355, 499, 881, 92, -640, 542, 38, 626, -260, -34, -378,\
+598, 890, 305, -118, 711, -385, 600, -570, 27, -129, -893, 354, 459, 374, 816, 470, 356, 661, 877, 735, -286, -780, 620, 943, -169, -888, 978, 441, -667, -399, 662, 249, 137, 598, -863, -453, 722, -815, -251, -995, -294, -707, 901, 763, 977, 137, 431, -994, 905, 593, 694, 444, -626, -816, 252, 282, 616, 841, 360, -932, 817, -908, 50, 394, -120, -786, -338, 499, -982, -95, -454, 838, -312, 320, -127, -653, 53, 16, 988, -968, -151, -369, -836, 293, -271, 483, 18, 724, -204, -965, 245, 310,\
+987, 552, -835, -912, -861, 254, 560, 124, 145, 798, 178, 476, 138, -311, 151, -907, -886, -592, 728, -43, -489, 873, -422, -439, -489, 375, -703, -459, 338, 418, -25, 332, -454, 730, -604, -800, 37, -172, -197, -568, -563, -332, 228, -182, 994, -123, 444, -567, 98, 78, 0, -504, -150, 88, -936, 199, -651, -776, 192, 46, 526, -727, -991, 534, -659, -738, 256, -894, 965, -76, 816, 435, -418, 800, 838, 67, -733, 570, 112, -514, -416\r\
+";
+                thread_local! {
+                    static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
+                        build_test_term(&LINE, 5, 50);
+                }
+                TEST_TERM_AND_POINT.with(|(term, point)| {
+                    assert_eq!(
+                        find_from_grid_point_bench(term, *point)
+                            .map(|(path, ..)| path)
+                            .unwrap_or_default(),
+                        "392",
+                        "Hyperlink should have been found"
+                    );
+                });
+            }
+
+            #[perf]
+            // https://github.com/zed-industries/zed/issues/44510
+            pub fn issue_44510_hyperlink_benchmark() {
+                const LINE: &str = "..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................\
+...............................................E.\r\
+";
+                thread_local! {
+                    static TEST_TERM_AND_POINT: (Term<VoidListener>, AlacPoint) =
+                        build_test_term(&LINE, 5, 50);
+                }
+                TEST_TERM_AND_POINT.with(|(term, point)| {
+                    assert_eq!(
+                        find_from_grid_point_bench(term, *point)
+                            .map(|(path, ..)| path)
+                            .unwrap_or_default(),
+                        LINE.trim_end_matches(['.', '\r', '\n']),
                         "Hyperlink should have been found"
                     );
                 });

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

@@ -790,8 +790,7 @@ impl TerminalPanel {
                 }
 
                 pane.update(cx, |pane, cx| {
-                    let focus = pane.has_focus(window, cx)
-                        || matches!(reveal_strategy, RevealStrategy::Always);
+                    let focus = matches!(reveal_strategy, RevealStrategy::Always);
                     pane.add_item(terminal_view, true, focus, None, window, cx);
                 });
 
@@ -853,8 +852,7 @@ impl TerminalPanel {
                         }
 
                         pane.update(cx, |pane, cx| {
-                            let focus = pane.has_focus(window, cx)
-                                || matches!(reveal_strategy, RevealStrategy::Always);
+                            let focus = matches!(reveal_strategy, RevealStrategy::Always);
                             pane.add_item(terminal_view, true, focus, None, window, cx);
                         });
 

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

@@ -8,8 +8,8 @@ mod terminal_slash_command;
 use assistant_slash_command::SlashCommandRegistry;
 use editor::{EditorSettings, actions::SelectAll, blink_manager::BlinkManager};
 use gpui::{
-    Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
-    KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
+    Action, AnyElement, App, ClipboardEntry, DismissEvent, Entity, EventEmitter, FocusHandle,
+    Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
     ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div,
 };
 use persistence::TERMINAL_DB;
@@ -687,12 +687,32 @@ impl TerminalView {
 
     ///Attempt to paste the clipboard into the terminal
     fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
-        if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) {
+        let Some(clipboard) = cx.read_from_clipboard() else {
+            return;
+        };
+
+        if clipboard.entries().iter().any(|entry| match entry {
+            ClipboardEntry::Image(image) => !image.bytes.is_empty(),
+            _ => false,
+        }) {
+            self.forward_ctrl_v(cx);
+            return;
+        }
+
+        if let Some(text) = clipboard.text() {
             self.terminal
-                .update(cx, |terminal, _cx| terminal.paste(&clipboard_string));
+                .update(cx, |terminal, _cx| terminal.paste(&text));
         }
     }
 
+    /// Emits a raw Ctrl+V so TUI agents can read the OS clipboard directly
+    /// and attach images using their native workflows.
+    fn forward_ctrl_v(&self, cx: &mut Context<Self>) {
+        self.terminal.update(cx, |term, _| {
+            term.input(vec![0x16]);
+        });
+    }
+
     fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context<Self>) {
         self.clear_bell(cx);
         self.terminal.update(cx, |term, _| {

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

@@ -121,7 +121,7 @@ impl RenderOnce for Callout {
             Severity::Info => (
                 IconName::Info,
                 Color::Muted,
-                cx.theme().colors().panel_background.opacity(0.),
+                cx.theme().status().info_background.opacity(0.1),
             ),
             Severity::Success => (
                 IconName::Check,

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

@@ -886,8 +886,12 @@ impl<T: Item> ItemHandle for Entity<T> {
                         // Only trigger autosave if focus has truly left the item.
                         // If focus is still within the item's hierarchy (e.g., moved to a context menu),
                         // don't trigger autosave to avoid unwanted formatting and cursor jumps.
+                        // Also skip autosave if focus moved to a modal (e.g., command palette),
+                        // since the user is still interacting with the workspace.
                         let focus_handle = item.item_focus_handle(cx);
-                        if !focus_handle.contains_focused(window, cx) {
+                        if !focus_handle.contains_focused(window, cx)
+                            && !workspace.has_active_modal(window, cx)
+                        {
                             Pane::autosave_item(&item, workspace.project.clone(), window, cx)
                                 .detach_and_log_err(cx);
                         }

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

@@ -193,6 +193,12 @@ impl Render for ModalLayer {
                 background.fade_out(0.2);
                 this.bg(background)
             })
+            .on_mouse_down(
+                MouseButton::Left,
+                cx.listener(|this, _, window, cx| {
+                    this.hide_modal(window, cx);
+                }),
+            )
             .child(
                 v_flex()
                     .h(px(0.0))

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

@@ -3296,4 +3296,53 @@ mod tests {
 
         assert_eq!(workspace.center_group, new_workspace.center_group);
     }
+
+    #[gpui::test]
+    async fn test_empty_workspace_window_bounds() {
+        zlog::init_test();
+
+        let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
+        let id = db.next_id().await.unwrap();
+
+        // Create a workspace with empty paths (empty workspace)
+        let empty_paths: &[&str] = &[];
+        let display_uuid = Uuid::new_v4();
+        let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
+            origin: point(px(100.0), px(200.0)),
+            size: size(px(800.0), px(600.0)),
+        }));
+
+        let workspace = SerializedWorkspace {
+            id,
+            paths: PathList::new(empty_paths),
+            location: SerializedWorkspaceLocation::Local,
+            center_group: Default::default(),
+            window_bounds: None,
+            display: None,
+            docks: Default::default(),
+            breakpoints: Default::default(),
+            centered_layout: false,
+            session_id: None,
+            window_id: None,
+            user_toolchains: Default::default(),
+        };
+
+        // Save the workspace (this creates the record with empty paths)
+        db.save_workspace(workspace.clone()).await;
+
+        // Save window bounds separately (as the actual code does via set_window_open_status)
+        db.set_window_open_status(id, window_bounds, display_uuid)
+            .await
+            .unwrap();
+
+        // Retrieve it using empty paths
+        let retrieved = db.workspace_for_roots(empty_paths).unwrap();
+
+        // Verify window bounds were persisted
+        assert_eq!(retrieved.id, id);
+        assert!(retrieved.window_bounds.is_some());
+        assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
+        assert!(retrieved.display.is_some());
+        assert_eq!(retrieved.display.unwrap(), display_uuid);
+    }
 }

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

@@ -1749,26 +1749,18 @@ impl Workspace {
                 window
             } else {
                 let window_bounds_override = window_bounds_env_override();
-                let is_empty_workspace = project_paths.is_empty();
 
                 let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
                     (Some(WindowBounds::Windowed(bounds)), None)
-                } else if let Some(workspace) = serialized_workspace.as_ref() {
+                } else if let Some(workspace) = serialized_workspace.as_ref()
+                    && let Some(display) = workspace.display
+                    && let Some(bounds) = workspace.window_bounds.as_ref()
+                {
                     // Reopening an existing workspace - restore its saved bounds
-                    if let (Some(display), Some(bounds)) =
-                        (workspace.display, workspace.window_bounds.as_ref())
-                    {
-                        (Some(bounds.0), Some(display))
-                    } else {
-                        (None, None)
-                    }
-                } else if is_empty_workspace {
-                    // Empty workspace - try to restore the last known no-project window bounds
-                    if let Some((display, bounds)) = persistence::read_default_window_bounds() {
-                        (Some(bounds), Some(display))
-                    } else {
-                        (None, None)
-                    }
+                    (Some(bounds.0), Some(display))
+                } else if let Some((display, bounds)) = persistence::read_default_window_bounds() {
+                    // New or empty workspace - use the last known window bounds
+                    (Some(bounds), Some(display))
                 } else {
                     // New window - let GPUI's default_bounds() handle cascading
                     (None, None)
@@ -5674,12 +5666,24 @@ impl Workspace {
                     persistence::DB.save_workspace(serialized_workspace).await;
                 })
             }
-            WorkspaceLocation::DetachFromSession => window.spawn(cx, async move |_| {
-                persistence::DB
-                    .set_session_id(database_id, None)
-                    .await
-                    .log_err();
-            }),
+            WorkspaceLocation::DetachFromSession => {
+                let window_bounds = SerializedWindowBounds(window.window_bounds());
+                let display = window.display(cx).and_then(|d| d.uuid().ok());
+                window.spawn(cx, async move |_| {
+                    persistence::DB
+                        .set_window_open_status(
+                            database_id,
+                            window_bounds,
+                            display.unwrap_or_default(),
+                        )
+                        .await
+                        .log_err();
+                    persistence::DB
+                        .set_session_id(database_id, None)
+                        .await
+                        .log_err();
+                })
+            }
             WorkspaceLocation::None => Task::ready(()),
         }
     }

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

@@ -145,23 +145,6 @@ fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context<Ed
             },
         ))
         .detach();
-    editor
-        .register_action(cx.listener(
-            |editor, _: &copilot::NextSuggestion, window: &mut Window, cx: &mut Context<Editor>| {
-                editor.next_edit_prediction(&Default::default(), window, cx);
-            },
-        ))
-        .detach();
-    editor
-        .register_action(cx.listener(
-            |editor,
-             _: &copilot::PreviousSuggestion,
-             window: &mut Window,
-             cx: &mut Context<Editor>| {
-                editor.previous_edit_prediction(&Default::default(), window, cx);
-            },
-        ))
-        .detach();
 }
 
 fn assign_edit_prediction_provider(

docs/src/completions.md πŸ”—

@@ -15,6 +15,10 @@ When there is an appropriate language server available, Zed will provide complet
 
 You can manually trigger completions with `ctrl-space` or by triggering the `editor::ShowCompletions` action from the command palette.
 
+> Note: Using `ctrl-space` in Zed requires disabling the macOS global shortcut.
+> Open **System Settings** > **Keyboard** > **Keyboard Shortcut**s >
+> **Input Sources** and uncheck **Select the previous input source**.
+
 For more information, see:
 
 - [Configuring Supported Languages](./configuring-languages.md)

script/bundle-mac πŸ”—

@@ -106,6 +106,17 @@ mv Cargo.toml.backup Cargo.toml
 popd
 echo "Bundled ${app_path}"
 
+# DocumentTypes.plist references CFBundleTypeIconFile "Document", so the bundle must contain Document.icns.
+# We use the app icon as a placeholder document icon for now.
+document_icon_source="crates/zed/resources/Document.icns"
+document_icon_target="${app_path}/Contents/Resources/Document.icns"
+if [[ -f "${document_icon_source}" ]]; then
+    mkdir -p "$(dirname "${document_icon_target}")"
+    cp "${document_icon_source}" "${document_icon_target}"
+else
+    echo "cargo::warning=Missing ${document_icon_source}; macOS document icons may not appear in Finder."
+fi
+
 if [[ -n "${MACOS_CERTIFICATE:-}" && -n "${MACOS_CERTIFICATE_PASSWORD:-}" && -n "${APPLE_NOTARIZATION_KEY:-}" && -n "${APPLE_NOTARIZATION_KEY_ID:-}" && -n "${APPLE_NOTARIZATION_ISSUER_ID:-}" ]]; then
     can_code_sign=true
 

script/verify-macos-document-icon πŸ”—

@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+usage() {
+  cat <<'USAGE'
+Usage:
+  script/verify-macos-document-icon /path/to/Zed.app
+
+Verifies that the given macOS app bundle's Info.plist references a document icon
+named "Document" and that the corresponding icon file exists in the bundle.
+
+Specifically checks:
+  - CFBundleDocumentTypes[*].CFBundleTypeIconFile includes "Document"
+  - Contents/Resources/Document.icns exists
+
+Exit codes:
+  0 - success
+  1 - verification failed
+  2 - invalid usage / missing prerequisites
+USAGE
+}
+
+fail() {
+  echo "error: $*" >&2
+  exit 1
+}
+
+if [[ $# -ne 1 ]]; then
+  usage >&2
+  exit 2
+fi
+
+app_path="$1"
+
+if [[ ! -d "${app_path}" ]]; then
+  fail "app bundle not found: ${app_path}"
+fi
+
+info_plist="${app_path}/Contents/Info.plist"
+if [[ ! -f "${info_plist}" ]]; then
+  fail "missing Info.plist: ${info_plist}"
+fi
+
+if ! command -v plutil >/dev/null 2>&1; then
+  fail "plutil not found (required on macOS to read Info.plist)"
+fi
+
+# Convert to JSON for robust parsing. plutil outputs JSON to stdout in this mode.
+info_json="$(plutil -convert json -o - "${info_plist}")"
+
+# Check that CFBundleDocumentTypes exists and that at least one entry references "Document".
+# We use Python for JSON parsing; macOS ships with Python 3 on many setups, but not all.
+# If python3 isn't available, fall back to a simpler grep-based check.
+has_document_icon_ref="false"
+if command -v python3 >/dev/null 2>&1; then
+  has_document_icon_ref="$(python3 -c "import json,sys; d=json.load(sys.stdin); types=d.get('CFBundleDocumentTypes', []); vals=[t.get('CFBundleTypeIconFile') for t in types if isinstance(t, dict)]; print('true' if 'Document' in vals else 'false')" <<<"${info_json}")"
+else
+  # This is a best-effort fallback. It may produce false negatives if the JSON formatting differs.
+  if echo "${info_json}" | grep -q '"CFBundleTypeIconFile"[[:space:]]*:[[:space:]]*"Document"'; then
+    has_document_icon_ref="true"
+  fi
+fi
+
+if [[ "${has_document_icon_ref}" != "true" ]]; then
+  echo "Verification failed for: ${app_path}" >&2
+  echo "Expected Info.plist to reference CFBundleTypeIconFile \"Document\" in CFBundleDocumentTypes." >&2
+  echo "Tip: This bundle may be missing DocumentTypes.plist extensions or may have different icon naming." >&2
+  exit 1
+fi
+
+document_icon_path="${app_path}/Contents/Resources/Document.icns"
+if [[ ! -f "${document_icon_path}" ]]; then
+  echo "Verification failed for: ${app_path}" >&2
+  echo "Expected document icon to exist: ${document_icon_path}" >&2
+  echo "Tip: The bundle script should copy crates/zed/resources/Document.icns into Contents/Resources/Document.icns." >&2
+  exit 1
+fi
+
+echo "OK: ${app_path}"
+echo " - Info.plist references CFBundleTypeIconFile \"Document\""
+echo " - Found ${document_icon_path}"

tooling/xtask/src/tasks/workflows/autofix_pr.rs πŸ”—

@@ -109,19 +109,6 @@ fn run_autofix(pr_number: &WorkflowInput, run_clippy: &WorkflowInput) -> NamedJo
 }
 
 fn commit_changes(pr_number: &WorkflowInput, autofix_job: &NamedJob) -> NamedJob {
-    fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
-        let step = named::uses(
-            "actions",
-            "create-github-app-token",
-            "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
-        )
-        .add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
-        .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
-        .id("get-app-token");
-        let output = StepOutput::new(&step, "token");
-        (step, output)
-    }
-
     fn checkout_pr(pr_number: &WorkflowInput, token: &StepOutput) -> Step<Run> {
         named::bash(&format!("gh pr checkout {pr_number}")).add_env(("GITHUB_TOKEN", token))
     }
@@ -148,7 +135,7 @@ fn commit_changes(pr_number: &WorkflowInput, autofix_job: &NamedJob) -> NamedJob
         .add_env(("GITHUB_TOKEN", token))
     }
 
-    let (authenticate, token) = authenticate_as_zippy();
+    let (authenticate, token) = steps::authenticate_as_zippy();
 
     named::job(
         Job::default()

tooling/xtask/src/tasks/workflows/cherry_pick.rs πŸ”—

@@ -3,7 +3,7 @@ use gh_workflow::*;
 use crate::tasks::workflows::{
     runners,
     steps::{self, NamedJob, named},
-    vars::{self, StepOutput, WorkflowInput},
+    vars::{StepOutput, WorkflowInput},
 };
 
 pub fn cherry_pick() -> Workflow {
@@ -29,19 +29,6 @@ fn run_cherry_pick(
     commit: &WorkflowInput,
     channel: &WorkflowInput,
 ) -> NamedJob {
-    fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
-        let step = named::uses(
-            "actions",
-            "create-github-app-token",
-            "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
-        ) // v2
-        .add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
-        .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
-        .id("get-app-token");
-        let output = StepOutput::new(&step, "token");
-        (step, output)
-    }
-
     fn cherry_pick(
         branch: &WorkflowInput,
         commit: &WorkflowInput,
@@ -54,7 +41,7 @@ fn run_cherry_pick(
             .add_env(("GITHUB_TOKEN", token))
     }
 
-    let (authenticate, token) = authenticate_as_zippy();
+    let (authenticate, token) = steps::authenticate_as_zippy();
 
     named::job(
         Job::default()

tooling/xtask/src/tasks/workflows/release.rs πŸ”—

@@ -97,17 +97,20 @@ pub(crate) fn create_sentry_release() -> Step<Use> {
 }
 
 fn auto_release_preview(deps: &[&NamedJob; 1]) -> NamedJob {
+    let (authenticate, token) = steps::authenticate_as_zippy();
+
     named::job(
         dependant_job(deps)
             .runs_on(runners::LINUX_SMALL)
             .cond(Expression::new(indoc::indoc!(
                 r#"startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')"#
             )))
+            .add_step(authenticate)
             .add_step(
                 steps::script(
                     r#"gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false"#,
                 )
-                .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)),
+                .add_env(("GITHUB_TOKEN", &token)),
             )
     )
 }

tooling/xtask/src/tasks/workflows/run_tests.rs πŸ”—

@@ -45,11 +45,15 @@ pub(crate) fn run_tests() -> Workflow {
         &should_run_tests,
     ]);
 
+    let check_style = check_style();
+    let run_tests_linux = run_platform_tests(Platform::Linux);
+    let call_autofix = call_autofix(&check_style, &run_tests_linux);
+
     let mut jobs = vec![
         orchestrate,
-        check_style(),
+        check_style,
         should_run_tests.guard(run_platform_tests(Platform::Windows)),
-        should_run_tests.guard(run_platform_tests(Platform::Linux)),
+        should_run_tests.guard(run_tests_linux),
         should_run_tests.guard(run_platform_tests(Platform::Mac)),
         should_run_tests.guard(doctests()),
         should_run_tests.guard(check_workspace_binaries()),
@@ -106,6 +110,7 @@ pub(crate) fn run_tests() -> Workflow {
             workflow
         })
         .add_job(tests_pass.name, tests_pass.job)
+        .add_job(call_autofix.name, call_autofix.job)
 }
 
 // Generates a bash script that checks changed files against regex patterns
@@ -221,6 +226,8 @@ pub fn tests_pass(jobs: &[NamedJob]) -> NamedJob {
     named::job(job)
 }
 
+pub const STYLE_FAILED_OUTPUT: &str = "style_failed";
+
 fn check_style() -> NamedJob {
     fn check_for_typos() -> Step<Use> {
         named::uses(
@@ -236,15 +243,58 @@ fn check_style() -> NamedJob {
             .add_step(steps::checkout_repo())
             .add_step(steps::cache_rust_dependencies_namespace())
             .add_step(steps::setup_pnpm())
-            .add_step(steps::script("./script/prettier"))
+            .add_step(steps::prettier())
             .add_step(steps::cargo_fmt())
-            .add_step(steps::trigger_autofix(false))
+            .add_step(steps::record_style_failure())
             .add_step(steps::script("./script/check-todos"))
             .add_step(steps::script("./script/check-keymaps"))
-            .add_step(check_for_typos()),
+            .add_step(check_for_typos())
+            .outputs([(
+                STYLE_FAILED_OUTPUT.to_owned(),
+                format!(
+                    "${{{{ steps.{}.outputs.failed == 'true' }}}}",
+                    steps::RECORD_STYLE_FAILURE_STEP_ID
+                ),
+            )]),
     )
 }
 
+fn call_autofix(check_style: &NamedJob, run_tests_linux: &NamedJob) -> NamedJob {
+    fn dispatch_autofix(run_tests_linux_name: &str) -> Step<Run> {
+        let clippy_failed_expr = format!(
+            "needs.{}.outputs.{} == 'true'",
+            run_tests_linux_name, CLIPPY_FAILED_OUTPUT
+        );
+        named::bash(format!(
+            "gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy=${{{{ {} }}}}",
+            clippy_failed_expr
+        ))
+        .add_env(("GITHUB_TOKEN", "${{ steps.get-app-token.outputs.token }}"))
+    }
+
+    let style_failed_expr = format!(
+        "needs.{}.outputs.{} == 'true'",
+        check_style.name, STYLE_FAILED_OUTPUT
+    );
+    let clippy_failed_expr = format!(
+        "needs.{}.outputs.{} == 'true'",
+        run_tests_linux.name, CLIPPY_FAILED_OUTPUT
+    );
+    let (authenticate, _token) = steps::authenticate_as_zippy();
+
+    let job = Job::default()
+        .runs_on(runners::LINUX_SMALL)
+        .cond(Expression::new(format!(
+            "always() && ({} || {}) && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
+            style_failed_expr, clippy_failed_expr
+        )))
+        .needs(vec![check_style.name.clone(), run_tests_linux.name.clone()])
+        .add_step(authenticate)
+        .add_step(dispatch_autofix(&run_tests_linux.name));
+
+    named::job(job)
+}
+
 fn check_dependencies() -> NamedJob {
     fn install_cargo_machete() -> Step<Use> {
         named::uses(
@@ -305,6 +355,8 @@ fn check_workspace_binaries() -> NamedJob {
     )
 }
 
+pub const CLIPPY_FAILED_OUTPUT: &str = "clippy_failed";
+
 pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
     let runner = match platform {
         Platform::Windows => runners::WINDOWS_DEFAULT,
@@ -327,12 +379,23 @@ pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
             .add_step(steps::setup_node())
             .add_step(steps::clippy(platform))
             .when(platform == Platform::Linux, |job| {
-                job.add_step(steps::trigger_autofix(true))
-                    .add_step(steps::cargo_install_nextest())
+                job.add_step(steps::record_clippy_failure())
+            })
+            .when(platform == Platform::Linux, |job| {
+                job.add_step(steps::cargo_install_nextest())
             })
             .add_step(steps::clear_target_dir_if_large(platform))
             .add_step(steps::cargo_nextest(platform))
-            .add_step(steps::cleanup_cargo_config(platform)),
+            .add_step(steps::cleanup_cargo_config(platform))
+            .when(platform == Platform::Linux, |job| {
+                job.outputs([(
+                    CLIPPY_FAILED_OUTPUT.to_owned(),
+                    format!(
+                        "${{{{ steps.{}.outputs.failed == 'true' }}}}",
+                        steps::RECORD_CLIPPY_FAILURE_STEP_ID
+                    ),
+                )])
+            }),
     }
 }
 

tooling/xtask/src/tasks/workflows/steps.rs πŸ”—

@@ -54,8 +54,25 @@ pub fn setup_sentry() -> Step<Use> {
     .add_with(("token", vars::SENTRY_AUTH_TOKEN))
 }
 
+pub const PRETTIER_STEP_ID: &str = "prettier";
+pub const CARGO_FMT_STEP_ID: &str = "cargo_fmt";
+pub const RECORD_STYLE_FAILURE_STEP_ID: &str = "record_style_failure";
+
+pub fn prettier() -> Step<Run> {
+    named::bash("./script/prettier").id(PRETTIER_STEP_ID)
+}
+
 pub fn cargo_fmt() -> Step<Run> {
-    named::bash("cargo fmt --all -- --check")
+    named::bash("cargo fmt --all -- --check").id(CARGO_FMT_STEP_ID)
+}
+
+pub fn record_style_failure() -> Step<Run> {
+    named::bash(format!(
+        "echo \"failed=${{{{ steps.{}.outcome == 'failure' || steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
+        PRETTIER_STEP_ID, CARGO_FMT_STEP_ID
+    ))
+    .id(RECORD_STYLE_FAILURE_STEP_ID)
+    .if_condition(Expression::new("always()"))
 }
 
 pub fn cargo_install_nextest() -> Step<Use> {
@@ -101,13 +118,25 @@ pub fn clear_target_dir_if_large(platform: Platform) -> Step<Run> {
     }
 }
 
+pub const CLIPPY_STEP_ID: &str = "clippy";
+pub const RECORD_CLIPPY_FAILURE_STEP_ID: &str = "record_clippy_failure";
+
 pub fn clippy(platform: Platform) -> Step<Run> {
     match platform {
-        Platform::Windows => named::pwsh("./script/clippy.ps1"),
-        _ => named::bash("./script/clippy"),
+        Platform::Windows => named::pwsh("./script/clippy.ps1").id(CLIPPY_STEP_ID),
+        _ => named::bash("./script/clippy").id(CLIPPY_STEP_ID),
     }
 }
 
+pub fn record_clippy_failure() -> Step<Run> {
+    named::bash(format!(
+        "echo \"failed=${{{{ steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
+        CLIPPY_STEP_ID
+    ))
+    .id(RECORD_CLIPPY_FAILURE_STEP_ID)
+    .if_condition(Expression::new("always()"))
+}
+
 pub fn cache_rust_dependencies_namespace() -> Step<Use> {
     named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust"))
 }
@@ -345,12 +374,15 @@ pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step<Run> {
     ))
 }
 
-pub fn trigger_autofix(run_clippy: bool) -> Step<Run> {
-    named::bash(format!(
-        "gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy={run_clippy}"
-    ))
-    .if_condition(Expression::new(
-        "failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
-    ))
-    .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN))
+pub fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
+    let step = named::uses(
+        "actions",
+        "create-github-app-token",
+        "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
+    )
+    .add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
+    .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
+    .id("get-app-token");
+    let output = StepOutput::new(&step, "token");
+    (step, output)
 }