From 9bc80a7b088ce5816b07473c349815baf050c875 Mon Sep 17 00:00:00 2001 From: Silen Locatelli <88315530+SilenLoc@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:02:37 +0100 Subject: [PATCH] feat: use egui code editor --- .vscode/settings.json | 5 ++ Cargo.toml | 1 + src/app.rs | 2 +- src/editor/highlighter.rs | 138 -------------------------------------- src/editor/mod.rs | 69 ++++++------------- src/editor/parser.rs | 2 +- src/editor/syntax.rs | 49 ++++++++++++++ 7 files changed, 76 insertions(+), 190 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 src/editor/highlighter.rs create mode 100644 src/editor/syntax.rs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..352a626 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.linkedProjects": [ + "./Cargo.toml" + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 4ac7e29..e02910b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ egui = { version = "0.26", default-features = true } eframe = { version = "0.26", default-features = true, features = [ "default_fonts", # Embed the default egui fonts. ] } +egui_code_editor = { version = "0.2.2"} ehttp = { version = "0.5"} poll-promise = { version = "0.3", default-features = false } serde_json = "1.0" diff --git a/src/app.rs b/src/app.rs index 25728df..473e6b9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -70,7 +70,7 @@ fn create_tree() -> egui_tiles::Tree { let root = tiles.insert_tab_tile(tabs); - egui_tiles::Tree::new(egui::Id::new("uniqueGlobal"),root, tiles, ) + egui_tiles::Tree::new(egui::Id::new("uniqueGlobal"), root, tiles) } struct Pane { diff --git a/src/editor/highlighter.rs b/src/editor/highlighter.rs deleted file mode 100644 index 718aeef..0000000 --- a/src/editor/highlighter.rs +++ /dev/null @@ -1,138 +0,0 @@ -use egui::text::LayoutJob; -use egui::{Color32, TextFormat}; - -#[derive(serde::Deserialize, serde::Serialize, Default)] -struct Highlighter {} - -impl Highlighter { - #[allow(clippy::unused_self, clippy::unnecessary_wraps)] - fn highlight(&self, theme: &CodeTheme, mut text: &str) -> LayoutJob { - let mut job = LayoutJob::default(); - - while !text.is_empty() { - if text.starts_with('#') { - let end = text.find('\n').unwrap_or(text.len()); - job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone()); - text = &text[end..]; - } else if text.starts_with('"') { - let end = text[1..] - .find('"') - .map(|i| i + 2) - .or_else(|| text.find('\n')) - .unwrap_or(text.len()); - job.append( - &text[..end], - 0.0, - theme.formats[TokenType::StringLiteral].clone(), - ); - text = &text[end..]; - } else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) { - let end = text[1..] - .find(|c: char| !c.is_ascii_alphanumeric()) - .map_or_else(|| text.len(), |i| i + 1); - let word = &text[..end]; - - if is_http(word) { - job.append(word, 0.0, theme.formats[TokenType::Http].clone()); - text = &text[end..]; - } else if is_keyword(word) { - job.append(word, 0.0, theme.formats[TokenType::Keyword].clone()); - text = &text[end..]; - } else { - job.append(word, 0.0, theme.formats[TokenType::Literal].clone()); - text = &text[end..]; - }; - } else if text.starts_with(|c: char| c.is_ascii_whitespace()) { - let end = text[1..] - .find(|c: char| !c.is_ascii_whitespace()) - .map_or_else(|| text.len(), |i| i + 1); - job.append( - &text[..end], - 0.0, - theme.formats[TokenType::Whitespace].clone(), - ); - text = &text[end..]; - } else { - let mut it = text.char_indices(); - it.next(); - let end = it.next().map_or(text.len(), |(idx, _chr)| idx); - job.append( - &text[..end], - 0.0, - theme.formats[TokenType::Punctuation].clone(), - ); - text = &text[end..]; - } - } - - job - } -} - -#[derive(Clone, Copy, PartialEq, serde::Deserialize, serde::Serialize, enum_map::Enum)] -enum TokenType { - Comment, - Keyword, - Http, - Literal, - StringLiteral, - Punctuation, - Whitespace, -} - -fn is_keyword(word: &str) -> bool { - matches!(word, |"GET"| "PATCH" - | "POST" - | "PUT" - | "Asserts" - | "Captures") -} - -fn is_http(word: &str) -> bool { - word == "HTTP" -} - -#[allow(clippy::unsafe_derive_deserialize)] -#[derive(Clone, Hash, PartialEq, serde::Deserialize, serde::Serialize)] -#[serde(default)] -pub struct CodeTheme { - dark_mode: bool, - #[serde(skip)] - formats: enum_map::EnumMap, -} - -impl Default for CodeTheme { - fn default() -> Self { - Self::dark() - } -} - -impl CodeTheme { - pub fn dark() -> Self { - let font_id = egui::FontId::monospace(15.0); - Self { - dark_mode: true, - formats: enum_map::enum_map![ - TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::from_gray(120)), - TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(255, 100, 100)), - TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(87, 165, 171)), - TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(109, 147, 226)), - TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::LIGHT_GRAY), - TokenType::Http => TextFormat::simple(font_id.clone(), Color32::from_rgb(171, 32, 253)), - TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT), - ], - } - } -} - -pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str) -> LayoutJob { - impl egui::util::cache::ComputerMut<(&CodeTheme, &str), LayoutJob> for Highlighter { - fn compute(&mut self, (theme, code): (&CodeTheme, &str)) -> LayoutJob { - self.highlight(theme, code) - } - } - - type HighlightCache = egui::util::cache::FrameCache; - - ctx.memory_mut(|mem| mem.caches.cache::().get((theme, code))) -} diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 5e1b78d..df5f1e5 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,17 +1,16 @@ use eframe::egui; +use egui_code_editor::{CodeEditor, ColorTheme}; -use self::{ - highlighter::{highlight, CodeTheme}, - parser::Parser, -}; +use self::parser::Parser; -mod highlighter; mod parser; +mod syntax; pub struct Editor { text: String, parser: Parser, marker: usize, + syntax: egui_code_editor::Syntax, } const ABSTRACT: &str = r"# ------------------------------------------------------------------- @@ -31,14 +30,13 @@ impl Default for Editor { text: ABSTRACT.to_owned(), parser: Parser::default(), marker: usize::default(), + syntax: syntax::hurl(), } } } - impl Editor { pub fn render(&mut self, ui: &mut egui::Ui) { self.parser.parse(&self.text); - let theme = CodeTheme::default(); if let Some(err) = self.parser.try_to_get_err() { self.marker = err.pos.line; @@ -63,11 +61,6 @@ impl Editor { ui.add_space(1.0); ui.vertical(|ui| { - let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { - let layout_job = highlight(ui.ctx(), &theme, string); - ui.fonts(|f| f.layout_job(layout_job)) - }; - egui::ScrollArea::vertical() .id_source("some inner 3") .min_scrolled_height(750.0) @@ -76,48 +69,24 @@ impl Editor { .show(ui, |ui| { ui.push_id("second_some", |ui| { ui.horizontal_top(|ui| { - let mut current: String = self - .text - .lines() - .take(1000) - .enumerate() - .map(|(s, _)| { - (s + 1).to_string() - + { - if s + 1 == self.marker { - " >" - } else { - "" - } - } - + "\n" - }) - .collect(); - - egui::TextEdit::multiline(&mut current) - .font(egui::TextStyle::Monospace) - .interactive(false) - .desired_width(60.0) - .code_editor() - .font(egui::FontId::monospace(15.0)) - .show(ui); - - egui::TextEdit::multiline(&mut self.text) - .font(egui::TextStyle::Monospace) - .desired_width(f32::INFINITY) - .code_editor() - .lock_focus(true) - .layouter(&mut layouter) - .show(ui); + let mut editor = CodeEditor::default() + .id_source("code editor") + .with_rows(10) + .with_fontsize(14.0) + .with_theme(ColorTheme::SONOKAI) + .with_syntax(self.syntax.to_owned()) + .with_numlines(true) + .vscroll(true); + editor.show(ui, &mut self.text); }); }); - }); - if let Err(err) = self.parser.try_to_get_file() { - render_error(&err, ui); - } + if let Err(err) = self.parser.try_to_get_file() { + render_error(&err, ui); + } - ui.add_space(10.0); + ui.add_space(10.0); + }); }); }); } diff --git a/src/editor/parser.rs b/src/editor/parser.rs index 95cd0cb..a96705c 100644 --- a/src/editor/parser.rs +++ b/src/editor/parser.rs @@ -85,4 +85,4 @@ pub fn post(url: impl ToString, body: Vec) -> ehttp::Request { ]), mode: ehttp::Mode::NoCors, } -} \ No newline at end of file +} diff --git a/src/editor/syntax.rs b/src/editor/syntax.rs new file mode 100644 index 0000000..53680cf --- /dev/null +++ b/src/editor/syntax.rs @@ -0,0 +1,49 @@ +use egui_code_editor::Syntax; +use std::collections::BTreeSet; + +pub fn hurl() -> Syntax { + Syntax { + language: "Hurl", + case_sensitive: true, + comment: "#", + comment_multiline: ["#", "#"], + hyperlinks: BTreeSet::from([]), + keywords: BTreeSet::from([ + "jsonpath", + "count", + "==", + ">=", + "<=", + "<", + ">", + "!=", + "not", + "isString", + "isCollection", + "isDate", + "isBoolean", + "isFloat", + "isInteger", + "includes", + "isEmpty", + "exists", + "matches", + "contains", + "endsWith", + "startsWith", + "", + ]), + types: BTreeSet::from([ + "[Captures]", + "[Asserts]", + "[FormParams]", + "[Captures]", + "[Asserts]", + "[Asserts]", + "HTTP", + ]), + special: BTreeSet::from([ + "GET", "POST", "HTTP", "HEAD", "PUT", "DELETE", "OPTIONS", "TRACE", "PATCH", + ]), + } +}