Merge branch 'main' into v0.119.x

Joseph T. Lyons created

Change summary

crates/assistant/src/assistant_panel.rs           | 85 +++++++++++++---
crates/vim/src/editor_events.rs                   |  7 
crates/vim/src/normal/repeat.rs                   | 13 ++
crates/vim/src/test/neovim_backed_test_context.rs |  2 
crates/vim/test_data/test_repeat_over_blur.json   | 11 ++
5 files changed, 95 insertions(+), 23 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -920,6 +920,39 @@ impl AssistantPanel {
         self.editors.get(self.active_editor_index?)
     }
 
+    fn render_api_key_editor(
+        &self,
+        editor: &View<Editor>,
+        cx: &mut ViewContext<Self>,
+    ) -> impl IntoElement {
+        let settings = ThemeSettings::get_global(cx);
+        let text_style = TextStyle {
+            color: if editor.read(cx).read_only(cx) {
+                cx.theme().colors().text_disabled
+            } else {
+                cx.theme().colors().text
+            },
+            font_family: settings.ui_font.family.clone(),
+            font_features: settings.ui_font.features,
+            font_size: rems(0.875).into(),
+            font_weight: FontWeight::NORMAL,
+            font_style: FontStyle::Normal,
+            line_height: relative(1.3).into(),
+            background_color: None,
+            underline: None,
+            white_space: WhiteSpace::Normal,
+        };
+        EditorElement::new(
+            &editor,
+            EditorStyle {
+                background: cx.theme().colors().editor_background,
+                local_player: cx.theme().players().local(),
+                text: text_style,
+                ..Default::default()
+            },
+        )
+    }
+
     fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
         IconButton::new("hamburger_button", IconName::Menu)
             .on_click(cx.listener(|this, _event, cx| {
@@ -1091,28 +1124,42 @@ fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
 impl Render for AssistantPanel {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         if let Some(api_key_editor) = self.api_key_editor.clone() {
+            const INSTRUCTIONS: [&'static str; 5] = [
+                "To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
+                " - You can create an API key at: platform.openai.com/api-keys",
+                " - Having a subscription for another service like GitHub Copilot won't work.",
+                " ",
+                "Paste your OpenAI API key and press Enter to use the assistant:"
+            ];
+
             v_flex()
+                .p_4()
+                .size_full()
                 .on_action(cx.listener(AssistantPanel::save_credentials))
                 .track_focus(&self.focus_handle)
-                .child(Label::new(
-                    "To use the assistant panel or inline assistant, you need to add your OpenAI api key.",
-                ))
-                .child(Label::new(
-                    " - Having a subscription for another service like GitHub Copilot won't work."
-                ))
-                .child(Label::new(
-                    " - You can create a api key at: platform.openai.com/api-keys"
-                ))
-                .child(Label::new(
-                    " "
-                ))
-                .child(Label::new(
-                    "Paste your OpenAI API key and press Enter to use the assistant"
-                ))
-                .child(api_key_editor)
-                .child(Label::new(
-                    "Click on the Z button in the status bar to close this panel."
-                ))
+                .children(
+                    INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
+                )
+                .child(
+                    h_flex()
+                        .w_full()
+                        .my_2()
+                        .px_2()
+                        .py_1()
+                        .bg(cx.theme().colors().editor_background)
+                        .rounded_md()
+                        .child(self.render_api_key_editor(&api_key_editor, cx)),
+                )
+                .child(
+                    h_flex()
+                        .gap_2()
+                        .child(Label::new("Click on").size(LabelSize::Small))
+                        .child(Icon::new(IconName::Ai).size(IconSize::XSmall))
+                        .child(
+                            Label::new("in the status bar to close this panel.")
+                                .size(LabelSize::Small),
+                        ),
+                )
         } else {
             let header = TabBar::new("assistant_header")
                 .start_child(

crates/vim/src/editor_events.rs 🔗

@@ -1,6 +1,6 @@
-use crate::Vim;
+use crate::{insert::NormalBefore, Vim};
 use editor::{Editor, EditorEvent};
-use gpui::{AppContext, Entity, EntityId, View, ViewContext, WindowContext};
+use gpui::{Action, AppContext, Entity, EntityId, View, ViewContext, WindowContext};
 
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(|_, cx: &mut ViewContext<Editor>| {
@@ -34,8 +34,7 @@ fn focused(editor: View<Editor>, cx: &mut WindowContext) {
 
 fn blurred(editor: View<Editor>, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
-        vim.workspace_state.recording = false;
-        vim.workspace_state.recorded_actions.clear();
+        vim.stop_recording_immediately(NormalBefore.boxed_clone());
         if let Some(previous_editor) = vim.active_editor.clone() {
             if previous_editor
                 .upgrade()

crates/vim/src/normal/repeat.rs 🔗

@@ -493,4 +493,17 @@ mod test {
         cx.simulate_keystrokes(["escape"]);
         cx.assert_state("ˇjhello\n", Mode::Normal);
     }
+
+    #[gpui::test]
+    async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("ˇhello hello hello\n").await;
+        cx.simulate_shared_keystrokes(["c", "f", "o", "x", "escape"])
+            .await;
+        cx.assert_shared_state("ˇx hello hello\n").await;
+        cx.simulate_shared_keystrokes([":", "escape"]).await;
+        cx.simulate_shared_keystrokes(["."]).await;
+        cx.assert_shared_state("ˇx hello\n").await;
+    }
 }

crates/vim/src/test/neovim_backed_test_context.rs 🔗

@@ -62,6 +62,8 @@ pub struct NeovimBackedTestContext {
 
 impl NeovimBackedTestContext {
     pub async fn new(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext {
+        #[cfg(feature = "neovim")]
+        cx.executor().allow_parking();
         // rust stores the name of the test on the current thread.
         // We use this to automatically name a file that will store
         // the neovim connection's requests/responses so that we can

crates/vim/test_data/test_repeat_over_blur.json 🔗

@@ -0,0 +1,11 @@
+{"Put":{"state":"ˇhello hello hello\n"}}
+{"Key":"c"}
+{"Key":"f"}
+{"Key":"o"}
+{"Key":"x"}
+{"Key":"escape"}
+{"Get":{"state":"ˇx hello hello\n","mode":"Normal"}}
+{"Key":":"}
+{"Key":"escape"}
+{"Key":"."}
+{"Get":{"state":"ˇx hello\n","mode":"Normal"}}