Merge remote-tracking branch 'origin/settings2' into mic-denoising

David Kleingeld created

Change summary

Cargo.lock                                                                      |   6 
assets/keymaps/default-linux.json                                               |   7 
assets/keymaps/default-macos.json                                               |   7 
assets/keymaps/default-windows.json                                             |   7 
crates/agent_settings/src/agent_settings.rs                                     |   1 
crates/agent_ui/src/agent_configuration.rs                                      | 346 
crates/agent_ui/src/agent_configuration/configure_context_server_tools_modal.rs | 176 
crates/dap/src/client.rs                                                        |   3 
crates/dap/src/transport.rs                                                     |   1 
crates/edit_prediction_context/Cargo.toml                                       |   7 
crates/edit_prediction_context/src/declaration.rs                               | 193 
crates/edit_prediction_context/src/declaration_scoring.rs                       | 326 
crates/edit_prediction_context/src/edit_prediction_context.rs                   | 216 
crates/edit_prediction_context/src/excerpt.rs                                   |   2 
crates/edit_prediction_context/src/outline.rs                                   |  12 
crates/edit_prediction_context/src/reference.rs                                 |   2 
crates/edit_prediction_context/src/syntax_index.rs                              | 638 
crates/edit_prediction_context/src/text_similarity.rs                           | 241 
crates/edit_prediction_context/src/wip_requests.rs                              |  35 
crates/language_models/src/provider/vercel.rs                                   |   1 
crates/project/src/context_server_store.rs                                      |   9 
crates/project/src/project_settings.rs                                          |   5 
crates/recent_projects/src/remote_servers.rs                                    |   2 
crates/remote/src/transport/wsl.rs                                              |   4 
crates/settings/src/settings_store.rs                                           |   1 
crates/terminal/src/terminal_settings.rs                                        |   1 
crates/ui/src/components/divider.rs                                             |   2 
docs/src/SUMMARY.md                                                             |   1 
docs/src/configuring-languages.md                                               |   6 
docs/src/languages/python.md                                                    | 285 
docs/src/toolchains.md                                                          |  28 
31 files changed, 1,998 insertions(+), 573 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -5174,17 +5174,23 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "arrayvec",
+ "clap",
  "collections",
  "futures 0.3.31",
  "gpui",
  "indoc",
+ "itertools 0.14.0",
  "language",
  "log",
+ "ordered-float 2.10.1",
  "pretty_assertions",
  "project",
+ "regex",
+ "serde",
  "serde_json",
  "settings",
  "slotmap",
+ "strum 0.27.1",
  "text",
  "tree-sitter",
  "util",

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

@@ -1140,6 +1140,13 @@
       "ctrl-enter": "menu::Confirm"
     }
   },
+  {
+    "context": "ContextServerToolsModal",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "menu::Cancel"
+    }
+  },
   {
     "context": "OnboardingAiConfigurationModal",
     "use_key_equivalents": true,

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

@@ -1244,6 +1244,13 @@
       "cmd-enter": "menu::Confirm"
     }
   },
+  {
+    "context": "ContextServerToolsModal",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "menu::Cancel"
+    }
+  },
   {
     "context": "OnboardingAiConfigurationModal",
     "use_key_equivalents": true,

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

@@ -1160,6 +1160,13 @@
       "ctrl-enter": "menu::Confirm"
     }
   },
