Skip to main content

gpui_base/input/editor/
highlighting.rs

1use std::{ops::Range, rc::Rc, sync::Arc};
2
3use gpui::{AnyElement, Context, HighlightStyle, Hsla, SharedString, Window};
4use ropey::Rope;
5
6use super::{EditorState, FoldRange, InputEdit};
7use crate::SemanticThemeTokens;
8
9/// Resolves semantic highlight names into renderable GPUI styles.
10///
11/// Base deliberately knows nothing about a concrete syntax theme. UI crates and
12/// applications can provide any resolver, independently of their parser.
13pub trait HighlightStyleResolver: Send + Sync {
14    fn style(&self, name: &str) -> Option<HighlightStyle>;
15}
16
17#[derive(Default)]
18struct NoHighlightStyles;
19
20impl HighlightStyleResolver for NoHighlightStyles {
21    fn style(&self, _: &str) -> Option<HighlightStyle> {
22        None
23    }
24}
25
26/// Parser-independent syntax highlighting seam consumed by the Base editor.
27///
28/// Implementations own parsing, incremental state, and language-specific
29/// behavior. Base only asks for styled ranges and fold candidates.
30pub trait InputHighlighter {
31    fn language(&self) -> SharedString;
32
33    fn update(
34        &mut self,
35        edit: Option<InputEdit>,
36        text: &Rope,
37        folding: bool,
38        window: &mut Window,
39        cx: &mut Context<EditorState>,
40    );
41
42    /// Apply several edits made as one change, such as typing with multiple
43    /// cursors. Each entry is an edit with the text as it stood right after
44    /// it, in the order the edits were applied.
45    ///
46    /// The default hands each edit to [`Self::update`] in turn. Override it to
47    /// reparse once for the whole change.
48    fn update_batch(
49        &mut self,
50        edits: &[(InputEdit, Rope)],
51        folding: bool,
52        window: &mut Window,
53        cx: &mut Context<EditorState>,
54    ) {
55        for (edit, text) in edits {
56            self.update(Some(*edit), text, folding, window, cx);
57        }
58    }
59
60    /// Return ordered, non-overlapping style runs that fully cover `range`.
61    /// Use [`HighlightStyle::default`] for text without a semantic style.
62    fn styles(
63        &self,
64        range: &Range<usize>,
65        resolver: &dyn HighlightStyleResolver,
66    ) -> Vec<(Range<usize>, HighlightStyle)>;
67
68    fn fold_ranges(&self, text: &Rope) -> Vec<FoldRange>;
69
70    fn fold_ranges_for_edit(&self, range: Range<usize>, text: &Rope) -> Vec<FoldRange> {
71        let _ = range;
72        self.fold_ranges(text)
73    }
74}
75
76pub type InputHighlighterFactory = Rc<dyn Fn(&str) -> Option<Box<dyn InputHighlighter>>>;
77pub type SharedHighlightStyleResolver = Arc<dyn HighlightStyleResolver>;
78pub type FoldIconRenderer = Rc<dyn Fn(usize, bool) -> AnyElement>;
79
80/// Where in the syntax tree an offset sits, for editing decisions.
81///
82/// Parser-independent: `gpui-component` answers from tree-sitter, apps may
83/// answer heuristically. `None` (no provider installed) means `Code`.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum SyntaxContext {
86    Code,
87    String,
88    Comment,
89}
90
91/// Answers syntax context for editing decisions (pairing, skip, indent).
92///
93/// Created per editor by the application's [`super::LanguageProvider`].
94/// Base never imports a parser; implementations live in UI crates or apps.
95pub trait SyntaxContextProvider {
96    fn context_at(&self, text: &Rope, offset: usize) -> SyntaxContext;
97}
98
99#[derive(Clone, Copy, Default)]
100pub struct DiagnosticColors {
101    pub error: Hsla,
102    pub warning: Hsla,
103    pub info: Hsla,
104    pub hint: Hsla,
105}
106
107/// Application-owned colors and highlight resolver consumed by editor painting.
108#[derive(Clone)]
109pub struct InputEditorStyle {
110    pub foreground: Hsla,
111    pub muted_foreground: Hsla,
112    pub background: Hsla,
113    pub border: Hsla,
114    pub selection: Hsla,
115    pub caret: Hsla,
116    pub diagnostics: DiagnosticColors,
117    pub highlight_styles: SharedHighlightStyleResolver,
118    pub editor_invisible: Option<Hsla>,
119    pub editor_active_line: Option<Hsla>,
120    pub editor_gutter_background: Option<Hsla>,
121    pub fold_icon_renderer: Option<FoldIconRenderer>,
122}
123
124impl InputEditorStyle {
125    /// Fills in every colour that was left unset, from the active palette.
126    ///
127    /// `Hsla::default()` is fully transparent, and every colour on `Default` is
128    /// that — so an input nothing projected onto painted its glyphs, its caret
129    /// and its selection in nothing at all. Transparent is not a colour anyone
130    /// means for ink, which is what makes it usable as "unset" here.
131    ///
132    /// This is resolution, not assignment: whatever a consumer did project is
133    /// kept exactly. `crates/component` projects the whole style on every render and
134    /// never reaches this; a consumer that projects once at construction gets
135    /// the palette that is current now rather than the one that happened to be
136    /// installed when the state was built.
137    pub fn resolved(&self, tokens: &SemanticThemeTokens) -> Self {
138        let colors = &tokens.colors;
139        let unset = |value: Hsla| value.a == 0.;
140        let or = |value: Hsla, fallback: Hsla| if unset(value) { fallback } else { value };
141
142        let foreground = or(self.foreground, colors.foreground);
143        let mut selection = self.selection;
144        if unset(selection) {
145            selection = colors.accent;
146            // A selection must not hide the glyphs it selects.
147            selection.a = 0.4;
148        }
149
150        Self {
151            foreground,
152            muted_foreground: or(self.muted_foreground, colors.muted_foreground),
153            background: or(self.background, colors.surface),
154            border: or(self.border, colors.border),
155            selection,
156            caret: or(self.caret, foreground),
157            ..self.clone()
158        }
159    }
160}
161
162impl Default for InputEditorStyle {
163    fn default() -> Self {
164        Self {
165            foreground: Hsla::default(),
166            muted_foreground: Hsla::default(),
167            background: Hsla::default(),
168            border: Hsla::default(),
169            selection: Hsla::default(),
170            caret: Hsla::default(),
171            diagnostics: DiagnosticColors::default(),
172            highlight_styles: Arc::new(NoHighlightStyles),
173            editor_invisible: None,
174            editor_active_line: None,
175            editor_gutter_background: None,
176            fold_icon_renderer: None,
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use gpui::hsla;
184
185    use super::InputEditorStyle;
186    use crate::SemanticThemeTokens;
187
188    fn dark() -> SemanticThemeTokens {
189        let mut tokens = SemanticThemeTokens::default();
190        tokens.colors.foreground = hsla(0., 0., 0.98, 1.0);
191        tokens.colors.muted_foreground = hsla(0., 0., 0.64, 1.0);
192        tokens.colors.surface = hsla(0., 0., 0.04, 1.0);
193        tokens.colors.border = hsla(0., 0., 0.15, 1.0);
194        tokens.colors.accent = hsla(0.6, 0.5, 0.5, 1.0);
195        tokens
196    }
197
198    #[test]
199    fn an_unprojected_style_takes_its_ink_from_the_palette() {
200        let tokens = dark();
201        let resolved = InputEditorStyle::default().resolved(&tokens);
202
203        assert_eq!(resolved.foreground, tokens.colors.foreground);
204        assert_eq!(resolved.caret, tokens.colors.foreground);
205        assert_eq!(resolved.muted_foreground, tokens.colors.muted_foreground);
206        assert_eq!(resolved.background, tokens.colors.surface);
207        assert_eq!(resolved.border, tokens.colors.border);
208        // The point of the change: every one of these was transparent, so an
209        // input nothing projected onto painted its text in nothing at all.
210        for colour in [
211            resolved.foreground,
212            resolved.caret,
213            resolved.muted_foreground,
214            resolved.selection,
215        ] {
216            assert!(colour.a > 0., "{colour:?} is still invisible");
217        }
218    }
219
220    #[test]
221    fn a_selection_stays_translucent_enough_to_read_through() {
222        let resolved = InputEditorStyle::default().resolved(&dark());
223        assert_eq!(resolved.selection.a, 0.4);
224    }
225
226    #[test]
227    fn projected_colours_are_kept_verbatim() {
228        let chosen = hsla(0.3, 0.4, 0.5, 1.0);
229        let style = InputEditorStyle {
230            foreground: chosen,
231            caret: chosen,
232            ..Default::default()
233        };
234        let resolved = style.resolved(&dark());
235
236        assert_eq!(resolved.foreground, chosen);
237        assert_eq!(resolved.caret, chosen);
238        // And what was not projected still comes from the palette.
239        assert_eq!(resolved.border, dark().colors.border);
240    }
241
242    #[test]
243    fn resolution_never_consumes_its_own_output() {
244        // The projected style is kept verbatim precisely so that this holds:
245        // resolving against a second palette must follow it, not stay on the
246        // first. Resolving in place would have frozen after one pass.
247        let projected = InputEditorStyle::default();
248        let first = projected.resolved(&dark());
249
250        let mut light = SemanticThemeTokens::default();
251        light.colors.foreground = hsla(0., 0., 0.04, 1.0);
252        let second = projected.resolved(&light);
253
254        assert_ne!(first.foreground, second.foreground);
255        assert_eq!(second.foreground, light.colors.foreground);
256    }
257}