diff --git a/src/ui/help/mod.rs b/src/ui/help/mod.rs index 898253c..beaddf8 100644 --- a/src/ui/help/mod.rs +++ b/src/ui/help/mod.rs @@ -1,12 +1,11 @@ -use ratatui::text::Text; -use tui_markdown; +use crate::ui::render::markdown::Markdown; -pub fn render_topic(topic: &str) -> Text<'static> { +pub fn to_widget(topic: &str) -> Markdown { match topic { - "navigate" => tui_markdown::from_str(include_str!("../../../docs/navigation.md")), - "edit" => tui_markdown::from_str(include_str!("../../../docs/edit.md")), - "command" => tui_markdown::from_str(include_str!("../../../docs/command.md")), - "visual" => tui_markdown::from_str(include_str!("../../../docs/visual.md")), - _ => tui_markdown::from_str(include_str!("../../../docs/intro.md")), + "navigate" => Markdown::from_str(include_str!("../../../docs/navigation.md")), + "edit" => Markdown::from_str(include_str!("../../../docs/edit.md")), + "command" => Markdown::from_str(include_str!("../../../docs/command.md")), + "visual" => Markdown::from_str(include_str!("../../../docs/visual.md")), + _ => Markdown::from_str(include_str!("../../../docs/intro.md")), } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 604064a..7acd492 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, Result}; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use ironcalc::base::{expressions::types::Area, Model}; use ratatui::{ - buffer::Buffer, layout::{Constraint, Flex, Layout}, style::{Modifier, Style}, text::{Line, Text}, widgets::Block + buffer::Buffer, layout::{Constraint, Flex, Layout}, style::{Modifier, Style}, widgets::Block }; use tui_prompts::{State, Status, TextPrompt, TextState}; use tui_textarea::{CursorMove, TextArea}; @@ -19,7 +19,7 @@ pub mod render; mod test; use cmd::Cmd; -use render::viewport::ViewportState; +use render::{markdown::Markdown, viewport::ViewportState}; #[derive(Default, Debug, PartialEq, Clone)] pub enum Modality { @@ -80,7 +80,7 @@ pub struct AppState<'ws> { pub range_select: RangeSelection, pub dialog_scroll: u16, dirty: bool, - popup: Text<'ws>, + popup: Option, clipboard: Option, } @@ -299,16 +299,16 @@ impl<'ws> Workspace<'ws> { Ok(None) } - fn render_help_text(&self) -> Text<'static> { + fn render_help_text(&self) -> Markdown { // TODO(zaphar): We should be sourcing these from our actual help documentation. // Ideally we would also render the markdown content properly. // https://github.com/zaphar/sheetsui/issues/22 match self.state.modality() { - Modality::Navigate => help::render_topic("navigate"), - Modality::CellEdit => help::render_topic("edit"), - Modality::Command => help::render_topic("command"), - Modality::RangeSelect => help::render_topic("visual"), - _ => help::render_topic(""), + Modality::Navigate => help::to_widget("navigate"), + Modality::CellEdit => help::to_widget("edit"), + Modality::Command => help::to_widget("command"), + Modality::RangeSelect => help::to_widget("visual"), + _ => help::to_widget(""), } } @@ -361,8 +361,10 @@ impl<'ws> Workspace<'ws> { KeyCode::Char('k') | KeyCode::Up => { self.state.dialog_scroll = self.state.dialog_scroll.saturating_sub(1); } - _ => { - // NOOP + code => { + if let Some(widget) = &self.state.popup { + widget.handle_input(code); + } } } } @@ -414,7 +416,7 @@ impl<'ws> Workspace<'ws> { Ok(None) } Ok(Some(Cmd::Help(maybe_topic))) => { - self.enter_dialog_mode(help::render_topic(maybe_topic.unwrap_or(""))); + self.enter_dialog_mode(help::to_widget(maybe_topic.unwrap_or(""))); Ok(None) } Ok(Some(Cmd::Write(maybe_path))) => { @@ -508,11 +510,11 @@ impl<'ws> Workspace<'ws> { Ok(None) } Ok(None) => { - self.enter_dialog_mode(vec![Line::from(format!("Unrecognized commmand {}", cmd_text))]); + self.enter_dialog_mode(Markdown::from_str(&format!("Unrecognized commmand {}", cmd_text))); Ok(None) } Err(msg) => { - self.enter_dialog_mode(vec![Line::from(msg.to_owned())]); + self.enter_dialog_mode(Markdown::from_str(msg)); Ok(None) } } @@ -951,8 +953,8 @@ impl<'ws> Workspace<'ws> { self.state.command_state.focus(); } - fn enter_dialog_mode>>(&mut self, msg: T) { - self.state.popup = msg.into(); + fn enter_dialog_mode(&mut self, msg: Markdown) { + self.state.popup = Some(msg); self.state.modality_stack.push(Modality::Dialog); } diff --git a/src/ui/render/markdown.rs b/src/ui/render/markdown.rs index ebb0013..9b1a05a 100644 --- a/src/ui/render/markdown.rs +++ b/src/ui/render/markdown.rs @@ -1,124 +1,55 @@ -use core::panic; use std::collections::BTreeSet; -use ratatui::{ - text::{Line, Span, Text}, - widgets::Widget, -}; +use crossterm::event::KeyCode; +use ratatui::{text::Text, widgets::Widget}; -use pulldown_cmark::{Event, HeadingLevel, LinkType, Parser, Tag, TagEnd, TextMergeStream}; +use pulldown_cmark::{Event,LinkType, Parser, Tag, TextMergeStream}; -enum State { - Para, - NumberList, - BulletList, - Heading, - BlockQuote, -} +//enum State { +// Para, +// NumberList, +// BulletList, +// Heading, +// BlockQuote, +//} -struct WidgetWriter<'i> { - input: &'i str, - state_stack: Vec, - heading_stack: Vec<&'static str>, - list_stack: Vec, - accumulator: String, - lines: Vec, +#[derive(Debug, Clone, PartialEq)] +pub struct Markdown { + input: String, links: BTreeSet, } -impl<'i> WidgetWriter<'i> -{ - pub fn from_str(input: &'i str) -> Self { - Self { - input, - state_stack: Default::default(), - heading_stack: Default::default(), - list_stack: Default::default(), - accumulator: Default::default(), - lines: Default::default(), +impl Markdown { + pub fn from_str(input: &str) -> Self { + let mut me = Self { + input: input.to_owned(), links: Default::default(), - } + }; + me.parse(); + me } - pub fn parse(&mut self) { - let iter = TextMergeStream::new(Parser::new(self.input)); + fn parse(&mut self) { + let input = self.input.clone(); + let iter = TextMergeStream::new(Parser::new(&input)); for event in iter { match event { Event::Start(tag) => { self.start_tag(&tag); - }, - Event::End(tag) => { - self.end_tag(tag); - }, - Event::Text(txt) - | Event::Code(txt) - | Event::InlineHtml(txt) - | Event::Html(txt) => { - let prefix = if let Some(State::BlockQuote) = self.state_stack.first() { - "| " - } else { - "" - }; - for ln in txt.lines() { - self.accumulator.push_str(prefix); - self.accumulator.push_str(ln); - } - }, - Event::Rule => { /* noop */ }, - Event::SoftBreak => { /* noop */ }, - Event::HardBreak => { /* noop */ }, - // We don't support these - Event::InlineMath(_) => todo!(), - Event::DisplayMath(_) => todo!(), - Event::FootnoteReference(_) => todo!(), - Event::TaskListMarker(_) => todo!(), + } + _ => { /* noop */ } } } } - fn start_tag(&mut self, tag: &Tag<'i>) { + fn start_tag(&mut self, tag: &Tag<'_>) { match tag { - Tag::Paragraph => { - self.state_stack.push(State::Para); - }, - Tag::Heading { level, id: _id, classes: _classes, attrs: _attrs } => { - self.heading_stack.push(match level { - HeadingLevel::H1 => "1", - HeadingLevel::H2 => "2", - HeadingLevel::H3 => "3", - HeadingLevel::H4 => "4", - HeadingLevel::H5 => "5", - HeadingLevel::H6 => "6", - }); - self.state_stack.push(State::Heading); - let prefix = self.heading_stack.join("."); - self.accumulator.push_str(&prefix); - self.accumulator.push_str(" "); - }, - Tag::List(Some(first)) => { - self.list_stack.push(*first); - self.state_stack.push(State::NumberList); - }, - Tag::List(None) => { - self.state_stack.push(State::BulletList); - }, - Tag::Item => { - if let Some(State::BulletList) = self.state_stack.first() { - self.accumulator.push_str("- "); - } else if let Some(State::NumberList) = self.state_stack.first() { - let num = self.list_stack.pop().unwrap_or(1); - self.accumulator.push_str(&format!("{}. ", num)); - self.list_stack.push(num + 1); - } - panic!("No list type in our state stack"); - }, - Tag::Emphasis => { - self.accumulator.push_str("*"); - }, - Tag::Strong => { - self.accumulator.push_str("**"); - }, - Tag::Link { link_type, dest_url, title, id } => { + Tag::Link { + link_type, + dest_url, + title, + id, + } => { let dest = match link_type { // [foo](bar) LinkType::Inline => format!("({})", dest_url), @@ -135,77 +66,41 @@ impl<'i> WidgetWriter<'i> LinkType::Email => todo!(), LinkType::WikiLink { has_pothole: _ } => todo!(), }; - self.accumulator.push_str(&format!("[{}]{}", title, dest)); self.links.insert(dest); - }, - Tag::BlockQuote(_) => { - self.state_stack.push(State::BlockQuote); - }, - // these are all noops - Tag::CodeBlock(_) => {}, - Tag::HtmlBlock => {}, - Tag::FootnoteDefinition(_) => {}, - Tag::DefinitionList => {}, - Tag::DefinitionListTitle => {}, - Tag::DefinitionListDefinition => {}, - Tag::Table(_) => {}, - Tag::TableHead => {}, - Tag::TableRow => {}, - Tag::TableCell => {}, - Tag::Strikethrough => {}, - Tag::Superscript => {}, - Tag::Subscript => {} - Tag::Image { link_type: _link_type, dest_url: _dest_url, title: _title, id: _id } => {}, - Tag::MetadataBlock(_) => {}, + } + _ => { /* noop */ } } } - - fn end_tag(&mut self, tag: TagEnd) { - match tag { - TagEnd::Paragraph => { - self.state_stack.pop(); - self.lines.push("\n".to_owned()); - }, - TagEnd::Heading(_level) => { - self.heading_stack.pop(); - self.state_stack.pop(); - self.lines.extend(self.accumulator.lines().map(|s| s.to_owned())); - self.accumulator.clear(); - }, - TagEnd::List(_ordered) => { - self.state_stack.pop(); - }, - TagEnd::BlockQuote(_kind) => { - self.state_stack.pop(); - }, - TagEnd::CodeBlock => { - todo!() - }, - TagEnd::HtmlBlock => { - todo!() - }, - TagEnd::Item => { /* noop */ }, - TagEnd::Link => { /* noop */ }, - // We don't support these - TagEnd::FootnoteDefinition => todo!(), - TagEnd::DefinitionList => todo!(), - TagEnd::DefinitionListTitle => todo!(), - TagEnd::DefinitionListDefinition => todo!(), - TagEnd::Table => todo!(), - TagEnd::TableHead => todo!(), - TagEnd::TableRow => todo!(), - TagEnd::TableCell => todo!(), - TagEnd::Emphasis => { - self.accumulator.push_str("*"); - }, - TagEnd::Strong => { - self.accumulator.push_str("**"); - }, - TagEnd::Strikethrough => todo!(), - TagEnd::Superscript => todo!(), - TagEnd::Subscript => todo!(), - TagEnd::Image => todo!(), - TagEnd::MetadataBlock(_) => todo!(), - } + + pub fn handle_input(&self, code: KeyCode) -> Option { + let num = match code { + KeyCode::Char('0') => 0, + KeyCode::Char('1') => 1, + KeyCode::Char('2') => 2, + KeyCode::Char('3') => 3, + KeyCode::Char('4') => 4, + KeyCode::Char('5') => 5, + KeyCode::Char('6') => 6, + KeyCode::Char('7') => 7, + KeyCode::Char('8') => 8, + KeyCode::Char('9') => 9, + _ => return None, + }; + self.links.iter().nth(num).cloned() + } + + pub fn get_text<'w>(&'w self) -> Text<'_> { + Text::raw(&self.input) + } +} + +// TODO(jwall): We need this to be lines instead of just a render. +impl Widget for Markdown { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) + where + Self: Sized, + { + let text = Text::raw(self.input); + text.render(area, buf); } } diff --git a/src/ui/render/mod.rs b/src/ui/render/mod.rs index 447ce22..f153014 100644 --- a/src/ui/render/mod.rs +++ b/src/ui/render/mod.rs @@ -100,7 +100,7 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { Self: Sized, { if self.state.modality() == &Modality::Dialog { - let lines = Text::from_iter(self.state.popup.iter().cloned()); + let lines = self.state.popup.as_ref().map(|md| md.get_text()).unwrap_or_else(|| Text::raw("Popup message here")); let popup = dialog::Dialog::new(lines, "Help").scroll(self.state.dialog_scroll); popup.render(area, buf); } else if self.state.modality() == &Modality::Quit { diff --git a/src/ui/test.rs b/src/ui/test.rs index d75bf28..2896b50 100644 --- a/src/ui/test.rs +++ b/src/ui/test.rs @@ -395,7 +395,7 @@ macro_rules! assert_help_dialog { .run(&mut ws) .expect("Failed to handle 'alt-h' key event"); assert_eq!(Some(&Modality::Dialog), ws.state.modality_stack.last()); - assert_eq!(edit_help, ws.state.popup); + assert_eq!(Some(edit_help), ws.state.popup); $exit.run(&mut ws).expect("Failed to handle key event"); assert_eq!(Some(&Modality::CellEdit), ws.state.modality_stack.last()); }}; @@ -431,7 +431,7 @@ fn test_navigation_mode_help_keycode() { .run(&mut ws) .expect("Failed to handle 'alt-h' key event"); assert_eq!(Some(&Modality::Dialog), ws.state.modality_stack.last()); - assert_eq!(help_text, ws.state.popup); + assert_eq!(Some(help_text), ws.state.popup); } #[test] @@ -449,7 +449,7 @@ fn test_command_mode_help_keycode() { .run(&mut ws) .expect("Failed to handle 'alt-h' key event"); assert_eq!(Some(&Modality::Dialog), ws.state.modality_stack.last()); - assert_eq!(edit_help, ws.state.popup); + assert_eq!(Some(edit_help), ws.state.popup); } #[test]