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