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
9pub 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
26pub 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum SyntaxContext {
86 Code,
87 String,
88 Comment,
89}
90
91pub 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#[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 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 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 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 assert_eq!(resolved.border, dark().colors.border);
240 }
241
242 #[test]
243 fn resolution_never_consumes_its_own_output() {
244 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}