217 lines
6.5 KiB
Rust
217 lines
6.5 KiB
Rust
|
|
//! Code editor panel
|
||
|
|
//!
|
||
|
|
//! Provides a multi-line text editor for writing Geolog code.
|
||
|
|
|
||
|
|
use eframe::egui;
|
||
|
|
|
||
|
|
use crate::gui::state::GuiState;
|
||
|
|
|
||
|
|
/// Code editor panel
|
||
|
|
pub struct EditorPanel {
|
||
|
|
/// Whether to show line numbers
|
||
|
|
show_line_numbers: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl EditorPanel {
|
||
|
|
pub fn new() -> Self {
|
||
|
|
Self {
|
||
|
|
show_line_numbers: true,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn show(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||
|
|
// Header with title and buttons
|
||
|
|
ui.horizontal(|ui| {
|
||
|
|
ui.heading("Editor");
|
||
|
|
|
||
|
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||
|
|
if ui.button("Clear").clicked() {
|
||
|
|
state.editor_content.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
if ui.button("Run").clicked() {
|
||
|
|
state.execute_editor();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Show current file if any
|
||
|
|
if let Some(path) = &state.current_file {
|
||
|
|
ui.label(format!(
|
||
|
|
"{}",
|
||
|
|
path.file_name()
|
||
|
|
.map(|s| s.to_string_lossy())
|
||
|
|
.unwrap_or_default()
|
||
|
|
));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
ui.separator();
|
||
|
|
|
||
|
|
// Editor area with syntax highlighting
|
||
|
|
egui::ScrollArea::vertical()
|
||
|
|
.auto_shrink([false, false])
|
||
|
|
.show(ui, |ui| {
|
||
|
|
self.show_editor_content(ui, state);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
fn show_editor_content(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||
|
|
// Use a monospace font for code
|
||
|
|
let font_id = egui::FontId::monospace(14.0);
|
||
|
|
|
||
|
|
// Calculate line numbers if needed
|
||
|
|
let line_count = state.editor_content.lines().count().max(1);
|
||
|
|
let line_number_width = if self.show_line_numbers {
|
||
|
|
let digits = (line_count as f32).log10().floor() as usize + 1;
|
||
|
|
digits.max(2) as f32 * 10.0 + 8.0
|
||
|
|
} else {
|
||
|
|
0.0
|
||
|
|
};
|
||
|
|
|
||
|
|
ui.horizontal(|ui| {
|
||
|
|
// Line numbers column
|
||
|
|
if self.show_line_numbers {
|
||
|
|
ui.allocate_ui_with_layout(
|
||
|
|
egui::vec2(line_number_width, ui.available_height()),
|
||
|
|
egui::Layout::top_down(egui::Align::RIGHT),
|
||
|
|
|ui| {
|
||
|
|
ui.style_mut().visuals.override_text_color =
|
||
|
|
Some(egui::Color32::GRAY);
|
||
|
|
|
||
|
|
for i in 1..=line_count {
|
||
|
|
ui.label(
|
||
|
|
egui::RichText::new(format!("{}", i))
|
||
|
|
.font(font_id.clone())
|
||
|
|
.color(egui::Color32::GRAY),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
ui.separator();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Main editor area
|
||
|
|
let text_edit = egui::TextEdit::multiline(&mut state.editor_content)
|
||
|
|
.font(font_id)
|
||
|
|
.code_editor()
|
||
|
|
.desired_width(f32::INFINITY)
|
||
|
|
.desired_rows(20)
|
||
|
|
.lock_focus(true);
|
||
|
|
|
||
|
|
let response = ui.add(text_edit);
|
||
|
|
|
||
|
|
// Handle keyboard shortcuts
|
||
|
|
if response.has_focus() {
|
||
|
|
let modifiers = ui.input(|i| i.modifiers);
|
||
|
|
if modifiers.ctrl || modifiers.command {
|
||
|
|
if ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||
|
|
state.execute_editor();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Default for EditorPanel {
|
||
|
|
fn default() -> Self {
|
||
|
|
Self::new()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Simple syntax highlighting for Geolog code
|
||
|
|
/// Returns a layouter function for egui's code_editor
|
||
|
|
#[allow(dead_code)]
|
||
|
|
fn geolog_highlighter(
|
||
|
|
_ui: &egui::Ui,
|
||
|
|
text: &str,
|
||
|
|
_wrap_width: f32,
|
||
|
|
) -> egui::text::LayoutJob {
|
||
|
|
let mut job = egui::text::LayoutJob::default();
|
||
|
|
|
||
|
|
let keywords = [
|
||
|
|
"theory", "instance", "query", "forall", "exists", "Sort", "Prop", "true", "false",
|
||
|
|
];
|
||
|
|
|
||
|
|
let font_id = egui::FontId::monospace(14.0);
|
||
|
|
let default_color = egui::Color32::WHITE;
|
||
|
|
let keyword_color = egui::Color32::from_rgb(86, 156, 214); // Blue
|
||
|
|
let comment_color = egui::Color32::from_rgb(106, 153, 85); // Green
|
||
|
|
let string_color = egui::Color32::from_rgb(206, 145, 120); // Orange
|
||
|
|
|
||
|
|
let mut chars = text.char_indices().peekable();
|
||
|
|
|
||
|
|
while let Some((i, c)) = chars.next() {
|
||
|
|
// Check for comments
|
||
|
|
if c == '/' && chars.peek().map(|(_, c)| *c) == Some('/') {
|
||
|
|
// Line comment - consume until end of line
|
||
|
|
let start = i;
|
||
|
|
while let Some((_, c)) = chars.next() {
|
||
|
|
if c == '\n' {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let end = chars.peek().map(|(i, _)| *i).unwrap_or(text.len());
|
||
|
|
job.append(
|
||
|
|
&text[start..end],
|
||
|
|
0.0,
|
||
|
|
egui::TextFormat::simple(font_id.clone(), comment_color),
|
||
|
|
);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for strings
|
||
|
|
if c == '"' {
|
||
|
|
let start = i;
|
||
|
|
while let Some((_, c)) = chars.next() {
|
||
|
|
if c == '"' {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if c == '\\' {
|
||
|
|
chars.next(); // Skip escaped character
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let end = chars.peek().map(|(i, _)| *i).unwrap_or(text.len());
|
||
|
|
job.append(
|
||
|
|
&text[start..end],
|
||
|
|
0.0,
|
||
|
|
egui::TextFormat::simple(font_id.clone(), string_color),
|
||
|
|
);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for identifiers/keywords
|
||
|
|
if c.is_alphabetic() || c == '_' {
|
||
|
|
let start = i;
|
||
|
|
while chars.peek().map(|(_, c)| c.is_alphanumeric() || *c == '_') == Some(true) {
|
||
|
|
chars.next();
|
||
|
|
}
|
||
|
|
let end = chars.peek().map(|(i, _)| *i).unwrap_or(text.len());
|
||
|
|
let word = &text[start..end];
|
||
|
|
|
||
|
|
let color = if keywords.contains(&word) {
|
||
|
|
keyword_color
|
||
|
|
} else {
|
||
|
|
default_color
|
||
|
|
};
|
||
|
|
|
||
|
|
job.append(
|
||
|
|
word,
|
||
|
|
0.0,
|
||
|
|
egui::TextFormat::simple(font_id.clone(), color),
|
||
|
|
);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Default: single character
|
||
|
|
job.append(
|
||
|
|
&text[i..i + c.len_utf8()],
|
||
|
|
0.0,
|
||
|
|
egui::TextFormat::simple(font_id.clone(), default_color),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
job
|
||
|
|
}
|