1#![allow(rustdoc::invalid_rust_codeblocks)]
2#[cfg(feature = "egui")]
70mod completer;
71pub mod highlighting;
72#[cfg(feature = "egui")]
73mod hyperlinks;
74mod syntax;
75#[cfg(test)]
76mod tests;
77mod themes;
78
79#[cfg(feature = "egui")]
80use egui::Stroke;
81#[cfg(feature = "egui")]
82use egui::text::LayoutJob;
83#[cfg(feature = "egui")]
84use egui::widgets::text_edit::TextEditOutput;
85pub use highlighting::Token;
86#[cfg(feature = "egui")]
87use highlighting::highlight;
88#[cfg(feature = "egui")]
89use hyperlinks::handle_links;
90#[cfg(feature = "editor")]
91use std::hash::{Hash, Hasher};
92pub use syntax::{Patch, Syntax, TokenType};
93pub use themes::ColorTheme;
94pub use themes::DEFAULT_THEMES;
95
96#[cfg(feature = "egui")]
97pub use crate::completer::Completer;
98
99#[cfg(feature = "egui")]
100pub trait Editor: Hash {
101 fn append(&self, job: &mut LayoutJob, token: &Token);
102}
103
104#[cfg(feature = "editor")]
105#[derive(Clone, Debug, PartialEq)]
106pub struct CodeEditor {
108 id: String,
109 theme: ColorTheme,
110 numlines: bool,
112 numlines_shift: isize,
113 numlines_only_natural: bool,
114 fontsize: f32,
115 clickable_links: bool,
116 rows: usize,
117 vscroll: bool,
118 stick_to_bottom: bool,
119 desired_width: f32,
120 wrap: bool,
121 hint_text: Option<String>,
122}
123
124#[cfg(feature = "editor")]
125impl Hash for CodeEditor {
126 fn hash<H: Hasher>(&self, state: &mut H) {
127 self.theme.hash(state);
128 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
129 (self.fontsize as u32).hash(state);
130 }
131}
132
133#[cfg(feature = "editor")]
134impl Default for CodeEditor {
135 fn default() -> CodeEditor {
136 CodeEditor {
137 id: String::from("Code Editor"),
138 theme: ColorTheme::GRUVBOX,
139 numlines: true,
140 numlines_shift: 0,
141 numlines_only_natural: false,
142 fontsize: 10.0,
143 clickable_links: true,
144 rows: 10,
145 vscroll: true,
146 stick_to_bottom: false,
147 desired_width: f32::INFINITY,
148 wrap: false,
149 hint_text: None,
150 }
151 }
152}
153
154#[cfg(feature = "editor")]
155impl CodeEditor {
156 pub fn id_source(self, id_source: impl Into<String>) -> Self {
157 CodeEditor {
158 id: id_source.into(),
159 ..self
160 }
161 }
162
163 pub fn with_rows(self, rows: usize) -> Self {
167 CodeEditor { rows, ..self }
168 }
169
170 pub fn with_theme(self, theme: ColorTheme) -> Self {
174 CodeEditor { theme, ..self }
175 }
176
177 pub fn with_fontsize(self, fontsize: f32) -> Self {
181 CodeEditor { fontsize, ..self }
182 }
183
184 #[cfg(feature = "egui")]
185 pub fn with_ui_fontsize(self, ui: &mut egui::Ui) -> Self {
187 CodeEditor {
188 fontsize: egui::TextStyle::Monospace.resolve(ui.style()).size,
189 ..self
190 }
191 }
192
193 #[cfg(feature = "egui")]
194 pub fn with_clickable_links(self, clickable_links: bool) -> Self {
196 CodeEditor {
197 clickable_links,
198 ..self
199 }
200 }
201 pub fn with_numlines(self, numlines: bool) -> Self {
205 CodeEditor { numlines, ..self }
206 }
207
208 pub fn with_numlines_shift(self, numlines_shift: isize) -> Self {
212 CodeEditor {
213 numlines_shift,
214 ..self
215 }
216 }
217
218 pub fn with_numlines_only_natural(self, numlines_only_natural: bool) -> Self {
222 CodeEditor {
223 numlines_only_natural,
224 ..self
225 }
226 }
227
228 pub fn with_wrap(self, wrap: bool) -> Self {
232 CodeEditor { wrap, ..self }
233 }
234 pub fn vscroll(self, vscroll: bool) -> Self {
245 CodeEditor { vscroll, ..self }
246 }
247 pub fn auto_shrink(self, shrink: bool) -> Self {
251 CodeEditor {
252 desired_width: if shrink { 0.0 } else { self.desired_width },
253 ..self
254 }
255 }
256
257 pub fn desired_width(self, width: f32) -> Self {
261 CodeEditor {
262 desired_width: width,
263 ..self
264 }
265 }
266
267 pub fn stick_to_bottom(self, stick_to_bottom: bool) -> Self {
277 CodeEditor {
278 stick_to_bottom,
279 ..self
280 }
281 }
282
283 pub fn hint_text<S: Into<String>>(self, hint_text: S) -> Self {
284 let hint_text = hint_text.into();
285 let rows = self.rows.max(hint_text.lines().count());
286 CodeEditor {
287 hint_text: Some(hint_text),
288 rows,
289 ..self
290 }
291 }
292
293 #[cfg(feature = "egui")]
294 pub fn format_token(&self, ty: TokenType) -> egui::text::TextFormat {
295 format_token(&self.theme, self.fontsize, ty)
296 }
297
298 #[cfg(feature = "egui")]
299 fn numlines_show(&self, ui: &mut egui::Ui, text: &str) {
300 use egui::TextBuffer;
301
302 let total = if text.ends_with('\n') || text.is_empty() {
303 text.lines().count() + 1
304 } else {
305 text.lines().count()
306 }
307 .max(self.rows) as isize;
308 let max_indent = total
309 .to_string()
310 .len()
311 .max(!self.numlines_only_natural as usize * self.numlines_shift.to_string().len());
312 let mut counter = (1..=total)
313 .map(|i| {
314 let num = i + self.numlines_shift;
315 if num <= 0 && self.numlines_only_natural {
316 String::new()
317 } else {
318 let label = num.to_string();
319 format!(
320 "{}{label}",
321 " ".repeat(max_indent.saturating_sub(label.len()))
322 )
323 }
324 })
325 .collect::<Vec<String>>()
326 .join("\n");
327
328 #[allow(clippy::cast_precision_loss)]
329 let width = max_indent as f32
330 * self.fontsize
331 * 0.5
332 * !(total + self.numlines_shift <= 0 && self.numlines_only_natural) as u8 as f32;
333
334 let mut layouter = |ui: &egui::Ui, text_buffer: &dyn TextBuffer, _wrap_width: f32| {
335 let layout_job = egui::text::LayoutJob::single_section(
336 text_buffer.as_str().to_string(),
337 egui::TextFormat::simple(
338 egui::FontId::monospace(self.fontsize),
339 self.theme.type_color(TokenType::Comment(true)),
340 ),
341 );
342 ui.fonts_mut(|f| f.layout_job(layout_job))
343 };
344
345 ui.add(
346 egui::TextEdit::multiline(&mut counter)
347 .id_source(format!("{}_numlines", self.id))
348 .font(egui::TextStyle::Monospace)
349 .interactive(false)
350 .frame(egui::Frame::NONE)
351 .desired_rows(self.rows)
352 .desired_width(width)
353 .layouter(&mut layouter),
354 );
355 }
356
357 #[cfg(feature = "egui")]
358 pub fn show_with_completer(
360 &mut self,
361 ui: &mut egui::Ui,
362 text: &mut dyn egui::TextBuffer,
363 syntax: &Syntax,
364 completer: &mut Completer,
365 ) -> TextEditOutput {
366 completer.handle_input(ui.ctx());
367 let mut editor_output = self.show(ui, text, syntax);
368 completer.show(syntax, &self.theme, self.fontsize, &mut editor_output);
369 editor_output
370 }
371
372 #[cfg(feature = "egui")]
373 pub fn show(
375 &mut self,
376 ui: &mut egui::Ui,
377 text: &mut dyn egui::TextBuffer,
378 syntax: &Syntax,
379 ) -> TextEditOutput {
380 use egui::TextBuffer;
381 let mut text_edit_output: Option<TextEditOutput> = None;
382 let mut code_editor = |ui: &mut egui::Ui| {
383 let frame = egui::Frame::new().fill(self.theme.bg());
384 frame.show(ui, |ui| {
385 ui.horizontal_top(|h| {
386 self.theme.modify_style(h, self.fontsize);
387 if self.numlines {
388 self.numlines_show(h, text.as_str());
389 }
390 egui::ScrollArea::horizontal()
391 .id_salt(format!("{}_inner_scroll", self.id))
392 .show(h, |ui| {
393 use crate::highlighting::Links;
394
395 let mut links_ranges = Links::default();
396 let mut layouter =
397 |ui: &egui::Ui, text_buffer: &dyn TextBuffer, wrap_width: f32| {
398 let text_str = text_buffer.as_str();
399 let (mut layout_job, links) =
400 highlight(ui.ctx(), self, text_str, syntax);
401 links_ranges = links;
402
403 if !self.numlines && self.wrap {
404 layout_job.wrap =
405 egui::text::TextWrapping::wrap_at_width(wrap_width);
406 }
407 ui.fonts_mut(|f| f.layout_job(layout_job))
408 };
409
410 let mut text_edit = egui::TextEdit::multiline(text)
411 .id_source(&self.id)
412 .lock_focus(true)
413 .desired_rows(self.rows)
414 .desired_width(self.desired_width)
415 .layouter(&mut layouter);
416 if let Some(hint) = self.hint_text.as_ref() {
417 text_edit = text_edit.hint_text(hint);
418 }
419 let output = text_edit.show(ui);
420
421 if self.clickable_links {
422 handle_links(&output, &links_ranges);
423 }
424 text_edit_output = Some(output);
425 });
426 });
427 });
428 };
429 if self.vscroll {
430 egui::ScrollArea::vertical()
431 .id_salt(format!("{}_outer_scroll", self.id))
432 .stick_to_bottom(self.stick_to_bottom)
433 .show(ui, code_editor);
434 } else {
435 code_editor(ui);
436 }
437
438 text_edit_output.expect("TextEditOutput should exist at this point")
439 }
440}
441
442#[cfg(feature = "editor")]
443#[cfg(feature = "egui")]
444impl Editor for CodeEditor {
445 fn append(&self, job: &mut LayoutJob, token: &Token) {
446 if !token.buffer().is_empty() {
447 job.append(token.buffer(), 0.0, self.format_token(token.ty()));
448 }
449 }
450}
451
452#[cfg(feature = "egui")]
453pub fn format_token(theme: &ColorTheme, fontsize: f32, ty: TokenType) -> egui::text::TextFormat {
454 let font_id = egui::FontId::monospace(fontsize);
455 let color = theme.type_color(ty);
456
457 let mut tf = egui::text::TextFormat::simple(font_id, color);
458 if ty == TokenType::Hyperlink {
459 tf.underline = Stroke::new(fontsize * 0.1, color);
460 }
461 tf
462}