1use std::rc::Rc;
2
3use gpui::{
4 App, DefiniteLength, Entity, IntoElement, RenderOnce, SharedString, StyleRefinement, Styled,
5 Window, prelude::FluentBuilder as _, relative,
6};
7
8use super::{EditorState, Input};
9use crate::native_menu::NativeMenu;
10use crate::{ActiveTheme as _, RoleOverride, StyledExt as _};
11
12const EDITOR_LINE_HEIGHT: f32 = 1.5;
15
16#[derive(IntoElement)]
18pub struct Editor {
19 state: Entity<EditorState>,
20 style: StyleRefinement,
21 height: Option<DefiniteLength>,
22 appearance: bool,
23 bordered: bool,
24 disabled: bool,
25 readonly: bool,
26 tab_index: isize,
27 role: RoleOverride,
28 aria_label: Option<SharedString>,
29
30 context_menu_builder: Option<Rc<dyn Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu>>,
34}
35
36impl Editor {
37 pub fn new(state: &Entity<EditorState>) -> Self {
38 Self {
39 state: state.clone(),
40 style: StyleRefinement::default(),
41 height: None,
42 appearance: true,
43 bordered: true,
44 disabled: false,
45 readonly: false,
46 tab_index: 0,
47 role: RoleOverride::default(),
48 aria_label: None,
49 context_menu_builder: None,
50 }
51 }
52
53 pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
54 self.height = Some(height.into());
55 self
56 }
57
58 pub fn appearance(mut self, appearance: bool) -> Self {
59 self.appearance = appearance;
60 self
61 }
62
63 pub fn bordered(mut self, bordered: bool) -> Self {
64 self.bordered = bordered;
65 self
66 }
67
68 pub fn disabled(mut self, disabled: bool) -> Self {
69 self.disabled = disabled;
70 self
71 }
72
73 pub fn readonly(mut self, readonly: bool) -> Self {
79 self.readonly = readonly;
80 self
81 }
82
83 pub fn tab_index(mut self, index: isize) -> Self {
84 self.tab_index = index;
85 self
86 }
87
88 pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
89 self.role = role.into();
90 self
91 }
92
93 pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
94 self.aria_label = Some(label.into());
95 self
96 }
97
98 pub fn context_menu(
103 mut self,
104 f: impl Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu + 'static,
105 ) -> Self {
106 self.context_menu_builder = Some(Rc::new(f));
107 self
108 }
109}
110
111impl Styled for Editor {
112 fn style(&mut self) -> &mut StyleRefinement {
113 &mut self.style
114 }
115}
116
117impl RenderOnce for Editor {
118 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
119 Input::from_state(self.state.clone())
120 .font_family(cx.theme().mono_font_family.clone())
125 .text_size(cx.theme().mono_font_size)
126 .line_height(relative(EDITOR_LINE_HEIGHT))
127 .appearance(self.appearance)
128 .bordered(self.bordered)
129 .focus_bordered(false)
130 .disabled(self.disabled)
131 .readonly(self.readonly)
132 .tab_index(self.tab_index)
133 .role(self.role)
134 .when_some(self.height, |this, height| this.h(height))
135 .when_some(self.aria_label, |this, label| this.aria_label(label))
136 .when_some(self.context_menu_builder, |this, build| {
137 this.context_menu(move |menu, window, cx| build(menu, window, cx))
138 })
139 .refine_style(&self.style)
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::input::EditorState;
147 use gpui::{
148 AppContext as _, Context, ParentElement as _, Pixels, Render, TestAppContext,
149 VisualTestContext, div, px,
150 };
151
152 struct Harness {
153 state: Entity<EditorState>,
154 text_size: Option<Pixels>,
156 }
157
158 impl Render for Harness {
159 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
160 div().size_full().child(
161 Editor::new(&self.state)
162 .when_some(self.text_size, |this, size| this.text_size(size)),
163 )
164 }
165 }
166
167 fn line_height(cx: &mut TestAppContext, text_size: Option<Pixels>) -> Pixels {
169 cx.update(crate::init);
170 let mut state = None;
171 let (_, cx) = cx.add_window_view(|window, cx| {
172 let editor = cx.new(|cx| EditorState::new(window, cx).default_value("fn main() {}"));
173 state = Some(editor.clone());
174 Harness {
175 state: editor,
176 text_size,
177 }
178 });
179 let state = state.unwrap();
180 VisualTestContext::update(cx, |window, cx| window.draw(cx).clear(cx));
181
182 cx.read(|cx| {
183 state
184 .read(cx)
185 .line_height()
186 .expect("the editor must lay out")
187 })
188 }
189
190 #[gpui::test]
191 fn the_rows_follow_the_font_size(cx: &mut TestAppContext) {
192 assert_eq!(line_height(cx, None), px(20.));
194 assert_eq!(line_height(cx, Some(px(24.))), px(36.));
196 assert_eq!(line_height(cx, Some(px(40.))), px(60.));
197 }
198 #[gpui::test]
199 fn language_config_works_without_render_sync(cx: &mut TestAppContext) {
200 use crate::input::{AutoClosingPair, language_config::LanguageConfig, set_language_config};
201 use gpui::EntityInputHandler as _;
202 cx.update(crate::init);
203 let mut state = None;
204 let (_, cx) = cx.add_window_view(|window, cx| {
205 let editor = cx.new(|cx| EditorState::new(window, cx).language("plaintext"));
206 editor.update(cx, |state, cx| {
208 state.replace_text_in_range(None, "(", window, cx);
209 assert_eq!(state.text().to_string(), "(");
210 state.set_value("", window, cx);
211 state.set_highlighter("json", cx);
212 state.replace_text_in_range(None, "[", window, cx);
213 assert_eq!(state.text().to_string(), "[]");
214 });
215 state = Some(editor.clone());
216 Harness {
217 state: editor,
218 text_size: None,
219 }
220 });
221 let state = state.unwrap();
222 VisualTestContext::update(cx, |_, cx| {
223 set_language_config(
224 "json",
225 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
226 cx,
227 );
228 });
229 VisualTestContext::update(cx, |window, cx| window.draw(cx).clear(cx));
231 VisualTestContext::update(cx, |window, cx| {
232 state.update(cx, |state, cx| {
233 state.set_value("", window, cx);
234 state.replace_text_in_range(None, "«", window, cx);
235 assert_eq!(state.text().to_string(), "«»");
236 state.set_value("if enabled:", window, cx);
237 state.set_selected_range(11..11, cx);
238 state.set_highlighter("python", cx);
239 state.focus(window, cx);
240 });
241 });
242 VisualTestContext::update(cx, |window, cx| window.draw(cx).clear(cx));
243 cx.simulate_keystrokes("enter");
244 cx.read(|cx| assert_eq!(state.read(cx).text().to_string(), "if enabled:\n "));
245 }
246
247 #[gpui::test]
248 fn aliases_share_config_even_when_registered_before_init(cx: &mut TestAppContext) {
249 use crate::input::{AutoClosingPair, language_config::LanguageConfig, set_language_config};
250 use gpui::EntityInputHandler as _;
251 cx.update(|cx| {
252 set_language_config(
253 "py",
254 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("«", "»")]),
255 cx,
256 )
257 });
258 cx.update(crate::init);
259 cx.add_window_view(|window, cx| {
260 let editor = cx.new(|cx| EditorState::new(window, cx).language("PYTHON"));
261 editor.update(cx, |state, cx| {
262 state.replace_text_in_range(None, "«", window, cx);
263 assert_eq!(state.text().to_string(), "«»");
264 });
265 set_language_config("pyi", LanguageConfig::default().auto_closing_pairs([]), cx);
266 editor.update(cx, |state, cx| {
267 state.set_value("", window, cx);
268 state.set_highlighter("py", cx);
269 state.replace_text_in_range(None, "(", window, cx);
270 assert_eq!(state.text().to_string(), "(");
271 });
272 Harness {
273 state: editor,
274 text_size: None,
275 }
276 });
277 }
278
279 #[cfg(all(feature = "tree-sitter-python", feature = "tree-sitter-rust"))]
280 #[gpui::test]
281 fn syntax_follows_language_before_first_render_and_after_switch(cx: &mut TestAppContext) {
282 use gpui::EntityInputHandler as _;
283 cx.update(crate::init);
284 cx.add_window_view(|window, cx| {
285 let editor = cx.new(|cx| {
286 EditorState::new(window, cx)
287 .language("python")
288 .default_value("\"hello world\"")
289 });
290 editor.update(cx, |state, cx| {
291 state.set_selected_range(6..6, cx);
292 state.replace_text_in_range(None, "(", window, cx);
293 assert_eq!(state.text().to_string(), "\"hello( world\"");
294 state.set_value("# comment ", window, cx);
295 state.set_selected_range(10..10, cx);
296 state.replace_text_in_range(None, "(", window, cx);
297 assert_eq!(state.text().to_string(), "# comment (");
298 state.set_value("# comment ", window, cx);
299 state.set_selected_range(10..10, cx);
300 state.set_highlighter("rust", cx);
301 state.replace_text_in_range(None, "(", window, cx);
302 assert_eq!(state.text().to_string(), "# comment ()");
303 });
304 Harness {
305 state: editor,
306 text_size: None,
307 }
308 });
309 }
310
311 #[cfg(feature = "tree-sitter-python")]
312 #[gpui::test]
313 fn python_pairing_uses_pre_edit_context(cx: &mut TestAppContext) {
314 use gpui::EntityInputHandler as _;
315 cx.update(crate::init);
316 for (before, cursor, typed, expected) in [
317 ("x = ", 4, "\"", "x = \"\""),
318 ("x = f\"{value}\"", 12, "(", "x = f\"{value()}\""),
319 ("x = \"value\"", 7, "(", "x = \"va(lue\""),
320 ("x = \"value\"", 5, "(", "x = \"(value\""),
321 ("x = \"value\"", 10, "(", "x = \"value(\""),
322 ] {
323 let mut state = None;
324 let (_, cx) = cx.add_window_view(|window, cx| {
325 let editor = cx.new(|cx| EditorState::new(window, cx).default_value(before));
326 state = Some(editor.clone());
327 Harness {
328 state: editor,
329 text_size: None,
330 }
331 });
332 let state = state.unwrap();
333 VisualTestContext::update(cx, |window, cx| {
334 state.update(cx, |state, cx| {
335 state.set_highlighter("python", cx);
336 state.set_selected_range(cursor..cursor, cx);
337 state.replace_text_in_range(None, typed, window, cx);
338 assert_eq!(state.text().to_string(), expected);
339 });
340 });
341 }
342 }
343 #[cfg(feature = "tree-sitter-rust")]
344 #[gpui::test]
345 fn generated_comment_closer_survives_syntax_changes(cx: &mut TestAppContext) {
346 use crate::input::{AutoClosingPair, SyntaxContext, language_config::LanguageConfig};
347 use gpui::EntityInputHandler as _;
348 cx.update(crate::init);
349 cx.update(|cx| {
350 crate::input::set_language_config(
351 "rust",
352 LanguageConfig::default().auto_closing_pairs([AutoClosingPair::new("/*", "*/")
353 .not_in([SyntaxContext::String, SyntaxContext::Comment])]),
354 cx,
355 )
356 });
357 let mut state = None;
358 let (_, cx) = cx.add_window_view(|window, cx| {
359 let editor = cx.new(|cx| EditorState::new(window, cx).language("rust"));
360 state = Some(editor.clone());
361 Harness {
362 state: editor,
363 text_size: None,
364 }
365 });
366 let state = state.unwrap();
367 VisualTestContext::update(cx, |window, cx| {
368 state.update(cx, |state, cx| {
369 state.set_highlighter("rust", cx);
370
371 for text in ["/", "*", "x", "*", "/"] {
372 state.replace_text_in_range(None, text, window, cx);
373 }
374 assert_eq!(state.text().to_string(), "/*x*/");
375 state.replace_text_in_range(None, "!", window, cx);
376 assert_eq!(state.text().to_string(), "/*x*/!");
377 });
378 });
379 }
380}