//! One Markdown file, its text kept for the shell. use std::path::PathBuf; use ely_gpui_component::documents::{DocumentEvent, LARGEST_READ, MarkdownDocument, MarkdownMode}; use gpui::{ AppContext as _, Context, Entity, EventEmitter, FocusHandle, Focusable as _, InteractiveElement as _, IntoElement, ParentElement as _, Render, Styled as _, Window, div, }; use crate::engine::{Doc, mark}; /// The text changed, or a read is in. pub struct Reported; /// What went wrong, for the shell to say. pub struct Failed(pub String); /// How a document shows (MARK-02). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Mode { Visual, Source, Preview, } impl Mode { pub const ALL: [Mode; 3] = [Mode::Visual, Mode::Source, Mode::Preview]; pub fn name(self) -> &'static str { match self { Mode::Visual => "visual", Mode::Source => "source", Mode::Preview => "preview", } } pub fn parse(name: &str) -> Option { Mode::ALL.into_iter().find(|mode| mode.name() == name) } fn shown(self) -> MarkdownMode { match self { Mode::Visual => MarkdownMode::Visual, Mode::Source => MarkdownMode::Source, Mode::Preview => MarkdownMode::Preview, } } } /// One heading of the outline, and its line from zero. #[derive(Clone, Debug, PartialEq)] pub struct Heading { pub level: usize, pub title: String, pub line: usize, } /// The caret and the first line in view. #[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Place { pub line: u64, #[serde(default)] pub column: u64, #[serde(default)] pub top: u64, } pub struct MarkView { document: Entity, doc: Doc, /// Text changes so far; autosave counts them. changed: u64, /// The mark at the last load or save. saved: String, pub mode: Mode, /// The file's folder: pictures resolve here. pub(crate) folder: PathBuf, /// The mode first took input (PERF-07). pub(crate) shown: bool, pub focus: FocusHandle, } impl EventEmitter for MarkView {} impl EventEmitter for MarkView {} impl MarkView { pub fn new( doc: Doc, saved: String, folder: PathBuf, window: &mut Window, cx: &mut Context, ) -> Self { let text = doc.lock().expect("the text lock").current.clone(); let mode = match text.len() > LARGEST_READ { true => Mode::Source, false => Mode::Visual, }; let view = cx.weak_entity(); let document = cx.new(|cx| { let mut document = MarkdownDocument::new(text, mode.shown(), window, cx); document.set_base(folder.as_path(), cx); document.on_link(move |url, _, cx| { view.update(cx, |view, cx| view.follow(url, cx)).ok(); }); document }); cx.subscribe_in(&document, window, Self::heard).detach(); // The shell hears the load. cx.defer_in(window, |view, _, cx| { view.show_once(cx); cx.emit(Reported); }); let focus = cx.focus_handle(); // The view's focus is the document's. cx.on_focus(&focus, window, |view, window, cx| { let inner = view.document.focus_handle(cx); window.focus(&inner, cx); }) .detach(); MarkView { document, doc, changed: 0, saved, mode, folder, shown: false, focus, } } pub(crate) fn document(&self) -> &Entity { &self.document } /// Every change is in the text at once. pub fn settled(&self) -> bool { true } pub fn changed(&self) -> u64 { self.changed } /// The text's size now, in bytes. pub fn bytes(&self) -> usize { self.doc.lock().expect("the text lock").current.len() } /// Too long to read: source alone (MARK-14). pub fn large(&self) -> bool { self.bytes() > LARGEST_READ } pub fn modified(&self) -> bool { mark(&self.doc.lock().expect("the text lock").current) != self.saved } pub fn saved(&mut self, mark: &str, cx: &mut Context) { self.saved = mark.to_owned(); cx.emit(Reported); } /// The same text in `mode` (MARK-02). pub fn switch(&mut self, mode: Mode, window: &mut Window, cx: &mut Context) { if self.large() && mode != Mode::Source { log::info!("mark: a large file stays in source"); let said = ely_ui::i18n::text(cx, "mark.large.modes", &[]).to_string(); return cx.emit(Failed(said)); } log::info!("mark: switches to {}", mode.name()); self.mode = mode; self.document.update(cx, |document, cx| { document.set_mode(mode.shown(), window, cx) }); cx.notify(); } /// The headings as last read. pub fn outline(&self, cx: &gpui::App) -> Vec { let outline = self.document.read(cx).outline(cx); outline .into_iter() .map(|(heading, line)| Heading { level: usize::from(heading.depth), title: heading.title, line, }) .collect() } /// Line `line` in view and focus. pub fn reveal(&mut self, line: usize, window: &mut Window, cx: &mut Context) { self.document .update(cx, |document, cx| document.reveal(line, window, cx)); } /// The caret and the view, to keep across launches. pub fn place(&self, cx: &gpui::App) -> Place { let document = self.document.read(cx); let (line, column) = document.caret(cx); Place { line: line as u64, column: column as u64, top: document.top_line(cx) as u64, } } /// The disk's text replaces a clean one; undo starts there. pub fn refresh(&mut self, source: crate::text::Source, cx: &mut Context) { let text = source.text.clone(); { let mut doc = self.doc.lock().expect("the text lock"); doc.current = text.clone(); doc.source = source; } self.saved = mark(&text); self.document .update(cx, |document, cx| document.reset(text, cx)); cx.emit(Reported); } /// `text` replaces the document's, as one undoable edit. pub fn adopt(&mut self, text: String, cx: &mut Context) { self.document .update(cx, |document, cx| document.set_text(text, cx)); } /// Shows as `mode` at `place`; recovered text unsaved. pub fn reopen( &mut self, mode: Mode, place: Option, text: Option, window: &mut Window, cx: &mut Context, ) { if let Some(text) = text { self.document .update(cx, |document, cx| document.reset(text, cx)); } let mode = if self.large() { Mode::Source } else { mode }; self.switch(mode, window, cx); if let Some(place) = place { self.document.update(cx, |document, cx| { document.set_caret(place.line as usize, place.column as usize, cx); document.set_top_line(place.top as usize, window, cx); }); } } /// The file moved; pictures follow it. pub fn moved(&mut self, folder: PathBuf, cx: &mut Context) { if folder == self.folder { return; } self.document .update(cx, |document, cx| document.set_base(folder.as_path(), cx)); self.folder = folder; } fn heard( &mut self, document: &Entity, event: &DocumentEvent, window: &mut Window, cx: &mut Context, ) { if *event == DocumentEvent::Changed { let text = document.read(cx).text(cx); self.doc.lock().expect("the text lock").current = text; self.changed += 1; if self.large() && self.mode != Mode::Source { log::info!("mark: the text grew past the read limit"); self.switch(Mode::Source, window, cx); } } self.show_once(cx); cx.emit(Reported); } /// Logs the first moment the mode takes input. fn show_once(&mut self, cx: &gpui::App) { let ready = self.mode == Mode::Source || self.document.read(cx).read_now(cx); if ready && !self.shown { self.shown = true; log::info!("mark: shown as {:?}", self.mode.name()); } } /// A pressed link: web and mail open outside (M-05). fn follow(&mut self, url: &str, cx: &mut Context) { if ely_office::outward(url) { log::info!("mark: opens a link outside"); return cx.open_url(url); } log::info!("mark: a link of another kind was refused"); let said = ely_ui::i18n::text(cx, "office.link.refused", &[]).to_string(); cx.emit(Failed(said)); } } impl Render for MarkView { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .size_full() .track_focus(&self.focus) .child(self.document.clone()) } } #[cfg(test)] mod tests;