Position IME input according to where the selection is rendered

Antonio Scandurra created

Change summary

crates/editor/src/display_map.rs                |  8 ++
crates/editor/src/element.rs                    | 39 ++++++++++++++
crates/gpui/examples/text.rs                    | 15 +++++
crates/gpui/src/app.rs                          |  8 +++
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 | 18 +++++-
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                     |  7 +-
crates/gpui/src/platform/mac/window.rs          | 29 ++++++++++
crates/gpui/src/presenter.rs                    | 46 +++++++++++++++++
crates/terminal/src/terminal_element.rs         | 12 ++++
crates/workspace/src/workspace.rs               | 13 +++++
31 files changed, 563 insertions(+), 27 deletions(-)

Detailed changes

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};
@@ -549,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/element.rs 🔗

@@ -30,7 +30,7 @@ use gpui::{
     WeakViewHandle,
 };
 use json::json;
-use language::{Bias, DiagnosticSeverity, Selection};
+use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
 use project::ProjectPath;
 use settings::Settings;
 use smallvec::SmallVec;
@@ -1517,6 +1517,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,

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,6 +3,7 @@ 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,
@@ -445,6 +446,13 @@ impl InputHandler for WindowInputHandler {
             );
         });
     }
+
+    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"))]

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, MouseEvent, MouseRegion, NavigationDirection,
-    PaintContext, SizeConstraint,
+    geometry::vector::Vector2F, presenter::MeasurementContext, CursorRegion, DebugContext, Element,
+    ElementBox, Event, EventContext, LayoutContext, MouseButton, MouseEvent, MouseRegion,
+    NavigationDirection, PaintContext, SizeConstraint,
 };
 use pathfinder_geometry::rect::RectF;
 use serde_json::json;
-use std::{any::TypeId, rc::Rc};
+use std::{any::TypeId, ops::Range, rc::Rc};
 
 pub struct EventHandler {
     child: ElementBox,
@@ -158,6 +158,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,4 +1,4 @@
-use std::{any::TypeId, rc::Rc};
+use std::{any::TypeId, ops::Range, rc::Rc};
 
 use super::Padding;
 use crate::{
@@ -8,8 +8,8 @@ use crate::{
     },
     platform::CursorStyle,
     scene::CursorRegion,
-    DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion, MouseState,
-    PaintContext, RenderContext, SizeConstraint, View,
+    DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
+    MouseState, PaintContext, RenderContext, SizeConstraint, View, presenter::MeasurementContext,
 };
 use serde_json::json;
 
@@ -192,6 +192,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, 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 🔗

@@ -91,17 +91,18 @@ pub trait Dispatcher: Send + Sync {
 
 pub trait InputHandler {
     fn selected_text_range(&self) -> Option<Range<usize>>;
-    fn set_selected_text_range(&mut self, range: Range<usize>);
-    fn text_for_range(&self, range: Range<usize>) -> Option<String>;
+    fn set_selected_text_range(&mut self, range_utf16: 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: Option<Range<usize>>,
+        range_utf16: Option<Range<usize>>,
         new_text: &str,
         new_selected_range: Option<Range<usize>>,
     );
     fn marked_text_range(&self) -> Option<Range<usize>>;
     fn unmark_text(&mut self);
+    fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF>;
 }
 
 pub trait Window: WindowContext {

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

@@ -1003,8 +1003,33 @@ extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
         .map_or(NSRange::invalid(), |range| range.into())
 }
 
-extern "C" fn first_rect_for_character_range(_: &Object, _: Sel, _: NSRange, _: id) -> NSRect {
-    NSRect::new(NSPoint::new(0., 0.), NSSize::new(20., 20.))
+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) {

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();
@@ -777,6 +788,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,
@@ -936,6 +968,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/terminal/src/terminal_element.rs 🔗

@@ -395,6 +395,18 @@ impl Element for TerminalEl {
         }
     }
 
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &gpui::MeasurementContext,
+    ) -> Option<RectF> {
+        todo!()
+    }
+
     fn debug(
         &self,
         _bounds: gpui::geometry::rect::RectF,

crates/workspace/src/workspace.rs 🔗

@@ -43,6 +43,7 @@ use std::{
     fmt,
     future::Future,
     mem,
+    ops::Range,
     path::{Path, PathBuf},
     rc::Rc,
     sync::{
@@ -2538,6 +2539,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,