Merge branch 'main' into v0.173.x

Joseph T. Lyons created

Change summary

Cargo.lock                                                      |   7 
Cargo.toml                                                      |   1 
crates/assistant/src/inline_assistant.rs                        |   2 
crates/assistant2/src/inline_assistant.rs                       |   2 
crates/editor/src/editor.rs                                     | 222 
crates/editor/src/element.rs                                    |  83 
crates/feedback/src/feedback_modal.rs                           |   2 
crates/git_ui/src/git_panel.rs                                  |  89 
crates/gpui/src/window.rs                                       |  12 
crates/inline_completion_button/src/inline_completion_button.rs |   2 
crates/language_tools/src/lsp_log.rs                            |   4 
crates/project/src/lsp_store.rs                                 | 483 +-
crates/prompt_library/src/prompt_library.rs                     |   4 
crates/zed/src/zed.rs                                           |   2 
crates/zed/src/zed/quick_action_bar.rs                          |   4 
crates/zeta/src/rate_completion_modal.rs                        |   2 
extensions/EXTRACTION.md                                        |  61 
extensions/php/Cargo.toml                                       |  16 
extensions/php/LICENSE-APACHE                                   |   1 
extensions/php/extension.toml                                   |  25 
extensions/php/languages/php/brackets.scm                       |   4 
extensions/php/languages/php/config.toml                        |  18 
extensions/php/languages/php/embedding.scm                      |  36 
extensions/php/languages/php/highlights.scm                     | 137 
extensions/php/languages/php/indents.scm                        |   1 
extensions/php/languages/php/injections.scm                     |  11 
extensions/php/languages/php/outline.scm                        |  46 
extensions/php/languages/php/runnables.scm                      | 105 
extensions/php/languages/php/tags.scm                           |  40 
extensions/php/languages/php/tasks.json                         |  19 
extensions/php/languages/php/textobjects.scm                    |  45 
extensions/php/languages/phpdoc/config.toml                     |   9 
extensions/php/languages/phpdoc/highlights.scm                  |  13 
extensions/php/src/language_servers.rs                          |   5 
extensions/php/src/language_servers/intelephense.rs             | 209 -
extensions/php/src/language_servers/phpactor.rs                 |  85 
extensions/php/src/php.rs                                       |  72 
37 files changed, 467 insertions(+), 1,412 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -16823,13 +16823,6 @@ dependencies = [
  "zed_extension_api 0.1.0",
 ]
 
-[[package]]
-name = "zed_php"
-version = "0.2.4"
-dependencies = [
- "zed_extension_api 0.1.0",
-]
-
 [[package]]
 name = "zed_proto"
 version = "0.2.1"

Cargo.toml 🔗

