Merge pull request #1405 from zed-industries/ime-support-2

Max Brunsfeld created

Improve support for non-US keyboards and input

Change summary

assets/keymaps/default.json                     |  70 +-
crates/collab/src/integration_tests.rs          |  10 
crates/editor/src/display_map.rs                |  13 
crates/editor/src/display_map/fold_map.rs       |  28 
crates/editor/src/editor.rs                     | 424 ++++++++++++++++--
crates/editor/src/element.rs                    |  66 ++
crates/editor/src/link_go_to_definition.rs      |   1 
crates/editor/src/multi_buffer.rs               | 202 +++++++-
crates/editor/src/multi_buffer/anchor.rs        |  10 
crates/editor/src/test.rs                       |   8 
crates/file_finder/src/file_finder.rs           |  12 
crates/gpui/examples/text.rs                    |  15 
crates/gpui/src/app.rs                          | 237 +++++++++
crates/gpui/src/elements.rs                     |  49 ++
crates/gpui/src/elements/align.rs               |  16 
crates/gpui/src/elements/canvas.rs              |  13 
crates/gpui/src/elements/constrained_box.rs     |  18 
crates/gpui/src/elements/container.rs           |  15 
crates/gpui/src/elements/empty.rs               |  15 
crates/gpui/src/elements/event_handler.rs       |  20 
crates/gpui/src/elements/expanded.rs            |  18 
crates/gpui/src/elements/flex.rs                |  29 +
crates/gpui/src/elements/hook.rs                |  15 
crates/gpui/src/elements/image.rs               |  15 
crates/gpui/src/elements/keystroke_label.rs     |  12 
crates/gpui/src/elements/label.rs               |  15 
crates/gpui/src/elements/list.rs                |  46 ++
crates/gpui/src/elements/mouse_event_handler.rs |  21 
crates/gpui/src/elements/overlay.rs             |  15 
crates/gpui/src/elements/stack.rs               |  18 
crates/gpui/src/elements/svg.rs                 |  15 
crates/gpui/src/elements/text.rs                |  18 
crates/gpui/src/elements/tooltip.rs             |  14 
crates/gpui/src/elements/uniform_list.rs        |  16 
crates/gpui/src/gpui.rs                         |   3 
crates/gpui/src/platform.rs                     |  18 
crates/gpui/src/platform/event.rs               |   2 
crates/gpui/src/platform/mac/event.rs           | 217 +++++----
crates/gpui/src/platform/mac/window.rs          | 353 ++++++++++++++-
crates/gpui/src/platform/test.rs                |   4 
crates/gpui/src/presenter.rs                    |  46 ++
crates/language/src/buffer.rs                   |   4 
crates/language/src/proto.rs                    |  18 
crates/project/src/fs.rs                        |   2 
crates/rpc/proto/zed.proto                      |   3 
crates/rpc/src/rpc.rs                           |   2 
crates/terminal/src/connected_el.rs             |  21 
crates/terminal/src/connected_view.rs           |  18 
crates/terminal/src/mappings/keys.rs            |  54 --
crates/text/src/offset_utf16.rs                 |  50 ++
crates/text/src/rope.rs                         | 194 +++++++-
crates/text/src/tests.rs                        |  39 +
crates/text/src/text.rs                         | 422 ++++++++++--------
crates/theme/src/theme.rs                       |   1 
crates/vim/src/editor_events.rs                 |  17 
crates/vim/src/vim.rs                           |  12 
crates/workspace/src/workspace.rs               |  13 
crates/zed/src/menus.rs                         |   4 
crates/zed/src/zed.rs                           |  12 
styles/package-lock.json                        |   1 
styles/src/styleTree/editor.ts                  |  11 
61 files changed, 2,382 insertions(+), 668 deletions(-)

Detailed changes

assets/keymaps/default.json πŸ”—

@@ -11,22 +11,22 @@
             "enter": "menu::Confirm",
             "escape": "menu::Cancel",
             "ctrl-c": "menu::Cancel",
-            "shift-cmd-{": "pane::ActivatePrevItem",
-            "shift-cmd-}": "pane::ActivateNextItem",
+            "cmd-{": "pane::ActivatePrevItem",
+            "cmd-}": "pane::ActivateNextItem",
             "alt-cmd-left": "pane::ActivatePrevItem",
             "alt-cmd-right": "pane::ActivateNextItem",
             "cmd-w": "pane::CloseActiveItem",
-            "cmd-shift-W": "workspace::CloseWindow",
+            "cmd-shift-w": "workspace::CloseWindow",
             "alt-cmd-t": "pane::CloseInactiveItems",
             "cmd-s": "workspace::Save",
-            "cmd-shift-S": "workspace::SaveAs",
+            "cmd-shift-s": "workspace::SaveAs",
             "cmd-=": "zed::IncreaseBufferFontSize",
             "cmd--": "zed::DecreaseBufferFontSize",
             "cmd-0": "zed::ResetBufferFontSize",
             "cmd-,": "zed::OpenSettings",
             "cmd-q": "zed::Quit",
             "cmd-n": "workspace::NewFile",
-            "cmd-shift-N": "workspace::NewWindow",
+            "cmd-shift-n": "workspace::NewWindow",
             "cmd-o": "workspace::Open"
         }
     },
@@ -53,7 +53,7 @@
             "cmd-c": "editor::Copy",
             "cmd-v": "editor::Paste",
             "cmd-z": "editor::Undo",
-            "cmd-shift-Z": "editor::Redo",
+            "cmd-shift-z": "editor::Redo",
             "up": "editor::MoveUp",
             "down": "editor::MoveDown",
             "left": "editor::MoveLeft",
@@ -73,17 +73,17 @@
             "cmd-up": "editor::MoveToBeginning",
             "cmd-down": "editor::MoveToEnd",
             "shift-up": "editor::SelectUp",
-            "ctrl-shift-P": "editor::SelectUp",
+            "ctrl-shift-p": "editor::SelectUp",
             "shift-down": "editor::SelectDown",
-            "ctrl-shift-N": "editor::SelectDown",
+            "ctrl-shift-n": "editor::SelectDown",
             "shift-left": "editor::SelectLeft",
-            "ctrl-shift-B": "editor::SelectLeft",
+            "ctrl-shift-b": "editor::SelectLeft",
             "shift-right": "editor::SelectRight",
-            "ctrl-shift-F": "editor::SelectRight",
+            "ctrl-shift-f": "editor::SelectRight",
             "alt-shift-left": "editor::SelectToPreviousWordStart",
-            "alt-shift-B": "editor::SelectToPreviousWordStart",
+            "alt-shift-b": "editor::SelectToPreviousWordStart",
             "alt-shift-right": "editor::SelectToNextWordEnd",
-            "alt-shift-F": "editor::SelectToNextWordEnd",
+            "alt-shift-f": "editor::SelectToNextWordEnd",
             "cmd-shift-up": "editor::SelectToBeginning",
             "cmd-shift-down": "editor::SelectToEnd",
             "cmd-a": "editor::SelectAll",
@@ -94,7 +94,7 @@
                     "stop_at_soft_wraps": true
                 }
             ],
-            "ctrl-shift-A": [
+            "ctrl-shift-a": [
                 "editor::SelectToBeginningOfLine",
                 {
                     "stop_at_soft_wraps": true
@@ -106,14 +106,15 @@
                     "stop_at_soft_wraps": true
                 }
             ],
-            "ctrl-shift-E": [
+            "ctrl-shift-e": [
                 "editor::SelectToEndOfLine",
                 {
                     "stop_at_soft_wraps": true
                 }
             ],
             "pageup": "editor::PageUp",
-            "pagedown": "editor::PageDown"
+            "pagedown": "editor::PageDown",
+            "ctrl-cmd-space": "editor::ShowCharacterPalette"
         }
     },
     {
@@ -137,10 +138,7 @@
     {
         "context": "Editor && mode == auto_height",
         "bindings": {
-            "alt-enter": [
-                "editor::Input",
-                "\n"
-            ]
+            "alt-enter": "editor::Newline"
         }
     },
     {
@@ -157,7 +155,7 @@
         "bindings": {
             "cmd-f": "project_search::ToggleFocus",
             "cmd-g": "search::SelectNextMatch",
-            "cmd-shift-G": "search::SelectPrevMatch",
+            "cmd-shift-g": "search::SelectPrevMatch",
             "alt-cmd-c": "search::ToggleCaseSensitive",
             "alt-cmd-w": "search::ToggleWholeWord",
             "alt-cmd-r": "search::ToggleRegex"
@@ -189,7 +187,7 @@
             "alt-up": "editor::SelectLargerSyntaxNode",
             "alt-down": "editor::SelectSmallerSyntaxNode",
             "cmd-u": "editor::UndoSelection",
-            "cmd-shift-U": "editor::RedoSelection",
+            "cmd-shift-u": "editor::RedoSelection",
             "f8": "editor::GoToDiagnostic",
             "shift-f8": "editor::GoToPrevDiagnostic",
             "f2": "editor::Rename",
@@ -205,7 +203,7 @@
     {
         "context": "Editor && mode == full",
         "bindings": {
-            "cmd-shift-O": "outline::Toggle",
+            "cmd-shift-o": "outline::Toggle",
             "ctrl-g": "go_to_line::Toggle"
         }
     },
@@ -250,9 +248,9 @@
             ],
             "ctrl-0": "pane::ActivateLastItem",
             "ctrl--": "pane::GoBack",
-            "shift-ctrl-_": "pane::GoForward",
-            "cmd-shift-T": "pane::ReopenClosedItem",
-            "cmd-shift-F": "project_search::ToggleFocus"
+            "ctrl-_": "pane::GoForward",
+            "cmd-shift-t": "pane::ReopenClosedItem",
+            "cmd-shift-f": "project_search::ToggleFocus"
         }
     },
     {
@@ -295,14 +293,14 @@
                 8
             ],
             "cmd-b": "workspace::ToggleLeftSidebar",
-            "cmd-shift-F": "project_search::Deploy",
+            "cmd-shift-f": "project_search::Deploy",
             "cmd-k cmd-t": "theme_selector::Toggle",
             "cmd-k cmd-s": "zed::OpenKeymap",
             "cmd-t": "project_symbols::Toggle",
             "cmd-p": "file_finder::Toggle",
-            "cmd-shift-P": "command_palette::Toggle",
-            "cmd-shift-M": "diagnostics::Deploy",
-            "cmd-shift-E": "project_panel::Toggle",
+            "cmd-shift-p": "command_palette::Toggle",
+            "cmd-shift-m": "diagnostics::Deploy",
+            "cmd-shift-e": "project_panel::Toggle",
             "cmd-alt-s": "workspace::SaveAll"
         }
     },
