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

Cole Miller created

Change summary

assets/settings/default.json                                              |  4 
crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs | 28 
crates/assistant_tools/src/schema.rs                                      |  4 
crates/debugger_ui/src/session/running/console.rs                         | 88 
crates/editor/src/code_context_menus.rs                                   |  9 
crates/extension/src/extension_builder.rs                                 | 11 
crates/extension/src/extension_manifest.rs                                | 33 
crates/extension_host/src/extension_host.rs                               | 17 
crates/extension_host/src/headless_host.rs                                | 27 
crates/gpui/src/platform/linux/platform.rs                                |  4 
crates/project/src/debugger/session.rs                                    |  4 
crates/project/src/lsp_store.rs                                           | 17 
crates/project/src/project.rs                                             |  4 
crates/project_panel/src/project_panel.rs                                 |  7 
crates/proto/proto/lsp.proto                                              |  2 
crates/util/src/paths.rs                                                  | 15 
docs/src/languages/ruby.md                                                | 21 
extensions/emmet/extension.toml                                           |  3 
18 files changed, 239 insertions(+), 59 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -746,8 +746,6 @@
     "default_width": 380
   },
   "agent": {
-    // Version of this setting.
-    "version": "2",
     // Whether the agent is enabled.
     "enabled": true,
     /// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
@@ -1658,7 +1656,6 @@
   // Different settings for specific language models.
   "language_models": {
     "anthropic": {
-      "version": "1",
       "api_url": "https://api.anthropic.com"
     },
     "google": {
@@ -1668,7 +1665,6 @@
       "api_url": "http://localhost:11434"
     },
     "openai": {
-      "version": "1",
       "api_url": "https://api.openai.com/v1"
     },
     "open_router": {

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

@@ -379,6 +379,14 @@ impl ConfigureContextServerModal {
         };
 
         self.state = State::Waiting;
+
+        let existing_server = self.context_server_store.read(cx).get_running_server(&id);
+        if existing_server.is_some() {
+            self.context_server_store.update(cx, |store, cx| {
+                store.stop_server(&id, cx).log_err();
+            });
+        }
+
         let wait_for_context_server_task =
             wait_for_context_server(&self.context_server_store, id.clone(), cx);
         cx.spawn({
@@ -399,13 +407,21 @@ impl ConfigureContextServerModal {
         })
         .detach();
 
-        // When we write the settings to the file, the context server will be restarted.
-        workspace.update(cx, |workspace, cx| {
-            let fs = workspace.app_state().fs.clone();
-            update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
-                project_settings.context_servers.insert(id.0, settings);
+        let settings_changed =
+            ProjectSettings::get_global(cx).context_servers.get(&id.0) != Some(&settings);
+
+        if settings_changed {
+            // When we write the settings to the file, the context server will be restarted.
+            workspace.update(cx, |workspace, cx| {
+                let fs = workspace.app_state().fs.clone();
+                update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
+                    project_settings.context_servers.insert(id.0, settings);
+                });
             });
-        });
+        } else if let Some(existing_server) = existing_server {
+            self.context_server_store
+                .update(cx, |store, cx| store.start_server(existing_server, cx));
+        }
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {

crates/assistant_tools/src/schema.rs 🔗

@@ -25,9 +25,7 @@ fn schema_to_json(
 fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
     let mut generator = match format {
         LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
-        // TODO: Gemini docs mention using a subset of OpenAPI 3, so this may benefit from using
-        // `SchemaSettings::openapi3()`.
-        LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::draft07()
+        LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
             .with(|settings| {
                 settings.meta_schema = None;
                 settings.inline_subschemas = true;

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -5,7 +5,7 @@ use super::{
 use alacritty_terminal::vte::ansi;
 use anyhow::Result;
 use collections::HashMap;
-use dap::OutputEvent;
+use dap::{CompletionItem, CompletionItemType, OutputEvent};
 use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
 use fuzzy::StringMatchCandidate;
 use gpui::{
@@ -17,6 +17,7 @@ use menu::{Confirm, SelectNext, SelectPrevious};
 use project::{
     Completion, CompletionResponse,
     debugger::session::{CompletionsQuery, OutputToken, Session},
+    lsp_store::CompletionDocumentation,
     search_history::{SearchHistory, SearchHistoryCursor},
 };
 use settings::Settings;
@@ -555,15 +556,27 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
         buffer: &Entity<Buffer>,
         position: language::Anchor,
         text: &str,
-        _trigger_in_words: bool,
+        trigger_in_words: bool,
         menu_is_open: bool,
         cx: &mut Context<Editor>,
     ) -> bool {
+        let mut chars = text.chars();
+        let char = if let Some(char) = chars.next() {
+            char
+        } else {
+            return false;
+        };
+
         let snapshot = buffer.read(cx).snapshot();
         if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
             return false;
         }
 
+        let classifier = snapshot.char_classifier_at(position).for_completion(true);
+        if trigger_in_words && classifier.is_word(char) {
+            return true;
+        }
+
         self.0
             .read_with(cx, |console, cx| {
                 console
@@ -596,21 +609,28 @@ impl ConsoleQueryBarCompletionProvider {
                 variable_list.completion_variables(cx)
             }) {
                 if let Some(evaluate_name) = &variable.evaluate_name {
-                    variables.insert(evaluate_name.clone(), variable.value.clone());
+                    if variables
+                        .insert(evaluate_name.clone(), variable.value.clone())
+                        .is_none()
+                    {
+                        string_matches.push(StringMatchCandidate {
+                            id: 0,
+                            string: evaluate_name.clone(),
+                            char_bag: evaluate_name.chars().collect(),
+                        });
+                    }
+                }
+
+                if variables
+                    .insert(variable.name.clone(), variable.value.clone())
+                    .is_none()
+                {
                     string_matches.push(StringMatchCandidate {
                         id: 0,
-                        string: evaluate_name.clone(),
-                        char_bag: evaluate_name.chars().collect(),
+                        string: variable.name.clone(),
+                        char_bag: variable.name.chars().collect(),
                     });
                 }
-
-                variables.insert(variable.name.clone(), variable.value.clone());
-
-                string_matches.push(StringMatchCandidate {
-                    id: 0,
-                    string: variable.name.clone(),
-                    char_bag: variable.name.chars().collect(),
-                });
             }
 
             (variables, string_matches)
@@ -656,11 +676,13 @@ impl ConsoleQueryBarCompletionProvider {
                         new_text: string_match.string.clone(),
                         label: CodeLabel {
                             filter_range: 0..string_match.string.len(),
-                            text: format!("{} {}", string_match.string, variable_value),
+                            text: string_match.string.clone(),
                             runs: Vec::new(),
                         },
                         icon_path: None,
-                        documentation: None,
+                        documentation: Some(CompletionDocumentation::MultiLineMarkdown(
+                            variable_value.into(),
+                        )),
                         confirm: None,
                         source: project::CompletionSource::Custom,
                         insert_text_mode: None,
@@ -675,6 +697,32 @@ impl ConsoleQueryBarCompletionProvider {
         })
     }
 
+    const fn completion_type_score(completion_type: CompletionItemType) -> usize {
+        match completion_type {
+            CompletionItemType::Field | CompletionItemType::Property => 0,
+            CompletionItemType::Variable | CompletionItemType::Value => 1,
+            CompletionItemType::Method
+            | CompletionItemType::Function
+            | CompletionItemType::Constructor => 2,
+            CompletionItemType::Class
+            | CompletionItemType::Interface
+            | CompletionItemType::Module => 3,
+            _ => 4,
+        }
+    }
+
+    fn completion_item_sort_text(completion_item: &CompletionItem) -> String {
+        completion_item.sort_text.clone().unwrap_or_else(|| {
+            format!(
+                "{:03}_{}",
+                Self::completion_type_score(
+                    completion_item.type_.unwrap_or(CompletionItemType::Text)
+                ),
+                completion_item.label.to_ascii_lowercase()
+            )
+        })
+    }
+
     fn client_completions(
         &self,
         console: &Entity<Console>,
@@ -699,6 +747,7 @@ impl ConsoleQueryBarCompletionProvider {
             let completions = completions
                 .into_iter()
                 .map(|completion| {
+                    let sort_text = Self::completion_item_sort_text(&completion);
                     let new_text = completion
                         .text
                         .as_ref()
@@ -731,12 +780,11 @@ impl ConsoleQueryBarCompletionProvider {
                             runs: Vec::new(),
                         },
                         icon_path: None,
-                        documentation: None,
+                        documentation: completion.detail.map(|detail| {
+                            CompletionDocumentation::MultiLineMarkdown(detail.into())
+                        }),
                         confirm: None,
-                        source: project::CompletionSource::BufferWord {
-                            word_range: buffer_position..language::Anchor::MAX,
-                            resolved: false,
-                        },
+                        source: project::CompletionSource::Dap { sort_text },
                         insert_text_mode: None,
                     }
                 })

crates/editor/src/code_context_menus.rs 🔗

@@ -1083,11 +1083,10 @@ impl CompletionsMenu {
                 if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
             );
 
-            let sort_text = if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source
-            {
-                lsp_completion.sort_text.as_deref()
-            } else {
-                None
+            let sort_text = match &completion.source {
+                CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
+                CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
+                _ => None,
             };
 
             let (sort_kind, sort_label) = completion.sort_key();

crates/extension/src/extension_builder.rs 🔗

@@ -1,5 +1,6 @@
 use crate::{
-    ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, parse_wasm_extension_version,
+    ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, build_debug_adapter_schema_path,
+    parse_wasm_extension_version,
 };
 use anyhow::{Context as _, Result, bail};
 use async_compression::futures::bufread::GzipDecoder;
@@ -99,12 +100,8 @@ impl ExtensionBuilder {
         }
 
         for (debug_adapter_name, meta) in &mut extension_manifest.debug_adapters {
-            let debug_adapter_relative_schema_path =
-                meta.schema_path.clone().unwrap_or_else(|| {
-                    Path::new("debug_adapter_schemas")
-                        .join(Path::new(debug_adapter_name.as_ref()).with_extension("json"))
-                });
-            let debug_adapter_schema_path = extension_dir.join(debug_adapter_relative_schema_path);
+            let debug_adapter_schema_path =
+                extension_dir.join(build_debug_adapter_schema_path(debug_adapter_name, meta));
 
             let debug_adapter_schema = fs::read_to_string(&debug_adapter_schema_path)
                 .with_context(|| {

crates/extension/src/extension_manifest.rs 🔗

@@ -132,6 +132,16 @@ impl ExtensionManifest {
     }
 }
 
+pub fn build_debug_adapter_schema_path(
+    adapter_name: &Arc<str>,
+    meta: &DebugAdapterManifestEntry,
+) -> PathBuf {
+    meta.schema_path.clone().unwrap_or_else(|| {
+        Path::new("debug_adapter_schemas")
+            .join(Path::new(adapter_name.as_ref()).with_extension("json"))
+    })
+}
+
 /// A capability for an extension.
 #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
 #[serde(tag = "kind")]
@@ -320,6 +330,29 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_build_adapter_schema_path_with_schema_path() {
+        let adapter_name = Arc::from("my_adapter");
+        let entry = DebugAdapterManifestEntry {
+            schema_path: Some(PathBuf::from("foo/bar")),
+        };
+
+        let path = build_debug_adapter_schema_path(&adapter_name, &entry);
+        assert_eq!(path, PathBuf::from("foo/bar"));
+    }
+
+    #[test]
+    fn test_build_adapter_schema_path_without_schema_path() {
+        let adapter_name = Arc::from("my_adapter");
+        let entry = DebugAdapterManifestEntry { schema_path: None };
+
+        let path = build_debug_adapter_schema_path(&adapter_name, &entry);
+        assert_eq!(
+            path,
+            PathBuf::from("debug_adapter_schemas").join("my_adapter.json")
+        );
+    }
+
     #[test]
     fn test_allow_exact_match() {
         let manifest = ExtensionManifest {

crates/extension_host/src/extension_host.rs 🔗

@@ -1639,6 +1639,23 @@ impl ExtensionStore {
                 }
             }
 
+            for (adapter_name, meta) in loaded_extension.manifest.debug_adapters.iter() {
+                let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta);
+
+                if fs.is_file(&src_dir.join(schema_path)).await {
+                    match schema_path.parent() {
+                        Some(parent) => fs.create_dir(&tmp_dir.join(parent)).await?,
+                        None => {}
+                    }
+                    fs.copy_file(
+                        &src_dir.join(schema_path),
+                        &tmp_dir.join(schema_path),
+                        fs::CopyOptions::default(),
+                    )
+                    .await?
+                }
+            }
+
             Ok(())
         })
     }

crates/extension_host/src/headless_host.rs 🔗

@@ -4,8 +4,8 @@ use anyhow::{Context as _, Result};
 use client::{TypedEnvelope, proto};
 use collections::{HashMap, HashSet};
 use extension::{
-    Extension, ExtensionHostProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
-    ExtensionManifest,
+    Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy,
+    ExtensionLanguageServerProxy, ExtensionManifest,
 };
 use fs::{Fs, RemoveOptions, RenameOptions};
 use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
@@ -169,8 +169,9 @@ impl HeadlessExtensionStore {
             return Ok(());
         }
 
-        let wasm_extension: Arc<dyn Extension> =
-            Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?);
+        let wasm_extension: Arc<dyn Extension> = Arc::new(
+            WasmExtension::load(extension_dir.clone(), &manifest, wasm_host.clone(), &cx).await?,
+        );
 
         for (language_server_id, language_server_config) in &manifest.language_servers {
             for language in language_server_config.languages() {
@@ -186,6 +187,24 @@ impl HeadlessExtensionStore {
                     );
                 })?;
             }
+            for (debug_adapter, meta) in &manifest.debug_adapters {
+                let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta);
+
+                this.update(cx, |this, _cx| {
+                    this.proxy.register_debug_adapter(
+                        wasm_extension.clone(),
+                        debug_adapter.clone(),
+                        &extension_dir.join(schema_path),
+                    );
+                })?;
+            }
+
+            for debug_adapter in manifest.debug_locators.keys() {
+                this.update(cx, |this, _cx| {
+                    this.proxy
+                        .register_debug_locator(wasm_extension.clone(), debug_adapter.clone());
+                })?;
+            }
         }
 
         Ok(())

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

@@ -200,8 +200,8 @@ impl<P: LinuxClient + 'static> Platform for P {
             app_path = app_path.display()
         );
 
-        // execute the script using /bin/bash
-        let restart_process = Command::new("/bin/bash")
+        let restart_process = Command::new("/usr/bin/env")
+            .arg("bash")
             .arg("-c")
             .arg(script)
             .process_group(0)

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

@@ -1935,12 +1935,14 @@ impl Session {
     }
 
     pub fn continue_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
+        let supports_single_thread_execution_requests =
+            self.capabilities.supports_single_thread_execution_requests;
         self.thread_states.continue_thread(thread_id);
         self.request(
             ContinueCommand {
                 args: ContinueArguments {
                     thread_id: thread_id.0,
-                    single_thread: Some(true),
+                    single_thread: supports_single_thread_execution_requests,
                 },
             },
             Self::on_step_response::<ContinueCommand>(thread_id),

crates/project/src/lsp_store.rs 🔗

@@ -6043,7 +6043,9 @@ impl LspStore {
                     );
                     server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
                 }
-                CompletionSource::BufferWord { .. } | CompletionSource::Custom => {
+                CompletionSource::BufferWord { .. }
+                | CompletionSource::Dap { .. }
+                | CompletionSource::Custom => {
                     return Ok(());
                 }
             }
@@ -6195,7 +6197,9 @@ impl LspStore {
                     }
                     serde_json::to_string(lsp_completion).unwrap().into_bytes()
                 }
-                CompletionSource::Custom | CompletionSource::BufferWord { .. } => {
+                CompletionSource::Custom
+                | CompletionSource::Dap { .. }
+                | CompletionSource::BufferWord { .. } => {
                     return Ok(());
                 }
             }
@@ -11081,6 +11085,10 @@ impl LspStore {
                 serialized_completion.source = proto::completion::Source::Custom as i32;
                 serialized_completion.resolved = true;
             }
+            CompletionSource::Dap { sort_text } => {
+                serialized_completion.source = proto::completion::Source::Dap as i32;
+                serialized_completion.sort_text = Some(sort_text.clone());
+            }
         }
 
         serialized_completion
@@ -11135,6 +11143,11 @@ impl LspStore {
                         resolved: completion.resolved,
                     }
                 }
+                Some(proto::completion::Source::Dap) => CompletionSource::Dap {
+                    sort_text: completion
+                        .sort_text
+                        .context("expected sort text to exist")?,
+                },
                 _ => anyhow::bail!("Unexpected completion source {}", completion.source),
             },
         })

