From 63c0150cc24430838e97affd1bcfa59bb350f90b Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Mon, 10 Feb 2025 13:39:03 -0700 Subject: [PATCH 1/7] Fix handling of holding modifier to show edit prediction (#24580) Meant to include this in #24442 Release Notes: - N/A --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bdf1fd905686049ac33ab68c28edb202de20ee6d..6d1ed150156757dd13c3657a15a40e81e1180123 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5102,7 +5102,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if !self.show_edit_predictions_in_menu(cx) { + if self.show_edit_predictions_in_menu(cx) { let accept_binding = AcceptEditPredictionBinding::resolve(self.focus_handle(cx), window); if let Some(accept_keystroke) = accept_binding.keystroke() { From 973cb916f3c6ebd28700ad2577eff30437a7ee9a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 10 Feb 2025 13:57:07 -0700 Subject: [PATCH 2/7] Fix fill-co-authors, and collaborator cursors (#24575) Co-authored-by: mikayla-maki Release Notes: - N/A Co-authored-by: mikayla-maki --- crates/git_ui/src/git_panel.rs | 89 +++++++++++++++++++++------------- crates/zed/src/zed.rs | 2 +- 2 files changed, 55 insertions(+), 36 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index d8a676313c8878bcd79df25f57ae935d94397a39..0de9859dcad53c1e6dda0d66381d151e1b94d9b9 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -176,23 +176,21 @@ pub struct GitPanel { } fn commit_message_editor( - commit_message_buffer: Option>, + commit_message_buffer: Entity, + project: Entity, 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>, cx: &mut Context, ) -> Entity { 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::>(); - 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::>(); + 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) + }); } }) }) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 7fe30db8fec7250d6f6c09c4c683d081c559f0de..9b66e92b9f5ad2d76637504e3baf1da0966cea69 100644 --- a/crates/zed/src/zed.rs +++ b/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::(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); }); })?; From 0af048a7cfa94969076eca68718ca041b9a0802c Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 10 Feb 2025 17:57:25 -0300 Subject: [PATCH 3/7] edit predictions: Cache settings across renders (#24581) We were reading edit prediction settings too often, causing frames to be dropped. We'll now cache them and update them from `update_visible_inline_completion`. Release Notes: - N/A --- crates/assistant/src/inline_assistant.rs | 2 +- crates/assistant2/src/inline_assistant.rs | 2 +- crates/editor/src/editor.rs | 193 ++++++++++-------- crates/editor/src/element.rs | 9 +- crates/feedback/src/feedback_modal.rs | 2 +- .../src/inline_completion_button.rs | 2 +- crates/language_tools/src/lsp_log.rs | 4 +- crates/prompt_library/src/prompt_library.rs | 4 +- crates/zed/src/zed/quick_action_bar.rs | 4 +- crates/zeta/src/rate_completion_modal.rs | 2 +- 10 files changed, 123 insertions(+), 101 deletions(-) diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 286440f9896a7f22e2cafed614861f09ee64e921..370d6d1119dfd6324e1d47d0bfbf8e98be52b5b2 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/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::( Anchor::min()..Anchor::max(), cx.theme().status().deleted_background, diff --git a/crates/assistant2/src/inline_assistant.rs b/crates/assistant2/src/inline_assistant.rs index a59534148161f4d82c2fc6da41c4c77a246f51d5..2158f3279f6a959e13e667e0aa033273d5a5dd76 100644 --- a/crates/assistant2/src/inline_assistant.rs +++ b/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::( Anchor::min()..Anchor::max(), cx.theme().status().deleted_background, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6d1ed150156757dd13c3657a15a40e81e1180123..3c004aad008c80011139c4309f9fb4c10bdef85e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -497,6 +497,23 @@ struct InlineCompletionState { invalidation_range: Range, } +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 +700,7 @@ pub struct Editor { active_inline_completion: Option, /// Used to prevent flickering as the user types while the menu is open stale_inline_completion_in_menu: Option, + edit_prediction_settings: EditPredictionSettings, inline_completions_hidden_for_vim_mode: bool, show_inline_completions_override: Option, menu_inline_completions_policy: MenuInlineCompletionsPolicy, @@ -1395,6 +1413,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, @@ -1530,7 +1549,7 @@ impl Editor { key_context.add("copilot_suggestion"); key_context.add("edit_prediction"); - 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); } } @@ -1886,23 +1905,14 @@ impl Editor { cx: &mut Context, ) { 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, window: &mut Window, @@ -3019,7 +3029,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 +3922,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 +3936,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 +3986,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 +4678,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 +4697,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_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 +4812,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 +4918,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 +5105,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 +5117,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,7 +5127,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if self.show_edit_predictions_in_menu(cx) { + if self.show_edit_predictions_in_menu() { let accept_binding = AcceptEditPredictionBinding::resolve(self.focus_handle(cx), window); if let Some(accept_keystroke) = accept_binding.keystroke() { @@ -5140,10 +5165,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 +5186,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 +5260,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 +5336,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, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 784c1d51785458e68551137ae77fd71a8a2b7a2e..6fe274572ec3e5605c08d8f71c658ae2bf9bd8a6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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; @@ -3557,7 +3556,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; } diff --git a/crates/feedback/src/feedback_modal.rs b/crates/feedback/src/feedback_modal.rs index 34f8e2fa49967f6e39fd58cd7f8206506b59fa0a..f2b6b464471298b70204e6aca615729fd0999cef 100644 --- a/crates/feedback/src/feedback_modal.rs +++ b/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(); diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 1b9e7309692bbb6fc8a5a89872aa9fa3e0cabc48..244fa8324bbc8b1d2f1a5a0e407c6d3a12fdf6c4 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/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; diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 9d4a236f5fb12ec68a1c5806da30ef530e39755b..5241eb210d5e56f472a494641603e932b64839d7 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/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 }); diff --git a/crates/prompt_library/src/prompt_library.rs b/crates/prompt_library/src/prompt_library.rs index be5e31d6b55f53a26b698e0f2856e67e770a77d5..a37957bfd6bdadf8dbe53ae0a4a35821a9c4002e 100644 --- a/crates/prompt_library/src/prompt_library.rs +++ b/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); diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 5f2b98d444e3a8f6da2035e3db03498399bbf2b7..3e1b57125c17a97f5771d2ba291fd4f6cdc266dd 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/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, ) }; diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index dda838c21b3b927af387a93dbf731e76dead44c6..ed79f41fe8f3b1943770107a714dd6cd235a6563 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/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); From 62bb3398edeae513eeb4f97dc724ee1021942f2e Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 10 Feb 2025 16:07:38 -0500 Subject: [PATCH 4/7] Migate PHP Extension to zed-extensions/php (#24583) PHP Extension has been extracted to it's own repository available here: - https://github.com/zed-extensions/php --- Cargo.lock | 7 - Cargo.toml | 1 - 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 - .../php/languages/phpdoc/highlights.scm | 13 -- extensions/php/src/language_servers.rs | 5 - .../php/src/language_servers/intelephense.rs | 209 ------------------ .../php/src/language_servers/phpactor.rs | 85 ------- extensions/php/src/php.rs | 72 ------ 22 files changed, 905 deletions(-) delete mode 100644 extensions/php/Cargo.toml delete mode 120000 extensions/php/LICENSE-APACHE delete mode 100644 extensions/php/extension.toml delete mode 100644 extensions/php/languages/php/brackets.scm delete mode 100644 extensions/php/languages/php/config.toml delete mode 100644 extensions/php/languages/php/embedding.scm delete mode 100644 extensions/php/languages/php/highlights.scm delete mode 100644 extensions/php/languages/php/indents.scm delete mode 100644 extensions/php/languages/php/injections.scm delete mode 100644 extensions/php/languages/php/outline.scm delete mode 100644 extensions/php/languages/php/runnables.scm delete mode 100644 extensions/php/languages/php/tags.scm delete mode 100644 extensions/php/languages/php/tasks.json delete mode 100644 extensions/php/languages/php/textobjects.scm delete mode 100644 extensions/php/languages/phpdoc/config.toml delete mode 100644 extensions/php/languages/phpdoc/highlights.scm delete mode 100644 extensions/php/src/language_servers.rs delete mode 100644 extensions/php/src/language_servers/intelephense.rs delete mode 100644 extensions/php/src/language_servers/phpactor.rs delete mode 100644 extensions/php/src/php.rs diff --git a/Cargo.lock b/Cargo.lock index c114e0ee526c1fa338cb57460daac4395f7a25c3..63c66cf0456d7aa72199bacaf0ebbfd2e912a8cb 100644 --- a/Cargo.lock +++ b/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" diff --git a/Cargo.toml b/Cargo.toml index e0481d632a900f24bd80b9d73e64fb3e5ad446ad..935b05337814c9032d268598c200050a6772e395 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -171,7 +171,6 @@ members = [ "extensions/haskell", "extensions/html", "extensions/lua", - "extensions/php", "extensions/perplexity", "extensions/proto", "extensions/purescript", diff --git a/extensions/php/Cargo.toml b/extensions/php/Cargo.toml deleted file mode 100644 index b3f0ad663dd9d8f28ebc261b8523cd0d9211f769..0000000000000000000000000000000000000000 --- a/extensions/php/Cargo.toml +++ /dev/null @@ -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" diff --git a/extensions/php/LICENSE-APACHE b/extensions/php/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3affae83854be02a0afdec3b7a9ec4d..0000000000000000000000000000000000000000 --- a/extensions/php/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/php/extension.toml b/extensions/php/extension.toml deleted file mode 100644 index 0f89e3f885c1a0544073151695c9bf5f26c0ab8e..0000000000000000000000000000000000000000 --- a/extensions/php/extension.toml +++ /dev/null @@ -1,25 +0,0 @@ -id = "php" -name = "PHP" -description = "PHP support." -version = "0.2.4" -schema_version = 1 -authors = ["Piotr Osiewicz "] -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" diff --git a/extensions/php/languages/php/brackets.scm b/extensions/php/languages/php/brackets.scm deleted file mode 100644 index 988602aa8d715a2a771b2298a022d661c7683465..0000000000000000000000000000000000000000 --- a/extensions/php/languages/php/brackets.scm +++ /dev/null @@ -1,4 +0,0 @@ -("{" @open "}" @close) -("(" @open ")" @close) -("[" @open "]" @close) -("\"" @open "\"" @close) diff --git a/extensions/php/languages/php/config.toml b/extensions/php/languages/php/config.toml deleted file mode 100644 index 54b6c2905fe1efbcf8d943c455f33207e5287c09..0000000000000000000000000000000000000000 --- a/extensions/php/languages/php/config.toml +++ /dev/null @@ -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"] diff --git a/extensions/php/languages/php/embedding.scm b/extensions/php/languages/php/embedding.scm deleted file mode 100644 index db277775b38fe43f5bc3dc981cf468048202dc17..0000000000000000000000000000000000000000 --- a/extensions/php/languages/php/embedding.scm +++ /dev/null @@ -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 - ) diff --git a/extensions/php/languages/php/highlights.scm b/extensions/php/languages/php/highlights.scm deleted file mode 100644 index 6afeb1090b7a0bf677a25a369fc590a850ecbd04..0000000000000000000000000000000000000000 --- a/extensions/php/languages/php/highlights.scm +++ /dev/null @@ -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 diff --git a/extensions/php/languages/php/indents.scm b/extensions/php/languages/php/indents.scm deleted file mode 100644 index e9754690920500f55e611f981e46d0365560eb4f..0000000000000000000000000000000000000000 --- a/extensions/php/languages/php/indents.scm +++ /dev/null @@ -1 +0,0 @@ -(_ "{" "}" @end) @indent diff --git a/extensions/php/languages/php/injections.scm b/extensions/php/languages/php/injections.scm deleted file mode 100644 index 657145c13f62debfedeaefccbc8cf8acf1adb319..0000000000000000000000000000000000000000 --- a/extensions/php/languages/php/injections.scm +++ /dev/null @@ -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 diff --git a/extensions/php/languages/php/outline.scm b/extensions/php/languages/php/outline.scm deleted file mode 100644 index 13c11676b9c94828a1202bcd34d361dd3f0a8d2b..0000000000000000000000000000000000000000 --- a/extensions/php/languages/php/outline.scm +++ /dev/null @@ -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 diff --git a/extensions/php/languages/php/runnables.scm b/extensions/php/languages/php/runnables.scm deleted file mode 100644 index 96c90d2f8ad2757285f1f5ffd2b82d838f9efa38..0000000000000000000000000000000000000000 --- a/extensions/php/languages/php/runnables.scm +++ /dev/null @@ -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) -) diff --git a/extensions/php/languages/php/tags.scm b/extensions/php/languages/php/tags.scm deleted file mode 100644 index 66d594c254748c26623ce40456bab1304305be43..0000000000000000000000000000000000000000 --- a/extensions/php/languages/php/tags.scm +++ /dev/null @@ -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 diff --git a/extensions/php/languages/php/tasks.json b/extensions/php/languages/php/tasks.json deleted file mode 100644 index 65800eebe2e5bd5df19551c7beb9b39f22db0336..0000000000000000000000000000000000000000 --- a/extensions/php/languages/php/tasks.json +++ /dev/null @@ -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"] - } -] diff --git a/extensions/php/languages/php/textobjects.scm b/extensions/php/languages/php/textobjects.scm deleted file mode 100644 index d86a0c125212d94c0a5313f637868cda7b285f36..0000000000000000000000000000000000000000 --- a/extensions/php/languages/php/textobjects.scm +++ /dev/null @@ -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 diff --git a/extensions/php/languages/phpdoc/config.toml b/extensions/php/languages/phpdoc/config.toml deleted file mode 100644 index 78aa5e64aa3be4e45f7b2fbd331d36162c100d8b..0000000000000000000000000000000000000000 --- a/extensions/php/languages/phpdoc/config.toml +++ /dev/null @@ -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 }, -] diff --git a/extensions/php/languages/phpdoc/highlights.scm b/extensions/php/languages/phpdoc/highlights.scm deleted file mode 100644 index 767d8e62207984d8724894ebb9ff5ec83625c0f9..0000000000000000000000000000000000000000 --- a/extensions/php/languages/phpdoc/highlights.scm +++ /dev/null @@ -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 diff --git a/extensions/php/src/language_servers.rs b/extensions/php/src/language_servers.rs deleted file mode 100644 index f209d1c00ac2639a9933cc52e64fc23bc75ac971..0000000000000000000000000000000000000000 --- a/extensions/php/src/language_servers.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod intelephense; -mod phpactor; - -pub use intelephense::*; -pub use phpactor::*; diff --git a/extensions/php/src/language_servers/intelephense.rs b/extensions/php/src/language_servers/intelephense.rs deleted file mode 100644 index 23f47ac5c06344b641a88a0c46900fed36a12bfa..0000000000000000000000000000000000000000 --- a/extensions/php/src/language_servers/intelephense.rs +++ /dev/null @@ -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 { - 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 { - 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> { - 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 { - 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, - } - } -} diff --git a/extensions/php/src/language_servers/phpactor.rs b/extensions/php/src/language_servers/phpactor.rs deleted file mode 100644 index 3ba668b1e82986b19fc116e895e8cd06a4f4a3c7..0000000000000000000000000000000000000000 --- a/extensions/php/src/language_servers/phpactor.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::fs; - -use zed_extension_api::{self as zed, LanguageServerId, Result}; - -pub struct Phpactor { - cached_binary_path: Option, -} - -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 { - 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) - } -} diff --git a/extensions/php/src/php.rs b/extensions/php/src/php.rs deleted file mode 100644 index 53b4c299516241e28cebcd6d8bbd9d423055105b..0000000000000000000000000000000000000000 --- a/extensions/php/src/php.rs +++ /dev/null @@ -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, - phpactor: Option, -} - -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 { - 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> { - 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 { - match language_server_id.as_ref() { - Intelephense::LANGUAGE_SERVER_ID => { - self.intelephense.as_ref()?.label_for_completion(completion) - } - _ => None, - } - } -} - -zed::register_extension!(PhpExtension); From 89e051d6509fde19d76a7b6ad5056094c59eaee2 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 10 Feb 2025 16:13:26 -0500 Subject: [PATCH 5/7] Update extension extraction documentation (2025-02-10) (#24585) Include lessons learned from PHP Extension extraction. --- extensions/EXTRACTION.md | 61 ++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/extensions/EXTRACTION.md b/extensions/EXTRACTION.md index e5ff27bb680d91002d77c71957b83aa31ddf767b..09fed7970d87f2eaf2aad178614d6578a9deaeba 100644 --- a/extensions/EXTRACTION.md +++ b/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:(?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:(?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. \ No newline at end of file +Create PR and reference the Zed PR with removal from tree. From 1f288f732718b65f2418bf520097424e89402f6c Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 10 Feb 2025 18:49:46 -0300 Subject: [PATCH 6/7] edit predictions: Fix predictions bar disappearing while loading (#24582) Release Notes: - N/A --------- Co-authored-by: Max --- crates/editor/src/editor.rs | 29 +++++++++++--- crates/editor/src/element.rs | 74 +++++++++++++++--------------------- crates/gpui/src/window.rs | 12 ++++++ 3 files changed, 65 insertions(+), 50 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3c004aad008c80011139c4309f9fb4c10bdef85e..fba395c906bdf04e390aabf0e9fd0b6a975d615f 100644 --- a/crates/editor/src/editor.rs +++ b/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"; @@ -1488,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) -> 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 { @@ -1547,7 +1548,7 @@ 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() { key_context.add(EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT); @@ -1561,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, @@ -5128,8 +5145,7 @@ impl Editor { cx: &mut Context, ) { if self.show_edit_predictions_in_menu() { - let accept_binding = - AcceptEditPredictionBinding::resolve(self.focus_handle(cx), window); + 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 @@ -14408,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) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6fe274572ec3e5605c08d8f71c658ae2bf9bd8a6..ecac1da5b430ca7944b45b33b512cfab367cae58 100644 --- a/crates/editor/src/element.rs +++ b/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::{ @@ -3167,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, @@ -3569,7 +3567,7 @@ impl EditorElement { "Jump to Edit", Some(IconName::ArrowUp), previewing, - self.editor.focus_handle(cx), + editor, window, cx, )?; @@ -3582,7 +3580,7 @@ impl EditorElement { "Jump to Edit", Some(IconName::ArrowDown), previewing, - self.editor.focus_handle(cx), + editor, window, cx, )?; @@ -3598,7 +3596,7 @@ impl EditorElement { "Jump to Edit", None, previewing, - self.editor.focus_handle(cx), + editor, window, cx, )?; @@ -3657,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.)), @@ -5675,11 +5670,11 @@ fn inline_completion_accept_indicator( label: impl Into, icon: Option, previewing: bool, - editor_focus_handle: FocusHandle, - window: &Window, + editor: &Editor, + window: &mut Window, cx: &App, ) -> Option { - 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() @@ -5728,18 +5723,9 @@ fn inline_completion_accept_indicator( ) } -pub struct AcceptEditPredictionBinding(Option); +pub struct AcceptEditPredictionBinding(pub(crate) Option); 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() { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 7639f5e676c25a33aac90083f99f97dfae583625..dbbe42f9846a4ad503aff7610578c91df5c504b7 100644 --- a/crates/gpui/src/window.rs +++ b/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 { + 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( &self, From dab9c4179932f40792f8e1da0a11c0a09c89d41c Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 10 Feb 2025 16:18:14 -0600 Subject: [PATCH 7/7] Fix formatters not running in order (#24584) Previously, if multiple formatters were specified for the same language, they would be run in parallel on the state of the file, and then all edits would be applied. This lead to incorrect output with many unwanted artifacts. This PR refactors the formatting code to clean it up, and ensure results from previous formatters are passed in to subsequent formatters. Closes #15544 Release Notes: - Fixed an issue where when running multiple formatters they would be ran in parallel rather than sequentially, leading to unwanted artifacts and incorrect output. --------- Co-authored-by: Conrad --- crates/project/src/lsp_store.rs | 483 ++++++++++++-------------------- 1 file changed, 184 insertions(+), 299 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index df0c833ab019cd750e0d175e3af5f23600b5c1b6..7fbb781f0f8a12791c8e324c3f1667bee12368d7 100644 --- a/crates/project/src/lsp_store.rs +++ b/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, mut buffers: Vec, - 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::>(); - - 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::>() + })?; 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 = 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>>, - primary_server_and_path: Option<(&Arc, &PathBuf)>, + async fn execute_formatters( lsp_store: WeakEntity, + formatters: &[Formatter], + buffer: &FormattableBuffer, settings: &LanguageSettings, adapters_and_servers: &[(Arc, Arc)], push_to_history: bool, - transaction: &mut ProjectTransaction, + mut initial_transaction_id: Option, + project_transaction: &mut ProjectTransaction, cx: &mut AsyncApp, - ) -> Result, 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, - buffer: &FormattableBuffer, - ranges: &Vec>, + buffer_handle: &Entity, + ranges: &[Range], abs_path: &Path, language_server: &Arc, 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, abs_path: Option, env: Option>, + ranges: Option>>, } 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(),