Merge remote-tracking branch 'origin/main' into auto-extract-dap-schemas

Cole Miller created

Change summary

.github/workflows/ci.yml                                                |   2 
Cargo.lock                                                              |   1 
assets/keymaps/vim.json                                                 |   1 
assets/settings/default.json                                            |   4 
crates/agent/src/prompts/stale_files_prompt_header.txt                  |   3 
crates/agent/src/thread.rs                                              | 222 
crates/agent_ui/src/agent_configuration.rs                              |   2 
crates/agent_ui/src/context_picker.rs                                   |   2 
crates/agent_ui/src/context_picker/completion_provider.rs               |   1 
crates/assistant_tool/src/tool_schema.rs                                |   9 
crates/assistant_tools/src/assistant_tools.rs                           |   3 
crates/assistant_tools/src/project_notifications_tool.rs                | 193 
crates/assistant_tools/src/project_notifications_tool/description.md    |   3 
crates/assistant_tools/src/project_notifications_tool/prompt_header.txt |   3 
crates/assistant_tools/src/terminal_tool.rs                             |   2 
crates/copilot/src/copilot_chat.rs                                      |  16 
crates/dap_adapters/src/javascript.rs                                   |  12 
crates/debug_adapter_extension/src/extension_locator_adapter.rs         |   6 
crates/debugger_ui/src/session/running.rs                               |  87 
crates/debugger_ui/src/session/running/breakpoint_list.rs               | 117 
crates/editor/src/actions.rs                                            |   2 
crates/editor/src/editor.rs                                             |  11 
crates/editor/src/editor_tests.rs                                       |  36 
crates/editor/src/element.rs                                            |   1 
crates/eval/src/examples/file_change_notification.rs                    |   2 
crates/extension/src/extension_manifest.rs                              |   6 
crates/extension_host/src/extension_host.rs                             |   2 
crates/extension_host/src/headless_host.rs                              |  40 
crates/extension_host/src/wasm_host/wit.rs                              |   2 
crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs                 |  12 
crates/gpui/src/elements/uniform_list.rs                                |  83 
crates/gpui/src/platform/keystroke.rs                                   |   2 
crates/gpui/src/platform/windows/events.rs                              |  41 
crates/gpui/src/platform/windows/window.rs                              |   3 
crates/inline_completion_button/src/inline_completion_button.rs         |   4 
crates/language_models/src/provider/copilot_chat.rs                     |  17 
crates/language_tools/Cargo.toml                                        |   1 
crates/language_tools/src/lsp_tool.rs                                   | 871 
crates/languages/src/go/outline.scm                                     |   2 
crates/languages/src/javascript/overrides.scm                           |   7 
crates/languages/src/python/config.toml                                 |   1 
crates/languages/src/python/indents.scm                                 |   2 
crates/languages/src/tsx/overrides.scm                                  |   7 
crates/languages/src/typescript/overrides.scm                           |   3 
crates/project/src/context_server_store.rs                              |   9 
crates/project/src/debugger/dap_store.rs                                | 117 
crates/project/src/debugger/locators/cargo.rs                           |   2 
crates/project/src/debugger/session.rs                                  |  60 
crates/project/src/project_tests.rs                                     |   2 
crates/project/src/terminals.rs                                         |  19 
crates/project_panel/src/project_panel.rs                               | 779 
crates/project_panel/src/project_panel_settings.rs                      |   5 
crates/proto/proto/debugger.proto                                       |   2 
crates/settings/src/keymap_file.rs                                      | 187 
crates/settings_ui/src/keybindings.rs                                   | 163 
crates/task/src/shell_builder.rs                                        |  26 
crates/task/src/task.rs                                                 |   2 
crates/task/src/task_template.rs                                        |   8 
crates/terminal_view/src/terminal_panel.rs                              |   2 
crates/ui/src/components.rs                                             |   2 
crates/ui/src/components/context_menu.rs                                |  25 
crates/ui/src/components/popover_menu.rs                                |  18 
crates/ui/src/components/sticky_items.rs                                | 150 
crates/util/src/shell_env.rs                                            |  12 
crates/util/src/util.rs                                                 |  46 
crates/vim/src/command.rs                                               |   2 
crates/vim/src/normal/convert.rs                                        |  41 
crates/zed/src/main.rs                                                  |   5 
script/generate-licenses                                                |  11 
script/licenses/zed-licenses.toml                                       |   6 
70 files changed, 2,421 insertions(+), 1,127 deletions(-)

Detailed changes

.github/workflows/ci.yml ๐Ÿ”—

@@ -65,7 +65,7 @@ jobs:
           else
             echo "run_docs=false" >> $GITHUB_OUTPUT
           fi
-          if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^Cargo.lock') ]]; then
+          if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P '^(Cargo.lock|script/.*licenses)') ]]; then
             echo "run_license=true" >> $GITHUB_OUTPUT
           else
             echo "run_license=false" >> $GITHUB_OUTPUT

Cargo.lock ๐Ÿ”—

@@ -9026,7 +9026,6 @@ dependencies = [
  "itertools 0.14.0",
  "language",
  "lsp",
- "picker",
  "project",
  "release_channel",
  "serde_json",

assets/keymaps/vim.json ๐Ÿ”—

@@ -327,6 +327,7 @@
       "g shift-r": ["vim::Paste", { "preserve_clipboard": true }],
       "g c": "vim::ToggleComments",
       "g q": "vim::Rewrap",
+      "g w": "vim::Rewrap",
       "g ?": "vim::ConvertToRot13",
       // "g ?": "vim::ConvertToRot47",
       "\"": "vim::PushRegister",

assets/settings/default.json ๐Ÿ”—

@@ -617,6 +617,8 @@
     // 3. Mark files with errors and warnings:
     //    "all"
     "show_diagnostics": "all",
+    // Whether to stick parent directories at top of the project panel.
+    "sticky_scroll": true,
     // Settings related to indent guides in the project panel.
     "indent_guides": {
       // When to show indent guides in the project panel.
@@ -808,6 +810,7 @@
           "edit_file": true,
           "fetch": true,
           "list_directory": true,
+          "project_notifications": true,
           "move_path": true,
           "now": true,
           "find_path": true,
@@ -827,6 +830,7 @@
           "diagnostics": true,
           "fetch": true,
           "list_directory": true,
+          "project_notifications": true,
           "now": true,
           "find_path": true,
           "read_file": true,

crates/agent/src/thread.rs ๐Ÿ”—

@@ -25,8 +25,8 @@ use language_model::{
     ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
     LanguageModelId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
     LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent,
-    LanguageModelToolUseId, MessageContent, ModelRequestLimitReachedError, PaymentRequiredError,
-    Role, SelectedModel, StopReason, TokenUsage,
+    LanguageModelToolUse, LanguageModelToolUseId, MessageContent, ModelRequestLimitReachedError,
+    PaymentRequiredError, Role, SelectedModel, StopReason, TokenUsage,
 };
 use postage::stream::Stream as _;
 use project::{
@@ -45,7 +45,7 @@ use std::{
     time::{Duration, Instant},
 };
 use thiserror::Error;
-use util::{ResultExt as _, post_inc};
+use util::{ResultExt as _, debug_panic, post_inc};
 use uuid::Uuid;
 use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
 
@@ -1248,6 +1248,8 @@ impl Thread {
 
         self.remaining_turns -= 1;
 
+        self.flush_notifications(model.clone(), intent, cx);
+
         let request = self.to_completion_request(model.clone(), intent, cx);
 
         self.stream_completion(request, model, intent, window, cx);
@@ -1481,6 +1483,110 @@ impl Thread {
         request
     }
 
+    /// Insert auto-generated notifications (if any) to the thread
+    fn flush_notifications(
+        &mut self,
+        model: Arc<dyn LanguageModel>,
+        intent: CompletionIntent,
+        cx: &mut Context<Self>,
+    ) {
+        match intent {
+            CompletionIntent::UserPrompt | CompletionIntent::ToolResults => {
+                if let Some(pending_tool_use) = self.attach_tracked_files_state(model, cx) {
+                    cx.emit(ThreadEvent::ToolFinished {
+                        tool_use_id: pending_tool_use.id.clone(),
+                        pending_tool_use: Some(pending_tool_use),
+                    });
+                }
+            }
+            CompletionIntent::ThreadSummarization
+            | CompletionIntent::ThreadContextSummarization
+            | CompletionIntent::CreateFile
+            | CompletionIntent::EditFile
+            | CompletionIntent::InlineAssist
+            | CompletionIntent::TerminalInlineAssist
+            | CompletionIntent::GenerateGitCommitMessage => {}
+        };
+    }
+
+    fn attach_tracked_files_state(
+        &mut self,
+        model: Arc<dyn LanguageModel>,
+        cx: &mut App,
+    ) -> Option<PendingToolUse> {
+        let action_log = self.action_log.read(cx);
+
+        action_log.stale_buffers(cx).next()?;
+
+        // Represent notification as a simulated `project_notifications` tool call
+        let tool_name = Arc::from("project_notifications");
+        let Some(tool) = self.tools.read(cx).tool(&tool_name, cx) else {
+            debug_panic!("`project_notifications` tool not found");
+            return None;
+        };
+
+        if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
+            return None;
+        }
+
+        let input = serde_json::json!({});
+        let request = Arc::new(LanguageModelRequest::default()); // unused
+        let window = None;
+        let tool_result = tool.run(
+            input,
+            request,
+            self.project.clone(),
+            self.action_log.clone(),
+            model.clone(),
+            window,
+            cx,
+        );
+
+        let tool_use_id =
+            LanguageModelToolUseId::from(format!("project_notifications_{}", self.messages.len()));
+
+        let tool_use = LanguageModelToolUse {
+            id: tool_use_id.clone(),
+            name: tool_name.clone(),
+            raw_input: "{}".to_string(),
+            input: serde_json::json!({}),
+            is_input_complete: true,
+        };
+
+        let tool_output = cx.background_executor().block(tool_result.output);
+
+        // Attach a project_notification tool call to the latest existing
+        // Assistant message. We cannot create a new Assistant message
+        // because thinking models require a `thinking` block that we
+        // cannot mock. We cannot send a notification as a normal
+        // (non-tool-use) User message because this distracts Agent
+        // too much.
+        let tool_message_id = self
+            .messages
+            .iter()
+            .enumerate()
+            .rfind(|(_, message)| message.role == Role::Assistant)
+            .map(|(_, message)| message.id)?;
+
+        let tool_use_metadata = ToolUseMetadata {
+            model: model.clone(),
+            thread_id: self.id.clone(),
+            prompt_id: self.last_prompt_id.clone(),
+        };
+
+        self.tool_use
+            .request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx);
+
+        let pending_tool_use = self.tool_use.insert_tool_output(
+            tool_use_id.clone(),
+            tool_name,
+            tool_output,
+            self.configured_model.as_ref(),
+        );
+
+        pending_tool_use
+    }
+
     pub fn stream_completion(
         &mut self,
         request: LanguageModelRequest,
@@ -3156,10 +3262,13 @@ mod tests {
     const TEST_RATE_LIMIT_RETRY_SECS: u64 = 30;
     use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters};
     use assistant_tool::ToolRegistry;
+    use assistant_tools;
     use futures::StreamExt;
     use futures::future::BoxFuture;
     use futures::stream::BoxStream;
     use gpui::TestAppContext;
+    use http_client;
+    use indoc::indoc;
     use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
     use language_model::{
         LanguageModelCompletionError, LanguageModelName, LanguageModelProviderId,
@@ -3487,6 +3596,105 @@ fn main() {{
         );
     }
 
+    #[gpui::test]
+    async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
+        init_test_settings(cx);
+
+        let project = create_test_project(
+            cx,
+            json!({"code.rs": "fn main() {\n    println!(\"Hello, world!\");\n}"}),
+        )
+        .await;
+
+        let (_workspace, _thread_store, thread, context_store, model) =
+            setup_test_environment(cx, project.clone()).await;
+
+        // Add a buffer to the context. This will be a tracked buffer
+        let buffer = add_file_to_context(&project, &context_store, "test/code.rs", cx)
+            .await
+            .unwrap();
+
+        let context = context_store
+            .read_with(cx, |store, _| store.context().next().cloned())
+            .unwrap();
+        let loaded_context = cx
+            .update(|cx| load_context(vec![context], &project, &None, cx))
+            .await;
+
+        // Insert user message and assistant response
+        thread.update(cx, |thread, cx| {
+            thread.insert_user_message("Explain this code", loaded_context, None, Vec::new(), cx);
+            thread.insert_assistant_message(
+                vec![MessageSegment::Text("This code prints 42.".into())],
+                cx,
+            );
+        });
+
+        // We shouldn't have a stale buffer notification yet
+        let notification = thread.read_with(cx, |thread, _| {
+            find_tool_use(thread, "project_notifications")
+        });
+        assert!(
+            notification.is_none(),
+            "Should not have stale buffer notification before buffer is modified"
+        );
+
+        // Modify the buffer
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit(
+                [(1..1, "\n    println!(\"Added a new line\");\n")],
+                None,
+                cx,
+            );
+        });
+
+        // Insert another user message
+        thread.update(cx, |thread, cx| {
+            thread.insert_user_message(
+                "What does the code do now?",
+                ContextLoadResult::default(),
+                None,
+                Vec::new(),
+                cx,
+            )
+        });
+
+        // Check for the stale buffer warning
+        thread.update(cx, |thread, cx| {
+            thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx)
+        });
+
+        let Some(notification_result) = thread.read_with(cx, |thread, _cx| {
+            find_tool_use(thread, "project_notifications")
+        }) else {
+            panic!("Should have a `project_notifications` tool use");
+        };
+
+        let Some(notification_content) = notification_result.content.to_str() else {
+            panic!("`project_notifications` should return text");
+        };
+
+        let expected_content = indoc! {"[The following is an auto-generated notification; do not reply]
+
+        These files have changed since the last read:
+        - code.rs
+        "};
+        assert_eq!(notification_content, expected_content);
+    }
+
+    fn find_tool_use(thread: &Thread, tool_name: &str) -> Option<LanguageModelToolResult> {
+        thread
+            .messages()
+            .filter_map(|message| {
+                thread
+                    .tool_results_for_message(message.id)
+                    .into_iter()
+                    .find(|result| result.tool_name == tool_name.into())
+            })
+            .next()
+            .cloned()
+    }
+
     #[gpui::test]
     async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
         init_test_settings(cx);
@@ -5052,6 +5260,14 @@ fn main() {{
             language_model::init_settings(cx);
             ThemeSettings::register(cx);
             ToolRegistry::default_global(cx);
+            assistant_tool::init(cx);
+
+            let http_client = Arc::new(http_client::HttpClientWithUrl::new(
+                http_client::FakeHttpClient::with_200_response(),
+                "http://localhost".to_string(),
+                None,
+            ));
+            assistant_tools::init(http_client, cx);
         });
     }
 

crates/agent_ui/src/agent_configuration.rs ๐Ÿ”—

@@ -436,7 +436,7 @@ impl AgentConfiguration {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
-        let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone();
+        let context_server_ids = self.context_server_store.read(cx).configured_server_ids();
 
         v_flex()
             .p(DynamicSpacing::Base16.rems(cx))

crates/agent_ui/src/context_picker.rs ๐Ÿ”—

@@ -426,6 +426,7 @@ impl ContextPicker {
                             this.add_recent_file(project_path.clone(), window, cx);
                         })
                     },
+                    None,
                 )
             }
             RecentEntry::Thread(thread) => {
@@ -443,6 +444,7 @@ impl ContextPicker {
                                 .detach_and_log_err(cx);
                         })
                     },
+                    None,
                 )
             }
         }

crates/agent_ui/src/context_picker/completion_provider.rs ๐Ÿ”—

@@ -686,6 +686,7 @@ impl ContextPickerCompletionProvider {
         let mut label = CodeLabel::plain(symbol.name.clone(), None);
         label.push_str(" ", None);
         label.push_str(&file_name, comment_id);
+        label.push_str(&format!(" L{}", symbol.range.start.0.row + 1), comment_id);
 
         let new_text = format!("{} ", MentionLink::for_symbol(&symbol.name, &full_path));
         let new_text_len = new_text.len();

crates/assistant_tool/src/tool_schema.rs ๐Ÿ”—

@@ -25,10 +25,15 @@ fn preprocess_json_schema(json: &mut Value) -> Result<()> {
     // `additionalProperties` defaults to `false` unless explicitly specified.
     // This prevents models from hallucinating tool parameters.
     if let Value::Object(obj) = json {
-        if let Some(Value::String(type_str)) = obj.get("type") {
-            if type_str == "object" && !obj.contains_key("additionalProperties") {
+        if matches!(obj.get("type"), Some(Value::String(s)) if s == "object") {
+            if !obj.contains_key("additionalProperties") {
                 obj.insert("additionalProperties".to_string(), Value::Bool(false));
             }
+
+            // OpenAI API requires non-missing `properties`
+            if !obj.contains_key("properties") {
+                obj.insert("properties".to_string(), Value::Object(Default::default()));
+            }
         }
     }
     Ok(())

crates/assistant_tools/src/assistant_tools.rs ๐Ÿ”—

@@ -11,6 +11,7 @@ mod list_directory_tool;
 mod move_path_tool;
 mod now_tool;
 mod open_tool;
+mod project_notifications_tool;
 mod read_file_tool;
 mod schema;
 mod templates;
@@ -45,6 +46,7 @@ pub use edit_file_tool::{EditFileMode, EditFileToolInput};
 pub use find_path_tool::FindPathToolInput;
 pub use grep_tool::{GrepTool, GrepToolInput};
 pub use open_tool::OpenTool;
+pub use project_notifications_tool::ProjectNotificationsTool;
 pub use read_file_tool::{ReadFileTool, ReadFileToolInput};
 pub use terminal_tool::TerminalTool;
 
@@ -61,6 +63,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
     registry.register_tool(ListDirectoryTool);
     registry.register_tool(NowTool);
     registry.register_tool(OpenTool);
+    registry.register_tool(ProjectNotificationsTool);
     registry.register_tool(FindPathTool);
     registry.register_tool(ReadFileTool);
     registry.register_tool(GrepTool);

crates/assistant_tools/src/project_notifications_tool.rs ๐Ÿ”—

@@ -0,0 +1,193 @@
+use crate::schema::json_schema_for;
+use anyhow::Result;
+use assistant_tool::{ActionLog, Tool, ToolResult};
+use gpui::{AnyWindowHandle, App, Entity, Task};
+use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::fmt::Write as _;
+use std::sync::Arc;
+use ui::IconName;
+
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct ProjectUpdatesToolInput {}
+
+pub struct ProjectNotificationsTool;
+
+impl Tool for ProjectNotificationsTool {
+    fn name(&self) -> String {
+        "project_notifications".to_string()
+    }
+
+    fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
+        false
+    }
+    fn may_perform_edits(&self) -> bool {
+        false
+    }
+    fn description(&self) -> String {
+        include_str!("./project_notifications_tool/description.md").to_string()
+    }
+
+    fn icon(&self) -> IconName {
+        IconName::Envelope
+    }
+
+    fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
+        json_schema_for::<ProjectUpdatesToolInput>(format)
+    }
+
+    fn ui_text(&self, _input: &serde_json::Value) -> String {
+        "Check project notifications".into()
+    }
+
+    fn run(
+        self: Arc<Self>,
+        _input: serde_json::Value,
+        _request: Arc<LanguageModelRequest>,
+        _project: Entity<Project>,
+        action_log: Entity<ActionLog>,
+        _model: Arc<dyn LanguageModel>,
+        _window: Option<AnyWindowHandle>,
+        cx: &mut App,
+    ) -> ToolResult {
+        let mut stale_files = String::new();
+
+        let action_log = action_log.read(cx);
+
+        for stale_file in action_log.stale_buffers(cx) {
+            if let Some(file) = stale_file.read(cx).file() {
+                writeln!(&mut stale_files, "- {}", file.path().display()).ok();
+            }
+        }
+
+        let response = if stale_files.is_empty() {
+            "No new notifications".to_string()
+        } else {
+            // NOTE: Changes to this prompt require a symmetric update in the LLM Worker
+            const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt");
+            format!("{HEADER}{stale_files}").replace("\r\n", "\n")
+        };
+
+        Task::ready(Ok(response.into())).into()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use assistant_tool::ToolResultContent;
+    use gpui::{AppContext, TestAppContext};
+    use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModelProvider};
+    use project::{FakeFs, Project};
+    use serde_json::json;
+    use settings::SettingsStore;
+    use std::sync::Arc;
+    use util::path;
+
+    #[gpui::test]
+    async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/test"),
+            json!({"code.rs": "fn main() {\n    println!(\"Hello, world!\");\n}"}),
+        )
+        .await;
+
+        let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+
+        let buffer_path = project
+            .read_with(cx, |project, cx| {
+                project.find_project_path("test/code.rs", cx)
+            })
+            .unwrap();
+
+        let buffer = project
+            .update(cx, |project, cx| {
+                project.open_buffer(buffer_path.clone(), cx)
+            })
+            .await
+            .unwrap();
+
+        // Start tracking the buffer
+        action_log.update(cx, |log, cx| {
+            log.buffer_read(buffer.clone(), cx);
+        });
+
+        // Run the tool before any changes
+        let tool = Arc::new(ProjectNotificationsTool);
+        let provider = Arc::new(FakeLanguageModelProvider);
+        let model: Arc<dyn LanguageModel> = Arc::new(provider.test_model());
+        let request = Arc::new(LanguageModelRequest::default());
+        let tool_input = json!({});
+
+        let result = cx.update(|cx| {
+            tool.clone().run(
+                tool_input.clone(),
+                request.clone(),
+                project.clone(),
+                action_log.clone(),
+                model.clone(),
+                None,
+                cx,
+            )
+        });
+
+        let response = result.output.await.unwrap();
+        let response_text = match &response.content {
+            ToolResultContent::Text(text) => text.clone(),
+            _ => panic!("Expected text response"),
+        };
+        assert_eq!(
+            response_text.as_str(),
+            "No new notifications",
+            "Tool should return 'No new notifications' when no stale buffers"
+        );
+
+        // Modify the buffer (makes it stale)
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit([(1..1, "\nChange!\n")], None, cx);
+        });
+
+        // Run the tool again
+        let result = cx.update(|cx| {
+            tool.run(
+                tool_input.clone(),
+                request.clone(),
+                project.clone(),
+                action_log,
+                model.clone(),
+                None,
+                cx,
+            )
+        });
+
+        // This time the buffer is stale, so the tool should return a notification
+        let response = result.output.await.unwrap();
+        let response_text = match &response.content {
+            ToolResultContent::Text(text) => text.clone(),
+            _ => panic!("Expected text response"),
+        };
+
+        let expected_content = "[The following is an auto-generated notification; do not reply]\n\nThese files have changed since the last read:\n- code.rs\n";
+        assert_eq!(
+            response_text.as_str(),
+            expected_content,
+            "Tool should return the stale buffer notification"
+        );
+    }
+
+    fn init_test(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let settings_store = SettingsStore::test(cx);
+            cx.set_global(settings_store);
+            language::init(cx);
+            Project::init_settings(cx);
+            assistant_tool::init(cx);
+        });
+    }
+}