crates/project/src/project.rs 🔗

@@ -456,6 +456,10 @@ pub enum CompletionSource {
         /// Whether this completion has been resolved, to ensure it happens once per completion.
         resolved: bool,
     },
+    Dap {
+        /// The sort text for this completion.
+        sort_text: String,
+    },
     Custom,
     BufferWord {
         word_range: Range<Anchor>,

crates/project_panel/src/project_panel.rs 🔗

@@ -3303,12 +3303,13 @@ impl ProjectPanel {
     fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef<'_>)> {
         let mut offset = 0;
         for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
-            if visible_worktree_entries.len() > offset + index {
+            let current_len = visible_worktree_entries.len();
+            if index < offset + current_len {
                 return visible_worktree_entries
-                    .get(index)
+                    .get(index - offset)
                     .map(|entry| (*worktree_id, entry.to_ref()));
             }
-            offset += visible_worktree_entries.len();
+            offset += current_len;
         }
         None
     }

crates/proto/proto/lsp.proto 🔗

@@ -222,11 +222,13 @@ message Completion {
     optional Anchor buffer_word_end = 10;
     Anchor old_insert_start = 11;
     Anchor old_insert_end = 12;
+    optional string sort_text = 13;
 
     enum Source {
         Lsp = 0;
         Custom = 1;
         BufferWord = 2;
+        Dap = 3;
     }
 }
 

crates/util/src/paths.rs 🔗

@@ -170,6 +170,12 @@ impl<T: AsRef<Path>> From<T> for SanitizedPath {
 pub const FILE_ROW_COLUMN_DELIMITER: char = ':';
 
 const ROW_COL_CAPTURE_REGEX: &str = r"(?xs)
+    ([^\(]+)\:(?:
+        \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column)
+        |
+        \((\d+)\)()     # filename:(row)
+    )
+    |
     ([^\(]+)(?:
         \((\d+)[,:](\d+)\) # filename(row,column), filename(row:column)
         |
@@ -674,6 +680,15 @@ mod tests {
                 column: None
             }
         );
+
+        assert_eq!(
+            PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"),
+            PathWithPosition {
+                path: PathBuf::from("Types.hs"),
+                row: Some(617),
+                column: Some(9),
+            }
+        );
     }
 
     #[test]

docs/src/languages/ruby.md 🔗

@@ -256,7 +256,7 @@ In order to do that, you need to configure the language server so that it knows
     "tailwindcss-language-server": {
       "settings": {
         "includeLanguages": {
-          "erb": "html",
+          "html/erb": "html",
           "ruby": "html"
         },
         "experimental": {
@@ -379,3 +379,22 @@ The Ruby extension provides a debug adapter for debugging Ruby code. Zed's name
   }
 ]
 ```
+
+## Formatters
+
+### `erb-formatter`
+
+To format ERB templates, you can use the `erb-formatter` formatter. This formatter uses the [`erb-formatter`](https://rubygems.org/gems/erb-formatter) gem to format ERB templates.
+
+```jsonc
+{
+  "HTML/ERB": {
+    "formatter": {
+      "external": {
+        "command": "erb-formatter",
+        "arguments": ["--stdin-filename", "{buffer_path}"],
+      },
+    },
+  },
+}
+```

extensions/emmet/extension.toml 🔗

@@ -9,12 +9,13 @@ repository = "https://github.com/zed-industries/zed"
 [language_servers.emmet-language-server]
 name = "Emmet Language Server"
 language = "HTML"
-languages = ["HTML", "PHP", "ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir"]
+languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir"]
 
 [language_servers.emmet-language-server.language_ids]
 "HTML" = "html"
 "PHP" = "php"
 "ERB" = "eruby"
+"HTML/ERB" = "eruby"
 "JavaScript" = "javascriptreact"
 "TSX" = "typescriptreact"
 "CSS" = "css"