Merge branch 'main' into autoindent-on-paste

Max Brunsfeld created

Change summary

Cargo.lock                               |  2 
assets/keymaps/vim.json                  | 30 ++++----
crates/gpui/src/keymap.rs                |  7 +
crates/gpui/src/platform/mac/event.rs    | 18 +++-
crates/gpui/src/platform/mac/platform.rs |  1 
crates/gpui/src/platform/mac/window.rs   | 92 +++++++++++++++++--------
crates/terminal/src/connected_view.rs    |  8 ++
crates/terminal/src/mappings/keys.rs     |  1 
crates/terminal/src/terminal.rs          |  8 --
crates/vim/src/normal.rs                 | 22 +++---
crates/vim/src/normal/change.rs          | 10 +-
crates/vim/src/normal/delete.rs          | 10 +-
crates/vim/src/visual.rs                 |  8 +-
crates/workspace/src/workspace.rs        |  4 
crates/zed/Cargo.toml                    |  2 
crates/zed/src/main.rs                   |  8 ++
crates/zed/src/zed.rs                    | 70 ++++++++++++++++++
17 files changed, 211 insertions(+), 90 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -6994,7 +6994,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.49.0"
+version = "0.49.1"
 dependencies = [
  "activity_indicator",
  "anyhow",

assets/keymaps/vim.json 🔗

@@ -14,30 +14,30 @@
             "k": "vim::Up",
             "l": "vim::Right",
             "0": "vim::StartOfLine",
-            "shift-$": "vim::EndOfLine",
-            "shift-G": "vim::EndOfDocument",
+            "$": "vim::EndOfLine",
+            "shift-g": "vim::EndOfDocument",
             "w": "vim::NextWordStart",
-            "shift-W": [
+            "shift-w": [
                 "vim::NextWordStart",
                 {
                     "ignorePunctuation": true
                 }
             ],
             "e": "vim::NextWordEnd",
-            "shift-E": [
+            "shift-e": [
                 "vim::NextWordEnd",
                 {
                     "ignorePunctuation": true
                 }
             ],
             "b": "vim::PreviousWordStart",
-            "shift-B": [
+            "shift-b": [
                 "vim::PreviousWordStart",
                 {
                     "ignorePunctuation": true
                 }
             ],
-            "shift-%": "vim::Matching",
+            "%": "vim::Matching",
             "escape": "editor::Cancel"
         }
     },
@@ -48,12 +48,12 @@
                 "vim::PushOperator",
                 "Change"
             ],
-            "shift-C": "vim::ChangeToEndOfLine",
+            "shift-c": "vim::ChangeToEndOfLine",
             "d": [
                 "vim::PushOperator",
                 "Delete"
             ],
-            "shift-D": "vim::DeleteToEndOfLine",
+            "shift-d": "vim::DeleteToEndOfLine",
             "y": [
                 "vim::PushOperator",
                 "Yank"
@@ -62,14 +62,14 @@
                 "vim::SwitchMode",
                 "Insert"
             ],
-            "shift-I": "vim::InsertFirstNonWhitespace",
+            "shift-i": "vim::InsertFirstNonWhitespace",
             "a": "vim::InsertAfter",
-            "shift-A": "vim::InsertEndOfLine",
+            "shift-a": "vim::InsertEndOfLine",
             "x": "vim::DeleteRight",
-            "shift-X": "vim::DeleteLeft",
-            "shift-^": "vim::FirstNonWhitespace",
+            "shift-x": "vim::DeleteLeft",
+            "^": "vim::FirstNonWhitespace",
             "o": "vim::InsertLineBelow",
-            "shift-O": "vim::InsertLineAbove",
+            "shift-o": "vim::InsertLineAbove",
             "v": [
                 "vim::SwitchMode",
                 {
@@ -78,7 +78,7 @@
                     }
                 }
             ],
