1use 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#[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#[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#[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 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}
147content_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#[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#[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 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 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 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 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);