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 styles(
45 &self,
46 range: &Range<usize>,
47 resolver: &dyn HighlightStyleResolver,
48 ) -> Vec<(Range<usize>, HighlightStyle)>;
49
50 fn fold_ranges(&self, text: &Rope) -> Vec<FoldRange>;
51
52 fn fold_ranges_for_edit(&self, range: Range<usize>, text: &Rope) -> Vec<FoldRange> {
53 let _ = range;
54 self.fold_ranges(text)
55 }
56}
57
58pub type InputHighlighterFactory = Rc<dyn Fn(&str) -> Option<Box<dyn InputHighlighter>>>;
59pub type SharedHighlightStyleResolver = Arc<dyn HighlightStyleResolver>;
60pub type FoldIconRenderer = Rc<dyn Fn(usize, bool) -> AnyElement>;
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum SyntaxContext {
68 Code,
69 String,
70 Comment,
71}
72
73pub trait SyntaxContextProvider {
78 fn context_at(&self, text: &Rope, offset: usize) -> SyntaxContext;
79}
80
81#[derive(Clone, Copy, Default)]
82pub struct DiagnosticColors {
83 pub error: Hsla,
84 pub warning: Hsla,
85 pub info: Hsla,
86 pub hint: Hsla,
87}
88
89#[derive(Clone)]
91pub struct InputEditorStyle {
92 pub foreground: Hsla,
93 pub muted_foreground: Hsla,
94 pub background: Hsla,
95 pub border: Hsla,
96 pub selection: Hsla,
97 pub caret: Hsla,
98 pub diagnostics: DiagnosticColors,
99 pub highlight_styles: SharedHighlightStyleResolver,
100 pub editor_invisible: Option<Hsla>,
101 pub editor_active_line: Option<Hsla>,
102 pub editor_gutter_background: Option<Hsla>,
103 pub fold_icon_renderer: Option<FoldIconRenderer>,
104}
105
106impl InputEditorStyle {
107 pub fn resolved(&self, tokens: &SemanticThemeTokens) -> Self {
120 let colors = &tokens.colors;
121 let unset = |value: Hsla| value.a == 0.;
122 let or = |value: Hsla, fallback: Hsla| if unset(value) { fallback } else { value };
123
124 let foreground = or(self.foreground, colors.foreground);
125 let mut selection = self.selection;
126 if unset(selection) {
127 selection = colors.accent;
128 selection.a = 0.4;
130 }
131
132 Self {
133 foreground,
134 muted_foreground: or(self.muted_foreground, colors.muted_foreground),
135 background: or(self.background, colors.surface),
136 border: or(self.border, colors.border),
137 selection,
138 caret: or(self.caret, foreground),
139 ..self.clone()
140 }
141 }
142}
143
144impl Default for InputEditorStyle {
145 fn default() -> Self {
146 Self {
147 foreground: Hsla::default(),
148 muted_foreground: Hsla::default(),
149 background: Hsla::default(),
150 border: Hsla::default(),
151 selection: Hsla::default(),
152 caret: Hsla::default(),
153 diagnostics: DiagnosticColors::default(),
154 highlight_styles: Arc::new(NoHighlightStyles),
155 editor_invisible: None,
156 editor_active_line: None,
157 editor_gutter_background: None,
158 fold_icon_renderer: None,
159 }
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use gpui::hsla;
166
167 use super::InputEditorStyle;
168 use crate::SemanticThemeTokens;
169
170 fn dark() -> SemanticThemeTokens {
171 let mut tokens = SemanticThemeTokens::default();
172 tokens.colors.foreground = hsla(0., 0., 0.98, 1.0);
173 tokens.colors.muted_foreground = hsla(0., 0., 0.64, 1.0);
174 tokens.colors.surface = hsla(0., 0., 0.04, 1.0);
175 tokens.colors.border = hsla(0., 0., 0.15, 1.0);
176 tokens.colors.accent = hsla(0.6, 0.5, 0.5, 1.0);
177 tokens
178 }
179
180 #[test]
181 fn an_unprojected_style_takes_its_ink_from_the_palette() {
182 let tokens = dark();
183 let resolved = InputEditorStyle::default().resolved(&tokens);
184
185 assert_eq!(resolved.foreground, tokens.colors.foreground);
186 assert_eq!(resolved.caret, tokens.colors.foreground);
187 assert_eq!(resolved.muted_foreground, tokens.colors.muted_foreground);
188 assert_eq!(resolved.background, tokens.colors.surface);
189 assert_eq!(resolved.border, tokens.colors.border);
190 for colour in [
193 resolved.foreground,
194 resolved.caret,
195 resolved.muted_foreground,
196 resolved.selection,
197 ] {
198 assert!(colour.a > 0., "{colour:?} is still invisible");
199 }
200 }
201
202 #[test]
203 fn a_selection_stays_translucent_enough_to_read_through() {
204 let resolved = InputEditorStyle::default().resolved(&dark());
205 assert_eq!(resolved.selection.a, 0.4);
206 }
207
208 #[test]
209 fn projected_colours_are_kept_verbatim() {
210 let chosen = hsla(0.3, 0.4, 0.5, 1.0);
211 let style = InputEditorStyle {
212 foreground: chosen,
213 caret: chosen,
214 ..Default::default()
215 };
216 let resolved = style.resolved(&dark());
217
218 assert_eq!(resolved.foreground, chosen);
219 assert_eq!(resolved.caret, chosen);
220 assert_eq!(resolved.border, dark().colors.border);
222 }
223
224 #[test]
225 fn resolution_never_consumes_its_own_output() {
226 let projected = InputEditorStyle::default();
230 let first = projected.resolved(&dark());
231
232 let mut light = SemanticThemeTokens::default();
233 light.colors.foreground = hsla(0., 0., 0.04, 1.0);
234 let second = projected.resolved(&light);
235
236 assert_ne!(first.foreground, second.foreground);
237 assert_eq!(second.foreground, light.colors.foreground);
238 }
239}