crates/assistant_tools/src/terminal_tool.rs ๐Ÿ”—

@@ -218,7 +218,7 @@ impl Tool for TerminalTool {
                     .update(cx, |project, cx| {
                         project.create_terminal(
                             TerminalKind::Task(task::SpawnInTerminal {
-                                command: program,
+                                command: Some(program),
                                 args,
                                 cwd,
                                 env,

crates/copilot/src/copilot_chat.rs ๐Ÿ”—

@@ -528,6 +528,7 @@ impl CopilotChat {
 
     pub async fn stream_completion(
         request: Request,
+        is_user_initiated: bool,
         mut cx: AsyncApp,
     ) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
         let this = cx
@@ -562,7 +563,14 @@ impl CopilotChat {
         };
 
         let api_url = configuration.api_url_from_endpoint(&token.api_endpoint);
-        stream_completion(client.clone(), token.api_key, api_url.into(), request).await
+        stream_completion(
+            client.clone(),
+            token.api_key,
+            api_url.into(),
+            request,
+            is_user_initiated,
+        )
+        .await
     }
 
     pub fn set_configuration(
@@ -697,6 +705,7 @@ async fn stream_completion(
     api_key: String,
     completion_url: Arc<str>,
     request: Request,
+    is_user_initiated: bool,
 ) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
     let is_vision_request = request.messages.iter().any(|message| match message {
       ChatMessage::User { content }
@@ -707,6 +716,8 @@ async fn stream_completion(
       _ => false,
   });
 
+    let request_initiator = if is_user_initiated { "user" } else { "agent" };
+
     let mut request_builder = HttpRequest::builder()
         .method(Method::POST)
         .uri(completion_url.as_ref())
@@ -719,7 +730,8 @@ async fn stream_completion(
         )
         .header("Authorization", format!("Bearer {}", api_key))
         .header("Content-Type", "application/json")
-        .header("Copilot-Integration-Id", "vscode-chat");
+        .header("Copilot-Integration-Id", "vscode-chat")
+        .header("X-Initiator", request_initiator);
 
     if is_vision_request {
         request_builder =

crates/dap_adapters/src/javascript.rs ๐Ÿ”—

@@ -1,11 +1,11 @@
 use adapters::latest_github_release;
 use anyhow::Context as _;
+use collections::HashMap;
 use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
 use gpui::AsyncApp;
 use serde_json::Value;
 use std::{
     borrow::Cow,
-    collections::HashMap,
     path::PathBuf,
     sync::{LazyLock, OnceLock},
 };
@@ -75,6 +75,8 @@ impl JsDebugAdapter {
         let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 
+        let mut envs = HashMap::default();
+
         let mut configuration = task_definition.config.clone();
         if let Some(configuration) = configuration.as_object_mut() {
             maybe!({
@@ -115,6 +117,12 @@ impl JsDebugAdapter {
                 }
             }
 
+            if let Some(env) = configuration.get("env").cloned() {
+                if let Ok(env) = serde_json::from_value(env) {
+                    envs = env;
+                }
+            }
+
             configuration
                 .entry("cwd")
                 .or_insert(delegate.worktree_root_path().to_string_lossy().into());
@@ -163,7 +171,7 @@ impl JsDebugAdapter {
             ),
             arguments,
             cwd: Some(delegate.worktree_root_path().to_path_buf()),
-            envs: HashMap::default(),
+            envs,
             connection: Some(adapters::TcpArguments {
                 host,
                 port,

crates/debug_adapter_extension/src/extension_locator_adapter.rs ๐Ÿ”—

@@ -44,7 +44,9 @@ impl DapLocator for ExtensionLocatorAdapter {
             .flatten()
     }
 
-    async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
-        Err(anyhow::anyhow!("Not implemented"))
+    async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
+        self.extension
+            .run_dap_locator(self.locator_name.as_ref().to_owned(), build_config)
+            .await
     }
 }

crates/debugger_ui/src/session/running.rs ๐Ÿ”—

@@ -973,7 +973,7 @@ impl RunningState {
 
                 let task_with_shell = SpawnInTerminal {
                     command_label,
-                    command,
+                    command: Some(command),
                     args,
                     ..task.resolved.clone()
                 };
@@ -1085,19 +1085,6 @@ impl RunningState {
             .map(PathBuf::from)
             .or_else(|| session.binary().unwrap().cwd.clone());
 
-        let mut args = request.args.clone();
-
-        // Handle special case for NodeJS debug adapter
-        // If only the Node binary path is provided, we set the command to None
-        // This prevents the NodeJS REPL from appearing, which is not the desired behavior
-        // The expected usage is for users to provide their own Node command, e.g., `node test.js`
-        // This allows the NodeJS debug client to attach correctly
-        let command = if args.len() > 1 {
-            Some(args.remove(0))
-        } else {
-            None
-        };
-
         let mut envs: HashMap<String, String> =
             self.session.read(cx).task_context().project_env.clone();
         if let Some(Value::Object(env)) = &request.env {
@@ -1111,32 +1098,58 @@ impl RunningState {
             }
         }
 
-        let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
-        let kind = if let Some(command) = command {
-            let title = request.title.clone().unwrap_or(command.clone());
-            TerminalKind::Task(task::SpawnInTerminal {
-                id: task::TaskId("debug".to_string()),
-                full_label: title.clone(),
-                label: title.clone(),
-                command: command.clone(),
-                args,
-                command_label: title.clone(),
-                cwd,
-                env: envs,
-                use_new_terminal: true,
-                allow_concurrent_runs: true,
-                reveal: task::RevealStrategy::NoFocus,
-                reveal_target: task::RevealTarget::Dock,
-                hide: task::HideStrategy::Never,
-                shell,
-                show_summary: false,
-                show_command: false,
-                show_rerun: false,
-            })
+        let mut args = request.args.clone();
+        let command = if envs.contains_key("VSCODE_INSPECTOR_OPTIONS") {
+            // Handle special case for NodeJS debug adapter
+            // If the Node binary path is provided (possibly with arguments like --experimental-network-inspection),
+            // we set the command to None
+            // This prevents the NodeJS REPL from appearing, which is not the desired behavior
+            // The expected usage is for users to provide their own Node command, e.g., `node test.js`
+            // This allows the NodeJS debug client to attach correctly
+            if args
+                .iter()
+                .filter(|arg| !arg.starts_with("--"))
+                .collect::<Vec<_>>()
+                .len()
+                > 1
+            {
+                Some(args.remove(0))
+            } else {
+                None
+            }
+        } else if args.len() > 0 {
+            Some(args.remove(0))
         } else {
-            TerminalKind::Shell(cwd.map(|c| c.to_path_buf()))
+            None
         };
 
+        let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
+        let title = request
+            .title
+            .clone()
+            .filter(|title| !title.is_empty())
+            .or_else(|| command.clone())
+            .unwrap_or_else(|| "Debug terminal".to_string());
+        let kind = TerminalKind::Task(task::SpawnInTerminal {
+            id: task::TaskId("debug".to_string()),
+            full_label: title.clone(),
+            label: title.clone(),
+            command: command.clone(),
+            args,
+            command_label: title.clone(),
+            cwd,
+            env: envs,
+            use_new_terminal: true,
+            allow_concurrent_runs: true,
+            reveal: task::RevealStrategy::NoFocus,
+            reveal_target: task::RevealTarget::Dock,
+            hide: task::HideStrategy::Never,
+            shell,
+            show_summary: false,
+            show_command: false,
+            show_rerun: false,
+        });
+
         let workspace = self.workspace.clone();
         let weak_project = project.downgrade();
 

crates/debugger_ui/src/session/running/breakpoint_list.rs ๐Ÿ”—

@@ -5,7 +5,8 @@ use std::{
     time::Duration,
 };
 
-use dap::{Capabilities, ExceptionBreakpointsFilter};
+use dap::{Capabilities, ExceptionBreakpointsFilter, adapters::DebugAdapterName};
+use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
 use gpui::{
     Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
@@ -16,6 +17,7 @@ use project::{
     Project,
     debugger::{
         breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint},
+        dap_store::{DapStore, PersistedAdapterOptions},
         session::Session,
     },
     worktree_store::WorktreeStore,
@@ -48,6 +50,7 @@ pub(crate) enum SelectedBreakpointKind {
 pub(crate) struct BreakpointList {
     workspace: WeakEntity<Workspace>,
     breakpoint_store: Entity<BreakpointStore>,
+    dap_store: Entity<DapStore>,
     worktree_store: Entity<WorktreeStore>,
     scrollbar_state: ScrollbarState,
     breakpoints: Vec<BreakpointEntry>,
@@ -59,6 +62,7 @@ pub(crate) struct BreakpointList {
     selected_ix: Option<usize>,
     input: Entity<Editor>,
     strip_mode: Option<ActiveBreakpointStripMode>,
+    serialize_exception_breakpoints_task: Option<Task<anyhow::Result<()>>>,
 }
 
 impl Focusable for BreakpointList {
@@ -85,24 +89,34 @@ impl BreakpointList {
         let project = project.read(cx);
         let breakpoint_store = project.breakpoint_store();
         let worktree_store = project.worktree_store();
+        let dap_store = project.dap_store();
         let focus_handle = cx.focus_handle();
         let scroll_handle = UniformListScrollHandle::new();
         let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
 
-        cx.new(|cx| Self {
-            breakpoint_store,
-            worktree_store,
-            scrollbar_state,
-            breakpoints: Default::default(),
-            hide_scrollbar_task: None,
-            show_scrollbar: false,
-            workspace,
-            session,
-            focus_handle,
-            scroll_handle,
-            selected_ix: None,
-            input: cx.new(|cx| Editor::single_line(window, cx)),
-            strip_mode: None,
+        let adapter_name = session.as_ref().map(|session| session.read(cx).adapter());
+        cx.new(|cx| {
+            let this = Self {
+                breakpoint_store,
+                dap_store,
+                worktree_store,
+                scrollbar_state,
+                breakpoints: Default::default(),
+                hide_scrollbar_task: None,
+                show_scrollbar: false,
+                workspace,
+                session,
+                focus_handle,
+                scroll_handle,
+                selected_ix: None,
+                input: cx.new(|cx| Editor::single_line(window, cx)),
+                strip_mode: None,
+                serialize_exception_breakpoints_task: None,
+            };
+            if let Some(name) = adapter_name {
+                _ = this.deserialize_exception_breakpoints(name, cx);
+            }
+            this
         })
     }
 
@@ -404,12 +418,8 @@ impl BreakpointList {
                 self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
             }
             BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
-                if let Some(session) = &self.session {
-                    let id = exception_breakpoint.id.clone();
-                    session.update(cx, |session, cx| {
-                        session.toggle_exception_breakpoint(&id, cx);
-                    });
-                }
+                let id = exception_breakpoint.id.clone();
+                self.toggle_exception_breakpoint(&id, cx);
             }
         }
         cx.notify();
@@ -480,6 +490,64 @@ impl BreakpointList {
         cx.notify();
     }
 
+    fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
+        if let Some(session) = &self.session {
+            session.update(cx, |this, cx| {
+                this.toggle_exception_breakpoint(&id, cx);
+            });
+            cx.notify();
+            const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1);
+            self.serialize_exception_breakpoints_task = Some(cx.spawn(async move |this, cx| {
+                cx.background_executor()
+                    .timer(EXCEPTION_SERIALIZATION_INTERVAL)
+                    .await;
+                this.update(cx, |this, cx| this.serialize_exception_breakpoints(cx))?
+                    .await?;
+                Ok(())
+            }));
+        }
+    }
+
+    fn kvp_key(adapter_name: &str) -> String {
+        format!("debug_adapter_`{adapter_name}`_persistence")
+    }
+    fn serialize_exception_breakpoints(
+        &mut self,
+        cx: &mut Context<Self>,
+    ) -> Task<anyhow::Result<()>> {
+        if let Some(session) = self.session.as_ref() {
+            let key = {
+                let session = session.read(cx);
+                let name = session.adapter().0;
+                Self::kvp_key(&name)
+            };
+            let settings = self.dap_store.update(cx, |this, cx| {
+                this.sync_adapter_options(session, cx);
+            });
+            let value = serde_json::to_string(&settings);
+
+            cx.background_executor()
+                .spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await })
+        } else {
+            return Task::ready(Result::Ok(()));
+        }
+    }
+
+    fn deserialize_exception_breakpoints(
+        &self,
+        adapter_name: DebugAdapterName,
+        cx: &mut Context<Self>,
+    ) -> anyhow::Result<()> {
+        let Some(val) = KEY_VALUE_STORE.read_kvp(&Self::kvp_key(&adapter_name))? else {
+            return Ok(());
+        };
+        let value: PersistedAdapterOptions = serde_json::from_str(&val)?;
+        self.dap_store
+            .update(cx, |this, _| this.set_adapter_options(adapter_name, value));
+
+        Ok(())
+    }
+
     fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
         self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
@@ -988,12 +1056,7 @@ impl ExceptionBreakpoint {
                     let list = list.clone();
                     move |_, _, cx| {
                         list.update(cx, |this, cx| {
-                            if let Some(session) = &this.session {
-                                session.update(cx, |this, cx| {
-                                    this.toggle_exception_breakpoint(&id, cx);
-                                });
-                                cx.notify();
-                            }
+                            this.toggle_exception_breakpoint(&id, cx);
                         })
                         .ok();
                     }

crates/editor/src/actions.rs ๐Ÿ”—

@@ -635,6 +635,8 @@ actions!(
         SignatureHelpNext,
         /// Navigates to the previous signature in the signature help popup.
         SignatureHelpPrevious,
+        /// Sorts selected lines by length.
+        SortLinesByLength,
         /// Sorts selected lines case-insensitively.
         SortLinesCaseInsensitive,
         /// Sorts selected lines case-sensitively.

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

@@ -10204,6 +10204,17 @@ impl Editor {
         self.manipulate_immutable_lines(window, cx, |lines| lines.sort())
     }
 
+    pub fn sort_lines_by_length(
+        &mut self,
+        _: &SortLinesByLength,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.manipulate_immutable_lines(window, cx, |lines| {
+            lines.sort_by_key(|&line| line.chars().count())
+        })
+    }
+
     pub fn sort_lines_case_insensitive(
         &mut self,
         _: &SortLinesCaseInsensitive,

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

@@ -4075,6 +4075,29 @@ async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppC
         Zห‡ยป
     "});
 
+    // Test sort_lines_by_length()
+    //
+    // Demonstrates:
+    // - โˆž is 3 bytes UTF-8, but sorted by its char count (1)
+    // - sort is stable
+    cx.set_state(indoc! {"
+        ยซ123
+        รฆ
+        12
+        โˆž
+        1
+        รฆห‡ยป
+    "});
+    cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
+    cx.assert_editor_state(indoc! {"
+        ยซรฆ
+        โˆž
+        1
+        รฆ
+        12
+        123ห‡ยป
+    "});
+
     // Test reverse_lines()
     cx.set_state(indoc! {"
         ยซ5
@@ -22325,6 +22348,19 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
         def f() -> list[str]:
             aห‡
     "});
+
+    // test does not outdent on typing : after case keyword
+    cx.set_state(indoc! {"
+        match 1:
+            caseห‡
+    "});
+    cx.update_editor(|editor, window, cx| {
+        editor.handle_input(":", window, cx);
+    });
+    cx.assert_editor_state(indoc! {"
+        match 1:
+            case:ห‡
+    "});
 }
 
 #[gpui::test]

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

@@ -225,6 +225,7 @@ impl EditorElement {
         register_action(editor, window, Editor::autoindent);
         register_action(editor, window, Editor::delete_line);
         register_action(editor, window, Editor::join_lines);
+        register_action(editor, window, Editor::sort_lines_by_length);
         register_action(editor, window, Editor::sort_lines_case_sensitive);
         register_action(editor, window, Editor::sort_lines_case_insensitive);
         register_action(editor, window, Editor::reverse_lines);

crates/eval/src/examples/file_change_notification.rs ๐Ÿ”—

@@ -14,7 +14,7 @@ impl Example for FileChangeNotificationExample {
             url: "https://github.com/octocat/hello-world".to_string(),
             revision: "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d".to_string(),
             language_server: None,
-            max_assertions: Some(1),
+            max_assertions: None,
             profile_id: AgentProfileId::default(),
             existing_thread_json: None,
             max_turns: Some(3),

crates/extension/src/extension_manifest.rs ๐Ÿ”—

@@ -130,6 +130,12 @@ impl ExtensionManifest {
 
         Ok(())
     }
+
+    pub fn allow_remote_load(&self) -> bool {
+        !self.language_servers.is_empty()
+            || !self.debug_adapters.is_empty()
+            || !self.debug_locators.is_empty()
+    }
 }
 
 pub fn build_debug_adapter_schema_path(

crates/extension_host/src/extension_host.rs ๐Ÿ”—

@@ -1670,7 +1670,7 @@ impl ExtensionStore {
                 .extensions
                 .iter()
                 .filter_map(|(id, entry)| {
-                    if entry.manifest.language_servers.is_empty() {
+                    if !entry.manifest.allow_remote_load() {
                         return None;
                     }
                     Some(proto::Extension {

crates/extension_host/src/headless_host.rs ๐Ÿ”—

@@ -125,7 +125,7 @@ impl HeadlessExtensionStore {
 
         let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?);
 
-        debug_assert!(!manifest.languages.is_empty() || !manifest.language_servers.is_empty());
+        debug_assert!(!manifest.languages.is_empty() || manifest.allow_remote_load());
 
         if manifest.version.as_ref() != extension.version.as_str() {
             anyhow::bail!(
@@ -165,7 +165,7 @@ impl HeadlessExtensionStore {
             })?;
         }
 
-        if manifest.language_servers.is_empty() {
+        if !manifest.allow_remote_load() {
             return Ok(());
         }
 
@@ -187,24 +187,28 @@ impl HeadlessExtensionStore {
                     );
                 })?;
             }
-            for (debug_adapter, meta) in &manifest.debug_adapters {
-                let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta);
+            log::info!("Loaded language server: {}", language_server_id);
+        }
 
-                this.update(cx, |this, _cx| {
-                    this.proxy.register_debug_adapter(
-                        wasm_extension.clone(),
-                        debug_adapter.clone(),
-                        &extension_dir.join(schema_path),
-                    );
-                })?;
-            }
+        for (debug_adapter, meta) in &manifest.debug_adapters {
+            let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta);
 
-            for debug_adapter in manifest.debug_locators.keys() {
-                this.update(cx, |this, _cx| {
-                    this.proxy
-                        .register_debug_locator(wasm_extension.clone(), debug_adapter.clone());
-                })?;
-            }
+            this.update(cx, |this, _cx| {
+                this.proxy.register_debug_adapter(
+                    wasm_extension.clone(),
+                    debug_adapter.clone(),
+                    &extension_dir.join(schema_path),
+                );
+            })?;
+            log::info!("Loaded debug adapter: {}", debug_adapter);
+        }
+
+        for debug_locator in manifest.debug_locators.keys() {
+            this.update(cx, |this, _cx| {
+                this.proxy
+                    .register_debug_locator(wasm_extension.clone(), debug_locator.clone());
+            })?;
+            log::info!("Loaded debug locator: {}", debug_locator);
         }
 
         Ok(())

crates/extension_host/src/wasm_host/wit.rs ๐Ÿ”—

@@ -999,7 +999,7 @@ impl Extension {
     ) -> Result<Result<DebugRequest, String>> {
         match self {
             Extension::V0_6_0(ext) => {
-                let build_config_template = resolved_build_task.into();
+                let build_config_template = resolved_build_task.try_into()?;
                 let dap_request = ext
                     .call_run_dap_locator(store, &locator_name, &build_config_template)
                     .await?

crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs ๐Ÿ”—

@@ -299,15 +299,17 @@ impl From<extension::DebugScenario> for DebugScenario {
     }
 }
 
-impl From<SpawnInTerminal> for ResolvedTask {
-    fn from(value: SpawnInTerminal) -> Self {
-        Self {
+impl TryFrom<SpawnInTerminal> for ResolvedTask {
+    type Error = anyhow::Error;
+
+    fn try_from(value: SpawnInTerminal) -> Result<Self, Self::Error> {
+        Ok(Self {
             label: value.label,
-            command: value.command,
+            command: value.command.context("missing command")?,
             args: value.args,
             env: value.env.into_iter().collect(),
             cwd: value.cwd.map(|s| s.to_string_lossy().into_owned()),
-        }
+        })
     }
 }
 

crates/gpui/src/elements/uniform_list.rs ๐Ÿ”—

@@ -7,8 +7,8 @@
 use crate::{
     AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
     Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
-    ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window,
-    point, size,
+    ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled,
+    Window, point, size,
 };
 use smallvec::SmallVec;
 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -42,6 +42,7 @@ where
         item_count,
         item_to_measure_index: 0,
         render_items: Box::new(render_range),
+        top_slot: None,
         decorations: Vec::new(),
         interactivity: Interactivity {
             element_id: Some(id),
@@ -61,6 +62,7 @@ pub struct UniformList {
     render_items: Box<
         dyn for<'a> Fn(Range<usize>, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>,
     >,
+    top_slot: Option<Box<dyn UniformListTopSlot>>,
     decorations: Vec<Box<dyn UniformListDecoration>>,
     interactivity: Interactivity,
     scroll_handle: Option<UniformListScrollHandle>,
@@ -71,6 +73,7 @@ pub struct UniformList {
 /// Frame state used by the [UniformList].
 pub struct UniformListFrameState {
     items: SmallVec<[AnyElement; 32]>,
+    top_slot_items: SmallVec<[AnyElement; 8]>,
     decorations: SmallVec<[AnyElement; 1]>,
 }
 
@@ -88,6 +91,8 @@ pub enum ScrollStrategy {
     /// May not be possible if there's not enough list items above the item scrolled to:
     /// in this case, the element will be placed at the closest possible position.
     Center,
+    /// Scrolls the element to be at the given item index from the top of the viewport.
+    ToPosition(usize),
 }
 
 #[derive(Clone, Debug, Default)]
@@ -212,6 +217,7 @@ impl Element for UniformList {
             UniformListFrameState {
                 items: SmallVec::new(),
                 decorations: SmallVec::new(),
+                top_slot_items: SmallVec::new(),
             },
         )
     }
@@ -345,6 +351,15 @@ impl Element for UniformList {
                                     }
                                 }
                             }
+                            ScrollStrategy::ToPosition(sticky_index) => {
+                                let target_y_in_viewport = item_height * sticky_index;
+                                let target_scroll_top = item_top - target_y_in_viewport;
+                                let max_scroll_top =
+                                    (content_height - list_height).max(Pixels::ZERO);
+                                let new_scroll_top =
+                                    target_scroll_top.clamp(Pixels::ZERO, max_scroll_top);
+                                updated_scroll_offset.y = -new_scroll_top;
+                            }
                         }
                         scroll_offset = *updated_scroll_offset
                     }
@@ -354,7 +369,17 @@ impl Element for UniformList {
                     let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
                         / item_height)
                         .ceil() as usize;
-                    let visible_range = first_visible_element_ix
+                    let initial_range = first_visible_element_ix
+                        ..cmp::min(last_visible_element_ix, self.item_count);
+
+                    let mut top_slot_elements = if let Some(ref mut top_slot) = self.top_slot {
+                        top_slot.compute(initial_range, window, cx)
+                    } else {
+                        SmallVec::new()
+                    };
+                    let top_slot_offset = top_slot_elements.len();
+
+                    let visible_range = (top_slot_offset + first_visible_element_ix)
                         ..cmp::min(last_visible_element_ix, self.item_count);
 
                     let items = if y_flipped {
@@ -393,6 +418,20 @@ impl Element for UniformList {
                             frame_state.items.push(item);
                         }
 
+                        if let Some(ref top_slot) = self.top_slot {
+                            top_slot.prepaint(
+                                &mut top_slot_elements,
+                                padded_bounds,
+                                item_height,
+                                scroll_offset,
+                                padding,
+                                can_scroll_horizontally,
+                                window,
+                                cx,
+                            );
+                        }
+                        frame_state.top_slot_items = top_slot_elements;
+
                         let bounds = Bounds::new(
                             padded_bounds.origin
                                 + point(
@@ -454,6 +493,9 @@ impl Element for UniformList {
                 for decoration in &mut request_layout.decorations {
                     decoration.paint(window, cx);
                 }
+                if let Some(ref top_slot) = self.top_slot {
+                    top_slot.paint(&mut request_layout.top_slot_items, window, cx);
+                }
             },
         )
     }
@@ -483,6 +525,35 @@ pub trait UniformListDecoration {
     ) -> AnyElement;
 }
 
+/// A trait for implementing top slots in a [`UniformList`].
+/// Top slots are elements that appear at the top of the list and can adjust
+/// the visible range of list items.
+pub trait UniformListTopSlot {
+    /// Returns elements to render at the top slot for the given visible range.
+    fn compute(
+        &mut self,
+        visible_range: Range<usize>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> SmallVec<[AnyElement; 8]>;
+
+    /// Layout and prepaint the top slot elements.
+    fn prepaint(
+        &self,
+        elements: &mut SmallVec<[AnyElement; 8]>,
+        bounds: Bounds<Pixels>,
+        item_height: Pixels,
+        scroll_offset: Point<Pixels>,
+        padding: crate::Edges<Pixels>,
+        can_scroll_horizontally: bool,
+        window: &mut Window,
+        cx: &mut App,
+    );
+
+    /// Paint the top slot elements.
+    fn paint(&self, elements: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App);
+}
+
 impl UniformList {
     /// Selects a specific list item for measurement.
     pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
@@ -521,6 +592,12 @@ impl UniformList {
         self
     }
 
+    /// Sets a top slot for the list.
+    pub fn with_top_slot(mut self, top_slot: impl UniformListTopSlot + 'static) -> Self {
+        self.top_slot = Some(Box::new(top_slot));
+        self
+    }
+
     fn measure_item(
         &self,
         list_width: Option<Pixels>,

crates/gpui/src/platform/keystroke.rs ๐Ÿ”—

@@ -55,7 +55,7 @@ impl Keystroke {
     ///
     /// This method assumes that `self` was typed and `target' is in the keymap, and checks
     /// both possibilities for self against the target.
-    pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
+    pub fn should_match(&self, target: &Keystroke) -> bool {
         #[cfg(not(target_os = "windows"))]
         if let Some(key_char) = self
             .key_char

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

@@ -466,12 +466,7 @@ fn handle_keyup_msg(
 }
 
 fn handle_char_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
-    let Some(input) = char::from_u32(wparam.0 as u32)
-        .filter(|c| !c.is_control())
-        .map(String::from)
-    else {
-        return Some(1);
-    };
+    let input = parse_char_message(wparam, &state_ptr)?;
     with_input_handler(&state_ptr, |input_handler| {
         input_handler.replace_text_in_range(None, &input);
     });
@@ -1228,6 +1223,36 @@ fn handle_input_language_changed(
     Some(0)
 }
 
+#[inline]
+fn parse_char_message(wparam: WPARAM, state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<String> {
+    let code_point = wparam.loword();
+    let mut lock = state_ptr.state.borrow_mut();
+    // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630
+    match code_point {
+        0xD800..=0xDBFF => {
+            // High surrogate, wait for low surrogate
+            lock.pending_surrogate = Some(code_point);
+            None
+        }
+        0xDC00..=0xDFFF => {
+            if let Some(high_surrogate) = lock.pending_surrogate.take() {
+                // Low surrogate, combine with pending high surrogate
+                String::from_utf16(&[high_surrogate, code_point]).ok()
+            } else {
+                // Invalid low surrogate without a preceding high surrogate
+                log::warn!(
+                    "Received low surrogate without a preceding high surrogate: {code_point:x}"
+                );
+                None
+            }
+        }
+        _ => {
+            lock.pending_surrogate = None;
+            String::from_utf16(&[code_point]).ok()
+        }
+    }
+}
+
 #[inline]
 fn translate_message(handle: HWND, wparam: WPARAM, lparam: LPARAM) {
     let msg = MSG {
@@ -1270,6 +1295,10 @@ where
                 capslock: current_capslock(),
             }))
         }
+        VK_PACKET => {
+            translate_message(handle, wparam, lparam);
+            None
+        }
         VK_CAPITAL => {
             let capslock = current_capslock();
             if state

crates/gpui/src/platform/windows/window.rs ๐Ÿ”—

@@ -43,6 +43,7 @@ pub struct WindowsWindowState {
 
     pub callbacks: Callbacks,
     pub input_handler: Option<PlatformInputHandler>,
+    pub pending_surrogate: Option<u16>,
     pub last_reported_modifiers: Option<Modifiers>,
     pub last_reported_capslock: Option<Capslock>,
     pub system_key_handled: bool,
@@ -105,6 +106,7 @@ impl WindowsWindowState {
         let renderer = windows_renderer::init(gpu_context, hwnd, transparent)?;
         let callbacks = Callbacks::default();
         let input_handler = None;
+        let pending_surrogate = None;
         let last_reported_modifiers = None;
         let last_reported_capslock = None;
         let system_key_handled = false;
@@ -126,6 +128,7 @@ impl WindowsWindowState {
             min_size,
             callbacks,
             input_handler,
+            pending_surrogate,
             last_reported_modifiers,
             last_reported_capslock,
             system_key_handled,

crates/language_models/src/provider/copilot_chat.rs ๐Ÿ”—

@@ -30,6 +30,7 @@ use settings::SettingsStore;
 use std::time::Duration;
 use ui::prelude::*;
 use util::debug_panic;
+use zed_llm_client::CompletionIntent;
 
 use super::anthropic::count_anthropic_tokens;
 use super::google::count_google_tokens;
@@ -268,6 +269,19 @@ impl LanguageModel for CopilotChatLanguageModel {
             LanguageModelCompletionError,
         >,
     > {
+        let is_user_initiated = request.intent.is_none_or(|intent| match intent {
+            CompletionIntent::UserPrompt
+            | CompletionIntent::ThreadContextSummarization
+            | CompletionIntent::InlineAssist
+            | CompletionIntent::TerminalInlineAssist
+            | CompletionIntent::GenerateGitCommitMessage => true,
+
+            CompletionIntent::ToolResults
+            | CompletionIntent::ThreadSummarization
+            | CompletionIntent::CreateFile
+            | CompletionIntent::EditFile => false,
+        });
+
         let copilot_request = match into_copilot_chat(&self.model, request) {
             Ok(request) => request,
             Err(err) => return futures::future::ready(Err(err.into())).boxed(),
@@ -276,7 +290,8 @@ impl LanguageModel for CopilotChatLanguageModel {
 
         let request_limiter = self.request_limiter.clone();
         let future = cx.spawn(async move |cx| {
-            let request = CopilotChat::stream_completion(copilot_request, cx.clone());
+            let request =
+                CopilotChat::stream_completion(copilot_request, is_user_initiated, cx.clone());
             request_limiter
                 .stream(async move {
                     let response = request.await?;

crates/language_tools/Cargo.toml ๐Ÿ”—

@@ -24,7 +24,6 @@ gpui.workspace = true
 itertools.workspace = true
 language.workspace = true
 lsp.workspace = true
-picker.workspace = true
 project.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/language_tools/src/lsp_tool.rs ๐Ÿ”—

@@ -1,19 +1,18 @@
-use std::{collections::hash_map, path::PathBuf, sync::Arc, time::Duration};
+use std::{collections::hash_map, path::PathBuf, rc::Rc, time::Duration};
 
 use client::proto;
 use collections::{HashMap, HashSet};
 use editor::{Editor, EditorEvent};
 use feature_flags::FeatureFlagAppExt as _;
-use gpui::{
-    Corner, DismissEvent, Entity, Focusable as _, MouseButton, Subscription, Task, WeakEntity,
-    actions,
-};
+use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
 use language::{BinaryStatus, BufferId, LocalFile, ServerHealth};
 use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
-use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};
 use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings};
 use settings::{Settings as _, SettingsStore};
-use ui::{Context, Indicator, PopoverMenuHandle, Tooltip, Window, prelude::*};
+use ui::{
+    Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
+    Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, Window, prelude::*,
+};
 
 use workspace::{StatusItemView, Workspace};
 
@@ -28,33 +27,38 @@ actions!(
 );
 
 pub struct LspTool {
-    state: Entity<PickerState>,
-    popover_menu_handle: PopoverMenuHandle<Picker<LspPickerDelegate>>,
-    lsp_picker: Option<Entity<Picker<LspPickerDelegate>>>,
+    server_state: Entity<LanguageServerState>,
+    popover_menu_handle: PopoverMenuHandle<ContextMenu>,
+    lsp_menu: Option<Entity<ContextMenu>>,
+    lsp_menu_refresh: Task<()>,
     _subscriptions: Vec<Subscription>,
 }
 
-struct PickerState {
+#[derive(Debug)]
+struct LanguageServerState {
+    items: Vec<LspItem>,
+    other_servers_start_index: Option<usize>,
     workspace: WeakEntity<Workspace>,
     lsp_store: WeakEntity<LspStore>,
     active_editor: Option<ActiveEditor>,
     language_servers: LanguageServers,
 }
 
-#[derive(Debug)]
-pub struct LspPickerDelegate {
-    state: Entity<PickerState>,
-    selected_index: usize,
-    items: Vec<LspItem>,
-    other_servers_start_index: Option<usize>,
-}
-
 struct ActiveEditor {
     editor: WeakEntity<Editor>,
     _editor_subscription: Subscription,
     editor_buffers: HashSet<BufferId>,
 }
 
+impl std::fmt::Debug for ActiveEditor {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("ActiveEditor")
+            .field("editor", &self.editor)
+            .field("editor_buffers", &self.editor_buffers)
+            .finish_non_exhaustive()
+    }
+}
+
 #[derive(Debug, Default, Clone)]
 struct LanguageServers {
     health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
@@ -104,192 +108,154 @@ impl LanguageServerHealthStatus {
     }
 }
 
-impl LspPickerDelegate {
-    fn regenerate_items(&mut self, cx: &mut Context<Picker<Self>>) {
-        self.state.update(cx, |state, cx| {
-            let editor_buffers = state
-                .active_editor
-                .as_ref()
-                .map(|active_editor| active_editor.editor_buffers.clone())
-                .unwrap_or_default();
-            let editor_buffer_paths = editor_buffers
-                .iter()
-                .filter_map(|buffer_id| {
-                    let buffer_path = state
-                        .lsp_store
-                        .update(cx, |lsp_store, cx| {
-                            Some(
-                                project::File::from_dyn(
-                                    lsp_store
-                                        .buffer_store()
+impl LanguageServerState {
+    fn fill_menu(&self, mut menu: ContextMenu, cx: &mut Context<Self>) -> ContextMenu {
+        let lsp_logs = cx
+            .try_global::<GlobalLogStore>()
+            .and_then(|lsp_logs| lsp_logs.0.upgrade());
+        let lsp_store = self.lsp_store.upgrade();
+        let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else {
+            return menu;
+        };
+
+        for (i, item) in self.items.iter().enumerate() {
+            if let LspItem::ToggleServersButton { restart } = item {
+                let label = if *restart {
+                    "Restart All Servers"
+                } else {
+                    "Stop All Servers"
+                };
+                let restart = *restart;
+                let button = ContextMenuEntry::new(label).handler({
+                    let state = cx.entity();
+                    move |_, cx| {
+                        let lsp_store = state.read(cx).lsp_store.clone();
+                        lsp_store
+                            .update(cx, |lsp_store, cx| {
+                                if restart {
+                                    let Some(workspace) = state.read(cx).workspace.upgrade() else {
+                                        return;
+                                    };
+                                    let project = workspace.read(cx).project().clone();
+                                    let buffer_store = project.read(cx).buffer_store().clone();
+                                    let worktree_store = project.read(cx).worktree_store();
+
+                                    let buffers = state
                                         .read(cx)
-                                        .get(*buffer_id)?
+                                        .language_servers
+                                        .servers_per_buffer_abs_path
+                                        .keys()
+                                        .filter_map(|abs_path| {
+                                            worktree_store.read(cx).find_worktree(abs_path, cx)
+                                        })
+                                        .filter_map(|(worktree, relative_path)| {
+                                            let entry =
+                                                worktree.read(cx).entry_for_path(&relative_path)?;
+                                            project.read(cx).path_for_entry(entry.id, cx)
+                                        })
+                                        .filter_map(|project_path| {
+                                            buffer_store.read(cx).get_by_path(&project_path)
+                                        })
+                                        .collect();
+                                    let selectors = state
                                         .read(cx)
-                                        .file(),
-                                )?
-                                .abs_path(cx),
-                            )
-                        })
-                        .ok()??;
-                    Some(buffer_path)
-                })
-                .collect::<Vec<_>>();
-
-            let mut servers_with_health_checks = HashSet::default();
-            let mut server_ids_with_health_checks = HashSet::default();
-            let mut buffer_servers =
-                Vec::with_capacity(state.language_servers.health_statuses.len());
-            let mut other_servers =
-                Vec::with_capacity(state.language_servers.health_statuses.len());
-            let buffer_server_ids = editor_buffer_paths
-                .iter()
-                .filter_map(|buffer_path| {
-                    state
-                        .language_servers
-                        .servers_per_buffer_abs_path
-                        .get(buffer_path)
-                })
-                .flatten()
-                .fold(HashMap::default(), |mut acc, (server_id, name)| {
-                    match acc.entry(*server_id) {
-                        hash_map::Entry::Occupied(mut o) => {
-                            let old_name: &mut Option<&LanguageServerName> = o.get_mut();
-                            if old_name.is_none() {
-                                *old_name = name.as_ref();
-                            }
-                        }
-                        hash_map::Entry::Vacant(v) => {
-                            v.insert(name.as_ref());
-                        }
+                                        .items
+                                        .iter()
+                                        // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
+                                        .flat_map(|item| match item {
+                                            LspItem::ToggleServersButton { .. } => None,
+                                            LspItem::WithHealthCheck(_, status, ..) => Some(
+                                                LanguageServerSelector::Name(status.name.clone()),
+                                            ),
+                                            LspItem::WithBinaryStatus(_, server_name, ..) => Some(
+                                                LanguageServerSelector::Name(server_name.clone()),
+                                            ),
+                                        })
+                                        .collect();
+                                    lsp_store.restart_language_servers_for_buffers(
+                                        buffers, selectors, cx,
+                                    );
+                                } else {
+                                    lsp_store.stop_all_language_servers(cx);
+                                }
+                            })
+                            .ok();
                     }
-                    acc
                 });
-            for (server_id, server_state) in &state.language_servers.health_statuses {
-                let binary_status = state
-                    .language_servers
-                    .binary_statuses
-                    .get(&server_state.name);
-                servers_with_health_checks.insert(&server_state.name);
-                server_ids_with_health_checks.insert(*server_id);
-                if buffer_server_ids.contains_key(server_id) {
-                    buffer_servers.push(ServerData::WithHealthCheck(
-                        *server_id,
-                        server_state,
-                        binary_status,
-                    ));
-                } else {
-                    other_servers.push(ServerData::WithHealthCheck(
-                        *server_id,
-                        server_state,
-                        binary_status,
-                    ));
-                }
-            }
-
-            let mut can_stop_all = false;
-            let mut can_restart_all = true;
+                menu = menu.separator().item(button);
+                continue;
+            };
+            let Some(server_info) = item.server_info() else {
+                continue;
+            };
+            let workspace = self.workspace.clone();
+            let server_selector = server_info.server_selector();
+            // TODO currently, Zed remote does not work well with the LSP logs
+            // https://github.com/zed-industries/zed/issues/28557
+            let has_logs = lsp_store.read(cx).as_local().is_some()
+                && lsp_logs.read(cx).has_server_logs(&server_selector);
+            let status_color = server_info
+                .binary_status
+                .and_then(|binary_status| match binary_status.status {
+                    BinaryStatus::None => None,
+                    BinaryStatus::CheckingForUpdate
+                    | BinaryStatus::Downloading
+                    | BinaryStatus::Starting => Some(Color::Modified),
+                    BinaryStatus::Stopping => Some(Color::Disabled),
+                    BinaryStatus::Stopped => Some(Color::Disabled),
+                    BinaryStatus::Failed { .. } => Some(Color::Error),
+                })
+                .or_else(|| {
+                    Some(match server_info.health? {
+                        ServerHealth::Ok => Color::Success,
+                        ServerHealth::Warning => Color::Warning,
+                        ServerHealth::Error => Color::Error,
+                    })
+                })
+                .unwrap_or(Color::Success);
 
-            for (server_name, status) in state
-                .language_servers
-                .binary_statuses
-                .iter()
-                .filter(|(name, _)| !servers_with_health_checks.contains(name))
+            if self
+                .other_servers_start_index
+                .is_some_and(|index| index == i)
             {
-                match status.status {
-                    BinaryStatus::None => {
-                        can_restart_all = false;
-                        can_stop_all = true;
-                    }
-                    BinaryStatus::CheckingForUpdate => {
-                        can_restart_all = false;
-                    }
-                    BinaryStatus::Downloading => {
-                        can_restart_all = false;
-                    }
-                    BinaryStatus::Starting => {
-                        can_restart_all = false;
-                    }
-                    BinaryStatus::Stopping => {
-                        can_restart_all = false;
-                    }
-                    BinaryStatus::Stopped => {}
-                    BinaryStatus::Failed { .. } => {}
-                }
-
-                let matching_server_id = state
-                    .language_servers
-                    .servers_per_buffer_abs_path
-                    .iter()
-                    .filter(|(path, _)| editor_buffer_paths.contains(path))
-                    .flat_map(|(_, server_associations)| server_associations.iter())
-                    .find_map(|(id, name)| {
-                        if name.as_ref() == Some(server_name) {
-                            Some(*id)
-                        } else {
-                            None
-                        }
-                    });
-                if let Some(server_id) = matching_server_id {
-                    buffer_servers.push(ServerData::WithBinaryStatus(
-                        Some(server_id),
-                        server_name,
-                        status,
-                    ));
-                } else {
-                    other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
-                }
+                menu = menu.separator();
             }
-
-            buffer_servers.sort_by_key(|data| data.name().clone());
-            other_servers.sort_by_key(|data| data.name().clone());
-
-            let mut other_servers_start_index = None;
-            let mut new_lsp_items =
-                Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1);
-            new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
-            if !new_lsp_items.is_empty() {
-                other_servers_start_index = Some(new_lsp_items.len());
-            }
-            new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
-            if !new_lsp_items.is_empty() {
-                if can_stop_all {
-                    new_lsp_items.push(LspItem::ToggleServersButton { restart: false });
-                } else if can_restart_all {
-                    new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
-                }
-            }
-
-            self.items = new_lsp_items;
-            self.other_servers_start_index = other_servers_start_index;
-        });
-    }
-
-    fn server_info(&self, ix: usize) -> Option<ServerInfo> {
-        match self.items.get(ix)? {
-            LspItem::ToggleServersButton { .. } => None,
-            LspItem::WithHealthCheck(
-                language_server_id,
-                language_server_health_status,
-                language_server_binary_status,
-            ) => Some(ServerInfo {
-                name: language_server_health_status.name.clone(),
-                id: Some(*language_server_id),
-                health: language_server_health_status.health(),
-                binary_status: language_server_binary_status.clone(),
-                message: language_server_health_status.message(),
-            }),
-            LspItem::WithBinaryStatus(
-                server_id,
-                language_server_name,
-                language_server_binary_status,
-            ) => Some(ServerInfo {
-                name: language_server_name.clone(),
-                id: *server_id,
-                health: None,
-                binary_status: Some(language_server_binary_status.clone()),
-                message: language_server_binary_status.message.clone(),
-            }),
+            menu = menu.item(ContextMenuItem::custom_entry(
+                move |_, _| {
+                    h_flex()
+                        .gap_1()
+                        .w_full()
+                        .child(Indicator::dot().color(status_color))
+                        .child(Label::new(server_info.name.0.clone()))
+                        .when(!has_logs, |div| div.cursor_default())
+                        .into_any_element()
+                },
+                {
+                    let lsp_logs = lsp_logs.clone();
+                    move |window, cx| {
+                        if !has_logs {
+                            cx.propagate();
+                            return;
+                        }
+                        lsp_logs.update(cx, |lsp_logs, cx| {
+                            lsp_logs.open_server_trace(
+                                workspace.clone(),
+                                server_selector.clone(),
+                                window,
+                                cx,
+                            );
+                        });
+                    }
+                },
+                server_info.message.map(|server_message| {
+                    DocumentationAside::new(
+                        DocumentationSide::Right,
+                        Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
+                    )
+                }),
+            ));
         }
+        menu
     }
 }
 
@@ -375,6 +341,36 @@ enum LspItem {
     },
 }
 
+impl LspItem {
+    fn server_info(&self) -> Option<ServerInfo> {
+        match self {
+            LspItem::ToggleServersButton { .. } => None,
+            LspItem::WithHealthCheck(
+                language_server_id,
+                language_server_health_status,
+                language_server_binary_status,
+            ) => Some(ServerInfo {
+                name: language_server_health_status.name.clone(),
+                id: Some(*language_server_id),
+                health: language_server_health_status.health(),
+                binary_status: language_server_binary_status.clone(),
+                message: language_server_health_status.message(),
+            }),
+            LspItem::WithBinaryStatus(
+                server_id,
+                language_server_name,
+                language_server_binary_status,
+            ) => Some(ServerInfo {
+                name: language_server_name.clone(),
+                id: *server_id,
+                health: None,
+                binary_status: Some(language_server_binary_status.clone()),
+                message: language_server_binary_status.message.clone(),
+            }),
+        }
+    }
+}
+
 impl ServerData<'_> {
     fn name(&self) -> &LanguageServerName {
         match self {
@@ -395,267 +391,21 @@ impl ServerData<'_> {
     }
 }
 
-impl PickerDelegate for LspPickerDelegate {
-    type ListItem = AnyElement;
-
-    fn match_count(&self) -> usize {
-        self.items.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
-        self.selected_index = ix;
-        cx.notify();
-    }
-
-    fn update_matches(
-        &mut self,
-        _: String,
-        _: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Task<()> {
-        cx.spawn(async move |lsp_picker, cx| {
-            cx.background_executor()
-                .timer(Duration::from_millis(30))
-                .await;
-            lsp_picker
-                .update(cx, |lsp_picker, cx| {
-                    lsp_picker.delegate.regenerate_items(cx);
-                })
-                .ok();
-        })
-    }
-
-    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        Arc::default()
-    }
-
-    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(self.selected_index)
-        {
-            let lsp_store = self.state.read(cx).lsp_store.clone();
-            lsp_store
-                .update(cx, |lsp_store, cx| {
-                    if *restart {
-                        let Some(workspace) = self.state.read(cx).workspace.upgrade() else {
-                            return;
-                        };
-                        let project = workspace.read(cx).project().clone();
-                        let buffer_store = project.read(cx).buffer_store().clone();
-                        let worktree_store = project.read(cx).worktree_store();
-
-                        let buffers = self
-                            .state
-                            .read(cx)
-                            .language_servers
-                            .servers_per_buffer_abs_path
-                            .keys()
-                            .filter_map(|abs_path| {
-                                worktree_store.read(cx).find_worktree(abs_path, cx)
-                            })
-                            .filter_map(|(worktree, relative_path)| {
-                                let entry = worktree.read(cx).entry_for_path(&relative_path)?;
-                                project.read(cx).path_for_entry(entry.id, cx)
-                            })
-                            .filter_map(|project_path| {
-                                buffer_store.read(cx).get_by_path(&project_path)
-                            })
-                            .collect();
-                        let selectors = self
-                            .items
-                            .iter()
-                            // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
-                            .flat_map(|item| match item {
-                                LspItem::ToggleServersButton { .. } => None,
-                                LspItem::WithHealthCheck(_, status, ..) => {
-                                    Some(LanguageServerSelector::Name(status.name.clone()))
-                                }
-                                LspItem::WithBinaryStatus(_, server_name, ..) => {
-                                    Some(LanguageServerSelector::Name(server_name.clone()))
-                                }
-                            })
-                            .collect();
-                        lsp_store.restart_language_servers_for_buffers(buffers, selectors, cx);
-                    } else {
-                        lsp_store.stop_all_language_servers(cx);
-                    }
-                })
-                .ok();
-        }
-
-        let Some(server_selector) = self
-            .server_info(self.selected_index)
-            .map(|info| info.server_selector())
-        else {
-            return;
-        };
-        let lsp_logs = cx.global::<GlobalLogStore>().0.clone();
-        let lsp_store = self.state.read(cx).lsp_store.clone();
-        let workspace = self.state.read(cx).workspace.clone();
-        lsp_logs
-            .update(cx, |lsp_logs, cx| {
-                let has_logs = lsp_store
-                    .update(cx, |lsp_store, _| {
-                        lsp_store.as_local().is_some() && lsp_logs.has_server_logs(&server_selector)
-                    })
-                    .unwrap_or(false);
-                if has_logs {
-                    lsp_logs.open_server_trace(workspace, server_selector, window, cx);
-                }
-            })
-            .ok();
-    }
-
-    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
-        cx.emit(DismissEvent);
-    }
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        _: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let rendered_match = h_flex().px_1().gap_1();
-        let rendered_match_contents = h_flex()
-            .id(("lsp-item", ix))
-            .w_full()
-            .px_2()
-            .gap_2()
-            .when(selected, |server_entry| {
-                server_entry.bg(cx.theme().colors().element_hover)
-            })
-            .hover(|s| s.bg(cx.theme().colors().element_hover));
-
-        if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(ix) {
-            let label = Label::new(if *restart {
-                "Restart All Servers"
-            } else {
-                "Stop All Servers"
-            });
-            return Some(
-                rendered_match
-                    .child(rendered_match_contents.child(label))
-                    .into_any_element(),
-            );
-        }
-
-        let server_info = self.server_info(ix)?;
-        let workspace = self.state.read(cx).workspace.clone();
-        let lsp_logs = cx.global::<GlobalLogStore>().0.upgrade()?;
-        let lsp_store = self.state.read(cx).lsp_store.upgrade()?;
-        let server_selector = server_info.server_selector();
-
-        // TODO currently, Zed remote does not work well with the LSP logs
-        // https://github.com/zed-industries/zed/issues/28557
-        let has_logs = lsp_store.read(cx).as_local().is_some()
-            && lsp_logs.read(cx).has_server_logs(&server_selector);
-
-        let status_color = server_info
-            .binary_status
-            .and_then(|binary_status| match binary_status.status {
-                BinaryStatus::None => None,
-                BinaryStatus::CheckingForUpdate
-                | BinaryStatus::Downloading
-                | BinaryStatus::Starting => Some(Color::Modified),
-                BinaryStatus::Stopping => Some(Color::Disabled),
-                BinaryStatus::Stopped => Some(Color::Disabled),
-                BinaryStatus::Failed { .. } => Some(Color::Error),
-            })
-            .or_else(|| {
-                Some(match server_info.health? {
-                    ServerHealth::Ok => Color::Success,
-                    ServerHealth::Warning => Color::Warning,
-                    ServerHealth::Error => Color::Error,
-                })
-            })
-            .unwrap_or(Color::Success);
-
-        Some(
-            rendered_match
-                .child(
-                    rendered_match_contents
-                        .child(Indicator::dot().color(status_color))
-                        .child(Label::new(server_info.name.0.clone()))
-                        .when_some(
-                            server_info.message.clone(),
-                            |server_entry, server_message| {
-                                server_entry.tooltip(Tooltip::text(server_message.clone()))
-                            },
-                        ),
-                )
-                .when_else(
-                    has_logs,
-                    |server_entry| {
-                        server_entry.on_mouse_down(MouseButton::Left, {
-                            let workspace = workspace.clone();
-                            let lsp_logs = lsp_logs.downgrade();
-                            let server_selector = server_selector.clone();
-                            move |_, window, cx| {
-                                lsp_logs
-                                    .update(cx, |lsp_logs, cx| {
-                                        lsp_logs.open_server_trace(
-                                            workspace.clone(),
-                                            server_selector.clone(),
-                                            window,
-                                            cx,
-                                        );
-                                    })
-                                    .ok();
-                            }
-                        })
-                    },
-                    |div| div.cursor_default(),
-                )
-                .into_any_element(),
-        )
-    }
-
-    fn render_editor(
-        &self,
-        editor: &Entity<Editor>,
-        _: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Div {
-        div().child(div().track_focus(&editor.focus_handle(cx)))
-    }
-
-    fn separators_after_indices(&self) -> Vec<usize> {
-        if self.items.is_empty() {
-            return Vec::new();
-        }
-        let mut indices = vec![self.items.len().saturating_sub(2)];
-        if let Some(other_servers_start_index) = self.other_servers_start_index {
-            if other_servers_start_index > 0 {
-                indices.insert(0, other_servers_start_index - 1);
-                indices.dedup();
-            }
-        }
-        indices
-    }
-}
-
 impl LspTool {
     pub fn new(
         workspace: &Workspace,
-        popover_menu_handle: PopoverMenuHandle<Picker<LspPickerDelegate>>,
+        popover_menu_handle: PopoverMenuHandle<ContextMenu>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
         let settings_subscription =
             cx.observe_global_in::<SettingsStore>(window, move |lsp_tool, window, cx| {
                 if ProjectSettings::get_global(cx).global_lsp_settings.button {
-                    if lsp_tool.lsp_picker.is_none() {
-                        lsp_tool.lsp_picker =
-                            Some(Self::new_lsp_picker(lsp_tool.state.clone(), window, cx));
-                        cx.notify();
+                    if lsp_tool.lsp_menu.is_none() {
+                        lsp_tool.refresh_lsp_menu(true, window, cx);
                         return;
                     }
-                } else if lsp_tool.lsp_picker.take().is_some() {
+                } else if lsp_tool.lsp_menu.take().is_some() {
                     cx.notify();
                 }
             });
@@ -666,17 +416,20 @@ impl LspTool {
                 lsp_tool.on_lsp_store_event(e, window, cx)
             });
 
-        let state = cx.new(|_| PickerState {
+        let state = cx.new(|_| LanguageServerState {
             workspace: workspace.weak_handle(),
+            items: Vec::new(),
+            other_servers_start_index: None,
             lsp_store: lsp_store.downgrade(),
             active_editor: None,
             language_servers: LanguageServers::default(),
         });
 
         Self {
-            state,
+            server_state: state,
             popover_menu_handle,
-            lsp_picker: None,
+            lsp_menu: None,
+            lsp_menu_refresh: Task::ready(()),
             _subscriptions: vec![settings_subscription, lsp_store_subscription],
         }
     }
@@ -687,7 +440,7 @@ impl LspTool {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Some(lsp_picker) = self.lsp_picker.clone() else {
+        if self.lsp_menu.is_none() {
             return;
         };
         let mut updated = false;
@@ -720,7 +473,7 @@ impl LspTool {
                                 BinaryStatus::Failed { error }
                             }
                         };
-                        self.state.update(cx, |state, _| {
+                        self.server_state.update(cx, |state, _| {
                             state.language_servers.update_binary_status(
                                 binary_status,
                                 status_update.message.as_deref(),
@@ -737,7 +490,7 @@ impl LspTool {
                             proto::ServerHealth::Warning => ServerHealth::Warning,
                             proto::ServerHealth::Error => ServerHealth::Error,
                         };
-                        self.state.update(cx, |state, _| {
+                        self.server_state.update(cx, |state, _| {
                             state.language_servers.update_server_health(
                                 *language_server_id,
                                 health,
@@ -756,7 +509,7 @@ impl LspTool {
                 message: proto::update_language_server::Variant::RegisteredForBuffer(update),
                 ..
             } => {
-                self.state.update(cx, |state, _| {
+                self.server_state.update(cx, |state, _| {
                     state
                         .language_servers
                         .servers_per_buffer_abs_path
@@ -770,27 +523,203 @@ impl LspTool {
         };
 
         if updated {
-            lsp_picker.update(cx, |lsp_picker, cx| {
-                lsp_picker.refresh(window, cx);
-            });
+            self.refresh_lsp_menu(false, window, cx);
         }
     }
 
-    fn new_lsp_picker(
-        state: Entity<PickerState>,
+    fn regenerate_items(&mut self, cx: &mut App) {
+        self.server_state.update(cx, |state, cx| {
+            let editor_buffers = state
+                .active_editor
+                .as_ref()
+                .map(|active_editor| active_editor.editor_buffers.clone())
+                .unwrap_or_default();
+            let editor_buffer_paths = editor_buffers
+                .iter()
+                .filter_map(|buffer_id| {
+                    let buffer_path = state
+                        .lsp_store
+                        .update(cx, |lsp_store, cx| {
+                            Some(
+                                project::File::from_dyn(
+                                    lsp_store
+                                        .buffer_store()
+                                        .read(cx)
+                                        .get(*buffer_id)?
+                                        .read(cx)
+                                        .file(),
+                                )?
+                                .abs_path(cx),
+                            )
+                        })
+                        .ok()??;
+                    Some(buffer_path)
+                })
+                .collect::<Vec<_>>();
+
+            let mut servers_with_health_checks = HashSet::default();
+            let mut server_ids_with_health_checks = HashSet::default();
+            let mut buffer_servers =
+                Vec::with_capacity(state.language_servers.health_statuses.len());
+            let mut other_servers =
+                Vec::with_capacity(state.language_servers.health_statuses.len());
+            let buffer_server_ids = editor_buffer_paths
+                .iter()
+                .filter_map(|buffer_path| {
+                    state
+                        .language_servers
+                        .servers_per_buffer_abs_path
+                        .get(buffer_path)
+                })
+                .flatten()
+                .fold(HashMap::default(), |mut acc, (server_id, name)| {
+                    match acc.entry(*server_id) {
+                        hash_map::Entry::Occupied(mut o) => {
+                            let old_name: &mut Option<&LanguageServerName> = o.get_mut();
+                            if old_name.is_none() {
+                                *old_name = name.as_ref();
+                            }
+                        }
+                        hash_map::Entry::Vacant(v) => {
+                            v.insert(name.as_ref());
+                        }
+                    }
+                    acc
+                });
+            for (server_id, server_state) in &state.language_servers.health_statuses {
+                let binary_status = state
+                    .language_servers
+                    .binary_statuses
+                    .get(&server_state.name);
+                servers_with_health_checks.insert(&server_state.name);
+                server_ids_with_health_checks.insert(*server_id);
+                if buffer_server_ids.contains_key(server_id) {
+                    buffer_servers.push(ServerData::WithHealthCheck(
+                        *server_id,
+                        server_state,
+                        binary_status,
+                    ));
+                } else {
+                    other_servers.push(ServerData::WithHealthCheck(
+                        *server_id,
+                        server_state,
+                        binary_status,
+                    ));
+                }
+            }
+
+            let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
+            let mut can_restart_all = state.language_servers.health_statuses.is_empty();
+            for (server_name, status) in state
+                .language_servers
+                .binary_statuses
+                .iter()
+                .filter(|(name, _)| !servers_with_health_checks.contains(name))
+            {
+                match status.status {
+                    BinaryStatus::None => {
+                        can_restart_all = false;
+                        can_stop_all |= true;
+                    }
+                    BinaryStatus::CheckingForUpdate => {
+                        can_restart_all = false;
+                        can_stop_all = false;
+                    }
+                    BinaryStatus::Downloading => {
+                        can_restart_all = false;
+                        can_stop_all = false;
+                    }
+                    BinaryStatus::Starting => {
+                        can_restart_all = false;
+                        can_stop_all = false;
+                    }
+                    BinaryStatus::Stopping => {
+                        can_restart_all = false;
+                        can_stop_all = false;
+                    }
+                    BinaryStatus::Stopped => {}
+                    BinaryStatus::Failed { .. } => {}
+                }
+
+                let matching_server_id = state
+                    .language_servers
+                    .servers_per_buffer_abs_path
+                    .iter()
+                    .filter(|(path, _)| editor_buffer_paths.contains(path))
+                    .flat_map(|(_, server_associations)| server_associations.iter())
+                    .find_map(|(id, name)| {
+                        if name.as_ref() == Some(server_name) {
+                            Some(*id)
+                        } else {
+                            None
+                        }
+                    });
+                if let Some(server_id) = matching_server_id {
+                    buffer_servers.push(ServerData::WithBinaryStatus(
+                        Some(server_id),
+                        server_name,
+                        status,
+                    ));
+                } else {
+                    other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
+                }
+            }
+
+            buffer_servers.sort_by_key(|data| data.name().clone());
+            other_servers.sort_by_key(|data| data.name().clone());
+
+            let mut other_servers_start_index = None;
+            let mut new_lsp_items =
+                Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1);
+            new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
+            if !new_lsp_items.is_empty() {
+                other_servers_start_index = Some(new_lsp_items.len());
+            }
+            new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
+            if !new_lsp_items.is_empty() {
+                if can_stop_all {
+                    new_lsp_items.push(LspItem::ToggleServersButton { restart: false });
+                } else if can_restart_all {
+                    new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
+                }
+            }
+
+            state.items = new_lsp_items;
+            state.other_servers_start_index = other_servers_start_index;
+        });
+    }
+
+    fn refresh_lsp_menu(
+        &mut self,
+        create_if_empty: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Entity<Picker<LspPickerDelegate>> {
-        cx.new(|cx| {
-            let mut delegate = LspPickerDelegate {
-                selected_index: 0,
-                other_servers_start_index: None,
-                items: Vec::new(),
-                state,
-            };
-            delegate.regenerate_items(cx);
-            Picker::list(delegate, window, cx)
-        })
+    ) {
+        if create_if_empty || self.lsp_menu.is_some() {
+            let state = self.server_state.clone();
+            self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_tool, cx| {
+                cx.background_executor()
+                    .timer(Duration::from_millis(30))
+                    .await;
+                lsp_tool
+                    .update_in(cx, |lsp_tool, window, cx| {
+                        lsp_tool.regenerate_items(cx);
+                        let menu = ContextMenu::build(window, cx, |menu, _, cx| {
+                            state.update(cx, |state, cx| state.fill_menu(menu, cx))
+                        });
+                        lsp_tool.lsp_menu = Some(menu.clone());
+                        // TODO kb will this work?
+                        // what about the selections?
+                        lsp_tool.popover_menu_handle.refresh_menu(
+                            window,
+                            cx,
+                            Rc::new(move |_, _| Some(menu.clone())),
+                        );
+                        cx.notify();
+                    })
+                    .ok();
+            });
+        }
     }
 }
 
@@ -805,7 +734,7 @@ impl StatusItemView for LspTool {
             if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
                 if Some(&editor)
                     != self
-                        .state
+                        .server_state
                         .read(cx)
                         .active_editor
                         .as_ref()

crates/languages/src/go/outline.scm ๐Ÿ”—

@@ -25,7 +25,7 @@
     receiver: (parameter_list
         "(" @context
         (parameter_declaration
-            name: (_) @name
+            name: (_) @context
             type: (_) @context)
         ")" @context)
     name: (field_identifier) @name

crates/languages/src/python/config.toml ๐Ÿ”—

@@ -34,5 +34,4 @@ decrease_indent_patterns = [
   { pattern = "^\\s*else\\b.*:",    valid_after = ["if", "elif", "for", "while", "except"] },
   { pattern = "^\\s*except\\b.*:",  valid_after = ["try", "except"] },
   { pattern = "^\\s*finally\\b.*:", valid_after = ["try", "except", "else"] },
-  { pattern = "^\\s*case\\b.*:",    valid_after = ["match", "case"] }
 ]

crates/languages/src/typescript/overrides.scm ๐Ÿ”—

@@ -1,6 +1,9 @@
 (comment) @comment.inclusive
+
 (string) @string
 
+(template_string (string_fragment) @string)
+
 (_ value: (call_expression
   function: (identifier) @function_name_before_type_arguments
   type_arguments: (type_arguments)))

crates/project/src/context_server_store.rs ๐Ÿ”—

@@ -171,6 +171,15 @@ impl ContextServerStore {
         )
     }
 
+    /// Returns all configured context server ids, regardless of enabled state.
+    pub fn configured_server_ids(&self) -> Vec<ContextServerId> {
+        self.context_server_settings
+            .keys()
+            .cloned()
+            .map(ContextServerId)
+            .collect()
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(
         registry: Entity<ContextServerDescriptorRegistry>,

crates/project/src/debugger/dap_store.rs ๐Ÿ”—

@@ -14,15 +14,13 @@ use anyhow::{Context as _, Result, anyhow};
 use async_trait::async_trait;
 use collections::HashMap;
 use dap::{
-    Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest,
-    EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, Source, StackFrameId,
+    Capabilities, DapRegistry, DebugRequest, EvaluateArgumentsContext, StackFrameId,
     adapters::{
         DapDelegate, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments,
     },
     client::SessionId,
     inline_value::VariableLookupKind,
     messages::Message,
-    requests::{Completions, Evaluate},
 };
 use fs::Fs;
 use futures::{
@@ -40,6 +38,7 @@ use rpc::{
     AnyProtoClient, TypedEnvelope,
     proto::{self},
 };
+use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsLocation, WorktreeId};
 use std::{
     borrow::Borrow,
@@ -93,10 +92,23 @@ pub struct DapStore {
     worktree_store: Entity<WorktreeStore>,
     sessions: BTreeMap<SessionId, Entity<Session>>,
     next_session_id: u32,
+    adapter_options: BTreeMap<DebugAdapterName, Arc<PersistedAdapterOptions>>,
 }
 
 impl EventEmitter<DapStoreEvent> for DapStore {}
 
+#[derive(Clone, Serialize, Deserialize)]
+pub struct PersistedExceptionBreakpoint {
+    pub enabled: bool,
+}
+
+/// Represents best-effort serialization of adapter state during last session (e.g. watches)
+#[derive(Clone, Default, Serialize, Deserialize)]
+pub struct PersistedAdapterOptions {
+    /// Which exception breakpoints were enabled during the last session with this adapter?
+    pub exception_breakpoints: BTreeMap<String, PersistedExceptionBreakpoint>,
+}
+
 impl DapStore {
     pub fn init(client: &AnyProtoClient, cx: &mut App) {
         static ADD_LOCATORS: Once = Once::new();
@@ -173,6 +185,7 @@ impl DapStore {
             breakpoint_store,
             worktree_store,
             sessions: Default::default(),
+            adapter_options: Default::default(),
         }
     }
 
@@ -520,65 +533,6 @@ impl DapStore {
         ))
     }
 
-    pub fn evaluate(
-        &self,
-        session_id: &SessionId,
-        stack_frame_id: u64,
-        expression: String,
-        context: EvaluateArgumentsContext,
-        source: Option<Source>,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<EvaluateResponse>> {
-        let Some(client) = self
-            .session_by_id(session_id)
-            .and_then(|client| client.read(cx).adapter_client())
-        else {
-            return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
-        };
-
-        cx.background_executor().spawn(async move {
-            client
-                .request::<Evaluate>(EvaluateArguments {
-                    expression: expression.clone(),
-                    frame_id: Some(stack_frame_id),
-                    context: Some(context),
-                    format: None,
-                    line: None,
-                    column: None,
-                    source,
-                })
-                .await
-        })
-    }
-
-    pub fn completions(
-        &self,
-        session_id: &SessionId,
-        stack_frame_id: u64,
-        text: String,
-        completion_column: u64,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<CompletionItem>>> {
-        let Some(client) = self
-            .session_by_id(session_id)
-            .and_then(|client| client.read(cx).adapter_client())
-        else {
-            return Task::ready(Err(anyhow!("Could not find client: {:?}", session_id)));
-        };
-
-        cx.background_executor().spawn(async move {
-            Ok(client
-                .request::<Completions>(CompletionsArguments {
-                    frame_id: Some(stack_frame_id),
-                    line: None,
-                    text,
-                    column: completion_column,
-                })
-                .await?
-                .targets)
-        })
-    }
-
     pub fn resolve_inline_value_locations(
         &self,
         session: Entity<Session>,
@@ -853,6 +807,45 @@ impl DapStore {
             })
         })
     }
+
+    pub fn sync_adapter_options(
+        &mut self,
+        session: &Entity<Session>,
+        cx: &App,
+    ) -> Arc<PersistedAdapterOptions> {
+        let session = session.read(cx);
+        let adapter = session.adapter();
+        let exceptions = session.exception_breakpoints();
+        let exception_breakpoints = exceptions
+            .map(|(exception, enabled)| {
+                (
+                    exception.filter.clone(),
+                    PersistedExceptionBreakpoint { enabled: *enabled },
+                )
+            })
+            .collect();
+        let options = Arc::new(PersistedAdapterOptions {
+            exception_breakpoints,
+        });
+        self.adapter_options.insert(adapter, options.clone());
+        options
+    }
+
+    pub fn set_adapter_options(
+        &mut self,
+        adapter: DebugAdapterName,
+        options: PersistedAdapterOptions,
+    ) {
+        self.adapter_options.insert(adapter, Arc::new(options));
+    }
+
+    pub fn adapter_options(&self, name: &str) -> Option<Arc<PersistedAdapterOptions>> {
+        self.adapter_options.get(name).cloned()
+    }
+
+    pub fn all_adapter_options(&self) -> &BTreeMap<DebugAdapterName, Arc<PersistedAdapterOptions>> {
+        &self.adapter_options
+    }
 }
 
 #[derive(Clone)]

crates/project/src/debugger/locators/cargo.rs ๐Ÿ”—

@@ -119,7 +119,7 @@ impl DapLocator for CargoLocator {
             .context("Couldn't get cwd from debug config which is needed for locators")?;
         let builder = ShellBuilder::new(true, &build_config.shell).non_interactive();
         let (program, args) = builder.build(
-            "cargo".into(),
+            Some("cargo".into()),
             &build_config
                 .args
                 .iter()

crates/project/src/debugger/session.rs ๐Ÿ”—

@@ -409,17 +409,6 @@ impl RunningMode {
         };
 
         let configuration_done_supported = ConfigurationDone::is_supported(capabilities);
-        let exception_filters = capabilities
-            .exception_breakpoint_filters
-            .as_ref()
-            .map(|exception_filters| {
-                exception_filters
-                    .iter()
-                    .filter(|filter| filter.default == Some(true))
-                    .cloned()
-                    .collect::<Vec<_>>()
-            })
-            .unwrap_or_default();
         // From spec (on initialization sequence):
         // client sends a setExceptionBreakpoints request if one or more exceptionBreakpointFilters have been defined (or if supportsConfigurationDoneRequest is not true)
         //
@@ -434,10 +423,20 @@ impl RunningMode {
             .unwrap_or_default();
         let this = self.clone();
         let worktree = self.worktree().clone();
+        let mut filters = capabilities
+            .exception_breakpoint_filters
+            .clone()
+            .unwrap_or_default();
         let configuration_sequence = cx.spawn({
-            async move |_, cx| {
-                let breakpoint_store =
-                    dap_store.read_with(cx, |dap_store, _| dap_store.breakpoint_store().clone())?;
+            async move |session, cx| {
+                let adapter_name = session.read_with(cx, |this, _| this.adapter())?;
+                let (breakpoint_store, adapter_defaults) =
+                    dap_store.read_with(cx, |dap_store, _| {
+                        (
+                            dap_store.breakpoint_store().clone(),
+                            dap_store.adapter_options(&adapter_name),
+                        )
+                    })?;
                 initialized_rx.await?;
                 let errors_by_path = cx
                     .update(|cx| this.send_source_breakpoints(false, &breakpoint_store, cx))?
@@ -471,7 +470,25 @@ impl RunningMode {
                 })?;
 
                 if should_send_exception_breakpoints {
-                    this.send_exception_breakpoints(exception_filters, supports_exception_filters)
+                    _ = session.update(cx, |this, _| {
+                        filters.retain(|filter| {
+                            let is_enabled = if let Some(defaults) = adapter_defaults.as_ref() {
+                                defaults
+                                    .exception_breakpoints
+                                    .get(&filter.filter)
+                                    .map(|options| options.enabled)
+                                    .unwrap_or_else(|| filter.default.unwrap_or_default())
+                            } else {
+                                filter.default.unwrap_or_default()
+                            };
+                            this.exception_breakpoints
+                                .entry(filter.filter.clone())
+                                .or_insert_with(|| (filter.clone(), is_enabled));
+                            is_enabled
+                        });
+                    });
+
+                    this.send_exception_breakpoints(filters, supports_exception_filters)
                         .await
                         .ok();
                 }
@@ -1233,18 +1250,7 @@ impl Session {
                     Ok(capabilities) => {
                         this.update(cx, |session, cx| {
                             session.capabilities = capabilities;
-                            let filters = session
-                                .capabilities
-                                .exception_breakpoint_filters
-                                .clone()
-                                .unwrap_or_default();
-                            for filter in filters {
-                                let default = filter.default.unwrap_or_default();
-                                session
-                                    .exception_breakpoints
-                                    .entry(filter.filter.clone())
-                                    .or_insert_with(|| (filter, default));
-                            }
+
                             cx.emit(SessionEvent::CapabilitiesLoaded);
                         })?;
                         return Ok(());

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

@@ -568,7 +568,7 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
             .into_iter()
             .map(|(source_kind, task)| {
                 let resolved = task.resolved;
-                (source_kind, resolved.command)
+                (source_kind, resolved.command.unwrap())
             })
             .collect::<Vec<_>>(),
         vec![(

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

@@ -149,7 +149,7 @@ impl Project {
         let settings = self.terminal_settings(&path, cx).clone();
 
         let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell).non_interactive();
-        let (command, args) = builder.build(command, &Vec::new());
+        let (command, args) = builder.build(Some(command), &Vec::new());
 
         let mut env = self
             .environment
@@ -297,7 +297,10 @@ impl Project {
                             .or_insert_with(|| "xterm-256color".to_string());
                         let (program, args) = wrap_for_ssh(
                             &ssh_command,
-                            Some((&spawn_task.command, &spawn_task.args)),
+                            spawn_task
+                                .command
+                                .as_ref()
+                                .map(|command| (command, &spawn_task.args)),
                             path.as_deref(),
                             env,
                             python_venv_directory.as_deref(),
@@ -317,14 +320,16 @@ impl Project {
                             add_environment_path(&mut env, &venv_path.join("bin")).log_err();
                         }
 
-                        (
-                            task_state,
+                        let shell = if let Some(program) = spawn_task.command {
                             Shell::WithArguments {
-                                program: spawn_task.command,
+                                program,
                                 args: spawn_task.args,
                                 title_override: None,
-                            },
-                        )
+                            }
+                        } else {
+                            Shell::System
+                        };
+                        (task_state, shell)
                     }
                 }
             }

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

@@ -56,7 +56,7 @@ use theme::ThemeSettings;
 use ui::{
     Color, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors,
     IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, Scrollbar,
-    ScrollbarState, Tooltip, prelude::*, v_flex,
+    ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex,
 };
 use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths};
 use workspace::{
@@ -173,6 +173,7 @@ struct EntryDetails {
     is_editing: bool,
     is_processing: bool,
     is_cut: bool,
+    sticky: Option<StickyDetails>,
     filename_text_color: Color,
     diagnostic_severity: Option<DiagnosticSeverity>,
     git_status: GitSummary,
@@ -181,6 +182,11 @@ struct EntryDetails {
     canonical_path: Option<Arc<Path>>,
 }
 
+#[derive(Debug, PartialEq, Eq, Clone)]
+struct StickyDetails {
+    sticky_index: usize,
+}
+
 /// Permanently deletes the selected file or directory.
 #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
 #[action(namespace = project_panel)]
@@ -3366,22 +3372,13 @@ impl ProjectPanel {
             }
 
             let end_ix = range.end.min(ix + visible_worktree_entries.len());
-            let (git_status_setting, show_file_icons, show_folder_icons) = {
+            let git_status_setting = {
                 let settings = ProjectPanelSettings::get_global(cx);
-                (
-                    settings.git_status,
-                    settings.file_icons,
-                    settings.folder_icons,
-                )
+                settings.git_status
             };
             if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
                 let snapshot = worktree.read(cx).snapshot();
                 let root_name = OsStr::new(snapshot.root_name());
-                let expanded_entry_ids = self
-                    .expanded_dir_ids
-                    .get(&snapshot.id())
-                    .map(Vec::as_slice)
-                    .unwrap_or(&[]);
 
                 let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
                 let entries = entries_paths.get_or_init(|| {
@@ -3394,80 +3391,17 @@ impl ProjectPanel {
                     let status = git_status_setting
                         .then_some(entry.git_summary)
                         .unwrap_or_default();
-                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
-                    let icon = match entry.kind {
-                        EntryKind::File => {
-                            if show_file_icons {
-                                FileIcons::get_icon(&entry.path, cx)
-                            } else {
-                                None
-                            }
-                        }
-                        _ => {
-                            if show_folder_icons {
-                                FileIcons::get_folder_icon(is_expanded, cx)
-                            } else {
-                                FileIcons::get_chevron_icon(is_expanded, cx)
-                            }
-                        }
-                    };
-
-                    let (depth, difference) =
-                        ProjectPanel::calculate_depth_and_difference(&entry, entries);
-
-                    let filename = match difference {
-                        diff if diff > 1 => entry
-                            .path
-                            .iter()
-                            .skip(entry.path.components().count() - diff)
-                            .collect::<PathBuf>()
-                            .to_str()
-                            .unwrap_or_default()
-                            .to_string(),
-                        _ => entry
-                            .path
-                            .file_name()
-                            .map(|name| name.to_string_lossy().into_owned())
-                            .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
-                    };
-                    let selection = SelectedEntry {
-                        worktree_id: snapshot.id(),
-                        entry_id: entry.id,
-                    };
 
-                    let is_marked = self.marked_entries.contains(&selection);
-
-                    let diagnostic_severity = self
-                        .diagnostics
-                        .get(&(*worktree_id, entry.path.to_path_buf()))
-                        .cloned();
-
-                    let filename_text_color =
-                        entry_git_aware_label_color(status, entry.is_ignored, is_marked);
-
-                    let mut details = EntryDetails {
-                        filename,
-                        icon,
-                        path: entry.path.clone(),
-                        depth,
-                        kind: entry.kind,
-                        is_ignored: entry.is_ignored,
-                        is_expanded,
-                        is_selected: self.selection == Some(selection),
-                        is_marked,
-                        is_editing: false,
-                        is_processing: false,
-                        is_cut: self
-                            .clipboard
-                            .as_ref()
-                            .map_or(false, |e| e.is_cut() && e.items().contains(&selection)),
-                        filename_text_color,
-                        diagnostic_severity,
-                        git_status: status,
-                        is_private: entry.is_private,
-                        worktree_id: *worktree_id,
-                        canonical_path: entry.canonical_path.clone(),
-                    };
+                    let mut details = self.details_for_entry(
+                        entry,
+                        *worktree_id,
+                        root_name,
+                        entries,
+                        status,
+                        None,
+                        window,
+                        cx,
+                    );
 
                     if let Some(edit_state) = &self.edit_state {
                         let is_edited_entry = if edit_state.is_new_entry() {
@@ -3879,6 +3813,8 @@ impl ProjectPanel {
         const GROUP_NAME: &str = "project_entry";
 
         let kind = details.kind;
+        let is_sticky = details.sticky.is_some();
+        let sticky_index = details.sticky.as_ref().map(|this| this.sticky_index);
         let settings = ProjectPanelSettings::get_global(cx);
         let show_editor = details.is_editing && !details.is_processing;
 
@@ -4002,141 +3938,144 @@ impl ProjectPanel {
             .border_r_2()
             .border_color(border_color)
             .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
-            .on_drag_move::<ExternalPaths>(cx.listener(
-                move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
-                    let is_current_target = this.drag_target_entry.as_ref()
-                         .map(|entry| entry.entry_id) == Some(entry_id);
-
-                    if !event.bounds.contains(&event.event.position) {
-                        // Entry responsible for setting drag target is also responsible to
-                        // clear it up after drag is out of bounds
+            .when(!is_sticky, |this| {
+                this
+                .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
+                .on_drag_move::<ExternalPaths>(cx.listener(
+                    move |this, event: &DragMoveEvent<ExternalPaths>, _, cx| {
+                        let is_current_target = this.drag_target_entry.as_ref()
+                             .map(|entry| entry.entry_id) == Some(entry_id);
+
+                        if !event.bounds.contains(&event.event.position) {
+                            // Entry responsible for setting drag target is also responsible to
+                            // clear it up after drag is out of bounds
+                            if is_current_target {
+                                this.drag_target_entry = None;
+                            }
+                            return;
+                        }
+
                         if is_current_target {
-                            this.drag_target_entry = None;
+                            return;
                         }
-                        return;
-                    }
 
-                    if is_current_target {
-                        return;
-                    }
+                        let Some((entry_id, highlight_entry_id)) = maybe!({
+                            let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx);
+                            let target_entry = target_worktree.entry_for_path(&path_for_external_paths)?;
+                            let highlight_entry_id = this.highlight_entry_for_external_drag(target_entry, target_worktree);
+                            Some((target_entry.id, highlight_entry_id))
+                        }) else {
+                            return;
+                        };
 
-                    let Some((entry_id, highlight_entry_id)) = maybe!({
-                        let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx);
-                        let target_entry = target_worktree.entry_for_path(&path_for_external_paths)?;
-                        let highlight_entry_id = this.highlight_entry_for_external_drag(target_entry, target_worktree);
-                        Some((target_entry.id, highlight_entry_id))
-                    }) else {
-                        return;
-                    };
+                        this.drag_target_entry = Some(DragTargetEntry {
+                            entry_id,
+                            highlight_entry_id,
+                        });
+                        this.marked_entries.clear();
+                    },
+                ))
+                .on_drop(cx.listener(
+                    move |this, external_paths: &ExternalPaths, window, cx| {
+                        this.drag_target_entry = None;
+                        this.hover_scroll_task.take();
+                        this.drop_external_files(external_paths.paths(), entry_id, window, cx);
+                        cx.stop_propagation();
+                    },
+                ))
+                .on_drag_move::<DraggedSelection>(cx.listener(
+                    move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
+                        let is_current_target = this.drag_target_entry.as_ref()
+                             .map(|entry| entry.entry_id) == Some(entry_id);
+
+                        if !event.bounds.contains(&event.event.position) {
+                            // Entry responsible for setting drag target is also responsible to
+                            // clear it up after drag is out of bounds
+                            if is_current_target {
+                                this.drag_target_entry = None;
+                            }
+                            return;
+                        }
 
-                    this.drag_target_entry = Some(DragTargetEntry {
-                        entry_id,
-                        highlight_entry_id,
-                    });
-                    this.marked_entries.clear();
-                },
-            ))
-            .on_drop(cx.listener(
-                move |this, external_paths: &ExternalPaths, window, cx| {
-                    this.drag_target_entry = None;
-                    this.hover_scroll_task.take();
-                    this.drop_external_files(external_paths.paths(), entry_id, window, cx);
-                    cx.stop_propagation();
-                },
-            ))
-            .on_drag_move::<DraggedSelection>(cx.listener(
-                move |this, event: &DragMoveEvent<DraggedSelection>, window, cx| {
-                    let is_current_target = this.drag_target_entry.as_ref()
-                         .map(|entry| entry.entry_id) == Some(entry_id);
-
-                    if !event.bounds.contains(&event.event.position) {
-                        // Entry responsible for setting drag target is also responsible to
-                        // clear it up after drag is out of bounds
                         if is_current_target {
-                            this.drag_target_entry = None;
+                            return;
                         }
-                        return;
-                    }
 
-                    if is_current_target {
-                        return;
-                    }
-
-                    let drag_state = event.drag(cx);
-                    let Some((entry_id, highlight_entry_id)) = maybe!({
-                        let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx);
-                        let target_entry = target_worktree.entry_for_path(&path_for_dragged_selection)?;
-                        let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, drag_state, cx);
-                        Some((target_entry.id, highlight_entry_id))
-                    }) else {
-                        return;
-                    };
+                        let drag_state = event.drag(cx);
+                        let Some((entry_id, highlight_entry_id)) = maybe!({
+                            let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx);
+                            let target_entry = target_worktree.entry_for_path(&path_for_dragged_selection)?;
+                            let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, drag_state, cx);
+                            Some((target_entry.id, highlight_entry_id))
+                        }) else {
+                            return;
+                        };
 
-                    this.drag_target_entry = Some(DragTargetEntry {
-                        entry_id,
-                        highlight_entry_id,
-                    });
-                    if drag_state.items().count() == 1 {
-                        this.marked_entries.clear();
-                        this.marked_entries.insert(drag_state.active_selection);
-                    }
-                    this.hover_expand_task.take();
+                        this.drag_target_entry = Some(DragTargetEntry {
+                            entry_id,
+                            highlight_entry_id,
+                        });
+                        if drag_state.items().count() == 1 {
+                            this.marked_entries.clear();
+                            this.marked_entries.insert(drag_state.active_selection);
+                        }
+                        this.hover_expand_task.take();
 
-                    if !kind.is_dir()
-                        || this
-                            .expanded_dir_ids
-                            .get(&details.worktree_id)
-                            .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
-                    {
-                        return;
-                    }
+                        if !kind.is_dir()
+                            || this
+                                .expanded_dir_ids
+                                .get(&details.worktree_id)
+                                .map_or(false, |ids| ids.binary_search(&entry_id).is_ok())
+                        {
+                            return;
+                        }
 
-                    let bounds = event.bounds;
-                    this.hover_expand_task =
-                        Some(cx.spawn_in(window, async move |this, cx| {
-                            cx.background_executor()
-                                .timer(Duration::from_millis(500))
-                                .await;
-                            this.update_in(cx, |this, window, cx| {
-                                this.hover_expand_task.take();
-                                if this.drag_target_entry.as_ref().map(|entry| entry.entry_id) == Some(entry_id)
-                                    && bounds.contains(&window.mouse_position())
-                                {
-                                    this.expand_entry(worktree_id, entry_id, cx);
-                                    this.update_visible_entries(
-                                        Some((worktree_id, entry_id)),
-                                        cx,
-                                    );
-                                    cx.notify();
-                                }
-                            })
-                            .ok();
-                        }));
-                },
-            ))
-            .on_drag(
-                dragged_selection,
-                move |selection, click_offset, _window, cx| {
-                    cx.new(|_| DraggedProjectEntryView {
-                        details: details.clone(),
-                        click_offset,
-                        selection: selection.active_selection,
-                        selections: selection.marked_selections.clone(),
-                    })
-                },
-            )
-            .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
-            .on_drop(
-                cx.listener(move |this, selections: &DraggedSelection, window, cx| {
-                    this.drag_target_entry = None;
-                    this.hover_scroll_task.take();
-                    this.hover_expand_task.take();
-                    if  folded_directory_drag_target.is_some() {
-                        return;
-                    }
-                    this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
-                }),
-            )
+                        let bounds = event.bounds;
+                        this.hover_expand_task =
+                            Some(cx.spawn_in(window, async move |this, cx| {
+                                cx.background_executor()
+                                    .timer(Duration::from_millis(500))
+                                    .await;
+                                this.update_in(cx, |this, window, cx| {
+                                    this.hover_expand_task.take();
+                                    if this.drag_target_entry.as_ref().map(|entry| entry.entry_id) == Some(entry_id)
+                                        && bounds.contains(&window.mouse_position())
+                                    {
+                                        this.expand_entry(worktree_id, entry_id, cx);
+                                        this.update_visible_entries(
+                                            Some((worktree_id, entry_id)),
+                                            cx,
+                                        );
+                                        cx.notify();
+                                    }
+                                })
+                                .ok();
+                            }));
+                    },
+                ))
+                .on_drag(
+                    dragged_selection,
+                    move |selection, click_offset, _window, cx| {
+                        cx.new(|_| DraggedProjectEntryView {
+                            details: details.clone(),
+                            click_offset,
+                            selection: selection.active_selection,
+                            selections: selection.marked_selections.clone(),
+                        })
+                    },
+                )
+                .on_drop(
+                    cx.listener(move |this, selections: &DraggedSelection, window, cx| {
+                        this.drag_target_entry = None;
+                        this.hover_scroll_task.take();
+                        this.hover_expand_task.take();
+                        if  folded_directory_drag_target.is_some() {
+                            return;
+                        }
+                        this.drag_onto(selections, entry_id, kind.is_file(), window, cx);
+                    }),
+                )
+            })
             .on_mouse_down(
                 MouseButton::Left,
                 cx.listener(move |this, _, _, cx| {
@@ -4168,7 +4107,7 @@ impl ProjectPanel {
                             current_selection.zip(target_selection)
                         {
                             let range_start = source_index.min(target_index);
-                            let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
+                            let range_end = source_index.max(target_index) + 1;
                             let mut new_selections = BTreeSet::new();
                             this.for_each_visible_entry(
                                 range_start..range_end,
@@ -4214,6 +4153,16 @@ impl ProjectPanel {
                         let allow_preview = preview_tabs_enabled && click_count == 1;
                         this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
                     }
+
+                    if is_sticky {
+                        if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) {
+                            let strategy = sticky_index
+                                .map(ScrollStrategy::ToPosition)
+                                .unwrap_or(ScrollStrategy::Top);
+                            this.scroll_handle.scroll_to_item(index, strategy);
+                            cx.notify();
+                        }
+                    }
                 }),
             )
             .child(
@@ -4328,51 +4277,99 @@ impl ProjectPanel {
                                                 let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned();
                                                 this = this.child(
                                                     div()
-                                                    .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
-                                                        this.hover_scroll_task.take();
-                                                        this.drag_target_entry = None;
-                                                        this.folded_directory_drag_target = None;
-                                                        if let Some(target_entry_id) = target_entry_id {
-                                                            this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
-                                                        }
-                                                    }))
+                                                    .when(!is_sticky, |div| {
+                                                        div
+                                                            .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| {
+                                                            this.hover_scroll_task.take();
+                                                            this.drag_target_entry = None;
+                                                            this.folded_directory_drag_target = None;
+                                                            if let Some(target_entry_id) = target_entry_id {
+                                                                this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
+                                                            }
+                                                        }))
+                                                        .on_drag_move(cx.listener(
+                                                            move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
+                                                                if event.bounds.contains(&event.event.position) {
+                                                                    this.folded_directory_drag_target = Some(
+                                                                        FoldedDirectoryDragTarget {
+                                                                            entry_id,
+                                                                            index: delimiter_target_index,
+                                                                            is_delimiter_target: true,
+                                                                        }
+                                                                    );
+                                                                } else {
+                                                                    let is_current_target = this.folded_directory_drag_target
+                                                                        .map_or(false, |target|
+                                                                            target.entry_id == entry_id &&
+                                                                            target.index == delimiter_target_index &&
+                                                                            target.is_delimiter_target
+                                                                        );
+                                                                    if is_current_target {
+                                                                        this.folded_directory_drag_target = None;
+                                                                    }
+                                                                }
+
+                                                            },
+                                                        ))
+                                                    })
+                                                    .child(
+                                                        Label::new(DELIMITER.clone())
+                                                            .single_line()
+                                                            .color(filename_text_color)
+                                                    )
+                                                );
+                                        }
+                                        let id = SharedString::from(format!(
+                                            "project_panel_path_component_{}_{index}",
+                                            entry_id.to_usize()
+                                        ));
+                                        let label = div()
+                                            .id(id)
+                                            .when(!is_sticky,| div| {
+                                                div
+                                                .when(index != components_len - 1, |div|{
+                                                    let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
+                                                    div
                                                     .on_drag_move(cx.listener(
                                                         move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
-                                                            if event.bounds.contains(&event.event.position) {
+                                                        if event.bounds.contains(&event.event.position) {
                                                                 this.folded_directory_drag_target = Some(
                                                                     FoldedDirectoryDragTarget {
                                                                         entry_id,
-                                                                        index: delimiter_target_index,
-                                                                        is_delimiter_target: true,
+                                                                        index,
+                                                                        is_delimiter_target: false,
                                                                     }
                                                                 );
                                                             } else {
                                                                 let is_current_target = this.folded_directory_drag_target
+                                                                    .as_ref()
                                                                     .map_or(false, |target|
                                                                         target.entry_id == entry_id &&
-                                                                        target.index == delimiter_target_index &&
-                                                                        target.is_delimiter_target
+                                                                        target.index == index &&
+                                                                        !target.is_delimiter_target
                                                                     );
                                                                 if is_current_target {
                                                                     this.folded_directory_drag_target = None;
                                                                 }
                                                             }
-
                                                         },
                                                     ))
-                                                    .child(
-                                                        Label::new(DELIMITER.clone())
-                                                            .single_line()
-                                                            .color(filename_text_color)
-                                                    )
-                                                );
-                                        }
-                                        let id = SharedString::from(format!(
-                                            "project_panel_path_component_{}_{index}",
-                                            entry_id.to_usize()
-                                        ));
-                                        let label = div()
-                                            .id(id)
+                                                    .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
+                                                        this.hover_scroll_task.take();
+                                                        this.drag_target_entry = None;
+                                                        this.folded_directory_drag_target = None;
+                                                        if let Some(target_entry_id) = target_entry_id {
+                                                            this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
+                                                        }
+                                                    }))
+                                                    .when(folded_directory_drag_target.map_or(false, |target|
+                                                        target.entry_id == entry_id &&
+                                                        target.index == index
+                                                    ), |this| {
+                                                        this.bg(item_colors.drag_over)
+                                                    })
+                                                })
+                                            })
                                             .on_click(cx.listener(move |this, _, _, cx| {
                                                 if index != active_index {
                                                     if let Some(folds) =
@@ -4384,48 +4381,6 @@ impl ProjectPanel {
                                                     }
                                                 }
                                             }))
-                                            .when(index != components_len - 1, |div|{
-                                                let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned();
-                                                div
-                                                .on_drag_move(cx.listener(
-                                                    move |this, event: &DragMoveEvent<DraggedSelection>, _, _| {
-                                                    if event.bounds.contains(&event.event.position) {
-                                                            this.folded_directory_drag_target = Some(
-                                                                FoldedDirectoryDragTarget {
-                                                                    entry_id,
-                                                                    index,
-                                                                    is_delimiter_target: false,
-                                                                }
-                                                            );
-                                                        } else {
-                                                            let is_current_target = this.folded_directory_drag_target
-                                                                .as_ref()
-                                                                .map_or(false, |target|
-                                                                    target.entry_id == entry_id &&
-                                                                    target.index == index &&
-                                                                    !target.is_delimiter_target
-                                                                );
-                                                            if is_current_target {
-                                                                this.folded_directory_drag_target = None;
-                                                            }
-                                                        }
-                                                    },
-                                                ))
-                                                .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| {
-                                                    this.hover_scroll_task.take();
-                                                    this.drag_target_entry = None;
-                                                    this.folded_directory_drag_target = None;
-                                                    if let Some(target_entry_id) = target_entry_id {
-                                                        this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx);
-                                                    }
-                                                }))
-                                                .when(folded_directory_drag_target.map_or(false, |target|
-                                                    target.entry_id == entry_id &&
-                                                    target.index == index
-                                                ), |this| {
-                                                    this.bg(item_colors.drag_over)
-                                                })
-                                            })
                                             .child(
                                                 Label::new(component)
                                                     .single_line()
@@ -4497,6 +4452,108 @@ impl ProjectPanel {
             )
     }
 
+    fn details_for_entry(
+        &self,
+        entry: &Entry,
+        worktree_id: WorktreeId,
+        root_name: &OsStr,
+        entries_paths: &HashSet<Arc<Path>>,
+        git_status: GitSummary,
+        sticky: Option<StickyDetails>,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> EntryDetails {
+        let (show_file_icons, show_folder_icons) = {
+            let settings = ProjectPanelSettings::get_global(cx);
+            (settings.file_icons, settings.folder_icons)
+        };
+
+        let expanded_entry_ids = self
+            .expanded_dir_ids
+            .get(&worktree_id)
+            .map(Vec::as_slice)
+            .unwrap_or(&[]);
+        let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
+
+        let icon = match entry.kind {
+            EntryKind::File => {
+                if show_file_icons {
+                    FileIcons::get_icon(&entry.path, cx)
+                } else {
+                    None
+                }
+            }
+            _ => {
+                if show_folder_icons {
+                    FileIcons::get_folder_icon(is_expanded, cx)
+                } else {
+                    FileIcons::get_chevron_icon(is_expanded, cx)
+                }
+            }
+        };
+
+        let (depth, difference) =
+            ProjectPanel::calculate_depth_and_difference(&entry, entries_paths);
+
+        let filename = match difference {
+            diff if diff > 1 => entry
+                .path
+                .iter()
+                .skip(entry.path.components().count() - diff)
+                .collect::<PathBuf>()
+                .to_str()
+                .unwrap_or_default()
+                .to_string(),
+            _ => entry
+                .path
+                .file_name()
+                .map(|name| name.to_string_lossy().into_owned())
+                .unwrap_or_else(|| root_name.to_string_lossy().to_string()),
+        };
+
+        let selection = SelectedEntry {
+            worktree_id,
+            entry_id: entry.id,
+        };
+        let is_marked = self.marked_entries.contains(&selection);
+        let is_selected = self.selection == Some(selection);
+
+        let diagnostic_severity = self
+            .diagnostics
+            .get(&(worktree_id, entry.path.to_path_buf()))
+            .cloned();
+
+        let filename_text_color =
+            entry_git_aware_label_color(git_status, entry.is_ignored, is_marked);
+
+        let is_cut = self
+            .clipboard
+            .as_ref()
+            .map_or(false, |e| e.is_cut() && e.items().contains(&selection));
+
+        EntryDetails {
+            filename,
+            icon,
+            path: entry.path.clone(),
+            depth,
+            kind: entry.kind,
+            is_ignored: entry.is_ignored,
+            is_expanded,
+            is_selected,
+            is_marked,
+            is_editing: false,
+            is_processing: false,
+            is_cut,
+            sticky,
+            filename_text_color,
+            diagnostic_severity,
+            git_status,
+            is_private: entry.is_private,
+            worktree_id,
+            canonical_path: entry.canonical_path.clone(),
+        }
+    }
+
     fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
         if !Self::should_show_scrollbar(cx)
             || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
@@ -4751,6 +4808,156 @@ impl ProjectPanel {
         }
         None
     }
+
+    fn candidate_entries_in_range_for_sticky(
+        &self,
+        range: Range<usize>,
+        _window: &mut Window,
+        _cx: &mut Context<Self>,
+    ) -> Vec<StickyProjectPanelCandidate> {
+        let mut result = Vec::new();
+        let mut current_offset = 0;
+
+        for (_, visible_worktree_entries, entries_paths) in &self.visible_entries {
+            let worktree_len = visible_worktree_entries.len();
+            let worktree_end_offset = current_offset + worktree_len;
+
+            if current_offset >= range.end {
+                break;
+            }
+
+            if worktree_end_offset > range.start {
+                let local_start = range.start.saturating_sub(current_offset);
+                let local_end = range.end.saturating_sub(current_offset).min(worktree_len);
+
+                let paths = entries_paths.get_or_init(|| {
+                    visible_worktree_entries
+                        .iter()
+                        .map(|e| e.path.clone())
+                        .collect()
+                });
+
+                let entries_from_this_worktree = visible_worktree_entries[local_start..local_end]
+                    .iter()
+                    .enumerate()
+                    .map(|(i, entry)| {
+                        let (depth, _) = Self::calculate_depth_and_difference(&entry.entry, paths);
+                        StickyProjectPanelCandidate {
+                            index: current_offset + local_start + i,
+                            depth,
+                        }
+                    });
+
+                result.extend(entries_from_this_worktree);
+            }
+
+            current_offset = worktree_end_offset;
+        }
+
+        result
+    }
+
+    fn render_sticky_entries(
+        &self,
+        child: StickyProjectPanelCandidate,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> SmallVec<[AnyElement; 8]> {
+        let project = self.project.read(cx);
+
+        let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else {
+            return SmallVec::new();
+        };
+
+        let Some((_, visible_worktree_entries, entries_paths)) = self
+            .visible_entries
+            .iter()
+            .find(|(id, _, _)| *id == worktree_id)
+        else {
+            return SmallVec::new();
+        };
+
+        let Some(worktree) = project.worktree_for_id(worktree_id, cx) else {
+            return SmallVec::new();
+        };
+        let worktree = worktree.read(cx).snapshot();
+
+        let paths = entries_paths.get_or_init(|| {
+            visible_worktree_entries
+                .iter()
+                .map(|e| e.path.clone())
+                .collect()
+        });
+
+        let mut sticky_parents = Vec::new();
+        let mut current_path = entry_ref.path.clone();
+
+        'outer: loop {
+            if let Some(parent_path) = current_path.parent() {
+                for ancestor_path in parent_path.ancestors() {
+                    if paths.contains(ancestor_path) {
+                        if let Some(parent_entry) = worktree.entry_for_path(ancestor_path) {
+                            sticky_parents.push(parent_entry.clone());
+                            current_path = parent_entry.path.clone();
+                            continue 'outer;
+                        }
+                    }
+                }
+            }
+            break 'outer;
+        }
+
+        sticky_parents.reverse();
+
+        let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status;
+        let root_name = OsStr::new(worktree.root_name());
+
+        let git_summaries_by_id = if git_status_enabled {
+            visible_worktree_entries
+                .iter()
+                .map(|e| (e.id, e.git_summary))
+                .collect::<HashMap<_, _>>()
+        } else {
+            Default::default()
+        };
+
+        sticky_parents
+            .iter()
+            .enumerate()
+            .map(|(index, entry)| {
+                let git_status = git_summaries_by_id
+                    .get(&entry.id)
+                    .copied()
+                    .unwrap_or_default();
+                let sticky_details = Some(StickyDetails {
+                    sticky_index: index,
+                });
+                let details = self.details_for_entry(
+                    entry,
+                    worktree_id,
+                    root_name,
+                    paths,
+                    git_status,
+                    sticky_details,
+                    window,
+                    cx,
+                );
+                self.render_entry(entry.id, details, window, cx).into_any()
+            })
+            .collect()
+    }
+}
+
+#[derive(Clone)]
+struct StickyProjectPanelCandidate {
+    index: usize,
+    depth: usize,
+}
+
+impl StickyCandidate for StickyProjectPanelCandidate {
+    fn depth(&self) -> usize {
+        self.depth
+    }
 }
 
 fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
@@ -4769,6 +4976,7 @@ impl Render for ProjectPanel {
         let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
         let show_indent_guides =
             ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
+        let show_sticky_scroll = ProjectPanelSettings::get_global(cx).sticky_scroll;
         let is_local = project.is_local();
 
         if has_worktree {
@@ -4963,6 +5171,17 @@ impl Render for ProjectPanel {
                             items
                         })
                     })
+                    .when(show_sticky_scroll, |list| {
+                        list.with_top_slot(ui::sticky_items(
+                            cx.entity().clone(),
+                            |this, range, window, cx| {
+                                this.candidate_entries_in_range_for_sticky(range, window, cx)
+                            },
+                            |this, marker_entry, window, cx| {
+                                this.render_sticky_entries(marker_entry, window, cx)
+                            },
+                        ))
+                    })
                     .when(show_indent_guides, |list| {
                         list.with_decoration(
                             ui::indent_guides(
@@ -5079,7 +5298,7 @@ impl Render for ProjectPanel {
                             .anchor(gpui::Corner::TopLeft)
                             .child(menu.clone()),
                     )
-                    .with_priority(1)
+                    .with_priority(3)
                 }))
         } else {
             v_flex()

crates/project_panel/src/project_panel_settings.rs ๐Ÿ”—

@@ -40,6 +40,7 @@ pub struct ProjectPanelSettings {
     pub git_status: bool,
     pub indent_size: f32,
     pub indent_guides: IndentGuidesSettings,
+    pub sticky_scroll: bool,
     pub auto_reveal_entries: bool,
     pub auto_fold_dirs: bool,
     pub scrollbar: ScrollbarSettings,
@@ -150,6 +151,10 @@ pub struct ProjectPanelSettingsContent {
     ///
     /// Default: false
     pub hide_root: Option<bool>,
+    /// Whether to stick parent directories at top of the project panel.
+    ///
+    /// Default: true
+    pub sticky_scroll: Option<bool>,
 }
 
 impl Settings for ProjectPanelSettings {

crates/proto/proto/debugger.proto ๐Ÿ”—

@@ -535,7 +535,7 @@ message DebugScenario {
 
 message SpawnInTerminal {
     string label = 1;
-    string command = 2;
+    optional string command = 2;
     repeated string args = 3;
     map<string, string> env = 4;
     optional string cwd = 5;

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

@@ -604,7 +604,7 @@ impl KeymapFile {
         // if trying to replace a keybinding that is not user-defined, treat it as an add operation
         match operation {
             KeybindUpdateOperation::Replace {
-                target_source,
+                target_keybind_source: target_source,
                 source,
                 ..
             } if target_source != KeybindSource::User => {
@@ -643,7 +643,12 @@ impl KeymapFile {
                     else {
                         continue;
                     };
-                    if keystrokes != target.keystrokes {
+                    if keystrokes.len() != target.keystrokes.len()
+                        || !keystrokes
+                            .iter()
+                            .zip(target.keystrokes)
+                            .all(|(a, b)| a.should_match(b))
+                    {
                         continue;
                     }
                     if action.0 != target_action_value {
@@ -655,18 +660,75 @@ impl KeymapFile {
             }
 
             if let Some(index) = found_index {
-                let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
-                    &keymap_contents,
-                    &["bindings", &target.keystrokes_unparsed()],
-                    Some(&source_action_value),
-                    Some(&source.keystrokes_unparsed()),
-                    index,
-                    tab_size,
-                )
-                .context("Failed to replace keybinding")?;
-                keymap_contents.replace_range(replace_range, &replace_value);
-
-                return Ok(keymap_contents);
+                if target.context == source.context {
+                    // if we are only changing the keybinding (common case)
+                    // not the context, etc. Then just update the binding in place
+
+                    let (replace_range, replace_value) =
+                        replace_top_level_array_value_in_json_text(
+                            &keymap_contents,
+                            &["bindings", &target.keystrokes_unparsed()],
+                            Some(&source_action_value),
+                            Some(&source.keystrokes_unparsed()),
+                            index,
+                            tab_size,
+                        )
+                        .context("Failed to replace keybinding")?;
+                    keymap_contents.replace_range(replace_range, &replace_value);
+
+                    return Ok(keymap_contents);
+                } else if keymap.0[index]
+                    .bindings
+                    .as_ref()
+                    .map_or(true, |bindings| bindings.len() == 1)
+                {
+                    // if we are replacing the only binding in the section,
+                    // just update the section in place, updating the context
+                    // and the binding
+
+                    let (replace_range, replace_value) =
+                        replace_top_level_array_value_in_json_text(
+                            &keymap_contents,
+                            &["bindings", &target.keystrokes_unparsed()],
+                            Some(&source_action_value),
+                            Some(&source.keystrokes_unparsed()),
+                            index,
+                            tab_size,
+                        )
+                        .context("Failed to replace keybinding")?;
+                    keymap_contents.replace_range(replace_range, &replace_value);
+
+                    let (replace_range, replace_value) =
+                        replace_top_level_array_value_in_json_text(
+                            &keymap_contents,
+                            &["context"],
+                            source.context.map(Into::into).as_ref(),
+                            None,
+                            index,
+                            tab_size,
+                        )
+                        .context("Failed to replace keybinding")?;
+                    keymap_contents.replace_range(replace_range, &replace_value);
+                    return Ok(keymap_contents);
+                } else {
+                    // if we are replacing one of multiple bindings in a section
+                    // with a context change, remove the existing binding from the
+                    // section, then treat this operation as an add operation of the
+                    // new binding with the updated context.
+
+                    let (replace_range, replace_value) =
+                        replace_top_level_array_value_in_json_text(
+                            &keymap_contents,
+                            &["bindings", &target.keystrokes_unparsed()],
+                            None,
+                            None,
+                            index,
+                            tab_size,
+                        )
+                        .context("Failed to replace keybinding")?;
+                    keymap_contents.replace_range(replace_range, &replace_value);
+                    operation = KeybindUpdateOperation::Add(source);
+                }
             } else {
                 log::warn!(
                     "Failed to find keybinding to update `{:?} -> {}` creating new binding for `{:?} -> {}` instead",
@@ -712,7 +774,7 @@ pub enum KeybindUpdateOperation<'a> {
         source: KeybindUpdateTarget<'a>,
         /// Describes the keybind to remove
         target: KeybindUpdateTarget<'a>,
-        target_source: KeybindSource,
+        target_keybind_source: KeybindSource,
     },
     Add(KeybindUpdateTarget<'a>),
 }
@@ -1001,7 +1063,7 @@ mod tests {
                     use_key_equivalents: false,
                     input: Some(r#"{"foo": "bar"}"#),
                 },
-                target_source: KeybindSource::Base,
+                target_keybind_source: KeybindSource::Base,
             },
             r#"[
                 {
@@ -1027,14 +1089,14 @@ mod tests {
             r#"[
                 {
                     "bindings": {
-                        "ctrl-a": "zed::SomeAction"
+                        "a": "zed::SomeAction"
                     }
                 }
             ]"#
             .unindent(),
             KeybindUpdateOperation::Replace {
                 target: KeybindUpdateTarget {
-                    keystrokes: &parse_keystrokes("ctrl-a"),
+                    keystrokes: &parse_keystrokes("a"),
                     action_name: "zed::SomeAction",
                     context: None,
                     use_key_equivalents: false,
@@ -1047,7 +1109,7 @@ mod tests {
                     use_key_equivalents: false,
                     input: Some(r#"{"foo": "bar"}"#),
                 },
-                target_source: KeybindSource::User,
+                target_keybind_source: KeybindSource::User,
             },
             r#"[
                 {
@@ -1088,7 +1150,7 @@ mod tests {
                     use_key_equivalents: false,
                     input: None,
                 },
-                target_source: KeybindSource::User,
+                target_keybind_source: KeybindSource::User,
             },
             r#"[
                 {
@@ -1131,7 +1193,7 @@ mod tests {
                     use_key_equivalents: false,
                     input: Some(r#"{"foo": "bar"}"#),
                 },
-                target_source: KeybindSource::User,
+                target_keybind_source: KeybindSource::User,
             },
             r#"[
                 {
@@ -1149,5 +1211,88 @@ mod tests {
             ]"#
             .unindent(),
         );
+
+        check_keymap_update(
+            r#"[
+                {
+                    "context": "SomeContext",
+                    "bindings": {
+                        "a": "foo::bar",
+                        "b": "baz::qux",
+                    }
+                }
+            ]"#
+            .unindent(),
+            KeybindUpdateOperation::Replace {
+                target: KeybindUpdateTarget {
+                    keystrokes: &parse_keystrokes("a"),
+                    action_name: "foo::bar",
+                    context: Some("SomeContext"),
+                    use_key_equivalents: false,
+                    input: None,
+                },
+                source: KeybindUpdateTarget {
+                    keystrokes: &parse_keystrokes("c"),
+                    action_name: "foo::baz",
+                    context: Some("SomeOtherContext"),
+                    use_key_equivalents: false,
+                    input: None,
+                },
+                target_keybind_source: KeybindSource::User,
+            },
+            r#"[
+                {
+                    "context": "SomeContext",
+                    "bindings": {
+                        "b": "baz::qux",
+                    }
+                },
+                {
+                    "context": "SomeOtherContext",
+                    "bindings": {
+                        "c": "foo::baz"
+                    }
+                }
+            ]"#
+            .unindent(),
+        );
+
+        check_keymap_update(
+            r#"[
+                {
+                    "context": "SomeContext",
+                    "bindings": {
+                        "a": "foo::bar",
+                    }
+                }
+            ]"#
+            .unindent(),
+            KeybindUpdateOperation::Replace {
+                target: KeybindUpdateTarget {
+                    keystrokes: &parse_keystrokes("a"),
+                    action_name: "foo::bar",
+                    context: Some("SomeContext"),
+                    use_key_equivalents: false,
+                    input: None,
+                },
+                source: KeybindUpdateTarget {
+                    keystrokes: &parse_keystrokes("c"),
+                    action_name: "foo::baz",
+                    context: Some("SomeOtherContext"),
+                    use_key_equivalents: false,
+                    input: None,
+                },
+                target_keybind_source: KeybindSource::User,
+            },
+            r#"[
+                {
+                    "context": "SomeOtherContext",
+                    "bindings": {
+                        "c": "foo::baz",
+                    }
+                }
+            ]"#
+            .unindent(),
+        );
     }
 }

crates/settings_ui/src/keybindings.rs ๐Ÿ”—

@@ -1,4 +1,7 @@
-use std::{ops::Range, sync::Arc};
+use std::{
+    ops::{Not, Range},
+    sync::Arc,
+};
 
 use anyhow::{Context as _, anyhow};
 use collections::HashSet;
@@ -824,6 +827,7 @@ impl RenderOnce for SyntaxHighlightedText {
 struct KeybindingEditorModal {
     editing_keybind: ProcessedKeybinding,
     keybind_editor: Entity<KeystrokeInput>,
+    context_editor: Entity<Editor>,
     fs: Arc<dyn Fs>,
     error: Option<String>,
 }
@@ -842,17 +846,86 @@ impl KeybindingEditorModal {
     pub fn new(
         editing_keybind: ProcessedKeybinding,
         fs: Arc<dyn Fs>,
-        _window: &mut Window,
+        window: &mut Window,
         cx: &mut App,
     ) -> Self {
         let keybind_editor = cx.new(KeystrokeInput::new);
+        let context_editor = cx.new(|cx| {
+            let mut editor = Editor::single_line(window, cx);
+            if let Some(context) = editing_keybind
+                .context
+                .as_ref()
+                .and_then(KeybindContextString::local)
+            {
+                editor.set_text(context.clone(), window, cx);
+            } else {
+                editor.set_placeholder_text("Keybinding context", cx);
+            }
+
+            editor
+        });
         Self {
             editing_keybind,
             fs,
             keybind_editor,
+            context_editor,
             error: None,
         }
     }
+
+    fn save(&mut self, cx: &mut Context<Self>) {
+        let existing_keybind = self.editing_keybind.clone();
+        let fs = self.fs.clone();
+        let new_keystrokes = self
+            .keybind_editor
+            .read_with(cx, |editor, _| editor.keystrokes().to_vec());
+        if new_keystrokes.is_empty() {
+            self.error = Some("Keystrokes cannot be empty".to_string());
+            cx.notify();
+            return;
+        }
+        let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
+        let new_context = self
+            .context_editor
+            .read_with(cx, |editor, cx| editor.text(cx));
+        let new_context = new_context.is_empty().not().then_some(new_context);
+        let new_context_err = new_context.as_deref().and_then(|context| {
+            gpui::KeyBindingContextPredicate::parse(context)
+                .context("Failed to parse key context")
+                .err()
+        });
+        if let Some(err) = new_context_err {
+            // TODO: store and display as separate error
+            // TODO: also, should be validating on keystroke
+            self.error = Some(err.to_string());
+            cx.notify();
+            return;
+        }
+
+        cx.spawn(async move |this, cx| {
+            if let Err(err) = save_keybinding_update(
+                existing_keybind,
+                &new_keystrokes,
+                new_context.as_deref(),
+                &fs,
+                tab_size,
+            )
+            .await
+            {
+                this.update(cx, |this, cx| {
+                    this.error = Some(err.to_string());
+                    cx.notify();
+                })
+                .log_err();
+            } else {
+                this.update(cx, |_this, cx| {
+                    cx.emit(DismissEvent);
+                })
+                .ok();
+            }
+        })
+        .detach();
+    }
 }
 
 impl Render for KeybindingEditorModal {
@@ -868,14 +941,35 @@ impl Render for KeybindingEditorModal {
                     .gap_2()
                     .child(
                         v_flex().child(Label::new("Edit Keystroke")).child(
-                            Label::new(
-                                "Input the desired keystroke for the selected action and hit save.",
-                            )
-                            .color(Color::Muted),
+                            Label::new("Input the desired keystroke for the selected action.")
+                                .color(Color::Muted),
                         ),
                     )
                     .child(self.keybind_editor.clone()),
             )
+            .child(
+                v_flex()
+                    .p_3()
+                    .gap_3()
+                    .child(
+                        v_flex().child(Label::new("Edit Keystroke")).child(
+                            Label::new("Input the desired keystroke for the selected action.")
+                                .color(Color::Muted),
+                        ),
+                    )
+                    .child(
+                        div()
+                            .w_full()
+                            .border_color(cx.theme().colors().border_variant)
+                            .border_1()
+                            .py_2()
+                            .px_3()
+                            .min_h_8()
+                            .rounded_md()
+                            .bg(theme.editor_background)
+                            .child(self.context_editor.clone()),
+                    ),
+            )
             .child(
                 h_flex()
                     .p_2()
@@ -888,38 +982,11 @@ impl Render for KeybindingEditorModal {
                         Button::new("cancel", "Cancel")
                             .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
                     )
-                    .child(Button::new("save-btn", "Save").on_click(cx.listener(
-                        |this, _event, _window, cx| {
-                            let existing_keybind = this.editing_keybind.clone();
-                            let fs = this.fs.clone();
-                            let new_keystrokes = this
-                                .keybind_editor
-                                .read_with(cx, |editor, _| editor.keystrokes.clone());
-                            if new_keystrokes.is_empty() {
-                                this.error = Some("Keystrokes cannot be empty".to_string());
-                                cx.notify();
-                                return;
-                            }
-                            let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
-                            cx.spawn(async move |this, cx| {
-                                if let Err(err) = save_keybinding_update(
-                                    existing_keybind,
-                                    &new_keystrokes,
-                                    &fs,
-                                    tab_size,
-                                )
-                                .await
-                                {
-                                    this.update(cx, |this, cx| {
-                                        this.error = Some(err.to_string());
-                                        cx.notify();
-                                    })
-                                    .log_err();
-                                }
-                            })
-                            .detach();
-                        },
-                    ))),
+                    .child(
+                        Button::new("save-btn", "Save").on_click(
+                            cx.listener(|this, _event, _window, cx| Self::save(this, cx)),
+                        ),
+                    ),
             )
             .when_some(self.error.clone(), |this, error| {
                 this.child(
@@ -937,6 +1004,7 @@ impl Render for KeybindingEditorModal {
 async fn save_keybinding_update(
     existing: ProcessedKeybinding,
     new_keystrokes: &[Keystroke],
+    new_context: Option<&str>,
     fs: &Arc<dyn Fs>,
     tab_size: usize,
 ) -> anyhow::Result<()> {
@@ -950,7 +1018,7 @@ async fn save_keybinding_update(
         .map(|keybinding| keybinding.keystrokes.as_slice())
         .unwrap_or_default();
 
-    let context = existing
+    let existing_context = existing
         .context
         .as_ref()
         .and_then(KeybindContextString::local_str);
@@ -963,18 +1031,18 @@ async fn save_keybinding_update(
     let operation = if existing.ui_key_binding.is_some() {
         settings::KeybindUpdateOperation::Replace {
             target: settings::KeybindUpdateTarget {
-                context,
+                context: existing_context,
                 keystrokes: existing_keystrokes,
                 action_name: &existing.action,
                 use_key_equivalents: false,
                 input,
             },
-            target_source: existing
+            target_keybind_source: existing
                 .source
                 .map(|(source, _name)| source)
                 .unwrap_or(KeybindSource::User),
             source: settings::KeybindUpdateTarget {
-                context,
+                context: new_context,
                 keystrokes: new_keystrokes,
                 action_name: &existing.action,
                 use_key_equivalents: false,
@@ -1071,6 +1139,17 @@ impl KeystrokeInput {
         cx.stop_propagation();
         cx.notify();
     }
+
+    fn keystrokes(&self) -> &[Keystroke] {
+        if self
+            .keystrokes
+            .last()
+            .map_or(false, |last| last.key.is_empty())
+        {
+            return &self.keystrokes[..self.keystrokes.len() - 1];
+        }
+        return &self.keystrokes;
+    }
 }
 
 impl Focusable for KeystrokeInput {

crates/task/src/shell_builder.rs ๐Ÿ”—

@@ -149,17 +149,23 @@ impl ShellBuilder {
         }
     }
     /// Returns the program and arguments to run this task in a shell.
-    pub fn build(mut self, task_command: String, task_args: &Vec<String>) -> (String, Vec<String>) {
-        let combined_command = task_args
-            .into_iter()
-            .fold(task_command, |mut command, arg| {
-                command.push(' ');
-                command.push_str(&self.kind.to_shell_variable(arg));
-                command
-            });
+    pub fn build(
+        mut self,
+        task_command: Option<String>,
+        task_args: &Vec<String>,
+    ) -> (String, Vec<String>) {
+        if let Some(task_command) = task_command {
+            let combined_command = task_args
+                .into_iter()
+                .fold(task_command, |mut command, arg| {
+                    command.push(' ');
+                    command.push_str(&self.kind.to_shell_variable(arg));
+                    command
+                });
 
-        self.args
-            .extend(self.kind.args_for_shell(self.interactive, combined_command));
+            self.args
+                .extend(self.kind.args_for_shell(self.interactive, combined_command));
+        }
 
         (self.program, self.args)
     }

crates/task/src/task.rs ๐Ÿ”—

@@ -44,7 +44,7 @@ pub struct SpawnInTerminal {
     /// Human readable name of the terminal tab.
     pub label: String,
     /// Executable command to spawn.
-    pub command: String,
+    pub command: Option<String>,
     /// Arguments to the command, potentially unsubstituted,
     /// to let the shell that spawns the command to do the substitution, if needed.
     pub args: Vec<String>,

crates/task/src/task_template.rs ๐Ÿ”—

@@ -255,7 +255,7 @@ impl TaskTemplate {
                         command_label
                     },
                 ),
-                command,
+                command: Some(command),
                 args: self.args.clone(),
                 env,
                 use_new_terminal: self.use_new_terminal,
@@ -635,7 +635,7 @@ mod tests {
                 "Human-readable label should have long substitutions trimmed"
             );
             assert_eq!(
-                spawn_in_terminal.command,
+                spawn_in_terminal.command.clone().unwrap(),
                 format!("echo test_file {long_value}"),
                 "Command should be substituted with variables and those should not be shortened"
             );
@@ -652,7 +652,7 @@ mod tests {
                 spawn_in_terminal.command_label,
                 format!(
                     "{} arg1 test_selected_text arg2 5678 arg3 {long_value}",
-                    spawn_in_terminal.command
+                    spawn_in_terminal.command.clone().unwrap()
                 ),
                 "Command label args should be substituted with variables and those should not be shortened"
             );
@@ -711,7 +711,7 @@ mod tests {
         assert_substituted_variables(&resolved_task, Vec::new());
         let resolved = resolved_task.resolved;
         assert_eq!(resolved.label, task.label);
-        assert_eq!(resolved.command, task.command);
+        assert_eq!(resolved.command, Some(task.command));
         assert_eq!(resolved.args, task.args);
     }
 

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

@@ -30,6 +30,7 @@ mod scrollbar;
 mod settings_container;
 mod settings_group;
 mod stack;
+mod sticky_items;
 mod tab;
 mod tab_bar;
 mod toggle;
@@ -70,6 +71,7 @@ pub use scrollbar::*;
 pub use settings_container::*;
 pub use settings_group::*;
 pub use stack::*;
+pub use sticky_items::*;
 pub use tab::*;
 pub use tab_bar::*;
 pub use toggle::*;

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

@@ -24,6 +24,7 @@ pub enum ContextMenuItem {
         entry_render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
         handler: Rc<dyn Fn(Option<&FocusHandle>, &mut Window, &mut App)>,
         selectable: bool,
+        documentation_aside: Option<DocumentationAside>,
     },
 }
 
@@ -31,11 +32,13 @@ impl ContextMenuItem {
     pub fn custom_entry(
         entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
         handler: impl Fn(&mut Window, &mut App) + 'static,
+        documentation_aside: Option<DocumentationAside>,
     ) -> Self {
         Self::CustomEntry {
             entry_render: Box::new(entry_render),
             handler: Rc::new(move |_, window, cx| handler(window, cx)),
             selectable: true,
+            documentation_aside,
         }
     }
 }
@@ -170,6 +173,12 @@ pub struct DocumentationAside {
     render: Rc<dyn Fn(&mut App) -> AnyElement>,
 }
 
+impl DocumentationAside {
+    pub fn new(side: DocumentationSide, render: Rc<dyn Fn(&mut App) -> AnyElement>) -> Self {
+        Self { side, render }
+    }
+}
+
 impl Focusable for ContextMenu {
     fn focus_handle(&self, _cx: &App) -> FocusHandle {
         self.focus_handle.clone()
@@ -456,6 +465,7 @@ impl ContextMenu {
             entry_render: Box::new(entry_render),
             handler: Rc::new(|_, _, _| {}),
             selectable: false,
+            documentation_aside: None,
         });
         self
     }
@@ -469,6 +479,7 @@ impl ContextMenu {
             entry_render: Box::new(entry_render),
             handler: Rc::new(move |_, window, cx| handler(window, cx)),
             selectable: true,
+            documentation_aside: None,
         });
         self
     }
@@ -705,10 +716,19 @@ impl ContextMenu {
         let item = self.items.get(ix)?;
         if item.is_selectable() {
             self.selected_index = Some(ix);
-            if let ContextMenuItem::Entry(entry) = item {
-                if let Some(callback) = &entry.documentation_aside {
+            match item {
+                ContextMenuItem::Entry(entry) => {
+                    if let Some(callback) = &entry.documentation_aside {
+                        self.documentation_aside = Some((ix, callback.clone()));
+                    }
+                }
+                ContextMenuItem::CustomEntry {
+                    documentation_aside: Some(callback),
+                    ..
+                } => {
                     self.documentation_aside = Some((ix, callback.clone()));
                 }
+                _ => (),
             }
         }
         Some(ix)
@@ -806,6 +826,7 @@ impl ContextMenu {
                 entry_render,
                 handler,
                 selectable,
+                ..
             } => {
                 let handler = handler.clone();
                 let menu = cx.entity().downgrade();

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

@@ -105,6 +105,24 @@ impl<M: ManagedView> PopoverMenuHandle<M> {
                 .map_or(false, |model| model.focus_handle(cx).is_focused(window))
         })
     }
+
+    pub fn refresh_menu(
+        &self,
+        window: &mut Window,
+        cx: &mut App,
+        new_menu_builder: Rc<dyn Fn(&mut Window, &mut App) -> Option<Entity<M>>>,
+    ) {
+        let show_menu = if let Some(state) = self.0.borrow_mut().as_mut() {
+            state.menu_builder = new_menu_builder;
+            state.menu.borrow().is_some()
+        } else {
+            false
+        };
+
+        if show_menu {
+            self.show(window, cx);
+        }
+    }
 }
 
 pub struct PopoverMenu<M: ManagedView> {

crates/ui/src/components/sticky_items.rs ๐Ÿ”—

@@ -0,0 +1,150 @@
+use std::ops::Range;
+
+use gpui::{
+    AnyElement, App, AvailableSpace, Bounds, Context, Entity, Pixels, Render, UniformListTopSlot,
+    Window, point, size,
+};
+use smallvec::SmallVec;
+
+pub trait StickyCandidate {
+    fn depth(&self) -> usize;
+}
+
+pub struct StickyItems<T> {
+    compute_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<T>>,
+    render_fn: Box<dyn Fn(T, &mut Window, &mut App) -> SmallVec<[AnyElement; 8]>>,
+    last_item_is_drifting: bool,
+    anchor_index: Option<usize>,
+}
+
+pub fn sticky_items<V, T>(
+    entity: Entity<V>,
+    compute_fn: impl Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<T> + 'static,
+    render_fn: impl Fn(&mut V, T, &mut Window, &mut Context<V>) -> SmallVec<[AnyElement; 8]> + 'static,
+) -> StickyItems<T>
+where
+    V: Render,
+    T: StickyCandidate + Clone + 'static,
+{
+    let entity_compute = entity.clone();
+    let entity_render = entity.clone();
+
+    let compute_fn = Box::new(
+        move |range: Range<usize>, window: &mut Window, cx: &mut App| -> Vec<T> {
+            entity_compute.update(cx, |view, cx| compute_fn(view, range, window, cx))
+        },
+    );
+    let render_fn = Box::new(
+        move |entry: T, window: &mut Window, cx: &mut App| -> SmallVec<[AnyElement; 8]> {
+            entity_render.update(cx, |view, cx| render_fn(view, entry, window, cx))
+        },
+    );
+    StickyItems {
+        compute_fn,
+        render_fn,
+        last_item_is_drifting: false,
+        anchor_index: None,
+    }
+}
+
+impl<T> UniformListTopSlot for StickyItems<T>
+where
+    T: StickyCandidate + Clone + 'static,
+{
+    fn compute(
+        &mut self,
+        visible_range: Range<usize>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> SmallVec<[AnyElement; 8]> {
+        let entries = (self.compute_fn)(visible_range.clone(), window, cx);
+
+        let mut anchor_entry = None;
+
+        let mut iter = entries.iter().enumerate().peekable();
+        while let Some((ix, current_entry)) = iter.next() {
+            let current_depth = current_entry.depth();
+            let index_in_range = ix;
+
+            if current_depth < index_in_range {
+                anchor_entry = Some(current_entry.clone());
+                break;
+            }
+
+            if let Some(&(_next_ix, next_entry)) = iter.peek() {
+                let next_depth = next_entry.depth();
+
+                if next_depth < current_depth && next_depth < index_in_range {
+                    self.last_item_is_drifting = true;
+                    self.anchor_index = Some(visible_range.start + ix);
+                    anchor_entry = Some(current_entry.clone());
+                    break;
+                }
+            }
+        }
+
+        if let Some(anchor_entry) = anchor_entry {
+            (self.render_fn)(anchor_entry, window, cx)
+        } else {
+            SmallVec::new()
+        }
+    }
+
+    fn prepaint(
+        &self,
+        items: &mut SmallVec<[AnyElement; 8]>,
+        bounds: Bounds<Pixels>,
+        item_height: Pixels,
+        scroll_offset: gpui::Point<Pixels>,
+        padding: gpui::Edges<Pixels>,
+        can_scroll_horizontally: bool,
+        window: &mut Window,
+        cx: &mut App,
+    ) {
+        let items_count = items.len();
+
+        for (ix, item) in items.iter_mut().enumerate() {
+            let mut item_y_offset = None;
+            if ix == items_count - 1 && self.last_item_is_drifting {
+                if let Some(anchor_index) = self.anchor_index {
+                    let scroll_top = -scroll_offset.y;
+                    let anchor_top = item_height * anchor_index;
+                    let sticky_area_height = item_height * items_count;
+                    item_y_offset =
+                        Some((anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO));
+                };
+            }
+
+            let sticky_origin = bounds.origin
+                + point(
+                    if can_scroll_horizontally {
+                        scroll_offset.x + padding.left
+                    } else {
+                        scroll_offset.x
+                    },
+                    item_height * ix + padding.top + item_y_offset.unwrap_or(Pixels::ZERO),
+                );
+
+            let available_width = if can_scroll_horizontally {
+                bounds.size.width + scroll_offset.x.abs()
+            } else {
+                bounds.size.width
+            };
+
+            let available_space = size(
+                AvailableSpace::Definite(available_width),
+                AvailableSpace::Definite(item_height),
+            );
+
+            item.layout_as_root(available_space, window, cx);
+            item.prepaint_at(sticky_origin, window, cx);
+        }
+    }
+
+    fn paint(&self, items: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App) {
+        // reverse so that last item is bottom most among sticky items
+        for item in items.iter_mut().rev() {
+            item.paint(window, cx);
+        }
+    }
+}

crates/util/src/shell_env.rs ๐Ÿ”—

@@ -16,8 +16,13 @@ pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<Strin
     let mut command_string = String::new();
     let mut command = std::process::Command::new(&shell_path);
     // In some shells, file descriptors greater than 2 cannot be used in interactive mode,
-    // so file descriptor 0 (stdin) is used instead. [Citation Needed]
+    // so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others.
+    // See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482
     const ENV_OUTPUT_FD: std::os::fd::RawFd = 0;
+    let redir = match shell_name {
+        Some("rc") => format!(">[1={}]", ENV_OUTPUT_FD), // `[1=0]`
+        _ => format!(">&{}", ENV_OUTPUT_FD),             // `>&0`
+    };
     command.stdin(Stdio::null());
     command.stdout(Stdio::piped());
     command.stderr(Stdio::piped());
@@ -38,10 +43,7 @@ pub fn capture(directory: &std::path::Path) -> Result<collections::HashMap<Strin
     }
     // cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
     command_string.push_str(&format!("cd '{}';", directory.display()));
-    command_string.push_str(&format!(
-        "sh -c \"{} --printenv >&{}\";",
-        zed_path, ENV_OUTPUT_FD
-    ));
+    command_string.push_str(&format!("{} --printenv {}", zed_path, redir));
     command.args(["-i", "-c", &command_string]);
 
     super::set_pre_exec_to_start_new_session(&mut command);

crates/util/src/util.rs ๐Ÿ”—

@@ -1097,52 +1097,6 @@ mod tests {
         assert_eq!(vec, &[1000, 101, 21, 19, 17, 13, 9, 8]);
     }
 
-    #[test]
-    fn test_get_shell_safe_zed_path_with_spaces() {
-        // Test that shlex::try_quote handles paths with spaces correctly
-        let path_with_spaces = "/Applications/Zed Nightly.app/Contents/MacOS/zed";
-        let quoted = shlex::try_quote(path_with_spaces).unwrap();
-
-        // The quoted path should be properly escaped for shell use
-        assert!(quoted.contains(path_with_spaces));
-
-        // When used in a shell command, it should not be split at spaces
-        let command = format!("sh -c '{} --printenv'", quoted);
-        println!("Command would be: {}", command);
-
-        // Test that shlex can parse it back correctly
-        let parsed = shlex::split(&format!("{} --printenv", quoted)).unwrap();
-        assert_eq!(parsed.len(), 2);
-        assert_eq!(parsed[0], path_with_spaces);
-        assert_eq!(parsed[1], "--printenv");
-    }
-
-    #[test]
-    fn test_shell_command_construction_with_quoted_path() {
-        // Test the specific pattern used in shell_env.rs to ensure proper quoting
-        let path_with_spaces = "/Applications/Zed Nightly.app/Contents/MacOS/zed";
-        let quoted_path = shlex::try_quote(path_with_spaces).unwrap();
-
-        // This should be: '/Applications/Zed Nightly.app/Contents/MacOS/zed'
-        assert_eq!(
-            quoted_path,
-            "'/Applications/Zed Nightly.app/Contents/MacOS/zed'"
-        );
-
-        // Test the command construction pattern from shell_env.rs
-        // The fixed version should use double quotes around the entire sh -c argument
-        let env_fd = 0;
-        let command = format!("sh -c \"{} --printenv >&{}\";", quoted_path, env_fd);
-
-        // This should produce: sh -c "'/Applications/Zed Nightly.app/Contents/MacOS/zed' --printenv >&0";
-        let expected =
-            "sh -c \"'/Applications/Zed Nightly.app/Contents/MacOS/zed' --printenv >&0\";";
-        assert_eq!(command, expected);
-
-        // The command should not contain the problematic double single-quote pattern
-        assert!(!command.contains("''"));
-    }
-
     #[test]
     fn test_truncate_to_bottom_n_sorted_by() {
         let mut vec: Vec<u32> = vec![5, 2, 3, 4, 1];

crates/vim/src/command.rs ๐Ÿ”—

@@ -1688,7 +1688,7 @@ impl ShellExec {
                     id: TaskId("vim".to_string()),
                     full_label: command.clone(),
                     label: command.clone(),
-                    command: command.clone(),
+                    command: Some(command.clone()),
                     args: Vec::new(),
                     command_label: command.clone(),
                     cwd,

crates/vim/src/normal/convert.rs ๐Ÿ”—

@@ -212,7 +212,19 @@ impl Vim {
                         }
                     }
 
-                    Mode::HelixNormal => {}
+                    Mode::HelixNormal => {
+                        if selection.is_empty() {
+                            // Handle empty selection by operating on the whole word
+                            let (word_range, _) = snapshot.surrounding_word(selection.start, false);
+                            let word_start = snapshot.offset_to_point(word_range.start);
+                            let word_end = snapshot.offset_to_point(word_range.end);
+                            ranges.push(word_start..word_end);
+                            cursor_positions.push(selection.start..selection.start);
+                        } else {
+                            ranges.push(selection.start..selection.end);
+                            cursor_positions.push(selection.start..selection.end);
+                        }
+                    }
                     Mode::Insert | Mode::Normal | Mode::Replace => {
                         let start = selection.start;
                         let mut end = start;
@@ -245,12 +257,16 @@ impl Vim {
                 })
             });
         });
-        self.switch_mode(Mode::Normal, true, window, cx)
+        if self.mode != Mode::HelixNormal {
+            self.switch_mode(Mode::Normal, true, window, cx)
+        }
     }
 }
 
 #[cfg(test)]
 mod test {
+    use crate::test::VimTestContext;
+
     use crate::{state::Mode, test::NeovimBackedTestContext};
 
     #[gpui::test]
@@ -419,4 +435,25 @@ mod test {
             .await
             .assert_eq("ห‡nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM");
     }
+
+    #[gpui::test]
+    async fn test_change_case_helix_mode(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        // Explicit selection
+        cx.set_state("ยซhello worldห‡ยป", Mode::HelixNormal);
+        cx.simulate_keystrokes("~");
+        cx.assert_state("ยซHELLO WORLDห‡ยป", Mode::HelixNormal);
+
+        // Cursor-only (empty) selection
+        cx.set_state("The ห‡quick brown", Mode::HelixNormal);
+        cx.simulate_keystrokes("~");
+        cx.assert_state("The ห‡QUICK brown", Mode::HelixNormal);
+
+        // With `e` motion (which extends selection to end of word in Helix)
+        cx.set_state("The ห‡quick brown fox", Mode::HelixNormal);
+        cx.simulate_keystrokes("e");
+        cx.simulate_keystrokes("~");
+        cx.assert_state("The ยซQUICKห‡ยป brown fox", Mode::HelixNormal);
+    }
 }

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

@@ -727,11 +727,10 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
 
     if let Some(connection_options) = request.ssh_connection {
         cx.spawn(async move |mut cx| {
-            let paths_with_position =
-                derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await;
+            let paths: Vec<PathBuf> = request.open_paths.into_iter().map(PathBuf::from).collect();
             open_ssh_project(
                 connection_options,
-                paths_with_position.into_iter().map(|p| p.path).collect(),
+                paths,
                 app_state,
                 workspace::OpenOptions::default(),
                 &mut cx,

script/generate-licenses ๐Ÿ”—

@@ -6,6 +6,15 @@ CARGO_ABOUT_VERSION="0.7"
 OUTPUT_FILE="${1:-$(pwd)/assets/licenses.md}"
 TEMPLATE_FILE="script/licenses/template.md.hbs"
 
+fail_on_stderr() {
+    local tmpfile=$(mktemp)
+    "$@" 2> >(tee "$tmpfile" >&2)
+    local rc=$?
+    [ -s "$tmpfile" ] && rc=1
+    rm "$tmpfile"
+    return $rc
+}
+
 echo -n "" >"$OUTPUT_FILE"
 
 {
@@ -28,7 +37,7 @@ fi
 echo "Generating cargo licenses"
 if [ -z "${ALLOW_MISSING_LICENSES-}" ]; then FAIL_FLAG=--fail; else FAIL_FLAG=""; fi
 set -x
-cargo about generate \
+fail_on_stderr cargo about generate \
     $FAIL_FLAG \
     -c script/licenses/zed-licenses.toml \
     "$TEMPLATE_FILE" >>"$OUTPUT_FILE"

script/licenses/zed-licenses.toml ๐Ÿ”—

@@ -177,9 +177,3 @@ license = "MIT"
 [[pet-windows-store.clarify.files]]
 path = '../../LICENSE'
 checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'
-
-[ring.clarify]
-license = "ISC AND OpenSSL"
-[[ring.clarify.files]]
-path = 'LICENSE'
-checksum = '76b39f9b371688eac9d8323f96ee80b3aef5ecbc2217f25377bd4e4a615296a9'