185 lines
6.3 KiB
Rust
Executable File
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")
|
|
}
|
|
} |