#![allow(rustdoc::invalid_rust_codeblocks)]
#[cfg(feature = "egui")]
mod completer;
pub mod highlighting;
#[cfg(feature = "egui")]
mod hyperlinks;
mod syntax;
#[cfg(test)]
mod tests;
mod themes;
#[cfg(feature = "egui")]
use egui::Stroke;
#[cfg(feature = "egui")]
use egui::text::LayoutJob;
#[cfg(feature = "egui")]
use egui::widgets::text_edit::TextEditOutput;
pub use highlighting::Token;
#[cfg(feature = "egui")]
use highlighting::highlight;
#[cfg(feature = "egui")]
use hyperlinks::handle_links;
#[cfg(feature = "editor")]
use std::hash::{Hash, Hasher};
pub use syntax::{Patch, Syntax, TokenType};
pub use themes::ColorTheme;
pub use themes::DEFAULT_THEMES;
#[cfg(feature = "egui")]
pub use crate::completer::Completer;
#[cfg(feature = "egui")]
pub trait Editor: Hash {
fn append(&self, job: &mut LayoutJob, token: &Token);
}
#[cfg(feature = "editor")]
#[derive(Clone, Debug, PartialEq)]
pub struct CodeEditor {
id: String,
theme: ColorTheme,
numlines: bool,
numlines_shift: isize,
numlines_only_natural: bool,
fontsize: f32,
clickable_links: bool,
rows: usize,
vscroll: bool,
stick_to_bottom: bool,
desired_width: f32,
wrap: bool,
hint_text: Option<String>,
}
#[cfg(feature = "editor")]
impl Hash for CodeEditor {
fn hash<H: Hasher>(&self, state: &mut H) {
self.theme.hash(state);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
(self.fontsize as u32).hash(state);
}
}
#[cfg(feature = "editor")]
impl Default for CodeEditor {
fn default() -> CodeEditor {
CodeEditor {
id: String::from("Code Editor"),
theme: ColorTheme::GRUVBOX,
numlines: true,
numlines_shift: 0,
numlines_only_natural: false,
fontsize: 10.0,
clickable_links: true,
rows: 10,
vscroll: true,
stick_to_bottom: false,
desired_width: f32::INFINITY,
wrap: false,
hint_text: None,
}
}
}
#[cfg(feature = "editor")]
impl CodeEditor {
pub fn id_source(self, id_source: impl Into<String>) -> Self {
CodeEditor {
id: id_source.into(),
..self
}
}
pub fn with_rows(self, rows: usize) -> Self {
CodeEditor { rows, ..self }
}
pub fn with_theme(self, theme: ColorTheme) -> Self {
CodeEditor { theme, ..self }
}
pub fn with_fontsize(self, fontsize: f32) -> Self {
CodeEditor { fontsize, ..self }
}
#[cfg(feature = "egui")]
pub fn with_ui_fontsize(self, ui: &mut egui::Ui) -> Self {
CodeEditor {
fontsize: egui::TextStyle::Monospace.resolve(ui.style()).size,
..self
}
}
#[cfg(feature = "egui")]
pub fn with_clickable_links(self, clickable_links: bool) -> Self {
CodeEditor {
clickable_links,
..self
}
}
pub fn with_numlines(self, numlines: bool) -> Self {
CodeEditor { numlines, ..self }
}
pub fn with_numlines_shift(self, numlines_shift: isize) -> Self {
CodeEditor {
numlines_shift,
..self
}
}
pub fn with_numlines_only_natural(self, numlines_only_natural: bool) -> Self {
CodeEditor {
numlines_only_natural,
..self
}
}
pub fn with_wrap(self, wrap: bool) -> Self {
CodeEditor { wrap, ..self }
}
pub fn vscroll(self, vscroll: bool) -> Self {
CodeEditor { vscroll, ..self }
}
pub fn auto_shrink(self, shrink: bool) -> Self {
CodeEditor {
desired_width: if shrink { 0.0 } else { self.desired_width },
..self
}
}
pub fn desired_width(self, width: f32) -> Self {
CodeEditor {
desired_width: width,
..self
}
}
pub fn stick_to_bottom(self, stick_to_bottom: bool) -> Self {
CodeEditor {
stick_to_bottom,
..self
}
}
pub fn hint_text<S: Into<String>>(self, hint_text: S) -> Self {
let hint_text = hint_text.into();
let rows = self.rows.max(hint_text.lines().count());
CodeEditor {
hint_text: Some(hint_text),
rows,
..self
}
}
#[cfg(feature = "egui")]
pub fn format_token(&self, ty: TokenType) -> egui::text::TextFormat {
format_token(&self.theme, self.fontsize, ty)
}
#[cfg(feature = "egui")]
fn numlines_show(&self, ui: &mut egui::Ui, text: &str) {
use egui::TextBuffer;
let total = if text.ends_with('\n') || text.is_empty() {
text.lines().count() + 1
} else {
text.lines().count()
}
.max(self.rows) as isize;
let max_indent = total
.to_string()
.len()
.max(!self.numlines_only_natural as usize * self.numlines_shift.to_string().len());
let mut counter = (1..=total)
.map(|i| {
let num = i + self.numlines_shift;
if num <= 0 && self.numlines_only_natural {
String::new()
} else {
let label = num.to_string();
format!(
"{}{label}",
" ".repeat(max_indent.saturating_sub(label.len()))
)
}
})
.collect::<Vec<String>>()
.join("\n");
#[allow(clippy::cast_precision_loss)]
let width = max_indent as f32
* self.fontsize
* 0.5
* !(total + self.numlines_shift <= 0 && self.numlines_only_natural) as u8 as f32;
let mut layouter = |ui: &egui::Ui, text_buffer: &dyn TextBuffer, _wrap_width: f32| {
let layout_job = egui::text::LayoutJob::single_section(
text_buffer.as_str().to_string(),
egui::TextFormat::simple(
egui::FontId::monospace(self.fontsize),
self.theme.type_color(TokenType::Comment(true)),
),
);
ui.fonts_mut(|f| f.layout_job(layout_job))
};
ui.add(
egui::TextEdit::multiline(&mut counter)
.id_source(format!("{}_numlines", self.id))
.font(egui::TextStyle::Monospace)
.interactive(false)
.frame(egui::Frame::NONE)
.desired_rows(self.rows)
.desired_width(width)
.layouter(&mut layouter),
);
}
#[cfg(feature = "egui")]
pub fn show_with_completer(
&mut self,
ui: &mut egui::Ui,
text: &mut dyn egui::TextBuffer,
syntax: &Syntax,
completer: &mut Completer,
) -> TextEditOutput {
completer.handle_input(ui.ctx());
let mut editor_output = self.show(ui, text, syntax);
completer.text_edit_id = Some(editor_output.response.id);
completer.show(syntax, &self.theme, self.fontsize, &mut editor_output);
editor_output
}
#[cfg(feature = "egui")]
pub fn show(
&mut self,
ui: &mut egui::Ui,
text: &mut dyn egui::TextBuffer,
syntax: &Syntax,
) -> TextEditOutput {
use egui::TextBuffer;
let mut text_edit_output: Option<TextEditOutput> = None;
let mut code_editor = |ui: &mut egui::Ui| {
let frame = egui::Frame::new().fill(self.theme.bg());
frame.show(ui, |ui| {
ui.horizontal_top(|h| {
self.theme.modify_style(h, self.fontsize);
if self.numlines {
self.numlines_show(h, text.as_str());
}
egui::ScrollArea::horizontal()
.id_salt(format!("{}_inner_scroll", self.id))
.show(h, |ui| {
use crate::highlighting::Links;
let mut links_ranges = Links::default();
let mut layouter =
|ui: &egui::Ui, text_buffer: &dyn TextBuffer, wrap_width: f32| {
let text_str = text_buffer.as_str();
let (mut layout_job, links) =
highlight(ui.ctx(), self, text_str, syntax);
links_ranges = links;
if !self.numlines && self.wrap {
layout_job.wrap =
egui::text::TextWrapping::wrap_at_width(wrap_width);
}
ui.fonts_mut(|f| f.layout_job(layout_job))
};
let mut text_edit = egui::TextEdit::multiline(text)
.id_source(&self.id)
.lock_focus(true)
.desired_rows(self.rows)
.desired_width(self.desired_width)
.layouter(&mut layouter);
if let Some(hint) = self.hint_text.as_ref() {
text_edit = text_edit.hint_text(hint);
}
let output = text_edit.show(ui);
if self.clickable_links {
handle_links(&output, &links_ranges);
}
text_edit_output = Some(output);
});
});
});
};
if self.vscroll {
egui::ScrollArea::vertical()
.id_salt(format!("{}_outer_scroll", self.id))
.stick_to_bottom(self.stick_to_bottom)
.show(ui, code_editor);
} else {
code_editor(ui);
}
text_edit_output.expect("TextEditOutput should exist at this point")
}
}
#[cfg(feature = "editor")]
#[cfg(feature = "egui")]
impl Editor for CodeEditor {
fn append(&self, job: &mut LayoutJob, token: &Token) {
if !token.buffer().is_empty() {
job.append(token.buffer(), 0.0, self.format_token(token.ty()));
}
}
}
#[cfg(feature = "egui")]
pub fn format_token(theme: &ColorTheme, fontsize: f32, ty: TokenType) -> egui::text::TextFormat {
let font_id = egui::FontId::monospace(fontsize);
let color = theme.type_color(ty);
let mut tf = egui::text::TextFormat::simple(font_id, color);
if ty == TokenType::Hyperlink {
tf.underline = Stroke::new(fontsize * 0.1, color);
}
tf
}