use iced::widget::text_editor; use std::fs; use std::path::PathBuf; pub struct EditorTab { pub title: String, pub content: text_editor::Content, pub is_dirty: bool, pub file_path: Option, pub override_syntax: Option, pub symbols: Vec<(usize, String)>, pub bookmarks: Vec, } 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 } pub fn parse_symbols(&mut self) { self.symbols.clear(); for (i, line) in self.content.text().lines().enumerate() { let t = line.trim_start(); if t.starts_with("fn ") || t.starts_with("def ") || t.starts_with("class ") || t.starts_with("struct ") || t.starts_with("impl ") || t.starts_with("enum ") || t.starts_with("trait ") || t.starts_with("mod ") || t.starts_with("pub fn ") || t.starts_with("pub struct ") || t.starts_with("pub enum ") || t.starts_with("pub trait ") || t.starts_with("pub mod ") || t.starts_with("macro_rules!") { let name = t .split('{') .next() .unwrap_or(t) .split('(') .next() .unwrap_or(t) .split('!') .next() .unwrap_or(t) .trim() .to_string(); self.symbols.push((i + 1, name)); } } } 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 { 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()) } pub fn save_as(&mut self, new_path: &str) -> Result { 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() } pub fn get_syntax(&self) -> &'static str { if let Some(ref syn) = self.override_syntax { return match syn.as_str() { "Rust" => "rs", "Python" => "py", "C/C++" | "C" | "C++" => "cpp", "Shell" | "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", _ => "txt", }; } let name = self.title.to_lowercase(); if name.contains("dockerfile") || name.contains("containerfile") { return "dockerfile"; } if name.contains("nginx") { return "nginx"; } if name.contains("apache") || name.contains("httpd") { return "apache"; } if name.ends_with("rc") { return "sh"; } if name.ends_with("makefile") || name == "makefile" { return "makefile"; } let parts: Vec<&str> = name.split('.').collect(); if let Some(ext) = parts.last() { match *ext { "rs" => "rs", "sh" | "bash" | "zsh" | "command" => "sh", "ps1" | "psm1" => "ps1", "bat" | "cmd" | "dosbat" => "bat", "vbs" | "vbe" | "wscript" | "vbasic" => "vbs", "conf" | "cfg" | "ini" | "service" | "target" => "ini", "yaml" | "yml" => "yaml", "toml" => "toml", "json" => "json", "xml" => "xml", "html" | "htm" | "xhtml" => "html", "css" | "scss" | "sass" | "less" => "css", "js" | "mjs" | "cjs" => "js", "ts" | "tsx" => "ts", "py" | "pyw" => "py", "c" | "h" => "c", "cpp" | "hpp" | "cc" | "cxx" | "hxx" => "cpp", "go" => "go", "rb" => "rb", "php" | "phtml" => "php", "java" => "java", "cs" => "cs", "sql" => "sql", "md" | "markdown" => "md", "r" => "r", "lua" => "lua", "pl" | "pm" => "pl", "ex" | "exs" => "ex", "hs" => "hs", "scala" => "scala", "swift" => "swift", "kt" | "kts" => "kt", "dart" => "dart", _ => "txt", } } else { "txt" } } }