Skip to main content

editor/editor/
input.rs

1//! The platform's text input handler.
2
3use std::ops::Range;
4
5use gpui::{Context, EntityInputHandler, UTF16Selection, Window};
6use markdown::{Cursor, Selection};
7use ui::input::{composition_selection, offset_to_utf16, range_from_utf16, range_to_utf16};
8
9use crate::editor::Editor;
10
11/// Typed text and IME arrive here. Offsets are within the caret's block, which
12/// is the unit the platform is told about — a block is a paragraph's worth of
13/// text, so a candidate window never has to be anchored across one.
14impl EntityInputHandler for Editor {
15    fn text_for_range(
16        &mut self,
17        range: Range<usize>,
18        adjusted: &mut Option<Range<usize>>,
19        _: &mut Window,
20        _: &mut Context<Self>,
21    ) -> Option<String> {
22        let text = self.caret_text()?;
23        let range = range_from_utf16(&text.text, range);
24        *adjusted = Some(range_to_utf16(&text.text, range.clone()));
25        Some(text.text.get(range)?.to_string())
26    }
27
28    fn selected_text_range(
29        &mut self,
30        _: bool,
31        _: &mut Window,
32        _: &mut Context<Self>,
33    ) -> Option<UTF16Selection> {
34        // The platform is told about one text at a time, so a selection that
35        // leaves the caret's own is reported collapsed — there are no
36        // coordinates here in which to express it.
37        let (start, end) = self.selection.ordered();
38        let spans_one = start.block == end.block && start.part == end.part;
39        let head = self.selection.head;
40        let range = if spans_one {
41            start.offset..end.offset
42        } else {
43            head.offset..head.offset
44        };
45        Some(UTF16Selection {
46            reversed: spans_one && self.selection.head == start,
47            range: range_to_utf16(&self.caret_text()?.text, range),
48        })
49    }
50
51    fn marked_text_range(&self, _: &mut Window, _: &mut Context<Self>) -> Option<Range<usize>> {
52        Some(range_to_utf16(
53            &self.caret_text()?.text,
54            self.marked.clone()?,
55        ))
56    }
57
58    fn unmark_text(&mut self, _: &mut Window, _: &mut Context<Self>) {
59        self.marked = None;
60    }
61
62    fn replace_text_in_range(
63        &mut self,
64        range: Option<Range<usize>>,
65        text: &str,
66        _: &mut Window,
67        cx: &mut Context<Self>,
68    ) {
69        // A whole URL arriving in one insert is a paste whatever delivered it,
70        // and on the web it is the only shape one arrives in: the browser's
71        // clipboard cannot be read synchronously, so gpui hands the DOM paste
72        // event to this handler rather than to the `Paste` action. Typing
73        // cannot reach here — a URL typed by hand arrives a character at a
74        // time, and none of those characters is a URL.
75        if range.is_none() && self.marked.is_none() && markdown::is_url(text.trim()) {
76            return self.paste_url(text.trim().to_string(), cx);
77        }
78        // The platform's range is within the caret's own text, so it becomes a
79        // selection there and the insert path does the rest.
80        if let Some(range) = range
81            .and_then(|range| {
82                self.caret_text()
83                    .map(|text| range_from_utf16(&text.text, range))
84            })
85            .or_else(|| self.marked.clone())
86        {
87            let at = self.cursor();
88            self.selection = Selection::new(
89                Cursor {
90                    offset: range.start,
91                    ..at
92                },
93                Cursor {
94                    offset: range.end,
95                    ..at
96                },
97            );
98        }
99        self.marked = None;
100        self.insert(text, cx);
101    }
102
103    fn replace_and_mark_text_in_range(
104        &mut self,
105        range: Option<Range<usize>>,
106        text: &str,
107        marked: Option<Range<usize>>,
108        _: &mut Window,
109        cx: &mut Context<Self>,
110    ) {
111        if let Some(range) = range
112            .and_then(|range| {
113                self.caret_text()
114                    .map(|text| range_from_utf16(&text.text, range))
115            })
116            .or_else(|| self.marked.clone())
117        {
118            let at = self.cursor();
119            self.doc.edit_at(at, |body| body.remove(range.clone()));
120            self.place(Cursor {
121                offset: range.start,
122                ..at
123            });
124        }
125        // Composition runs outside the shortcut path: a half-typed candidate is
126        // not a markdown prefix, and turning it into one mid-composition would
127        // pull the text out from under the IME.
128        let at = self.cursor();
129        let start = at.offset;
130        self.doc.edit_at(at, |body| body.insert(start, text));
131        self.marked = (!text.is_empty()).then_some(start..start + text.len());
132        let selected = composition_selection(text, start, marked);
133        self.selection = Selection::new(
134            Cursor {
135                offset: selected.start,
136                ..at
137            },
138            Cursor {
139                offset: selected.end,
140                ..at
141            },
142        );
143        self.reveal = true;
144        self.caret_moved();
145        cx.notify();
146    }
147
148    /// Where a range paints, so a candidate window opens under the text it is
149    /// composing rather than at the window's origin.
150    fn bounds_for_range(
151        &mut self,
152        range: Range<usize>,
153        _: gpui::Bounds<gpui::Pixels>,
154        _: &mut Window,
155        _: &mut Context<Self>,
156    ) -> Option<gpui::Bounds<gpui::Pixels>> {
157        let range = range_from_utf16(&self.caret_text()?.text, range);
158        let at = self.cursor();
159        let start = Cursor {
160            offset: range.start,
161            ..at
162        };
163        let (origin, line_height) = self.layouts.position(start)?;
164        let end = self
165            .layouts
166            .position(Cursor {
167                offset: range.end,
168                ..at
169            })
170            .map(|(point, _)| point)
171            .filter(|point| point.y == origin.y);
172        let width = end.map_or(gpui::px(0.0), |point| point.x - origin.x);
173        Some(gpui::Bounds::new(origin, gpui::size(width, line_height)))
174    }
175
176    fn character_index_for_point(
177        &mut self,
178        point: gpui::Point<gpui::Pixels>,
179        _: &mut Window,
180        _: &mut Context<Self>,
181    ) -> Option<usize> {
182        let hit = self.layouts.hit(point)?;
183        let at = self.cursor();
184        (hit.block == at.block && hit.part == at.part)
185            .then_some(offset_to_utf16(&self.caret_text()?.text, hit.offset))
186    }
187}