+  {
+    "context": "ContextServerToolsModal",
+    "use_key_equivalents": true,
+    "bindings": {
+      "escape": "menu::Cancel"
+    }
+  },
   {
     "context": "OnboardingAiConfigurationModal",
     "use_key_equivalents": true,

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

@@ -147,7 +147,6 @@ impl Default for AgentProfileId {
 }
 
 impl Settings for AgentSettings {
-    // todo!() test preserved keys logic
     fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         let agent = content.agent.clone().unwrap();
         Self {

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

@@ -1,5 +1,6 @@
 mod add_llm_provider_modal;
 mod configure_context_server_modal;
+mod configure_context_server_tools_modal;
 mod manage_profiles_modal;
 mod tool_picker;
 
@@ -25,9 +26,7 @@ use language_model::{
 };
 use notifications::status_toast::{StatusToast, ToastIcon};
 use project::{
-    agent_server_store::{
-        AgentServerStore, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME,
-    },
+    agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, GEMINI_NAME},
     context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
 };
 use settings::{Settings, SettingsStore, update_settings_file};
@@ -40,12 +39,12 @@ use workspace::{Workspace, create_and_open_local_file};
 use zed_actions::ExtensionCategoryFilter;
 
 pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
+pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
 pub(crate) use manage_profiles_modal::ManageProfilesModal;
 
 use crate::{
-    AddContextServer, ExternalAgent, NewExternalAgentThread,
+    AddContextServer,
     agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
-    placeholder_command,
 };
 
 pub struct AgentConfiguration {
@@ -198,9 +197,8 @@ impl AgentConfiguration {
             .when(is_expanded, |this| this.mb_2())
             .child(
                 div()
-                    .opacity(0.6)
                     .px_2()
-                    .child(Divider::horizontal().color(DividerColor::Border)),
+                    .child(Divider::horizontal().color(DividerColor::BorderFaded)),
             )
             .child(
                 h_flex()
@@ -225,7 +223,7 @@ impl AgentConfiguration {
                             .child(
                                 h_flex()
                                     .w_full()
-                                    .gap_2()
+                                    .gap_1p5()
                                     .child(
                                         Icon::new(provider.icon())
                                             .size(IconSize::Small)
@@ -343,6 +341,8 @@ impl AgentConfiguration {
                                         PopoverMenu::new("add-provider-popover")
                                             .trigger(
                                                 Button::new("add-provider", "Add Provider")
+                                                    .style(ButtonStyle::Filled)
+                                                    .layer(ElevationIndex::ModalSurface)
                                                     .icon_position(IconPosition::Start)
                                                     .icon(IconName::Plus)
                                                     .icon_size(IconSize::Small)
@@ -534,10 +534,6 @@ impl AgentConfiguration {
         }
     }
 
-    fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
-        cx.theme().colors().background.opacity(0.25)
-    }
-
     fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
         cx.theme().colors().border.opacity(0.6)
     }
@@ -547,7 +543,73 @@ impl AgentConfiguration {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
-        let context_server_ids = self.context_server_store.read(cx).configured_server_ids();
+        let mut registry_descriptors = self
+            .context_server_store
+            .read(cx)
+            .all_registry_descriptor_ids(cx);
+        let server_count = registry_descriptors.len();
+
+        // Sort context servers: non-mcp-server ones first, then mcp-server ones
+        registry_descriptors.sort_by(|a, b| {
+            let has_mcp_prefix_a = a.0.starts_with("mcp-server-");
+            let has_mcp_prefix_b = b.0.starts_with("mcp-server-");
+
+            match (has_mcp_prefix_a, has_mcp_prefix_b) {
+                // If one has mcp-server- prefix and other doesn't, non-mcp comes first
+                (true, false) => std::cmp::Ordering::Greater,
+                (false, true) => std::cmp::Ordering::Less,
+                // If both have same prefix status, sort by appropriate key
+                _ => {
+                    let get_sort_key = |server_id: &str| -> String {
+                        if let Some(suffix) = server_id.strip_prefix("mcp-server-") {
+                            suffix.to_string()
+                        } else {
+                            server_id.to_string()
+                        }
+                    };
+
+                    let key_a = get_sort_key(&a.0);
+                    let key_b = get_sort_key(&b.0);
+                    key_a.cmp(&key_b)
+                }
+            }
+        });
+
+        let add_server_popover = PopoverMenu::new("add-server-popover")
+            .trigger(
+                Button::new("add-server", "Add Server")
+                    .style(ButtonStyle::Filled)
+                    .layer(ElevationIndex::ModalSurface)
+                    .icon_position(IconPosition::Start)
+                    .icon(IconName::Plus)
+                    .icon_size(IconSize::Small)
+                    .icon_color(Color::Muted)
+                    .label_size(LabelSize::Small),
+            )
+            .anchor(gpui::Corner::TopRight)
+            .menu({
+                move |window, cx| {
+                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
+                        menu.entry("Add Custom Server", None, {
+                            |window, cx| window.dispatch_action(AddContextServer.boxed_clone(), cx)
+                        })
+                        .entry("Install from Extensions", None, {
+                            |window, cx| {
+                                window.dispatch_action(
+                                    zed_actions::Extensions {
+                                        category_filter: Some(
+                                            ExtensionCategoryFilter::ContextServers,
+                                        ),
+                                        id: None,
+                                    }
+                                    .boxed_clone(),
+                                    cx,
+                                )
+                            }
+                        })
+                    }))
+                }
+            });
 
         v_flex()
             .p(DynamicSpacing::Base16.rems(cx))
@@ -556,18 +618,26 @@ impl AgentConfiguration {
             .border_b_1()
             .border_color(cx.theme().colors().border)
             .child(
-                v_flex()
-                    .gap_0p5()
-                    .child(Headline::new("Model Context Protocol (MCP) Servers"))
+                h_flex()
+                    .w_full()
+                    .items_start()
+                    .justify_between()
+                    .gap_1()
                     .child(
-                        Label::new(
-                            "All context servers connected through the Model Context Protocol.",
-                        )
-                        .color(Color::Muted),
-                    ),
+                        v_flex()
+                            .gap_0p5()
+                            .child(Headline::new("Model Context Protocol (MCP) Servers"))
+                            .child(
+                                Label::new(
+                                    "All MCP servers connected directly or via a Zed extension.",
+                                )
+                                .color(Color::Muted),
+                            ),
+                    )
+                    .child(add_server_popover),
             )
-            .map(|parent| {
-                if context_server_ids.is_empty() {
+            .child(v_flex().w_full().gap_1().map(|parent| {
+                if registry_descriptors.is_empty() {
                     parent.child(
                         h_flex()
                             .p_4()
@@ -583,56 +653,28 @@ impl AgentConfiguration {
                             ),
                     )
                 } else {
-                    parent.children(context_server_ids.into_iter().map(|context_server_id| {
-                        self.render_context_server(context_server_id, window, cx)
-                    }))
+                    {
+                        parent.children(registry_descriptors.into_iter().enumerate().flat_map(
+                            |(index, context_server_id)| {
+                                let mut elements: Vec<AnyElement> = vec![
+                                    self.render_context_server(context_server_id, window, cx)
+                                        .into_any_element(),
+                                ];
+
+                                if index < server_count - 1 {
+                                    elements.push(
+                                        Divider::horizontal()
+                                            .color(DividerColor::BorderFaded)
+                                            .into_any_element(),
+                                    );
+                                }
+
+                                elements
+                            },
+                        ))
+                    }
                 }
-            })
-            .child(
-                h_flex()
-                    .justify_between()
-                    .gap_1p5()
-                    .child(
-                        h_flex().w_full().child(
-                            Button::new("add-context-server", "Add Custom Server")
-                                .style(ButtonStyle::Filled)
-                                .layer(ElevationIndex::ModalSurface)
-                                .full_width()
-                                .icon(IconName::Plus)
-                                .icon_size(IconSize::Small)
-                                .icon_position(IconPosition::Start)
-                                .on_click(|_event, window, cx| {
-                                    window.dispatch_action(AddContextServer.boxed_clone(), cx)
-                                }),
-                        ),
-                    )
-                    .child(
-                        h_flex().w_full().child(
-                            Button::new(
-                                "install-context-server-extensions",
-                                "Install MCP Extensions",
-                            )
-                            .style(ButtonStyle::Filled)
-                            .layer(ElevationIndex::ModalSurface)
-                            .full_width()
-                            .icon(IconName::ToolHammer)
-                            .icon_size(IconSize::Small)
-                            .icon_position(IconPosition::Start)
-                            .on_click(|_event, window, cx| {
-                                window.dispatch_action(
-                                    zed_actions::Extensions {
-                                        category_filter: Some(
-                                            ExtensionCategoryFilter::ContextServers,
-                                        ),
-                                        id: None,
-                                    }
-                                    .boxed_clone(),
-                                    cx,
-                                )
-                            }),
-                        ),
-                    ),
-            )
+            }))
     }
 
     fn render_context_server(
@@ -725,7 +767,7 @@ impl AgentConfiguration {
                 IconButton::new("context-server-config-menu", IconName::Settings)
                     .icon_color(Color::Muted)
                     .icon_size(IconSize::Small),
-                Tooltip::text("Open MCP server options"),
+                Tooltip::text("Configure MCP Server"),
             )
             .anchor(Corner::TopRight)
             .menu({
@@ -734,6 +776,8 @@ impl AgentConfiguration {
                 let language_registry = self.language_registry.clone();
                 let context_server_store = self.context_server_store.clone();
                 let workspace = self.workspace.clone();
+                let tools = self.tools.clone();
+
                 move |window, cx| {
                     Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
                         menu.entry("Configure Server", None, {
@@ -750,7 +794,28 @@ impl AgentConfiguration {
                                 )
                                 .detach_and_log_err(cx);
                             }
-                        })
+                        }).when(tool_count >= 1, |this| this.entry("View Tools", None, {
+                            let context_server_id = context_server_id.clone();
+                            let tools = tools.clone();
+                            let workspace = workspace.clone();
+
+                            move |window, cx| {
+                                let context_server_id = context_server_id.clone();
+                                let tools = tools.clone();
+                                let workspace = workspace.clone();
+
+                                workspace.update(cx, |workspace, cx| {
+                                    ConfigureContextServerToolsModal::toggle(
+                                        context_server_id,
+                                        tools,
+                                        workspace,
+                                        window,
+                                        cx,
+                                    );
+                                })
+                                .ok();
+                            }
+                        }))
                         .separator()
                         .entry("Uninstall", None, {
                             let fs = fs.clone();
@@ -821,17 +886,11 @@ impl AgentConfiguration {
 
         v_flex()
             .id(item_id.clone())
-            .border_1()
-            .rounded_md()
-            .border_color(self.card_item_border_color(cx))
-            .bg(self.card_item_bg_color(cx))
-            .overflow_hidden()
             .child(
                 h_flex()
-                    .p_1()
                     .justify_between()
                     .when(
-                        error.is_some() || are_tools_expanded && tool_count >= 1,
+                        error.is_none() && are_tools_expanded && tool_count >= 1,
                         |element| {
                             element
                                 .border_b_1()
@@ -842,31 +901,12 @@ impl AgentConfiguration {
                         h_flex()
                             .flex_1()
                             .min_w_0()
-                            .child(
-                                Disclosure::new(
-                                    "tool-list-disclosure",
-                                    are_tools_expanded || error.is_some(),
-                                )
-                                .disabled(tool_count == 0)
-                                .on_click(cx.listener({
-                                    let context_server_id = context_server_id.clone();
-                                    move |this, _event, _window, _cx| {
-                                        let is_open = this
-                                            .expanded_context_server_tools
-                                            .entry(context_server_id.clone())
-                                            .or_insert(false);
-
-                                        *is_open = !*is_open;
-                                    }
-                                })),
-                            )
                             .child(
                                 h_flex()
                                     .id(SharedString::from(format!("tooltip-{}", item_id)))
                                     .h_full()
                                     .w_3()
-                                    .ml_1()
-                                    .mr_1p5()
+                                    .mr_2()
                                     .justify_center()
                                     .tooltip(Tooltip::text(tooltip_text))
                                     .child(status_indicator),
@@ -956,8 +996,8 @@ impl AgentConfiguration {
                 if let Some(error) = error {
                     return parent.child(
                         h_flex()
-                            .p_2()
                             .gap_2()
+                            .pr_4()
                             .items_start()
                             .child(
                                 h_flex()
@@ -985,37 +1025,11 @@ impl AgentConfiguration {
                     return parent;
                 }
 
-                parent.child(v_flex().py_1p5().px_1().gap_1().children(
-                    tools.iter().enumerate().map(|(ix, tool)| {
-                        h_flex()
-                            .id(("tool-item", ix))
-                            .px_1()
-                            .gap_2()
-                            .justify_between()
-                            .hover(|style| style.bg(cx.theme().colors().element_hover))
-                            .rounded_sm()
-                            .child(
-                                Label::new(tool.name())
-                                    .buffer_font(cx)
-                                    .size(LabelSize::Small),
-                            )
-                            .child(
-                                Icon::new(IconName::Info)
-                                    .size(IconSize::Small)
-                                    .color(Color::Ignored),
-                            )
-                            .tooltip(Tooltip::text(tool.description()))
-                    }),
-                ))
+                parent
             })
     }
 
     fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
-        let custom_settings = cx
-            .global::<SettingsStore>()
-            .get::<AllAgentServersSettings>(None)
-            .custom
-            .clone();
         let user_defined_agents = self
             .agent_server_store
             .read(cx)
@@ -1023,22 +1037,12 @@ impl AgentConfiguration {
             .filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME)
             .cloned()
             .collect::<Vec<_>>();
+
         let user_defined_agents = user_defined_agents
             .into_iter()
             .map(|name| {
-                self.render_agent_server(
-                    IconName::Ai,
-                    name.clone(),
-                    ExternalAgent::Custom {
-                        name: name.clone().into(),
-                        command: custom_settings
-                            .get(&name.0)
-                            .map(|settings| settings.command.clone())
-                            .unwrap_or(placeholder_command()),
-                    },
-                    cx,
-                )
-                .into_any_element()
+                self.render_agent_server(IconName::Ai, name)
+                    .into_any_element()
             })
             .collect::<Vec<_>>();
 
@@ -1062,6 +1066,8 @@ impl AgentConfiguration {
                                     .child(Headline::new("External Agents"))
                                     .child(
                                         Button::new("add-agent", "Add Agent")
+                                            .style(ButtonStyle::Filled)
+                                            .layer(ElevationIndex::ModalSurface)
                                             .icon_position(IconPosition::Start)
                                             .icon(IconName::Plus)
                                             .icon_size(IconSize::Small)
@@ -1094,14 +1100,11 @@ impl AgentConfiguration {
                     .child(self.render_agent_server(
                         IconName::AiGemini,
                         "Gemini CLI",
-                        ExternalAgent::Gemini,
-                        cx,
                     ))
+                    .child(Divider::horizontal().color(DividerColor::BorderFaded))
                     .child(self.render_agent_server(
                         IconName::AiClaude,
                         "Claude Code",
-                        ExternalAgent::ClaudeCode,
-                        cx,
                     ))
                     .children(user_defined_agents),
             )
@@ -1111,47 +1114,18 @@ impl AgentConfiguration {
         &self,
         icon: IconName,
         name: impl Into<SharedString>,
-        agent: ExternalAgent,
-        cx: &mut Context<Self>,
     ) -> impl IntoElement {
-        let name = name.into();
-        h_flex()
-            .p_1()
-            .pl_2()
-            .gap_1p5()
-            .justify_between()
-            .border_1()
-            .rounded_md()
-            .border_color(self.card_item_border_color(cx))
-            .bg(self.card_item_bg_color(cx))
-            .overflow_hidden()
-            .child(
-                h_flex()
-                    .gap_1p5()
-                    .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
-                    .child(Label::new(name.clone())),
-            )
-            .child(
-                Button::new(
-                    SharedString::from(format!("start_acp_thread-{name}")),
-                    "Start New Thread",
-                )
-                .layer(ElevationIndex::ModalSurface)
-                .label_size(LabelSize::Small)
-                .icon(IconName::Thread)
-                .icon_position(IconPosition::Start)
-                .icon_size(IconSize::XSmall)
-                .icon_color(Color::Muted)
-                .on_click(move |_, window, cx| {
-                    window.dispatch_action(
-                        NewExternalAgentThread {
-                            agent: Some(agent.clone()),
-                        }
-                        .boxed_clone(),
-                        cx,
-                    );
-                }),
-            )
+        h_flex().gap_1p5().justify_between().child(
+            h_flex()
+                .gap_1p5()
+                .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
+                .child(Label::new(name.into()))
+                .child(
+                    Icon::new(IconName::Check)
+                        .size(IconSize::Small)
+                        .color(Color::Success),
+                ),
+        )
     }
 }
 

crates/agent_ui/src/agent_configuration/configure_context_server_tools_modal.rs πŸ”—

@@ -0,0 +1,176 @@
+use assistant_tool::{ToolSource, ToolWorkingSet};
+use context_server::ContextServerId;
+use gpui::{
+    DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Window, prelude::*,
+};
+use ui::{Divider, DividerColor, Modal, ModalHeader, WithScrollbar, prelude::*};
+use workspace::{ModalView, Workspace};
+
+pub struct ConfigureContextServerToolsModal {
+    context_server_id: ContextServerId,
+    tools: Entity<ToolWorkingSet>,
+    focus_handle: FocusHandle,
+    expanded_tools: std::collections::HashMap<String, bool>,
+    scroll_handle: ScrollHandle,
+}
+
+impl ConfigureContextServerToolsModal {
+    fn new(
+        context_server_id: ContextServerId,
+        tools: Entity<ToolWorkingSet>,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        Self {
+            context_server_id,
+            tools,
+            focus_handle: cx.focus_handle(),
+            expanded_tools: std::collections::HashMap::new(),
+            scroll_handle: ScrollHandle::new(),
+        }
+    }
+
+    pub fn toggle(
+        context_server_id: ContextServerId,
+        tools: Entity<ToolWorkingSet>,
+        workspace: &mut Workspace,
+        window: &mut Window,
+        cx: &mut Context<Workspace>,
+    ) {
+        workspace.toggle_modal(window, cx, |window, cx| {
+            Self::new(context_server_id, tools, window, cx)
+        });
+    }
+
+    fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+        cx.emit(DismissEvent)
+    }
+
+    fn render_modal_content(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
+        let tools_by_source = self.tools.read(cx).tools_by_source(cx);
+        let server_tools = tools_by_source
+            .get(&ToolSource::ContextServer {
+                id: self.context_server_id.0.clone().into(),
+            })
+            .map(|tools| tools.as_slice())
+            .unwrap_or(&[]);
+
+        div()
+            .size_full()
+            .pb_2()
+            .child(
+                v_flex()
+                    .id("modal_content")
+                    .px_2()
+                    .gap_1()
+                    .max_h_128()
+                    .overflow_y_scroll()
+                    .track_scroll(&self.scroll_handle)
+                    .children(server_tools.iter().enumerate().flat_map(|(index, tool)| {
+                        let tool_name = tool.name();
+                        let is_expanded = self
+                            .expanded_tools
+                            .get(&tool_name)
+                            .copied()
+                            .unwrap_or(false);
+
+                        let icon = if is_expanded {
+                            IconName::ChevronUp
+                        } else {
+                            IconName::ChevronDown
+                        };
+
+                        let mut items = vec![
+                            v_flex()
+                                .child(
+                                    h_flex()
+                                        .id(SharedString::from(format!("tool-header-{}", index)))
+                                        .py_1()
+                                        .pl_1()
+                                        .pr_2()
+                                        .w_full()
+                                        .justify_between()
+                                        .rounded_sm()
+                                        .hover(|s| s.bg(cx.theme().colors().element_hover))
+                                        .child(
+                                            Label::new(tool_name.clone())
+                                                .buffer_font(cx)
+                                                .size(LabelSize::Small),
+                                        )
+                                        .child(
+                                            Icon::new(icon)
+                                                .size(IconSize::Small)
+                                                .color(Color::Muted),
+                                        )
+                                        .on_click(cx.listener({
+                                            move |this, _event, _window, _cx| {
+                                                let current = this
+                                                    .expanded_tools
+                                                    .get(&tool_name)
+                                                    .copied()
+                                                    .unwrap_or(false);
+                                                this.expanded_tools
+                                                    .insert(tool_name.clone(), !current);
+                                                _cx.notify();
+                                            }
+                                        })),
+                                )
+                                .when(is_expanded, |this| {
+                                    this.child(
+                                        Label::new(tool.description()).color(Color::Muted).mx_1(),
+                                    )
+                                })
+                                .into_any_element(),
+                        ];
+
+                        if index < server_tools.len() - 1 {
+                            items.push(
+                                h_flex()
+                                    .w_full()
+                                    .child(Divider::horizontal().color(DividerColor::BorderVariant))
+                                    .into_any_element(),
+                            );
+                        }
+
+                        items
+                    })),
+            )
+            .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
+            .into_any_element()
+    }
+}
+
+impl ModalView for ConfigureContextServerToolsModal {}
+
+impl Focusable for ConfigureContextServerToolsModal {
+    fn focus_handle(&self, _cx: &App) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl EventEmitter<DismissEvent> for ConfigureContextServerToolsModal {}
+
+impl Render for ConfigureContextServerToolsModal {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        div()
+            .key_context("ContextServerToolsModal")
+            .occlude()
+            .elevation_3(cx)
+            .w(rems(34.))
+            .on_action(cx.listener(Self::cancel))
+            .track_focus(&self.focus_handle)
+            .child(
+                Modal::new("configure-context-server-tools", None::<ScrollHandle>)
+                    .header(
+                        ModalHeader::new()
+                            .headline(format!("Tools from {}", self.context_server_id.0))
+                            .show_dismiss_button(true),
+                    )
+                    .child(self.render_modal_content(window, cx)),
+            )
+    }
+}

crates/dap/src/client.rs πŸ”—

@@ -118,6 +118,7 @@ impl DebugAdapterClient {
             R::COMMAND,
             sequence_id
         );
+        log::debug!("  request: {request:?}");
 
         self.send_message(Message::Request(request)).await?;
 
@@ -130,6 +131,8 @@ impl DebugAdapterClient {
             command,
             sequence_id
         );
+        log::debug!("  response: {response:?}");
+
         match response.success {
             true => {
                 if let Some(json) = response.body {

crates/dap/src/transport.rs πŸ”—

@@ -262,6 +262,7 @@ impl TransportDelegate {
                     break;
                 }
             }
+            log::debug!("stderr: {line}");
 
             for (kind, handler) in log_handlers.lock().iter_mut() {
                 if matches!(kind, LogKind::Adapter) {

crates/edit_prediction_context/Cargo.toml πŸ”—

@@ -15,17 +15,24 @@ path = "src/edit_prediction_context.rs"
 anyhow.workspace = true
 arrayvec.workspace = true
 collections.workspace = true
+futures.workspace = true
 gpui.workspace = true
+itertools.workspace = true
 language.workspace = true
 log.workspace = true
+ordered-float.workspace = true
 project.workspace = true
+regex.workspace = true
+serde.workspace = true
 slotmap.workspace = true
+strum.workspace = true
 text.workspace = true
 tree-sitter.workspace = true
 util.workspace = true
 workspace-hack.workspace = true
 
 [dev-dependencies]
+clap.workspace = true
 futures.workspace = true
 gpui = { workspace = true, features = ["test-support"] }
 indoc.workspace = true

crates/edit_prediction_context/src/declaration.rs πŸ”—

@@ -0,0 +1,193 @@
+use language::LanguageId;
+use project::ProjectEntryId;
+use std::borrow::Cow;
+use std::ops::Range;
+use std::sync::Arc;
+use text::{Bias, BufferId, Rope};
+
+use crate::outline::OutlineDeclaration;
+
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct Identifier {
+    pub name: Arc<str>,
+    pub language_id: LanguageId,
+}
+
+slotmap::new_key_type! {
+    pub struct DeclarationId;
+}
+
+#[derive(Debug, Clone)]
+pub enum Declaration {
+    File {
+        project_entry_id: ProjectEntryId,
+        declaration: FileDeclaration,
+    },
+    Buffer {
+        project_entry_id: ProjectEntryId,
+        buffer_id: BufferId,
+        rope: Rope,
+        declaration: BufferDeclaration,
+    },
+}
+
+const ITEM_TEXT_TRUNCATION_LENGTH: usize = 1024;
+
+impl Declaration {
+    pub fn identifier(&self) -> &Identifier {
+        match self {
+            Declaration::File { declaration, .. } => &declaration.identifier,
+            Declaration::Buffer { declaration, .. } => &declaration.identifier,
+        }
+    }
+
+    pub fn project_entry_id(&self) -> Option<ProjectEntryId> {
+        match self {
+            Declaration::File {
+                project_entry_id, ..
+            } => Some(*project_entry_id),
+            Declaration::Buffer {
+                project_entry_id, ..
+            } => Some(*project_entry_id),
+        }
+    }
+
+    pub fn item_text(&self) -> (Cow<'_, str>, bool) {
+        match self {
+            Declaration::File { declaration, .. } => (
+                declaration.text.as_ref().into(),
+                declaration.text_is_truncated,
+            ),
+            Declaration::Buffer {
+                rope, declaration, ..
+            } => (
+                rope.chunks_in_range(declaration.item_range.clone())
+                    .collect::<Cow<str>>(),
+                declaration.item_range_is_truncated,
+            ),
+        }
+    }
+
+    pub fn signature_text(&self) -> (Cow<'_, str>, bool) {
+        match self {
+            Declaration::File { declaration, .. } => (
+                declaration.text[declaration.signature_range_in_text.clone()].into(),
+                declaration.signature_is_truncated,
+            ),
+            Declaration::Buffer {
+                rope, declaration, ..
+            } => (
+                rope.chunks_in_range(declaration.signature_range.clone())
+                    .collect::<Cow<str>>(),
+                declaration.signature_range_is_truncated,
+            ),
+        }
+    }
+}
+
+fn expand_range_to_line_boundaries_and_truncate(
+    range: &Range<usize>,
+    limit: usize,
+    rope: &Rope,
+) -> (Range<usize>, bool) {
+    let mut point_range = rope.offset_to_point(range.start)..rope.offset_to_point(range.end);
+    point_range.start.column = 0;
+    point_range.end.row += 1;
+    point_range.end.column = 0;
+
+    let mut item_range =
+        rope.point_to_offset(point_range.start)..rope.point_to_offset(point_range.end);
+    let is_truncated = item_range.len() > limit;
+    if is_truncated {
+        item_range.end = item_range.start + limit;
+    }
+    item_range.end = rope.clip_offset(item_range.end, Bias::Left);
+    (item_range, is_truncated)
+}
+
+#[derive(Debug, Clone)]
+pub struct FileDeclaration {
+    pub parent: Option<DeclarationId>,
+    pub identifier: Identifier,
+    /// offset range of the declaration in the file, expanded to line boundaries and truncated
+    pub item_range_in_file: Range<usize>,
+    /// text of `item_range_in_file`
+    pub text: Arc<str>,
+    /// whether `text` was truncated
+    pub text_is_truncated: bool,
+    /// offset range of the signature within `text`
+    pub signature_range_in_text: Range<usize>,
+    /// whether `signature` was truncated
+    pub signature_is_truncated: bool,
+}
+
+impl FileDeclaration {
+    pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> FileDeclaration {
+        let (item_range_in_file, text_is_truncated) = expand_range_to_line_boundaries_and_truncate(
+            &declaration.item_range,
+            ITEM_TEXT_TRUNCATION_LENGTH,
+            rope,
+        );
+
+        // TODO: consider logging if unexpected
+        let signature_start = declaration
+            .signature_range
+            .start
+            .saturating_sub(item_range_in_file.start);
+        let mut signature_end = declaration
+            .signature_range
+            .end
+            .saturating_sub(item_range_in_file.start);
+        let signature_is_truncated = signature_end > item_range_in_file.len();
+        if signature_is_truncated {
+            signature_end = item_range_in_file.len();
+        }
+
+        FileDeclaration {
+            parent: None,
+            identifier: declaration.identifier,
+            signature_range_in_text: signature_start..signature_end,
+            signature_is_truncated,
+            text: rope
+                .chunks_in_range(item_range_in_file.clone())
+                .collect::<String>()
+                .into(),
+            text_is_truncated,
+            item_range_in_file,
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct BufferDeclaration {
+    pub parent: Option<DeclarationId>,
+    pub identifier: Identifier,
+    pub item_range: Range<usize>,
+    pub item_range_is_truncated: bool,
+    pub signature_range: Range<usize>,
+    pub signature_range_is_truncated: bool,
+}
+
+impl BufferDeclaration {
+    pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> Self {
+        let (item_range, item_range_is_truncated) = expand_range_to_line_boundaries_and_truncate(
+            &declaration.item_range,
+            ITEM_TEXT_TRUNCATION_LENGTH,
+            rope,
+        );
+        let (signature_range, signature_range_is_truncated) =
+            expand_range_to_line_boundaries_and_truncate(
+                &declaration.signature_range,
+                ITEM_TEXT_TRUNCATION_LENGTH,
+                rope,
+            );
+        Self {
+            parent: None,
+            identifier: declaration.identifier,
+            item_range,
+            item_range_is_truncated,
+            signature_range,
+            signature_range_is_truncated,
+        }
+    }
+}

crates/edit_prediction_context/src/declaration_scoring.rs πŸ”—

@@ -0,0 +1,326 @@
+use itertools::Itertools as _;
+use language::BufferSnapshot;
+use ordered_float::OrderedFloat;
+use serde::Serialize;
+use std::{collections::HashMap, ops::Range};
+use strum::EnumIter;
+use text::{OffsetRangeExt, Point, ToPoint};
+
+use crate::{
+    Declaration, EditPredictionExcerpt, EditPredictionExcerptText, Identifier,
+    reference::{Reference, ReferenceRegion},
+    syntax_index::SyntaxIndexState,
+    text_similarity::{IdentifierOccurrences, jaccard_similarity, weighted_overlap_coefficient},
+};
+
+const MAX_IDENTIFIER_DECLARATION_COUNT: usize = 16;
+
+// TODO:
+//
+// * Consider adding declaration_file_count
+
+#[derive(Clone, Debug)]
+pub struct ScoredSnippet {
+    pub identifier: Identifier,
+    pub declaration: Declaration,
+    pub score_components: ScoreInputs,
+    pub scores: Scores,
+}
+
+// TODO: Consider having "Concise" style corresponding to `concise_text`
+#[derive(EnumIter, Clone, Copy, PartialEq, Eq, Hash, Debug)]
+pub enum SnippetStyle {
+    Signature,
+    Declaration,
+}
+
+impl ScoredSnippet {
+    /// Returns the score for this snippet with the specified style.
+    pub fn score(&self, style: SnippetStyle) -> f32 {
+        match style {
+            SnippetStyle::Signature => self.scores.signature,
+            SnippetStyle::Declaration => self.scores.declaration,
+        }
+    }
+
+    pub fn size(&self, style: SnippetStyle) -> usize {
+        // TODO: how to handle truncation?
+        match &self.declaration {
+            Declaration::File { declaration, .. } => match style {
+                SnippetStyle::Signature => declaration.signature_range_in_text.len(),
+                SnippetStyle::Declaration => declaration.text.len(),
+            },
+            Declaration::Buffer { declaration, .. } => match style {
+                SnippetStyle::Signature => declaration.signature_range.len(),
+                SnippetStyle::Declaration => declaration.item_range.len(),
+            },
+        }
+    }
+
+    pub fn score_density(&self, style: SnippetStyle) -> f32 {
+        self.score(style) / (self.size(style)) as f32
+    }
+}
+
+pub fn scored_snippets(
+    index: &SyntaxIndexState,
+    excerpt: &EditPredictionExcerpt,
+    excerpt_text: &EditPredictionExcerptText,
+    identifier_to_references: HashMap<Identifier, Vec<Reference>>,
+    cursor_offset: usize,
+    current_buffer: &BufferSnapshot,
+) -> Vec<ScoredSnippet> {
+    let containing_range_identifier_occurrences =
+        IdentifierOccurrences::within_string(&excerpt_text.body);
+    let cursor_point = cursor_offset.to_point(&current_buffer);
+
+    let start_point = Point::new(cursor_point.row.saturating_sub(2), 0);
+    let end_point = Point::new(cursor_point.row + 1, 0);
+    let adjacent_identifier_occurrences = IdentifierOccurrences::within_string(
+        &current_buffer
+            .text_for_range(start_point..end_point)
+            .collect::<String>(),
+    );
+
+    let mut snippets = identifier_to_references
+        .into_iter()
+        .flat_map(|(identifier, references)| {
+            let declarations =
+                index.declarations_for_identifier::<MAX_IDENTIFIER_DECLARATION_COUNT>(&identifier);
+            let declaration_count = declarations.len();
+
+            declarations
+                .iter()
+                .filter_map(|declaration| match declaration {
+                    Declaration::Buffer {
+                        buffer_id,
+                        declaration: buffer_declaration,
+                        ..
+                    } => {
+                        let is_same_file = buffer_id == &current_buffer.remote_id();
+
+                        if is_same_file {
+                            range_intersection(
+                                &buffer_declaration.item_range.to_offset(&current_buffer),
+                                &excerpt.range,
+                            )
+                            .is_none()
+                            .then(|| {
+                                let declaration_line = buffer_declaration
+                                    .item_range
+                                    .start
+                                    .to_point(current_buffer)
+                                    .row;
+                                (
+                                    true,
+                                    (cursor_point.row as i32 - declaration_line as i32)
+                                        .unsigned_abs(),
+                                    declaration,
+                                )
+                            })
+                        } else {
+                            // TODO should we prefer the current file instead?
+                            Some((false, 0, declaration))
+                        }
+                    }
+                    Declaration::File { .. } => {
+                        // TODO should we prefer the current file instead?
+                        // We can assume that a file declaration is in a different file,
+                        // because the current one must be open
+                        Some((false, 0, declaration))
+                    }
+                })
+                .sorted_by_key(|&(_, distance, _)| distance)
+                .enumerate()
+                .map(
+                    |(
+                        declaration_line_distance_rank,
+                        (is_same_file, declaration_line_distance, declaration),
+                    )| {
+                        let same_file_declaration_count = index.file_declaration_count(declaration);
+
+                        score_snippet(
+                            &identifier,
+                            &references,
+                            declaration.clone(),
+                            is_same_file,
+                            declaration_line_distance,
+                            declaration_line_distance_rank,
+                            same_file_declaration_count,
+                            declaration_count,
+                            &containing_range_identifier_occurrences,
+                            &adjacent_identifier_occurrences,
+                            cursor_point,
+                            current_buffer,
+                        )
+                    },
+                )
+                .collect::<Vec<_>>()
+        })
+        .flatten()
+        .collect::<Vec<_>>();
+
+    snippets.sort_unstable_by_key(|snippet| {
+        OrderedFloat(
+            snippet
+                .score_density(SnippetStyle::Declaration)
+                .max(snippet.score_density(SnippetStyle::Signature)),
+        )
+    });
+
+    snippets
+}
+
+fn range_intersection<T: Ord + Clone>(a: &Range<T>, b: &Range<T>) -> Option<Range<T>> {
+    let start = a.start.clone().max(b.start.clone());
+    let end = a.end.clone().min(b.end.clone());
+    if start < end {
+        Some(Range { start, end })
+    } else {
+        None
+    }
+}
+
+fn score_snippet(
+    identifier: &Identifier,
+    references: &[Reference],
+    declaration: Declaration,
+    is_same_file: bool,
+    declaration_line_distance: u32,
+    declaration_line_distance_rank: usize,
+    same_file_declaration_count: usize,
+    declaration_count: usize,
+    containing_range_identifier_occurrences: &IdentifierOccurrences,
+    adjacent_identifier_occurrences: &IdentifierOccurrences,
+    cursor: Point,
+    current_buffer: &BufferSnapshot,
+) -> Option<ScoredSnippet> {
+    let is_referenced_nearby = references
+        .iter()
+        .any(|r| r.region == ReferenceRegion::Nearby);
+    let is_referenced_in_breadcrumb = references
+        .iter()
+        .any(|r| r.region == ReferenceRegion::Breadcrumb);
+    let reference_count = references.len();
+    let reference_line_distance = references
+        .iter()
+        .map(|r| {
+            let reference_line = r.range.start.to_point(current_buffer).row as i32;
+            (cursor.row as i32 - reference_line).unsigned_abs()
+        })
+        .min()
+        .unwrap();
+
+    let item_source_occurrences = IdentifierOccurrences::within_string(&declaration.item_text().0);
+    let item_signature_occurrences =
+        IdentifierOccurrences::within_string(&declaration.signature_text().0);
+    let containing_range_vs_item_jaccard = jaccard_similarity(
+        containing_range_identifier_occurrences,
+        &item_source_occurrences,
+    );
+    let containing_range_vs_signature_jaccard = jaccard_similarity(
+        containing_range_identifier_occurrences,
+        &item_signature_occurrences,
+    );
+    let adjacent_vs_item_jaccard =
+        jaccard_similarity(adjacent_identifier_occurrences, &item_source_occurrences);
+    let adjacent_vs_signature_jaccard =
+        jaccard_similarity(adjacent_identifier_occurrences, &item_signature_occurrences);
+
+    let containing_range_vs_item_weighted_overlap = weighted_overlap_coefficient(
+        containing_range_identifier_occurrences,
+        &item_source_occurrences,
+    );
+    let containing_range_vs_signature_weighted_overlap = weighted_overlap_coefficient(
+        containing_range_identifier_occurrences,
+        &item_signature_occurrences,
+    );
+    let adjacent_vs_item_weighted_overlap =
+        weighted_overlap_coefficient(adjacent_identifier_occurrences, &item_source_occurrences);
+    let adjacent_vs_signature_weighted_overlap =
+        weighted_overlap_coefficient(adjacent_identifier_occurrences, &item_signature_occurrences);
+
+    let score_components = ScoreInputs {
+        is_same_file,
+        is_referenced_nearby,
+        is_referenced_in_breadcrumb,
+        reference_line_distance,
+        declaration_line_distance,
+        declaration_line_distance_rank,
+        reference_count,
+        same_file_declaration_count,
+        declaration_count,
+        containing_range_vs_item_jaccard,
+        containing_range_vs_signature_jaccard,
+        adjacent_vs_item_jaccard,
+        adjacent_vs_signature_jaccard,
+        containing_range_vs_item_weighted_overlap,
+        containing_range_vs_signature_weighted_overlap,
+        adjacent_vs_item_weighted_overlap,
+        adjacent_vs_signature_weighted_overlap,
+    };
+
+    Some(ScoredSnippet {
+        identifier: identifier.clone(),
+        declaration: declaration,
+        scores: score_components.score(),
+        score_components,
+    })
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct ScoreInputs {
+    pub is_same_file: bool,
+    pub is_referenced_nearby: bool,
+    pub is_referenced_in_breadcrumb: bool,
+    pub reference_count: usize,
+    pub same_file_declaration_count: usize,
+    pub declaration_count: usize,
+    pub reference_line_distance: u32,
+    pub declaration_line_distance: u32,
+    pub declaration_line_distance_rank: usize,
+    pub containing_range_vs_item_jaccard: f32,
+    pub containing_range_vs_signature_jaccard: f32,
+    pub adjacent_vs_item_jaccard: f32,
+    pub adjacent_vs_signature_jaccard: f32,
+    pub containing_range_vs_item_weighted_overlap: f32,
+    pub containing_range_vs_signature_weighted_overlap: f32,
+    pub adjacent_vs_item_weighted_overlap: f32,
+    pub adjacent_vs_signature_weighted_overlap: f32,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct Scores {
+    pub signature: f32,
+    pub declaration: f32,
+}
+
+impl ScoreInputs {
+    fn score(&self) -> Scores {
+        // Score related to how likely this is the correct declaration, range 0 to 1
+        let accuracy_score = if self.is_same_file {
+            // TODO: use declaration_line_distance_rank
+            1.0 / self.same_file_declaration_count as f32
+        } else {
+            1.0 / self.declaration_count as f32
+        };
+
+        // Score related to the distance between the reference and cursor, range 0 to 1
+        let distance_score = if self.is_referenced_nearby {
+            1.0 / (1.0 + self.reference_line_distance as f32 / 10.0).powf(2.0)
+        } else {
+            // same score as ~14 lines away, rationale is to not overly penalize references from parent signatures
+            0.5
+        };
+
+        // For now instead of linear combination, the scores are just multiplied together.
+        let combined_score = 10.0 * accuracy_score * distance_score;
+
+        Scores {
+            signature: combined_score * self.containing_range_vs_signature_weighted_overlap,
+            // declaration score gets boosted both by being multiplied by 2 and by there being more
+            // weighted overlap.
+            declaration: 2.0 * combined_score * self.containing_range_vs_item_weighted_overlap,
+        }
+    }
+}

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

@@ -1,8 +1,220 @@
+mod declaration;
+mod declaration_scoring;
 mod excerpt;
 mod outline;
 mod reference;
-mod tree_sitter_index;
+mod syntax_index;
+mod text_similarity;
 
+pub use declaration::{BufferDeclaration, Declaration, FileDeclaration, Identifier};
 pub use excerpt::{EditPredictionExcerpt, EditPredictionExcerptOptions, EditPredictionExcerptText};
+use gpui::{App, AppContext as _, Entity, Task};
+use language::BufferSnapshot;
 pub use reference::references_in_excerpt;
-pub use tree_sitter_index::{BufferDeclaration, Declaration, FileDeclaration, TreeSitterIndex};
+pub use syntax_index::SyntaxIndex;
+use text::{Point, ToOffset as _};
+
+use crate::declaration_scoring::{ScoredSnippet, scored_snippets};
+
+pub struct EditPredictionContext {
+    pub excerpt: EditPredictionExcerpt,
+    pub excerpt_text: EditPredictionExcerptText,
+    pub snippets: Vec<ScoredSnippet>,
+}
+
+impl EditPredictionContext {
+    pub fn gather(
+        cursor_point: Point,
+        buffer: BufferSnapshot,
+        excerpt_options: EditPredictionExcerptOptions,
+        syntax_index: Entity<SyntaxIndex>,
+        cx: &mut App,
+    ) -> Task<Self> {
+        let index_state = syntax_index.read_with(cx, |index, _cx| index.state().clone());
+        cx.background_spawn(async move {
+            let index_state = index_state.lock().await;
+
+            let excerpt =
+                EditPredictionExcerpt::select_from_buffer(cursor_point, &buffer, &excerpt_options)
+                    .unwrap();
+            let excerpt_text = excerpt.text(&buffer);
+            let references = references_in_excerpt(&excerpt, &excerpt_text, &buffer);
+            let cursor_offset = cursor_point.to_offset(&buffer);
+
+            let snippets = scored_snippets(
+                &index_state,
+                &excerpt,
+                &excerpt_text,
+                references,
+                cursor_offset,
+                &buffer,
+            );
+
+            Self {
+                excerpt,
+                excerpt_text,
+                snippets,
+            }
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::sync::Arc;
+
+    use gpui::{Entity, TestAppContext};
+    use indoc::indoc;
+    use language::{Language, LanguageConfig, LanguageId, LanguageMatcher, tree_sitter_rust};
+    use project::{FakeFs, Project};
+    use serde_json::json;
+    use settings::SettingsStore;
+    use util::path;
+
+    use crate::{EditPredictionExcerptOptions, SyntaxIndex};
+
+    #[gpui::test]
+    async fn test_call_site(cx: &mut TestAppContext) {
+        let (project, index, _rust_lang_id) = init_test(cx).await;
+
+        let buffer = project
+            .update(cx, |project, cx| {
+                let project_path = project.find_project_path("c.rs", cx).unwrap();
+                project.open_buffer(project_path, cx)
+            })
+            .await
+            .unwrap();
+
+        cx.run_until_parked();
+
+        // first process_data call site
+        let cursor_point = language::Point::new(8, 21);
+        let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+
+        let context = cx
+            .update(|cx| {
+                EditPredictionContext::gather(
+                    cursor_point,
+                    buffer_snapshot,
+                    EditPredictionExcerptOptions {
+                        max_bytes: 40,
+                        min_bytes: 10,
+                        target_before_cursor_over_total_bytes: 0.5,
+                        include_parent_signatures: false,
+                    },
+                    index,
+                    cx,
+                )
+            })
+            .await;
+
+        assert_eq!(context.snippets.len(), 1);
+        assert_eq!(context.snippets[0].identifier.name.as_ref(), "process_data");
+        drop(buffer);
+    }
+
+    async fn init_test(
+        cx: &mut TestAppContext,
+    ) -> (Entity<Project>, Entity<SyntaxIndex>, LanguageId) {
+        cx.update(|cx| {
+            let settings_store = SettingsStore::test(cx);
+            cx.set_global(settings_store);
+            language::init(cx);
+            Project::init_settings(cx);
+        });
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/root"),
+            json!({
+                "a.rs": indoc! {r#"
+                    fn main() {
+                        let x = 1;
+                        let y = 2;
+                        let z = add(x, y);
+                        println!("Result: {}", z);
+                    }
+
+                    fn add(a: i32, b: i32) -> i32 {
+                        a + b
+                    }
+                "#},
+                "b.rs": indoc! {"
+                    pub struct Config {
+                        pub name: String,
+                        pub value: i32,
+                    }
+
+                    impl Config {
+                        pub fn new(name: String, value: i32) -> Self {
+                            Config { name, value }
+                        }
+                    }
+                "},
+                "c.rs": indoc! {r#"
+                    use std::collections::HashMap;
+
+                    fn main() {
+                        let args: Vec<String> = std::env::args().collect();
+                        let data: Vec<i32> = args[1..]
+                            .iter()
+                            .filter_map(|s| s.parse().ok())
+                            .collect();
+                        let result = process_data(data);
+                        println!("{:?}", result);
+                    }
+
+                    fn process_data(data: Vec<i32>) -> HashMap<i32, usize> {
+                        let mut counts = HashMap::new();
+                        for value in data {
+                            *counts.entry(value).or_insert(0) += 1;
+                        }
+                        counts
+                    }
+
+                    #[cfg(test)]
+                    mod tests {
+                        use super::*;
+
+                        #[test]
+                        fn test_process_data() {
+                            let data = vec![1, 2, 2, 3];
+                            let result = process_data(data);
+                            assert_eq!(result.get(&2), Some(&2));
+                        }
+                    }
+                "#}
+            }),
+        )
+        .await;
+        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        let lang = rust_lang();
+        let lang_id = lang.id();
+        language_registry.add(Arc::new(lang));
+
+        let index = cx.new(|cx| SyntaxIndex::new(&project, cx));
+        cx.run_until_parked();
+
+        (project, index, lang_id)
+    }
+
+    fn rust_lang() -> Language {
+        Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                matcher: LanguageMatcher {
+                    path_suffixes: vec!["rs".to_string()],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::LANGUAGE.into()),
+        )
+        .with_highlights_query(include_str!("../../languages/src/rust/highlights.scm"))
+        .unwrap()
+        .with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
+        .unwrap()
+    }
+}

crates/edit_prediction_context/src/excerpt.rs πŸ”—

@@ -31,7 +31,7 @@ pub struct EditPredictionExcerptOptions {
     pub include_parent_signatures: bool,
 }
 
-#[derive(Clone)]
+#[derive(Debug, Clone)]
 pub struct EditPredictionExcerpt {
     pub range: Range<usize>,
     pub parent_signature_ranges: Vec<Range<usize>>,

crates/edit_prediction_context/src/outline.rs πŸ”—

@@ -1,5 +1,7 @@
-use language::{BufferSnapshot, LanguageId, SyntaxMapMatches};
-use std::{cmp::Reverse, ops::Range, sync::Arc};
+use language::{BufferSnapshot, SyntaxMapMatches};
+use std::{cmp::Reverse, ops::Range};
+
+use crate::declaration::Identifier;
 
 // TODO:
 //
@@ -18,12 +20,6 @@ pub struct OutlineDeclaration {
     pub signature_range: Range<usize>,
 }
 
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct Identifier {
-    pub name: Arc<str>,
-    pub language_id: LanguageId,
-}
-
 pub fn declarations_in_buffer(buffer: &BufferSnapshot) -> Vec<OutlineDeclaration> {
     declarations_overlapping_range(0..buffer.len(), buffer)
 }

crates/edit_prediction_context/src/tree_sitter_index.rs β†’ crates/edit_prediction_context/src/syntax_index.rs πŸ”—

@@ -1,20 +1,26 @@
+use std::sync::Arc;
+
 use collections::{HashMap, HashSet};
+use futures::lock::Mutex;
 use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
-use language::{Buffer, BufferEvent, BufferSnapshot};
+use language::{Buffer, BufferEvent};
 use project::buffer_store::{BufferStore, BufferStoreEvent};
 use project::worktree_store::{WorktreeStore, WorktreeStoreEvent};
 use project::{PathChange, Project, ProjectEntryId, ProjectPath};
 use slotmap::SlotMap;
-use std::ops::Range;
-use std::sync::Arc;
-use text::Anchor;
+use text::BufferId;
 use util::{ResultExt as _, debug_panic, some_or_debug_panic};
 
-use crate::outline::{Identifier, OutlineDeclaration, declarations_in_buffer};
+use crate::declaration::{
+    BufferDeclaration, Declaration, DeclarationId, FileDeclaration, Identifier,
+};
+use crate::outline::declarations_in_buffer;
 
 // TODO:
 //
 // * Skip for remote projects
+//
+// * Consider making SyntaxIndex not an Entity.
 
 // Potential future improvements:
 //
@@ -34,17 +40,19 @@ use crate::outline::{Identifier, OutlineDeclaration, declarations_in_buffer};
 // * Concurrent slotmap
 //
 // * Use queue for parsing
+//
 
-slotmap::new_key_type! {
-    pub struct DeclarationId;
+pub struct SyntaxIndex {
+    state: Arc<Mutex<SyntaxIndexState>>,
+    project: WeakEntity<Project>,
 }
 
-pub struct TreeSitterIndex {
+#[derive(Default)]
+pub struct SyntaxIndexState {
     declarations: SlotMap<DeclarationId, Declaration>,
     identifiers: HashMap<Identifier, HashSet<DeclarationId>>,
     files: HashMap<ProjectEntryId, FileState>,
-    buffers: HashMap<WeakEntity<Buffer>, BufferState>,
-    project: WeakEntity<Project>,
+    buffers: HashMap<BufferId, BufferState>,
 }
 
 #[derive(Debug, Default)]
@@ -59,52 +67,11 @@ struct BufferState {
     task: Option<Task<()>>,
 }
 
-#[derive(Debug, Clone)]
-pub enum Declaration {
-    File {
-        project_entry_id: ProjectEntryId,
-        declaration: FileDeclaration,
-    },
-    Buffer {
-        buffer: WeakEntity<Buffer>,
-        declaration: BufferDeclaration,
-    },
-}
-
-impl Declaration {
-    fn identifier(&self) -> &Identifier {
-        match self {
-            Declaration::File { declaration, .. } => &declaration.identifier,
-            Declaration::Buffer { declaration, .. } => &declaration.identifier,
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct FileDeclaration {
-    pub parent: Option<DeclarationId>,
-    pub identifier: Identifier,
-    pub item_range: Range<usize>,
-    pub signature_range: Range<usize>,
-    pub signature_text: Arc<str>,
-}
-
-#[derive(Debug, Clone)]
-pub struct BufferDeclaration {
-    pub parent: Option<DeclarationId>,
-    pub identifier: Identifier,
-    pub item_range: Range<Anchor>,
-    pub signature_range: Range<Anchor>,
-}
-
-impl TreeSitterIndex {
+impl SyntaxIndex {
     pub fn new(project: &Entity<Project>, cx: &mut Context<Self>) -> Self {
         let mut this = Self {
-            declarations: SlotMap::with_key(),
-            identifiers: HashMap::default(),
             project: project.downgrade(),
-            files: HashMap::default(),
-            buffers: HashMap::default(),
+            state: Arc::new(Mutex::new(SyntaxIndexState::default())),
         };
 
         let worktree_store = project.read(cx).worktree_store();
@@ -139,73 +106,6 @@ impl TreeSitterIndex {
         this
     }
 
-    pub fn declaration(&self, id: DeclarationId) -> Option<&Declaration> {
-        self.declarations.get(id)
-    }
-
-    pub fn declarations_for_identifier<const N: usize>(
-        &self,
-        identifier: Identifier,
-        cx: &App,
-    ) -> Vec<Declaration> {
-        // make sure to not have a large stack allocation
-        assert!(N < 32);
-
-        let Some(declaration_ids) = self.identifiers.get(&identifier) else {
-            return vec![];
-        };
-
-        let mut result = Vec::with_capacity(N);
-        let mut included_buffer_entry_ids = arrayvec::ArrayVec::<_, N>::new();
-        let mut file_declarations = Vec::new();
-
-        for declaration_id in declaration_ids {
-            let declaration = self.declarations.get(*declaration_id);
-            let Some(declaration) = some_or_debug_panic(declaration) else {
-                continue;
-            };
-            match declaration {
-                Declaration::Buffer { buffer, .. } => {
-                    if let Ok(Some(entry_id)) = buffer.read_with(cx, |buffer, cx| {
-                        project::File::from_dyn(buffer.file()).and_then(|f| f.project_entry_id(cx))
-                    }) {
-                        included_buffer_entry_ids.push(entry_id);
-                        result.push(declaration.clone());
-                        if result.len() == N {
-                            return result;
-                        }
-                    }
-                }
-                Declaration::File {
-                    project_entry_id, ..
-                } => {
-                    if !included_buffer_entry_ids.contains(project_entry_id) {
-                        file_declarations.push(declaration.clone());
-                    }
-                }
-            }
-        }
-
-        for declaration in file_declarations {
-            match declaration {
-                Declaration::File {
-                    project_entry_id, ..
-                } => {
-                    if !included_buffer_entry_ids.contains(&project_entry_id) {
-                        result.push(declaration);
-
-                        if result.len() == N {
-                            return result;
-                        }
-                    }
-                }
-                Declaration::Buffer { .. } => {}
-            }
-        }
-
-        result
-    }
-
     fn handle_worktree_store_event(
         &mut self,
         _worktree_store: Entity<WorktreeStore>,
@@ -215,21 +115,33 @@ impl TreeSitterIndex {
         use WorktreeStoreEvent::*;
         match event {
             WorktreeUpdatedEntries(worktree_id, updated_entries_set) => {
-                for (path, entry_id, path_change) in updated_entries_set.iter() {
-                    if let PathChange::Removed = path_change {
-                        self.files.remove(entry_id);
-                    } else {
-                        let project_path = ProjectPath {
-                            worktree_id: *worktree_id,
-                            path: path.clone(),
-                        };
-                        self.update_file(*entry_id, project_path, cx);
+                let state = Arc::downgrade(&self.state);
+                let worktree_id = *worktree_id;
+                let updated_entries_set = updated_entries_set.clone();
+                cx.spawn(async move |this, cx| {
+                    let Some(state) = state.upgrade() else { return };
+                    for (path, entry_id, path_change) in updated_entries_set.iter() {
+                        if let PathChange::Removed = path_change {
+                            state.lock().await.files.remove(entry_id);
+                        } else {
+                            let project_path = ProjectPath {
+                                worktree_id,
+                                path: path.clone(),
+                            };
+                            this.update(cx, |this, cx| {
+                                this.update_file(*entry_id, project_path, cx);
+                            })
+                            .ok();
+                        }
                     }
-                }
+                })
+                .detach();
             }
             WorktreeDeletedEntry(_worktree_id, project_entry_id) => {
-                // TODO: Is this needed?
-                self.files.remove(project_entry_id);
+                let project_entry_id = *project_entry_id;
+                self.with_state(cx, move |state| {
+                    state.files.remove(&project_entry_id);
+                })
             }
             _ => {}
         }
@@ -251,15 +163,42 @@ impl TreeSitterIndex {
         }
     }
 
+    pub fn state(&self) -> &Arc<Mutex<SyntaxIndexState>> {
+        &self.state
+    }
+
+    fn with_state(&self, cx: &mut App, f: impl FnOnce(&mut SyntaxIndexState) + Send + 'static) {
+        if let Some(mut state) = self.state.try_lock() {
+            f(&mut state);
+            return;
+        }
+        let state = Arc::downgrade(&self.state);
+        cx.background_spawn(async move {
+            let Some(state) = state.upgrade() else {
+                return;
+            };
+            let mut state = state.lock().await;
+            f(&mut state)
+        })
+        .detach();
+    }
+
     fn register_buffer(&mut self, buffer: &Entity<Buffer>, cx: &mut Context<Self>) {
-        self.buffers
-            .insert(buffer.downgrade(), BufferState::default());
-        let weak_buf = buffer.downgrade();
-        cx.observe_release(buffer, move |this, _buffer, _cx| {
-            this.buffers.remove(&weak_buf);
+        let buffer_id = buffer.read(cx).remote_id();
+        cx.observe_release(buffer, move |this, _buffer, cx| {
+            this.with_state(cx, move |state| {
+                if let Some(buffer_state) = state.buffers.remove(&buffer_id) {
+                    SyntaxIndexState::remove_buffer_declarations(
+                        &buffer_state.declarations,
+                        &mut state.declarations,
+                        &mut state.identifiers,
+                    );
+                }
+            })
         })
         .detach();
         cx.subscribe(buffer, Self::handle_buffer_event).detach();
+
         self.update_buffer(buffer.clone(), cx);
     }
 
@@ -275,10 +214,19 @@ impl TreeSitterIndex {
         }
     }
 
-    fn update_buffer(&mut self, buffer: Entity<Buffer>, cx: &Context<Self>) {
-        let mut parse_status = buffer.read(cx).parse_status();
+    fn update_buffer(&mut self, buffer_entity: Entity<Buffer>, cx: &mut Context<Self>) {
+        let buffer = buffer_entity.read(cx);
+
+        let Some(project_entry_id) =
+            project::File::from_dyn(buffer.file()).and_then(|f| f.project_entry_id(cx))
+        else {
+            return;
+        };
+        let buffer_id = buffer.remote_id();
+
+        let mut parse_status = buffer.parse_status();
         let snapshot_task = cx.spawn({
-            let weak_buffer = buffer.downgrade();
+            let weak_buffer = buffer_entity.downgrade();
             async move |_, cx| {
                 while *parse_status.borrow() != language::ParseStatus::Idle {
                     parse_status.changed().await?;
@@ -289,75 +237,77 @@ impl TreeSitterIndex {
 
         let parse_task = cx.background_spawn(async move {
             let snapshot = snapshot_task.await?;
+            let rope = snapshot.text.as_rope().clone();
 
-            anyhow::Ok(
+            anyhow::Ok((
                 declarations_in_buffer(&snapshot)
                     .into_iter()
                     .map(|item| {
                         (
                             item.parent_index,
-                            BufferDeclaration::from_outline(item, &snapshot),
+                            BufferDeclaration::from_outline(item, &rope),
                         )
                     })
                     .collect::<Vec<_>>(),
-            )
+                rope,
+            ))
         });
 
         let task = cx.spawn({
-            let weak_buffer = buffer.downgrade();
             async move |this, cx| {
-                let Ok(declarations) = parse_task.await else {
+                let Ok((declarations, rope)) = parse_task.await else {
                     return;
                 };
 
-                this.update(cx, |this, _cx| {
-                    let buffer_state = this
-                        .buffers
-                        .entry(weak_buffer.clone())
-                        .or_insert_with(Default::default);
-
-                    for old_declaration_id in &buffer_state.declarations {
-                        let Some(declaration) = this.declarations.remove(*old_declaration_id)
-                        else {
-                            debug_panic!("declaration not found");
-                            continue;
-                        };
-                        if let Some(identifier_declarations) =
-                            this.identifiers.get_mut(declaration.identifier())
-                        {
-                            identifier_declarations.remove(old_declaration_id);
+                this.update(cx, move |this, cx| {
+                    this.with_state(cx, move |state| {
+                        let buffer_state = state
+                            .buffers
+                            .entry(buffer_id)
+                            .or_insert_with(Default::default);
+
+                        SyntaxIndexState::remove_buffer_declarations(
+                            &buffer_state.declarations,
+                            &mut state.declarations,
+                            &mut state.identifiers,
+                        );
+
+                        let mut new_ids = Vec::with_capacity(declarations.len());
+                        state.declarations.reserve(declarations.len());
+                        for (parent_index, mut declaration) in declarations {
+                            declaration.parent = parent_index
+                                .and_then(|ix| some_or_debug_panic(new_ids.get(ix).copied()));
+
+                            let identifier = declaration.identifier.clone();
+                            let declaration_id = state.declarations.insert(Declaration::Buffer {
+                                rope: rope.clone(),
+                                buffer_id,
+                                declaration,
+                                project_entry_id,
+                            });
+                            new_ids.push(declaration_id);
+
+                            state
+                                .identifiers
+                                .entry(identifier)
+                                .or_default()
+                                .insert(declaration_id);
                         }
-                    }
-
-                    let mut new_ids = Vec::with_capacity(declarations.len());
-                    this.declarations.reserve(declarations.len());
-                    for (parent_index, mut declaration) in declarations {
-                        declaration.parent = parent_index
-                            .and_then(|ix| some_or_debug_panic(new_ids.get(ix).copied()));
-
-                        let identifier = declaration.identifier.clone();
-                        let declaration_id = this.declarations.insert(Declaration::Buffer {
-                            buffer: weak_buffer.clone(),
-                            declaration,
-                        });
-                        new_ids.push(declaration_id);
-
-                        this.identifiers
-                            .entry(identifier)
-                            .or_default()
-                            .insert(declaration_id);
-                    }
 
-                    buffer_state.declarations = new_ids;
+                        buffer_state.declarations = new_ids;
+                    });
                 })
                 .ok();
             }
         });
 
-        self.buffers
-            .entry(buffer.downgrade())
-            .or_insert_with(Default::default)
-            .task = Some(task);
+        self.with_state(cx, move |state| {
+            state
+                .buffers
+                .entry(buffer_id)
+                .or_insert_with(Default::default)
+                .task = Some(task)
+        });
     }
 
     fn update_file(
@@ -401,14 +351,10 @@ impl TreeSitterIndex {
 
         let parse_task = cx.background_spawn(async move {
             let snapshot = snapshot_task.await?;
+            let rope = snapshot.as_rope();
             let declarations = declarations_in_buffer(&snapshot)
                 .into_iter()
-                .map(|item| {
-                    (
-                        item.parent_index,
-                        FileDeclaration::from_outline(item, &snapshot),
-                    )
-                })
+                .map(|item| (item.parent_index, FileDeclaration::from_outline(item, rope)))
                 .collect::<Vec<_>>();
             anyhow::Ok(declarations)
         });
@@ -419,84 +365,160 @@ impl TreeSitterIndex {
                 let Ok(declarations) = parse_task.await else {
                     return;
                 };
-                this.update(cx, |this, _cx| {
-                    let file_state = this.files.entry(entry_id).or_insert_with(Default::default);
-
-                    for old_declaration_id in &file_state.declarations {
-                        let Some(declaration) = this.declarations.remove(*old_declaration_id)
-                        else {
-                            debug_panic!("declaration not found");
-                            continue;
-                        };
-                        if let Some(identifier_declarations) =
-                            this.identifiers.get_mut(declaration.identifier())
-                        {
-                            identifier_declarations.remove(old_declaration_id);
+                this.update(cx, |this, cx| {
+                    this.with_state(cx, move |state| {
+                        let file_state =
+                            state.files.entry(entry_id).or_insert_with(Default::default);
+
+                        for old_declaration_id in &file_state.declarations {
+                            let Some(declaration) = state.declarations.remove(*old_declaration_id)
+                            else {
+                                debug_panic!("declaration not found");
+                                continue;
+                            };
+                            if let Some(identifier_declarations) =
+                                state.identifiers.get_mut(declaration.identifier())
+                            {
+                                identifier_declarations.remove(old_declaration_id);
+                            }
                         }
-                    }
-
-                    let mut new_ids = Vec::with_capacity(declarations.len());
-                    this.declarations.reserve(declarations.len());
 
-                    for (parent_index, mut declaration) in declarations {
-                        declaration.parent = parent_index
-                            .and_then(|ix| some_or_debug_panic(new_ids.get(ix).copied()));
-
-                        let identifier = declaration.identifier.clone();
-                        let declaration_id = this.declarations.insert(Declaration::File {
-                            project_entry_id: entry_id,
-                            declaration,
-                        });
-                        new_ids.push(declaration_id);
-
-                        this.identifiers
-                            .entry(identifier)
-                            .or_default()
-                            .insert(declaration_id);
-                    }
+                        let mut new_ids = Vec::with_capacity(declarations.len());
+                        state.declarations.reserve(declarations.len());
+
+                        for (parent_index, mut declaration) in declarations {
+                            declaration.parent = parent_index
+                                .and_then(|ix| some_or_debug_panic(new_ids.get(ix).copied()));
+
+                            let identifier = declaration.identifier.clone();
+                            let declaration_id = state.declarations.insert(Declaration::File {
+                                project_entry_id: entry_id,
+                                declaration,
+                            });
+                            new_ids.push(declaration_id);
+
+                            state
+                                .identifiers
+                                .entry(identifier)
+                                .or_default()
+                                .insert(declaration_id);
+                        }
 
-                    file_state.declarations = new_ids;
+                        file_state.declarations = new_ids;
+                    });
                 })
                 .ok();
             }
         });
 
-        self.files
-            .entry(entry_id)
-            .or_insert_with(Default::default)
-            .task = Some(task);
+        self.with_state(cx, move |state| {
+            state
+                .files
+                .entry(entry_id)
+                .or_insert_with(Default::default)
+                .task = Some(task);
+        });
     }
 }
 
-impl BufferDeclaration {
-    pub fn from_outline(declaration: OutlineDeclaration, snapshot: &BufferSnapshot) -> Self {
-        // use of anchor_before is a guess that the proper behavior is to expand to include
-        // insertions immediately before the declaration, but not for insertions immediately after
-        Self {
-            parent: None,
-            identifier: declaration.identifier,
-            item_range: snapshot.anchor_before(declaration.item_range.start)
-                ..snapshot.anchor_before(declaration.item_range.end),
-            signature_range: snapshot.anchor_before(declaration.signature_range.start)
-                ..snapshot.anchor_before(declaration.signature_range.end),
+impl SyntaxIndexState {
+    pub fn declaration(&self, id: DeclarationId) -> Option<&Declaration> {
+        self.declarations.get(id)
+    }
+
+    /// Returns declarations for the identifier. If the limit is exceeded, returns an empty vector.
+    ///
+    /// TODO: Consider doing some pre-ranking and instead truncating when N is exceeded.
+    pub fn declarations_for_identifier<const N: usize>(
+        &self,
+        identifier: &Identifier,
+    ) -> Vec<Declaration> {
+        // make sure to not have a large stack allocation
+        assert!(N < 32);
+
+        let Some(declaration_ids) = self.identifiers.get(&identifier) else {
+            return vec![];
+        };
+
+        let mut result = Vec::with_capacity(N);
+        let mut included_buffer_entry_ids = arrayvec::ArrayVec::<_, N>::new();
+        let mut file_declarations = Vec::new();
+
+        for declaration_id in declaration_ids {
+            let declaration = self.declarations.get(*declaration_id);
+            let Some(declaration) = some_or_debug_panic(declaration) else {
+                continue;
+            };
+            match declaration {
+                Declaration::Buffer {
+                    project_entry_id, ..
+                } => {
+                    included_buffer_entry_ids.push(*project_entry_id);
+                    result.push(declaration.clone());
+                    if result.len() == N {
+                        return Vec::new();
+                    }
+                }
+                Declaration::File {
+                    project_entry_id, ..
+                } => {
+                    if !included_buffer_entry_ids.contains(&project_entry_id) {
+                        file_declarations.push(declaration.clone());
+                    }
+                }
+            }
+        }
+
+        for declaration in file_declarations {
+            match declaration {
+                Declaration::File {
+                    project_entry_id, ..
+                } => {
+                    if !included_buffer_entry_ids.contains(&project_entry_id) {
+                        result.push(declaration);
+
+                        if result.len() == N {
+                            return Vec::new();
+                        }
+                    }
+                }
+                Declaration::Buffer { .. } => {}
+            }
+        }
+
+        result
+    }
+
+    pub fn file_declaration_count(&self, declaration: &Declaration) -> usize {
+        match declaration {
+            Declaration::File {
+                project_entry_id, ..
+            } => self
+                .files
+                .get(project_entry_id)
+                .map(|file_state| file_state.declarations.len())
+                .unwrap_or_default(),
+            Declaration::Buffer { buffer_id, .. } => self
+                .buffers
+                .get(buffer_id)
+                .map(|buffer_state| buffer_state.declarations.len())
+                .unwrap_or_default(),
         }
     }
-}
 
-impl FileDeclaration {
-    pub fn from_outline(
-        declaration: OutlineDeclaration,
-        snapshot: &BufferSnapshot,
-    ) -> FileDeclaration {
-        FileDeclaration {
-            parent: None,
-            identifier: declaration.identifier,
-            item_range: declaration.item_range,
-            signature_text: snapshot
-                .text_for_range(declaration.signature_range.clone())
-                .collect::<String>()
-                .into(),
-            signature_range: declaration.signature_range,
+    fn remove_buffer_declarations(
+        old_declaration_ids: &[DeclarationId],
+        declarations: &mut SlotMap<DeclarationId, Declaration>,
+        identifiers: &mut HashMap<Identifier, HashSet<DeclarationId>>,
+    ) {
+        for old_declaration_id in old_declaration_ids {
+            let Some(declaration) = declarations.remove(*old_declaration_id) else {
+                debug_panic!("declaration not found");
+                continue;
+            };
+            if let Some(identifier_declarations) = identifiers.get_mut(declaration.identifier()) {
+                identifier_declarations.remove(old_declaration_id);
+            }
         }
     }
 }
@@ -509,13 +531,13 @@ mod tests {
     use gpui::TestAppContext;
     use indoc::indoc;
     use language::{Language, LanguageConfig, LanguageId, LanguageMatcher, tree_sitter_rust};
-    use project::{FakeFs, Project, ProjectItem};
+    use project::{FakeFs, Project};
     use serde_json::json;
     use settings::SettingsStore;
     use text::OffsetRangeExt as _;
     use util::path;
 
-    use crate::tree_sitter_index::TreeSitterIndex;
+    use crate::syntax_index::SyntaxIndex;
 
     #[gpui::test]
     async fn test_unopen_indexed_files(cx: &mut TestAppContext) {
@@ -525,17 +547,19 @@ mod tests {
             language_id: rust_lang_id,
         };
 
-        index.read_with(cx, |index, cx| {
-            let decls = index.declarations_for_identifier::<8>(main.clone(), cx);
+        let index_state = index.read_with(cx, |index, _cx| index.state().clone());
+        let index_state = index_state.lock().await;
+        cx.update(|cx| {
+            let decls = index_state.declarations_for_identifier::<8>(&main);
             assert_eq!(decls.len(), 2);
 
             let decl = expect_file_decl("c.rs", &decls[0], &project, cx);
             assert_eq!(decl.identifier, main.clone());
-            assert_eq!(decl.item_range, 32..279);
+            assert_eq!(decl.item_range_in_file, 32..280);
 
             let decl = expect_file_decl("a.rs", &decls[1], &project, cx);
             assert_eq!(decl.identifier, main);
-            assert_eq!(decl.item_range, 0..97);
+            assert_eq!(decl.item_range_in_file, 0..98);
         });
     }
 
@@ -547,15 +571,17 @@ mod tests {
             language_id: rust_lang_id,
         };
 
-        index.read_with(cx, |index, cx| {
-            let decls = index.declarations_for_identifier::<8>(test_process_data.clone(), cx);
+        let index_state = index.read_with(cx, |index, _cx| index.state().clone());
+        let index_state = index_state.lock().await;
+        cx.update(|cx| {
+            let decls = index_state.declarations_for_identifier::<8>(&test_process_data);
             assert_eq!(decls.len(), 1);
 
             let decl = expect_file_decl("c.rs", &decls[0], &project, cx);
             assert_eq!(decl.identifier, test_process_data);
 
             let parent_id = decl.parent.unwrap();
-            let parent = index.declaration(parent_id).unwrap();
+            let parent = index_state.declaration(parent_id).unwrap();
             let parent_decl = expect_file_decl("c.rs", &parent, &project, cx);
             assert_eq!(
                 parent_decl.identifier,
@@ -586,16 +612,18 @@ mod tests {
 
         cx.run_until_parked();
 
-        index.read_with(cx, |index, cx| {
-            let decls = index.declarations_for_identifier::<8>(test_process_data.clone(), cx);
+        let index_state = index.read_with(cx, |index, _cx| index.state().clone());
+        let index_state = index_state.lock().await;
+        cx.update(|cx| {
+            let decls = index_state.declarations_for_identifier::<8>(&test_process_data);
             assert_eq!(decls.len(), 1);
 
-            let decl = expect_buffer_decl("c.rs", &decls[0], cx);
+            let decl = expect_buffer_decl("c.rs", &decls[0], &project, cx);
             assert_eq!(decl.identifier, test_process_data);
 
             let parent_id = decl.parent.unwrap();
-            let parent = index.declaration(parent_id).unwrap();
-            let parent_decl = expect_buffer_decl("c.rs", &parent, cx);
+            let parent = index_state.declaration(parent_id).unwrap();
+            let parent_decl = expect_buffer_decl("c.rs", &parent, &project, cx);
             assert_eq!(
                 parent_decl.identifier,
                 Identifier {
@@ -613,16 +641,13 @@ mod tests {
     async fn test_declarations_limt(cx: &mut TestAppContext) {
         let (_, index, rust_lang_id) = init_test(cx).await;
 
-        index.read_with(cx, |index, cx| {
-            let decls = index.declarations_for_identifier::<1>(
-                Identifier {
-                    name: "main".into(),
-                    language_id: rust_lang_id,
-                },
-                cx,
-            );
-            assert_eq!(decls.len(), 1);
+        let index_state = index.read_with(cx, |index, _cx| index.state().clone());
+        let index_state = index_state.lock().await;
+        let decls = index_state.declarations_for_identifier::<1>(&Identifier {
+            name: "main".into(),
+            language_id: rust_lang_id,
         });
+        assert_eq!(decls.len(), 0);
     }
 
     #[gpui::test]
@@ -644,24 +669,31 @@ mod tests {
 
         cx.run_until_parked();
 
-        index.read_with(cx, |index, cx| {
-            let decls = index.declarations_for_identifier::<8>(main.clone(), cx);
-            assert_eq!(decls.len(), 2);
-            let decl = expect_buffer_decl("c.rs", &decls[0], cx);
-            assert_eq!(decl.identifier, main);
-            assert_eq!(decl.item_range.to_offset(&buffer.read(cx)), 32..279);
+        let index_state_arc = index.read_with(cx, |index, _cx| index.state().clone());
+        {
+            let index_state = index_state_arc.lock().await;
 
-            expect_file_decl("a.rs", &decls[1], &project, cx);
-        });
+            cx.update(|cx| {
+                let decls = index_state.declarations_for_identifier::<8>(&main);
+                assert_eq!(decls.len(), 2);
+                let decl = expect_buffer_decl("c.rs", &decls[0], &project, cx);
+                assert_eq!(decl.identifier, main);
+                assert_eq!(decl.item_range.to_offset(&buffer.read(cx)), 32..280);
+
+                expect_file_decl("a.rs", &decls[1], &project, cx);
+            });
+        }
 
-        // Need to trigger flush_effects so that the observe_release handler will run.
-        cx.update(|_cx| {
+        // Drop the buffer and wait for release
+        cx.update(|_| {
             drop(buffer);
         });
         cx.run_until_parked();
 
-        index.read_with(cx, |index, cx| {
-            let decls = index.declarations_for_identifier::<8>(main, cx);
+        let index_state = index_state_arc.lock().await;
+
+        cx.update(|cx| {
+            let decls = index_state.declarations_for_identifier::<8>(&main);
             assert_eq!(decls.len(), 2);
             expect_file_decl("c.rs", &decls[0], &project, cx);
             expect_file_decl("a.rs", &decls[1], &project, cx);
@@ -671,24 +703,20 @@ mod tests {
     fn expect_buffer_decl<'a>(
         path: &str,
         declaration: &'a Declaration,
+        project: &Entity<Project>,
         cx: &App,
     ) -> &'a BufferDeclaration {
         if let Declaration::Buffer {
             declaration,
-            buffer,
+            project_entry_id,
+            ..
         } = declaration
         {
-            assert_eq!(
-                buffer
-                    .upgrade()
-                    .unwrap()
-                    .read(cx)
-                    .project_path(cx)
-                    .unwrap()
-                    .path
-                    .as_ref(),
-                Path::new(path),
-            );
+            let project_path = project
+                .read(cx)
+                .path_for_entry(*project_entry_id, cx)
+                .unwrap();
+            assert_eq!(project_path.path.as_ref(), Path::new(path),);
             declaration
         } else {
             panic!("Expected a buffer declaration, found {:?}", declaration);
@@ -723,7 +751,7 @@ mod tests {
 
     async fn init_test(
         cx: &mut TestAppContext,
-    ) -> (Entity<Project>, Entity<TreeSitterIndex>, LanguageId) {
+    ) -> (Entity<Project>, Entity<SyntaxIndex>, LanguageId) {
         cx.update(|cx| {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
@@ -801,7 +829,7 @@ mod tests {
         let lang_id = lang.id();
         language_registry.add(Arc::new(lang));
 
-        let index = cx.new(|cx| TreeSitterIndex::new(&project, cx));
+        let index = cx.new(|cx| SyntaxIndex::new(&project, cx));
         cx.run_until_parked();
 
         (project, index, lang_id)

crates/edit_prediction_context/src/text_similarity.rs πŸ”—

@@ -0,0 +1,241 @@
+use regex::Regex;
+use std::{collections::HashMap, sync::LazyLock};
+
+use crate::reference::Reference;
+
+// TODO: Consider implementing sliding window similarity matching like
+// https://github.com/sourcegraph/cody-public-snapshot/blob/8e20ac6c1460c08b0db581c0204658112a246eda/vscode/src/completions/context/retrievers/jaccard-similarity/bestJaccardMatch.ts
+//
+// That implementation could actually be more efficient - no need to track words in the window that
+// are not in the query.
+
+static IDENTIFIER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\b\w+\b").unwrap());
+
+#[derive(Debug)]
+pub struct IdentifierOccurrences {
+    identifier_to_count: HashMap<String, usize>,
+    total_count: usize,
+}
+
+impl IdentifierOccurrences {
+    pub fn within_string(code: &str) -> Self {
+        Self::from_iterator(IDENTIFIER_REGEX.find_iter(code).map(|mat| mat.as_str()))
+    }
+
+    #[allow(dead_code)]
+    pub fn within_references(references: &[Reference]) -> Self {
+        Self::from_iterator(
+            references
+                .iter()
+                .map(|reference| reference.identifier.name.as_ref()),
+        )
+    }
+
+    pub fn from_iterator<'a>(identifier_iterator: impl Iterator<Item = &'a str>) -> Self {
+        let mut identifier_to_count = HashMap::new();
+        let mut total_count = 0;
+        for identifier in identifier_iterator {
+            // TODO: Score matches that match case higher?
+            //
+            // TODO: Also include unsplit identifier?
+            for identifier_part in split_identifier(identifier) {
+                identifier_to_count
+                    .entry(identifier_part.to_lowercase())
+                    .and_modify(|count| *count += 1)
+                    .or_insert(1);
+                total_count += 1;
+            }
+        }
+        IdentifierOccurrences {
+            identifier_to_count,
+            total_count,
+        }
+    }
+}
+
+// Splits camelcase / snakecase / kebabcase / pascalcase
+//
+// TODO: Make this more efficient / elegant.
+fn split_identifier(identifier: &str) -> Vec<&str> {
+    let mut parts = Vec::new();
+    let mut start = 0;
+    let chars: Vec<char> = identifier.chars().collect();
+
+    if chars.is_empty() {
+        return parts;
+    }
+
+    let mut i = 0;
+    while i < chars.len() {
+        let ch = chars[i];
+
+        // Handle explicit delimiters (underscore and hyphen)
+        if ch == '_' || ch == '-' {
+            if i > start {
+                parts.push(&identifier[start..i]);
+            }
+            start = i + 1;
+            i += 1;
+            continue;
+        }
+
+        // Handle camelCase and PascalCase transitions
+        if i > 0 && i < chars.len() {
+            let prev_char = chars[i - 1];
+
+            // Transition from lowercase/digit to uppercase
+            if (prev_char.is_lowercase() || prev_char.is_ascii_digit()) && ch.is_uppercase() {
+                parts.push(&identifier[start..i]);
+                start = i;
+            }
+            // Handle sequences like "XMLParser" -> ["XML", "Parser"]
+            else if i + 1 < chars.len()
+                && ch.is_uppercase()
+                && chars[i + 1].is_lowercase()
+                && prev_char.is_uppercase()
+            {
+                parts.push(&identifier[start..i]);
+                start = i;
+            }
+        }
+
+        i += 1;
+    }
+
+    // Add the last part if there's any remaining
+    if start < identifier.len() {
+        parts.push(&identifier[start..]);
+    }
+
+    // Filter out empty strings
+    parts.into_iter().filter(|s| !s.is_empty()).collect()
+}
+
+pub fn jaccard_similarity<'a>(
+    mut set_a: &'a IdentifierOccurrences,
+    mut set_b: &'a IdentifierOccurrences,
+) -> f32 {
+    if set_a.identifier_to_count.len() > set_b.identifier_to_count.len() {
+        std::mem::swap(&mut set_a, &mut set_b);
+    }
+    let intersection = set_a
+        .identifier_to_count
+        .keys()
+        .filter(|key| set_b.identifier_to_count.contains_key(*key))
+        .count();
+    let union = set_a.identifier_to_count.len() + set_b.identifier_to_count.len() - intersection;
+    intersection as f32 / union as f32
+}
+
+// TODO
+#[allow(dead_code)]
+pub fn overlap_coefficient<'a>(
+    mut set_a: &'a IdentifierOccurrences,
+    mut set_b: &'a IdentifierOccurrences,
+) -> f32 {
+    if set_a.identifier_to_count.len() > set_b.identifier_to_count.len() {
+        std::mem::swap(&mut set_a, &mut set_b);
+    }
+    let intersection = set_a
+        .identifier_to_count
+        .keys()
+        .filter(|key| set_b.identifier_to_count.contains_key(*key))
+        .count();
+    intersection as f32 / set_a.identifier_to_count.len() as f32
+}
+
+// TODO
+#[allow(dead_code)]
+pub fn weighted_jaccard_similarity<'a>(
+    mut set_a: &'a IdentifierOccurrences,
+    mut set_b: &'a IdentifierOccurrences,
+) -> f32 {
+    if set_a.identifier_to_count.len() > set_b.identifier_to_count.len() {
+        std::mem::swap(&mut set_a, &mut set_b);
+    }
+
+    let mut numerator = 0;
+    let mut denominator_a = 0;
+    let mut used_count_b = 0;
+    for (symbol, count_a) in set_a.identifier_to_count.iter() {
+        let count_b = set_b.identifier_to_count.get(symbol).unwrap_or(&0);
+        numerator += count_a.min(count_b);
+        denominator_a += count_a.max(count_b);
+        used_count_b += count_b;
+    }
+
+    let denominator = denominator_a + (set_b.total_count - used_count_b);
+    if denominator == 0 {
+        0.0
+    } else {
+        numerator as f32 / denominator as f32
+    }
+}
+
+pub fn weighted_overlap_coefficient<'a>(
+    mut set_a: &'a IdentifierOccurrences,
+    mut set_b: &'a IdentifierOccurrences,
+) -> f32 {
+    if set_a.identifier_to_count.len() > set_b.identifier_to_count.len() {
+        std::mem::swap(&mut set_a, &mut set_b);
+    }
+
+    let mut numerator = 0;
+    for (symbol, count_a) in set_a.identifier_to_count.iter() {
+        let count_b = set_b.identifier_to_count.get(symbol).unwrap_or(&0);
+        numerator += count_a.min(count_b);
+    }
+
+    let denominator = set_a.total_count.min(set_b.total_count);
+    if denominator == 0 {
+        0.0
+    } else {
+        numerator as f32 / denominator as f32
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_split_identifier() {
+        assert_eq!(split_identifier("snake_case"), vec!["snake", "case"]);
+        assert_eq!(split_identifier("kebab-case"), vec!["kebab", "case"]);
+        assert_eq!(split_identifier("PascalCase"), vec!["Pascal", "Case"]);
+        assert_eq!(split_identifier("camelCase"), vec!["camel", "Case"]);
+        assert_eq!(split_identifier("XMLParser"), vec!["XML", "Parser"]);
+    }
+
+    #[test]
+    fn test_similarity_functions() {
+        // 10 identifier parts, 8 unique
+        // Repeats: 2 "outline", 2 "items"
+        let set_a = IdentifierOccurrences::within_string(
+            "let mut outline_items = query_outline_items(&language, &tree, &source);",
+        );
+        // 14 identifier parts, 11 unique
+        // Repeats: 2 "outline", 2 "language", 2 "tree"
+        let set_b = IdentifierOccurrences::within_string(
+            "pub fn query_outline_items(language: &Language, tree: &Tree, source: &str) -> Vec<OutlineItem> {",
+        );
+
+        // 6 overlaps: "outline", "items", "query", "language", "tree", "source"
+        // 7 non-overlaps: "let", "mut", "pub", "fn", "vec", "item", "str"
+        assert_eq!(jaccard_similarity(&set_a, &set_b), 6.0 / (6.0 + 7.0));
+
+        // Numerator is one more than before due to both having 2 "outline".
+        // Denominator is the same except for 3 more due to the non-overlapping duplicates
+        assert_eq!(
+            weighted_jaccard_similarity(&set_a, &set_b),
+            7.0 / (7.0 + 7.0 + 3.0)
+        );
+
+        // Numerator is the same as jaccard_similarity. Denominator is the size of the smaller set, 8.
+        assert_eq!(overlap_coefficient(&set_a, &set_b), 6.0 / 8.0);
+
+        // Numerator is the same as weighted_jaccard_similarity. Denominator is the total weight of
+        // the smaller set, 10.
+        assert_eq!(weighted_overlap_coefficient(&set_a, &set_b), 7.0 / 10.0);
+    }
+}

crates/edit_prediction_context/src/wip_requests.rs πŸ”—

@@ -0,0 +1,35 @@
+// To discuss: What to send to the new endpoint? Thinking it'd make sense to put `prompt.rs` from
+// `zeta_context.rs` in cloud.
+//
+// * Run excerpt selection at several different sizes, send the largest size with offsets within for
+// the smaller sizes.
+//
+// * Longer event history.
+//
+// * Many more snippets than could fit in model context - allows ranking experimentation.
+
+pub struct Zeta2Request {
+    pub event_history: Vec<Event>,
+    pub excerpt: String,
+    pub excerpt_subsets: Vec<Zeta2ExcerptSubset>,
+    /// Within `excerpt`
+    pub cursor_position: usize,
+    pub signatures: Vec<String>,
+    pub retrieved_declarations: Vec<ReferencedDeclaration>,
+}
+
+pub struct Zeta2ExcerptSubset {
+    /// Within `excerpt` text.
+    pub excerpt_range: Range<usize>,
+    /// Within `signatures`.
+    pub parent_signatures: Vec<usize>,
+}
+
+pub struct ReferencedDeclaration {
+    pub text: Arc<str>,
+    /// Range within `text`
+    pub signature_range: Range<usize>,
+    /// Indices within `signatures`.
+    pub parent_signatures: Vec<usize>,
+    // A bunch of score metrics
+}

crates/language_models/src/provider/vercel.rs πŸ”—

@@ -25,7 +25,6 @@ use crate::{api_key::ApiKeyState, ui::InstructionListItem};
 const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("vercel");
 const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Vercel");
 
-// todo!() -> Remove default implementation
 const API_KEY_ENV_VAR_NAME: &str = "VERCEL_API_KEY";
 static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
 

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

@@ -286,6 +286,15 @@ impl ContextServerStore {
         self.servers.keys().cloned().collect()
     }
 
+    pub fn all_registry_descriptor_ids(&self, cx: &App) -> Vec<ContextServerId> {
+        self.registry
+            .read(cx)
+            .context_server_descriptors()
+            .into_iter()
+            .map(|(id, _)| ContextServerId(id))
+            .collect()
+    }
+
     pub fn running_servers(&self) -> Vec<Arc<ContextServer>> {
         self.servers
             .values()

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

@@ -47,7 +47,10 @@ pub struct ProjectSettings {
     /// To override settings for a language, add an entry for that language server's
     /// name to the lsp value.
     /// Default: null
-    // todo! should these hash map types be Map<key, SettingsContent> or Map<Key, Settings>
+    // todo(settings-follow-up)
+    // We should change to use a non content type (settings::LspSettings is a content type)
+    // Note: Will either require merging with defaults, which also requires deciding where the defaults come from,
+    //       or case by case deciding which fields are optional and which are actually required.
     pub lsp: HashMap<LanguageServerName, settings::LspSettings>,
 
     /// Common language server settings.

crates/recent_projects/src/remote_servers.rs πŸ”—

@@ -1360,7 +1360,7 @@ impl RemoteServerProjects {
             setting
                 .wsl_connections
                 .get_or_insert(Default::default())
-                .push(crate::remote_connections::WslConnection {
+                .push(settings::WslConnection {
                     distro_name: SharedString::from(connection_options.distro_name),
                     user: connection_options.user,
                     projects: BTreeSet::new(),

crates/remote/src/transport/wsl.rs πŸ”—

@@ -409,7 +409,7 @@ impl RemoteConnection for WslRemoteConnection {
                 "--".to_string(),
                 self.shell.clone(),
                 "-c".to_string(),
-                shlex::try_quote(&script)?.to_string(),
+                script,
             ]
         } else {
             vec![
@@ -420,7 +420,7 @@ impl RemoteConnection for WslRemoteConnection {
                 "--".to_string(),
                 self.shell.clone(),
                 "-c".to_string(),
-                shlex::try_quote(&script)?.to_string(),
+                script,
             ]
         };
 

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

@@ -250,7 +250,6 @@ impl settings::Settings for TerminalSettings {
                 }
             }
         }
-        // todo!() test that this works.
         if content.terminal.is_none() && default != TerminalSettingsContent::default() {
             content.terminal = Some(default)
         }

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

@@ -36,6 +36,7 @@ enum DividerDirection {
 #[derive(Default)]
 pub enum DividerColor {
     Border,
+    BorderFaded,
     #[default]
     BorderVariant,
 }
@@ -44,6 +45,7 @@ impl DividerColor {
     pub fn hsla(self, cx: &mut App) -> Hsla {
         match self {
             DividerColor::Border => cx.theme().colors().border,
+            DividerColor::BorderFaded => cx.theme().colors().border.opacity(0.6),
             DividerColor::BorderVariant => cx.theme().colors().border_variant,
         }
     }

docs/src/SUMMARY.md πŸ”—

@@ -15,6 +15,7 @@
 
 - [Configuring Zed](./configuring-zed.md)
 - [Configuring Languages](./configuring-languages.md)
+  - [Toolchains](./toolchains.md)
 - [Key bindings](./key-bindings.md)
   - [All Actions](./all-actions.md)
 - [Snippets](./snippets.md)

docs/src/configuring-languages.md πŸ”—

@@ -135,6 +135,12 @@ In this example:
 
 This configuration allows you to tailor the language server setup to your specific needs, ensuring that you get the most suitable functionality for your development workflow.
 
+### Toolchains
+
+Some language servers need to be configured with a current "toolchain", which is an installation of a specific version of a programming language compiler or/and interpreter, which can possibly include a full set of dependencies of a project.
+An example of what Zed considers a toolchain is a virtual environment in Python.
+Not all languages in Zed support toolchain discovery and selection, but for those that do, you can specify the toolchain from a toolchain picker (via {#action toolchain::Select}). To learn more about toolchains in Zed, see [`toolchains`](./toolchains.md).
+
 ### Configuring Language Servers
 
 Many language servers accept custom configuration options. You can set these in the `lsp` section of your `settings.json`:

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

@@ -1,59 +1,134 @@
-# Python
+# How to Set Up Python in Zed
 
 Python support is available natively in Zed.
 
 - Tree-sitter: [tree-sitter-python](https://github.com/zed-industries/tree-sitter-python)
 - Language Servers:
+  - [DetachHead/basedpyright](https://github.com/DetachHead/basedpyright)
+  - [astral-sh/ruff](https://github.com/astral-sh/ruff)
+  - [astral-sh/ty](https://github.com/astral-sh/ty)
   - [microsoft/pyright](https://github.com/microsoft/pyright)
   - [python-lsp/python-lsp-server](https://github.com/python-lsp/python-lsp-server) (PyLSP)
 - Debug Adapter: [debugpy](https://github.com/microsoft/debugpy)
 
-## Language Servers
+## Install Python
 
-Zed supports multiple Python language servers some of which may require configuration to work properly.
+You'll need both Zed and Python installed before you can begin.
 
-See: [Working with Language Servers](https://zed.dev/docs/configuring-languages#working-with-language-servers) for more information.
+### Step 1: Install Python
 
-## Virtual Environments in the Terminal {#terminal-detect_venv}
+Zed does not bundle a Python runtime, so you’ll need to install one yourself.
+Choose one of the following options:
 
-Zed will detect Python virtual environments and automatically activate them in terminal if available.
-See: [detect_venv documentation](../configuring-zed.md#terminal-detect_venv) for more.
+- uv (recommended)
 
-## PyLSP
+```bash
+curl -LsSf https://astral.sh/uv/install.sh | sh
+```
 
-[python-lsp-server](https://github.com/python-lsp/python-lsp-server/), more commonly known as PyLSP, by default integrates with a number of external tools (autopep8, mccabe, pycodestyle, yapf) while others are optional and must be explicitly enabled and configured (flake8, pylint).
+To learn more, visit [Astral’s installation guide](https://docs.astral.sh/uv/getting-started/installation/).
 
-See [Python Language Server Configuration](https://github.com/python-lsp/python-lsp-server/blob/develop/CONFIGURATION.md) for more.
+- Homebrew:
+
+```bash
+brew install python
+```
+
+- Python.org installer: Download the latest version from [python.org/downloads](https://python.org/downloads).
+
+### Step 2: Verify Python Installation
+
+Confirm Python is installed and available in your shell:
+
+```bash
+python3 --version
+```
+
+You should see an output like `Python 3.x.x`.
+
+## Open Your First Python Project in Zed
+
+Once Zed and Python are installed, open a folder containing Python code to start working.
+
+### Step 1: Launch Zed with a Python Project
+
+Open Zed.
+From the menu bar, choose File > Open Folder, or launch from the terminal:
+
+```bash
+zed path/to/your/project
+```
+
+Zed will recognize `.py` files automatically using its native tree-sitter-python parser, with no plugins or manual setup required.
+
+### Step 2: Use the Integrated Terminal (Optional)
+
+Zed includes an integrated terminal, accessible from the bottom panel. If Zed detects that your project is using a [virtual environment](#virtual-environments), it will be activated automatically in newly-created terminals. You can configure this behavior with the [`detect_venv`](../configuring-zed.md#terminal-detect_venv) setting.
+
+## Configure Python Language Servers in Zed
+
+Zed provides several Python language servers out of the box. By default, [basedpyright](https://github.com/DetachHead/basedpyright) is the primary language server, and [Ruff](https://github.com/astral-sh/ruff) is used for formatting and linting.
 
-## PyRight
+Other built-in language servers are:
 
-### PyRight Configuration
+- [Ty](https://docs.astral.sh/ty/)&mdash;Up-and-coming language server from Astral, built for speed.
+- [Pyright](https://github.com/microsoft/pyright)&mdash;The basis for basedpyright.
+- [PyLSP](https://github.com/python-lsp/python-lsp-server)&mdash;A plugin-based language server that integrates with tools like `pycodestyle`, `autopep8`, and `yapf`.
 
-The [pyright](https://github.com/microsoft/pyright) language server offers flexible configuration options specified in a JSON-formatted text configuration. By default, the file is called `pyrightconfig.json` and is located within the root directory of your project. Pyright settings can also be specified in a `[tool.pyright]` section of a `pyproject.toml` file. A `pyrightconfig.json` file always takes precedence over `pyproject.toml` if both are present.
+These are disabled by default, but can be enabled in your settings. For example:
 
-For more information, see the Pyright [configuration documentation](https://microsoft.github.io/pyright/#/configuration).
+```json
+{
+  "languages": {
+    "Python": {
+      "language_servers": {
+        // Disable basedpyright and enable Ty, and otherwise
+        // use the default configuration.
+        "ty", "!basedpyright", ".."
+      }
+    }
+  }
+}
+```
+
+See: [Working with Language Servers](https://zed.dev/docs/configuring-languages#working-with-language-servers) for more information about how to enable and disable language servers.
+
+### Basedpyright
+
+[basedpyright](https://docs.basedpyright.com/latest/) is the primary Python language server in Zed beginning with Zed v0.204.0. It provides core language server functionality like navigation (go to definition/find all references) and type checking. Compared to Pyright, it adds support for additional language server features (like inlay hints) and checking rules.
+
+Note that while basedpyright in isolation defaults to the `recommended` [type-checking mode](https://docs.basedpyright.com/latest/benefits-over-pyright/better-defaults/#typecheckingmode), Zed configures it to use the less-strict `standard` mode by default, which matches the behavior of Pyright. You can set the type-checking mode for your project using the `typeCheckingMode` setting in `pyrightconfig.json` or `pyproject.toml`, which will override Zed's default. Read on more for more details about how to configure basedpyright.
 
-### PyRight Settings
+#### Basedpyright Configuration
 
-The [pyright](https://github.com/microsoft/pyright) language server also accepts specific LSP-related settings, not necessarily connected to a project. These can be changed in the `lsp` section of your `settings.json`.
+basedpyright reads configuration options from two different kinds of sources:
+
+- Language server settings ("workspace configuration"), which must be configured per-editor (using `settings.json` in Zed's case) but apply to all projects opened in that editor
+- Configuration files (`pyrightconfig.json`, `pyproject.toml`), which are editor-independent but specific to the project where they are placed
+
+As a rule of thumb, options that are only relevant when using basedpyright from an editor must be set in language server settings, and options that are relevant even if you're running it [as a command-line tool](https://docs.basedpyright.com/latest/configuration/command-line/) must be set in configuration files. Settings related to inlay hints are examples of the first category, and the [diagnostic category](https://docs.basedpyright.com/latest/configuration/config-files/#diagnostic-categories) settings are examples of the second category.
+
+Examples of both kinds of configuration are provided below. Refer to the basedpyright documentation on [language server settings](https://docs.basedpyright.com/latest/configuration/language-server-settings/) and [configuration files](https://docs.basedpyright.com/latest/configuration/config-files/) for comprehensive lists of available options.
+
+##### Language server settings
+
+Language server settings for basedpyright in Zed can be set in the `lsp` section of your `settings.json`.
 
 For example, in order to:
 
-- use strict type-checking level
 - diagnose all files in the workspace instead of the only open files default
-- provide the path to a specific Python interpreter
+- disable inlay hints on function arguments
+
+You can use the following configuration:
 
 ```json
 {
   "lsp": {
-    "pyright": {
+    "basedpyright": {
       "settings": {
-        "python.analysis": {
+        "basedpyright.analysis": {
           "diagnosticMode": "workspace",
-          "typeCheckingMode": "strict"
-        },
-        "python": {
-          "pythonPath": ".venv/bin/python"
+          "inlayHints.callArgumentNames": false
         }
       }
     }
@@ -61,54 +136,103 @@ For example, in order to:
 }
 ```
 
-For more information, see the Pyright [settings documentation](https://microsoft.github.io/pyright/#/settings).
+##### Configuration files
+
+basedpyright reads project-specific configuration from the `pyrightconfig.json` configuration file and from the `[tool.basedpyright]` and `[tool.pyright]` sections of `pyproject.toml` manifests. `pyrightconfig.json` overrides `pyproject.toml` if configuration is present in both places.
+
+Here's an example `pyrightconfig.json` file that configures basedpyright to use the `strict` type-checking mode and not to issue diagnostics for any files in `__pycache__` directories:
+
+```json
+{
+  "typeCheckingMode": "strict",
+  "ignore": ["**/__pycache__"]
+}
+```
+
+### PyLSP
 
-### Pyright Virtual environments
+[python-lsp-server](https://github.com/python-lsp/python-lsp-server/), more commonly known as PyLSP, by default integrates with a number of external tools (autopep8, mccabe, pycodestyle, yapf) while others are optional and must be explicitly enabled and configured (flake8, pylint).
+
+See [Python Language Server Configuration](https://github.com/python-lsp/python-lsp-server/blob/develop/CONFIGURATION.md) for more.
 
-A Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) allows you to store all of a project's dependencies, including the Python interpreter and package manager, in a single directory that's isolated from any other Python projects on your computer.
+## Virtual Environments
 
-By default, the Pyright language server will look for Python packages in the default global locations. But you can also configure Pyright to use the packages installed in a given virtual environment.
+[Virtual environments](https://docs.python.org/3/library/venv.html) are a useful tool for fixing a Python version and set of dependencies for a specific project, in a way that's isolated from other projects on the same machine. Zed has built-in support for discovering, configuring, and activating virtual environments, based on the language-agnostic concept of a [toolchain](../toolchains.md).
 
-To do this, create a JSON file called `pyrightconfig.json` at the root of your project. This file must include two keys:
+Note that if you have a global Python installation, it is also counted as a toolchain for Zed's purposes.
 
-- `venvPath`: a relative path from your project directory to any directory that _contains_ one or more virtual environment directories
-- `venv`: the name of a virtual environment directory
+### Create a Virtual Environment
 
-For example, a common approach is to create a virtual environment directory called `.venv` at the root of your project directory with the following commands:
+If your project doesn't have a virtual environment set up already, you can create one as follows:
 
-```sh
-# create a virtual environment in the .venv directory
+```bash
 python3 -m venv .venv
-# set up the current shell to use that virtual environment
-source .venv/bin/activate
 ```
 
-Having done that, you would create a `pyrightconfig.json` with the following content:
+Alternatively, if you're using `uv`, running `uv sync` will create a virtual environment the first time you run it.
+
+### How Zed Uses Python Toolchains
+
+Zed uses the selected Python toolchain for your project in the following ways:
+
+- Built-in language servers will be automatically configured with the path to the toolchain's Python interpreter and, if applicable, virtual environment. This is important so that they can resolve dependencies. (Note that language servers provided by extensions can't be automatically configured like this currently.)
+- Python tasks (such as pytest tests) will be run using the toolchain's Python interpreter.
+- If the toolchain is a virtual environment, the environment's activation script will be run automatically when you launch a new shell in Zed's integrated terminal, giving you convenient access to the selected Python interpreter and dependency set.
+- If a built-in language server is installed in the active virtual environment, that binary will be used instead of Zed's private automatically-installed binary. This also applies to debugpy.
+
+### Selecting a Toolchain
+
+For most projects, Zed will automatically select the right Python toolchain. In complex projects with multiple virtual environments, it might be necessary to override this selection. You can use the [toolchain selector](../toolchains.md#selecting-toolchains) to pick a toolchain from the list discovered by Zed, or [specify the path to a toolchain manually](../toolchains.md#adding-toolchains-manually) if it's not on the list.
+
+## Code Formatting & Linting
+
+Zed provides the [Ruff](https://docs.astral.sh/ruff/) formatter and linter for Python code. (Specifically, Zed runs Ruff as an LSP server using the `ruff server` subcommand.) Both formatting and linting are enabled by default, including format-on-save.
+
+### Configuring formatting
+
+You can disable format-on-save for Python files in your `settings.json`:
 
 ```json
 {
-  "venvPath": ".",
-  "venv": ".venv"
+  "languages": {
+    "Python": {
+      "format_on_save": false
+    }
+  }
 }
 ```
 
-If you prefer to use a `pyproject.toml` file, you can add the following section:
+Alternatively, you can use the `black` command-line tool for Python formatting, while keeping Ruff enabled for linting:
 
-```toml
-[tool.pyright]
-venvPath = "."
-venv = ".venv"
+```json
+{
+  "languages": {
+    "Python": {
+      "formatter": {
+        "external": {
+          "command": "black",
+          "arguments": ["--stdin-filename", "{buffer_path}", "-"]
+        }
+      }
+      // Or use `"formatter": null` to disable formatting entirely.
+    }
+  }
+}
 ```
 
-You can also configure this option directly in your `settings.json` file ([pyright settings](#pyright-settings)), as recommended in [Configuring Your Python Environment](https://microsoft.github.io/pyright/#/import-resolution?id=configuring-your-python-environment).
+### Configuring Ruff
+
+Like basedpyright, Ruff reads options from both Zed's language server settings and configuration files (`ruff.toml`) when used in Zed. Unlike basedpyright, _all_ options can be configured in either of these locations, so the choice of where to put your Ruff configuration comes down to whether you want it to be shared between projects but specific to Zed (in which case you should use language server settings), or specific to one project but common to all Ruff invocations (in which case you should use `ruff.toml`).
+
+Here's an example of using language server settings in Zed's `settings.json` to disable all Ruff lints in Zed (while still using Ruff as a formatter):
 
 ```json
 {
   "lsp": {
-    "pyright": {
-      "settings": {
-        "python": {
-          "pythonPath": ".venv/bin/python"
+    "ruff": {
+      "initialization_options": {
+        "settings": {
+          "exclude": ["*"]
         }
       }
     }
@@ -116,24 +240,40 @@ You can also configure this option directly in your `settings.json` file ([pyrig
 }
 ```
 
-### Code formatting & Linting
+And here's an example `ruff.toml` with linting and formatting options, adapted from the Ruff documentation:
 
-The Pyright language server does not provide code formatting or linting. If you want to detect lint errors and reformat your Python code upon saving, you'll need to set up.
+```toml
+[lint]
+# Avoid enforcing line-length violations (`E501`)
+ignore = ["E501"]
 
-A common tool for formatting Python code is [Ruff](https://docs.astral.sh/ruff/). It is another tool written in Rust, an extremely fast Python linter and code formatter. It is available through the [Ruff extension](https://github.com/zed-industries/zed/tree/main/extensions/ruff/). To configure the Ruff extension to work within Zed, see the setup documentation [here](https://docs.astral.sh/ruff/editors/setup/#zed).
+[format]
+# Use single quotes when formatting.
+quote-style = "single"
+```
 
-<!--
-TBD: Expand Python Ruff docs.
-TBD: Ruff pyproject.toml, ruff.toml docs. `ruff.configuration`.
--->
+For more details, refer to the Ruff documentation about [configuration files](https://docs.astral.sh/ruff/configuration/) and [language server settings](https://docs.astral.sh/ruff/editors/settings/), and the [list of options](https://docs.astral.sh/ruff/settings/).
 
 ## Debugging
 
-Zed supports zero-configuration debugging of Python module entry points and pytest tests.
-Run {#action debugger::Start} ({#kb debugger::Start}) to see a contextual list for the current project.
-For greater control, you can add debug configurations to `.zed/debug.json`. See the examples below.
+Zed supports Python debugging through the `debugpy` adapter. You can start with no configuration or define custom launch profiles in `.zed/debug.json`.
+
+### Start Debugging with No Setup
+
+Zed can automatically detect debuggable Python entry points. Press F4 (or run debugger: start from the Command Palette) to see available options for your current project.
+This works for:
+
+- Python scripts
+- Modules
+- pytest tests
 
-### Debug Active File
+Zed uses `debugpy` under the hood, but no manual adapter configuration is required.
+
+### Define Custom Debug Configurations
+
+For reusable setups, create a `.zed/debug.json` file in your project root. This gives you more control over how Zed runs and debugs your code.
+
+#### Debug Active File
 
 ```json
 [
@@ -146,9 +286,11 @@ For greater control, you can add debug configurations to `.zed/debug.json`. See
 ]
 ```
 
-### Flask App
+This runs the file currently open in the editor.
 
-For a common Flask Application with a file structure similar to the following:
+#### Debug a Flask App
+
+For projects using Flask, you can define a full launch configuration:
 
 ```
 .venv/
@@ -190,3 +332,22 @@ requirements.txt
   }
 ]
 ```
+
+These can be combined to tailor the experience for web servers, test runners, or custom scripts.
+
+## Troubleshoot and Maintain a Productive Python Setup
+
+Zed is designed to minimize configuration overhead, but occasional issues can still ariseβ€”especially around environments, language servers, or tooling. Here's how to keep your Python setup working smoothly.
+
+### Resolve Language Server Startup Issues
+
+If a language server isn't responding or features like diagnostics or autocomplete aren't available:
+
+- Check your Zed log (using the {#action zed::OpenLog} action) for errors related to the language server you're trying to use. This is where you're likely to find useful information if the language server failed to start up at all.
+- Use the language server logs view to understand the lifecycle of the affected language server. You can access this view using the {#action dev::OpenLanguageServerLogs} action, or by clicking the lightning bolt icon in the status bar and selecting your language server. The most useful pieces of data in this view are:
+  - "Server Logs", which shows any errors printed by the language server
+  - "Server Info", which shows details about how the language server was started
+- Verify your `settings.json` or `pyrightconfig.json` is syntactically correct.
+- Restart Zed to reinitialize language server connections, or try restarting the language server using the {#action editor::RestartLanguageServer}
+
+If the language server is failing to resolve imports, and you're using a virtual environment, make sure that the right environment is chosen in the selector. You can use "Server Info" view to confirm which virtual environment Zed is sending to the language server&mdash;look for the `* Configuration` section at the end.

docs/src/toolchains.md πŸ”—

@@ -0,0 +1,28 @@
+# Toolchains
+
+Zed projects offer a dedicated UI for toolchain selection, which lets you pick a set of tools for working with a given language in a current project.
+Imagine you're working with Python project, which has virtual environments that encapsulate a set of dependencies of your project along with a suitable interpreter to run it with. The language server has to know which virtual environment you're working with, as it uses it to understand your project's code.
+With toolchain selector, you don't need to spend time configuring your language server to point it at the right virtual environment directory - you can just select the right virtual environment (toolchain) from a dropdown.
+You can even select different toolchains for different subprojects within your Zed project. A definition of a subproject is language-specific.
+In collaborative scenarios, only the project owner can see and modify an active toolchain.
+In [remote projects](./remote-development.md)., you can use the toolchain selector to control the active toolchain on the SSH host. When [sharing your project](./collaboration.md), the toolchain selector is not available to guests.
+
+## Why do we need toolchains?
+
+The active toolchain is relevant for launching language servers, which may need it to function properly - it may not be able to resolve dependencies, which in turn may make functionalities like "Go to definition" or "Code completions" unavailable.
+
+The active toolchain is also relevant when launching a shell in the terminal panel: some toolchains provide "activation scripts" for shells, which make those toolchains available in the shell environment for your convenience. Zed will run these activation scripts automatically when you create a new terminal.
+
+This also applies to [tasks](./tasks.md) - Zed tasks behave "as if" you opened a new terminal tab and ran a given task invocation yourself, which in turn means that Zed task execution is affected by the active toolchain and its activation script.
+
+## Selecting toolchains
+
+The active toolchain (if there is one) is displayed in the status bar (on the right hand side). Click on it to access the toolchain selector - you can also use an action from a command palette ({#action toolchain::Select}).
+
+Zed will automatically infer a set of toolchains to choose from based on the project you're working with. A default will also be selected on your behalf on a best-effort basis when you open a project for the first time.
+
+The toolchain selection applies to a current subproject, which - depending on the structure of your Zed project - might be your whole project or just a subset of it. For example, if you have a monorepo with multiple subprojects, you might want to select a different toolchain for each subproject.
+
+## Adding toolchains manually
+
+If automatic detection does not suffice for you, you can add toolchains manually. To do that, click on the "Add toolchain" button in the toolchain selector. From there you can provide a path to a toolchain and set a name of your liking for it.