@@ -171,7 +171,6 @@ members = [
     "extensions/haskell",
     "extensions/html",
     "extensions/lua",
-    "extensions/php",
     "extensions/perplexity",
     "extensions/proto",
     "extensions/purescript",

crates/assistant/src/inline_assistant.rs 🔗

@@ -1255,7 +1255,7 @@ impl InlineAssistant {
                     editor.scroll_manager.set_forbid_vertical_scroll(true);
                     editor.set_show_scrollbars(false, cx);
                     editor.set_read_only(true);
-                    editor.set_show_inline_completions(Some(false), window, cx);
+                    editor.set_show_edit_predictions(Some(false), window, cx);
                     editor.highlight_rows::<DeletedLines>(
                         Anchor::min()..Anchor::max(),
                         cx.theme().status().deleted_background,

crates/assistant2/src/inline_assistant.rs 🔗

@@ -1345,7 +1345,7 @@ impl InlineAssistant {
                     editor.scroll_manager.set_forbid_vertical_scroll(true);
                     editor.set_show_scrollbars(false, cx);
                     editor.set_read_only(true);
-                    editor.set_show_inline_completions(Some(false), window, cx);
+                    editor.set_show_edit_predictions(Some(false), window, cx);
                     editor.highlight_rows::<DeletedLines>(
                         Anchor::min()..Anchor::max(),
                         cx.theme().status().deleted_background,

crates/editor/src/editor.rs 🔗

@@ -190,6 +190,7 @@ pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
 pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
 
+pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
 pub(crate) const EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT: &str =
     "edit_prediction_requires_modifier";
 
@@ -497,6 +498,23 @@ struct InlineCompletionState {
     invalidation_range: Range<Anchor>,
 }
 
+enum EditPredictionSettings {
+    Disabled,
+    Enabled {
+        show_in_menu: bool,
+        preview_requires_modifier: bool,
+    },
+}
+
+impl EditPredictionSettings {
+    pub fn is_enabled(&self) -> bool {
+        match self {
+            EditPredictionSettings::Disabled => false,
+            EditPredictionSettings::Enabled { .. } => true,
+        }
+    }
+}
+
 enum InlineCompletionHighlight {}
 
 pub enum MenuInlineCompletionsPolicy {
@@ -683,6 +701,7 @@ pub struct Editor {
     active_inline_completion: Option<InlineCompletionState>,
     /// Used to prevent flickering as the user types while the menu is open
     stale_inline_completion_in_menu: Option<InlineCompletionState>,
+    edit_prediction_settings: EditPredictionSettings,
     inline_completions_hidden_for_vim_mode: bool,
     show_inline_completions_override: Option<bool>,
     menu_inline_completions_policy: MenuInlineCompletionsPolicy,
@@ -1395,6 +1414,7 @@ impl Editor {
             inline_completions_hidden_for_vim_mode: false,
             show_inline_completions_override: None,
             menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider,
+            edit_prediction_settings: EditPredictionSettings::Disabled,
             custom_context_menu: None,
             show_git_blame_gutter: false,
             show_git_blame_inline: false,
@@ -1469,13 +1489,13 @@ impl Editor {
         this
     }
 
-    pub fn mouse_menu_is_focused(&self, window: &mut Window, cx: &mut App) -> bool {
+    pub fn mouse_menu_is_focused(&self, window: &Window, cx: &App) -> bool {
         self.mouse_context_menu
             .as_ref()
             .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window))
     }
 
-    fn key_context(&self, window: &mut Window, cx: &mut Context<Self>) -> KeyContext {
+    fn key_context(&self, window: &Window, cx: &App) -> KeyContext {
         let mut key_context = KeyContext::new_with_defaults();
         key_context.add("Editor");
         let mode = match self.mode {
@@ -1528,9 +1548,9 @@ impl Editor {
 
         if self.has_active_inline_completion() {
             key_context.add("copilot_suggestion");
-            key_context.add("edit_prediction");
+            key_context.add(EDIT_PREDICTION_KEY_CONTEXT);
 
-            if showing_completions || self.edit_prediction_requires_modifier(cx) {
+            if showing_completions || self.edit_prediction_requires_modifier() {
                 key_context.add(EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT);
             }
         }
@@ -1542,6 +1562,22 @@ impl Editor {
         key_context
     }
 
+    pub fn accept_edit_prediction_keybind(
+        &self,
+        window: &Window,
+        cx: &App,
+    ) -> AcceptEditPredictionBinding {
+        let mut context = self.key_context(window, cx);
+        context.add(EDIT_PREDICTION_KEY_CONTEXT);
+
+        AcceptEditPredictionBinding(
+            window
+                .bindings_for_action_in_context(&AcceptEditPrediction, context)
+                .into_iter()
+                .next(),
+        )
+    }
+
     pub fn new_file(
         workspace: &mut Workspace,
         _: &workspace::NewFile,
@@ -1886,23 +1922,14 @@ impl Editor {
         cx: &mut Context<Self>,
     ) {
         if self.show_inline_completions_override.is_some() {
-            self.set_show_inline_completions(None, window, cx);
+            self.set_show_edit_predictions(None, window, cx);
         } else {
-            let cursor = self.selections.newest_anchor().head();
-            if let Some((buffer, cursor_buffer_position)) =
-                self.buffer.read(cx).text_anchor_for_position(cursor, cx)
-            {
-                let show_inline_completions = !self.should_show_inline_completions_in_buffer(
-                    &buffer,
-                    cursor_buffer_position,
-                    cx,
-                );
-                self.set_show_inline_completions(Some(show_inline_completions), window, cx);
-            }
+            let show_edit_predictions = !self.edit_predictions_enabled();
+            self.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
         }
     }
 
-    pub fn set_show_inline_completions(
+    pub fn set_show_edit_predictions(
         &mut self,
         show_edit_predictions: Option<bool>,
         window: &mut Window,
@@ -3019,7 +3046,7 @@ impl Editor {
             }
 
             let trigger_in_words =
-                this.show_edit_predictions_in_menu(cx) || !had_active_inline_completion;
+                this.show_edit_predictions_in_menu() || !had_active_inline_completion;
             this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
             linked_editing_ranges::refresh_linked_ranges(this, window, cx);
             this.refresh_inline_completion(true, false, window, cx);
@@ -3912,7 +3939,7 @@ impl Editor {
                         *editor.context_menu.borrow_mut() =
                             Some(CodeContextMenu::Completions(menu));
 
-                        if editor.show_edit_predictions_in_menu(cx) {
+                        if editor.show_edit_predictions_in_menu() {
                             editor.update_visible_inline_completion(window, cx);
                         } else {
                             editor.discard_inline_completion(false, cx);
@@ -3926,7 +3953,7 @@ impl Editor {
                         // If it was already hidden and we don't show inline
                         // completions in the menu, we should also show the
                         // inline-completion when available.
-                        if was_hidden && editor.show_edit_predictions_in_menu(cx) {
+                        if was_hidden && editor.show_edit_predictions_in_menu() {
                             editor.update_visible_inline_completion(window, cx);
                         }
                     }
@@ -3976,7 +4003,7 @@ impl Editor {
 
         let entries = completions_menu.entries.borrow();
         let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?;
-        if self.show_edit_predictions_in_menu(cx) {
+        if self.show_edit_predictions_in_menu() {
             self.discard_inline_completion(true, cx);
         }
         let candidate_id = mat.candidate_id;
@@ -4668,7 +4695,7 @@ impl Editor {
         }
 
         if !user_requested
-            && (!self.should_show_inline_completions_in_buffer(&buffer, cursor_buffer_position, cx)
+            && (!self.should_show_edit_predictions()
                 || !self.is_focused(window)
                 || buffer.read(cx).is_empty())
         {
@@ -4687,58 +4714,79 @@ impl Editor {
         Some(())
     }
 
-    pub fn should_show_inline_completions(&self, cx: &App) -> bool {
-        let cursor = self.selections.newest_anchor().head();
-        if let Some((buffer, cursor_position)) =
-            self.buffer.read(cx).text_anchor_for_position(cursor, cx)
-        {
-            self.should_show_inline_completions_in_buffer(&buffer, cursor_position, cx)
-        } else {
-            false
+    fn show_edit_predictions_in_menu(&self) -> bool {
+        match self.edit_prediction_settings {
+            EditPredictionSettings::Disabled => false,
+            EditPredictionSettings::Enabled { show_in_menu, .. } => show_in_menu,
         }
     }
 
-    fn edit_prediction_requires_modifier(&self, cx: &App) -> bool {
-        let cursor = self.selections.newest_anchor().head();
+    pub fn edit_predictions_enabled(&self) -> bool {
+        match self.edit_prediction_settings {
+            EditPredictionSettings::Disabled => false,
+            EditPredictionSettings::Enabled { .. } => true,
+        }
+    }
 
-        self.buffer
-            .read(cx)
-            .text_anchor_for_position(cursor, cx)
-            .map(|(buffer, _)| {
-                all_language_settings(buffer.read(cx).file(), cx).inline_completions_preview_mode()
-                    == InlineCompletionPreviewMode::WhenHoldingModifier
-            })
-            .unwrap_or(false)
+    fn edit_prediction_requires_modifier(&self) -> bool {
+        match self.edit_prediction_settings {
+            EditPredictionSettings::Disabled => false,
+            EditPredictionSettings::Enabled {
+                preview_requires_modifier,
+                ..
+            } => preview_requires_modifier,
+        }
     }
 
-    fn should_show_inline_completions_in_buffer(
+    fn edit_prediction_settings_at_position(
         &self,
         buffer: &Entity<Buffer>,
         buffer_position: language::Anchor,
         cx: &App,
-    ) -> bool {
-        if !self.snippet_stack.is_empty() {
-            return false;
+    ) -> EditPredictionSettings {
+        if self.mode != EditorMode::Full
+            || !self.show_inline_completions_override.unwrap_or(true)
+            || self.inline_completions_disabled_in_scope(buffer, buffer_position, cx)
+        {
+            return EditPredictionSettings::Disabled;
         }
 
-        if self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) {
-            return false;
-        }
+        let buffer = buffer.read(cx);
 
-        if let Some(show_inline_completions) = self.show_inline_completions_override {
-            show_inline_completions
-        } else {
-            let buffer = buffer.read(cx);
-            self.mode == EditorMode::Full
-                && language_settings(
-                    buffer.language_at(buffer_position).map(|l| l.name()),
-                    buffer.file(),
-                    cx,
-                )
-                .show_edit_predictions
+        let file = buffer.file();
+
+        if !language_settings(buffer.language().map(|l| l.name()), file, cx).show_edit_predictions {
+            return EditPredictionSettings::Disabled;
+        };
+
+        let by_provider = matches!(
+            self.menu_inline_completions_policy,
+            MenuInlineCompletionsPolicy::ByProvider
+        );
+
+        let show_in_menu = by_provider
+            && EditorSettings::get_global(cx).show_edit_predictions_in_menu
+            && self
+                .edit_prediction_provider
+                .as_ref()
+                .map_or(false, |provider| {
+                    provider.provider.show_completions_in_menu()
+                });
+
+        let preview_requires_modifier = all_language_settings(file, cx)
+            .inline_completions_preview_mode()
+            == InlineCompletionPreviewMode::WhenHoldingModifier;
+
+        EditPredictionSettings::Enabled {
+            show_in_menu,
+            preview_requires_modifier,
         }
     }
 
+    fn should_show_edit_predictions(&self) -> bool {
+        self.snippet_stack.is_empty() && self.edit_predictions_enabled()
+    }
+
     pub fn inline_completions_enabled(&self, cx: &App) -> bool {
         let cursor = self.selections.newest_anchor().head();
         if let Some((buffer, cursor_position)) =
@@ -4781,9 +4829,7 @@ impl Editor {
         let cursor = self.selections.newest_anchor().head();
         let (buffer, cursor_buffer_position) =
             self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
-        if self.inline_completions_hidden_for_vim_mode
-            || !self.should_show_inline_completions_in_buffer(&buffer, cursor_buffer_position, cx)
-        {
+        if self.inline_completions_hidden_for_vim_mode || !self.should_show_edit_predictions() {
             return None;
         }
 
@@ -4889,7 +4935,7 @@ impl Editor {
             }
         }
 
-        if self.show_edit_predictions_in_menu(cx) {
+        if self.show_edit_predictions_in_menu() {
             self.hide_context_menu(window, cx);
         }
 
@@ -5076,14 +5122,10 @@ impl Editor {
     /// Returns true when we're displaying the inline completion popover below the cursor
     /// like we are not previewing and the LSP autocomplete menu is visible
     /// or we are in `when_holding_modifier` mode.
-    pub fn inline_completion_visible_in_cursor_popover(
-        &self,
-        has_completion: bool,
-        cx: &App,
-    ) -> bool {
+    pub fn edit_prediction_visible_in_cursor_popover(&self, has_completion: bool) -> bool {
         if self.previewing_inline_completion
-            || !self.show_edit_predictions_in_menu(cx)
-            || !self.should_show_inline_completions(cx)
+            || !self.show_edit_predictions_in_menu()
+            || !self.edit_predictions_enabled()
         {
             return false;
         }
@@ -5092,7 +5134,7 @@ impl Editor {
             return true;
         }
 
-        has_completion && self.edit_prediction_requires_modifier(cx)
+        has_completion && self.edit_prediction_requires_modifier()
     }
 
     fn handle_modifiers_changed(
@@ -5102,9 +5144,8 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        if !self.show_edit_predictions_in_menu(cx) {
-            let accept_binding =
-                AcceptEditPredictionBinding::resolve(self.focus_handle(cx), window);
+        if self.show_edit_predictions_in_menu() {
+            let accept_binding = self.accept_edit_prediction_keybind(window, cx);
             if let Some(accept_keystroke) = accept_binding.keystroke() {
                 let was_previewing_inline_completion = self.previewing_inline_completion;
                 self.previewing_inline_completion = modifiers == accept_keystroke.modifiers
@@ -5140,10 +5181,11 @@ impl Editor {
         let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
         let excerpt_id = cursor.excerpt_id;
 
-        let show_in_menu = self.show_edit_predictions_in_menu(cx);
+        let show_in_menu = self.show_edit_predictions_in_menu();
         let completions_menu_has_precedence = !show_in_menu
             && (self.context_menu.borrow().is_some()
                 || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion()));
+
         if completions_menu_has_precedence
             || !offset_selection.is_empty()
             || self
@@ -5160,11 +5202,22 @@ impl Editor {
         }
 
         self.take_active_inline_completion(cx);
-        let provider = self.edit_prediction_provider()?;
+        let Some(provider) = self.edit_prediction_provider() else {
+            self.edit_prediction_settings = EditPredictionSettings::Disabled;
+            return None;
+        };
 
         let (buffer, cursor_buffer_position) =
             self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
 
+        self.edit_prediction_settings =
+            self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx);
+
+        if !self.edit_prediction_settings.is_enabled() {
+            self.discard_inline_completion(false, cx);
+            return None;
+        }
+
         let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?;
         let edits = inline_completion
             .edits
@@ -5223,8 +5276,7 @@ impl Editor {
                 snapshot,
             }
         } else {
-            let show_completions_in_buffer = !self
-                .inline_completion_visible_in_cursor_popover(true, cx)
+            let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true)
                 && !self.inline_completions_hidden_for_vim_mode;
             if show_completions_in_buffer {
                 if edits
@@ -5300,19 +5352,6 @@ impl Editor {
         Some(self.edit_prediction_provider.as_ref()?.provider.clone())
     }
 
-    fn show_edit_predictions_in_menu(&self, cx: &App) -> bool {
-        let by_provider = matches!(
-            self.menu_inline_completions_policy,
-            MenuInlineCompletionsPolicy::ByProvider
-        );
-
-        by_provider
-            && EditorSettings::get_global(cx).show_edit_predictions_in_menu
-            && self
-                .edit_prediction_provider()
-                .map_or(false, |provider| provider.show_completions_in_menu())
-    }
-
     fn render_code_actions_indicator(
         &self,
         _style: &EditorStyle,
@@ -14385,7 +14424,8 @@ impl Editor {
         });
         supports
     }
-    pub fn is_focused(&self, window: &mut Window) -> bool {
+
+    pub fn is_focused(&self, window: &Window) -> bool {
         self.focus_handle.is_focused(window)
     }
 

crates/editor/src/element.rs 🔗

@@ -34,12 +34,12 @@ use gpui::{
     anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
     relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds,
     ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
-    Edges, Element, ElementInputHandler, Entity, FocusHandle, Focusable as _, FontId,
-    GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, KeyBindingContextPredicate,
-    Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
-    MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
-    SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun,
-    TextStyleRefinement, WeakEntity, Window,
+    Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
+    Hsla, InteractiveElement, IntoElement, KeyBindingContextPredicate, Keystroke, Length,
+    ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
+    ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
+    StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement,
+    WeakEntity, Window,
 };
 use itertools::Itertools;
 use language::{
@@ -3088,10 +3088,9 @@ impl EditorElement {
 
         {
             let editor = self.editor.read(cx);
-            if editor.inline_completion_visible_in_cursor_popover(
-                editor.has_active_inline_completion(),
-                cx,
-            ) {
+            if editor
+                .edit_prediction_visible_in_cursor_popover(editor.has_active_inline_completion())
+            {
                 height_above_menu +=
                     editor.edit_prediction_cursor_popover_height() + POPOVER_Y_PADDING;
                 edit_prediction_popover_visible = true;
@@ -3168,10 +3167,8 @@ impl EditorElement {
                 );
 
                 let edit_prediction = if edit_prediction_popover_visible {
-                    let accept_binding =
-                        AcceptEditPredictionBinding::resolve(self.editor.focus_handle(cx), window);
-
                     self.editor.update(cx, move |editor, cx| {
+                        let accept_binding = editor.accept_edit_prediction_keybind(window, cx);
                         let mut element = editor.render_edit_prediction_cursor_popover(
                             min_width,
                             max_width,
@@ -3557,7 +3554,7 @@ impl EditorElement {
         let editor = self.editor.read(cx);
         let active_inline_completion = editor.active_inline_completion.as_ref()?;
 
-        if editor.inline_completion_visible_in_cursor_popover(true, cx) {
+        if editor.edit_prediction_visible_in_cursor_popover(true) {
             return None;
         }
 
@@ -3570,7 +3567,7 @@ impl EditorElement {
                         "Jump to Edit",
                         Some(IconName::ArrowUp),
                         previewing,
-                        self.editor.focus_handle(cx),
+                        editor,
                         window,
                         cx,
                     )?;
@@ -3583,7 +3580,7 @@ impl EditorElement {
                         "Jump to Edit",
                         Some(IconName::ArrowDown),
                         previewing,
-                        self.editor.focus_handle(cx),
+                        editor,
                         window,
                         cx,
                     )?;
@@ -3599,7 +3596,7 @@ impl EditorElement {
                         "Jump to Edit",
                         None,
                         previewing,
-                        self.editor.focus_handle(cx),
+                        editor,
                         window,
                         cx,
                     )?;
@@ -3658,26 +3655,23 @@ impl EditorElement {
                             target_display_point.row(),
                             editor_snapshot.line_len(target_display_point.row()),
                         );
-                        let (previewing_inline_completion, origin) =
-                            self.editor.update(cx, |editor, _cx| {
-                                Some((
+                        let (mut element, origin) = self.editor.update(cx, |editor, cx| {
+                            Some((
+                                inline_completion_accept_indicator(
+                                    "Accept",
+                                    None,
                                     editor.previewing_inline_completion,
-                                    editor.display_to_pixel_point(
-                                        target_line_end,
-                                        editor_snapshot,
-                                        window,
-                                    )?,
-                                ))
-                            })?;
-
-                        let mut element = inline_completion_accept_indicator(
-                            "Accept",
-                            None,
-                            previewing_inline_completion,
-                            self.editor.focus_handle(cx),
-                            window,
-                            cx,
-                        )?;
+                                    editor,
+                                    window,
+                                    cx,
+                                )?,
+                                editor.display_to_pixel_point(
+                                    target_line_end,
+                                    editor_snapshot,
+                                    window,
+                                )?,
+                            ))
+                        })?;
 
                         element.prepaint_as_root(
                             text_bounds.origin + origin + point(PADDING_X, px(0.)),
@@ -5676,11 +5670,11 @@ fn inline_completion_accept_indicator(
     label: impl Into<SharedString>,
     icon: Option<IconName>,
     previewing: bool,
-    editor_focus_handle: FocusHandle,
-    window: &Window,
+    editor: &Editor,
+    window: &mut Window,
     cx: &App,
 ) -> Option<AnyElement> {
-    let accept_binding = AcceptEditPredictionBinding::resolve(editor_focus_handle, window);
+    let accept_binding = editor.accept_edit_prediction_keybind(window, cx);
     let accept_keystroke = accept_binding.keystroke()?;
 
     let accept_key = h_flex()
@@ -5729,18 +5723,9 @@ fn inline_completion_accept_indicator(
     )
 }
 
-pub struct AcceptEditPredictionBinding(Option<gpui::KeyBinding>);
+pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
 
 impl AcceptEditPredictionBinding {
-    pub fn resolve(editor_focus_handle: FocusHandle, window: &Window) -> Self {
-        AcceptEditPredictionBinding(
-            window
-                .bindings_for_action_in(&AcceptEditPrediction, &editor_focus_handle)
-                .into_iter()
-                .next(),
-        )
-    }
-
     pub fn keystroke(&self) -> Option<&Keystroke> {
         if let Some(binding) = self.0.as_ref() {
             match &binding.keystrokes() {

crates/feedback/src/feedback_modal.rs 🔗

@@ -191,7 +191,7 @@ impl FeedbackModal {
             );
             editor.set_show_gutter(false, cx);
             editor.set_show_indent_guides(false, cx);
-            editor.set_show_inline_completions(Some(false), window, cx);
+            editor.set_show_edit_predictions(Some(false), window, cx);
             editor.set_vertical_scroll_margin(5, cx);
             editor.set_use_modal_editing(false);
             editor.set_soft_wrap();

crates/git_ui/src/git_panel.rs 🔗

@@ -176,23 +176,21 @@ pub struct GitPanel {
 }
 
 fn commit_message_editor(
-    commit_message_buffer: Option<Entity<Buffer>>,
+    commit_message_buffer: Entity<Buffer>,
+    project: Entity<Project>,
     window: &mut Window,
     cx: &mut Context<'_, Editor>,
 ) -> Editor {
-    let mut commit_editor = if let Some(commit_message_buffer) = commit_message_buffer {
-        let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
-        Editor::new(
-            EditorMode::AutoHeight { max_lines: 6 },
-            buffer,
-            None,
-            false,
-            window,
-            cx,
-        )
-    } else {
-        Editor::auto_height(6, window, cx)
-    };
+    let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
+    let mut commit_editor = Editor::new(
+        EditorMode::AutoHeight { max_lines: 6 },
+        buffer,
+        None,
+        false,
+        window,
+        cx,
+    );
+    commit_editor.set_collaboration_hub(Box::new(project));
     commit_editor.set_use_autoclose(false);
     commit_editor.set_show_gutter(false, cx);
     commit_editor.set_show_wrap_guides(false, cx);
@@ -205,7 +203,6 @@ impl GitPanel {
     pub fn new(
         workspace: &mut Workspace,
         window: &mut Window,
-        commit_message_buffer: Option<Entity<Buffer>>,
         cx: &mut Context<Workspace>,
     ) -> Entity<Self> {
         let fs = workspace.app_state().fs.clone();
@@ -222,8 +219,11 @@ impl GitPanel {
             })
             .detach();
 
+            // just to let us render a placeholder editor.
+            // Once the active git repo is set, this buffer will be replaced.
+            let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
             let commit_editor =
-                cx.new(|cx| commit_message_editor(commit_message_buffer, window, cx));
+                cx.new(|cx| commit_message_editor(temporary_buffer, project.clone(), window, cx));
             commit_editor.update(cx, |editor, cx| {
                 editor.clear(window, cx);
             });
@@ -773,21 +773,35 @@ impl GitPanel {
             })
             .collect::<HashSet<_>>();
 
-        let new_co_authors = room
-            .read(cx)
-            .remote_participants()
-            .values()
-            .filter(|participant| participant.can_write())
-            .map(|participant| participant.user.as_ref())
-            .filter_map(|user| {
-                let email = user.email.as_deref()?;
-                let name = user.name.as_deref().unwrap_or(&user.github_login);
-                Some(format!("{CO_AUTHOR_PREFIX}{name} <{email}>"))
-            })
-            .filter(|co_author| {
-                !existing_co_authors.contains(co_author.to_ascii_lowercase().as_str())
-            })
-            .collect::<Vec<_>>();
+        let project = self.project.read(cx);
+        let room = room.read(cx);
+        let mut new_co_authors = Vec::new();
+
+        for (peer_id, collaborator) in project.collaborators() {
+            if collaborator.is_host {
+                continue;
+            }
+
+            let Some(participant) = room.remote_participant_for_peer_id(*peer_id) else {
+                continue;
+            };
+            if participant.can_write() && participant.user.email.is_some() {
+                let email = participant.user.email.clone().unwrap();
+
+                if !existing_co_authors.contains(&email.as_ref()) {
+                    new_co_authors.push((participant.user.github_login.clone(), email))
+                }
+            }
+        }
+        if !project.is_local() && !project.is_read_only(cx) {
+            if let Some(user) = room.local_participant_user(cx) {
+                if let Some(email) = user.email.clone() {
+                    if !existing_co_authors.contains(&email.as_ref()) {
+                        new_co_authors.push((user.github_login.clone(), email.clone()))
+                    }
+                }
+            }
+        }
         if new_co_authors.is_empty() {
             return;
         }
@@ -798,9 +812,13 @@ impl GitPanel {
             if !ends_with_co_authors {
                 edit.push('\n');
             }
-            for co_author in new_co_authors {
+            for (name, email) in new_co_authors {
                 edit.push('\n');
-                edit.push_str(&co_author);
+                edit.push_str(CO_AUTHOR_PREFIX);
+                edit.push_str(&name);
+                edit.push_str(" <");
+                edit.push_str(&email);
+                edit.push('>');
             }
 
             editor.edit(Some((editor_end..editor_end, edit)), cx);
@@ -857,8 +875,9 @@ impl GitPanel {
                     .as_ref()
                     != Some(&buffer)
                 {
-                    git_panel.commit_editor =
-                        cx.new(|cx| commit_message_editor(Some(buffer), window, cx));
+                    git_panel.commit_editor = cx.new(|cx| {
+                        commit_message_editor(buffer, git_panel.project.clone(), window, cx)
+                    });
                 }
             })
         })

crates/gpui/src/window.rs 🔗

@@ -3671,6 +3671,18 @@ impl Window {
         dispatch_tree.bindings_for_action(action, &context_stack)
     }
 
+    /// Returns the key bindings for the given action in the given context.
+    pub fn bindings_for_action_in_context(
+        &self,
+        action: &dyn Action,
+        context: KeyContext,
+    ) -> Vec<KeyBinding> {
+        let dispatch_tree = &self.rendered_frame.dispatch_tree;
+        dispatch_tree.bindings_for_action(action, &[context])
+    }
+
+    /// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle.
+
     /// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle.
     pub fn listener_for<V: Render, E>(
         &self,

crates/inline_completion_button/src/inline_completion_button.rs 🔗

@@ -611,7 +611,7 @@ impl InlineCompletionButton {
                 .unwrap_or(true),
             )
         };
-        self.editor_show_predictions = editor.should_show_inline_completions(cx);
+        self.editor_show_predictions = editor.edit_predictions_enabled();
         self.edit_prediction_provider = editor.edit_prediction_provider();
         self.language = language.cloned();
         self.file = file;

crates/language_tools/src/lsp_log.rs 🔗

@@ -708,7 +708,7 @@ impl LspLogView {
             editor.set_text(log_contents, window, cx);
             editor.move_to_end(&MoveToEnd, window, cx);
             editor.set_read_only(true);
-            editor.set_show_inline_completions(Some(false), window, cx);
+            editor.set_show_edit_predictions(Some(false), window, cx);
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor
         });
@@ -751,7 +751,7 @@ impl LspLogView {
             );
             editor.set_text(server_info, window, cx);
             editor.set_read_only(true);
-            editor.set_show_inline_completions(Some(false), window, cx);
+            editor.set_show_edit_predictions(Some(false), window, cx);
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor
         });

crates/project/src/lsp_store.rs 🔗

@@ -76,7 +76,7 @@ use std::{
     sync::Arc,
     time::{Duration, Instant},
 };
-use text::{Anchor, BufferId, LineEnding, OffsetRangeExt};
+use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, TransactionId};
 use util::{
     debug_panic, defer, maybe, merge_json_value_into, paths::SanitizedPath, post_inc, ResultExt,
     TryFutureExt as _,
@@ -1098,7 +1098,6 @@ impl LocalLspStore {
     async fn format_locally(
         lsp_store: WeakEntity<LspStore>,
         mut buffers: Vec<FormattableBuffer>,
-        target: &LspFormatTarget,
         push_to_history: bool,
         trigger: FormatTrigger,
         mut cx: AsyncApp,
@@ -1131,25 +1130,16 @@ impl LocalLspStore {
 
         let mut project_transaction = ProjectTransaction::default();
         for buffer in &buffers {
-            let (primary_adapter_and_server, adapters_and_servers) =
-                lsp_store.update(&mut cx, |lsp_store, cx| {
-                    let buffer = buffer.handle.read(cx);
-
-                    let adapters_and_servers = lsp_store
-                        .as_local()
-                        .unwrap()
-                        .language_servers_for_buffer(buffer, cx)
-                        .map(|(adapter, lsp)| (adapter.clone(), lsp.clone()))
-                        .collect::<Vec<_>>();
-
-                    let primary_adapter = lsp_store
-                        .as_local()
-                        .unwrap()
-                        .primary_language_server_for_buffer(buffer, cx)
-                        .map(|(adapter, lsp)| (adapter.clone(), lsp.clone()));
+            let adapters_and_servers = lsp_store.update(&mut cx, |lsp_store, cx| {
+                let buffer = buffer.handle.read(cx);
 
-                    (primary_adapter, adapters_and_servers)
-                })?;
+                lsp_store
+                    .as_local()
+                    .unwrap()
+                    .language_servers_for_buffer(buffer, cx)
+                    .map(|(adapter, lsp)| (adapter.clone(), lsp.clone()))
+                    .collect::<Vec<_>>()
+            })?;
 
             let settings = buffer.handle.update(&mut cx, |buffer, cx| {
                 language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
@@ -1182,6 +1172,8 @@ impl LocalLspStore {
                 buffer.end_transaction(cx)
             })?;
 
+            let initial_transaction_id = whitespace_transaction_id;
+
             // Apply the `code_actions_on_format` before we run the formatter.
             let code_actions = deserialize_code_actions(&settings.code_actions_on_format);
             #[allow(clippy::nonminimal_bool)]
@@ -1200,278 +1192,99 @@ impl LocalLspStore {
                 .await?;
             }
 
-            // Apply language-specific formatting using either the primary language server
-            // or external command.
-            // Except for code actions, which are applied with all connected language servers.
-            let primary_language_server =
-                primary_adapter_and_server.map(|(_adapter, server)| server.clone());
-            let primary_server_and_path = primary_language_server
-                .as_ref()
-                .zip(buffer.abs_path.as_ref());
-
             let prettier_settings = buffer.handle.read_with(&cx, |buffer, cx| {
                 language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
                     .prettier
                     .clone()
             })?;
 
-            let ranges = match target {
-                LspFormatTarget::Buffers => None,
-                LspFormatTarget::Ranges(ranges) => {
-                    let Some(ranges) = ranges.get(&buffer.id) else {
-                        return Err(anyhow!("No format ranges provided for buffer"));
-                    };
-                    Some(ranges)
-                }
-            };
-
-            let mut format_operations: Vec<FormatOperation> = vec![];
-            {
-                match trigger {
-                    FormatTrigger::Save => {
-                        match &settings.format_on_save {
-                            FormatOnSave::Off => {
-                                // nothing
-                            }
-                            FormatOnSave::On => {
-                                match &settings.formatter {
-                                    SelectedFormatter::Auto => {
-                                        // do the auto-format: prefer prettier, fallback to primary language server
-                                        let diff = {
-                                            if prettier_settings.allowed {
-                                                Self::perform_format(
-                                                    &Formatter::Prettier,
-                                                    buffer,
-                                                    ranges,
-                                                    primary_server_and_path,
-                                                    lsp_store.clone(),
-                                                    &settings,
-                                                    &adapters_and_servers,
-                                                    push_to_history,
-                                                    &mut project_transaction,
-                                                    &mut cx,
-                                                )
-                                                .await
-                                            } else {
-                                                Self::perform_format(
-                                                    &Formatter::LanguageServer { name: None },
-                                                    buffer,
-                                                    ranges,
-                                                    primary_server_and_path,
-                                                    lsp_store.clone(),
-                                                    &settings,
-                                                    &adapters_and_servers,
-                                                    push_to_history,
-                                                    &mut project_transaction,
-                                                    &mut cx,
-                                                )
-                                                .await
-                                            }
-                                        }?;
-
-                                        if let Some(op) = diff {
-                                            format_operations.push(op);
-                                        }
-                                    }
-                                    SelectedFormatter::List(formatters) => {
-                                        for formatter in formatters.as_ref() {
-                                            let diff = Self::perform_format(
-                                                formatter,
-                                                buffer,
-                                                ranges,
-                                                primary_server_and_path,
-                                                lsp_store.clone(),
-                                                &settings,
-                                                &adapters_and_servers,
-                                                push_to_history,
-                                                &mut project_transaction,
-                                                &mut cx,
-                                            )
-                                            .await?;
-                                            if let Some(op) = diff {
-                                                format_operations.push(op);
-                                            }
-
-                                            // format with formatter
-                                        }
-                                    }
-                                }
-                            }
-                            FormatOnSave::List(formatters) => {
-                                for formatter in formatters.as_ref() {
-                                    let diff = Self::perform_format(
-                                        formatter,
-                                        buffer,
-                                        ranges,
-                                        primary_server_and_path,
-                                        lsp_store.clone(),
-                                        &settings,
-                                        &adapters_and_servers,
-                                        push_to_history,
-                                        &mut project_transaction,
-                                        &mut cx,
-                                    )
-                                    .await?;
-                                    if let Some(op) = diff {
-                                        format_operations.push(op);
-                                    }
-                                }
-                            }
-                        }
-                    }
-                    FormatTrigger::Manual => {
-                        match &settings.formatter {
-                            SelectedFormatter::Auto => {
-                                // do the auto-format: prefer prettier, fallback to primary language server
-                                let diff = {
-                                    if prettier_settings.allowed {
-                                        Self::perform_format(
-                                            &Formatter::Prettier,
-                                            buffer,
-                                            ranges,
-                                            primary_server_and_path,
-                                            lsp_store.clone(),
-                                            &settings,
-                                            &adapters_and_servers,
-                                            push_to_history,
-                                            &mut project_transaction,
-                                            &mut cx,
-                                        )
-                                        .await
-                                    } else {
-                                        let formatter = Formatter::LanguageServer {
-                                            name: primary_language_server
-                                                .as_ref()
-                                                .map(|server| server.name().to_string()),
-                                        };
-                                        Self::perform_format(
-                                            &formatter,
-                                            buffer,
-                                            ranges,
-                                            primary_server_and_path,
-                                            lsp_store.clone(),
-                                            &settings,
-                                            &adapters_and_servers,
-                                            push_to_history,
-                                            &mut project_transaction,
-                                            &mut cx,
-                                        )
-                                        .await
-                                    }
-                                }?;
-
-                                if let Some(op) = diff {
-                                    format_operations.push(op)
-                                }
-                            }
-                            SelectedFormatter::List(formatters) => {
-                                for formatter in formatters.as_ref() {
-                                    // format with formatter
-                                    let diff = Self::perform_format(
-                                        formatter,
-                                        buffer,
-                                        ranges,
-                                        primary_server_and_path,
-                                        lsp_store.clone(),
-                                        &settings,
-                                        &adapters_and_servers,
-                                        push_to_history,
-                                        &mut project_transaction,
-                                        &mut cx,
-                                    )
-                                    .await?;
-                                    if let Some(op) = diff {
-                                        format_operations.push(op);
-                                    }
-                                }
+            let formatters = match (trigger, &settings.format_on_save) {
+                (FormatTrigger::Save, FormatOnSave::Off) => &[],
+                (FormatTrigger::Save, FormatOnSave::List(formatters)) => formatters.as_ref(),
+                (FormatTrigger::Manual, _) | (FormatTrigger::Save, FormatOnSave::On) => {
+                    match &settings.formatter {
+                        SelectedFormatter::Auto => {
+                            if prettier_settings.allowed {
+                                std::slice::from_ref(&Formatter::Prettier)
+                            } else {
+                                std::slice::from_ref(&Formatter::LanguageServer { name: None })
                             }
                         }
+                        SelectedFormatter::List(formatter_list) => formatter_list.as_ref(),
                     }
                 }
-            }
-
-            buffer.handle.update(&mut cx, |b, cx| {
-                // If the buffer had its whitespace formatted and was edited while the language-specific
-                // formatting was being computed, avoid applying the language-specific formatting, because
-                // it can't be grouped with the whitespace formatting in the undo history.
-                if let Some(transaction_id) = whitespace_transaction_id {
-                    if b.peek_undo_stack()
-                        .map_or(true, |e| e.transaction_id() != transaction_id)
-                    {
-                        format_operations.clear();
-                    }
-                }
-
-                // Apply any language-specific formatting, and group the two formatting operations
-                // in the buffer's undo history.
-                for operation in format_operations {
-                    match operation {
-                        FormatOperation::Lsp(edits) => {
-                            b.edit(edits, None, cx);
-                        }
-                        FormatOperation::External(diff) => {
-                            b.apply_diff(diff, cx);
-                        }
-                        FormatOperation::Prettier(diff) => {
-                            b.apply_diff(diff, cx);
-                        }
-                    }
-
-                    if let Some(transaction_id) = whitespace_transaction_id {
-                        b.group_until_transaction(transaction_id);
-                    } else if let Some(transaction) = project_transaction.0.get(&buffer.handle) {
-                        b.group_until_transaction(transaction.id)
-                    }
-                }
-
-                if let Some(transaction) = b.finalize_last_transaction().cloned() {
-                    if !push_to_history {
-                        b.forget_transaction(transaction.id);
-                    }
-                    project_transaction
-                        .0
-                        .insert(buffer.handle.clone(), transaction);
-                }
-            })?;
+            };
+            Self::execute_formatters(
+                lsp_store.clone(),
+                formatters,
+                buffer,
+                &settings,
+                &adapters_and_servers,
+                push_to_history,
+                initial_transaction_id,
+                &mut project_transaction,
+                &mut cx,
+            )
+            .await?;
         }
 
         Ok(project_transaction)
     }
 
     #[allow(clippy::too_many_arguments)]
-    async fn perform_format(
-        formatter: &Formatter,
-        buffer: &FormattableBuffer,
-        ranges: Option<&Vec<Range<Anchor>>>,
-        primary_server_and_path: Option<(&Arc<LanguageServer>, &PathBuf)>,
+    async fn execute_formatters(
         lsp_store: WeakEntity<LspStore>,
+        formatters: &[Formatter],
+        buffer: &FormattableBuffer,
         settings: &LanguageSettings,
         adapters_and_servers: &[(Arc<CachedLspAdapter>, Arc<LanguageServer>)],
         push_to_history: bool,
-        transaction: &mut ProjectTransaction,
+        mut initial_transaction_id: Option<TransactionId>,
+        project_transaction: &mut ProjectTransaction,
         cx: &mut AsyncApp,
-    ) -> Result<Option<FormatOperation>, anyhow::Error> {
-        let result = match formatter {
-            Formatter::LanguageServer { name } => {
-                if let Some((language_server, buffer_abs_path)) = primary_server_and_path {
+    ) -> anyhow::Result<()> {
+        let mut prev_transaction_id = initial_transaction_id;
+
+        for formatter in formatters {
+            let operation = match formatter {
+                Formatter::LanguageServer { name } => {
+                    let Some(language_server) = lsp_store.update(cx, |lsp_store, cx| {
+                        lsp_store
+                            .as_local()
+                            .unwrap()
+                            .primary_language_server_for_buffer(buffer.handle.read(cx), cx)
+                            .map(|(_, lsp)| lsp.clone())
+                    })?
+                    else {
+                        continue;
+                    };
+                    let Some(buffer_abs_path) = buffer.abs_path.as_ref() else {
+                        continue;
+                    };
+
                     let language_server = if let Some(name) = name {
                         adapters_and_servers
                             .iter()
                             .find_map(|(adapter, server)| {
-                                adapter.name.0.as_ref().eq(name.as_str()).then_some(server)
+                                adapter
+                                    .name
+                                    .0
+                                    .as_ref()
+                                    .eq(name.as_str())
+                                    .then_some(server.clone())
                             })
                             .unwrap_or(language_server)
                     } else {
                         language_server
                     };
 
-                    let result = if let Some(ranges) = ranges {
+                    let result = if let Some(ranges) = &buffer.ranges {
                         Self::format_ranges_via_lsp(
                             &lsp_store,
-                            &buffer,
+                            &buffer.handle,
                             ranges,
                             buffer_abs_path,
-                            language_server,
+                            &language_server,
                             settings,
                             cx,
                         )
@@ -1482,7 +1295,7 @@ impl LocalLspStore {
                             &lsp_store,
                             &buffer.handle,
                             buffer_abs_path,
-                            language_server,
+                            &language_server,
                             settings,
                             cx,
                         )
@@ -1491,51 +1304,114 @@ impl LocalLspStore {
                     };
 
                     Some(FormatOperation::Lsp(result))
-                } else {
+                }
+                Formatter::Prettier => {
+                    let prettier = lsp_store.update(cx, |lsp_store, _cx| {
+                        lsp_store.prettier_store().unwrap().downgrade()
+                    })?;
+                    prettier_store::format_with_prettier(&prettier, &buffer.handle, cx)
+                        .await
+                        .transpose()?
+                }
+                Formatter::External { command, arguments } => {
+                    Self::format_via_external_command(buffer, command, arguments.as_deref(), cx)
+                        .await
+                        .context(format!(
+                            "failed to format via external command {:?}",
+                            command
+                        ))?
+                        .map(FormatOperation::External)
+                }
+                Formatter::CodeActions(code_actions) => {
+                    let code_actions = deserialize_code_actions(code_actions);
+                    if !code_actions.is_empty() {
+                        Self::execute_code_actions_on_servers(
+                            &lsp_store,
+                            adapters_and_servers,
+                            code_actions,
+                            &buffer.handle,
+                            push_to_history,
+                            project_transaction,
+                            cx,
+                        )
+                        .await?;
+                        let buf_transaction_id =
+                            project_transaction.0.get(&buffer.handle).map(|t| t.id);
+                        // NOTE: same logic as in buffer.handle.update below
+                        if initial_transaction_id.is_none() {
+                            initial_transaction_id = buf_transaction_id;
+                        }
+                        if buf_transaction_id.is_some() {
+                            prev_transaction_id = buf_transaction_id;
+                        }
+                    }
                     None
                 }
+            };
+            let Some(operation) = operation else {
+                continue;
+            };
+
+            let should_continue_formatting = buffer.handle.update(cx, |b, cx| {
+                // If a previous format succeeded and the buffer was edited while the language-specific
+                // formatting information for this format was being computed, avoid applying the
+                // language-specific formatting, because it can't be grouped with the previous formatting
+                // in the undo history.
+                let should_continue_formatting = match (prev_transaction_id, b.peek_undo_stack()) {
+                    (Some(prev_transaction_id), Some(last_history_entry)) => {
+                        let last_history_transaction_id = last_history_entry.transaction_id();
+                        let is_same_as_prev = last_history_transaction_id == prev_transaction_id;
+                        is_same_as_prev
+                    }
+                    (Some(_), None) => false,
+                    (_, _) => true,
+                };
+
+                if should_continue_formatting {
+                    // Apply any language-specific formatting, and group the two formatting operations
+                    // in the buffer's undo history.
+                    let this_transaction_id = match operation {
+                        FormatOperation::Lsp(edits) => b.edit(edits, None, cx),
+                        FormatOperation::External(diff) => b.apply_diff(diff, cx),
+                        FormatOperation::Prettier(diff) => b.apply_diff(diff, cx),
+                    };
+                    if initial_transaction_id.is_none() {
+                        initial_transaction_id = this_transaction_id;
+                    }
+                    if this_transaction_id.is_some() {
+                        prev_transaction_id = this_transaction_id;
+                    }
+                }
+
+                if let Some(transaction_id) = initial_transaction_id {
+                    b.group_until_transaction(transaction_id);
+                } else if let Some(transaction) = project_transaction.0.get(&buffer.handle) {
+                    b.group_until_transaction(transaction.id)
+                }
+                return should_continue_formatting;
+            })?;
+            if !should_continue_formatting {
+                break;
             }
-            Formatter::Prettier => {
-                let prettier = lsp_store.update(cx, |lsp_store, _cx| {
-                    lsp_store.prettier_store().unwrap().downgrade()
-                })?;
-                prettier_store::format_with_prettier(&prettier, &buffer.handle, cx)
-                    .await
-                    .transpose()?
-            }
-            Formatter::External { command, arguments } => {
-                Self::format_via_external_command(buffer, command, arguments.as_deref(), cx)
-                    .await
-                    .context(format!(
-                        "failed to format via external command {:?}",
-                        command
-                    ))?
-                    .map(FormatOperation::External)
-            }
-            Formatter::CodeActions(code_actions) => {
-                let code_actions = deserialize_code_actions(code_actions);
-                if !code_actions.is_empty() {
-                    Self::execute_code_actions_on_servers(
-                        &lsp_store,
-                        adapters_and_servers,
-                        code_actions,
-                        &buffer.handle,
-                        push_to_history,
-                        transaction,
-                        cx,
-                    )
-                    .await?;
+        }
+
+        buffer.handle.update(cx, |b, _cx| {
+            if let Some(transaction) = b.finalize_last_transaction().cloned() {
+                if !push_to_history {
+                    b.forget_transaction(transaction.id);
+                    project_transaction
+                        .0
+                        .insert(buffer.handle.clone(), transaction);
                 }
-                None
             }
-        };
-        anyhow::Ok(result)
+        })?;
+        return Ok(());
     }
 
     pub async fn format_ranges_via_lsp(
         this: &WeakEntity<LspStore>,
-        buffer: &FormattableBuffer,
-        ranges: &Vec<Range<Anchor>>,
+        buffer_handle: &Entity<Buffer>,
+        ranges: &[Range<Anchor>],
         abs_path: &Path,
         language_server: &Arc<LanguageServer>,
         settings: &LanguageSettings,
@@ -1563,7 +1439,7 @@ impl LocalLspStore {
                 //
                 // TODO: Instead of using current snapshot, should use the latest snapshot sent to
                 // LSP.
-                let snapshot = buffer.handle.read(cx).snapshot();
+                let snapshot = buffer_handle.read(cx).snapshot();
                 for range in ranges {
                     lsp_ranges.push(range_to_lsp(range.to_point_utf16(&snapshot))?);
                 }
@@ -1590,7 +1466,7 @@ impl LocalLspStore {
         if let Some(lsp_edits) = lsp_edits {
             this.update(cx, |this, cx| {
                 this.as_local_mut().unwrap().edits_from_lsp(
-                    &buffer.handle,
+                    &buffer_handle,
                     lsp_edits,
                     language_server.server_id(),
                     None,
@@ -2779,10 +2655,10 @@ impl LocalLspStore {
 
 #[derive(Debug)]
 pub struct FormattableBuffer {
-    id: BufferId,
     handle: Entity<Buffer>,
     abs_path: Option<PathBuf>,
     env: Option<HashMap<String, String>>,
+    ranges: Option<Vec<Range<Anchor>>>,
 }
 
 pub struct RemoteLspStore {
@@ -7041,18 +6917,27 @@ impl LspStore {
                         })?
                         .await;
 
+                    let ranges = match &target {
+                        LspFormatTarget::Buffers => None,
+                        LspFormatTarget::Ranges(ranges) => {
+                            let Some(ranges) = ranges.get(&id) else {
+                                return Err(anyhow!("No format ranges provided for buffer"));
+                            };
+                            Some(ranges.clone())
+                        }
+                    };
+
                     formattable_buffers.push(FormattableBuffer {
-                        id,
                         handle,
                         abs_path,
                         env,
+                        ranges,
                     });
                 }
 
                 let result = LocalLspStore::format_locally(
                     lsp_store.clone(),
                     formattable_buffers,
-                    &target,
                     push_to_history,
                     trigger,
                     cx.clone(),

crates/prompt_library/src/prompt_library.rs 🔗

@@ -563,7 +563,7 @@ impl PromptLibrary {
                             editor.set_text(prompt_metadata.title.unwrap_or_default(), window, cx);
                             if prompt_id.is_built_in() {
                                 editor.set_read_only(true);
-                                editor.set_show_inline_completions(Some(false), window, cx);
+                                editor.set_show_edit_predictions(Some(false), window, cx);
                             }
                             editor
                         });
@@ -578,7 +578,7 @@ impl PromptLibrary {
                             let mut editor = Editor::for_buffer(buffer, None, window, cx);
                             if prompt_id.is_built_in() {
                                 editor.set_read_only(true);
-                                editor.set_show_inline_completions(Some(false), window, cx);
+                                editor.set_show_edit_predictions(Some(false), window, cx);
                             }
                             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
                             editor.set_show_gutter(false, cx);

crates/zed/src/zed.rs 🔗

@@ -406,7 +406,7 @@ fn initialize_panels(
             workspace.add_panel(chat_panel, window, cx);
             workspace.add_panel(notification_panel, window, cx);
             cx.when_flag_enabled::<GitUiFeatureFlag>(window, |workspace, window, cx| {
-                let git_panel = git_ui::git_panel::GitPanel::new(workspace, window, None, cx);
+                let git_panel = git_ui::git_panel::GitPanel::new(workspace, window, cx);
                 workspace.add_panel(git_panel, window, cx);
             });
         })?;

crates/zed/src/zed/quick_action_bar.rs 🔗

@@ -104,7 +104,7 @@ impl Render for QuickActionBar {
             let git_blame_inline_enabled = editor.git_blame_inline_enabled();
             let show_git_blame_gutter = editor.show_git_blame_gutter();
             let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
-            let show_inline_completions = editor.should_show_inline_completions(cx);
+            let show_edit_predictions = editor.edit_predictions_enabled();
             let inline_completion_enabled = editor.inline_completions_enabled(cx);
 
             (
@@ -114,7 +114,7 @@ impl Render for QuickActionBar {
                 git_blame_inline_enabled,
                 show_git_blame_gutter,
                 auto_signature_help_enabled,
-                show_inline_completions,
+                show_edit_predictions,
                 inline_completion_enabled,
             )
         };

crates/zeta/src/rate_completion_modal.rs 🔗

@@ -279,7 +279,7 @@ impl RateCompletionModal {
                 editor.set_show_runnables(false, cx);
                 editor.set_show_wrap_guides(false, cx);
                 editor.set_show_indent_guides(false, cx);
-                editor.set_show_inline_completions(Some(false), window, cx);
+                editor.set_show_edit_predictions(Some(false), window, cx);
                 editor.set_placeholder_text("Add your feedback…", cx);
                 if focus {
                     cx.focus_self(window);

extensions/EXTRACTION.md 🔗

@@ -12,41 +12,48 @@ brew install git-filter-repo
 
 ## Process
 
-1. Create an expressions.txt file somewhere (e.g. `~/projects/expressions.txt`)
-
-```
-ruby: ==>
-extension: ==>
-chore: ==>
-zed_extension_api: ==>
-regex:(?<![\[a-zA-Z0-9])(#[0-9]{3,5})==>zed-industries/zed\1
-```
-
-This file takes the form of `patern==>replacement`, where the replacement is optional.
-Note whitespace matters so `ruby: ==>` is removing the `ruby:` prefix from a commit messages and adding a space after `==> ` means the replacement begins with a space. Regex capture groups are numbered `\1`, `\2`, etc.
-
-See: [Git Filter Repo Docs](https://htmlpreview.github.io/?https://github.com/newren/git-filter-repo/blob/docs/html/git-filter-repo.html) for more.
-
-2. Create a clean clone the zed repository, delete tags, delete branches and do the work.
+We are going to use a `$LANGNAME` variable for all these steps. Make sure it is set correctly.
 
 > **Note**
 > If you get `zsh: command not found: #` errors, run:
 > `setopt interactive_comments && echo "setopt interactive_comments" >> ~/.zshrc`
 
+1. Create a clean clone the zed repository, delete tags and delete branches.
+
 ```sh
-LANGNAME=ruby
+LANGNAME=your_language_name_here
+
 rm -rf $LANGNAME
 git clone --single-branch --no-tags git@github.com:zed-industries/zed.git $LANGNAME
 cd $LANGNAME
+```
+
+2. Create an expressions.txt file somewhere (e.g. `~/projects/$LANGNAME.txt`)
+
+This file takes the form of `patern==>replacement`, where the replacement is optional.
+Note whitespace matters so `ruby: ==>` is removing the `ruby:` prefix from a commit messages and adding a space after `==> ` means the replacement begins with a space. Regex capture groups are numbered `\1`, `\2`, etc.
+
+See: [Git Filter Repo Docs](https://htmlpreview.github.io/?https://github.com/newren/git-filter-repo/blob/docs/html/git-filter-repo.html) for more.
+
+```sh
+# Create regex mapping for rewriting commit messages (edit as appropriate)
+mkdir -p ~/projects
+echo "${LANGNAME}: ==>
+extension: ==>
+chore: ==>
+zed_extension_api: ==>
+"'regex:(?<![\[a-zA-Z0-9])(#[0-9]{3,5})==>zed-industries/zed\1' \
+  > ~/projects/${LANGNAME}.txt
 
 # This removes the LICENSE symlink
 git filter-repo --invert-paths --path extensions/$LANGNAME/LICENSE-APACHE
 
+# This does the work
 git filter-repo \
     --use-mailmap \
     --subdirectory-filter extensions/$LANGNAME/ \
     --path LICENSE-APACHE \
-    --replace-message ~/projects/expressions.txt
+    --replace-message ~/projects/${LANGNAME}.txt
 ```
 
 3. Review the commits.
@@ -100,7 +107,7 @@ git branch --set-upstream-to=origin/main main
 
 7. Publish a new version of the extension.
 
-```
+```sh
 OLD_VERSION=$(grep '^version = ' extension.toml | cut -d'"' -f2)
 NEW_VERSION=$(echo "$OLD_VERSION" | awk -F. '{$NF = $NF + 1;} 1' OFS=.)
 echo $OLD_VERSION $NEW_VERSION
@@ -124,7 +131,19 @@ git tag v${NEW_VERSION}
 git push origin v${NEW_VERSION}
 ```
 
-7. In zed repository, `rm -rf extension/langname` and push a PR.
+7. In zed repository, remove the old extension and push a PR.
+
+```sh
+rm -rf extensions/$LANGNAME
+sed -i '' "/extensions\/$LANGNAME/d" Cargo.toml
+cargo check
+git checkoout -b remove_$LANGNAME
+git add extensions/$LANGNAME
+git add Cargo.toml Cargo.lock extensions/$LANGNAME
+git commit -m "Migrate to $LANGNAME extension to zed-extensions/$LANGNAME"
+git push
+gh pr create --web
+```
 
 8. Update extensions repository:
 
@@ -151,4 +170,4 @@ git commit -m "Bump ${LANGNAME} to v${NEW_VERSION}"
 git push
 ```
 
-Create PR and reference the Zed PR with removal from tree.
+Create PR and reference the Zed PR with removal from tree.

extensions/php/Cargo.toml 🔗

@@ -1,16 +0,0 @@
-[package]
-name = "zed_php"
-version = "0.2.4"
-edition.workspace = true
-publish.workspace = true
-license = "Apache-2.0"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/php.rs"
-crate-type = ["cdylib"]
-
-[dependencies]
-zed_extension_api = "0.1.0"

extensions/php/extension.toml 🔗

@@ -1,25 +0,0 @@
-id = "php"
-name = "PHP"
-description = "PHP support."
-version = "0.2.4"
-schema_version = 1
-authors = ["Piotr Osiewicz <piotr@zed.dev>"]
-repository = "https://github.com/zed-industries/zed"
-
-[language_servers.intelephense]
-name = "Intelephense"
-language = "PHP"
-language_ids = { PHP = "php"}
-
-[language_servers.phpactor]
-name = "Phpactor"
-language = "PHP"
-
-[grammars.php]
-repository = "https://github.com/tree-sitter/tree-sitter-php"
-commit = "8ab93274065cbaf529ea15c24360cfa3348ec9e4"
-path = "php"
-
-[grammars.phpdoc]
-repository = "https://github.com/claytonrcarter/tree-sitter-phpdoc"
-commit = "1d0e255b37477d0ca46f1c9e9268c8fa76c0b3fc"

extensions/php/languages/php/config.toml 🔗

@@ -1,18 +0,0 @@
-name = "PHP"
-grammar = "php"
-path_suffixes = ["php"]
-first_line_pattern = '^#!.*php'
-line_comments = ["// ", "# "]
-autoclose_before = ";:.,=}])>"
-brackets = [
-    { start = "{", end = "}", close = true, newline = true },
-    { start = "[", end = "]", close = true, newline = true },
-    { start = "(", end = ")", close = true, newline = true },
-    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
-    { start = "'", end = "'", close = true, newline = false, not_in = ["string"] },
-]
-collapsed_placeholder = "/* ... */"
-word_characters = ["$"]
-scope_opt_in_language_servers = ["tailwindcss-language-server"]
-prettier_parser_name = "php"
-prettier_plugins = ["@prettier/plugin-php"]

extensions/php/languages/php/embedding.scm 🔗

@@ -1,36 +0,0 @@
-(
-    (comment)* @context
-    .
-    [
-        (function_definition
-            "function" @name
-            name: (_) @name
-            body: (_
-                "{" @keep
-                "}" @keep) @collapse
-            )
-
-        (trait_declaration
-            "trait" @name
-            name: (_) @name)
-
-        (method_declaration
-            "function" @name
-            name: (_) @name
-            body: (_
-                "{" @keep
-                "}" @keep) @collapse
-            )
-
-        (interface_declaration
-            "interface" @name
-            name: (_) @name
-            )
-
-        (enum_declaration
-            "enum" @name
-            name: (_) @name
-            )
-
-        ] @item
-    )

extensions/php/languages/php/highlights.scm 🔗

@@ -1,137 +0,0 @@
-(php_tag) @tag
-"?>" @tag
-
-; Types
-
-(primitive_type) @type.builtin
-(cast_type) @type.builtin
-(named_type (name) @type) @type
-(named_type (qualified_name) @type) @type
-
-; Functions
-
-(array_creation_expression "array" @function.builtin)
-(list_literal "list" @function.builtin)
-
-(method_declaration
-  name: (name) @function.method)
-
-(function_call_expression
-  function: [(qualified_name (name)) (name)] @function)
-
-(scoped_call_expression
-  name: (name) @function)
-
-(member_call_expression
-  name: (name) @function.method)
-
-(function_definition
-  name: (name) @function)
-
-; Member
-
-(property_element
-  (variable_name) @property)
-
-(member_access_expression
-  name: (variable_name (name)) @property)
-(member_access_expression
-  name: (name) @property)
-
-; Variables
-
-(relative_scope) @variable.builtin
-
-((name) @constant
- (#match? @constant "^_?[A-Z][A-Z\\d_]+$"))
-((name) @constant.builtin
- (#match? @constant.builtin "^__[A-Z][A-Z\d_]+__$"))
-
-((name) @constructor
- (#match? @constructor "^[A-Z]"))
-
-((name) @variable.builtin
- (#eq? @variable.builtin "this"))
-
-(variable_name) @variable
-
-; Basic tokens
-[
-  (string)
-  (string_value)
-  (encapsed_string)
-  (heredoc)
-  (heredoc_body)
-  (nowdoc_body)
-] @string
-(boolean) @constant.builtin
-(null) @constant.builtin
-(integer) @number
-(float) @number
-(comment) @comment
-
-"$" @operator
-
-; Keywords
-
-"abstract" @keyword
-"and" @keyword
-"as" @keyword
-"break" @keyword
-"callable" @keyword
-"case" @keyword
-"catch" @keyword
-"class" @keyword
-"clone" @keyword
-"const" @keyword
-"continue" @keyword
-"declare" @keyword
-"default" @keyword
-"do" @keyword
-"echo" @keyword
-"else" @keyword
-"elseif" @keyword
-"enum" @keyword
-"enddeclare" @keyword
-"endfor" @keyword
-"endforeach" @keyword
-"endif" @keyword
-"endswitch" @keyword
-"endwhile" @keyword
-"extends" @keyword
-"final" @keyword
-"readonly" @keyword
-"finally" @keyword
-"for" @keyword
-"foreach" @keyword
-"fn" @keyword
-"function" @keyword
-"global" @keyword
-"goto" @keyword
-"if" @keyword
-"implements" @keyword
-"include_once" @keyword
-"include" @keyword
-"instanceof" @keyword
-"insteadof" @keyword
-"interface" @keyword
-"match" @keyword
-"namespace" @keyword
-"new" @keyword
-"or" @keyword
-"print" @keyword
-"private" @keyword
-"protected" @keyword
-"public" @keyword
-"readonly" @keyword
-"require_once" @keyword
-"require" @keyword
-"return" @keyword
-"static" @keyword
-"switch" @keyword
-"throw" @keyword
-"trait" @keyword
-"try" @keyword
-"use" @keyword
-"while" @keyword
-"xor" @keyword

extensions/php/languages/php/injections.scm 🔗

@@ -1,11 +0,0 @@
-((text) @injection.content
- (#set! injection.language "html")
- (#set! injection.combined))
-
-((comment) @injection.content
-  (#match? @injection.content "^/\\*\\*[^*]")
-  (#set! injection.language "phpdoc"))
-
-((heredoc_body) (heredoc_end) @injection.language) @injection.content
-
-((nowdoc_body) (heredoc_end) @injection.language) @injection.content

extensions/php/languages/php/outline.scm 🔗

@@ -1,46 +0,0 @@
-(class_declaration
-    "class" @context
-    name: (name) @name
-    ) @item
-
-(function_definition
-    "function" @context
-    name: (_) @name
-    ) @item
-
-(method_declaration
-    "function" @context
-    name: (_) @name
-    ) @item
-
-(interface_declaration
-    "interface" @context
-    name: (_) @name
-    ) @item
-
-(enum_declaration
-    "enum" @context
-    name: (_) @name
-    ) @item
-
-(trait_declaration
-    "trait" @context
-    name: (_) @name
-    ) @item
-
-; Add support for Pest runnable
-(function_call_expression
-    function: (_) @context
-    (#any-of? @context "it" "test" "describe")
-    arguments: (arguments
-        .
-        (argument
-            [
-              (encapsed_string (string_value) @name)
-              (string (string_value) @name)
-            ]
-        )
-    )
-) @item
-
-(comment) @annotation

extensions/php/languages/php/runnables.scm 🔗

@@ -1,105 +0,0 @@
-; Class that follow the naming convention of PHPUnit test classes
-; and that doesn't have the abstract modifier
-; and have a method that follow the naming convention of PHPUnit test methods
-; and the method is public
-(
-    (class_declaration
-        modifier: (_)? @_modifier
-        (#not-eq? @_modifier "abstract")
-        name: (_) @_name
-        (#match? @_name ".*Test$")
-        body: (declaration_list
-            (method_declaration
-                (visibility_modifier)? @_visibility
-                (#eq? @_visibility "public")
-                name: (_) @run
-                (#match? @run "^test.*")
-            )
-        )
-    ) @_phpunit-test
-    (#set! tag phpunit-test)
-)
-
-; Class that follow the naming convention of PHPUnit test classes
-; and that doesn't have the abstract modifier
-; and have a method that has the @test annotation
-; and the method is public
-(
-    (class_declaration
-        modifier: (_)? @_modifier
-        (#not-eq? @_modifier "abstract")
-        name: (_) @_name
-        (#match? @_name ".*Test$")
-        body: (declaration_list
-            ((comment) @_comment
-                (#match? @_comment ".*@test\\b.*")
-            .
-            (method_declaration
-                (visibility_modifier)? @_visibility
-                (#eq? @_visibility "public")
-                name: (_) @run
-                (#not-match? @run "^test.*")
-            ))
-        )
-    ) @_phpunit-test
-    (#set! tag phpunit-test)
-)
-
-; Class that follow the naming convention of PHPUnit test classes
-; and that doesn't have the abstract modifier
-; and have a method that has the #[Test] attribute
-; and the method is public
-(
-    (class_declaration
-        modifier: (_)? @_modifier
-        (#not-eq? @_modifier "abstract")
-        name: (_) @_name
-        (#match? @_name ".*Test$")
-        body: (declaration_list
-            (method_declaration
-                (attribute_list
-                    (attribute_group
-                        (attribute (name) @_attribute)
-                    )
-                )
-                (#eq? @_attribute "Test")
-                (visibility_modifier)? @_visibility
-                (#eq? @_visibility "public")
-                name: (_) @run
-                (#not-match? @run "^test.*")
-            )
-        )
-    ) @_phpunit-test
-    (#set! tag phpunit-test)
-)
-
-; Class that follow the naming convention of PHPUnit test classes
-; and that doesn't have the abstract modifier
-(
-    (class_declaration
-        modifier: (_)? @_modifier
-        (#not-eq? @_modifier "abstract")
-        name: (_) @run
-        (#match? @run ".*Test$")
-    ) @_phpunit-test
-    (#set! tag phpunit-test)
-)
-
-; Add support for Pest runnable
-; Function expression that has `it`, `test` or `describe` as the function name
-(
-    (function_call_expression
-        function: (_) @_name
-        (#any-of? @_name "it" "test" "describe")
-        arguments: (arguments
-            .
-            (argument
-                [
-                  (encapsed_string (string_value) @run)
-                  (string (string_value) @run)
-                ]
-            )
-        )
-    ) @_pest-test
-    (#set! tag pest-test)
-)

extensions/php/languages/php/tags.scm 🔗

@@ -1,40 +0,0 @@
-(namespace_definition
-  name: (namespace_name) @name) @module
-
-(interface_declaration
-  name: (name) @name) @definition.interface
-
-(trait_declaration
-  name: (name) @name) @definition.interface
-
-(class_declaration
-  name: (name) @name) @definition.class
-
-(class_interface_clause [(name) (qualified_name)] @name) @impl
-
-(property_declaration
-  (property_element (variable_name (name) @name))) @definition.field
-
-(function_definition
-  name: (name) @name) @definition.function
-
-(method_declaration
-  name: (name) @name) @definition.function
-
-(object_creation_expression
-  [
-    (qualified_name (name) @name)
-    (variable_name (name) @name)
-  ]) @reference.class
-
-(function_call_expression
-  function: [
-    (qualified_name (name) @name)
-    (variable_name (name)) @name
-  ]) @reference.call
-
-(scoped_call_expression
-  name: (name) @name) @reference.call
-
-(member_call_expression
-  name: (name) @name) @reference.call

extensions/php/languages/php/tasks.json 🔗

@@ -1,19 +0,0 @@
-[
-  {
-    "label": "phpunit test $ZED_SYMBOL",
-    "command": "./vendor/bin/phpunit",
-    "args": ["--filter $ZED_SYMBOL $ZED_FILE"],
-    "tags": ["phpunit-test"]
-  },
-  {
-    "label": "pest test $ZED_SYMBOL",
-    "command": "./vendor/bin/pest",
-    "args": ["--filter \"$ZED_SYMBOL\" $ZED_FILE"],
-    "tags": ["pest-test"]
-  },
-  {
-    "label": "execute selection $ZED_SELECTED_TEXT",
-    "command": "php",
-    "args": ["-r", "$ZED_SELECTED_TEXT"]
-  }
-]

extensions/php/languages/php/textobjects.scm 🔗

@@ -1,45 +0,0 @@
-(function_definition
-    body: (_
-        "{"
-        (_)* @function.inside
-        "}" )) @function.around
-
-(method_declaration
-    body: (_
-        "{"
-        (_)* @function.inside
-        "}" )) @function.around
-
-(method_declaration) @function.around
-
-(class_declaration
-    body: (_
-        "{"
-        (_)* @class.inside
-        "}")) @class.around
-
-(interface_declaration
-    body: (_
-        "{"
-        (_)* @class.inside
-        "}")) @class.around
-
-(trait_declaration
-    body: (_
-        "{"
-        (_)* @class.inside
-        "}")) @class.around
-
-(enum_declaration
-    body: (_
-        "{"
-        (_)* @class.inside
-        "}")) @class.around
-
-(namespace_definition
-    body: (_
-        "{"
-        (_)* @class.inside
-        "}")) @class.around
-
-(comment)+ @comment.around

extensions/php/languages/phpdoc/config.toml 🔗

@@ -1,9 +0,0 @@
-name = "PHPDoc"
-grammar = "phpdoc"
-autoclose_before = "]})>"
-brackets = [
-  { start = "{", end = "}", close = true, newline = false },
-  { start = "[", end = "]", close = true, newline = false },
-  { start = "(", end = ")", close = true, newline = false },
-  { start = "<", end = ">", close = true, newline = false },
-]

extensions/php/languages/phpdoc/highlights.scm 🔗

@@ -1,13 +0,0 @@
-(tag_name) @keyword
-
-(tag (variable_name) @variable)
-(variable_name "$" @operator)
-
-(tag
-  (tag_name) @keyword
-  (#eq? @keyword "@method")
-  (name) @function.method)
-
-(primitive_type) @type.builtin
-(named_type (name) @type) @type
-(named_type (qualified_name) @type) @type

extensions/php/src/language_servers/intelephense.rs 🔗

@@ -1,209 +0,0 @@
-use std::{env, fs};
-
-use zed::{CodeLabel, CodeLabelSpan};
-use zed_extension_api::settings::LspSettings;
-use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result};
-
-const SERVER_PATH: &str = "node_modules/intelephense/lib/intelephense.js";
-const PACKAGE_NAME: &str = "intelephense";
-
-pub struct Intelephense {
-    did_find_server: bool,
-}
-
-impl Intelephense {
-    pub const LANGUAGE_SERVER_ID: &'static str = "intelephense";
-
-    pub fn new() -> Self {
-        Self {
-            did_find_server: false,
-        }
-    }
-
-    pub fn language_server_command(
-        &mut self,
-        language_server_id: &LanguageServerId,
-        worktree: &zed::Worktree,
-    ) -> Result<zed::Command> {
-        if let Some(path) = worktree.which("intelephense") {
-            return Ok(zed::Command {
-                command: path,
-                args: vec!["--stdio".to_string()],
-                env: Default::default(),
-            });
-        }
-
-        let server_path = self.server_script_path(language_server_id)?;
-        Ok(zed::Command {
-            command: zed::node_binary_path()?,
-            args: vec![
-                env::current_dir()
-                    .unwrap()
-                    .join(&server_path)
-                    .to_string_lossy()
-                    .to_string(),
-                "--stdio".to_string(),
-            ],
-            env: Default::default(),
-        })
-    }
-
-    fn server_exists(&self) -> bool {
-        fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
-    }
-
-    fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result<String> {
-        let server_exists = self.server_exists();
-        if self.did_find_server && server_exists {
-            return Ok(SERVER_PATH.to_string());
-        }
-
-        zed::set_language_server_installation_status(
-            language_server_id,
-            &zed::LanguageServerInstallationStatus::CheckingForUpdate,
-        );
-        let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
-
-        if !server_exists
-            || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
-        {
-            zed::set_language_server_installation_status(
-                language_server_id,
-                &zed::LanguageServerInstallationStatus::Downloading,
-            );
-            let result = zed::npm_install_package(PACKAGE_NAME, &version);
-            match result {
-                Ok(()) => {
-                    if !self.server_exists() {
-                        Err(format!(
-                            "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
-                        ))?;
-                    }
-                }
-                Err(error) => {
-                    if !self.server_exists() {
-                        Err(error)?;
-                    }
-                }
-            }
-        }
-
-        self.did_find_server = true;
-        Ok(SERVER_PATH.to_string())
-    }
-
-    pub fn language_server_workspace_configuration(
-        &mut self,
-        worktree: &zed::Worktree,
-    ) -> Result<Option<serde_json::Value>> {
-        let settings = LspSettings::for_worktree("intelephense", worktree)
-            .ok()
-            .and_then(|lsp_settings| lsp_settings.settings.clone())
-            .unwrap_or_default();
-
-        Ok(Some(serde_json::json!({
-            "intelephense": settings
-        })))
-    }
-
-    pub fn label_for_completion(&self, completion: zed::lsp::Completion) -> Option<CodeLabel> {
-        let label = &completion.label;
-
-        match completion.kind? {
-            zed::lsp::CompletionKind::Method => {
-                // __construct method doesn't have a detail
-                if let Some(ref detail) = completion.detail {
-                    if detail.is_empty() {
-                        return Some(CodeLabel {
-                            spans: vec![
-                                CodeLabelSpan::literal(label, Some("function.method".to_string())),
-                                CodeLabelSpan::literal("()", None),
-                            ],
-                            filter_range: (0..label.len()).into(),
-                            code: completion.label,
-                        });
-                    }
-                }
-
-                let mut parts = completion.detail.as_ref()?.split(":");
-                // E.g., `foo(string $var)`
-                let name_and_params = parts.next()?;
-                let return_type = parts.next()?.trim();
-
-                let (_, params) = name_and_params.split_once("(")?;
-                let params = params.trim_end_matches(")");
-
-                Some(CodeLabel {
-                    spans: vec![
-                        CodeLabelSpan::literal(label, Some("function.method".to_string())),
-                        CodeLabelSpan::literal("(", None),
-                        CodeLabelSpan::literal(params, Some("comment".to_string())),
-                        CodeLabelSpan::literal("): ", None),
-                        CodeLabelSpan::literal(return_type, Some("type".to_string())),
-                    ],
-                    filter_range: (0..label.len()).into(),
-                    code: completion.label,
-                })
-            }
-            zed::lsp::CompletionKind::Constant | zed::lsp::CompletionKind::EnumMember => {
-                if let Some(ref detail) = completion.detail {
-                    if !detail.is_empty() {
-                        return Some(CodeLabel {
-                            spans: vec![
-                                CodeLabelSpan::literal(label, Some("constant".to_string())),
-                                CodeLabelSpan::literal(" ", None),
-                                CodeLabelSpan::literal(detail, Some("comment".to_string())),
-                            ],
-                            filter_range: (0..label.len()).into(),
-                            code: completion.label,
-                        });
-                    }
-                }
-
-                Some(CodeLabel {
-                    spans: vec![CodeLabelSpan::literal(label, Some("constant".to_string()))],
-                    filter_range: (0..label.len()).into(),
-                    code: completion.label,
-                })
-            }
-            zed::lsp::CompletionKind::Property => {
-                let return_type = completion.detail?;
-                Some(CodeLabel {
-                    spans: vec![
-                        CodeLabelSpan::literal(label, Some("attribute".to_string())),
-                        CodeLabelSpan::literal(": ", None),
-                        CodeLabelSpan::literal(return_type, Some("type".to_string())),
-                    ],
-                    filter_range: (0..label.len()).into(),
-                    code: completion.label,
-                })
-            }
-            zed::lsp::CompletionKind::Variable => {
-                // See https://www.php.net/manual/en/reserved.variables.php
-                const SYSTEM_VAR_NAMES: &[&str] =
-                    &["argc", "argv", "php_errormsg", "http_response_header"];
-
-                let var_name = completion.label.trim_start_matches("$");
-                let is_uppercase = var_name
-                    .chars()
-                    .filter(|c| c.is_alphabetic())
-                    .all(|c| c.is_uppercase());
-                let is_system_constant = var_name.starts_with("_");
-                let is_reserved = SYSTEM_VAR_NAMES.contains(&var_name);
-
-                let highlight = if is_uppercase || is_system_constant || is_reserved {
-                    Some("comment".to_string())
-                } else {
-                    None
-                };
-
-                Some(CodeLabel {
-                    spans: vec![CodeLabelSpan::literal(label, highlight)],
-                    filter_range: (0..label.len()).into(),
-                    code: completion.label,
-                })
-            }
-            _ => None,
-        }
-    }
-}

extensions/php/src/language_servers/phpactor.rs 🔗

@@ -1,85 +0,0 @@
-use std::fs;
-
-use zed_extension_api::{self as zed, LanguageServerId, Result};
-
-pub struct Phpactor {
-    cached_binary_path: Option<String>,
-}
-
-impl Phpactor {
-    pub const LANGUAGE_SERVER_ID: &'static str = "phpactor";
-
-    pub fn new() -> Self {
-        Self {
-            cached_binary_path: None,
-        }
-    }
-
-    pub fn language_server_binary_path(
-        &mut self,
-        language_server_id: &LanguageServerId,
-        worktree: &zed::Worktree,
-    ) -> Result<String> {
-        if let Some(path) = worktree.which("phpactor") {
-            return Ok(path);
-        }
-
-        if let Some(path) = &self.cached_binary_path {
-            if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
-                return Ok(path.clone());
-            }
-        }
-
-        zed::set_language_server_installation_status(
-            language_server_id,
-            &zed::LanguageServerInstallationStatus::CheckingForUpdate,
-        );
-        let release = zed::latest_github_release(
-            "phpactor/phpactor",
-            zed::GithubReleaseOptions {
-                require_assets: true,
-                pre_release: false,
-            },
-        )?;
-
-        let asset_name = "phpactor.phar";
-        let asset = release
-            .assets
-            .iter()
-            .find(|asset| asset.name == asset_name)
-            .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
-
-        let version_dir = format!("phpactor-{}", release.version);
-        fs::create_dir_all(&version_dir).map_err(|e| format!("failed to create directory: {e}"))?;
-
-        let binary_path = format!("{version_dir}/phpactor.phar");
-
-        if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
-            zed::set_language_server_installation_status(
-                language_server_id,
-                &zed::LanguageServerInstallationStatus::Downloading,
-            );
-
-            zed::download_file(
-                &asset.download_url,
-                &binary_path,
-                zed::DownloadedFileType::Uncompressed,
-            )
-            .map_err(|e| format!("failed to download file: {e}"))?;
-
-            zed::make_file_executable(&binary_path)?;
-
-            let entries =
-                fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
-            for entry in entries {
-                let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
-                if entry.file_name().to_str() != Some(&version_dir) {
-                    fs::remove_dir_all(entry.path()).ok();
-                }
-            }
-        }
-
-        self.cached_binary_path = Some(binary_path.clone());
-        Ok(binary_path)
-    }
-}

extensions/php/src/php.rs 🔗

@@ -1,72 +0,0 @@
-mod language_servers;
-
-use zed::CodeLabel;
-use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result};
-
-use crate::language_servers::{Intelephense, Phpactor};
-
-struct PhpExtension {
-    intelephense: Option<Intelephense>,
-    phpactor: Option<Phpactor>,
-}
-
-impl zed::Extension for PhpExtension {
-    fn new() -> Self {
-        Self {
-            intelephense: None,
-            phpactor: None,
-        }
-    }
-
-    fn language_server_command(
-        &mut self,
-        language_server_id: &LanguageServerId,
-        worktree: &zed::Worktree,
-    ) -> Result<zed::Command> {
-        match language_server_id.as_ref() {
-            Intelephense::LANGUAGE_SERVER_ID => {
-                let intelephense = self.intelephense.get_or_insert_with(Intelephense::new);
-                intelephense.language_server_command(language_server_id, worktree)
-            }
-            Phpactor::LANGUAGE_SERVER_ID => {
-                let phpactor = self.phpactor.get_or_insert_with(Phpactor::new);
-
-                Ok(zed::Command {
-                    command: phpactor.language_server_binary_path(language_server_id, worktree)?,
-                    args: vec!["language-server".into()],
-                    env: Default::default(),
-                })
-            }
-            language_server_id => Err(format!("unknown language server: {language_server_id}")),
-        }
-    }
-
-    fn language_server_workspace_configuration(
-        &mut self,
-        language_server_id: &LanguageServerId,
-        worktree: &zed::Worktree,
-    ) -> Result<Option<serde_json::Value>> {
-        if language_server_id.as_ref() == Intelephense::LANGUAGE_SERVER_ID {
-            if let Some(intelephense) = self.intelephense.as_mut() {
-                return intelephense.language_server_workspace_configuration(worktree);
-            }
-        }
-
-        Ok(None)
-    }
-
-    fn label_for_completion(
-        &self,
-        language_server_id: &zed::LanguageServerId,
-        completion: zed::lsp::Completion,
-    ) -> Option<CodeLabel> {
-        match language_server_id.as_ref() {
-            Intelephense::LANGUAGE_SERVER_ID => {
-                self.intelephense.as_ref()?.label_for_completion(completion)
-            }
-            _ => None,
-        }
-    }
-}
-
-zed::register_extension!(PhpExtension);