1use std::{fs, marker::PhantomData, path::PathBuf};
15
16use self::parser::InnerParsers;
17pub use self::parser::{FileParts, FileSnapshot, Parser, ParserBox, ParserCfg, Parsers};
18use crate::{
19 cfg::{NewLine, PrintCfg, ScrollOff, TabStops, WordChars, WrapMethod},
20 context::{self, Cache, Handle},
21 data::Pass,
22 form::Painter,
23 hook::{self, FileWritten},
24 mode::{Selection, Selections},
25 text::{Bytes, Text, txt},
26 ui::{Area, BuildInfo, PushSpecs, Ui, Widget, WidgetCfg},
27};
28
29mod parser;
30
31#[derive(Default)]
33#[doc(hidden)]
34pub struct FileCfg<U: Ui> {
35 text_op: TextOp,
36 print_cfg: PrintCfg,
37 add_parsers: Option<Box<dyn FnOnce(&mut File<U>)>>,
38}
39
40impl<U: Ui> FileCfg<U> {
41 pub(crate) fn new() -> Self {
43 FileCfg {
44 text_op: TextOp::NewBuffer,
45 print_cfg: PrintCfg::default_for_input(),
46 add_parsers: None,
47 }
48 }
49
50 pub fn with_parser(mut self, parser_cfg: impl ParserCfg<U> + 'static) -> Self {
52 let add_parsers = std::mem::take(&mut self.add_parsers);
53 self.add_parsers = Some(Box::new(move |file| {
54 if let Some(prev_add_parsers) = add_parsers {
55 prev_add_parsers(file)
56 }
57
58 if let Err(err) = file.parsers.add(file, parser_cfg) {
59 context::error!("{err}");
60 }
61 }));
62
63 self
64 }
65
66 pub fn print_cfg(&mut self) -> &mut PrintCfg {
73 &mut self.print_cfg
74 }
75
76 pub const fn dont_wrap(mut self) -> Self {
78 self.print_cfg.dont_wrap();
79 self
80 }
81
82 pub const fn wrap_on_edge(mut self) -> Self {
84 self.print_cfg.wrap_method = WrapMethod::Edge;
85 self
86 }
87
88 pub const fn wrap_on_word(mut self) -> Self {
92 self.print_cfg.wrap_method = WrapMethod::Edge;
93 self
94 }
95
96 pub const fn wrap_at(mut self, cap: u8) -> Self {
104 self.print_cfg.wrap_method = WrapMethod::Capped(cap);
105 self
106 }
107
108 pub const fn indent_wraps(mut self, value: bool) -> Self {
110 self.print_cfg.indent_wrapped = value;
111 self
112 }
113
114 pub const fn tabstop(mut self, tabstop: u8) -> Self {
116 self.print_cfg.tab_stops = TabStops(tabstop);
117 self
118 }
119
120 pub const fn new_line_as(mut self, char: char) -> Self {
122 self.print_cfg.new_line = NewLine::AlwaysAs(char);
123 self
124 }
125
126 pub const fn trailing_new_line_as(mut self, char: char) -> Self {
129 self.print_cfg.new_line = NewLine::AfterSpaceAs(char);
130 self
131 }
132
133 pub const fn scrolloff(mut self, x: u8, y: u8) -> Self {
135 self.print_cfg.scrolloff = ScrollOff { x, y };
136 self
137 }
138
139 pub const fn x_scrolloff(mut self, x: u8) -> Self {
141 self.print_cfg.scrolloff = ScrollOff { y: self.print_cfg.scrolloff.y, x };
142 self
143 }
144
145 pub const fn y_scrolloff(mut self, y: u8) -> Self {
147 self.print_cfg.scrolloff = ScrollOff { y, x: self.print_cfg.scrolloff.x };
148 self
149 }
150
151 pub const fn word_chars(mut self, word_chars: WordChars) -> Self {
153 self.print_cfg.word_chars = word_chars;
154 self
155 }
156
157 pub const fn forced_horizontal_scrolloff(mut self, value: bool) -> Self {
170 self.print_cfg.force_scrolloff = value;
171 self
172 }
173
174 pub fn path_set(&self) -> Option<String> {
178 match &self.text_op {
179 TextOp::TakeBuf(_, PathKind::NotSet(_), _) | TextOp::NewBuffer => None,
180 TextOp::TakeBuf(_, PathKind::SetExists(path) | PathKind::SetAbsent(path), _)
181 | TextOp::OpenPath(path) => Some(path.to_str()?.to_string()),
182 }
183 }
184
185 pub(crate) fn open_path(self, path: PathBuf) -> Self {
187 Self { text_op: TextOp::OpenPath(path), ..self }
188 }
189
190 pub(crate) fn take_from_prev(
192 self,
193 bytes: Bytes,
194 pk: PathKind,
195 has_unsaved_changes: bool,
196 ) -> Self {
197 Self {
198 text_op: TextOp::TakeBuf(bytes, pk, has_unsaved_changes),
199 ..self
200 }
201 }
202}
203
204impl<U: Ui> WidgetCfg<U> for FileCfg<U> {
205 type Widget = File<U>;
206
207 fn build(self, _: &mut Pass, _: BuildInfo<U>) -> (Self::Widget, PushSpecs) {
208 let (text, path) = match self.text_op {
209 TextOp::NewBuffer => (Text::new_with_history(), PathKind::new_unset()),
210 TextOp::TakeBuf(bytes, pk, has_unsaved_changes) => match &pk {
211 PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
212 let selections = {
213 let cursor = Cache::new().load(path).unwrap_or_default();
214 Selections::new(cursor)
215 };
216 let text = Text::from_file(bytes, selections, path, has_unsaved_changes);
217 (text, pk)
218 }
219 PathKind::NotSet(_) => (
220 Text::from_bytes(bytes, Selections::new(Selection::default()), true),
221 pk,
222 ),
223 },
224 TextOp::OpenPath(path) => {
225 let canon_path = path.canonicalize();
226 if let Ok(path) = &canon_path
227 && let Ok(file) = std::fs::read_to_string(path)
228 {
229 let selections = {
230 let cursor = Cache::new().load(path).unwrap_or_default();
231 Selections::new(cursor)
232 };
233 let text = Text::from_file(Bytes::new(&file), selections, path, false);
234 (text, PathKind::SetExists(path.clone()))
235 } else if canon_path.is_err()
236 && let Ok(mut canon_path) = path.with_file_name(".").canonicalize()
237 {
238 canon_path.push(path.file_name().unwrap());
239 (Text::new_with_history(), PathKind::SetAbsent(canon_path))
240 } else {
241 (Text::new_with_history(), PathKind::new_unset())
242 }
243 }
244 };
245
246 let mut file = File {
247 path,
248 text,
249 cfg: self.print_cfg,
250 printed_lines: (0..40).map(|i| (i, i == 1)).collect(),
251 parsers: InnerParsers::default(),
252 layout_order: 0,
253 _ghost: PhantomData,
254 };
255
256 if let Some(add_parsers) = self.add_parsers {
257 add_parsers(&mut file);
258 }
259
260 (file, PushSpecs::above())
262 }
263}
264
265impl<U: Ui> Clone for FileCfg<U> {
266 fn clone(&self) -> Self {
267 Self {
268 text_op: self.text_op.clone(),
269 print_cfg: self.print_cfg,
270 add_parsers: None,
271 }
272 }
273}
274
275pub struct File<U: Ui> {
277 path: PathKind,
278 text: Text,
279 printed_lines: Vec<(usize, bool)>,
280 parsers: InnerParsers<U>,
281 pub cfg: PrintCfg,
283 pub(crate) layout_order: usize,
284 _ghost: PhantomData<U>,
285}
286
287impl<U: Ui> File<U> {
288 pub fn save(&mut self) -> Result<Option<usize>, Text> {
292 self.save_quit(false)
293 }
294
295 pub(crate) fn save_quit(&mut self, quit: bool) -> Result<Option<usize>, Text> {
296 if let PathKind::SetExists(path) | PathKind::SetAbsent(path) = &self.path {
297 let path = path.clone();
298 if self.text.has_unsaved_changes() {
299 let bytes = self
300 .text
301 .write_to(std::io::BufWriter::new(fs::File::create(&path)?))
302 .inspect(|_| self.path = PathKind::SetExists(path.clone()))?;
303
304 let path = path.to_string_lossy().to_string();
305 hook::queue(FileWritten((path, bytes, quit)));
306
307 Ok(Some(bytes))
308 } else {
309 Ok(None)
310 }
311 } else {
312 Err(txt!("No file was set").build())
313 }
314 }
315
316 pub fn save_to(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<Option<usize>> {
320 self.save_quit_to(path, false)
321 }
322
323 pub(crate) fn save_quit_to(
327 &self,
328 path: impl AsRef<std::path::Path>,
329 quit: bool,
330 ) -> std::io::Result<Option<usize>> {
331 if self.text.has_unsaved_changes() {
332 let path = path.as_ref();
333 let res = self
334 .text
335 .write_to(std::io::BufWriter::new(fs::File::create(path)?))
336 .map(Some);
337
338 if let Ok(Some(bytes)) = res.as_ref() {
339 hook::queue(FileWritten((
340 path.to_string_lossy().to_string(),
341 *bytes,
342 quit,
343 )));
344 }
345
346 res
347 } else {
348 Ok(None)
349 }
350 }
351
352 pub fn path(&self) -> String {
358 self.path.path()
359 }
360
361 pub fn path_set(&self) -> Option<String> {
365 self.path.path_set()
366 }
367
368 pub fn name(&self) -> String {
372 self.path.name()
373 }
374
375 pub fn name_set(&self) -> Option<String> {
379 self.path.name_set()
380 }
381
382 pub fn path_kind(&self) -> PathKind {
388 self.path.clone()
389 }
390
391 pub fn printed_lines(&self) -> &[(usize, bool)] {
397 &self.printed_lines
398 }
399
400 pub fn bytes(&self) -> &Bytes {
404 self.text.bytes()
405 }
406
407 pub fn len_bytes(&self) -> usize {
409 self.text.len().byte()
410 }
411
412 pub fn len_chars(&self) -> usize {
414 self.text.len().char()
415 }
416
417 pub fn len_lines(&self) -> usize {
419 self.text.len().line()
420 }
421
422 pub fn selections(&self) -> &Selections {
425 self.text.selections()
426 }
427
428 pub fn selections_mut(&mut self) -> &mut Selections {
430 self.text.selections_mut()
431 }
432
433 pub fn exists(&self) -> bool {
435 self.path_set()
436 .is_some_and(|p| std::fs::exists(PathBuf::from(&p)).is_ok_and(|e| e))
437 }
438
439 pub fn read_parser<Rd: Parser<U>, Ret>(&self, read: impl FnOnce(&Rd) -> Ret) -> Option<Ret> {
453 let pa = &mut unsafe { Pass::new() };
456
457 if let Some(moments) = self.text.unprocessed_moments() {
466 let cfg = self.print_cfg();
467 for moment in moments {
468 self.parsers.process_moment(pa, moment, cfg);
469 }
470 }
471
472 self.parsers.read_parser(read)
473 }
474
475 pub fn try_read_parser<Rd: Parser<U>, Ret>(
489 &self,
490 read: impl FnOnce(&Rd) -> Ret,
491 ) -> Option<Ret> {
492 let pa = &mut unsafe { Pass::new() };
495
496 if let Some(moments) = self.text.unprocessed_moments() {
497 let cfg = self.print_cfg();
498 for moment in moments {
499 self.parsers.process_moment(pa, moment, cfg);
500 }
501 }
502
503 self.parsers.try_read_parser(read)
504 }
505}
506
507impl<U: Ui> Handle<File<U>, U> {
508 pub fn add_parser(&mut self, pa: &mut Pass, cfg: impl ParserCfg<U>) {
512 let file = self.widget().read(pa);
513
514 if let Err(err) = file.parsers.add(file, cfg) {
515 context::error!("{err}");
516 }
517 }
518}
519
520impl<U: Ui> Widget<U> for File<U> {
521 type Cfg = FileCfg<U>;
522
523 fn cfg() -> Self::Cfg {
524 FileCfg::new()
525 }
526
527 fn update(pa: &mut Pass, handle: &Handle<Self, U>) {
528 let parsers = std::mem::take(&mut handle.write(pa).parsers);
529
530 let file = handle.read(pa);
531 let cfg = file.cfg;
532
533 for moment in file.text.unprocessed_moments().into_iter().flatten() {
534 parsers.process_moment(pa, moment, cfg);
535 }
536
537 let (file, area) = handle.write_with_area(pa);
538
539 if let Some(main) = file.text().selections().get_main() {
540 area.scroll_around_point(file.text(), main.caret(), file.print_cfg());
541 }
542
543 let (start, _) = area.start_points(&file.text, file.cfg);
544 let (end, _) = area.end_points(&file.text, file.cfg);
545
546 parsers.update_range(&mut file.text, start..end);
547 file.parsers = parsers;
548
549 file.text.update_bounds();
550 }
551
552 fn needs_update(&self, _: &Pass) -> bool {
553 self.parsers.needs_update()
554 }
555
556 fn text(&self) -> &Text {
557 &self.text
558 }
559
560 fn text_mut(&mut self) -> &mut Text {
561 &mut self.text
562 }
563
564 fn print_cfg(&self) -> PrintCfg {
565 self.cfg
566 }
567
568 fn print(&mut self, painter: Painter, area: &<U as Ui>::Area) {
569 let (start, _) = area.start_points(&self.text, self.cfg);
570
571 let mut last_line = area
572 .rev_print_iter(self.text.iter_rev(start), self.cfg)
573 .find_map(|(caret, item)| caret.wrap.then_some(item.line()));
574
575 self.printed_lines.clear();
576 let printed_lines = &mut self.printed_lines;
577
578 let mut has_wrapped = false;
579
580 area.print_with(&mut self.text, self.cfg, painter, move |caret, item| {
581 has_wrapped |= caret.wrap;
582 if has_wrapped && item.part.is_char() {
583 has_wrapped = false;
584 let line = item.line();
585 let wrapped = last_line.is_some_and(|ll| ll == line);
586 last_line = Some(line);
587 printed_lines.push((line, wrapped));
588 }
589 })
590 }
591
592 fn once() -> Result<(), Text> {
593 Ok(())
594 }
595}
596
597#[derive(Debug, Clone, PartialEq, Eq)]
599pub enum PathKind {
600 SetExists(PathBuf),
602 SetAbsent(PathBuf),
604 NotSet(usize),
612}
613
614impl PathKind {
615 fn new_unset() -> PathKind {
617 use std::sync::atomic::{AtomicUsize, Ordering};
618 static UNSET_COUNT: AtomicUsize = AtomicUsize::new(1);
619
620 PathKind::NotSet(UNSET_COUNT.fetch_add(1, Ordering::Relaxed))
621 }
622
623 pub fn path(&self) -> String {
627 match self {
628 PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
629 path.to_string_lossy().to_string()
630 }
631 PathKind::NotSet(id) => {
632 format!("*scratch file*#{id}")
633 }
634 }
635 }
636
637 pub fn path_set(&self) -> Option<String> {
641 match self {
642 PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
643 Some(path.to_string_lossy().to_string())
644 }
645 PathKind::NotSet(_) => None,
646 }
647 }
648
649 pub fn name(&self) -> String {
653 match self {
654 PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
655 let cur_dir = context::cur_dir();
656 if let Ok(path) = path.strip_prefix(cur_dir) {
657 path.to_string_lossy().to_string()
658 } else {
659 path.to_string_lossy().to_string()
660 }
661 }
662 PathKind::NotSet(id) => format!("*scratch file #{id}*"),
663 }
664 }
665
666 pub fn name_set(&self) -> Option<String> {
670 match self {
671 PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
672 let cur_dir = context::cur_dir();
673 Some(if let Ok(path) = path.strip_prefix(cur_dir) {
674 path.to_string_lossy().to_string()
675 } else {
676 path.to_string_lossy().to_string()
677 })
678 }
679 PathKind::NotSet(_) => None,
680 }
681 }
682
683 pub fn path_txt(&self) -> Text {
699 match self {
700 PathKind::SetExists(path) | PathKind::SetAbsent(path) => txt!("[file]{path}").build(),
701 PathKind::NotSet(id) => txt!("[file.new.scratch]*scratch file #{id}*").build(),
702 }
703 }
704
705 pub fn name_txt(&self) -> Text {
721 match self {
722 PathKind::SetExists(path) | PathKind::SetAbsent(path) => {
723 let cur_dir = context::cur_dir();
724 if let Ok(path) = path.strip_prefix(cur_dir) {
725 path.to_string_lossy().to_string()
726 } else {
727 path.to_string_lossy().to_string()
728 };
729 txt!("[file]{path}").build()
730 }
731 PathKind::NotSet(id) => txt!("[file.new.scratch]*scratch file #{id}*").build(),
732 }
733 }
734}
735
736#[derive(Default, Clone)]
738enum TextOp {
739 #[default]
740 NewBuffer,
741 TakeBuf(Bytes, PathKind, bool),
742 OpenPath(PathBuf),
743}