Skip to main content

gpui_base/input/base/
inline_tokens.rs

1//! Document annotations for atomic inline objects. Coordinates are always source bytes.
2use std::ops::Range;
3
4use gpui::{Context, EntityInputHandler as _, SharedString, Window};
5use unicode_segmentation::UnicodeSegmentation as _;
6
7use super::{InputBaseState, InputModeKind, undo_manager::EditIntent};
8
9/// An application-defined reference rendered as one inline editing unit. The
10/// ID names the referenced resource; the same ID may occur more than once.
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct InlineToken {
13    id: SharedString,
14    text: SharedString,
15    label: SharedString,
16}
17
18impl InlineToken {
19    pub fn new(id: impl Into<SharedString>, text: impl Into<SharedString>) -> Self {
20        let text = text.into();
21        Self {
22            id: id.into(),
23            label: text.clone(),
24            text,
25        }
26    }
27    pub fn with_label(mut self, label: impl Into<SharedString>) -> Self {
28        self.label = label.into();
29        self
30    }
31    pub fn id(&self) -> &SharedString {
32        &self.id
33    }
34    pub fn text(&self) -> &SharedString {
35        &self.text
36    }
37    pub fn label(&self) -> &SharedString {
38        &self.label
39    }
40
41    fn validate(&self) -> Result<(), InlineTokenError> {
42        if self.id.trim().is_empty()
43            || [&self.text, &self.label].iter().any(|s| {
44                s.is_empty()
45                    || s.chars()
46                        .any(|c| c.is_control() || matches!(c, '\u{2028}' | '\u{2029}'))
47            })
48        {
49            return Err(InlineTokenError::InvalidToken);
50        }
51        Ok(())
52    }
53}
54
55/// A token and its current half-open UTF-8 byte range.
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct InlineTokenSpan {
58    range: Range<usize>,
59    token: InlineToken,
60}
61impl InlineTokenSpan {
62    pub fn range(&self) -> Range<usize> {
63        self.range.clone()
64    }
65    pub fn token(&self) -> &InlineToken {
66        &self.token
67    }
68    fn shifted(&self, delta: isize) -> Self {
69        Self {
70            range: self
71                .range
72                .start
73                .checked_add_signed(delta)
74                .expect("valid token start")
75                ..self
76                    .range
77                    .end
78                    .checked_add_signed(delta)
79                    .expect("valid token end"),
80            token: self.token.clone(),
81        }
82    }
83}
84
85/// An owned, coherent text-and-token snapshot: what `content()` returns and
86/// what `set_value` accepts. Plain text converts into content without tokens,
87/// and every token is validated against the text as it is attached, so a
88/// content value is always consistent.
89#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct InputContent {
91    text: SharedString,
92    tokens: Vec<InlineTokenSpan>,
93}
94impl InputContent {
95    pub fn new(text: impl Into<SharedString>) -> Self {
96        Self {
97            text: text.into(),
98            tokens: vec![],
99        }
100    }
101    /// Attach a token to a half-open UTF-8 byte range of the text. The range
102    /// must be nonempty, sit on grapheme boundaries, contain exactly the
103    /// token's text and not overlap another token.
104    pub fn with_token(
105        mut self,
106        range: Range<usize>,
107        token: InlineToken,
108    ) -> Result<Self, InlineTokenError> {
109        token.validate()?;
110        validate_range(&self.text, &range)?;
111        if range.is_empty() {
112            return Err(InlineTokenError::InvalidRange);
113        }
114        if &self.text[range.clone()] != token.text.as_ref() {
115            return Err(InlineTokenError::TextMismatch);
116        }
117        let ix = self
118            .tokens
119            .partition_point(|span| span.range.end <= range.start);
120        if self
121            .tokens
122            .get(ix)
123            .is_some_and(|span| span.range.start < range.end)
124        {
125            return Err(InlineTokenError::OverlappingTokens);
126        }
127        self.tokens.insert(ix, InlineTokenSpan { range, token });
128        Ok(self)
129    }
130    pub fn text(&self) -> &SharedString {
131        &self.text
132    }
133    pub fn tokens(&self) -> &[InlineTokenSpan] {
134        &self.tokens
135    }
136}
137
138macro_rules! content_from_text {
139    ($($text:ty),* $(,)?) => {
140        $(impl From<$text> for InputContent {
141            fn from(text: $text) -> Self {
142                Self::new(text)
143            }
144        })*
145    };
146}
147// Every text type `set_value` accepted before it took content.
148content_from_text!(
149    &str,
150    &mut str,
151    &String,
152    String,
153    char,
154    Box<str>,
155    std::sync::Arc<str>,
156    &std::sync::Arc<str>,
157    std::borrow::Cow<'_, str>,
158    &SharedString,
159    SharedString,
160);
161
162/// A rejected token operation never partially changes the document.
163#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164#[non_exhaustive]
165pub enum InlineTokenError {
166    InvalidRange,
167    InvalidBoundary,
168    InvalidToken,
169    OverlappingTokens,
170    TextMismatch,
171    UnsupportedMode,
172    ValidationRejected,
173    CompositionActive,
174}
175
176impl std::error::Error for InlineTokenError {}
177impl std::fmt::Display for InlineTokenError {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        f.write_str(match self {
180            Self::InvalidRange => "token range is outside the document or empty",
181            Self::InvalidBoundary => "token range splits a Unicode grapheme",
182            Self::InvalidToken => {
183                "token requires a nonempty ID, text and label without control characters"
184            }
185            Self::OverlappingTokens => "token ranges overlap",
186            Self::TextMismatch => "token text does not match its range or input normalization",
187            Self::UnsupportedMode => "tokens are not supported by this input mode",
188            Self::ValidationRejected => "input validation rejected the content",
189            Self::CompositionActive => "finish the active IME composition before editing tokens",
190        })
191    }
192}
193
194fn validate_range(text: &str, range: &Range<usize>) -> Result<(), InlineTokenError> {
195    if range.start > range.end || range.end > text.len() {
196        return Err(InlineTokenError::InvalidRange);
197    }
198    let boundary =
199        |offset| offset == text.len() || text.grapheme_indices(true).any(|(ix, _)| ix == offset);
200    if !boundary(range.start) || !boundary(range.end) {
201        return Err(InlineTokenError::InvalidBoundary);
202    }
203    Ok(())
204}
205
206#[derive(Default)]
207pub(super) struct InlineTokenStore {
208    spans: Vec<InlineTokenSpan>,
209}
210
211/// Only affected records are retained in history. Ranges are relative to the edit start.
212#[derive(Clone, Debug, PartialEq, Eq)]
213pub(super) struct TokenDelta {
214    removed: Vec<InlineTokenSpan>,
215    inserted: Vec<InlineTokenSpan>,
216}
217
218impl InlineTokenStore {
219    fn replace(
220        &mut self,
221        range: &Range<usize>,
222        new_len: usize,
223        inserted: &[InlineTokenSpan],
224    ) -> Option<Box<TokenDelta>> {
225        let mut removed = Vec::new();
226        let shift = new_len as isize - range.len() as isize;
227        self.spans.retain_mut(|span| {
228            if span.range.start < range.end && range.start < span.range.end {
229                removed.push(span.shifted(-(range.start as isize)));
230                false
231            } else {
232                if span.range.start >= range.end {
233                    *span = span.shifted(shift);
234                }
235                true
236            }
237        });
238        self.spans
239            .extend(inserted.iter().map(|s| s.shifted(range.start as isize)));
240        if !inserted.is_empty() {
241            self.spans.sort_by_key(|s| s.range.start);
242        }
243        (!removed.is_empty() || !inserted.is_empty()).then(|| {
244            Box::new(TokenDelta {
245                removed,
246                inserted: inserted.to_vec(),
247            })
248        })
249    }
250}
251
252impl<M: InputModeKind> InputBaseState<M> {
253    pub(super) fn token_spans(&self) -> &[InlineTokenSpan] {
254        self.inline_tokens
255            .as_ref()
256            .map_or(&[], |store| &store.spans)
257    }
258    pub(super) fn token_boundary(&self, offset: usize, bias: sum_tree::Bias) -> usize {
259        let spans = self.token_spans();
260        let ix = spans.partition_point(|s| s.range.end <= offset);
261        if let Some(span) = spans.get(ix).filter(|s| s.range.start < offset) {
262            if bias == sum_tree::Bias::Left {
263                span.range.start
264            } else {
265                span.range.end
266            }
267        } else {
268            offset
269        }
270    }
271    pub(super) fn normalize_token_range(&self, range: Range<usize>) -> Range<usize> {
272        if self.replaying_history {
273            return range;
274        }
275        if range.is_empty() {
276            let offset = self.token_boundary(range.start, sum_tree::Bias::Right);
277            offset..offset
278        } else {
279            self.token_boundary(range.start, sum_tree::Bias::Left)
280                ..self.token_boundary(range.end, sum_tree::Bias::Right)
281        }
282    }
283    pub(super) fn edit_tokens(
284        &mut self,
285        range: &Range<usize>,
286        new_len: usize,
287    ) -> Option<Box<TokenDelta>> {
288        if self.replaying_history {
289            return None;
290        }
291        let inserted = self.pending_token.take().map(|token| InlineTokenSpan {
292            range: 0..new_len,
293            token,
294        });
295        if inserted.is_some() && self.inline_tokens.is_none() {
296            self.inline_tokens = Some(Box::default());
297        }
298        self.inline_tokens
299            .as_mut()?
300            .replace(range, new_len, inserted.as_slice())
301    }
302    pub(super) fn replay_tokens(
303        &mut self,
304        range: &Range<usize>,
305        new_len: usize,
306        delta: Option<&TokenDelta>,
307        undo: bool,
308    ) {
309        let inserted = delta
310            .map(|d| {
311                if undo {
312                    &d.removed[..]
313                } else {
314                    &d.inserted[..]
315                }
316            })
317            .unwrap_or_default();
318        if !inserted.is_empty() && self.inline_tokens.is_none() {
319            self.inline_tokens = Some(Box::default());
320        }
321        if let Some(store) = self.inline_tokens.as_mut() {
322            store.replace(range, new_len, inserted);
323        }
324    }
325    fn check_token_mode(&self) -> Result<(), InlineTokenError> {
326        if self.ime_marked_range.is_some() {
327            return Err(InlineTokenError::CompositionActive);
328        }
329        if M::CODE_EDITOR || self.masked || self.token_is_secret() || !self.mask_pattern.is_none() {
330            return Err(InlineTokenError::UnsupportedMode);
331        }
332        Ok(())
333    }
334    fn replace_token(
335        &mut self,
336        range: Range<usize>,
337        token: InlineToken,
338        window: &mut Window,
339        cx: &mut Context<Self>,
340    ) -> Result<(), InlineTokenError> {
341        self.check_token_mode()?;
342        token.validate()?;
343        let text = self.text.to_string();
344        validate_range(&text, &range)?;
345        let range = self.normalize_token_range(range);
346        let mut next = text;
347        next.replace_range(range.clone(), &token.text);
348        validate_range(&next, &(range.start..range.start + token.text.len()))?;
349        if self.normalize_input(&next) != next {
350            return Err(InlineTokenError::TextMismatch);
351        }
352        if !self.is_valid_input(&next, cx) {
353            return Err(InlineTokenError::ValidationRejected);
354        }
355        if self
356            .token_spans()
357            .iter()
358            .any(|s| s.range == range && s.token == token)
359        {
360            return Ok(());
361        }
362        let new_text = token.text.clone();
363        self.pending_token = Some(token);
364        self.undo_manager.break_transaction_coalescing();
365        self.undo_manager.set_pending_intent(EditIntent::Atomic);
366        let range_utf16 = self.range_to_utf16(&range);
367        self.validated_token_edit = true;
368        self.with_edits_allowed(|state| {
369            state.replace_text_in_range(Some(range_utf16), &new_text, window, cx)
370        });
371        self.validated_token_edit = false;
372        self.pending_token = None;
373        Ok(())
374    }
375    /// Adopt the tokens of content whose text was just installed by
376    /// `set_value`. Tokens are dropped when this mode cannot show them or when
377    /// normalization changed the text, since their ranges would no longer
378    /// describe it.
379    pub(super) fn install_tokens(&mut self, content: InputContent) {
380        let supported = !M::CODE_EDITOR && self.mask_pattern.is_none();
381        let text_kept = self.text == content.text.as_ref();
382        self.inline_tokens = (supported && text_kept && !content.tokens.is_empty()).then(|| {
383            Box::new(InlineTokenStore {
384                spans: content.tokens,
385            })
386        });
387    }
388}
389
390macro_rules! token_api {
391    ($mode:ty) => {
392        impl InputBaseState<$mode> {
393            /// Replace the selection with an atomic token; no delimiter is added.
394            pub fn replace_with_token(
395                &mut self,
396                token: InlineToken,
397                window: &mut Window,
398                cx: &mut Context<Self>,
399            ) -> Result<(), InlineTokenError> {
400                self.replace_token(self.selected_range(), token, window, cx)
401            }
402            /// Replace a UTF-8 byte range, expanding overlaps to whole tokens.
403            pub fn replace_range_with_token(
404                &mut self,
405                range: Range<usize>,
406                token: InlineToken,
407                window: &mut Window,
408                cx: &mut Context<Self>,
409            ) -> Result<(), InlineTokenError> {
410                self.replace_token(range, token, window, cx)
411            }
412            pub fn tokens(&self) -> &[InlineTokenSpan] {
413                self.token_spans()
414            }
415            /// The text with its tokens, as `set_value` accepts it.
416            pub fn content(&self) -> InputContent {
417                InputContent {
418                    text: self.value(),
419                    tokens: self.token_spans().to_vec(),
420                }
421            }
422        }
423    };
424}
425token_api!(super::InputMode);
426token_api!(super::TextareaMode);