-            "shift-V": [
+            "shift-v": [
                 "vim::SwitchMode",
                 {
                     "Visual": {
@@ -113,7 +113,7 @@
         "context": "Editor && vim_operator == c",
         "bindings": {
             "w": "vim::ChangeWord",
-            "shift-W": [
+            "shift-w": [
                 "vim::ChangeWord",
                 {
                     "ignorePunctuation": true

crates/gpui/src/keymap.rs 🔗

@@ -41,6 +41,7 @@ pub struct Keystroke {
     pub alt: bool,
     pub shift: bool,
     pub cmd: bool,
+    pub function: bool,
     pub key: String,
 }
 
@@ -277,6 +278,7 @@ impl Keystroke {
         let mut alt = false;
         let mut shift = false;
         let mut cmd = false;
+        let mut function = false;
         let mut key = None;
 
         let mut components = source.split("-").peekable();
@@ -286,6 +288,7 @@ impl Keystroke {
                 "alt" => alt = true,
                 "shift" => shift = true,
                 "cmd" => cmd = true,
+                "fn" => function = true,
                 _ => {
                     if let Some(component) = components.peek() {
                         if component.is_empty() && source.ends_with('-') {
@@ -306,6 +309,7 @@ impl Keystroke {
             alt,
             shift,
             cmd,
+            function,
             key: key.unwrap(),
         })
     }
@@ -464,6 +468,7 @@ mod tests {
                 alt: false,
                 shift: false,
                 cmd: false,
+                function: false,
             }
         );
 
@@ -475,6 +480,7 @@ mod tests {
                 alt: true,
                 shift: true,
                 cmd: false,
+                function: false,
             }
         );
 
@@ -486,6 +492,7 @@ mod tests {
                 alt: false,
                 shift: true,
                 cmd: true,
+                function: false,
             }
         );
 

crates/gpui/src/platform/mac/event.rs 🔗

@@ -210,19 +210,24 @@ impl Event {
 unsafe fn parse_keystroke(native_event: id) -> Keystroke {
     use cocoa::appkit::*;
 
+    let mut chars_ignoring_modifiers =
+        CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
+            .to_str()
+            .unwrap();
+    let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
     let modifiers = native_event.modifierFlags();
+
     let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
     let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
     let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
     let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
-
-    let mut chars_ignoring_modifiers =
-        CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
-            .to_str()
-            .unwrap();
+    let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask)
+        && first_char.map_or(true, |ch| {
+            ch < NSUpArrowFunctionKey || ch > NSModeSwitchFunctionKey
+        });
 
     #[allow(non_upper_case_globals)]
-    let key = match chars_ignoring_modifiers.chars().next().map(|ch| ch as u16) {
+    let key = match first_char {
         Some(SPACE_KEY) => "space",
         Some(BACKSPACE_KEY) => "backspace",
         Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter",
@@ -282,6 +287,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
         alt,
         shift,
         cmd,
+        function,
         key: key.into(),
     }
 }

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

@@ -184,6 +184,7 @@ impl MacForegroundPlatform {
                             (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
                             (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
                             (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
+                            (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask),
                         ] {
                             if *modifier {
                                 mask |= *flag;

crates/gpui/src/platform/mac/window.rs 🔗

@@ -154,6 +154,10 @@ unsafe fn build_classes() {
             sel!(performKeyEquivalent:),
             handle_key_equivalent as extern "C" fn(&Object, Sel, id) -> BOOL,
         );
+        decl.add_method(
+            sel!(keyDown:),
+            handle_key_down as extern "C" fn(&Object, Sel, id),
+        );
         decl.add_method(
             sel!(mouseDown:),
             handle_view_event as extern "C" fn(&Object, Sel, id),
@@ -275,7 +279,8 @@ struct WindowState {
     should_close_callback: Option<Box<dyn FnMut() -> bool>>,
     close_callback: Option<Box<dyn FnOnce()>>,
     input_handler: Option<Box<dyn InputHandler>>,
-    pending_key_down_event: Option<KeyDownEvent>,
+    pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
+    performed_key_equivalent: bool,
     synthetic_drag_counter: usize,
     executor: Rc<executor::Foreground>,
     scene_to_render: Option<Scene>,
@@ -287,6 +292,11 @@ struct WindowState {
     previous_modifiers_changed_event: Option<Event>,
 }
 
+struct InsertText {
+    replacement_range: Option<Range<usize>>,
+    text: String,
+}
+
 impl Window {
     pub fn open(
         id: usize,
@@ -359,7 +369,8 @@ impl Window {
                 close_callback: None,
                 activate_callback: None,
                 input_handler: None,
-                pending_key_down_event: None,
+                pending_key_down: None,
+                performed_key_equivalent: false,
                 synthetic_drag_counter: 0,
                 executor,
                 scene_to_render: Default::default(),
@@ -689,13 +700,28 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) {
 }
 
 extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> BOOL {
+    handle_key_event(this, native_event, true)
+}
+
+extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
+    handle_key_event(this, native_event, false);
+}
+
+extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
     let window_state = unsafe { get_window_state(this) };
 
     let mut window_state_borrow = window_state.as_ref().borrow_mut();
 
     let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
     if let Some(event) = event {
-        window_state_borrow.pending_key_down_event = match event {
+        if key_equivalent {
+            window_state_borrow.performed_key_equivalent = true;
+        } else if window_state_borrow.performed_key_equivalent {
+            return NO;
+        }
+
+        let function_is_held;
+        window_state_borrow.pending_key_down = match event {
             Event::KeyDown(event) => {
                 let keydown = event.keystroke.clone();
                 // Ignore events from held-down keys after some of the initially-pressed keys
@@ -708,19 +734,23 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) ->
                     window_state_borrow.last_fresh_keydown = Some(keydown);
                 }
 
-                Some(event)
+                function_is_held = event.keystroke.function;
+                Some((event, None))
             }
             _ => return NO,
         };
         drop(window_state_borrow);
 
-        unsafe {
-            let input_context: id = msg_send![this, inputContext];
-            let _: BOOL = msg_send![input_context, handleEvent: native_event];
+        if !function_is_held {
+            unsafe {
+                let input_context: id = msg_send![this, inputContext];
+                let _: BOOL = msg_send![input_context, handleEvent: native_event];
+            }
         }
 
+        let mut handled = false;
         let mut window_state_borrow = window_state.borrow_mut();
-        if let Some(event) = window_state_borrow.pending_key_down_event.take() {
+        if let Some((event, insert_text)) = window_state_borrow.pending_key_down.take() {
             if let Some(mut callback) = window_state_borrow.event_callback.take() {
                 drop(window_state_borrow);
 
@@ -729,14 +759,26 @@ extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) ->
                         .flatten()
                         .is_some();
                 if !is_composing {
-                    callback(Event::KeyDown(event));
+                    handled = callback(Event::KeyDown(event));
+                }
+
+                if !handled {
+                    if let Some(insert) = insert_text {
+                        handled = true;
+                        with_input_handler(this, |input_handler| {
+                            input_handler
+                                .replace_text_in_range(insert.replacement_range, &insert.text)
+                        });
+                    }
                 }
 
                 window_state.borrow_mut().event_callback = Some(callback);
             }
+        } else {
+            handled = true;
         }
 
-        YES
+        handled as BOOL
     } else {
         NO
     }
@@ -819,6 +861,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
         ctrl: false,
         alt: false,
         shift: false,
+        function: false,
         key: ".".into(),
     };
     let event = Event::KeyDown(KeyDownEvent {
@@ -837,6 +880,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
 extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
     unsafe {
         let () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];
+        get_window_state(this).borrow_mut().performed_key_equivalent = false;
     }
 }
 
@@ -1042,7 +1086,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
     unsafe {
         let window_state = get_window_state(this);
         let mut window_state_borrow = window_state.borrow_mut();
-        let pending_key_down_event = window_state_borrow.pending_key_down_event.take();
+        let pending_key_down = window_state_borrow.pending_key_down.take();
         drop(window_state_borrow);
 
         let is_attributed_string: BOOL =
@@ -1062,24 +1106,17 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
                 .flatten()
                 .is_some();
 
-        if is_composing || text.chars().count() > 1 || pending_key_down_event.is_none() {
+        if is_composing || text.chars().count() > 1 || pending_key_down.is_none() {
             with_input_handler(this, |input_handler| {
                 input_handler.replace_text_in_range(replacement_range, text)
             });
         } else {
-            let mut handled = false;
-
-            let event_callback = window_state.borrow_mut().event_callback.take();
-            if let Some(mut event_callback) = event_callback {
-                handled = event_callback(Event::KeyDown(pending_key_down_event.unwrap()));
-                window_state.borrow_mut().event_callback = Some(event_callback);
-            }
-
-            if !handled {
-                with_input_handler(this, |input_handler| {
-                    input_handler.replace_text_in_range(replacement_range, text)
-                });
-            }
+            let mut pending_key_down = pending_key_down.unwrap();
+            pending_key_down.1 = Some(InsertText {
+                replacement_range,
+                text: text.to_string(),
+            });
+            window_state.borrow_mut().pending_key_down = Some(pending_key_down);
         }
     }
 }
@@ -1092,10 +1129,7 @@ extern "C" fn set_marked_text(
     replacement_range: NSRange,
 ) {
     unsafe {
-        get_window_state(this)
-            .borrow_mut()
-            .pending_key_down_event
-            .take();
+        get_window_state(this).borrow_mut().pending_key_down.take();
 
         let is_attributed_string: BOOL =
             msg_send![text, isKindOfClass: [class!(NSAttributedString)]];

crates/terminal/src/connected_view.rs 🔗

@@ -173,4 +173,12 @@ impl View for ConnectedView {
         self.terminal
             .update(cx, |terminal, _| terminal.write_to_pty(text.into()));
     }
+
+    fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
+        let mut context = Self::default_keymap_context();
+        if self.modal {
+            context.set.insert("ModalTerminal".into());
+        }
+        context
+    }
 }

crates/terminal/src/mappings/keys.rs 🔗

@@ -311,6 +311,7 @@ mod test {
             alt: false,
             shift: false,
             cmd: false,
+            function: false,
             key: "🖖🏻".to_string(), //2 char string
         };
         assert_eq!(to_esc_str(&ks, &TermMode::NONE), None);

crates/terminal/src/terminal.rs 🔗

@@ -171,14 +171,6 @@ impl View for TerminalView {
             cx.focus(view.content.handle());
         });
     }
-
-    fn keymap_context(&self, _: &gpui::AppContext) -> gpui::keymap::Context {
-        let mut context = Self::default_keymap_context();
-        if self.modal {
-            context.set.insert("ModalTerminal".into());
-        }
-        context
-    }
 }
 
 impl View for ErrorView {

crates/vim/src/normal.rs 🔗

@@ -427,7 +427,7 @@ mod test {
     #[gpui::test]
     async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["shift-$"]);
+        let mut cx = cx.binding(["$"]);
         cx.assert("T|est test", "Test tes|t");
         cx.assert("Test tes|t", "Test tes|t");
         cx.assert(
@@ -471,7 +471,7 @@ mod test {
     #[gpui::test]
     async fn test_jump_to_end(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["shift-G"]);
+        let mut cx = cx.binding(["shift-g"]);
 
         cx.assert(
             indoc! {"
@@ -561,7 +561,7 @@ mod test {
         );
 
         for cursor_offset in cursor_offsets {
-            cx.simulate_keystroke("shift-W");
+            cx.simulate_keystroke("shift-w");
             cx.assert_editor_selections(vec![Selection::from_offset(cursor_offset)]);
         }
     }
@@ -607,7 +607,7 @@ mod test {
             Mode::Normal,
         );
         for cursor_offset in cursor_offsets {
-            cx.simulate_keystroke("shift-E");
+            cx.simulate_keystroke("shift-e");
             cx.assert_editor_selections(vec![Selection::from_offset(cursor_offset)]);
         }
     }
@@ -653,7 +653,7 @@ mod test {
             Mode::Normal,
         );
         for cursor_offset in cursor_offsets.into_iter().rev() {
-            cx.simulate_keystroke("shift-B");
+            cx.simulate_keystroke("shift-b");
             cx.assert_editor_selections(vec![Selection::from_offset(cursor_offset)]);
         }
     }
@@ -740,7 +740,7 @@ mod test {
     #[gpui::test]
     async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["shift-A"]).mode_after(Mode::Insert);
+        let mut cx = cx.binding(["shift-a"]).mode_after(Mode::Insert);
         cx.assert("The q|uick", "The quick|");
         cx.assert("The q|uick ", "The quick |");
         cx.assert("|", "|");
@@ -765,7 +765,7 @@ mod test {
     #[gpui::test]
     async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["shift-^"]);
+        let mut cx = cx.binding(["^"]);
         cx.assert("The q|uick", "|The quick");
         cx.assert(" The q|uick", " |The quick");
         cx.assert("|", "|");
@@ -792,7 +792,7 @@ mod test {
     #[gpui::test]
     async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["shift-I"]).mode_after(Mode::Insert);
+        let mut cx = cx.binding(["shift-i"]).mode_after(Mode::Insert);
         cx.assert("The q|uick", "|The quick");
         cx.assert(" The q|uick", " |The quick");
         cx.assert("|", "|");
@@ -817,7 +817,7 @@ mod test {
     #[gpui::test]
     async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["shift-D"]);
+        let mut cx = cx.binding(["shift-d"]);
         cx.assert(
             indoc! {"
                 The q|uick
@@ -858,7 +858,7 @@ mod test {
     #[gpui::test]
     async fn test_delete_left(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["shift-X"]);
+        let mut cx = cx.binding(["shift-x"]);
         cx.assert("Te|st", "T|st");
         cx.assert("T|est", "|est");
         cx.assert("|Test", "|Test");
@@ -956,7 +956,7 @@ mod test {
     #[gpui::test]
     async fn test_insert_line_above(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["shift-O"]).mode_after(Mode::Insert);
+        let mut cx = cx.binding(["shift-o"]).mode_after(Mode::Insert);
 
         cx.assert(
             "|",

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

@@ -139,7 +139,7 @@ mod test {
                 test"},
         );
 
-        let mut cx = cx.binding(["c", "shift-W"]);
+        let mut cx = cx.binding(["c", "shift-w"]);
         cx.assert("Test te|st-test test", "Test te| test");
     }
 
@@ -174,7 +174,7 @@ mod test {
                 test"},
         );
 
-        let mut cx = cx.binding(["c", "shift-E"]);
+        let mut cx = cx.binding(["c", "shift-e"]);
         cx.assert("Test te|st-test test", "Test te| test");
     }
 
@@ -204,14 +204,14 @@ mod test {
                 test"},
         );
 
-        let mut cx = cx.binding(["c", "shift-B"]);
+        let mut cx = cx.binding(["c", "shift-b"]);
         cx.assert("Test test-test |test", "Test |test");
     }
 
     #[gpui::test]
     async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "shift-$"]).mode_after(Mode::Insert);
+        let mut cx = cx.binding(["c", "$"]).mode_after(Mode::Insert);
         cx.assert(
             indoc! {"
                 The q|uick
@@ -347,7 +347,7 @@ mod test {
     #[gpui::test]
     async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["c", "shift-G"]).mode_after(Mode::Insert);
+        let mut cx = cx.binding(["c", "shift-g"]).mode_after(Mode::Insert);
         cx.assert(
             indoc! {"
                 The quick

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

@@ -109,7 +109,7 @@ mod test {
                 test"},
         );
 
-        let mut cx = cx.binding(["d", "shift-W"]);
+        let mut cx = cx.binding(["d", "shift-w"]);
         cx.assert("Test te|st-test test", "Test te|test");
     }
 
@@ -144,7 +144,7 @@ mod test {
                 test"},
         );
 
-        let mut cx = cx.binding(["d", "shift-E"]);
+        let mut cx = cx.binding(["d", "shift-e"]);
         cx.assert("Test te|st-test test", "Test te| test");
     }
 
@@ -176,14 +176,14 @@ mod test {
                 test"},
         );
 
-        let mut cx = cx.binding(["d", "shift-B"]);
+        let mut cx = cx.binding(["d", "shift-b"]);
         cx.assert("Test test-test |test", "Test |test");
     }
 
     #[gpui::test]
     async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "shift-$"]);
+        let mut cx = cx.binding(["d", "$"]);
         cx.assert(
             indoc! {"
                 The q|uick
@@ -304,7 +304,7 @@ mod test {
     #[gpui::test]
     async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["d", "shift-G"]);
+        let mut cx = cx.binding(["d", "shift-g"]);
         cx.assert(
             indoc! {"
                 The quick

crates/vim/src/visual.rs 🔗

@@ -422,7 +422,7 @@ mod test {
     #[gpui::test]
     async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["shift-V", "x"]);
+        let mut cx = cx.binding(["shift-v", "x"]);
         cx.assert(
             indoc! {"
                 The qu|ick brown
@@ -457,7 +457,7 @@ mod test {
                 The quick brown
                 fox ju|mps over"},
         );
-        let mut cx = cx.binding(["shift-V", "j", "x"]);
+        let mut cx = cx.binding(["shift-v", "j", "x"]);
         cx.assert(
             indoc! {"
                 The qu|ick brown
@@ -558,7 +558,7 @@ mod test {
     #[gpui::test]
     async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
         let cx = VimTestContext::new(cx, true).await;
-        let mut cx = cx.binding(["shift-V", "c"]).mode_after(Mode::Insert);
+        let mut cx = cx.binding(["shift-v", "c"]).mode_after(Mode::Insert);
         cx.assert(
             indoc! {"
                 The qu|ick brown
@@ -597,7 +597,7 @@ mod test {
                 fox jumps over
                 |"},
         );
-        let mut cx = cx.binding(["shift-V", "j", "c"]).mode_after(Mode::Insert);
+        let mut cx = cx.binding(["shift-v", "j", "c"]).mode_after(Mode::Insert);
         cx.assert(
             indoc! {"
                 The qu|ick brown

crates/workspace/src/workspace.rs 🔗

@@ -949,11 +949,11 @@ impl Workspace {
         &mut self,
         cx: &mut ViewContext<Self>,
         app_state: Arc<AppState>,
-        mut callback: F,
+        callback: F,
     ) -> T
     where
         T: 'static,
-        F: FnMut(&mut Workspace, &mut ViewContext<Workspace>) -> T,
+        F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
     {
         if self.project.read(cx).is_local() {
             callback(self, cx)

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.49.0"
+version = "0.49.1"
 
 [lib]
 name = "zed"

crates/zed/src/main.rs 🔗

@@ -57,6 +57,7 @@ fn main() {
     fs::create_dir_all(&logs_dir_path).expect("could not create logs path");
     init_logger(&logs_dir_path);
 
+    log::info!("========== starting zed ==========");
     let mut app = gpui::App::new(Assets).unwrap();
     let app_version = ZED_APP_VERSION
         .or_else(|| app.platform().app_version().ok())
@@ -210,6 +211,13 @@ fn init_logger(logs_dir_path: &Path) {
     } else {
         let level = LevelFilter::Info;
         let log_file_path = logs_dir_path.join("Zed.log");
+
+        // Prevent log file from becoming too large.
+        const MAX_LOG_BYTES: u64 = 1 * 1024 * 1024;
+        if fs::metadata(&log_file_path).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES) {
+            let _ = fs::rename(&log_file_path, logs_dir_path.join("Zed.log.old"));
+        }
+
         let log_file = OpenOptions::new()
             .create(true)
             .append(true)

crates/zed/src/zed.rs 🔗

@@ -9,6 +9,7 @@ use anyhow::{anyhow, Context, Result};
 use assets::Assets;
 use breadcrumbs::Breadcrumbs;
 pub use client;
+use collections::VecDeque;
 pub use contacts_panel;
 use contacts_panel::ContactsPanel;
 pub use editor;
@@ -52,6 +53,7 @@ actions!(
         Quit,
         DebugElements,
         OpenSettings,
+        OpenLog,
         OpenKeymap,
         OpenDefaultSettings,
         OpenDefaultKeymap,
@@ -65,9 +67,11 @@ actions!(
 const MIN_FONT_SIZE: f32 = 6.0;
 
 lazy_static! {
-    pub static ref ROOT_PATH: PathBuf = dirs::home_dir()
-        .expect("failed to determine home directory")
-        .join(".zed");
+    pub static ref HOME_PATH: PathBuf =
+        dirs::home_dir().expect("failed to determine home directory");
+    pub static ref LOG_PATH: PathBuf = HOME_PATH.join("Library/Logs/Zed/Zed.log");
+    pub static ref OLD_LOG_PATH: PathBuf = HOME_PATH.join("Library/Logs/Zed/Zed.log.old");
+    pub static ref ROOT_PATH: PathBuf = HOME_PATH.join(".zed");
     pub static ref SETTINGS_PATH: PathBuf = ROOT_PATH.join("settings.json");
     pub static ref KEYMAP_PATH: PathBuf = ROOT_PATH.join("keymap.json");
 }
@@ -120,6 +124,12 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
             });
         }
     });
+    cx.add_action({
+        let app_state = app_state.clone();
+        move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
+            open_log_file(workspace, app_state.clone(), cx);
+        }
+    });
     cx.add_action({
         let app_state = app_state.clone();
         move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
@@ -407,6 +417,60 @@ fn open_config_file(
     .detach_and_log_err(cx)
 }
 
+fn open_log_file(
+    workspace: &mut Workspace,
+    app_state: Arc<AppState>,
+    cx: &mut ViewContext<Workspace>,
+) {
+    const MAX_LINES: usize = 1000;
+
+    workspace.with_local_workspace(cx, app_state.clone(), |_, cx| {
+        cx.spawn_weak(|workspace, mut cx| async move {
+            let (old_log, new_log) = futures::join!(
+                app_state.fs.load(&OLD_LOG_PATH),
+                app_state.fs.load(&LOG_PATH)
+            );
+
+            if let Some(workspace) = workspace.upgrade(&cx) {
+                let mut lines = VecDeque::with_capacity(MAX_LINES);
+                for line in old_log
+                    .iter()
+                    .flat_map(|log| log.lines())
+                    .chain(new_log.iter().flat_map(|log| log.lines()))
+                {
+                    if lines.len() == MAX_LINES {
+                        lines.pop_front();
+                    }
+                    lines.push_back(line);
+                }
+                let log = lines
+                    .into_iter()
+                    .flat_map(|line| [line, "\n"])
+                    .collect::<String>();
+
+                workspace.update(&mut cx, |workspace, cx| {
+                    let project = workspace.project().clone();
+                    let buffer = project
+                        .update(cx, |project, cx| project.create_buffer("", None, cx))
+                        .expect("creating buffers on a local workspace always succeeds");
+                    buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
+
+                    let buffer = cx.add_model(|cx| {
+                        MultiBuffer::singleton(buffer, cx).with_title("Log".into())
+                    });
+                    workspace.add_item(
+                        Box::new(
+                            cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx)),
+                        ),
+                        cx,
+                    );
+                });
+            }
+        })
+        .detach();
+    });
+}
+
 fn open_bundled_config_file(
     workspace: &mut Workspace,
     app_state: Arc<AppState>,