Scitano/src/editor.rs

185 lines
6.3 KiB
Rust
Executable File

use iced::widget::text_editor;
use std::fs;
use std::path::PathBuf;
// ── Symbol parser prefixes (SEI CERT/MISRA: data-driven, no nested ifs) ──
const SYMBOL_PREFIXES: &[&str] = &[
"pub fn ", "pub struct ", "pub enum ", "pub trait ", "pub mod ",
"fn ", "def ", "class ", "struct ", "impl ", "enum ", "trait ", "mod ",
"macro_rules!",
];
// ── Override syntax → highlighter token map (PEP868-style table-driven) ──
const OVERRIDE_SYNTAX_MAP: &[(&str, &str)] = &[
("Rust", "rs"), ("Python", "py"), ("C/C++", "cpp"), ("C", "cpp"),
("Shell", "sh"), ("Bash", "sh"), ("JavaScript", "js"),
("TypeScript", "ts"), ("Markdown", "md"), ("HTML", "html"),
("CSS", "css"), ("JSON", "json"), ("XML", "xml"), ("YAML", "yaml"),
("Go", "go"), ("Ruby", "rb"), ("PHP", "php"), ("Java", "java"),
("C#", "cs"), ("SQL", "sql"),
];
// ── Special filename → syntax map (array, no nested ifs) ──
const SPECIAL_FILENAMES: &[(&str, &str)] = &[
("dockerfile", "dockerfile"), ("containerfile", "dockerfile"),
("nginx", "nginx"), ("apache", "apache"), ("httpd", "apache"),
];
// ── Extension → syntax map (single flat table, no nested ifs) ──
const EXT_SYNTAX_MAP: &[(&str, &str)] = &[
("rs", "rs"),
("sh", "sh"), ("bash", "sh"), ("zsh", "sh"), ("command", "sh"),
("ps1", "ps1"), ("psm1", "ps1"),
("bat", "bat"), ("cmd", "bat"), ("dosbat", "bat"),
("vbs", "vbs"), ("vbe", "vbs"), ("wscript", "vbs"), ("vbasic", "vbs"),
("conf", "ini"), ("cfg", "ini"), ("ini", "ini"),
("service", "ini"), ("target", "ini"),
("yaml", "yaml"), ("yml", "yaml"),
("toml", "toml"),
("json", "json"),
("xml", "xml"),
("html", "html"), ("htm", "html"), ("xhtml", "html"),
("css", "css"), ("scss", "css"), ("sass", "css"), ("less", "css"),
("js", "js"), ("mjs", "js"), ("cjs", "js"),
("ts", "ts"), ("tsx", "ts"),
("py", "py"), ("pyw", "py"),
("c", "c"), ("h", "c"),
("cpp", "cpp"), ("hpp", "cpp"), ("cc", "cpp"), ("cxx", "cpp"), ("hxx", "cpp"),
("go", "go"),
("rb", "rb"),
("php", "php"), ("phtml", "php"),
("java", "java"),
("cs", "cs"),
("sql", "sql"),
("md", "md"), ("markdown", "md"),
("r", "r"),
("lua", "lua"),
("pl", "pl"), ("pm", "pl"),
("ex", "ex"), ("exs", "ex"),
("hs", "hs"),
("scala", "scala"),
("swift", "swift"),
("kt", "kt"), ("kts", "kt"),
("dart", "dart"),
];
pub struct EditorTab {
pub title: String,
pub content: text_editor::Content,
pub is_dirty: bool,
pub file_path: Option<PathBuf>,
pub override_syntax: Option<String>,
pub symbols: Vec<(usize, String)>,
pub bookmarks: Vec<usize>,
}
impl EditorTab {
pub fn new(title: &str, initial_text: &str) -> Self {
let mut tab = Self {
title: title.to_string(),
content: text_editor::Content::with_text(initial_text),
is_dirty: false,
file_path: None,
override_syntax: None,
symbols: vec![],
bookmarks: vec![],
};
tab.parse_symbols();
tab
}
/// Parse symbols from buffer lines using prefix table.
/// No for/while loops — uses iterator chains with functional combinators.
pub fn parse_symbols(&mut self) {
self.symbols = self.content.text().lines()
.enumerate()
.filter_map(|(i, line)| {
let t = line.trim_start();
let matched = SYMBOL_PREFIXES.iter().any(|p| t.starts_with(p));
if !matched { return None; }
let name = t
.split('{').next().unwrap_or(t)
.split('(').next().unwrap_or(t)
.split('!').next().unwrap_or(t)
.trim()
.to_string();
Some((i + 1, name))
})
.collect();
}
pub fn toggle_bookmark(&mut self, line: usize) {
if self.bookmarks.contains(&line) {
self.bookmarks.retain(|&b| b != line);
} else {
self.bookmarks.push(line);
self.bookmarks.sort();
}
}
pub fn save(&mut self) -> Result<String, std::io::Error> {
let path = self
.file_path
.clone()
.unwrap_or_else(|| PathBuf::from(&self.title));
let text = self.content.text();
fs::write(&path, text)?;
self.is_dirty = false;
self.file_path = Some(path.clone());
Ok(path.to_string_lossy().to_string())
}
#[allow(dead_code)]
pub fn save_as(&mut self, new_path: &str) -> Result<String, std::io::Error> {
let path = PathBuf::from(new_path);
let text = self.content.text();
fs::write(&path, text)?;
self.is_dirty = false;
self.file_path = Some(path.clone());
self.title = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
Ok(path.to_string_lossy().to_string())
}
pub fn line_count(&self) -> usize {
self.content.text().lines().count().max(1)
}
pub fn byte_size(&self) -> usize {
self.content.text().len()
}
/// Determine syntax highlighter token from file extension or override.
/// Fully table-driven — zero nested if/else, zero match arms on strings.
pub fn get_syntax(&self) -> &'static str {
// 1. Check override (O(1) lookup via iterator find)
if let Some(ref syn) = self.override_syntax {
return OVERRIDE_SYNTAX_MAP
.iter()
.find(|(k, _)| *k == syn.as_str())
.map(|(_, v)| *v)
.unwrap_or("txt");
}
let name = self.title.to_lowercase();
// 2. Special filenames (Dockerfile, nginx, etc.)
if let Some((_, v)) = SPECIAL_FILENAMES.iter().find(|(pat, _)| name.contains(pat)) {
return v;
}
// 3. Suffix-based rules (.rc, Makefile)
if name.ends_with("rc") { return "sh"; }
if name.ends_with("makefile") || name == "makefile" { return "makefile"; }
// 4. Extension lookup from flat table
name.split('.')
.next_back()
.and_then(|ext| EXT_SYNTAX_MAP.iter().find(|(k, _)| *k == ext))
.map(|(_, v)| *v)
.unwrap_or("txt")
}
}