@@ -310,9 +308,9 @@
     {
         "context": "Editor",
         "bindings": {
-            "ctrl-shift-K": "editor::DeleteLine",
-            "cmd-shift-D": "editor::DuplicateLine",
-            "cmd-shift-L": "editor::SplitSelectionIntoLines",
+            "ctrl-shift-k": "editor::DeleteLine",
+            "cmd-shift-d": "editor::DuplicateLine",
+            "cmd-shift-l": "editor::SplitSelectionIntoLines",
             "ctrl-cmd-up": "editor::MoveLineUp",
             "ctrl-cmd-down": "editor::MoveLineDown",
             "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
@@ -324,9 +322,9 @@
             "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
             "ctrl-alt-f": "editor::MoveToNextSubwordEnd",
             "ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
-            "ctrl-alt-shift-B": "editor::SelectToPreviousSubwordStart",
+            "ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
             "ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
-            "ctrl-alt-shift-F": "editor::SelectToNextSubwordEnd"
+            "ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
         }
     },
     {
@@ -387,8 +385,8 @@
     {
         "context": "Workspace",
         "bindings": {
-            "cmd-shift-C": "contacts_panel::Toggle",
-            "cmd-shift-B": "workspace::ToggleRightSidebar"
+            "cmd-shift-c": "contacts_panel::Toggle",
+            "cmd-shift-b": "workspace::ToggleRightSidebar"
         }
     },
     {

crates/collab/src/integration_tests.rs πŸ”—

@@ -11,8 +11,8 @@ use client::{
 };
 use collections::{BTreeMap, HashMap, HashSet};
 use editor::{
-    self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
-    ToOffset, ToggleCodeActions, Undo,
+    self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset,
+    ToggleCodeActions, Undo,
 };
 use futures::{channel::mpsc, Future, StreamExt as _};
 use gpui::{
@@ -154,9 +154,7 @@ async fn test_share_project(
     //     .await;
 
     // Edit the buffer as client B and see that edit as client A.
-    editor_b.update(cx_b, |editor, cx| {
-        editor.handle_input(&Input("ok, ".into()), cx)
-    });
+    editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
     buffer_a
         .condition(&cx_a, |buffer, _| buffer.text() == "ok, b-contents")
         .await;
@@ -1751,7 +1749,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
     // Type a completion trigger character as the guest.
     editor_b.update(cx_b, |editor, cx| {
         editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
-        editor.handle_input(&Input(".".into()), cx);
+        editor.handle_input(".", cx);
         cx.focus(&editor_b);
     });
 

crates/editor/src/display_map.rs πŸ”—

@@ -11,7 +11,7 @@ use gpui::{
     fonts::{FontId, HighlightStyle},
     Entity, ModelContext, ModelHandle,
 };
-use language::{Point, Subscription as BufferSubscription};
+use language::{OffsetUtf16, Point, Subscription as BufferSubscription};
 use settings::Settings;
 use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
 use sum_tree::{Bias, TreeMap};
@@ -195,6 +195,11 @@ impl DisplayMap {
             .insert(Some(type_id), Arc::new((style, ranges)));
     }
 
+    pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
+        let highlights = self.text_highlights.get(&Some(type_id))?;
+        Some((highlights.0, &highlights.1))
+    }
+
     pub fn clear_text_highlights(
         &mut self,
         type_id: TypeId,
@@ -544,6 +549,12 @@ impl ToDisplayPoint for usize {
     }
 }
 
+impl ToDisplayPoint for OffsetUtf16 {
+    fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
+        self.to_offset(&map.buffer_snapshot).to_display_point(map)
+    }
+}
+
 impl ToDisplayPoint for Point {
     fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
         map.point_to_display_point(*self, Bias::Left)

crates/editor/src/display_map/fold_map.rs πŸ”—

@@ -63,14 +63,14 @@ impl FoldPoint {
             .cursor::<(FoldPoint, TransformSummary)>();
         cursor.seek(self, Bias::Right, &());
         let overshoot = self.0 - cursor.start().1.output.lines;
-        let mut offset = cursor.start().1.output.bytes;
+        let mut offset = cursor.start().1.output.len;
         if !overshoot.is_zero() {
             let transform = cursor.item().expect("display point out of range");
             assert!(transform.output_text.is_none());
             let end_buffer_offset = snapshot
                 .buffer_snapshot
                 .point_to_offset(cursor.start().1.input.lines + overshoot);
-            offset += end_buffer_offset - cursor.start().1.input.bytes;
+            offset += end_buffer_offset - cursor.start().1.input.len;
         }
         FoldOffset(offset)
     }
@@ -249,7 +249,7 @@ impl FoldMap {
     fn check_invariants(&self) {
         if cfg!(test) {
             assert_eq!(
-                self.transforms.lock().summary().input.bytes,
+                self.transforms.lock().summary().input.len,
                 self.buffer.lock().len(),
                 "transform tree does not match buffer's length"
             );
@@ -341,7 +341,7 @@ impl FoldMap {
                     let mut fold = folds.next().unwrap();
                     let sum = new_transforms.summary();
 
-                    assert!(fold.start >= sum.input.bytes);
+                    assert!(fold.start >= sum.input.len);
 
                     while folds
                         .peek()
@@ -353,9 +353,9 @@ impl FoldMap {
                         }
                     }
 
-                    if fold.start > sum.input.bytes {
+                    if fold.start > sum.input.len {
                         let text_summary = new_buffer
-                            .text_summary_for_range::<TextSummary, _>(sum.input.bytes..fold.start);
+                            .text_summary_for_range::<TextSummary, _>(sum.input.len..fold.start);
                         new_transforms.push(
                             Transform {
                                 summary: TransformSummary {
@@ -384,9 +384,9 @@ impl FoldMap {
                 }
 
                 let sum = new_transforms.summary();
-                if sum.input.bytes < edit.new.end {
+                if sum.input.len < edit.new.end {
                     let text_summary = new_buffer
-                        .text_summary_for_range::<TextSummary, _>(sum.input.bytes..edit.new.end);
+                        .text_summary_for_range::<TextSummary, _>(sum.input.len..edit.new.end);
                     new_transforms.push(
                         Transform {
                             summary: TransformSummary {
@@ -558,7 +558,7 @@ impl FoldSnapshot {
     }
 
     pub fn len(&self) -> FoldOffset {
-        FoldOffset(self.transforms.summary().output.bytes)
+        FoldOffset(self.transforms.summary().output.len)
     }
 
     pub fn line_len(&self, row: u32) -> u32 {
@@ -766,7 +766,7 @@ impl FoldSnapshot {
                 )
             }
         } else {
-            FoldOffset(self.transforms.summary().output.bytes)
+            FoldOffset(self.transforms.summary().output.len)
         }
     }
 
@@ -1050,7 +1050,7 @@ impl<'a> Iterator for FoldChunks<'a> {
         // advance the transform and buffer cursors to the end of the fold.
         if let Some(output_text) = transform.output_text {
             self.buffer_chunk.take();
-            self.buffer_offset += transform.summary.input.bytes;
+            self.buffer_offset += transform.summary.input.len;
             self.buffer_chunks.seek(self.buffer_offset);
 
             while self.buffer_offset >= self.transform_cursor.end(&()).1
@@ -1158,7 +1158,7 @@ impl FoldOffset {
         let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) {
             Point::new(0, (self.0 - cursor.start().0 .0) as u32)
         } else {
-            let buffer_offset = cursor.start().1.input.bytes + self.0 - cursor.start().0 .0;
+            let buffer_offset = cursor.start().1.input.len + self.0 - cursor.start().0 .0;
             let buffer_point = snapshot.buffer_snapshot.offset_to_point(buffer_offset);
             buffer_point - cursor.start().1.input.lines
         };
@@ -1176,7 +1176,7 @@ impl Sub for FoldOffset {
 
 impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldOffset {
     fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
-        self.0 += &summary.output.bytes;
+        self.0 += &summary.output.len;
     }
 }
 
@@ -1188,7 +1188,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point {
 
 impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize {
     fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
-        *self += &summary.input.bytes;
+        *self += &summary.input.len;
     }
 }
 

crates/editor/src/editor.rs πŸ”—

@@ -39,15 +39,15 @@ pub use items::MAX_TAB_TITLE_LEN;
 pub use language::{char_kind, CharKind};
 use language::{
     BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity,
-    IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal,
+    IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal,
     TransactionId,
 };
 use link_go_to_definition::LinkGoToDefinitionState;
-use multi_buffer::MultiBufferChunks;
 pub use multi_buffer::{
     Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
     ToPoint,
 };
+use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
 use ordered_float::OrderedFloat;
 use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
 use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
@@ -95,9 +95,6 @@ pub struct Jump {
     anchor: language::Anchor,
 }
 
-#[derive(Clone, Deserialize, PartialEq)]
-pub struct Input(pub String);
-
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct SelectToBeginningOfLine {
     #[serde(default)]
@@ -186,6 +183,7 @@ actions!(
         Tab,
         TabPrev,
         ToggleComments,
+        ShowCharacterPalette,
         SelectLargerSyntaxNode,
         SelectSmallerSyntaxNode,
         GoToDefinition,
@@ -210,7 +208,6 @@ actions!(
 impl_actions!(
     editor,
     [
-        Input,
         SelectNext,
         SelectToBeginningOfLine,
         SelectToEndOfLine,
@@ -224,6 +221,7 @@ impl_internal_actions!(editor, [Scroll, Select, Jump]);
 
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
+enum InputComposition {}
 
 #[derive(Copy, Clone, PartialEq, Eq)]
 pub enum Direction {
@@ -236,7 +234,6 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
     cx.add_action(Editor::select);
     cx.add_action(Editor::cancel);
-    cx.add_action(Editor::handle_input);
     cx.add_action(Editor::newline);
     cx.add_action(Editor::backspace);
     cx.add_action(Editor::delete);
@@ -310,6 +307,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::open_excerpts);
     cx.add_action(Editor::jump);
     cx.add_action(Editor::restart_language_server);
+    cx.add_action(Editor::show_character_palette);
     cx.add_async_action(Editor::confirm_completion);
     cx.add_async_action(Editor::confirm_code_action);
     cx.add_async_action(Editor::rename);
@@ -405,6 +403,7 @@ pub struct Editor {
     autoclose_stack: InvalidationStack<BracketPairState>,
     snippet_stack: InvalidationStack<SnippetState>,
     select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
+    ime_transaction: Option<TransactionId>,
     active_diagnostics: Option<ActiveDiagnosticGroup>,
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
@@ -992,6 +991,7 @@ impl Editor {
             autoclose_stack: Default::default(),
             snippet_stack: Default::default(),
             select_larger_syntax_node_stack: Vec::new(),
+            ime_transaction: Default::default(),
             active_diagnostics: None,
             soft_wrap_mode_override: None,
             get_field_editor_theme,
@@ -1808,13 +1808,11 @@ impl Editor {
         cx.propagate_action();
     }
 
-    pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext<Self>) {
+    pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
         if !self.input_enabled {
-            cx.propagate_action();
             return;
         }
 
-        let text = action.0.as_ref();
         if !self.skip_autoclose_end(text, cx) {
             self.transact(cx, |this, cx| {
                 if !this.surround_with_bracket_pair(text, cx) {
@@ -2481,14 +2479,17 @@ impl Editor {
                 });
                 if let Some((_, excerpted_buffer, excerpt_range)) = excerpt {
                     if excerpted_buffer == *buffer {
-                        let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
-                        let excerpt_range = excerpt_range.to_offset(&snapshot);
-                        if snapshot
-                            .edited_ranges_for_transaction(transaction)
-                            .all(|range| {
-                                excerpt_range.start <= range.start && excerpt_range.end >= range.end
-                            })
-                        {
+                        let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| {
+                            let excerpt_range = excerpt_range.to_offset(buffer);
+                            buffer
+                                .edited_ranges_for_transaction(transaction)
+                                .all(|range| {
+                                    excerpt_range.start <= range.start
+                                        && excerpt_range.end >= range.end
+                                })
+                        });
+
+                        if all_edits_within_excerpt {
                             return Ok(());
                         }
                     }
@@ -2501,12 +2502,12 @@ impl Editor {
         let mut ranges_to_highlight = Vec::new();
         let excerpt_buffer = cx.add_model(|cx| {
             let mut multibuffer = MultiBuffer::new(replica_id).with_title(title);
-            for (buffer, transaction) in &entries {
-                let snapshot = buffer.read(cx).snapshot();
+            for (buffer_handle, transaction) in &entries {
+                let buffer = buffer_handle.read(cx);
                 ranges_to_highlight.extend(
                     multibuffer.push_excerpts_with_context_lines(
-                        buffer.clone(),
-                        snapshot
+                        buffer_handle.clone(),
+                        buffer
                             .edited_ranges_for_transaction::<usize>(transaction)
                             .collect(),
                         1,
@@ -3614,6 +3615,7 @@ impl Editor {
                 });
             }
             self.request_autoscroll(Autoscroll::Fit, cx);
+            self.unmark_text(cx);
             cx.emit(Event::Edited);
         }
     }
@@ -3627,6 +3629,7 @@ impl Editor {
                 });
             }
             self.request_autoscroll(Autoscroll::Fit, cx);
+            self.unmark_text(cx);
             cx.emit(Event::Edited);
         }
     }
@@ -5026,6 +5029,10 @@ impl Editor {
         }
     }
 
+    fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
+        cx.show_character_palette();
+    }
+
     fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext<Editor>) {
         if let Some(active_diagnostics) = self.active_diagnostics.as_mut() {
             let buffer = self.buffer.read(cx).snapshot(cx);
@@ -5151,10 +5158,10 @@ impl Editor {
         &mut self,
         cx: &mut ViewContext<Self>,
         update: impl FnOnce(&mut Self, &mut ViewContext<Self>),
-    ) {
+    ) -> Option<TransactionId> {
         self.start_transaction_at(Instant::now(), cx);
         update(self, cx);
-        self.end_transaction_at(Instant::now(), cx);
+        self.end_transaction_at(Instant::now(), cx)
     }
 
     fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext<Self>) {
@@ -5168,7 +5175,11 @@ impl Editor {
         }
     }
 
-    fn end_transaction_at(&mut self, now: Instant, cx: &mut ViewContext<Self>) {
+    fn end_transaction_at(
+        &mut self,
+        now: Instant,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<TransactionId> {
         if let Some(tx_id) = self
             .buffer
             .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
@@ -5180,6 +5191,9 @@ impl Editor {
             }
 
             cx.emit(Event::Edited);
+            Some(tx_id)
+        } else {
+            None
         }
     }
 
@@ -5528,6 +5542,13 @@ impl Editor {
         cx.notify();
     }
 
+    pub fn text_highlights<'a, T: 'static>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> Option<(HighlightStyle, &'a [Range<Anchor>])> {
+        self.display_map.read(cx).text_highlights(TypeId::of::<T>())
+    }
+
     pub fn clear_text_highlights<T: 'static>(
         &mut self,
         cx: &mut ViewContext<Self>,
@@ -5718,6 +5739,44 @@ impl Editor {
         })
         .detach()
     }
+
+    fn marked_text_ranges(&self, cx: &AppContext) -> Option<Vec<Range<OffsetUtf16>>> {
+        let snapshot = self.buffer.read(cx).read(cx);
+        let (_, ranges) = self.text_highlights::<InputComposition>(cx)?;
+        Some(
+            ranges
+                .into_iter()
+                .map(move |range| {
+                    range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot)
+                })
+                .collect(),
+        )
+    }
+
+    fn selection_replacement_ranges(
+        &self,
+        range: Range<OffsetUtf16>,
+        cx: &AppContext,
+    ) -> Vec<Range<OffsetUtf16>> {
+        let selections = self.selections.all::<OffsetUtf16>(cx);
+        let newest_selection = selections
+            .iter()
+            .max_by_key(|selection| selection.id)
+            .unwrap();
+        let start_delta = range.start.0 as isize - newest_selection.start.0 as isize;
+        let end_delta = range.end.0 as isize - newest_selection.end.0 as isize;
+        let snapshot = self.buffer.read(cx).read(cx);
+        selections
+            .into_iter()
+            .map(|mut selection| {
+                selection.start.0 =
+                    (selection.start.0 as isize).saturating_add(start_delta) as usize;
+                selection.end.0 = (selection.end.0 as isize).saturating_add(end_delta) as usize;
+                snapshot.clip_offset_utf16(selection.start, Bias::Left)
+                    ..snapshot.clip_offset_utf16(selection.end, Bias::Right)
+            })
+            .collect()
+    }
 }
 
 impl EditorSnapshot {
@@ -5773,6 +5832,7 @@ pub enum Event {
     SelectionsChanged { local: bool },
     ScrollPositionChanged { local: bool },
     Closed,
+    IgnoredInput,
 }
 
 pub struct EditorFocused(pub ViewHandle<Editor>);
@@ -5877,6 +5937,168 @@ impl View for Editor {
 
         context
     }
+
+    fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> {
+        Some(
+            self.buffer
+                .read(cx)
+                .read(cx)
+                .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end))
+                .collect(),
+        )
+    }
+
+    fn selected_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
+        // Prevent the IME menu from appearing when holding down an alphabetic key
+        // while input is disabled.
+        if !self.input_enabled {
+            return None;
+        }
+
+        let range = self.selections.newest::<OffsetUtf16>(cx).range();
+        Some(range.start.0..range.end.0)
+    }
+
+    fn marked_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
+        let snapshot = self.buffer.read(cx).read(cx);
+        let range = self.text_highlights::<InputComposition>(cx)?.1.get(0)?;
+        Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0)
+    }
+
+    fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
+        self.clear_text_highlights::<InputComposition>(cx);
+        self.ime_transaction.take();
+    }
+
+    fn replace_text_in_range(
+        &mut self,
+        range_utf16: Option<Range<usize>>,
+        text: &str,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if !self.input_enabled {
+            cx.emit(Event::IgnoredInput);
+            return;
+        }
+
+        self.transact(cx, |this, cx| {
+            let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
+                let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
+                Some(this.selection_replacement_ranges(range_utf16, cx))
+            } else if let Some(marked_ranges) = this.marked_text_ranges(cx) {
+                Some(marked_ranges)
+            } else {
+                None
+            };
+
+            if let Some(new_selected_ranges) = new_selected_ranges {
+                this.change_selections(None, cx, |selections| {
+                    selections.select_ranges(new_selected_ranges)
+                });
+            }
+            this.handle_input(text, cx);
+        });
+
+        if let Some(transaction) = self.ime_transaction {
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.group_until_transaction(transaction, cx);
+            });
+        }
+
+        self.unmark_text(cx);
+    }
+
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        range_utf16: Option<Range<usize>>,
+        text: &str,
+        new_selected_range_utf16: Option<Range<usize>>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if !self.input_enabled {
+            cx.emit(Event::IgnoredInput);
+            return;
+        }
+
+        let transaction = self.transact(cx, |this, cx| {
+            let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) {
+                let snapshot = this.buffer.read(cx).read(cx);
+                if let Some(relative_range_utf16) = range_utf16.as_ref() {
+                    for marked_range in &mut marked_ranges {
+                        marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end;
+                        marked_range.start.0 += relative_range_utf16.start;
+                        marked_range.start =
+                            snapshot.clip_offset_utf16(marked_range.start, Bias::Left);
+                        marked_range.end =
+                            snapshot.clip_offset_utf16(marked_range.end, Bias::Right);
+                    }
+                }
+                Some(marked_ranges)
+            } else if let Some(range_utf16) = range_utf16 {
+                let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
+                Some(this.selection_replacement_ranges(range_utf16, cx))
+            } else {
+                None
+            };
+
+            if let Some(ranges) = ranges_to_replace {
+                this.change_selections(None, cx, |s| s.select_ranges(ranges));
+            }
+
+            let marked_ranges = {
+                let snapshot = this.buffer.read(cx).read(cx);
+                this.selections
+                    .disjoint_anchors()
+                    .into_iter()
+                    .map(|selection| {
+                        selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot)
+                    })
+                    .collect::<Vec<_>>()
+            };
+
+            if text.is_empty() {
+                this.unmark_text(cx);
+            } else {
+                this.highlight_text::<InputComposition>(
+                    marked_ranges.clone(),
+                    this.style(cx).composition_mark,
+                    cx,
+                );
+            }
+
+            this.handle_input(text, cx);
+
+            if let Some(new_selected_range) = new_selected_range_utf16 {
+                let snapshot = this.buffer.read(cx).read(cx);
+                let new_selected_ranges = marked_ranges
+                    .into_iter()
+                    .map(|marked_range| {
+                        let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0;
+                        let new_start = OffsetUtf16(new_selected_range.start + insertion_start);
+                        let new_end = OffsetUtf16(new_selected_range.end + insertion_start);
+                        snapshot.clip_offset_utf16(new_start, Bias::Left)
+                            ..snapshot.clip_offset_utf16(new_end, Bias::Right)
+                    })
+                    .collect::<Vec<_>>();
+
+                drop(snapshot);
+                this.change_selections(None, cx, |selections| {
+                    selections.select_ranges(new_selected_ranges)
+                });
+            }
+        });
+
+        self.ime_transaction = self.ime_transaction.or(transaction);
+        if let Some(transaction) = self.ime_transaction {
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.group_until_transaction(transaction, cx);
+            });
+        }
+
+        if self.text_highlights::<InputComposition>(cx).is_none() {
+            self.ime_transaction.take();
+        }
+    }
 }
 
 fn build_style(
@@ -6473,6 +6695,108 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    fn test_ime_composition(cx: &mut MutableAppContext) {
+        cx.set_global(Settings::test(cx));
+        let buffer = cx.add_model(|cx| {
+            let mut buffer = language::Buffer::new(0, "abcde", cx);
+            // Ensure automatic grouping doesn't occur.
+            buffer.set_group_interval(Duration::ZERO);
+            buffer
+        });
+
+        let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+        cx.add_window(Default::default(), |cx| {
+            let mut editor = build_editor(buffer.clone(), cx);
+
+            // Start a new IME composition.
+            editor.replace_and_mark_text_in_range(Some(0..1), "Γ ", None, cx);
+            editor.replace_and_mark_text_in_range(Some(0..1), "Γ‘", None, cx);
+            editor.replace_and_mark_text_in_range(Some(0..1), "Γ€", None, cx);
+            assert_eq!(editor.text(cx), "Γ€bcde");
+            assert_eq!(
+                editor.marked_text_ranges(cx),
+                Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
+            );
+
+            // Finalize IME composition.
+            editor.replace_text_in_range(None, "ā", cx);
+            assert_eq!(editor.text(cx), "ābcde");
+            assert_eq!(editor.marked_text_ranges(cx), None);
+
+            // IME composition edits are grouped and are undone/redone at once.
+            editor.undo(&Default::default(), cx);
+            assert_eq!(editor.text(cx), "abcde");
+            assert_eq!(editor.marked_text_ranges(cx), None);
+            editor.redo(&Default::default(), cx);
+            assert_eq!(editor.text(cx), "ābcde");
+            assert_eq!(editor.marked_text_ranges(cx), None);
+
+            // Start a new IME composition.
+            editor.replace_and_mark_text_in_range(Some(0..1), "Γ ", None, cx);
+            assert_eq!(
+                editor.marked_text_ranges(cx),
+                Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
+            );
+
+            // Undoing during an IME composition cancels it.
+            editor.undo(&Default::default(), cx);
+            assert_eq!(editor.text(cx), "ābcde");
+            assert_eq!(editor.marked_text_ranges(cx), None);
+
+            // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
+            editor.replace_and_mark_text_in_range(Some(4..999), "Γ¨", None, cx);
+            assert_eq!(editor.text(cx), "ābcdè");
+            assert_eq!(
+                editor.marked_text_ranges(cx),
+                Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
+            );
+
+            // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
+            editor.replace_text_in_range(Some(4..999), "Δ™", cx);
+            assert_eq!(editor.text(cx), "ābcdΔ™");
+            assert_eq!(editor.marked_text_ranges(cx), None);
+
+            // Start a new IME composition with multiple cursors.
+            editor.change_selections(None, cx, |s| {
+                s.select_ranges([
+                    OffsetUtf16(1)..OffsetUtf16(1),
+                    OffsetUtf16(3)..OffsetUtf16(3),
+                    OffsetUtf16(5)..OffsetUtf16(5),
+                ])
+            });
+            editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx);
+            assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
+            assert_eq!(
+                editor.marked_text_ranges(cx),
+                Some(vec![
+                    OffsetUtf16(0)..OffsetUtf16(3),
+                    OffsetUtf16(4)..OffsetUtf16(7),
+                    OffsetUtf16(8)..OffsetUtf16(11)
+                ])
+            );
+
+            // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
+            editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx);
+            assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
+            assert_eq!(
+                editor.marked_text_ranges(cx),
+                Some(vec![
+                    OffsetUtf16(1)..OffsetUtf16(2),
+                    OffsetUtf16(5)..OffsetUtf16(6),
+                    OffsetUtf16(9)..OffsetUtf16(10)
+                ])
+            );
+
+            // Finalize IME composition with multiple cursors.
+            editor.replace_text_in_range(Some(9..10), "2", cx);
+            assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
+            assert_eq!(editor.marked_text_ranges(cx), None);
+
+            editor
+        });
+    }
+
     #[gpui::test]
     fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));
@@ -8247,9 +8571,9 @@ mod tests {
         // is pasted at each cursor.
         cx.set_state("|two oneβœ… four three six five |");
         cx.update_editor(|e, cx| {
-            e.handle_input(&Input("( ".into()), cx);
+            e.handle_input("( ", cx);
             e.paste(&Paste, cx);
-            e.handle_input(&Input(") ".into()), cx);
+            e.handle_input(") ", cx);
         });
         cx.assert_editor_state(indoc! {"
             ( oneβœ… 
@@ -8924,9 +9248,9 @@ mod tests {
                 ])
             });
 
-            view.handle_input(&Input("{".to_string()), cx);
-            view.handle_input(&Input("{".to_string()), cx);
-            view.handle_input(&Input("{".to_string()), cx);
+            view.handle_input("{", cx);
+            view.handle_input("{", cx);
+            view.handle_input("{", cx);
             assert_eq!(
                 view.text(cx),
                 "
@@ -8939,9 +9263,9 @@ mod tests {
             );
 
             view.move_right(&MoveRight, cx);
-            view.handle_input(&Input("}".to_string()), cx);
-            view.handle_input(&Input("}".to_string()), cx);
-            view.handle_input(&Input("}".to_string()), cx);
+            view.handle_input("}", cx);
+            view.handle_input("}", cx);
+            view.handle_input("}", cx);
             assert_eq!(
                 view.text(cx),
                 "
@@ -8954,8 +9278,8 @@ mod tests {
             );
 
             view.undo(&Undo, cx);
-            view.handle_input(&Input("/".to_string()), cx);
-            view.handle_input(&Input("*".to_string()), cx);
+            view.handle_input("/", cx);
+            view.handle_input("*", cx);
             assert_eq!(
                 view.text(cx),
                 "
@@ -8974,7 +9298,7 @@ mod tests {
                     DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
                 ])
             });
-            view.handle_input(&Input("*".to_string()), cx);
+            view.handle_input("*", cx);
             assert_eq!(
                 view.text(cx),
                 "
@@ -8992,7 +9316,7 @@ mod tests {
             view.change_selections(None, cx, |s| {
                 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
             });
-            view.handle_input(&Input("{".to_string()), cx);
+            view.handle_input("{", cx);
             assert_eq!(
                 view.text(cx),
                 "
@@ -9008,7 +9332,7 @@ mod tests {
             view.change_selections(None, cx, |s| {
                 s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1)])
             });
-            view.handle_input(&Input("{".to_string()), cx);
+            view.handle_input("{", cx);
             assert_eq!(
                 view.text(cx),
                 "
@@ -9025,7 +9349,7 @@ mod tests {
             );
 
             view.undo(&Undo, cx);
-            view.handle_input(&Input("[".to_string()), cx);
+            view.handle_input("[", cx);
             assert_eq!(
                 view.text(cx),
                 "
@@ -9045,7 +9369,7 @@ mod tests {
             view.change_selections(None, cx, |s| {
                 s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)])
             });
-            view.handle_input(&Input("[".to_string()), cx);
+            view.handle_input("[", cx);
             assert_eq!(
                 view.text(cx),
                 "
@@ -9101,9 +9425,9 @@ mod tests {
                 ])
             });
 
-            view.handle_input(&Input("{".to_string()), cx);
-            view.handle_input(&Input("{".to_string()), cx);
-            view.handle_input(&Input("{".to_string()), cx);
+            view.handle_input("{", cx);
+            view.handle_input("{", cx);
+            view.handle_input("{", cx);
             assert_eq!(
                 view.text(cx),
                 "
@@ -9183,9 +9507,9 @@ mod tests {
                 ])
             });
 
-            editor.handle_input(&Input("{".to_string()), cx);
-            editor.handle_input(&Input("{".to_string()), cx);
-            editor.handle_input(&Input("_".to_string()), cx);
+            editor.handle_input("{", cx);
+            editor.handle_input("{", cx);
+            editor.handle_input("_", cx);
             assert_eq!(
                 editor.text(cx),
                 "
@@ -9699,7 +10023,9 @@ mod tests {
         cx.set_state("editor|");
         cx.simulate_keystroke(".");
         assert!(cx.editor(|e, _| e.context_menu.is_none()));
-        cx.simulate_keystrokes(["c", "l", "o"]);
+        cx.simulate_keystroke("c");
+        cx.simulate_keystroke("l");
+        cx.simulate_keystroke("o");
         cx.assert_editor_state("editor.clo|");
         assert!(cx.editor(|e, _| e.context_menu.is_none()));
         cx.update_editor(|editor, cx| {
@@ -9911,7 +10237,7 @@ mod tests {
                 ])
             });
 
-            view.handle_input(&Input("X".to_string()), cx);
+            view.handle_input("X", cx);
             assert_eq!(view.text(cx), "Xaaaa\nXbbbb");
             assert_eq!(
                 view.selections.ranges(cx),
@@ -9951,7 +10277,7 @@ mod tests {
             assert_eq!(view.text(cx), expected_text);
             view.change_selections(None, cx, |s| s.select_ranges(selection_ranges));
 
-            view.handle_input(&Input("X".to_string()), cx);
+            view.handle_input("X", cx);
 
             let (expected_text, expected_selections) = marked_text_ranges(indoc! {"
                 aaaa

crates/editor/src/element.rs πŸ”—

@@ -1,6 +1,6 @@
 use super::{
     display_map::{BlockContext, ToDisplayPoint},
-    Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Input, Scroll, Select, SelectPhase,
+    Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Scroll, Select, SelectPhase,
     SoftWrap, ToPoint, MAX_LINE_LEN,
 };
 use crate::{
@@ -24,13 +24,13 @@ use gpui::{
     json::{self, ToJson},
     platform::CursorStyle,
     text_layout::{self, Line, RunStyle, TextLayoutCache},
-    AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext, KeyDownEvent,
+    AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext,
     LayoutContext, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent,
     MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext,
     WeakViewHandle,
 };
 use json::json;
-use language::{Bias, DiagnosticSeverity, Selection};
+use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
 use project::ProjectPath;
 use settings::Settings;
 use smallvec::SmallVec;
@@ -283,21 +283,6 @@ impl EditorElement {
         true
     }
 
-    fn key_down(&self, input: Option<&str>, cx: &mut EventContext) -> bool {
-        let view = self.view.upgrade(cx.app).unwrap();
-
-        if view.is_focused(cx.app) {
-            if let Some(input) = input {
-                cx.dispatch_action(Input(input.to_string()));
-                true
-            } else {
-                false
-            }
-        } else {
-            false
-        }
-    }
-
     fn modifiers_changed(&self, cmd: bool, cx: &mut EventContext) -> bool {
         cx.dispatch_action(CmdChanged { cmd_down: cmd });
         false
@@ -1569,7 +1554,6 @@ impl Element for EditorElement {
                 delta,
                 precise,
             }) => self.scroll(*position, *delta, *precise, layout, paint, cx),
-            Event::KeyDown(KeyDownEvent { input, .. }) => self.key_down(input.as_deref(), cx),
             Event::ModifiersChanged(ModifiersChangedEvent { cmd, .. }) => {
                 self.modifiers_changed(*cmd, cx)
             }
@@ -1581,6 +1565,43 @@ impl Element for EditorElement {
         }
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        bounds: RectF,
+        _: RectF,
+        layout: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &gpui::MeasurementContext,
+    ) -> Option<RectF> {
+        let text_bounds = RectF::new(
+            bounds.origin() + vec2f(layout.gutter_size.x(), 0.0),
+            layout.text_size,
+        );
+        let content_origin = text_bounds.origin() + vec2f(layout.gutter_margin, 0.);
+        let scroll_position = layout.snapshot.scroll_position();
+        let start_row = scroll_position.y() as u32;
+        let scroll_top = scroll_position.y() * layout.line_height;
+        let scroll_left = scroll_position.x() * layout.em_width;
+
+        let range_start =
+            OffsetUtf16(range_utf16.start).to_display_point(&layout.snapshot.display_snapshot);
+        if range_start.row() < start_row {
+            return None;
+        }
+
+        let line = layout
+            .line_layouts
+            .get((range_start.row() - start_row) as usize)?;
+        let range_start_x = line.x_for_index(range_start.column() as usize);
+        let range_start_y = range_start.row() as f32 * layout.line_height;
+        Some(RectF::new(
+            content_origin + vec2f(range_start_x, range_start_y + layout.line_height)
+                - vec2f(scroll_left, scroll_top),
+            vec2f(layout.em_width, layout.line_height),
+        ))
+    }
+
     fn debug(
         &self,
         bounds: RectF,
@@ -1740,6 +1761,13 @@ impl Cursor {
         }
     }
 
+    pub fn bounding_rect(&self, origin: Vector2F) -> RectF {
+        RectF::new(
+            self.origin + origin,
+            vec2f(self.block_width, self.line_height),
+        )
+    }
+
     pub fn paint(&self, origin: Vector2F, cx: &mut PaintContext) {
         let bounds = match self.shape {
             CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)),
@@ -419,7 +419,6 @@ mod tests {
         requests.next().await;
         cx.foreground().run_until_parked();
 
-        println!("tag");
         cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
             fn test()
                 [do_work]();

crates/editor/src/multi_buffer.rs πŸ”—

@@ -9,7 +9,7 @@ pub use language::Completion;
 use language::{
     char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File,
     IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _,
-    ToPoint as _, ToPointUtf16 as _, TransactionId,
+    ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId,
 };
 use settings::Settings;
 use smallvec::SmallVec;
@@ -29,7 +29,7 @@ use text::{
     locator::Locator,
     rope::TextDimension,
     subscription::{Subscription, Topic},
-    Edit, Point, PointUtf16, TextSummary,
+    Edit, OffsetUtf16, Point, PointUtf16, TextSummary,
 };
 use theme::SyntaxTheme;
 use util::post_inc;
@@ -72,6 +72,10 @@ pub trait ToOffset: 'static + fmt::Debug {
     fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize;
 }
 
+pub trait ToOffsetUtf16: 'static + fmt::Debug {
+    fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16;
+}
+
 pub trait ToPoint: 'static + fmt::Debug {
     fn to_point(&self, snapshot: &MultiBufferSnapshot) -> Point;
 }
@@ -554,6 +558,20 @@ impl MultiBuffer {
         self.history.finalize_last_transaction();
     }
 
+    pub fn group_until_transaction(
+        &mut self,
+        transaction_id: TransactionId,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if let Some(buffer) = self.as_singleton() {
+            buffer.update(cx, |buffer, _| {
+                buffer.group_until_transaction(transaction_id)
+            });
+        } else {
+            self.history.group_until(transaction_id);
+        }
+    }
+
     pub fn set_active_selections(
         &mut self,
         selections: &[Selection<Anchor>],
@@ -809,7 +827,7 @@ impl MultiBuffer {
         let mut cursor = snapshot.excerpts.cursor::<Option<&ExcerptId>>();
         let mut new_excerpts = cursor.slice(&Some(prev_excerpt_id), Bias::Right, &());
 
-        let edit_start = new_excerpts.summary().text.bytes;
+        let edit_start = new_excerpts.summary().text.len;
         new_excerpts.update_last(
             |excerpt| {
                 excerpt.has_trailing_newline = true;
@@ -862,7 +880,7 @@ impl MultiBuffer {
             &(),
         );
 
-        let edit_end = new_excerpts.summary().text.bytes;
+        let edit_end = new_excerpts.summary().text.len;
 
         let suffix = cursor.suffix(&());
         let changed_trailing_excerpt = suffix.is_empty();
@@ -1068,7 +1086,7 @@ impl MultiBuffer {
 
                 // Push an edit for the removal of this run of excerpts.
                 let old_end = cursor.start().1;
-                let new_start = new_excerpts.summary().text.bytes;
+                let new_start = new_excerpts.summary().text.len;
                 edits.push(Edit {
                     old: old_start..old_end,
                     new: new_start..new_start,
@@ -1297,7 +1315,7 @@ impl MultiBuffer {
                         )
                         .map(|mut edit| {
                             let excerpt_old_start = cursor.start().1;
-                            let excerpt_new_start = new_excerpts.summary().text.bytes;
+                            let excerpt_new_start = new_excerpts.summary().text.len;
                             edit.old.start += excerpt_old_start;
                             edit.old.end += excerpt_old_start;
                             edit.new.start += excerpt_new_start;
@@ -1527,7 +1545,7 @@ impl MultiBufferSnapshot {
         let mut cursor = self.excerpts.cursor::<usize>();
         cursor.seek(&offset, Bias::Left, &());
         let mut excerpt_chunks = cursor.item().map(|excerpt| {
-            let end_before_footer = cursor.start() + excerpt.text_summary.bytes;
+            let end_before_footer = cursor.start() + excerpt.text_summary.len;
             let start = excerpt.range.context.start.to_offset(&excerpt.buffer);
             let end = start + (cmp::min(offset, end_before_footer) - cursor.start());
             excerpt.buffer.reversed_chunks_in_range(start..end)
@@ -1629,7 +1647,7 @@ impl MultiBufferSnapshot {
     }
 
     pub fn len(&self) -> usize {
-        self.excerpts.summary().text.bytes
+        self.excerpts.summary().text.len
     }
 
     pub fn max_buffer_row(&self) -> u32 {
@@ -1674,6 +1692,25 @@ impl MultiBufferSnapshot {
         *cursor.start() + overshoot
     }
 
+    pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 {
+        if let Some((_, _, buffer)) = self.as_singleton() {
+            return buffer.clip_offset_utf16(offset, bias);
+        }
+
+        let mut cursor = self.excerpts.cursor::<OffsetUtf16>();
+        cursor.seek(&offset, Bias::Right, &());
+        let overshoot = if let Some(excerpt) = cursor.item() {
+            let excerpt_start = excerpt.range.context.start.to_offset_utf16(&excerpt.buffer);
+            let buffer_offset = excerpt
+                .buffer
+                .clip_offset_utf16(excerpt_start + (offset - cursor.start()), bias);
+            OffsetUtf16(buffer_offset.0.saturating_sub(excerpt_start.0))
+        } else {
+            OffsetUtf16(0)
+        };
+        *cursor.start() + overshoot
+    }
+
     pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
         if let Some((_, _, buffer)) = self.as_singleton() {
             return buffer.clip_point_utf16(point, bias);
@@ -1781,7 +1818,7 @@ impl MultiBufferSnapshot {
                 .offset_to_point_utf16(excerpt_start_offset + overshoot);
             *start_point + (buffer_point - excerpt_start_point)
         } else {
-            self.excerpts.summary().text.lines_utf16
+            self.excerpts.summary().text.lines_utf16()
         }
     }
 
@@ -1803,7 +1840,7 @@ impl MultiBufferSnapshot {
                 .point_to_point_utf16(excerpt_start_point + overshoot);
             *start_point + (buffer_point - excerpt_start_point_utf16)
         } else {
-            self.excerpts.summary().text.lines_utf16
+            self.excerpts.summary().text.lines_utf16()
         }
     }
 
@@ -1824,7 +1861,53 @@ impl MultiBufferSnapshot {
                 .point_to_offset(excerpt_start_point + overshoot);
             *start_offset + buffer_offset - excerpt_start_offset
         } else {
-            self.excerpts.summary().text.bytes
+            self.excerpts.summary().text.len
+        }
+    }
+
+    pub fn offset_utf16_to_offset(&self, offset_utf16: OffsetUtf16) -> usize {
+        if let Some((_, _, buffer)) = self.as_singleton() {
+            return buffer.offset_utf16_to_offset(offset_utf16);
+        }
+
+        let mut cursor = self.excerpts.cursor::<(OffsetUtf16, usize)>();
+        cursor.seek(&offset_utf16, Bias::Right, &());
+        if let Some(excerpt) = cursor.item() {
+            let (start_offset_utf16, start_offset) = cursor.start();
+            let overshoot = offset_utf16 - start_offset_utf16;
+            let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer);
+            let excerpt_start_offset_utf16 =
+                excerpt.buffer.offset_to_offset_utf16(excerpt_start_offset);
+            let buffer_offset = excerpt
+                .buffer
+                .offset_utf16_to_offset(excerpt_start_offset_utf16 + overshoot);
+            *start_offset + (buffer_offset - excerpt_start_offset)
+        } else {
+            self.excerpts.summary().text.len
+        }
+    }
+
+    pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 {
+        if let Some((_, _, buffer)) = self.as_singleton() {
+            return buffer.offset_to_offset_utf16(offset);
+        }
+
+        let mut cursor = self.excerpts.cursor::<(usize, OffsetUtf16)>();
+        cursor.seek(&offset, Bias::Right, &());
+        if let Some(excerpt) = cursor.item() {
+            let (start_offset, start_offset_utf16) = cursor.start();
+            let overshoot = offset - start_offset;
+            let excerpt_start_offset_utf16 =
+                excerpt.range.context.start.to_offset_utf16(&excerpt.buffer);
+            let excerpt_start_offset = excerpt
+                .buffer
+                .offset_utf16_to_offset(excerpt_start_offset_utf16);
+            let buffer_offset_utf16 = excerpt
+                .buffer
+                .offset_to_offset_utf16(excerpt_start_offset + overshoot);
+            *start_offset_utf16 + (buffer_offset_utf16 - excerpt_start_offset_utf16)
+        } else {
+            self.excerpts.summary().text.len_utf16
         }
     }
 
@@ -1847,7 +1930,7 @@ impl MultiBufferSnapshot {
                 .point_utf16_to_offset(excerpt_start_point + overshoot);
             *start_offset + (buffer_offset - excerpt_start_offset)
         } else {
-            self.excerpts.summary().text.bytes
+            self.excerpts.summary().text.len
         }
     }
 
@@ -2311,7 +2394,7 @@ impl MultiBufferSnapshot {
                     .context
                     .start
                     .to_offset(&start_excerpt.buffer);
-                let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.bytes;
+                let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len;
 
                 let start_in_buffer =
                     excerpt_buffer_start + range.start.saturating_sub(*cursor.start());
@@ -2415,7 +2498,7 @@ impl MultiBufferSnapshot {
                     .context
                     .start
                     .to_offset(&start_excerpt.buffer);
-                let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.bytes;
+                let excerpt_buffer_end = excerpt_buffer_start + start_excerpt.text_summary.len;
 
                 let start_in_buffer =
                     excerpt_buffer_start + range.start.saturating_sub(*cursor.start());
@@ -2651,9 +2734,8 @@ impl History {
     }
 
     fn group(&mut self) -> Option<TransactionId> {
-        let mut new_len = self.undo_stack.len();
-        let mut transactions = self.undo_stack.iter_mut();
-
+        let mut count = 0;
+        let mut transactions = self.undo_stack.iter();
         if let Some(mut transaction) = transactions.next_back() {
             while let Some(prev_transaction) = transactions.next_back() {
                 if !prev_transaction.suppress_grouping
@@ -2661,13 +2743,31 @@ impl History {
                         <= self.group_interval
                 {
                     transaction = prev_transaction;
-                    new_len -= 1;
+                    count += 1;
                 } else {
                     break;
                 }
             }
         }
+        self.group_trailing(count)
+    }
+
+    fn group_until(&mut self, transaction_id: TransactionId) {
+        let mut count = 0;
+        for transaction in self.undo_stack.iter().rev() {
+            if transaction.id == transaction_id {
+                self.group_trailing(count);
+                break;
+            } else if transaction.suppress_grouping {
+                break;
+            } else {
+                count += 1;
+            }
+        }
+    }
 
+    fn group_trailing(&mut self, n: usize) -> Option<TransactionId> {
+        let new_len = self.undo_stack.len() - n;
         let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len);
         if let Some(last_transaction) = transactions_to_keep.last_mut() {
             if let Some(transaction) = transactions_to_merge.last() {
@@ -2717,11 +2817,11 @@ impl Excerpt {
     ) -> ExcerptChunks<'a> {
         let content_start = self.range.context.start.to_offset(&self.buffer);
         let chunks_start = content_start + range.start;
-        let chunks_end = content_start + cmp::min(range.end, self.text_summary.bytes);
+        let chunks_end = content_start + cmp::min(range.end, self.text_summary.len);
 
         let footer_height = if self.has_trailing_newline
-            && range.start <= self.text_summary.bytes
-            && range.end > self.text_summary.bytes
+            && range.start <= self.text_summary.len
+            && range.end > self.text_summary.len
         {
             1
         } else {
@@ -2739,10 +2839,10 @@ impl Excerpt {
     fn bytes_in_range(&self, range: Range<usize>) -> ExcerptBytes {
         let content_start = self.range.context.start.to_offset(&self.buffer);
         let bytes_start = content_start + range.start;
-        let bytes_end = content_start + cmp::min(range.end, self.text_summary.bytes);
+        let bytes_end = content_start + cmp::min(range.end, self.text_summary.len);
         let footer_height = if self.has_trailing_newline
-            && range.start <= self.text_summary.bytes
-            && range.end > self.text_summary.bytes
+            && range.start <= self.text_summary.len
+            && range.end > self.text_summary.len
         {
             1
         } else {
@@ -2836,13 +2936,13 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for TextSummary {
 
 impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for usize {
     fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
-        *self += summary.text.bytes;
+        *self += summary.text.len;
     }
 }
 
 impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize {
     fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering {
-        Ord::cmp(self, &cursor_location.text.bytes)
+        Ord::cmp(self, &cursor_location.text.len)
     }
 }
 
@@ -2852,6 +2952,12 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Option<&'a
     }
 }
 
+impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for OffsetUtf16 {
+    fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
+        *self += summary.text.len_utf16;
+    }
+}
+
 impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Point {
     fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
         *self += summary.text.lines;
@@ -2860,7 +2966,7 @@ impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Point {
 
 impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for PointUtf16 {
     fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) {
-        *self += summary.text.lines_utf16
+        *self += summary.text.lines_utf16()
     }
 }
 
@@ -3060,6 +3166,24 @@ impl ToOffset for usize {
     }
 }
 
+impl ToOffset for OffsetUtf16 {
+    fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize {
+        snapshot.offset_utf16_to_offset(*self)
+    }
+}
+
+impl ToOffsetUtf16 for OffsetUtf16 {
+    fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> OffsetUtf16 {
+        *self
+    }
+}
+
+impl ToOffsetUtf16 for usize {
+    fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 {
+        snapshot.offset_to_offset_utf16(*self)
+    }
+}
+
 impl ToPoint for usize {
     fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point {
         snapshot.offset_to_point(*self)
@@ -3823,11 +3947,11 @@ mod tests {
                     buffer.text_summary_for_range::<PointUtf16, _>(0..buffer_range.start);
 
                 let excerpt_start = excerpt_starts.next().unwrap();
-                let mut offset = excerpt_start.bytes;
+                let mut offset = excerpt_start.len;
                 let mut buffer_offset = buffer_range.start;
                 let mut point = excerpt_start.lines;
                 let mut buffer_point = buffer_start_point;
-                let mut point_utf16 = excerpt_start.lines_utf16;
+                let mut point_utf16 = excerpt_start.lines_utf16();
                 let mut buffer_point_utf16 = buffer_start_point_utf16;
                 for ch in buffer
                     .snapshot()
@@ -3841,7 +3965,7 @@ mod tests {
                         let buffer_right_offset = buffer.clip_offset(buffer_offset, Bias::Right);
                         assert_eq!(
                             left_offset,
-                            excerpt_start.bytes + (buffer_left_offset - buffer_range.start),
+                            excerpt_start.len + (buffer_left_offset - buffer_range.start),
                             "clip_offset({:?}, Left). buffer: {:?}, buffer offset: {:?}",
                             offset,
                             buffer_id,
@@ -3849,7 +3973,7 @@ mod tests {
                         );
                         assert_eq!(
                             right_offset,
-                            excerpt_start.bytes + (buffer_right_offset - buffer_range.start),
+                            excerpt_start.len + (buffer_right_offset - buffer_range.start),
                             "clip_offset({:?}, Right). buffer: {:?}, buffer offset: {:?}",
                             offset,
                             buffer_id,
@@ -3910,7 +4034,7 @@ mod tests {
                             buffer.clip_point_utf16(buffer_point_utf16, Bias::Right);
                         assert_eq!(
                             left_point_utf16,
-                            excerpt_start.lines_utf16
+                            excerpt_start.lines_utf16()
                                 + (buffer_left_point_utf16 - buffer_start_point_utf16),
                             "clip_point_utf16({:?}, Left). buffer: {:?}, buffer point_utf16: {:?}",
                             point_utf16,
@@ -3919,7 +4043,7 @@ mod tests {
                         );
                         assert_eq!(
                             right_point_utf16,
-                            excerpt_start.lines_utf16
+                            excerpt_start.lines_utf16()
                                 + (buffer_right_point_utf16 - buffer_start_point_utf16),
                             "clip_point_utf16({:?}, Right). buffer: {:?}, buffer point_utf16: {:?}",
                             point_utf16,
@@ -4069,7 +4193,7 @@ mod tests {
         let mut now = Instant::now();
 
         multibuffer.update(cx, |multibuffer, cx| {
-            multibuffer.start_transaction_at(now, cx);
+            let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap();
             multibuffer.edit(
                 [
                     (Point::new(0, 0)..Point::new(0, 0), "A"),
@@ -4152,6 +4276,16 @@ mod tests {
             assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678");
             multibuffer.undo(cx);
             assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
+
+            // Transactions can be grouped manually.
+            multibuffer.redo(cx);
+            multibuffer.redo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
+            multibuffer.group_until_transaction(transaction_1, cx);
+            multibuffer.undo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
+            multibuffer.redo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
         });
     }
 }

crates/editor/src/multi_buffer/anchor.rs πŸ”—

@@ -1,10 +1,10 @@
-use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint};
+use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint};
 use std::{
     cmp::Ordering,
     ops::{Range, Sub},
 };
 use sum_tree::Bias;
-use text::{rope::TextDimension, Point};
+use text::{rope::TextDimension, OffsetUtf16, Point};
 
 #[derive(Clone, Eq, PartialEq, Debug, Hash)]
 pub struct Anchor {
@@ -89,6 +89,12 @@ impl ToOffset for Anchor {
     }
 }
 
+impl ToOffsetUtf16 for Anchor {
+    fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 {
+        self.summary(snapshot)
+    }
+}
+
 impl ToPoint for Anchor {
     fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point {
         self.summary(snapshot)

crates/editor/src/test.rs πŸ”—

@@ -181,13 +181,7 @@ impl<'a> EditorTestContext<'a> {
 
     pub fn simulate_keystroke(&mut self, keystroke_text: &str) {
         let keystroke = Keystroke::parse(keystroke_text).unwrap();
-        let input = if keystroke.modified() {
-            None
-        } else {
-            Some(keystroke.key.clone())
-        };
-        self.cx
-            .dispatch_keystroke(self.window_id, keystroke, input, false);
+        self.cx.dispatch_keystroke(self.window_id, keystroke, false);
     }
 
     pub fn simulate_keystrokes<const COUNT: usize>(&mut self, keystroke_texts: [&str; COUNT]) {

crates/file_finder/src/file_finder.rs πŸ”—

@@ -279,7 +279,7 @@ impl PickerDelegate for FileFinder {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use editor::{Editor, Input};
+    use editor::Editor;
     use menu::{Confirm, SelectNext};
     use serde_json::json;
     use workspace::{AppState, Workspace};
@@ -318,12 +318,14 @@ mod tests {
         cx.dispatch_action(window_id, Toggle);
 
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
-        cx.dispatch_action(window_id, Input("b".into()));
-        cx.dispatch_action(window_id, Input("n".into()));
-        cx.dispatch_action(window_id, Input("a".into()));
         finder
-            .condition(&cx, |finder, _| finder.matches.len() == 2)
+            .update(cx, |finder, cx| {
+                finder.update_matches("bna".to_string(), cx)
+            })
             .await;
+        finder.read_with(cx, |finder, _| {
+            assert_eq!(finder.matches.len(), 2);
+        });
 
         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
         cx.dispatch_action(window_id, SelectNext);

crates/gpui/examples/text.rs πŸ”—

@@ -2,11 +2,12 @@ use gpui::{
     color::Color,
     fonts::{Properties, Weight},
     text_layout::RunStyle,
-    DebugContext, Element as _, Quad,
+    DebugContext, Element as _, MeasurementContext, Quad,
 };
 use log::LevelFilter;
 use pathfinder_geometry::rect::RectF;
 use simplelog::SimpleLogger;
+use std::ops::Range;
 
 fn main() {
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
@@ -112,6 +113,18 @@ impl gpui::Element for TextElement {
         false
     }
 
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
     fn debug(
         &self,
         _: RectF,

crates/gpui/src/app.rs πŸ”—

@@ -3,12 +3,13 @@ pub mod action;
 use crate::{
     elements::ElementBox,
     executor::{self, Task},
+    geometry::rect::RectF,
     keymap::{self, Binding, Keystroke},
     platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
     presenter::Presenter,
     util::post_inc,
-    AssetCache, AssetSource, ClipboardItem, FontCache, MouseRegionId, PathPromptOptions,
-    TextLayoutCache,
+    AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseRegionId,
+    PathPromptOptions, TextLayoutCache,
 };
 pub use action::*;
 use anyhow::{anyhow, Context, Result};
@@ -28,7 +29,7 @@ use std::{
     hash::{Hash, Hasher},
     marker::PhantomData,
     mem,
-    ops::{Deref, DerefMut},
+    ops::{Deref, DerefMut, Range},
     path::{Path, PathBuf},
     pin::Pin,
     rc::{self, Rc},
@@ -64,6 +65,32 @@ pub trait View: Entity + Sized {
     fn debug_json(&self, _: &AppContext) -> serde_json::Value {
         serde_json::Value::Null
     }
+
+    fn text_for_range(&self, _: Range<usize>, _: &AppContext) -> Option<String> {
+        None
+    }
+    fn selected_text_range(&self, _: &AppContext) -> Option<Range<usize>> {
+        None
+    }
+    fn marked_text_range(&self, _: &AppContext) -> Option<Range<usize>> {
+        None
+    }
+    fn unmark_text(&mut self, _: &mut ViewContext<Self>) {}
+    fn replace_text_in_range(
+        &mut self,
+        _: Option<Range<usize>>,
+        _: &str,
+        _: &mut ViewContext<Self>,
+    ) {
+    }
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        _: Option<Range<usize>>,
+        _: &str,
+        _: Option<Range<usize>>,
+        _: &mut ViewContext<Self>,
+    ) {
+    }
 }
 
 pub trait ReadModel {
@@ -154,6 +181,11 @@ pub struct TestAppContext {
     condition_duration: Option<Duration>,
 }
 
+pub struct WindowInputHandler {
+    app: Rc<RefCell<MutableAppContext>>,
+    window_id: usize,
+}
+
 impl App {
     pub fn new(asset_source: impl AssetSource) -> Result<Self> {
         let platform = platform::current::platform();
@@ -310,6 +342,87 @@ impl App {
     }
 }
 
+impl WindowInputHandler {
+    fn read_focused_view<T, F>(&self, f: F) -> Option<T>
+    where
+        F: FnOnce(&dyn AnyView, &AppContext) -> T,
+    {
+        let app = self.app.borrow();
+        let view_id = app.focused_view_id(self.window_id)?;
+        let view = app.cx.views.get(&(self.window_id, view_id))?;
+        let result = f(view.as_ref(), &app);
+        Some(result)
+    }
+
+    fn update_focused_view<T, F>(&mut self, f: F) -> Option<T>
+    where
+        F: FnOnce(usize, usize, &mut dyn AnyView, &mut MutableAppContext) -> T,
+    {
+        let mut app = self.app.borrow_mut();
+        app.update(|app| {
+            let view_id = app.focused_view_id(self.window_id)?;
+            let mut view = app.cx.views.remove(&(self.window_id, view_id))?;
+            let result = f(self.window_id, view_id, view.as_mut(), &mut *app);
+            app.cx.views.insert((self.window_id, view_id), view);
+            Some(result)
+        })
+    }
+}
+
+impl InputHandler for WindowInputHandler {
+    fn text_for_range(&self, range: Range<usize>) -> Option<String> {
+        self.read_focused_view(|view, cx| view.text_for_range(range.clone(), cx))
+            .flatten()
+    }
+
+    fn selected_text_range(&self) -> Option<Range<usize>> {
+        self.read_focused_view(|view, cx| view.selected_text_range(cx))
+            .flatten()
+    }
+
+    fn replace_text_in_range(&mut self, range: Option<Range<usize>>, text: &str) {
+        self.update_focused_view(|window_id, view_id, view, cx| {
+            view.replace_text_in_range(range, text, cx, window_id, view_id);
+        });
+    }
+
+    fn marked_text_range(&self) -> Option<Range<usize>> {
+        self.read_focused_view(|view, cx| view.marked_text_range(cx))
+            .flatten()
+    }
+
+    fn unmark_text(&mut self) {
+        self.update_focused_view(|window_id, view_id, view, cx| {
+            view.unmark_text(cx, window_id, view_id);
+        });
+    }
+
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        range: Option<Range<usize>>,
+        new_text: &str,
+        new_selected_range: Option<Range<usize>>,
+    ) {
+        self.update_focused_view(|window_id, view_id, view, cx| {
+            view.replace_and_mark_text_in_range(
+                range,
+                new_text,
+                new_selected_range,
+                cx,
+                window_id,
+                view_id,
+            );
+        });
+    }
+
+    fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
+        let app = self.app.borrow();
+        let (presenter, _) = app.presenters_and_platform_windows.get(&self.window_id)?;
+        let presenter = presenter.borrow();
+        presenter.rect_for_text_range(range_utf16, &app)
+    }
+}
+
 #[cfg(any(test, feature = "test-support"))]
 impl TestAppContext {
     pub fn new(
@@ -361,14 +474,8 @@ impl TestAppContext {
         self.cx.borrow_mut().dispatch_global_action(action);
     }
 
-    pub fn dispatch_keystroke(
-        &mut self,
-        window_id: usize,
-        keystroke: Keystroke,
-        input: Option<String>,
-        is_held: bool,
-    ) {
-        self.cx.borrow_mut().update(|cx| {
+    pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
+        let handled = self.cx.borrow_mut().update(|cx| {
             let presenter = cx
                 .presenters_and_platform_windows
                 .get(&window_id)
@@ -377,17 +484,29 @@ impl TestAppContext {
                 .clone();
             let dispatch_path = presenter.borrow().dispatch_path(cx.as_ref());
 
-            if !cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) {
-                presenter.borrow_mut().dispatch_event(
-                    Event::KeyDown(KeyDownEvent {
-                        keystroke,
-                        input,
-                        is_held,
-                    }),
-                    cx,
-                );
+            if cx.dispatch_keystroke(window_id, dispatch_path, &keystroke) {
+                return true;
+            }
+            if presenter.borrow_mut().dispatch_event(
+                Event::KeyDown(KeyDownEvent {
+                    keystroke: keystroke.clone(),
+                    is_held,
+                }),
+                cx,
+            ) {
+                return true;
             }
+
+            false
         });
+
+        if !handled && !keystroke.cmd && !keystroke.ctrl {
+            WindowInputHandler {
+                app: self.cx.clone(),
+                window_id,
+            }
+            .replace_text_in_range(None, &keystroke.key)
+        }
     }
 
     pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
@@ -1195,6 +1314,11 @@ impl MutableAppContext {
             .set_menus(menus, &self.keystroke_matcher);
     }
 
+    fn show_character_palette(&self, window_id: usize) {
+        let (_, window) = &self.presenters_and_platform_windows[&window_id];
+        window.show_character_palette();
+    }
+
     fn prompt(
         &self,
         window_id: usize,
@@ -1883,6 +2007,11 @@ impl MutableAppContext {
             }));
         }
 
+        window.set_input_handler(Box::new(WindowInputHandler {
+            app: self.upgrade().0,
+            window_id,
+        }));
+
         let scene =
             presenter
                 .borrow_mut()
@@ -3174,6 +3303,28 @@ pub trait AnyView {
     fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
     fn keymap_context(&self, cx: &AppContext) -> keymap::Context;
     fn debug_json(&self, cx: &AppContext) -> serde_json::Value;
+
+    fn text_for_range(&self, range: Range<usize>, cx: &AppContext) -> Option<String>;
+    fn selected_text_range(&self, cx: &AppContext) -> Option<Range<usize>>;
+    fn marked_text_range(&self, cx: &AppContext) -> Option<Range<usize>>;
+    fn unmark_text(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize);
+    fn replace_text_in_range(
+        &mut self,
+        range: Option<Range<usize>>,
+        text: &str,
+        cx: &mut MutableAppContext,
+        window_id: usize,
+        view_id: usize,
+    );
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        range: Option<Range<usize>>,
+        new_text: &str,
+        new_selected_range: Option<Range<usize>>,
+        cx: &mut MutableAppContext,
+        window_id: usize,
+        view_id: usize,
+    );
 }
 
 impl<T> AnyView for T
@@ -3224,6 +3375,48 @@ where
     fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
         View::debug_json(self, cx)
     }
+
+    fn text_for_range(&self, range: Range<usize>, cx: &AppContext) -> Option<String> {
+        View::text_for_range(self, range, cx)
+    }
+
+    fn selected_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
+        View::selected_text_range(self, cx)
+    }
+
+    fn marked_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
+        View::marked_text_range(self, cx)
+    }
+
+    fn unmark_text(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize) {
+        let mut cx = ViewContext::new(cx, window_id, view_id);
+        View::unmark_text(self, &mut cx)
+    }
+
+    fn replace_text_in_range(
+        &mut self,
+        range: Option<Range<usize>>,
+        text: &str,
+        cx: &mut MutableAppContext,
+        window_id: usize,
+        view_id: usize,
+    ) {
+        let mut cx = ViewContext::new(cx, window_id, view_id);
+        View::replace_text_in_range(self, range, text, &mut cx)
+    }
+
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        range: Option<Range<usize>>,
+        new_text: &str,
+        new_selected_range: Option<Range<usize>>,
+        cx: &mut MutableAppContext,
+        window_id: usize,
+        view_id: usize,
+    ) {
+        let mut cx = ViewContext::new(cx, window_id, view_id);
+        View::replace_and_mark_text_in_range(self, range, new_text, new_selected_range, &mut cx)
+    }
 }
 
 pub struct ModelContext<'a, T: ?Sized> {
@@ -3489,6 +3682,10 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.app.platform()
     }
 
+    pub fn show_character_palette(&self) {
+        self.app.show_character_palette(self.window_id);
+    }
+
     pub fn prompt(
         &self,
         level: PromptLevel,

crates/gpui/src/elements.rs πŸ”—

@@ -31,7 +31,9 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
-    json, Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext,
+    json,
+    presenter::MeasurementContext,
+    Action, DebugContext, Event, EventContext, LayoutContext, PaintContext, RenderContext,
     SizeConstraint, View,
 };
 use core::panic;
@@ -41,7 +43,7 @@ use std::{
     borrow::Cow,
     cell::RefCell,
     mem,
-    ops::{Deref, DerefMut},
+    ops::{Deref, DerefMut, Range},
     rc::Rc,
 };
 
@@ -49,6 +51,11 @@ trait AnyElement {
     fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F;
     fn paint(&mut self, origin: Vector2F, visible_bounds: RectF, cx: &mut PaintContext);
     fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool;
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        cx: &MeasurementContext,
+    ) -> Option<RectF>;
     fn debug(&self, cx: &DebugContext) -> serde_json::Value;
 
     fn size(&self) -> Vector2F;
@@ -83,6 +90,16 @@ pub trait Element {
         cx: &mut EventContext,
     ) -> bool;
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        bounds: RectF,
+        visible_bounds: RectF,
+        layout: &Self::LayoutState,
+        paint: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF>;
+
     fn metadata(&self) -> Option<&dyn Any> {
         None
     }
@@ -287,6 +304,26 @@ impl<T: Element> AnyElement for Lifecycle<T> {
         }
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        if let Lifecycle::PostPaint {
+            element,
+            bounds,
+            visible_bounds,
+            layout,
+            paint,
+            ..
+        } = self
+        {
+            element.rect_for_text_range(range_utf16, *bounds, *visible_bounds, layout, paint, cx)
+        } else {
+            None
+        }
+    }
+
     fn size(&self) -> Vector2F {
         match self {
             Lifecycle::Empty | Lifecycle::Init { .. } => panic!("invalid element lifecycle state"),
@@ -385,6 +422,14 @@ impl ElementRc {
         self.element.borrow_mut().dispatch_event(event, cx)
     }
 
+    pub fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.element.borrow().rect_for_text_range(range_utf16, cx)
+    }
+
     pub fn size(&self) -> Vector2F {
         self.element.borrow().size()
     }

crates/gpui/src/elements/align.rs πŸ”—

@@ -1,6 +1,8 @@
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+    json,
+    presenter::MeasurementContext,
+    DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
     SizeConstraint,
 };
 use json::ToJson;
@@ -94,6 +96,18 @@ impl Element for Align {
         self.child.dispatch_event(event, cx)
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: std::ops::Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.child.rect_for_text_range(range_utf16, cx)
+    }
+
     fn debug(
         &self,
         bounds: pathfinder_geometry::rect::RectF,

crates/gpui/src/elements/canvas.rs πŸ”—

@@ -1,6 +1,7 @@
 use super::Element;
 use crate::{
     json::{self, json},
+    presenter::MeasurementContext,
     DebugContext, PaintContext,
 };
 use json::ToJson;
@@ -67,6 +68,18 @@ where
         false
     }
 
+    fn rect_for_text_range(
+        &self,
+        _: std::ops::Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
     fn debug(
         &self,
         bounds: RectF,

crates/gpui/src/elements/constrained_box.rs πŸ”—

@@ -1,9 +1,13 @@
+use std::ops::Range;
+
 use json::ToJson;
 use serde_json::json;
 
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+    json,
+    presenter::MeasurementContext,
+    DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
     SizeConstraint,
 };
 
@@ -165,6 +169,18 @@ impl Element for ConstrainedBox {
         self.child.dispatch_event(event, cx)
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.child.rect_for_text_range(range_utf16, cx)
+    }
+
     fn debug(
         &self,
         _: RectF,

crates/gpui/src/elements/container.rs πŸ”—

@@ -1,3 +1,5 @@
+use std::ops::Range;
+
 use crate::{
     color::Color,
     geometry::{
@@ -7,6 +9,7 @@ use crate::{
     },
     json::ToJson,
     platform::CursorStyle,
+    presenter::MeasurementContext,
     scene::{self, Border, CursorRegion, Quad},
     Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
 };
@@ -271,6 +274,18 @@ impl Element for Container {
         self.child.dispatch_event(event, cx)
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.child.rect_for_text_range(range_utf16, cx)
+    }
+
     fn debug(
         &self,
         bounds: RectF,

crates/gpui/src/elements/empty.rs πŸ”—

@@ -1,9 +1,12 @@
+use std::ops::Range;
+
 use crate::{
     geometry::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
+    presenter::MeasurementContext,
     DebugContext,
 };
 use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
@@ -67,6 +70,18 @@ impl Element for Empty {
         false
     }
 
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
     fn debug(
         &self,
         bounds: RectF,

crates/gpui/src/elements/event_handler.rs πŸ”—

@@ -1,11 +1,11 @@
 use crate::{
-    geometry::vector::Vector2F, CursorRegion, DebugContext, Element, ElementBox, Event,
-    EventContext, LayoutContext, MouseButton, MouseButtonEvent, MouseRegion, NavigationDirection,
-    PaintContext, SizeConstraint,
+    geometry::vector::Vector2F, presenter::MeasurementContext, CursorRegion, DebugContext, Element,
+    ElementBox, Event, EventContext, LayoutContext, MouseButton, MouseButtonEvent, MouseRegion,
+    NavigationDirection, PaintContext, SizeConstraint,
 };
 use pathfinder_geometry::rect::RectF;
 use serde_json::json;
-use std::any::TypeId;
+use std::{any::TypeId, ops::Range};
 
 pub struct EventHandler {
     child: ElementBox,
@@ -150,6 +150,18 @@ impl Element for EventHandler {
         }
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.child.rect_for_text_range(range_utf16, cx)
+    }
+
     fn debug(
         &self,
         _: RectF,

crates/gpui/src/elements/expanded.rs πŸ”—

@@ -1,6 +1,10 @@
+use std::ops::Range;
+
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
-    json, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
+    json,
+    presenter::MeasurementContext,
+    DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
     SizeConstraint,
 };
 use serde_json::json;
@@ -74,6 +78,18 @@ impl Element for Expanded {
         self.child.dispatch_event(event, cx)
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.child.rect_for_text_range(range_utf16, cx)
+    }
+
     fn debug(
         &self,
         _: RectF,

crates/gpui/src/elements/flex.rs πŸ”—

@@ -1,7 +1,8 @@
-use std::{any::Any, f32::INFINITY};
+use std::{any::Any, f32::INFINITY, ops::Range};
 
 use crate::{
     json::{self, ToJson, Value},
+    presenter::MeasurementContext,
     Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
     LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint,
     Vector2FExt, View,
@@ -334,6 +335,20 @@ impl Element for Flex {
         handled
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.children
+            .iter()
+            .find_map(|child| child.rect_for_text_range(range_utf16.clone(), cx))
+    }
+
     fn debug(
         &self,
         bounds: RectF,
@@ -417,6 +432,18 @@ impl Element for FlexItem {
         self.child.dispatch_event(event, cx)
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.child.rect_for_text_range(range_utf16, cx)
+    }
+
     fn metadata(&self) -> Option<&dyn Any> {
         Some(&self.metadata)
     }

crates/gpui/src/elements/hook.rs πŸ”—

@@ -1,6 +1,9 @@
+use std::ops::Range;
+
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
+    presenter::MeasurementContext,
     DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
     SizeConstraint,
 };
@@ -65,6 +68,18 @@ impl Element for Hook {
         self.child.dispatch_event(event, cx)
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.child.rect_for_text_range(range_utf16, cx)
+    }
+
     fn debug(
         &self,
         _: RectF,

crates/gpui/src/elements/image.rs πŸ”—

@@ -5,11 +5,12 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{json, ToJson},
+    presenter::MeasurementContext,
     scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext,
     PaintContext, SizeConstraint,
 };
 use serde::Deserialize;
-use std::sync::Arc;
+use std::{ops::Range, sync::Arc};
 
 pub struct Image {
     data: Arc<ImageData>,
@@ -89,6 +90,18 @@ impl Element for Image {
         false
     }
 
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
     fn debug(
         &self,
         bounds: RectF,

crates/gpui/src/elements/keystroke_label.rs πŸ”—

@@ -76,6 +76,18 @@ impl Element for KeystrokeLabel {
         element.dispatch_event(event, cx)
     }
 
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
     fn debug(
         &self,
         _: RectF,

crates/gpui/src/elements/label.rs πŸ”—

@@ -1,3 +1,5 @@
+use std::ops::Range;
+
 use crate::{
     fonts::TextStyle,
     geometry::{
@@ -5,6 +7,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{ToJson, Value},
+    presenter::MeasurementContext,
     text_layout::{Line, RunStyle},
     DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
 };
@@ -174,6 +177,18 @@ impl Element for Label {
         false
     }
 
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
     fn debug(
         &self,
         bounds: RectF,

crates/gpui/src/elements/list.rs πŸ”—

@@ -4,6 +4,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::json,
+    presenter::MeasurementContext,
     DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
     RenderContext, ScrollWheelEvent, SizeConstraint, View, ViewContext,
 };
@@ -328,6 +329,39 @@ impl Element for List {
         handled
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        bounds: RectF,
+        _: RectF,
+        scroll_top: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        let state = self.state.0.borrow();
+        let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
+        let mut cursor = state.items.cursor::<Count>();
+        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
+        while let Some(item) = cursor.item() {
+            if item_origin.y() > bounds.max_y() {
+                break;
+            }
+
+            if let ListItem::Rendered(element) = item {
+                if let Some(rect) = element.rect_for_text_range(range_utf16.clone(), cx) {
+                    return Some(rect);
+                }
+
+                item_origin.set_y(item_origin.y() + element.size().y());
+                cursor.next(&());
+            } else {
+                unreachable!();
+            }
+        }
+
+        None
+    }
+
     fn debug(
         &self,
         bounds: RectF,
@@ -939,6 +973,18 @@ mod tests {
             todo!()
         }
 
+        fn rect_for_text_range(
+            &self,
+            _: Range<usize>,
+            _: RectF,
+            _: RectF,
+            _: &Self::LayoutState,
+            _: &Self::PaintState,
+            _: &MeasurementContext,
+        ) -> Option<RectF> {
+            todo!()
+        }
+
         fn debug(&self, _: RectF, _: &(), _: &(), _: &DebugContext) -> serde_json::Value {
             self.id.into()
         }

crates/gpui/src/elements/mouse_event_handler.rs πŸ”—

@@ -1,5 +1,3 @@
-use std::any::TypeId;
-
 use super::Padding;
 use crate::{
     geometry::{
@@ -8,11 +6,12 @@ use crate::{
     },
     platform::CursorStyle,
     scene::{CursorRegion, HandlerSet},
-    DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseButton,
-    MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseState, PaintContext, RenderContext,
-    SizeConstraint, View,
+    DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext,
+    MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MouseState, PaintContext,
+    RenderContext, SizeConstraint, View,
 };
 use serde_json::json;
+use std::{any::TypeId, ops::Range};
 
 pub struct MouseEventHandler {
     child: ElementBox,
@@ -150,6 +149,18 @@ impl Element for MouseEventHandler {
         self.child.dispatch_event(event, cx)
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.child.rect_for_text_range(range_utf16, cx)
+    }
+
     fn debug(
         &self,
         _: RectF,

crates/gpui/src/elements/overlay.rs πŸ”—

@@ -1,6 +1,9 @@
+use std::ops::Range;
+
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::ToJson,
+    presenter::MeasurementContext,
     DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
     PaintContext, SizeConstraint,
 };
@@ -126,6 +129,18 @@ impl Element for Overlay {
         self.child.dispatch_event(event, cx)
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.child.rect_for_text_range(range_utf16, cx)
+    }
+
     fn debug(
         &self,
         _: RectF,

crates/gpui/src/elements/stack.rs πŸ”—

@@ -1,6 +1,9 @@
+use std::ops::Range;
+
 use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::{self, json, ToJson},
+    presenter::MeasurementContext,
     DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext,
     SizeConstraint,
 };
@@ -64,6 +67,21 @@ impl Element for Stack {
         false
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.children
+            .iter()
+            .rev()
+            .find_map(|child| child.rect_for_text_range(range_utf16.clone(), cx))
+    }
+
     fn debug(
         &self,
         bounds: RectF,

crates/gpui/src/elements/svg.rs πŸ”—

@@ -1,4 +1,4 @@
-use std::borrow::Cow;
+use std::{borrow::Cow, ops::Range};
 
 use serde_json::json;
 
@@ -8,6 +8,7 @@ use crate::{
         rect::RectF,
         vector::{vec2f, Vector2F},
     },
+    presenter::MeasurementContext,
     scene, DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
 };
 
@@ -84,6 +85,18 @@ impl Element for Svg {
         false
     }
 
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
     fn debug(
         &self,
         bounds: RectF,

crates/gpui/src/elements/text.rs πŸ”—

@@ -6,6 +6,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{ToJson, Value},
+    presenter::MeasurementContext,
     text_layout::{Line, RunStyle, ShapedBoundary},
     DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext,
     SizeConstraint, TextLayoutCache,
@@ -63,7 +64,7 @@ impl Element for Text {
         cx: &mut LayoutContext,
     ) -> (Vector2F, Self::LayoutState) {
         // Convert the string and highlight ranges into an iterator of highlighted chunks.
-        
+
         let mut offset = 0;
         let mut highlight_ranges = self.highlights.iter().peekable();
         let chunks = std::iter::from_fn(|| {
@@ -81,7 +82,8 @@ impl Element for Text {
                         "Highlight out of text range. Text len: {}, Highlight range: {}..{}",
                         self.text.len(),
                         range.start,
-                        range.end);
+                        range.end
+                    );
                     result = None;
                 }
             } else if offset < self.text.len() {
@@ -188,6 +190,18 @@ impl Element for Text {
         false
     }
 
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
     fn debug(
         &self,
         bounds: RectF,

crates/gpui/src/elements/tooltip.rs πŸ”—

@@ -6,12 +6,14 @@ use crate::{
     fonts::TextStyle,
     geometry::{rect::RectF, vector::Vector2F},
     json::json,
+    presenter::MeasurementContext,
     Action, Axis, ElementStateHandle, LayoutContext, MouseMovedEvent, PaintContext, RenderContext,
     SizeConstraint, Task, View,
 };
 use serde::Deserialize;
 use std::{
     cell::{Cell, RefCell},
+    ops::Range,
     rc::Rc,
     time::Duration,
 };
@@ -196,6 +198,18 @@ impl Element for Tooltip {
         self.child.dispatch_event(event, cx)
     }
 
+    fn rect_for_text_range(
+        &self,
+        range: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        self.child.rect_for_text_range(range, cx)
+    }
+
     fn debug(
         &self,
         _: RectF,

crates/gpui/src/elements/uniform_list.rs πŸ”—

@@ -5,6 +5,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{self, json},
+    presenter::MeasurementContext,
     ElementBox, RenderContext, ScrollWheelEvent, View,
 };
 use json::ToJson;
@@ -327,6 +328,21 @@ impl Element for UniformList {
         handled
     }
 
+    fn rect_for_text_range(
+        &self,
+        range: Range<usize>,
+        _: RectF,
+        _: RectF,
+        layout: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        layout
+            .items
+            .iter()
+            .find_map(|child| child.rect_for_text_range(range.clone(), cx))
+    }
+
     fn debug(
         &self,
         bounds: RectF,

crates/gpui/src/gpui.rs πŸ”—

@@ -30,7 +30,8 @@ pub mod platform;
 pub use gpui_macros::test;
 pub use platform::*;
 pub use presenter::{
-    Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt,
+    Axis, DebugContext, EventContext, LayoutContext, MeasurementContext, PaintContext,
+    SizeConstraint, Vector2FExt,
 };
 
 pub use anyhow;

crates/gpui/src/platform.rs πŸ”—

@@ -26,6 +26,7 @@ use serde::Deserialize;
 use std::{
     any::Any,
     fmt::{self, Display},
+    ops::Range,
     path::{Path, PathBuf},
     rc::Rc,
     str::FromStr,
@@ -88,6 +89,21 @@ pub trait Dispatcher: Send + Sync {
     fn run_on_main_thread(&self, task: Runnable);
 }
 
+pub trait InputHandler {
+    fn selected_text_range(&self) -> Option<Range<usize>>;
+    fn marked_text_range(&self) -> Option<Range<usize>>;
+    fn text_for_range(&self, range_utf16: Range<usize>) -> Option<String>;
+    fn replace_text_in_range(&mut self, replacement_range: Option<Range<usize>>, text: &str);
+    fn replace_and_mark_text_in_range(
+        &mut self,
+        range_utf16: Option<Range<usize>>,
+        new_text: &str,
+        new_selected_range: Option<Range<usize>>,
+    );
+    fn unmark_text(&mut self);
+    fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF>;
+}
+
 pub trait Window: WindowContext {
     fn as_any_mut(&mut self) -> &mut dyn Any;
     fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>);
@@ -95,10 +111,12 @@ pub trait Window: WindowContext {
     fn on_resize(&mut self, callback: Box<dyn FnMut()>);
     fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
     fn on_close(&mut self, callback: Box<dyn FnOnce()>);
+    fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>);
     fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
     fn activate(&self);
     fn set_title(&mut self, title: &str);
     fn set_edited(&mut self, edited: bool);
+    fn show_character_palette(&self);
 }
 
 pub trait WindowContext {

crates/gpui/src/platform/event.rs πŸ”—

@@ -3,14 +3,12 @@ use crate::{geometry::vector::Vector2F, keymap::Keystroke};
 #[derive(Clone, Debug)]
 pub struct KeyDownEvent {
     pub keystroke: Keystroke,
-    pub input: Option<String>,
     pub is_held: bool,
 }
 
 #[derive(Clone, Debug)]
 pub struct KeyUpEvent {
     pub keystroke: Keystroke,
-    pub input: Option<String>,
 }
 
 #[derive(Clone, Debug)]

crates/gpui/src/platform/mac/event.rs πŸ”—

@@ -10,12 +10,26 @@ use cocoa::{
     base::{id, YES},
     foundation::NSString as _,
 };
+use core_graphics::{
+    event::{CGEvent, CGEventFlags, CGKeyCode},
+    event_source::{CGEventSource, CGEventSourceStateID},
+};
+use objc::{class, msg_send, sel, sel_impl};
 use std::{borrow::Cow, ffi::CStr, os::raw::c_char};
 
+const BACKSPACE_KEY: u16 = 0x7f;
+const SPACE_KEY: u16 = b' ' as u16;
+const ENTER_KEY: u16 = 0x0d;
+const NUMPAD_ENTER_KEY: u16 = 0x03;
+const ESCAPE_KEY: u16 = 0x1b;
+const TAB_KEY: u16 = 0x09;
+const SHIFT_TAB_KEY: u16 = 0x19;
+
 pub fn key_to_native(key: &str) -> Cow<str> {
     use cocoa::appkit::*;
     let code = match key {
-        "backspace" => 0x7F,
+        "space" => SPACE_KEY,
+        "backspace" => BACKSPACE_KEY,
         "up" => NSUpArrowFunctionKey,
         "down" => NSDownArrowFunctionKey,
         "left" => NSLeftArrowFunctionKey,
@@ -68,49 +82,13 @@ impl Event {
                     cmd,
                 }))
             }
-            NSEventType::NSKeyDown => {
-                let modifiers = native_event.modifierFlags();
-                let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
-                let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
-                let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
-                let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
-                let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
-
-                let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?;
-
-                Some(Self::KeyDown(KeyDownEvent {
-                    keystroke: Keystroke {
-                        ctrl,
-                        alt,
-                        shift,
-                        cmd,
-                        key: unmodified_chars.into(),
-                    },
-                    input,
-                    is_held: native_event.isARepeat() == YES,
-                }))
-            }
-            NSEventType::NSKeyUp => {
-                let modifiers = native_event.modifierFlags();
-                let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
-                let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
-                let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
-                let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
-                let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
-
-                let (unmodified_chars, input) = get_key_text(native_event, cmd, ctrl, function)?;
-
-                Some(Self::KeyUp(KeyUpEvent {
-                    keystroke: Keystroke {
-                        ctrl,
-                        alt,
-                        shift,
-                        cmd,
-                        key: unmodified_chars.into(),
-                    },
-                    input,
-                }))
-            }
+            NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent {
+                keystroke: parse_keystroke(native_event),
+                is_held: native_event.isARepeat() == YES,
+            })),
+            NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent {
+                keystroke: parse_keystroke(native_event),
+            })),
             NSEventType::NSLeftMouseDown
             | NSEventType::NSRightMouseDown
             | NSEventType::NSOtherMouseDown => {
@@ -229,72 +207,109 @@ impl Event {
     }
 }
 
-unsafe fn get_key_text(
-    native_event: id,
-    cmd: bool,
-    ctrl: bool,
-    function: bool,
-) -> Option<(&'static str, Option<String>)> {
-    let unmodified_chars =
+unsafe fn parse_keystroke(native_event: id) -> Keystroke {
+    use cocoa::appkit::*;
+
+    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 mut input = None;
-    let first_char = unmodified_chars.chars().next()?;
-    use cocoa::appkit::*;
-    const BACKSPACE_KEY: u16 = 0x7f;
-    const ENTER_KEY: u16 = 0x0d;
-    const NUMPAD_ENTER_KEY: u16 = 0x03;
-    const ESCAPE_KEY: u16 = 0x1b;
-    const TAB_KEY: u16 = 0x09;
-    const SHIFT_TAB_KEY: u16 = 0x19;
-    const SPACE_KEY: u16 = b' ' as u16;
-
     #[allow(non_upper_case_globals)]
-    let unmodified_chars = match first_char as u16 {
-        SPACE_KEY => {
-            input = Some(" ".to_string());
-            "space"
-        }
-        BACKSPACE_KEY => "backspace",
-        ENTER_KEY | NUMPAD_ENTER_KEY => "enter",
-        ESCAPE_KEY => "escape",
-        TAB_KEY => "tab",
-        SHIFT_TAB_KEY => "tab",
+    let key = match chars_ignoring_modifiers.chars().next().map(|ch| ch as u16) {
+        Some(SPACE_KEY) => "space",
+        Some(BACKSPACE_KEY) => "backspace",
+        Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter",
+        Some(ESCAPE_KEY) => "escape",
+        Some(TAB_KEY) => "tab",
+        Some(SHIFT_TAB_KEY) => "tab",
+        Some(NSUpArrowFunctionKey) => "up",
+        Some(NSDownArrowFunctionKey) => "down",
+        Some(NSLeftArrowFunctionKey) => "left",
+        Some(NSRightArrowFunctionKey) => "right",
+        Some(NSPageUpFunctionKey) => "pageup",
+        Some(NSPageDownFunctionKey) => "pagedown",
+        Some(NSDeleteFunctionKey) => "delete",
+        Some(NSF1FunctionKey) => "f1",
+        Some(NSF2FunctionKey) => "f2",
+        Some(NSF3FunctionKey) => "f3",
+        Some(NSF4FunctionKey) => "f4",
+        Some(NSF5FunctionKey) => "f5",
+        Some(NSF6FunctionKey) => "f6",
+        Some(NSF7FunctionKey) => "f7",
+        Some(NSF8FunctionKey) => "f8",
+        Some(NSF9FunctionKey) => "f9",
+        Some(NSF10FunctionKey) => "f10",
+        Some(NSF11FunctionKey) => "f11",
+        Some(NSF12FunctionKey) => "f12",
+        _ => {
+            let mut chars_ignoring_modifiers_and_shift =
+                chars_for_modified_key(native_event.keyCode(), false, false);
 
-        NSUpArrowFunctionKey => "up",
-        NSDownArrowFunctionKey => "down",
-        NSLeftArrowFunctionKey => "left",
-        NSRightArrowFunctionKey => "right",
-        NSPageUpFunctionKey => "pageup",
-        NSPageDownFunctionKey => "pagedown",
-        NSDeleteFunctionKey => "delete",
-        NSF1FunctionKey => "f1",
-        NSF2FunctionKey => "f2",
-        NSF3FunctionKey => "f3",
-        NSF4FunctionKey => "f4",
-        NSF5FunctionKey => "f5",
-        NSF6FunctionKey => "f6",
-        NSF7FunctionKey => "f7",
-        NSF8FunctionKey => "f8",
-        NSF9FunctionKey => "f9",
-        NSF10FunctionKey => "f10",
-        NSF11FunctionKey => "f11",
-        NSF12FunctionKey => "f12",
+            // Honor ⌘ when Dvorak-QWERTY is used.
+            let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
+            if cmd && chars_ignoring_modifiers_and_shift != chars_with_cmd {
+                chars_ignoring_modifiers =
+                    chars_for_modified_key(native_event.keyCode(), true, shift);
+                chars_ignoring_modifiers_and_shift = chars_with_cmd;
+            }
 
-        _ => {
-            if !cmd && !ctrl && !function {
-                input = Some(
-                    CStr::from_ptr(native_event.characters().UTF8String() as *mut c_char)
-                        .to_str()
-                        .unwrap()
-                        .into(),
-                );
+            if shift {
+                if chars_ignoring_modifiers_and_shift
+                    == chars_ignoring_modifiers.to_ascii_lowercase()
+                {
+                    chars_ignoring_modifiers_and_shift
+                } else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers {
+                    shift = false;
+                    chars_ignoring_modifiers
+                } else {
+                    chars_ignoring_modifiers
+                }
+            } else {
+                chars_ignoring_modifiers
             }
-            unmodified_chars
         }
     };
 
-    Some((unmodified_chars, input))
+    Keystroke {
+        ctrl,
+        alt,
+        shift,
+        cmd,
+        key: key.into(),
+    }
+}
+
+fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a str {
+    // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
+    // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
+    // an event with the given flags instead lets us access `characters`, which always
+    // returns a valid string.
+    let event = CGEvent::new_keyboard_event(
+        CGEventSource::new(CGEventSourceStateID::Private).unwrap(),
+        code,
+        true,
+    )
+    .unwrap();
+    let mut flags = CGEventFlags::empty();
+    if cmd {
+        flags |= CGEventFlags::CGEventFlagCommand;
+    }
+    if shift {
+        flags |= CGEventFlags::CGEventFlagShift;
+    }
+    event.set_flags(flags);
+
+    let event: id = unsafe { msg_send![class!(NSEvent), eventWithCGEvent: event] };
+    unsafe {
+        CStr::from_ptr(event.characters().UTF8String())
+            .to_str()
+            .unwrap()
+    }
 }

crates/gpui/src/platform/mac/window.rs πŸ”—

@@ -1,3 +1,4 @@
+use super::{geometry::RectFExt, renderer::Renderer};
 use crate::{
     executor,
     geometry::{
@@ -6,7 +7,8 @@ use crate::{
     },
     keymap::Keystroke,
     platform::{self, Event, WindowBounds, WindowContext},
-    KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, Scene,
+    InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
+    MouseMovedEvent, Scene,
 };
 use block::ConcreteBlock;
 use cocoa::{
@@ -15,7 +17,9 @@ use cocoa::{
         NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask,
     },
     base::{id, nil},
-    foundation::{NSAutoreleasePool, NSInteger, NSSize, NSString},
+    foundation::{
+        NSAutoreleasePool, NSInteger, NSNotFound, NSPoint, NSRect, NSSize, NSString, NSUInteger,
+    },
     quartzcore::AutoresizingMask,
 };
 use core_graphics::display::CGRect;
@@ -34,20 +38,71 @@ use std::{
     any::Any,
     cell::{Cell, RefCell},
     convert::TryInto,
-    ffi::c_void,
-    mem, ptr,
+    ffi::{c_void, CStr},
+    mem,
+    ops::Range,
+    os::raw::c_char,
+    ptr,
     rc::{Rc, Weak},
     sync::Arc,
     time::Duration,
 };
 
-use super::{geometry::RectFExt, renderer::Renderer};
-
 const WINDOW_STATE_IVAR: &'static str = "windowState";
 
 static mut WINDOW_CLASS: *const Class = ptr::null();
 static mut VIEW_CLASS: *const Class = ptr::null();
 
+#[repr(C)]
+#[derive(Copy, Clone, Debug)]
+struct NSRange {
+    pub location: NSUInteger,
+    pub length: NSUInteger,
+}
+
+impl NSRange {
+    fn invalid() -> Self {
+        Self {
+            location: NSNotFound as NSUInteger,
+            length: 0,
+        }
+    }
+
+    fn is_valid(&self) -> bool {
+        self.location != NSNotFound as NSUInteger
+    }
+
+    fn to_range(&self) -> Option<Range<usize>> {
+        if self.is_valid() {
+            let start = self.location as usize;
+            let end = start + self.length as usize;
+            Some(start..end)
+        } else {
+            None
+        }
+    }
+}
+
+impl From<Range<usize>> for NSRange {
+    fn from(range: Range<usize>) -> Self {
+        NSRange {
+            location: range.start as NSUInteger,
+            length: range.len() as NSUInteger,
+        }
+    }
+}
+
+unsafe impl objc::Encode for NSRange {
+    fn encode() -> objc::Encoding {
+        let encoding = format!(
+            "{{NSRange={}{}}}",
+            NSUInteger::encode().as_str(),
+            NSUInteger::encode().as_str()
+        );
+        unsafe { objc::Encoding::from_str(&encoding) }
+    }
+}
+
 #[allow(non_upper_case_globals)]
 const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
 
@@ -163,6 +218,48 @@ unsafe fn build_classes() {
             display_layer as extern "C" fn(&Object, Sel, id),
         );
 
+        decl.add_protocol(Protocol::get("NSTextInputClient").unwrap());
+        decl.add_method(
+            sel!(validAttributesForMarkedText),
+            valid_attributes_for_marked_text as extern "C" fn(&Object, Sel) -> id,
+        );
+        decl.add_method(
+            sel!(hasMarkedText),
+            has_marked_text as extern "C" fn(&Object, Sel) -> BOOL,
+        );
+        decl.add_method(
+            sel!(markedRange),
+            marked_range as extern "C" fn(&Object, Sel) -> NSRange,
+        );
+        decl.add_method(
+            sel!(selectedRange),
+            selected_range as extern "C" fn(&Object, Sel) -> NSRange,
+        );
+        decl.add_method(
+            sel!(firstRectForCharacterRange:actualRange:),
+            first_rect_for_character_range as extern "C" fn(&Object, Sel, NSRange, id) -> NSRect,
+        );
+        decl.add_method(
+            sel!(insertText:replacementRange:),
+            insert_text as extern "C" fn(&Object, Sel, id, NSRange),
+        );
+        decl.add_method(
+            sel!(setMarkedText:selectedRange:replacementRange:),
+            set_marked_text as extern "C" fn(&Object, Sel, id, NSRange, NSRange),
+        );
+        decl.add_method(sel!(unmarkText), unmark_text as extern "C" fn(&Object, Sel));
+        decl.add_method(
+            sel!(attributedSubstringForProposedRange:actualRange:),
+            attributed_substring_for_proposed_range
+                as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id,
+        );
+
+        // Suppress beep on keystrokes with modifier keys.
+        decl.add_method(
+            sel!(doCommandBySelector:),
+            do_command_by_selector as extern "C" fn(&Object, Sel, Sel),
+        );
+
         decl.register()
     };
 }
@@ -177,12 +274,14 @@ struct WindowState {
     resize_callback: Option<Box<dyn FnMut()>>,
     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>,
     synthetic_drag_counter: usize,
     executor: Rc<executor::Foreground>,
     scene_to_render: Option<Scene>,
     renderer: Renderer,
     command_queue: metal::CommandQueue,
-    last_fresh_keydown: Option<(Keystroke, Option<String>)>,
+    last_fresh_keydown: Option<Keystroke>,
     layer: id,
     traffic_light_position: Option<Vector2F>,
     previous_modifiers_changed_event: Option<Event>,
@@ -263,6 +362,8 @@ impl Window {
                 should_close_callback: None,
                 close_callback: None,
                 activate_callback: None,
+                input_handler: None,
+                pending_key_down_event: None,
                 synthetic_drag_counter: 0,
                 executor,
                 scene_to_render: Default::default(),
@@ -371,6 +472,10 @@ impl platform::Window for Window {
         self.0.as_ref().borrow_mut().activate_callback = Some(callback);
     }
 
+    fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>) {
+        self.0.as_ref().borrow_mut().input_handler = Some(input_handler);
+    }
+
     fn prompt(
         &self,
         level: platform::PromptLevel,
@@ -448,6 +553,14 @@ impl platform::Window for Window {
         // so we have to move it again.
         self.0.borrow().move_traffic_light();
     }
+
+    fn show_character_palette(&self) {
+        unsafe {
+            let app = NSApplication::sharedApplication(nil);
+            let window = self.0.borrow().native_window;
+            let _: () = msg_send![app, orderFrontCharacterPalette: window];
+        }
+    }
 }
 
 impl platform::WindowContext for Window {
@@ -581,38 +694,53 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) {
 
 extern "C" fn handle_key_equivalent(this: &Object, _: Sel, native_event: id) -> 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 {
-        match &event {
-            Event::KeyDown(KeyDownEvent {
-                keystroke,
-                input,
-                is_held,
-            }) => {
-                let keydown = (keystroke.clone(), input.clone());
+        window_state_borrow.pending_key_down_event = match event {
+            Event::KeyDown(event) => {
+                let keydown = event.keystroke.clone();
                 // Ignore events from held-down keys after some of the initially-pressed keys
                 // were released.
-                if *is_held {
+                if event.is_held {
                     if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
                         return YES;
                     }
                 } else {
                     window_state_borrow.last_fresh_keydown = Some(keydown);
                 }
+
+                Some(event)
             }
             _ => 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 let Some(mut callback) = window_state_borrow.event_callback.take() {
-            drop(window_state_borrow);
-            let handled = callback(event);
-            window_state.borrow_mut().event_callback = Some(callback);
-            handled as BOOL
-        } else {
-            NO
+        let mut window_state_borrow = window_state.borrow_mut();
+        if let Some(event) = window_state_borrow.pending_key_down_event.take() {
+            if let Some(mut callback) = window_state_borrow.event_callback.take() {
+                drop(window_state_borrow);
+
+                let is_composing =
+                    with_input_handler(this, |input_handler| input_handler.marked_text_range())
+                        .flatten()
+                        .is_some();
+                if !is_composing {
+                    callback(Event::KeyDown(event));
+                }
+
+                window_state.borrow_mut().event_callback = Some(callback);
+            }
         }
+
+        YES
     } else {
         NO
     }
@@ -624,7 +752,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     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 {
         match &event {
             Event::MouseMoved(
@@ -691,21 +818,19 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
     let window_state = unsafe { get_window_state(this) };
     let mut window_state_borrow = window_state.as_ref().borrow_mut();
 
-    let chars = ".".to_string();
     let keystroke = Keystroke {
         cmd: true,
         ctrl: false,
         alt: false,
         shift: false,
-        key: chars.clone(),
+        key: ".".into(),
     };
     let event = Event::KeyDown(KeyDownEvent {
         keystroke: keystroke.clone(),
-        input: Some(chars.clone()),
         is_held: false,
     });
 
-    window_state_borrow.last_fresh_keydown = Some((keystroke, Some(chars)));
+    window_state_borrow.last_fresh_keydown = Some(keystroke);
     if let Some(mut callback) = window_state_borrow.event_callback.take() {
         drop(window_state_borrow);
         callback(event);
@@ -866,6 +991,164 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
     }
 }
 
+extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
+    unsafe { msg_send![class!(NSArray), array] }
+}
+
+extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL {
+    with_input_handler(this, |input_handler| input_handler.marked_text_range())
+        .flatten()
+        .is_some() as BOOL
+}
+
+extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
+    with_input_handler(this, |input_handler| input_handler.marked_text_range())
+        .flatten()
+        .map_or(NSRange::invalid(), |range| range.into())
+}
+
+extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
+    with_input_handler(this, |input_handler| input_handler.selected_text_range())
+        .flatten()
+        .map_or(NSRange::invalid(), |range| range.into())
+}
+
+extern "C" fn first_rect_for_character_range(
+    this: &Object,
+    _: Sel,
+    range: NSRange,
+    _: id,
+) -> NSRect {
+    let frame = unsafe {
+        let window = get_window_state(this).borrow().native_window;
+        NSView::frame(window)
+    };
+
+    with_input_handler(this, |input_handler| {
+        input_handler.rect_for_range(range.to_range()?)
+    })
+    .flatten()
+    .map_or(
+        NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)),
+        |rect| {
+            NSRect::new(
+                NSPoint::new(
+                    frame.origin.x + rect.origin_x() as f64,
+                    frame.origin.y + frame.size.height - rect.origin_y() as f64,
+                ),
+                NSSize::new(rect.width() as f64, rect.height() as f64),
+            )
+        },
+    )
+}
+
+extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NSRange) {
+    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();
+        drop(window_state_borrow);
+
+        let is_attributed_string: BOOL =
+            msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
+        let text: id = if is_attributed_string == YES {
+            msg_send![text, string]
+        } else {
+            text
+        };
+        let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
+            .to_str()
+            .unwrap();
+        let replacement_range = replacement_range.to_range();
+
+        let is_composing =
+            with_input_handler(this, |input_handler| input_handler.marked_text_range())
+                .flatten()
+                .is_some();
+
+        if is_composing || text.chars().count() > 1 || pending_key_down_event.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)
+                });
+            }
+        }
+    }
+}
+
+extern "C" fn set_marked_text(
+    this: &Object,
+    _: Sel,
+    text: id,
+    selected_range: NSRange,
+    replacement_range: NSRange,
+) {
+    unsafe {
+        get_window_state(this)
+            .borrow_mut()
+            .pending_key_down_event
+            .take();
+
+        let is_attributed_string: BOOL =
+            msg_send![text, isKindOfClass: [class!(NSAttributedString)]];
+        let text: id = if is_attributed_string == YES {
+            msg_send![text, string]
+        } else {
+            text
+        };
+        let selected_range = selected_range.to_range();
+        let replacement_range = replacement_range.to_range();
+        let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
+            .to_str()
+            .unwrap();
+
+        with_input_handler(this, |input_handler| {
+            input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range);
+        });
+    }
+}
+
+extern "C" fn unmark_text(this: &Object, _: Sel) {
+    with_input_handler(this, |input_handler| input_handler.unmark_text());
+}
+
+extern "C" fn attributed_substring_for_proposed_range(
+    this: &Object,
+    _: Sel,
+    range: NSRange,
+    _actual_range: *mut c_void,
+) -> id {
+    with_input_handler(this, |input_handler| {
+        let range = range.to_range()?;
+        if range.is_empty() {
+            return None;
+        }
+
+        let selected_text = input_handler.text_for_range(range)?;
+        unsafe {
+            let string: id = msg_send![class!(NSAttributedString), alloc];
+            let string: id = msg_send![string, initWithString: ns_string(&selected_text)];
+            Some(string)
+        }
+    })
+    .flatten()
+    .unwrap_or(nil)
+}
+
+extern "C" fn do_command_by_selector(_: &Object, _: Sel, _: Sel) {}
+
 async fn synthetic_drag(
     window_state: Weak<RefCell<WindowState>>,
     drag_id: usize,
@@ -891,3 +1174,19 @@ async fn synthetic_drag(
 unsafe fn ns_string(string: &str) -> id {
     NSString::alloc(nil).init_str(string).autorelease()
 }
+
+fn with_input_handler<F, R>(window: &Object, f: F) -> Option<R>
+where
+    F: FnOnce(&mut dyn InputHandler) -> R,
+{
+    let window_state = unsafe { get_window_state(window) };
+    let mut window_state_borrow = window_state.as_ref().borrow_mut();
+    if let Some(mut input_handler) = window_state_borrow.input_handler.take() {
+        drop(window_state_borrow);
+        let result = f(input_handler.as_mut());
+        window_state.borrow_mut().input_handler = Some(input_handler);
+        Some(result)
+    } else {
+        None
+    }
+}

crates/gpui/src/platform/test.rs πŸ”—

@@ -255,6 +255,8 @@ impl super::Window for Window {
         self.close_handlers.push(callback);
     }
 
+    fn set_input_handler(&mut self, _: Box<dyn crate::InputHandler>) {}
+
     fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver<usize> {
         let (done_tx, done_rx) = oneshot::channel();
         self.pending_prompts.borrow_mut().push_back(done_tx);
@@ -274,6 +276,8 @@ impl super::Window for Window {
     fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
         self.should_close_handler = Some(callback);
     }
+
+    fn show_character_palette(&self) {}
 }
 
 pub fn platform() -> Platform {

crates/gpui/src/presenter.rs πŸ”—

@@ -19,7 +19,7 @@ use smallvec::SmallVec;
 use std::{
     collections::{HashMap, HashSet},
     marker::PhantomData,
-    ops::{Deref, DerefMut},
+    ops::{Deref, DerefMut, Range},
     sync::Arc,
 };
 
@@ -224,6 +224,17 @@ impl Presenter {
         }
     }
 
+    pub fn rect_for_text_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<RectF> {
+        cx.focused_view_id(self.window_id).and_then(|view_id| {
+            let cx = MeasurementContext {
+                app: cx,
+                rendered_views: &self.rendered_views,
+                window_id: self.window_id,
+            };
+            cx.rect_for_text_range(view_id, range_utf16)
+        })
+    }
+
     pub fn dispatch_event(&mut self, event: Event, cx: &mut MutableAppContext) -> bool {
         if let Some(root_view_id) = cx.root_view_id(self.window_id) {
             let mut invalidated_views = Vec::new();
@@ -717,6 +728,27 @@ impl<'a> DerefMut for EventContext<'a> {
     }
 }
 
+pub struct MeasurementContext<'a> {
+    app: &'a AppContext,
+    rendered_views: &'a HashMap<usize, ElementBox>,
+    pub window_id: usize,
+}
+
+impl<'a> Deref for MeasurementContext<'a> {
+    type Target = AppContext;
+
+    fn deref(&self) -> &Self::Target {
+        self.app
+    }
+}
+
+impl<'a> MeasurementContext<'a> {
+    fn rect_for_text_range(&self, view_id: usize, range_utf16: Range<usize>) -> Option<RectF> {
+        let element = self.rendered_views.get(&view_id)?;
+        element.rect_for_text_range(range_utf16, self)
+    }
+}
+
 pub struct DebugContext<'a> {
     rendered_views: &'a HashMap<usize, ElementBox>,
     pub font_cache: &'a FontCache,
@@ -876,6 +908,18 @@ impl Element for ChildView {
         cx.dispatch_event(self.view.id(), event)
     }
 
+    fn rect_for_text_range(
+        &self,
+        range_utf16: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        cx: &MeasurementContext,
+    ) -> Option<RectF> {
+        cx.rect_for_text_range(self.view.id(), range_utf16)
+    }
+
     fn debug(
         &self,
         bounds: RectF,

crates/language/src/buffer.rs πŸ”—

@@ -1076,6 +1076,10 @@ impl Buffer {
         self.text.finalize_last_transaction()
     }
 
+    pub fn group_until_transaction(&mut self, transaction_id: TransactionId) {
+        self.text.group_until_transaction(transaction_id);
+    }
+
     pub fn forget_transaction(&mut self, transaction_id: TransactionId) {
         self.text.forget_transaction(transaction_id);
     }

crates/language/src/proto.rs πŸ”—

@@ -39,11 +39,6 @@ pub fn serialize_operation(operation: &Operation) -> proto::Operation {
                 local_timestamp: undo.id.value,
                 lamport_timestamp: lamport_timestamp.value,
                 version: serialize_version(&undo.version),
-                transaction_ranges: undo
-                    .transaction_ranges
-                    .iter()
-                    .map(serialize_range)
-                    .collect(),
                 transaction_version: serialize_version(&undo.transaction_version),
                 counts: undo
                     .counts
@@ -204,11 +199,6 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<Operation> {
                             )
                         })
                         .collect(),
-                    transaction_ranges: undo
-                        .transaction_ranges
-                        .into_iter()
-                        .map(deserialize_range)
-                        .collect(),
                     transaction_version: deserialize_version(undo.transaction_version),
                 },
             }),
@@ -460,8 +450,6 @@ pub fn serialize_transaction(transaction: &Transaction) -> proto::Transaction {
             .map(serialize_local_timestamp)
             .collect(),
         start: serialize_version(&transaction.start),
-        end: serialize_version(&transaction.end),
-        ranges: transaction.ranges.iter().map(serialize_range).collect(),
     }
 }
 
@@ -478,12 +466,6 @@ pub fn deserialize_transaction(transaction: proto::Transaction) -> Result<Transa
             .map(deserialize_local_timestamp)
             .collect(),
         start: deserialize_version(transaction.start.into()),
-        end: deserialize_version(transaction.end),
-        ranges: transaction
-            .ranges
-            .into_iter()
-            .map(deserialize_range)
-            .collect(),
     })
 }
 

crates/project/src/fs.rs πŸ”—

@@ -162,7 +162,7 @@ impl Fs for RealFs {
     }
 
     async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
-        let buffer_size = text.summary().bytes.min(10 * 1024);
+        let buffer_size = text.summary().len.min(10 * 1024);
         let file = smol::fs::File::create(path).await?;
         let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
         for chunk in chunks(text, line_ending) {

crates/rpc/proto/zed.proto πŸ”—

@@ -534,8 +534,6 @@ message Transaction {
     LocalTimestamp id = 1;
     repeated LocalTimestamp edit_ids = 2;
     repeated VectorClockEntry start = 3;
-    repeated VectorClockEntry end = 4;
-    repeated Range ranges = 5;
 }
 
 message LocalTimestamp {
@@ -890,7 +888,6 @@ message Operation {
         uint32 local_timestamp = 2;
         uint32 lamport_timestamp = 3;
         repeated VectorClockEntry version = 4;
-        repeated Range transaction_ranges = 5;
         repeated VectorClockEntry transaction_version = 6;
         repeated UndoCount counts = 7;
     }

crates/rpc/src/rpc.rs πŸ”—

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 27;
+pub const PROTOCOL_VERSION: u32 = 28;

crates/terminal/src/connected_el.rs πŸ”—

@@ -796,6 +796,27 @@ impl Element for TerminalEl {
             "type": "TerminalElement",
         })
     }
+
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        bounds: RectF,
+        _: RectF,
+        layout: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &gpui::MeasurementContext,
+    ) -> Option<RectF> {
+        // Use the same origin that's passed to `Cursor::paint` in the paint
+        // method bove.
+        let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
+
+        // TODO - Why is it necessary to move downward one line to get correct
+        // positioning? I would think that we'd want the same rect that is
+        // painted for the cursor.
+        origin += vec2f(0., layout.size.line_height);
+
+        Some(layout.cursor.as_ref()?.bounding_rect(origin))
+    }
 }
 
 mod test {

crates/terminal/src/connected_view.rs πŸ”—

@@ -1,6 +1,6 @@
 use gpui::{
-    actions, keymap::Keystroke, ClipboardItem, Element, ElementBox, ModelHandle, MutableAppContext,
-    View, ViewContext,
+    actions, keymap::Keystroke, AppContext, ClipboardItem, Element, ElementBox, ModelHandle,
+    MutableAppContext, View, ViewContext,
 };
 
 use crate::{
@@ -159,4 +159,18 @@ impl View for ConnectedView {
     fn on_focus(&mut self, _cx: &mut ViewContext<Self>) {
         self.has_new_content = false;
     }
+
+    fn selected_text_range(&self, _: &AppContext) -> Option<std::ops::Range<usize>> {
+        Some(0..0)
+    }
+
+    fn replace_text_in_range(
+        &mut self,
+        _: Option<std::ops::Range<usize>>,
+        text: &str,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.terminal
+            .update(cx, |terminal, _| terminal.write_to_pty(text.into()));
+    }
 }

crates/terminal/src/mappings/keys.rs πŸ”—

@@ -231,53 +231,7 @@ pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option<String> {
         }
     }
 
-    //Fallback to sending the keystroke input directly
-    //Skin colors in utf8 are implemented as a seperate, invisible character
-    //that modifies the associated emoji. Some languages may have similarly
-    //implemented modifiers, e.g. certain diacritics that can be typed as a single character.
-    //This means that we need to assume some user input can result in multi-byte,
-    //multi-char strings. This is somewhat difficult, as GPUI normalizes all
-    //keys into a string representation. Hence, the check here to filter out GPUI
-    //keys that weren't captured above.
-    if !matches_gpui_key_str(&keystroke.key) {
-        return Some(keystroke.key.clone());
-    } else {
-        None
-    }
-}
-
-///Checks if the given string matches a GPUI key string.
-///Table made from reading the source at gpui/src/platform/mac/event.rs
-fn matches_gpui_key_str(str: &str) -> bool {
-    match str {
-        "backspace" => true,
-        "up" => true,
-        "down" => true,
-        "left" => true,
-        "right" => true,
-        "pageup" => true,
-        "pagedown" => true,
-        "home" => true,
-        "end" => true,
-        "delete" => true,
-        "enter" => true,
-        "escape" => true,
-        "tab" => true,
-        "f1" => true,
-        "f2" => true,
-        "f3" => true,
-        "f4" => true,
-        "f5" => true,
-        "f6" => true,
-        "f7" => true,
-        "f8" => true,
-        "f9" => true,
-        "f10" => true,
-        "f11" => true,
-        "f12" => true,
-        "space" => true,
-        _ => false,
-    }
+    None
 }
 
 ///   Code     Modifiers
@@ -351,17 +305,15 @@ mod test {
     }
 
     #[test]
-    fn test_multi_char_fallthrough() {
+    fn test_plain_inputs() {
         let ks = Keystroke {
             ctrl: false,
             alt: false,
             shift: false,
             cmd: false,
-
             key: "πŸ––πŸ»".to_string(), //2 char string
         };
-
-        assert_eq!(to_esc_str(&ks, &TermMode::NONE), Some("πŸ––πŸ»".to_string()));
+        assert_eq!(to_esc_str(&ks, &TermMode::NONE), None);
     }
 
     #[test]

crates/text/src/offset_utf16.rs πŸ”—

@@ -0,0 +1,50 @@
+use std::ops::{Add, AddAssign, Sub};
+
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
+pub struct OffsetUtf16(pub usize);
+
+impl<'a> Add<&'a Self> for OffsetUtf16 {
+    type Output = Self;
+
+    fn add(self, other: &'a Self) -> Self::Output {
+        Self(self.0 + other.0)
+    }
+}
+
+impl Add for OffsetUtf16 {
+    type Output = Self;
+
+    fn add(self, other: Self) -> Self::Output {
+        Self(self.0 + other.0)
+    }
+}
+
+impl<'a> Sub<&'a Self> for OffsetUtf16 {
+    type Output = Self;
+
+    fn sub(self, other: &'a Self) -> Self::Output {
+        debug_assert!(*other <= self);
+        Self(self.0 - other.0)
+    }
+}
+
+impl Sub for OffsetUtf16 {
+    type Output = OffsetUtf16;
+
+    fn sub(self, other: Self) -> Self::Output {
+        debug_assert!(other <= self);
+        Self(self.0 - other.0)
+    }
+}
+
+impl<'a> AddAssign<&'a Self> for OffsetUtf16 {
+    fn add_assign(&mut self, other: &'a Self) {
+        self.0 += other.0;
+    }
+}
+
+impl AddAssign<Self> for OffsetUtf16 {
+    fn add_assign(&mut self, other: Self) {
+        self.0 += other.0;
+    }
+}

crates/text/src/rope.rs πŸ”—

@@ -1,6 +1,5 @@
-use crate::PointUtf16;
-
 use super::Point;
+use crate::{OffsetUtf16, PointUtf16};
 use arrayvec::ArrayString;
 use bromberg_sl2::{DigestString, HashMatrix};
 use smallvec::SmallVec;
@@ -165,8 +164,34 @@ impl Rope {
         Chunks::new(self, range, true)
     }
 
+    pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 {
+        if offset >= self.summary().len {
+            return self.summary().len_utf16;
+        }
+        let mut cursor = self.chunks.cursor::<(usize, OffsetUtf16)>();
+        cursor.seek(&offset, Bias::Left, &());
+        let overshoot = offset - cursor.start().0;
+        cursor.start().1
+            + cursor.item().map_or(Default::default(), |chunk| {
+                chunk.offset_to_offset_utf16(overshoot)
+            })
+    }
+
+    pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize {
+        if offset >= self.summary().len_utf16 {
+            return self.summary().len;
+        }
+        let mut cursor = self.chunks.cursor::<(OffsetUtf16, usize)>();
+        cursor.seek(&offset, Bias::Left, &());
+        let overshoot = offset - cursor.start().0;
+        cursor.start().1
+            + cursor.item().map_or(Default::default(), |chunk| {
+                chunk.offset_utf16_to_offset(overshoot)
+            })
+    }
+
     pub fn offset_to_point(&self, offset: usize) -> Point {
-        if offset >= self.summary().bytes {
+        if offset >= self.summary().len {
             return self.summary().lines;
         }
         let mut cursor = self.chunks.cursor::<(usize, Point)>();
@@ -179,8 +204,8 @@ impl Rope {
     }
 
     pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 {
-        if offset >= self.summary().bytes {
-            return self.summary().lines_utf16;
+        if offset >= self.summary().len {
+            return self.summary().lines_utf16();
         }
         let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>();
         cursor.seek(&offset, Bias::Left, &());
@@ -193,7 +218,7 @@ impl Rope {
 
     pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 {
         if point >= self.summary().lines {
-            return self.summary().lines_utf16;
+            return self.summary().lines_utf16();
         }
         let mut cursor = self.chunks.cursor::<(Point, PointUtf16)>();
         cursor.seek(&point, Bias::Left, &());
@@ -206,7 +231,7 @@ impl Rope {
 
     pub fn point_to_offset(&self, point: Point) -> usize {
         if point >= self.summary().lines {
-            return self.summary().bytes;
+            return self.summary().len;
         }
         let mut cursor = self.chunks.cursor::<(Point, usize)>();
         cursor.seek(&point, Bias::Left, &());
@@ -218,8 +243,8 @@ impl Rope {
     }
 
     pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize {
-        if point >= self.summary().lines_utf16 {
-            return self.summary().bytes;
+        if point >= self.summary().lines_utf16() {
+            return self.summary().len;
         }
         let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>();
         cursor.seek(&point, Bias::Left, &());
@@ -231,7 +256,7 @@ impl Rope {
     }
 
     pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point {
-        if point >= self.summary().lines_utf16 {
+        if point >= self.summary().lines_utf16() {
             return self.summary().lines;
         }
         let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>();
@@ -262,7 +287,18 @@ impl Rope {
             }
             offset
         } else {
-            self.summary().bytes
+            self.summary().len
+        }
+    }
+
+    pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 {
+        let mut cursor = self.chunks.cursor::<OffsetUtf16>();
+        cursor.seek(&offset, Bias::Right, &());
+        if let Some(chunk) = cursor.item() {
+            let overshoot = offset - cursor.start();
+            *cursor.start() + chunk.clip_offset_utf16(overshoot, bias)
+        } else {
+            self.summary().len_utf16
         }
     }
 
@@ -284,7 +320,7 @@ impl Rope {
             let overshoot = point - cursor.start();
             *cursor.start() + chunk.clip_point_utf16(overshoot, bias)
         } else {
-            self.summary().lines_utf16
+            self.summary().lines_utf16()
         }
     }
 
@@ -543,6 +579,34 @@ impl<'a> io::Read for Bytes<'a> {
 struct Chunk(ArrayString<{ 2 * CHUNK_BASE }>);
 
 impl Chunk {
+    fn offset_to_offset_utf16(&self, target: usize) -> OffsetUtf16 {
+        let mut offset = 0;
+        let mut offset_utf16 = OffsetUtf16(0);
+        for ch in self.0.chars() {
+            if offset >= target {
+                break;
+            }
+
+            offset += ch.len_utf8();
+            offset_utf16.0 += ch.len_utf16();
+        }
+        offset_utf16
+    }
+
+    fn offset_utf16_to_offset(&self, target: OffsetUtf16) -> usize {
+        let mut offset_utf16 = OffsetUtf16(0);
+        let mut offset = 0;
+        for ch in self.0.chars() {
+            if offset_utf16 >= target {
+                break;
+            }
+
+            offset += ch.len_utf8();
+            offset_utf16.0 += ch.len_utf16();
+        }
+        offset
+    }
+
     fn offset_to_point(&self, target: usize) -> Point {
         let mut offset = 0;
         let mut point = Point::new(0, 0);
@@ -712,6 +776,18 @@ impl Chunk {
         }
         unreachable!()
     }
+
+    fn clip_offset_utf16(&self, target: OffsetUtf16, bias: Bias) -> OffsetUtf16 {
+        let mut code_units = self.0.encode_utf16();
+        let mut offset = code_units.by_ref().take(target.0 as usize).count();
+        if char::decode_utf16(code_units).next().transpose().is_err() {
+            match bias {
+                Bias::Left => offset -= 1,
+                Bias::Right => offset += 1,
+            }
+        }
+        OffsetUtf16(offset)
+    }
 }
 
 impl sum_tree::Item for Chunk {
@@ -748,31 +824,44 @@ impl sum_tree::Summary for ChunkSummary {
 
 #[derive(Clone, Debug, Default, Eq, PartialEq)]
 pub struct TextSummary {
-    pub bytes: usize,
+    pub len: usize,
+    pub len_utf16: OffsetUtf16,
     pub lines: Point,
-    pub lines_utf16: PointUtf16,
     pub first_line_chars: u32,
     pub last_line_chars: u32,
+    pub last_line_len_utf16: u32,
     pub longest_row: u32,
     pub longest_row_chars: u32,
 }
 
+impl TextSummary {
+    pub fn lines_utf16(&self) -> PointUtf16 {
+        PointUtf16 {
+            row: self.lines.row,
+            column: self.last_line_len_utf16,
+        }
+    }
+}
+
 impl<'a> From<&'a str> for TextSummary {
     fn from(text: &'a str) -> Self {
+        let mut len_utf16 = OffsetUtf16(0);
         let mut lines = Point::new(0, 0);
-        let mut lines_utf16 = PointUtf16::new(0, 0);
         let mut first_line_chars = 0;
         let mut last_line_chars = 0;
+        let mut last_line_len_utf16 = 0;
         let mut longest_row = 0;
         let mut longest_row_chars = 0;
         for c in text.chars() {
+            len_utf16.0 += c.len_utf16();
+
             if c == '\n' {
                 lines += Point::new(1, 0);
-                lines_utf16 += PointUtf16::new(1, 0);
+                last_line_len_utf16 = 0;
                 last_line_chars = 0;
             } else {
                 lines.column += c.len_utf8() as u32;
-                lines_utf16.column += c.len_utf16() as u32;
+                last_line_len_utf16 += c.len_utf16() as u32;
                 last_line_chars += 1;
             }
 
@@ -787,11 +876,12 @@ impl<'a> From<&'a str> for TextSummary {
         }
 
         TextSummary {
-            bytes: text.len(),
+            len: text.len(),
+            len_utf16,
             lines,
-            lines_utf16,
             first_line_chars,
             last_line_chars,
+            last_line_len_utf16,
             longest_row,
             longest_row_chars,
         }
@@ -833,13 +923,15 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
 
         if other.lines.row == 0 {
             self.last_line_chars += other.first_line_chars;
+            self.last_line_len_utf16 += other.last_line_len_utf16;
         } else {
             self.last_line_chars = other.last_line_chars;
+            self.last_line_len_utf16 = other.last_line_len_utf16;
         }
 
-        self.bytes += other.bytes;
+        self.len += other.len;
+        self.len_utf16 += other.len_utf16;
         self.lines += other.lines;
-        self.lines_utf16 += other.lines_utf16;
     }
 }
 
@@ -886,13 +978,29 @@ impl TextDimension for TextSummary {
 
 impl<'a> sum_tree::Dimension<'a, ChunkSummary> for usize {
     fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) {
-        *self += summary.text.bytes;
+        *self += summary.text.len;
     }
 }
 
 impl TextDimension for usize {
     fn from_text_summary(summary: &TextSummary) -> Self {
-        summary.bytes
+        summary.len
+    }
+
+    fn add_assign(&mut self, other: &Self) {
+        *self += other;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, ChunkSummary> for OffsetUtf16 {
+    fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) {
+        *self += summary.text.len_utf16;
+    }
+}
+
+impl TextDimension for OffsetUtf16 {
+    fn from_text_summary(summary: &TextSummary) -> Self {
+        summary.len_utf16
     }
 
     fn add_assign(&mut self, other: &Self) {
@@ -918,13 +1026,13 @@ impl TextDimension for Point {
 
 impl<'a> sum_tree::Dimension<'a, ChunkSummary> for PointUtf16 {
     fn add_summary(&mut self, summary: &'a ChunkSummary, _: &()) {
-        *self += summary.text.lines_utf16;
+        *self += summary.text.lines_utf16();
     }
 }
 
 impl TextDimension for PointUtf16 {
     fn from_text_summary(summary: &TextSummary) -> Self {
-        summary.lines_utf16
+        summary.lines_utf16()
     }
 
     fn add_assign(&mut self, other: &Self) {
@@ -1000,6 +1108,19 @@ mod tests {
             rope.clip_point_utf16(PointUtf16::new(0, 3), Bias::Right),
             PointUtf16::new(0, 2)
         );
+
+        assert_eq!(
+            rope.clip_offset_utf16(OffsetUtf16(1), Bias::Left),
+            OffsetUtf16(0)
+        );
+        assert_eq!(
+            rope.clip_offset_utf16(OffsetUtf16(1), Bias::Right),
+            OffsetUtf16(2)
+        );
+        assert_eq!(
+            rope.clip_offset_utf16(OffsetUtf16(3), Bias::Right),
+            OffsetUtf16(2)
+        );
     }
 
     #[gpui::test(iterations = 100)]
@@ -1054,6 +1175,7 @@ mod tests {
                 );
             }
 
+            let mut offset_utf16 = OffsetUtf16(0);
             let mut point = Point::new(0, 0);
             let mut point_utf16 = PointUtf16::new(0, 0);
             for (ix, ch) in expected.char_indices().chain(Some((expected.len(), '\0'))) {
@@ -1076,6 +1198,18 @@ mod tests {
                     "point_utf16_to_offset({:?})",
                     point_utf16
                 );
+                assert_eq!(
+                    actual.offset_to_offset_utf16(ix),
+                    offset_utf16,
+                    "offset_to_offset_utf16({:?})",
+                    ix
+                );
+                assert_eq!(
+                    actual.offset_utf16_to_offset(offset_utf16),
+                    ix,
+                    "offset_utf16_to_offset({:?})",
+                    offset_utf16
+                );
                 if ch == '\n' {
                     point += Point::new(1, 0);
                     point_utf16 += PointUtf16::new(1, 0);
@@ -1083,10 +1217,19 @@ mod tests {
                     point.column += ch.len_utf8() as u32;
                     point_utf16.column += ch.len_utf16() as u32;
                 }
+                offset_utf16.0 += ch.len_utf16();
             }
 
+            let mut offset_utf16 = OffsetUtf16(0);
             let mut point_utf16 = PointUtf16::zero();
             for unit in expected.encode_utf16() {
+                let left_offset = actual.clip_offset_utf16(offset_utf16, Bias::Left);
+                let right_offset = actual.clip_offset_utf16(offset_utf16, Bias::Right);
+                assert!(right_offset >= left_offset);
+                // Ensure translating UTF-16 offsets to UTF-8 offsets doesn't panic.
+                actual.offset_utf16_to_offset(left_offset);
+                actual.offset_utf16_to_offset(right_offset);
+
                 let left_point = actual.clip_point_utf16(point_utf16, Bias::Left);
                 let right_point = actual.clip_point_utf16(point_utf16, Bias::Right);
                 assert!(right_point >= left_point);
@@ -1094,6 +1237,7 @@ mod tests {
                 actual.point_utf16_to_offset(left_point);
                 actual.point_utf16_to_offset(right_point);
 
+                offset_utf16.0 += 1;
                 if unit == b'\n' as u16 {
                     point_utf16 += PointUtf16::new(1, 0);
                 } else {

crates/text/src/tests.rs πŸ”—

@@ -247,11 +247,12 @@ fn test_text_summary_for_range() {
     assert_eq!(
         buffer.text_summary_for_range::<TextSummary, _>(1..3),
         TextSummary {
-            bytes: 2,
+            len: 2,
+            len_utf16: OffsetUtf16(2),
             lines: Point::new(1, 0),
-            lines_utf16: PointUtf16::new(1, 0),
             first_line_chars: 1,
             last_line_chars: 0,
+            last_line_len_utf16: 0,
             longest_row: 0,
             longest_row_chars: 1,
         }
@@ -259,11 +260,12 @@ fn test_text_summary_for_range() {
     assert_eq!(
         buffer.text_summary_for_range::<TextSummary, _>(1..12),
         TextSummary {
-            bytes: 11,
+            len: 11,
+            len_utf16: OffsetUtf16(11),
             lines: Point::new(3, 0),
-            lines_utf16: PointUtf16::new(3, 0),
             first_line_chars: 1,
             last_line_chars: 0,
+            last_line_len_utf16: 0,
             longest_row: 2,
             longest_row_chars: 4,
         }
@@ -271,11 +273,12 @@ fn test_text_summary_for_range() {
     assert_eq!(
         buffer.text_summary_for_range::<TextSummary, _>(0..20),
         TextSummary {
-            bytes: 20,
+            len: 20,
+            len_utf16: OffsetUtf16(20),
             lines: Point::new(4, 1),
-            lines_utf16: PointUtf16::new(4, 1),
             first_line_chars: 2,
             last_line_chars: 1,
+            last_line_len_utf16: 1,
             longest_row: 3,
             longest_row_chars: 6,
         }
@@ -283,11 +286,12 @@ fn test_text_summary_for_range() {
     assert_eq!(
         buffer.text_summary_for_range::<TextSummary, _>(0..22),
         TextSummary {
-            bytes: 22,
+            len: 22,
+            len_utf16: OffsetUtf16(22),
             lines: Point::new(4, 3),
-            lines_utf16: PointUtf16::new(4, 3),
             first_line_chars: 2,
             last_line_chars: 3,
+            last_line_len_utf16: 3,
             longest_row: 3,
             longest_row_chars: 6,
         }
@@ -295,11 +299,12 @@ fn test_text_summary_for_range() {
     assert_eq!(
         buffer.text_summary_for_range::<TextSummary, _>(7..22),
         TextSummary {
-            bytes: 15,
+            len: 15,
+            len_utf16: OffsetUtf16(15),
             lines: Point::new(2, 3),
-            lines_utf16: PointUtf16::new(2, 3),
             first_line_chars: 4,
             last_line_chars: 3,
+            last_line_len_utf16: 3,
             longest_row: 1,
             longest_row_chars: 6,
         }
@@ -520,7 +525,7 @@ fn test_history() {
     let mut now = Instant::now();
     let mut buffer = Buffer::new(0, 0, "123456".into());
 
-    buffer.start_transaction_at(now);
+    let transaction_1 = buffer.start_transaction_at(now).unwrap();
     buffer.edit([(2..4, "cd")]);
     buffer.end_transaction_at(now);
     assert_eq!(buffer.text(), "12cd56");
@@ -559,7 +564,9 @@ fn test_history() {
     assert_eq!(buffer.text(), "12cde6");
 
     // Redo stack gets cleared after performing an edit.
+    buffer.start_transaction_at(now);
     buffer.edit([(0..0, "X")]);
+    buffer.end_transaction_at(now);
     assert_eq!(buffer.text(), "X12cde6");
     buffer.redo();
     assert_eq!(buffer.text(), "X12cde6");
@@ -567,6 +574,16 @@ fn test_history() {
     assert_eq!(buffer.text(), "12cde6");
     buffer.undo();
     assert_eq!(buffer.text(), "123456");
+
+    // Transactions can be grouped manually.
+    buffer.redo();
+    buffer.redo();
+    assert_eq!(buffer.text(), "X12cde6");
+    buffer.group_until_transaction(transaction_1);
+    buffer.undo();
+    assert_eq!(buffer.text(), "123456");
+    buffer.redo();
+    assert_eq!(buffer.text(), "X12cde6");
 }
 
 #[test]

crates/text/src/text.rs πŸ”—

@@ -2,6 +2,7 @@ mod anchor;
 pub mod locator;
 #[cfg(any(test, feature = "test-support"))]
 pub mod network;
+mod offset_utf16;
 pub mod operation_queue;
 mod patch;
 mod point;
@@ -20,6 +21,7 @@ use clock::ReplicaId;
 use collections::{HashMap, HashSet};
 use lazy_static::lazy_static;
 use locator::Locator;
+pub use offset_utf16::*;
 use operation_queue::OperationQueue;
 pub use patch::Patch;
 pub use point::*;
@@ -33,7 +35,7 @@ pub use rope::{Chunks, Rope, TextSummary};
 pub use selection::*;
 use std::{
     borrow::Cow,
-    cmp::{self, Ordering},
+    cmp::{self, Ordering, Reverse},
     future::Future,
     iter::Iterator,
     ops::{self, Deref, Range, Sub},
@@ -90,8 +92,6 @@ pub struct Transaction {
     pub id: TransactionId,
     pub edit_ids: Vec<clock::Local>,
     pub start: clock::Global,
-    pub end: clock::Global,
-    pub ranges: Vec<Range<FullOffset>>,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq)]
@@ -104,79 +104,31 @@ impl HistoryEntry {
     pub fn transaction_id(&self) -> TransactionId {
         self.transaction.id
     }
-
-    fn push_edit(&mut self, edit_operation: &EditOperation) {
-        self.transaction
-            .edit_ids
-            .push(edit_operation.timestamp.local());
-        self.transaction
-            .end
-            .observe(edit_operation.timestamp.local());
-
-        let mut edits = edit_operation
-            .ranges
-            .iter()
-            .zip(edit_operation.new_text.iter())
-            .peekable();
-        let mut new_ranges = Vec::new();
-        let mut delta = 0;
-
-        for mut self_range in self.transaction.ranges.iter().cloned() {
-            self_range.start += delta;
-            self_range.end += delta;
-
-            while let Some((other_range, new_text)) = edits.peek() {
-                let insertion_len = new_text.len();
-                let mut other_range = (*other_range).clone();
-                other_range.start += delta;
-                other_range.end += delta;
-
-                if other_range.start <= self_range.end {
-                    edits.next().unwrap();
-                    delta += insertion_len;
-
-                    if other_range.end < self_range.start {
-                        new_ranges.push(other_range.start..other_range.end + insertion_len);
-                        self_range.start += insertion_len;
-                        self_range.end += insertion_len;
-                    } else {
-                        self_range.start = cmp::min(self_range.start, other_range.start);
-                        self_range.end = cmp::max(self_range.end, other_range.end) + insertion_len;
-                    }
-                } else {
-                    break;
-                }
-            }
-
-            new_ranges.push(self_range);
-        }
-
-        for (other_range, new_text) in edits {
-            let insertion_len = new_text.len();
-            new_ranges.push(other_range.start + delta..other_range.end + delta + insertion_len);
-            delta += insertion_len;
-        }
-
-        self.transaction.ranges = new_ranges;
-    }
 }
 
-#[derive(Clone)]
 struct History {
     // TODO: Turn this into a String or Rope, maybe.
     base_text: Arc<str>,
     operations: HashMap<clock::Local, Operation>,
+    insertion_slices: HashMap<clock::Local, Vec<InsertionSlice>>,
     undo_stack: Vec<HistoryEntry>,
     redo_stack: Vec<HistoryEntry>,
     transaction_depth: usize,
     group_interval: Duration,
 }
 
+#[derive(Clone, Debug)]
+struct InsertionSlice {
+    insertion_id: clock::Local,
+    range: Range<usize>,
+}
+
 impl History {
     pub fn new(base_text: Arc<str>) -> Self {
         Self {
             base_text,
             operations: Default::default(),
+            insertion_slices: Default::default(),
             undo_stack: Vec::new(),
             redo_stack: Vec::new(),
             transaction_depth: 0,
@@ -200,10 +152,8 @@ impl History {
             self.undo_stack.push(HistoryEntry {
                 transaction: Transaction {
                     id,
-                    start: start.clone(),
-                    end: start,
+                    start,
                     edit_ids: Default::default(),
-                    ranges: Default::default(),
                 },
                 first_edit_at: now,
                 last_edit_at: now,
@@ -224,7 +174,7 @@ impl History {
                 .last()
                 .unwrap()
                 .transaction
-                .ranges
+                .edit_ids
                 .is_empty()
             {
                 self.undo_stack.pop();
@@ -241,34 +191,49 @@ impl History {
     }
 
     fn group(&mut self) -> Option<TransactionId> {
-        let mut new_len = self.undo_stack.len();
-        let mut entries = self.undo_stack.iter_mut();
-
+        let mut count = 0;
+        let mut entries = self.undo_stack.iter();
         if let Some(mut entry) = entries.next_back() {
             while let Some(prev_entry) = entries.next_back() {
                 if !prev_entry.suppress_grouping
                     && entry.first_edit_at - prev_entry.last_edit_at <= self.group_interval
-                    && entry.transaction.start == prev_entry.transaction.end
                 {
                     entry = prev_entry;
-                    new_len -= 1;
+                    count += 1;
                 } else {
                     break;
                 }
             }
         }
+        self.group_trailing(count)
+    }
+
+    fn group_until(&mut self, transaction_id: TransactionId) {
+        let mut count = 0;
+        for entry in self.undo_stack.iter().rev() {
+            if entry.transaction_id() == transaction_id {
+                self.group_trailing(count);
+                break;
+            } else if entry.suppress_grouping {
+                break;
+            } else {
+                count += 1;
+            }
+        }
+    }
 
+    fn group_trailing(&mut self, n: usize) -> Option<TransactionId> {
+        let new_len = self.undo_stack.len() - n;
         let (entries_to_keep, entries_to_merge) = self.undo_stack.split_at_mut(new_len);
         if let Some(last_entry) = entries_to_keep.last_mut() {
             for entry in &*entries_to_merge {
                 for edit_id in &entry.transaction.edit_ids {
-                    last_entry.push_edit(self.operations[edit_id].as_edit().unwrap());
+                    last_entry.transaction.edit_ids.push(*edit_id);
                 }
             }
 
             if let Some(entry) = entries_to_merge.last_mut() {
                 last_entry.last_edit_at = entry.last_edit_at;
-                last_entry.transaction.end = entry.transaction.end.clone();
             }
         }
 
@@ -296,9 +261,9 @@ impl History {
 
     fn push_undo(&mut self, op_id: clock::Local) {
         assert_ne!(self.transaction_depth, 0);
-        if let Some(Operation::Edit(edit)) = self.operations.get(&op_id) {
+        if let Some(Operation::Edit(_)) = self.operations.get(&op_id) {
             let last_transaction = self.undo_stack.last_mut().unwrap();
-            last_transaction.push_edit(&edit);
+            last_transaction.transaction.edit_ids.push(op_id);
         }
     }
 
@@ -547,7 +512,6 @@ pub struct EditOperation {
 pub struct UndoOperation {
     pub id: clock::Local,
     pub counts: HashMap<clock::Local, u32>,
-    pub transaction_ranges: Vec<Range<FullOffset>>,
     pub transaction_version: clock::Global,
     pub version: clock::Global,
 }
@@ -677,6 +641,7 @@ impl Buffer {
         };
         let mut new_insertions = Vec::new();
         let mut insertion_offset = 0;
+        let mut insertion_slices = Vec::new();
 
         let mut edits = edits
             .map(|(range, new_text)| (range.to_offset(&*self), new_text))
@@ -735,10 +700,6 @@ impl Buffer {
             if !new_text.is_empty() {
                 let new_start = new_fragments.summary().text.visible;
 
-                edits_patch.push(Edit {
-                    old: fragment_start..fragment_start,
-                    new: new_start..new_start + new_text.len(),
-                });
                 let fragment = Fragment {
                     id: Locator::between(
                         &new_fragments.summary().max_id,
@@ -753,6 +714,11 @@ impl Buffer {
                     max_undos: Default::default(),
                     visible: true,
                 };
+                edits_patch.push(Edit {
+                    old: fragment_start..fragment_start,
+                    new: new_start..new_start + new_text.len(),
+                });
+                insertion_slices.push(fragment.insertion_slice());
                 new_insertions.push(InsertionFragment::insert_new(&fragment));
                 new_ropes.push_str(new_text.as_ref());
                 new_fragments.push(fragment, &None);
@@ -781,6 +747,7 @@ impl Buffer {
                             old: fragment_start..intersection_end,
                             new: new_start..new_start,
                         });
+                        insertion_slices.push(intersection.insertion_slice());
                     }
                     new_insertions.push(InsertionFragment::insert_new(&intersection));
                     new_ropes.push_fragment(&intersection, fragment.visible);
@@ -823,6 +790,9 @@ impl Buffer {
         self.snapshot.visible_text = visible_text;
         self.snapshot.deleted_text = deleted_text;
         self.subscriptions.publish_mut(&edits_patch);
+        self.history
+            .insertion_slices
+            .insert(timestamp.local(), insertion_slices);
         edit_op
     }
 
@@ -892,6 +862,7 @@ impl Buffer {
 
         let edits = ranges.into_iter().zip(new_text.into_iter());
         let mut edits_patch = Patch::default();
+        let mut insertion_slices = Vec::new();
         let cx = Some(version.clone());
         let mut new_insertions = Vec::new();
         let mut insertion_offset = 0;
@@ -982,10 +953,6 @@ impl Buffer {
                     old_start += fragment_start.0 - old_fragments.start().0.full_offset().0;
                 }
                 let new_start = new_fragments.summary().text.visible;
-                edits_patch.push(Edit {
-                    old: old_start..old_start,
-                    new: new_start..new_start + new_text.len(),
-                });
                 let fragment = Fragment {
                     id: Locator::between(
                         &new_fragments.summary().max_id,
@@ -1000,6 +967,11 @@ impl Buffer {
                     max_undos: Default::default(),
                     visible: true,
                 };
+                edits_patch.push(Edit {
+                    old: old_start..old_start,
+                    new: new_start..new_start + new_text.len(),
+                });
+                insertion_slices.push(fragment.insertion_slice());
                 new_insertions.push(InsertionFragment::insert_new(&fragment));
                 new_ropes.push_str(new_text);
                 new_fragments.push(fragment, &None);
@@ -1021,6 +993,7 @@ impl Buffer {
                         Locator::between(&new_fragments.summary().max_id, &intersection.id);
                     intersection.deletions.insert(timestamp.local());
                     intersection.visible = false;
+                    insertion_slices.push(intersection.insertion_slice());
                 }
                 if intersection.len > 0 {
                     if fragment.visible && !intersection.visible {
@@ -1068,90 +1041,104 @@ impl Buffer {
         self.snapshot.visible_text = visible_text;
         self.snapshot.deleted_text = deleted_text;
         self.snapshot.insertions.edit(new_insertions, &());
+        self.history
+            .insertion_slices
+            .insert(timestamp.local(), insertion_slices);
         self.subscriptions.publish_mut(&edits_patch)
     }
 
-    fn apply_undo(&mut self, undo: &UndoOperation) -> Result<()> {
-        let mut edits = Patch::default();
-        self.snapshot.undo_map.insert(undo);
+    fn fragment_ids_for_edits<'a>(
+        &'a self,
+        edit_ids: impl Iterator<Item = &'a clock::Local>,
+    ) -> Vec<&'a Locator> {
+        // Get all of the insertion slices changed by the given edits.
+        let mut insertion_slices = Vec::new();
+        for edit_id in edit_ids {
+            if let Some(slices) = self.history.insertion_slices.get(edit_id) {
+                insertion_slices.extend_from_slice(slices)
+            }
+        }
+        insertion_slices
+            .sort_unstable_by_key(|s| (s.insertion_id, s.range.start, Reverse(s.range.end)));
 
-        let mut cx = undo.transaction_version.clone();
-        for edit_id in undo.counts.keys().copied() {
-            cx.observe(edit_id);
+        // Get all of the fragments corresponding to these insertion slices.
+        let mut fragment_ids = Vec::new();
+        let mut insertions_cursor = self.insertions.cursor::<InsertionFragmentKey>();
+        for insertion_slice in &insertion_slices {
+            if insertion_slice.insertion_id != insertions_cursor.start().timestamp
+                || insertion_slice.range.start > insertions_cursor.start().split_offset
+            {
+                insertions_cursor.seek_forward(
+                    &InsertionFragmentKey {
+                        timestamp: insertion_slice.insertion_id,
+                        split_offset: insertion_slice.range.start,
+                    },
+                    Bias::Left,
+                    &(),
+                );
+            }
+            while let Some(item) = insertions_cursor.item() {
+                if item.timestamp != insertion_slice.insertion_id
+                    || item.split_offset >= insertion_slice.range.end
+                {
+                    break;
+                }
+                fragment_ids.push(&item.fragment_id);
+                insertions_cursor.next(&());
+            }
         }
-        let cx = Some(cx);
+        fragment_ids.sort_unstable();
+        fragment_ids
+    }
 
-        let mut old_fragments = self.fragments.cursor::<(VersionedFullOffset, usize)>();
-        let mut new_fragments = old_fragments.slice(
-            &VersionedFullOffset::Offset(undo.transaction_ranges[0].start),
-            Bias::Right,
-            &cx,
-        );
+    fn apply_undo(&mut self, undo: &UndoOperation) -> Result<()> {
+        self.snapshot.undo_map.insert(undo);
+
+        let mut edits = Patch::default();
+        let mut old_fragments = self.fragments.cursor::<(Option<&Locator>, usize)>();
+        let mut new_fragments = SumTree::new();
         let mut new_ropes =
             RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0));
-        new_ropes.push_tree(new_fragments.summary().text);
 
-        for range in &undo.transaction_ranges {
-            let mut end_offset = old_fragments.end(&cx).0.full_offset();
-
-            if end_offset < range.start {
-                let preceding_fragments = old_fragments.slice(
-                    &VersionedFullOffset::Offset(range.start),
-                    Bias::Right,
-                    &cx,
-                );
-                new_ropes.push_tree(preceding_fragments.summary().text);
-                new_fragments.push_tree(preceding_fragments, &None);
-            }
+        for fragment_id in self.fragment_ids_for_edits(undo.counts.keys()) {
+            let preceding_fragments = old_fragments.slice(&Some(fragment_id), Bias::Left, &None);
+            new_ropes.push_tree(preceding_fragments.summary().text);
+            new_fragments.push_tree(preceding_fragments, &None);
 
-            while end_offset <= range.end {
-                if let Some(fragment) = old_fragments.item() {
-                    let mut fragment = fragment.clone();
-                    let fragment_was_visible = fragment.visible;
-
-                    if fragment.was_visible(&undo.transaction_version, &self.undo_map)
-                        || undo
-                            .counts
-                            .contains_key(&fragment.insertion_timestamp.local())
-                    {
-                        fragment.visible = fragment.is_visible(&self.undo_map);
-                        fragment.max_undos.observe(undo.id);
-                    }
+            if let Some(fragment) = old_fragments.item() {
+                let mut fragment = fragment.clone();
+                let fragment_was_visible = fragment.visible;
 
-                    let old_start = old_fragments.start().1;
-                    let new_start = new_fragments.summary().text.visible;
-                    if fragment_was_visible && !fragment.visible {
-                        edits.push(Edit {
-                            old: old_start..old_start + fragment.len,
-                            new: new_start..new_start,
-                        });
-                    } else if !fragment_was_visible && fragment.visible {
-                        edits.push(Edit {
-                            old: old_start..old_start,
-                            new: new_start..new_start + fragment.len,
-                        });
-                    }
-                    new_ropes.push_fragment(&fragment, fragment_was_visible);
-                    new_fragments.push(fragment, &None);
+                if fragment.was_visible(&undo.transaction_version, &self.undo_map)
+                    || undo
+                        .counts
+                        .contains_key(&fragment.insertion_timestamp.local())
+                {
+                    fragment.visible = fragment.is_visible(&self.undo_map);
+                    fragment.max_undos.observe(undo.id);
+                }
 
-                    old_fragments.next(&cx);
-                    if end_offset == old_fragments.end(&cx).0.full_offset() {
-                        let unseen_fragments = old_fragments.slice(
-                            &VersionedFullOffset::Offset(end_offset),
-                            Bias::Right,
-                            &cx,
-                        );
-                        new_ropes.push_tree(unseen_fragments.summary().text);
-                        new_fragments.push_tree(unseen_fragments, &None);
-                    }
-                    end_offset = old_fragments.end(&cx).0.full_offset();
-                } else {
-                    break;
+                let old_start = old_fragments.start().1;
+                let new_start = new_fragments.summary().text.visible;
+                if fragment_was_visible && !fragment.visible {
+                    edits.push(Edit {
+                        old: old_start..old_start + fragment.len,
+                        new: new_start..new_start,
+                    });
+                } else if !fragment_was_visible && fragment.visible {
+                    edits.push(Edit {
+                        old: old_start..old_start,
+                        new: new_start..new_start + fragment.len,
+                    });
                 }
+                new_ropes.push_fragment(&fragment, fragment_was_visible);
+                new_fragments.push(fragment, &None);
+
+                old_fragments.next(&None);
             }
         }
 
-        let suffix = old_fragments.suffix(&cx);
+        let suffix = old_fragments.suffix(&None);
         new_ropes.push_tree(suffix.summary().text);
         new_fragments.push_tree(suffix, &None);
 
@@ -1225,6 +1212,10 @@ impl Buffer {
         self.history.finalize_last_transaction()
     }
 
+    pub fn group_until_transaction(&mut self, transaction_id: TransactionId) {
+        self.history.group_until(transaction_id);
+    }
+
     pub fn base_text(&self) -> &Arc<str> {
         &self.history.base_text
     }
@@ -1302,7 +1293,6 @@ impl Buffer {
             id: self.local_clock.tick(),
             version: self.version(),
             counts,
-            transaction_ranges: transaction.ranges,
             transaction_version: transaction.start.clone(),
         };
         self.apply_undo(&undo)?;
@@ -1320,6 +1310,55 @@ impl Buffer {
         self.history.finalize_last_transaction();
     }
 
+    pub fn edited_ranges_for_transaction<'a, D>(
+        &'a self,
+        transaction: &'a Transaction,
+    ) -> impl 'a + Iterator<Item = Range<D>>
+    where
+        D: TextDimension,
+    {
+        // get fragment ranges
+        let mut cursor = self.fragments.cursor::<(Option<&Locator>, usize)>();
+        let offset_ranges = self
+            .fragment_ids_for_edits(transaction.edit_ids.iter())
+            .into_iter()
+            .filter_map(move |fragment_id| {
+                cursor.seek_forward(&Some(fragment_id), Bias::Left, &None);
+                let fragment = cursor.item()?;
+                let start_offset = cursor.start().1;
+                let end_offset = start_offset + if fragment.visible { fragment.len } else { 0 };
+                Some(start_offset..end_offset)
+            });
+
+        // combine adjacent ranges
+        let mut prev_range: Option<Range<usize>> = None;
+        let disjoint_ranges = offset_ranges
+            .map(Some)
+            .chain([None])
+            .filter_map(move |range| {
+                if let Some((range, prev_range)) = range.as_ref().zip(prev_range.as_mut()) {
+                    if prev_range.end == range.start {
+                        prev_range.end = range.end;
+                        return None;
+                    }
+                }
+                let result = prev_range.clone();
+                prev_range = range;
+                result
+            });
+
+        // convert to the desired text dimension.
+        let mut position = D::default();
+        let mut rope_cursor = self.visible_text.cursor(0);
+        disjoint_ranges.map(move |range| {
+            position.add_assign(&rope_cursor.summary(range.start));
+            let start = position.clone();
+            position.add_assign(&rope_cursor.summary(range.end));
+            let end = position.clone();
+            start..end
+        })
+    }
+
     pub fn subscribe(&mut self) -> Subscription {
         self.subscriptions.subscribe()
     }
@@ -1621,6 +1660,14 @@ impl BufferSnapshot {
         self.visible_text.point_utf16_to_point(point)
     }
 
+    pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize {
+        self.visible_text.offset_utf16_to_offset(offset)
+    }
+
+    pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 {
+        self.visible_text.offset_to_offset_utf16(offset)
+    }
+
     pub fn offset_to_point(&self, offset: usize) -> Point {
         self.visible_text.offset_to_point(offset)
     }
@@ -1854,6 +1901,10 @@ impl BufferSnapshot {
         self.visible_text.clip_point(point, bias)
     }
 
+    pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 {
+        self.visible_text.clip_offset_utf16(offset, bias)
+    }
+
     pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
         self.visible_text.clip_point_utf16(point, bias)
     }
@@ -1868,42 +1919,6 @@ impl BufferSnapshot {
         self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX)
     }
 
-    pub fn edited_ranges_for_transaction<'a, D>(
-        &'a self,
-        transaction: &'a Transaction,
-    ) -> impl 'a + Iterator<Item = Range<D>>
-    where
-        D: TextDimension,
-    {
-        let mut cursor = self.fragments.cursor::<(VersionedFullOffset, usize)>();
-        let mut rope_cursor = self.visible_text.cursor(0);
-        let cx = Some(transaction.end.clone());
-        let mut position = D::default();
-        transaction.ranges.iter().map(move |range| {
-            cursor.seek_forward(&VersionedFullOffset::Offset(range.start), Bias::Right, &cx);
-            let mut start_offset = cursor.start().1;
-            if cursor
-                .item()
-                .map_or(false, |fragment| fragment.is_visible(&self.undo_map))
-            {
-                start_offset += range.start - cursor.start().0.full_offset()
-            }
-            position.add_assign(&rope_cursor.summary(start_offset));
-            let start = position.clone();
-
-            cursor.seek_forward(&VersionedFullOffset::Offset(range.end), Bias::Left, &cx);
-            let mut end_offset = cursor.start().1;
-            if cursor
-                .item()
-                .map_or(false, |fragment| fragment.is_visible(&self.undo_map))
-            {
-                end_offset += range.end - cursor.start().0.full_offset();
-            }
-            position.add_assign(&rope_cursor.summary(end_offset));
-            start..position.clone()
-        })
-    }
-
     pub fn edits_since_in_range<'a, D>(
         &'a self,
         since: &'a clock::Global,
@@ -2090,6 +2105,13 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo
 }
 
 impl Fragment {
+    fn insertion_slice(&self) -> InsertionSlice {
+        InsertionSlice {
+            insertion_id: self.insertion_timestamp.local(),
+            range: self.insertion_offset..self.insertion_offset + self.len,
+        }
+    }
+
     fn is_visible(&self, undos: &UndoMap) -> bool {
         !undos.is_undone(self.insertion_timestamp.local())
             && self.deletions.iter().all(|d| undos.is_undone(*d))
@@ -2423,6 +2445,12 @@ impl ToOffset for usize {
     }
 }
 
+impl ToOffset for OffsetUtf16 {
+    fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
+        snapshot.offset_utf16_to_offset(*self)
+    }
+}
+
 impl ToOffset for Anchor {
     fn to_offset<'a>(&self, snapshot: &BufferSnapshot) -> usize {
         snapshot.summary_for_anchor(self)
@@ -2491,6 +2519,28 @@ impl ToPointUtf16 for Point {
     }
 }
 
+pub trait ToOffsetUtf16 {
+    fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16;
+}
+
+impl ToOffsetUtf16 for Anchor {
+    fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 {
+        snapshot.summary_for_anchor(self)
+    }
+}
+
+impl ToOffsetUtf16 for usize {
+    fn to_offset_utf16<'a>(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 {
+        snapshot.offset_to_offset_utf16(*self)
+    }
+}
+
+impl ToOffsetUtf16 for OffsetUtf16 {
+    fn to_offset_utf16<'a>(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 {
+        *self
+    }
+}
+
 pub trait Clip {
     fn clip(&self, bias: Bias, snapshot: &BufferSnapshot) -> Self;
 }

crates/theme/src/theme.rs πŸ”—

@@ -452,6 +452,7 @@ pub struct Editor {
     pub unnecessary_code_fade: f32,
     pub hover_popover: HoverPopover,
     pub link_definition: HighlightStyle,
+    pub composition_mark: HighlightStyle,
     pub jump_icon: Interactive<IconButton>,
 }
 

crates/vim/src/editor_events.rs πŸ”—

@@ -22,9 +22,20 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont
         vim.active_editor = Some(editor.downgrade());
         vim.selection_subscription = Some(cx.subscribe(editor, |editor, event, cx| {
             if editor.read(cx).leader_replica_id().is_none() {
-                if let editor::Event::SelectionsChanged { local: true } = event {
-                    let newest_empty = editor.read(cx).selections.newest::<usize>(cx).is_empty();
-                    editor_local_selections_changed(newest_empty, cx);
+                match event {
+                    editor::Event::SelectionsChanged { local: true } => {
+                        let newest_empty =
+                            editor.read(cx).selections.newest::<usize>(cx).is_empty();
+                        editor_local_selections_changed(newest_empty, cx);
+                    }
+                    editor::Event::IgnoredInput => {
+                        Vim::update(cx, |vim, cx| {
+                            if vim.active_operator().is_some() {
+                                vim.clear_operator(cx);
+                            }
+                        });
+                    }
+                    _ => (),
                 }
             }
         }));

crates/vim/src/vim.rs πŸ”—

@@ -11,7 +11,7 @@ mod visual;
 
 use collections::HashMap;
 use command_palette::CommandPaletteFilter;
-use editor::{Bias, Cancel, CursorShape, Editor, Input};
+use editor::{Bias, Cancel, CursorShape, Editor};
 use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
 use serde::Deserialize;
 
@@ -45,16 +45,6 @@ pub fn init(cx: &mut MutableAppContext) {
     );
 
     // Editor Actions
-    cx.add_action(|_: &mut Editor, _: &Input, cx| {
-        // If we have an unbound input with an active operator, cancel that operator. Otherwise forward
-        // the input to the editor
-        if Vim::read(cx).active_operator().is_some() {
-            // Defer without updating editor
-            MutableAppContext::defer(cx, |cx| Vim::update(cx, |vim, cx| vim.clear_operator(cx)))
-        } else {
-            cx.propagate_action()
-        }
-    });
     cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
         // If we are in a non normal mode or have an active operator, swap to normal mode
         // Otherwise forward cancel on to the editor

crates/workspace/src/workspace.rs πŸ”—

@@ -43,6 +43,7 @@ use std::{
     fmt,
     future::Future,
     mem,
+    ops::Range,
     path::{Path, PathBuf},
     rc::Rc,
     sync::{
@@ -2546,6 +2547,18 @@ impl Element for AvatarRibbon {
         false
     }
 
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &gpui::MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
     fn debug(
         &self,
         bounds: gpui::geometry::rect::RectF,

crates/zed/src/menus.rs πŸ”—

@@ -131,6 +131,10 @@ pub fn menus() -> Vec<Menu<'static>> {
                     name: "Toggle Line Comment",
                     action: Box::new(editor::ToggleComments),
                 },
+                MenuItem::Action {
+                    name: "Emoji & Symbols",
+                    action: Box::new(editor::ShowCharacterPalette),
+                },
             ],
         },
         Menu {

crates/zed/src/zed.rs πŸ”—

@@ -920,11 +920,7 @@ mod tests {
             item.downcast::<Editor>().unwrap()
         });
 
-        cx.update(|cx| {
-            editor.update(cx, |editor, cx| {
-                editor.handle_input(&editor::Input("x".into()), cx)
-            })
-        });
+        cx.update(|cx| editor.update(cx, |editor, cx| editor.handle_input("x", cx)));
         app_state
             .fs
             .as_fake()
@@ -971,7 +967,7 @@ mod tests {
                 editor.language_at(0, cx).unwrap(),
                 &languages::PLAIN_TEXT
             ));
-            editor.handle_input(&editor::Input("hi".into()), cx);
+            editor.handle_input("hi", cx);
             assert!(editor.is_dirty(cx));
         });
 
@@ -997,7 +993,7 @@ mod tests {
 
         // Edit the file and save it again. This time, there is no filename prompt.
         editor.update(cx, |editor, cx| {
-            editor.handle_input(&editor::Input(" there".into()), cx);
+            editor.handle_input(" there", cx);
             assert_eq!(editor.is_dirty(cx.as_ref()), true);
         });
         let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
@@ -1057,7 +1053,7 @@ mod tests {
                 editor.language_at(0, cx).unwrap(),
                 &languages::PLAIN_TEXT
             ));
-            editor.handle_input(&editor::Input("hi".into()), cx);
+            editor.handle_input("hi", cx);
             assert!(editor.is_dirty(cx.as_ref()));
         });
 

styles/package-lock.json πŸ”—

@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "styles",
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {

styles/src/styleTree/editor.ts πŸ”—

@@ -2,6 +2,7 @@ import Theme from "../themes/common/theme";
 import {
   backgroundColor,
   border,
+  borderColor,
   iconColor,
   player,
   popoverShadow,
@@ -138,8 +139,8 @@ export default function editor(theme: Theme) {
     invalidHintDiagnostic: diagnostic(theme, "muted"),
     invalidInformationDiagnostic: diagnostic(theme, "muted"),
     invalidWarningDiagnostic: diagnostic(theme, "muted"),
-    hover_popover: hoverPopover(theme),
-    link_definition: {
+    hoverPopover: hoverPopover(theme),
+    linkDefinition: {
       color: theme.syntax.linkUri.color,
       underline: theme.syntax.linkUri.underline,
     },
@@ -159,6 +160,12 @@ export default function editor(theme: Theme) {
         background: backgroundColor(theme, "on500", "base"),
       },
     },
+    compositionMark: {
+      underline: {
+        thickness: 1.0,
+        color: borderColor(theme, "active")
+      },
+    },
     syntax,
   };
 }