use std::{fs, io::ErrorKind, path::PathBuf, sync::Arc};
use self::read::{Reader, RevSearcher, Searcher};
use crate::{
data::RwData,
history::History,
input::{Cursors, InputMethod, KeyMap},
palette,
text::{IterCfg, Point, PrintCfg, Text},
ui::{Area, PushSpecs, Ui},
widgets::{ActiveWidget, PassiveWidget, Widget, WidgetCfg},
Context,
};
mod read;
pub struct FileCfg<U>
where
U: Ui,
{
text_op: TextOp,
builder: Arc<dyn Fn(File) -> Widget<U> + Send + Sync + 'static>,
cfg: PrintCfg,
specs: PushSpecs,
}
impl<U> FileCfg<U>
where
U: Ui,
{
pub(crate) fn new() -> Self {
FileCfg {
text_op: TextOp::NewBuffer,
builder: Arc::new(|file| Widget::active(file, RwData::new(KeyMap::new()))),
cfg: PrintCfg::default_for_files(),
specs: PushSpecs::above(),
}
}
pub(crate) fn build(self) -> (Widget<U>, Box<dyn Fn() -> bool>) {
let (text, path) = match self.text_op {
TextOp::NewBuffer => (Text::new(), Path::new_unset()),
TextOp::TakeText(text, path) => (text, path),
TextOp::OpenPath(path) => match path.canonicalize() {
Ok(path) => (Text::from_file(&path), Path::Set(path)),
Err(err) if matches!(err.kind(), ErrorKind::NotFound) => {
if path.parent().is_some_and(std::path::Path::exists) {
let parent = path.with_file_name("").canonicalize().unwrap();
let path = parent.with_file_name(path.file_name().unwrap());
(Text::new(), Path::Set(path))
} else {
(Text::new(), Path::new_unset())
}
}
Err(_) => (Text::new(), Path::new_unset()),
},
};
let text = {
let mut text = text;
use crate::{
palette::{self, Form},
text::{Marker, Tag},
};
let marker = Marker::new();
let form1 = palette::set_form("form1lmao", Form::new().red());
let form2 = palette::set_form("form2lmao", Form::new().on_blue());
for i in (0..text.len_bytes()).step_by(8) {
text.insert_tag(i, Tag::PushForm(form1), marker);
text.insert_tag(i + 4, Tag::PopForm(form1), marker);
}
text
};
let file = File {
path,
text,
cfg: self.cfg,
history: History::new(),
printed_lines: Vec::new(),
_readers: Vec::new(),
};
((self.builder)(file), Box::new(|| false))
}
pub(crate) fn open_path(self, path: PathBuf) -> Self {
Self {
text_op: TextOp::OpenPath(path),
..self
}
}
pub(crate) fn take_from_prev(self, prev: &mut File) -> Self {
let text = std::mem::take(&mut prev.text);
Self {
text_op: TextOp::TakeText(text, prev.path.clone()),
..self
}
}
pub(crate) fn set_print_cfg(&mut self, cfg: PrintCfg) {
self.cfg = cfg;
}
pub(crate) fn set_input(&mut self, input: impl InputMethod<U, Widget = File> + Clone) {
self.builder = Arc::new(move |file| Widget::active(file, RwData::new(input.clone())));
}
pub(crate) fn mut_print_cfg(&mut self) -> &mut PrintCfg {
&mut self.cfg
}
}
impl<U> WidgetCfg<U> for FileCfg<U>
where
U: Ui,
{
type Widget = File;
fn build(self, _context: Context<U>, _: bool) -> (Widget<U>, impl Fn() -> bool, PushSpecs) {
let specs = self.specs;
let (widget, checker) = self.build();
(widget, checker, specs)
}
}
impl<U> Default for FileCfg<U>
where
U: Ui,
{
fn default() -> Self {
Self::new()
}
}
impl<U> Clone for FileCfg<U>
where
U: Ui,
{
fn clone(&self) -> Self {
Self {
text_op: TextOp::NewBuffer,
builder: self.builder.clone(),
cfg: self.cfg.clone(),
specs: self.specs,
}
}
}
pub struct File {
path: Path,
text: Text,
cfg: PrintCfg,
history: History,
printed_lines: Vec<(usize, bool)>,
_readers: Vec<Box<dyn Reader>>,
}
impl File {
pub fn write(&self) -> Result<usize, String> {
if let Path::Set(path) = &self.path {
self.text
.write_to(std::io::BufWriter::new(
fs::File::create(path).map_err(|err| err.to_string())?,
))
.map_err(|err| err.to_string())
} else {
Err(String::from(
"The file has no associated path, and no path was given to write to",
))
}
}
pub fn write_to(&self, path: impl AsRef<str>) -> std::io::Result<usize> {
self.text
.write_to(std::io::BufWriter::new(fs::File::create(path.as_ref())?))
}
pub fn history_mut(&mut self) -> &mut History {
&mut self.history
}
pub fn text(&self) -> &Text {
&self.text
}
}
impl File {
pub fn search(&self) -> Searcher<'_> {
Searcher::new_at(Point::default(), self.text.iter().no_ghosts().no_conceals())
}
pub fn search_at(&self, point: Point) -> Searcher<'_> {
Searcher::new_at(point, self.text.iter_at(point).no_ghosts().no_conceals())
}
pub fn rev_search(&self) -> RevSearcher<'_> {
RevSearcher::new_at(
self.text.max_point(),
self.text.rev_iter().no_ghosts().no_conceals(),
)
}
pub fn rev_search_at(&self, point: Point) -> RevSearcher<'_> {
RevSearcher::new_at(
point,
self.text.rev_iter_at(point).no_ghosts().no_conceals(),
)
}
pub fn path(&self) -> String {
match &self.path {
Path::Set(path) => path.to_string_lossy().to_string(),
Path::UnSet(id) => format!("*scratch file*#{id}"),
}
}
pub fn path_set(&self) -> Option<String> {
match &self.path {
Path::Set(path) => Some(path.to_string_lossy().to_string()),
Path::UnSet(_) => None,
}
}
pub fn name(&self) -> String {
match &self.path {
Path::Set(path) => path.file_name().unwrap().to_string_lossy().to_string(),
Path::UnSet(id) => format!("*scratch file #{id}*"),
}
}
pub fn name_set(&self) -> Option<String> {
match &self.path {
Path::Set(path) => Some(path.file_name().unwrap().to_string_lossy().to_string()),
Path::UnSet(_) => None,
}
}
pub fn len_bytes(&self) -> usize {
self.text.len_bytes()
}
pub fn len_chars(&self) -> usize {
self.text.len_chars()
}
pub fn len_lines(&self) -> usize {
self.text.len_lines()
}
pub fn printed_lines(&self) -> &[(usize, bool)] {
&self.printed_lines
}
}
impl File {
pub fn add_moment(&mut self) {
self.history.add_moment()
}
pub fn redo(&mut self, area: &impl Area, cursors: &mut Cursors) {
self.history.redo(&mut self.text, area, &self.cfg, cursors)
}
pub fn undo(&mut self, area: &impl Area, cursors: &mut Cursors) {
self.history.undo(&mut self.text, area, &self.cfg, cursors)
}
pub fn mut_text_and_history(&mut self) -> (&mut Text, &mut History) {
(&mut self.text, &mut self.history)
}
}
impl<U> PassiveWidget<U> for File
where
U: Ui,
{
fn build(_context: Context<U>, _: bool) -> (Widget<U>, impl Fn() -> bool, crate::ui::PushSpecs)
where
Self: Sized,
{
let (widget, checker) = FileCfg::new().build();
(widget, checker, PushSpecs::above())
}
fn update(&mut self, _area: &U::Area) {}
fn text(&self) -> &Text {
&self.text
}
fn print_cfg(&self) -> &PrintCfg {
&self.cfg
}
fn once(_context: crate::Context<U>) {}
fn print(&mut self, area: &<U as Ui>::Area) {
let (start, _) = area.top_left();
let mut last_line = area
.rev_print_iter(self.text.rev_iter_at(start), IterCfg::new(&self.cfg))
.find_map(|(caret, item)| caret.wrap.then_some(item.line()));
self.printed_lines.clear();
let printed_lines = &mut self.printed_lines;
area.print_with(
&self.text,
&self.cfg,
palette::painter(),
move |caret, item| {
if caret.wrap {
let line = item.line();
let wrapped = last_line.is_some_and(|ll| ll == line);
last_line = Some(line);
printed_lines.push((line, wrapped));
}
},
)
}
}
impl<U> ActiveWidget<U> for File
where
U: Ui,
{
fn mut_text(&mut self) -> &mut Text {
&mut self.text
}
fn on_focus(&mut self, _area: &<U as Ui>::Area) {}
fn on_unfocus(&mut self, _area: &<U as Ui>::Area) {}
}
unsafe impl Send for File {}
unsafe impl Sync for File {}
#[derive(Clone)]
enum Path {
Set(PathBuf),
UnSet(usize),
}
impl Path {
fn new_unset() -> Path {
use std::sync::atomic::{AtomicUsize, Ordering};
static UNSET_COUNT: AtomicUsize = AtomicUsize::new(1);
Path::UnSet(UNSET_COUNT.fetch_add(1, Ordering::Relaxed))
}
}
enum TextOp {
NewBuffer,
TakeText(Text, Path),
OpenPath(PathBuf),
}