floem/views/editor/
phantom_text.rs

1use std::borrow::Cow;
2
3use crate::{
4    peniko::Color,
5    text::{Attrs, AttrsList},
6};
7use floem_editor_core::cursor::CursorAffinity;
8use smallvec::SmallVec;
9
10/// `PhantomText` is for text that is not in the actual document, but should be rendered with it.
11///
12/// Ex: Inlay hints, IME text, error lens' diagnostics, etc
13#[derive(Debug, Clone)]
14pub struct PhantomText {
15    /// The kind is currently used for sorting the phantom text on a line
16    pub kind: PhantomTextKind,
17    /// Column on the line that the phantom text should be displayed at
18    pub col: usize,
19    /// the affinity of cursor, e.g. for completion phantom text,
20    /// we want the cursor always before the phantom text
21    pub affinity: Option<CursorAffinity>,
22    pub text: String,
23    pub font_size: Option<usize>,
24    // font_family: Option<FontFamily>,
25    pub fg: Option<Color>,
26    pub bg: Option<Color>,
27    pub under_line: Option<Color>,
28}
29
30#[derive(Debug, Clone, Copy, Ord, Eq, PartialEq, PartialOrd)]
31pub enum PhantomTextKind {
32    /// Input methods
33    Ime,
34    Placeholder,
35    /// Completion lens / Inline completion
36    Completion,
37    /// Inlay hints supplied by an LSP/PSP (like type annotations)
38    InlayHint,
39    /// Error lens
40    Diagnostic,
41}
42
43/// Information about the phantom text on a specific line.
44///
45/// This has various utility functions for transforming a coordinate (typically a column) into the
46/// resulting coordinate after the phantom text is combined with the line's real content.
47#[derive(Debug, Default, Clone)]
48pub struct PhantomTextLine {
49    /// This uses a smallvec because most lines rarely have more than a couple phantom texts
50    pub text: SmallVec<[PhantomText; 6]>,
51}
52
53impl PhantomTextLine {
54    /// Translate a column position into the text into what it would be after combining
55    pub fn col_at(&self, pre_col: usize) -> usize {
56        let mut last = pre_col;
57        for (col_shift, size, col, _) in self.offset_size_iter() {
58            if pre_col >= col {
59                last = pre_col + col_shift + size;
60            }
61        }
62
63        last
64    }
65
66    /// Translate a column position into the text into what it would be after combining
67    ///
68    /// If `before_cursor` is false and the cursor is right at the start then it will stay there
69    /// (Think 'is the phantom text before the cursor')
70    pub fn col_after(&self, pre_col: usize, before_cursor: bool) -> usize {
71        let mut last = pre_col;
72        for (col_shift, size, col, text) in self.offset_size_iter() {
73            let before_cursor = match text.affinity {
74                Some(CursorAffinity::Forward) => true,
75                Some(CursorAffinity::Backward) => false,
76                None => before_cursor,
77            };
78
79            if pre_col > col || (pre_col == col && before_cursor) {
80                last = pre_col + col_shift + size;
81            }
82        }
83
84        last
85    }
86
87    /// Translate a column position into the text into what it would be after combining
88    ///
89    /// it only takes `before_cursor` in the params without considering the
90    /// cursor affinity in phantom text
91    pub fn col_after_force(&self, pre_col: usize, before_cursor: bool) -> usize {
92        let mut last = pre_col;
93        for (col_shift, size, col, _) in self.offset_size_iter() {
94            if pre_col > col || (pre_col == col && before_cursor) {
95                last = pre_col + col_shift + size;
96            }
97        }
98
99        last
100    }
101
102    /// Translate a column position into the text into what it would be after combining
103    ///
104    /// If `before_cursor` is false and the cursor is right at the start then it will stay there
105    ///
106    /// (Think 'is the phantom text before the cursor')
107    ///
108    /// This accepts a `PhantomTextKind` to ignore. Primarily for IME due to it needing to put the
109    /// cursor in the middle.
110    pub fn col_after_ignore(
111        &self,
112        pre_col: usize,
113        before_cursor: bool,
114        skip: impl Fn(&PhantomText) -> bool,
115    ) -> usize {
116        let mut last = pre_col;
117        for (col_shift, size, col, phantom) in self.offset_size_iter() {
118            if skip(phantom) {
119                continue;
120            }
121
122            if pre_col > col || (pre_col == col && before_cursor) {
123                last = pre_col + col_shift + size;
124            }
125        }
126
127        last
128    }
129
130    /// Translate a column position into the position it would be before combining
131    pub fn before_col(&self, col: usize) -> usize {
132        let mut last = col;
133        for (col_shift, size, hint_col, _) in self.offset_size_iter() {
134            let shifted_start = hint_col + col_shift;
135            let shifted_end = shifted_start + size;
136            if col >= shifted_start {
137                if col >= shifted_end {
138                    last = col - col_shift - size;
139                } else {
140                    last = hint_col;
141                }
142            }
143        }
144        last
145    }
146
147    /// Insert the hints at their positions in the text
148    pub fn combine_with_text<'a>(&self, text: &'a str) -> Cow<'a, str> {
149        let mut text = Cow::Borrowed(text);
150        let mut col_shift = 0;
151
152        for phantom in self.text.iter() {
153            let location = phantom.col + col_shift;
154
155            // Stop iterating if the location is bad
156            if text.get(location..).is_none() {
157                return text;
158            }
159
160            let mut text_o = text.into_owned();
161            text_o.insert_str(location, &phantom.text);
162            text = Cow::Owned(text_o);
163
164            col_shift += phantom.text.len();
165        }
166
167        text
168    }
169
170    /// Iterator over `(col_shift, size, hint, pre_column)`
171    /// Note that this only iterates over the ordered text, since those depend on the text for where
172    /// they'll be positioned
173    pub fn offset_size_iter(
174        &self,
175    ) -> impl Iterator<Item = (usize, usize, usize, &PhantomText)> + '_ {
176        let mut col_shift = 0;
177
178        self.text.iter().map(move |phantom| {
179            let pre_col_shift = col_shift;
180            col_shift += phantom.text.len();
181            (
182                pre_col_shift,
183                col_shift - pre_col_shift,
184                phantom.col,
185                phantom,
186            )
187        })
188    }
189
190    pub fn apply_attr_styles(&self, default: Attrs, attrs_list: &mut AttrsList) {
191        for (offset, size, col, phantom) in self.offset_size_iter() {
192            let start = col + offset;
193            let end = start + size;
194
195            let mut attrs = default.clone();
196            if let Some(fg) = phantom.fg {
197                attrs = attrs.color(fg);
198            }
199            if let Some(phantom_font_size) = phantom.font_size {
200                let font_size = attrs.font_size;
201                attrs = attrs.font_size((phantom_font_size as f32).min(font_size));
202            }
203
204            attrs_list.add_span(start..end, attrs);
205        }
206    }
207}