use super::{
history::Snapshot,
renderer::MarkdownRenderer,
search::SearchState,
text_buffer::TextBuffer,
theme::{EditorTheme, HighlightFn},
vim::{Input, Key, Mode, Transition, Vim, filter_commands, parse_command},
wrap_engine::WrapEngine,
};
use crossterm::{
event::{self, Event},
execute,
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
Frame, Terminal,
backend::CrosstermBackend,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph},
};
use std::io;
pub type ThemeGalleryItem = (&'static str, &'static str, EditorTheme);
pub struct MarkdownEditor {
buffer: TextBuffer,
wrap: WrapEngine,
vim: Vim,
search: SearchState,
renderer: MarkdownRenderer,
theme: EditorTheme,
title: String,
scroll_offset: usize,
viewport_height: usize,
viewport_width: usize,
cmd_popup_selected: usize,
theme_gallery: Vec<ThemeGalleryItem>,
theme_index: usize,
theme_popup_selected: usize,
status_message: Option<String>,
selected_theme_id: Option<&'static str>,
}
impl MarkdownEditor {
pub fn new(
title: &str,
content: &str,
theme: EditorTheme,
highlight_fn: HighlightFn,
theme_gallery: Vec<ThemeGalleryItem>,
) -> Self {
let buffer = TextBuffer::from_content(content);
let initial_mode = if content.is_empty() {
Mode::Insert
} else {
Mode::Normal
};
let mut vim = Vim::new(initial_mode.clone());
vim.push_snapshot(Snapshot::new(buffer.snapshot()), buffer.cursor());
let mut wrap = WrapEngine::new();
wrap.rebuild_cache(buffer.lines());
let renderer = MarkdownRenderer::new(theme.clone(), highlight_fn);
let viewport_width: usize = 80; wrap.set_width(viewport_width.saturating_sub(6));
let theme_index = theme_gallery
.iter()
.position(|(_, _, t)| *t == theme)
.unwrap_or(0);
Self {
buffer,
wrap,
vim,
search: SearchState::new(),
renderer,
theme,
title: title.to_string(),
scroll_offset: 0,
viewport_height: 20,
viewport_width,
cmd_popup_selected: 0,
theme_gallery,
theme_index,
theme_popup_selected: theme_index,
status_message: None,
selected_theme_id: None,
}
}
pub fn selected_theme_id(&self) -> Option<&'static str> {
self.selected_theme_id
}
pub fn cursor_visual_line(&self) -> usize {
let (row, col) = self.buffer.cursor();
self.wrap.logical_to_visual(row, col)
}
pub fn move_cursor_visual_up(&mut self) {
let current_visual = self.cursor_visual_line();
if current_visual == 0 {
return;
}
let target_visual = current_visual - 1;
let (_, current_col) = self.buffer.cursor();
let (target_logical, _) = self.wrap.visual_to_logical(target_visual);
self.wrap
.build_range(self.buffer.lines(), target_logical, target_logical + 1);
if let Some(target_vl) = self.wrap.get_visual_line(target_visual) {
let logical_line = target_vl.logical_line;
let end_col = target_vl.end_col;
let start_col = target_vl.start_col;
let new_col = current_col.min(end_col.saturating_sub(1)).max(start_col);
self.buffer.set_cursor(logical_line, new_col);
}
}
pub fn move_cursor_visual_down(&mut self) {
let current_visual = self.cursor_visual_line();
let total_visual = self.wrap.visual_line_count();
if current_visual >= total_visual.saturating_sub(1) {
return;
}
let target_visual = current_visual + 1;
let (_, current_col) = self.buffer.cursor();
let (target_logical, _) = self.wrap.visual_to_logical(target_visual);
self.wrap
.build_range(self.buffer.lines(), target_logical, target_logical + 1);
if let Some(target_vl) = self.wrap.get_visual_line(target_visual) {
let logical_line = target_vl.logical_line;
let end_col = target_vl.end_col;
let start_col = target_vl.start_col;
let new_col = current_col.min(end_col.saturating_sub(1)).max(start_col);
self.buffer.set_cursor(logical_line, new_col);
}
}
pub fn handle_input(&mut self, input: &Input) -> EditorAction {
self.status_message = None;
if self.vim.mode() == &Mode::ThemeSelect {
return self.handle_theme_select(input);
}
if input.ctrl && input.key == Key::Char('s') {
return EditorAction::Submit(self.buffer.to_string());
}
if input.ctrl && input.key == Key::Char('q') {
return EditorAction::Cancel;
}
if self.vim.mode() == &Mode::Normal && input.key == Key::Char('u') && !input.ctrl {
self.undo();
return EditorAction::Continue;
}
if self.vim.mode() == &Mode::Normal && input.key == Key::Char('r') && input.ctrl {
self.redo();
return EditorAction::Continue;
}
if self.vim.mode() == &Mode::Normal && self.search.is_searching() {
if input.key == Key::Char('n') && !input.ctrl {
self.search_next();
return EditorAction::Continue;
}
if input.key == Key::Char('N') && !input.ctrl {
self.search_prev();
return EditorAction::Continue;
}
}
{
let filter_clone = match self.vim.mode() {
Mode::CommandPanel(f) => Some(f.clone()),
_ => None,
};
if let Some(filter) = filter_clone {
let filtered = filter_commands(&filter);
match input.key {
Key::Up => {
if !filtered.is_empty() {
if self.cmd_popup_selected > 0 {
self.cmd_popup_selected -= 1;
} else {
self.cmd_popup_selected = filtered.len() - 1;
}
}
return EditorAction::Continue;
}
Key::Down => {
if !filtered.is_empty() {
if self.cmd_popup_selected < filtered.len() - 1 {
self.cmd_popup_selected += 1;
} else {
self.cmd_popup_selected = 0;
}
}
return EditorAction::Continue;
}
Key::Enter => {
let selected = self
.cmd_popup_selected
.min(filtered.len().saturating_sub(1));
if let Some(cmd) = filtered.get(selected) {
let full_cmd = if cmd.name == "jump" {
filter
} else {
cmd.name.to_string()
};
return self.execute_command(&full_cmd);
}
self.vim.set_mode(Mode::Normal);
return EditorAction::Continue;
}
_ => {}
}
}
}
if self.wrap.is_enabled() {
let is_normal = self.vim.mode() == &Mode::Normal;
let is_down = matches!(input.key, Key::Down)
|| (is_normal && matches!(input.key, Key::Char('j')));
let is_up =
matches!(input.key, Key::Up) || (is_normal && matches!(input.key, Key::Char('k')));
if is_down && !input.ctrl {
self.move_cursor_visual_down();
return EditorAction::Continue;
}
if is_up && !input.ctrl {
self.move_cursor_visual_up();
return EditorAction::Continue;
}
}
let old_mode = self.vim.mode().clone();
let transition = self.vim.handle_input(input, &mut self.buffer);
match transition {
Transition::Mode(new_mode) => {
if old_mode == Mode::Insert && new_mode != Mode::Insert {
self.vim
.push_snapshot(Snapshot::new(self.buffer.snapshot()), self.buffer.cursor());
}
self.vim.set_mode(new_mode);
self.rebuild_wrap_cache();
}
Transition::Submit => {
return EditorAction::Submit(self.buffer.to_string());
}
Transition::Cancel => {
return EditorAction::Cancel;
}
Transition::Nop => {
self.handle_mode_input(input);
}
Transition::NeedRebuild => {
if old_mode == Mode::Normal {
self.vim
.push_snapshot(Snapshot::new(self.buffer.snapshot()), self.buffer.cursor());
}
self.rebuild_wrap_cache();
}
Transition::ToggleWrap(enabled) => {
self.wrap.set_enabled(enabled);
self.rebuild_wrap_cache();
}
Transition::ExecuteCommand(cmd) => {
return self.execute_command(&cmd);
}
}
EditorAction::Continue
}
fn handle_mode_input(&mut self, input: &Input) {
match self.vim.mode() {
Mode::Command(cmd) => {
let mut cmd = cmd.clone();
match &input.key {
Key::Char(c) => cmd.push(*c),
Key::Backspace => {
cmd.pop();
}
_ => {}
}
self.vim.set_mode(Mode::Command(cmd));
}
Mode::Search(pattern) => {
let mut pattern = pattern.clone();
match &input.key {
Key::Char(c) => {
pattern.push(*c);
self.search.search(&pattern, self.buffer.lines());
}
Key::Backspace => {
pattern.pop();
self.search.search(&pattern, self.buffer.lines());
}
_ => {}
}
self.vim.set_mode(Mode::Search(pattern));
}
Mode::CommandPanel(filter) => {
let mut filter = filter.clone();
match &input.key {
Key::Char(c) => {
filter.push(*c);
self.cmd_popup_selected = 0;
}
Key::Backspace => {
if !filter.is_empty() {
filter.pop();
self.cmd_popup_selected = 0;
} else {
self.vim.set_mode(Mode::Normal);
return;
}
}
_ => {}
}
self.vim.set_mode(Mode::CommandPanel(filter));
}
_ => {}
}
}
pub fn undo(&mut self) {
if let Some(snap) = self.vim.undo() {
self.buffer.replace_lines(snap.lines.clone());
self.buffer.set_cursor(snap.cursor.0, snap.cursor.1);
self.rebuild_wrap_cache();
}
}
pub fn redo(&mut self) {
if let Some(snap) = self.vim.redo() {
self.buffer.replace_lines(snap.lines.clone());
self.buffer.set_cursor(snap.cursor.0, snap.cursor.1);
self.rebuild_wrap_cache();
}
}
pub fn search_next(&mut self) {
self.search.next_match();
if let Some(m) = self.search.current_match() {
self.buffer.set_cursor(m.line, m.start);
}
}
pub fn search_prev(&mut self) {
self.search.prev_match();
if let Some(m) = self.search.current_match() {
self.buffer.set_cursor(m.line, m.start);
}
}
fn execute_command(&mut self, cmd: &str) -> EditorAction {
let (name, arg) = parse_command(cmd);
match name {
"save" | "w" | "wq" | "x" => EditorAction::Submit(self.buffer.to_string()),
"quit" | "q" => EditorAction::Cancel,
"search" => {
self.vim.set_mode(Mode::Search(String::new()));
EditorAction::Continue
}
"wrap" => {
self.wrap.set_enabled(true);
self.rebuild_wrap_cache();
self.vim.set_mode(Mode::Normal);
EditorAction::Continue
}
"nowrap" => {
self.wrap.set_enabled(false);
self.rebuild_wrap_cache();
self.vim.set_mode(Mode::Normal);
EditorAction::Continue
}
"jump" => {
if let Ok(line_num) = arg.parse::<usize>()
&& line_num > 0
{
self.buffer.set_cursor(line_num - 1, 0);
}
self.rebuild_wrap_cache();
self.vim.set_mode(Mode::Normal);
EditorAction::Continue
}
"undo" => {
self.undo();
self.vim.set_mode(Mode::Normal);
EditorAction::Continue
}
"redo" => {
self.redo();
self.vim.set_mode(Mode::Normal);
EditorAction::Continue
}
"tohead" => {
self.buffer.move_cursor_top();
self.rebuild_wrap_cache();
self.vim.set_mode(Mode::Normal);
EditorAction::Continue
}
"toend" => {
self.buffer.move_cursor_bottom();
self.rebuild_wrap_cache();
self.vim.set_mode(Mode::Normal);
EditorAction::Continue
}
"theme" => {
self.theme_popup_selected = self.theme_index;
self.vim.set_mode(Mode::ThemeSelect);
EditorAction::Continue
}
"line-number" => {
self.renderer.set_show_line_numbers(true);
self.vim.set_mode(Mode::Normal);
EditorAction::Continue
}
"no-line-number" => {
self.renderer.set_show_line_numbers(false);
self.vim.set_mode(Mode::Normal);
EditorAction::Continue
}
_ => {
self.vim.set_mode(Mode::Normal);
EditorAction::Continue
}
}
}
fn handle_theme_select(&mut self, input: &Input) -> EditorAction {
let count = self.theme_gallery.len();
match input.key {
Key::Esc => {
self.vim.set_mode(Mode::Normal);
}
Key::Up => {
if self.theme_popup_selected > 0 {
self.theme_popup_selected -= 1;
} else {
self.theme_popup_selected = count - 1;
}
}
Key::Down => {
if self.theme_popup_selected < count - 1 {
self.theme_popup_selected += 1;
} else {
self.theme_popup_selected = 0;
}
}
Key::Enter => {
let idx = self.theme_popup_selected;
if idx < count {
self.theme_index = idx;
let (name, theme_id, new_theme) = &self.theme_gallery[idx];
self.theme = new_theme.clone();
self.renderer.set_theme(new_theme.clone());
self.selected_theme_id = Some(theme_id);
self.status_message = Some(format!("主题: {}", name));
}
self.vim.set_mode(Mode::Normal);
}
_ => {}
}
EditorAction::Continue
}
fn rebuild_wrap_cache(&mut self) {
self.wrap.rebuild_cache(self.buffer.lines());
self.renderer.invalidate_cache();
}
fn update_scroll_from_visual(&mut self, visual_pos: usize, viewport_height: usize) {
if visual_pos < self.scroll_offset {
self.scroll_offset = visual_pos;
} else if visual_pos >= self.scroll_offset + viewport_height {
self.scroll_offset = visual_pos - viewport_height + 1;
}
}
pub fn render(&mut self, f: &mut Frame<'_>, area: Rect) {
let content_height = area.height.saturating_sub(3) as usize; let content_width = area.width.saturating_sub(2) as usize;
self.viewport_height = content_height;
self.viewport_width = content_width;
let line_num_width = if self.renderer.is_show_line_numbers() {
6
} else {
0
};
let wrap_width = content_width.saturating_sub(line_num_width);
self.wrap.set_width(wrap_width);
if self.wrap.is_dirty() {
self.rebuild_wrap_cache();
}
let (cursor_row, mut cursor_col) = self.buffer.cursor();
let line_count = self.buffer.line_count();
if *self.vim.mode() == Mode::Normal {
let line_len = self.buffer.current_line_len();
if line_len > 0 {
cursor_col = cursor_col.min(line_len - 1);
}
}
self.renderer.ensure_cache_valid(self.buffer.lines());
let cursor_visual_pos = self.wrap.logical_to_visual(cursor_row, cursor_col);
self.update_scroll_from_visual(cursor_visual_pos, content_height);
let first_visible_visual = self.scroll_offset;
let last_visible_visual = self.scroll_offset + content_height;
let (start_logical, _) = self.wrap.visual_to_logical(first_visible_visual);
let (end_logical, _) = self.wrap.visual_to_logical(last_visible_visual);
let render_start = start_logical.saturating_sub(2).min(cursor_row);
let render_end = (end_logical + 3).min(line_count).max(cursor_row + 1);
self.wrap
.build_range(self.buffer.lines(), render_start, render_end);
let visual_offset = self.wrap.visual_offset_of(render_start);
let mut all_visual_lines: Vec<Line<'static>> = Vec::new();
for logical_line in render_start..render_end {
let is_cursor_line = logical_line == cursor_row;
let cached = self.wrap.get_cached_lines(logical_line);
for vl in cached {
let rendered = self.renderer.render_visual_line(
vl,
is_cursor_line,
if is_cursor_line {
Some(cursor_col)
} else {
None
},
&self.search,
&self.buffer,
wrap_width,
);
all_visual_lines.extend(rendered);
}
}
let scroll_in_rendered = self.scroll_offset.saturating_sub(visual_offset);
let visible_start = scroll_in_rendered.min(all_visual_lines.len().saturating_sub(1));
let visible_end = (scroll_in_rendered + content_height).min(all_visual_lines.len());
let mut lines_to_render: Vec<Line<'static>> = if visible_start < all_visual_lines.len() {
all_visual_lines[visible_start..visible_end].to_vec()
} else {
Vec::new()
};
for _ in lines_to_render.len()..content_height {
lines_to_render.push(Line::from(Span::styled(
"~",
Style::default()
.fg(Color::DarkGray)
.bg(self.theme.bg_primary),
)));
}
let border_color = self.vim.mode().border_color();
let block = Block::default()
.title(format!(" {} ", self.title))
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.style(Style::default().bg(self.theme.bg_primary));
let paragraph = Paragraph::new(lines_to_render).block(block);
f.render_widget(paragraph, area);
let status_bar = self.render_status_bar(area.width as usize);
let status_area = Rect::new(0, area.height - 1, area.width, 1);
let status_block = Block::default().style(Style::default().bg(self.theme.bg_primary));
f.render_widget(Paragraph::new(status_bar).block(status_block), status_area);
if matches!(
self.vim.mode(),
Mode::Command(_) | Mode::Search(_) | Mode::CommandPanel(_)
) {
let cmd_bar = self.render_command_bar();
let cmd_area = Rect::new(0, area.height - 2, area.width, 1);
let cmd_block = Block::default().style(Style::default().bg(self.theme.bg_primary));
f.render_widget(Paragraph::new(cmd_bar).block(cmd_block), cmd_area);
}
if let Mode::CommandPanel(filter) = self.vim.mode() {
let filter = filter.clone();
self.render_command_popup(f, &filter, area);
}
if self.vim.mode() == &Mode::ThemeSelect {
self.render_theme_popup(f, area);
}
}
fn render_status_bar(&self, width: usize) -> Line<'static> {
let mode_str = format!(" {} ", self.vim.mode());
let (row, col) = self.buffer.cursor();
let pos_str = format!(" {}:{} ", row + 1, col + 1);
let wrap_str = if self.wrap.is_enabled() {
" WRAP "
} else {
" NOWRAP "
};
let hints: String = if let Some(ref msg) = self.status_message {
msg.clone()
} else {
" Ctrl+S 保存 | Ctrl+Q 取消 | / 命令面板 ".to_string()
};
let used_width = mode_str.len() + pos_str.len() + wrap_str.len() + hints.len();
let separator = " ".repeat(width.saturating_sub(used_width));
let hints_style = if self.status_message.is_some() {
Style::default()
.fg(self.theme.text_bold)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(self.theme.text_dim)
};
Line::from(vec![
Span::styled(
mode_str,
Style::default()
.fg(Color::Black)
.bg(self.vim.mode().border_color())
.add_modifier(Modifier::BOLD),
),
Span::styled(pos_str, Style::default().fg(self.theme.text_dim)),
Span::styled(wrap_str, Style::default().fg(self.theme.text_dim)),
Span::styled(separator, Style::default().fg(self.theme.text_normal)),
Span::styled(hints, hints_style),
])
}
fn render_command_bar(&self) -> Line<'static> {
match self.vim.mode() {
Mode::Command(cmd) => Line::from(vec![
Span::styled(":", Style::default().fg(self.theme.text_normal)),
Span::styled(cmd.clone(), Style::default().fg(self.theme.text_normal)),
Span::styled(" ", Style::default().fg(self.theme.text_normal)),
]),
Mode::Search(pattern) => Line::from(vec![
Span::styled("/", Style::default().fg(Color::Magenta)),
Span::styled(pattern.clone(), Style::default().fg(self.theme.text_normal)),
Span::styled(" ", Style::default().fg(self.theme.text_normal)),
]),
Mode::CommandPanel(filter) => Line::from(vec![
Span::styled("/", Style::default().fg(Color::Magenta)),
Span::styled(filter.clone(), Style::default().fg(self.theme.text_normal)),
Span::styled(" ", Style::default().fg(self.theme.text_normal)),
]),
_ => Line::default(),
}
}
fn render_command_popup(&mut self, f: &mut Frame<'_>, filter: &str, area: Rect) {
let items = filter_commands(filter);
if items.is_empty() {
return;
}
let item_count = items.len();
let popup_height = (item_count as u16 + 2).min(area.height.saturating_sub(4));
let max_label_width = items
.iter()
.map(|cmd| {
2 + unicode_width::UnicodeWidthStr::width(cmd.name)
+ 3
+ unicode_width::UnicodeWidthStr::width(cmd.desc)
})
.max()
.unwrap_or(16)
.max(16);
let popup_width = (max_label_width as u16 + 2).min(area.width.saturating_sub(4));
let x = area.x + 2;
let y = area
.bottom()
.saturating_sub(popup_height + 2) .max(area.y + 2);
let popup_area = Rect::new(x, y, popup_width, popup_height);
let title = if filter.is_empty() {
" 命令面板 ".to_string()
} else {
format!(" 命令面板 [{}] ", filter)
};
self.cmd_popup_selected = self.cmd_popup_selected.min(item_count.saturating_sub(1));
let accent = self.theme.md_h1;
let popup_bg = self.theme.bg_primary;
let dim_color = self.theme.text_dim;
let label_ai = self.theme.label_ai;
let list_items: Vec<ListItem> = items
.iter()
.enumerate()
.map(|(i, cmd)| {
let is_selected = i == self.cmd_popup_selected;
let name_style = if is_selected {
Style::default().fg(label_ai).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(label_ai)
};
let desc_style = Style::default().fg(dim_color);
let pointer = if is_selected { "❯ " } else { " " };
ListItem::new(Line::from(vec![
Span::styled(pointer.to_string(), name_style),
Span::styled(format!("{:<10}", cmd.name), name_style),
Span::styled(cmd.desc.to_string(), desc_style),
]))
})
.collect();
let mut list_state = ListState::default();
list_state.select(Some(self.cmd_popup_selected));
let list = List::new(list_items)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(ratatui::widgets::BorderType::Rounded)
.border_style(Style::default().fg(accent))
.title(Span::styled(
title,
Style::default().fg(accent).add_modifier(Modifier::BOLD),
))
.style(Style::default().bg(popup_bg)),
)
.highlight_style(
Style::default()
.bg(accent)
.fg(popup_bg)
.add_modifier(Modifier::BOLD),
);
f.render_widget(Clear, popup_area);
f.render_stateful_widget(list, popup_area, &mut list_state);
}
fn render_theme_popup(&mut self, f: &mut Frame<'_>, area: Rect) {
let item_count = self.theme_gallery.len();
if item_count == 0 {
return;
}
let popup_height = (item_count as u16 + 2).min(area.height.saturating_sub(4));
let popup_width = 28u16.min(area.width.saturating_sub(4));
let x = area.x + 2;
let y = area
.bottom()
.saturating_sub(popup_height + 2)
.max(area.y + 2);
let popup_area = Rect::new(x, y, popup_width, popup_height);
self.theme_popup_selected = self.theme_popup_selected.min(item_count.saturating_sub(1));
let accent = self.theme.md_h1;
let popup_bg = self.theme.bg_primary;
let text_color = self.theme.text_normal;
let current_color = self.theme.md_link;
let list_items: Vec<ListItem> = self
.theme_gallery
.iter()
.enumerate()
.map(|(i, (name, _, _))| {
let is_selected = i == self.theme_popup_selected;
let is_current = i == self.theme_index;
let pointer = if is_selected { "❯ " } else { " " };
let check = if is_current { " ●" } else { "" };
let name_style = if is_selected {
Style::default().fg(text_color).add_modifier(Modifier::BOLD)
} else if is_current {
Style::default().fg(current_color)
} else {
Style::default().fg(text_color)
};
ListItem::new(Line::from(vec![
Span::styled(pointer.to_string(), name_style),
Span::styled(format!("{}{}", name, check), name_style),
]))
})
.collect();
let mut list_state = ListState::default();
list_state.select(Some(self.theme_popup_selected));
let list = List::new(list_items)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(ratatui::widgets::BorderType::Rounded)
.border_style(Style::default().fg(accent))
.title(Span::styled(
" 选择主题 ",
Style::default().fg(accent).add_modifier(Modifier::BOLD),
))
.style(Style::default().bg(popup_bg)),
)
.highlight_style(
Style::default()
.bg(accent)
.fg(popup_bg)
.add_modifier(Modifier::BOLD),
);
f.render_widget(Clear, popup_area);
f.render_stateful_widget(list, popup_area, &mut list_state);
}
}
#[derive(Debug)]
pub enum EditorAction {
Continue,
Submit(String),
Cancel,
}
pub fn open_markdown_editor_on_terminal(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
title: &str,
content: &str,
theme: &EditorTheme,
highlight_fn: HighlightFn,
theme_gallery: Vec<ThemeGalleryItem>,
) -> io::Result<(Option<String>, Option<&'static str>)> {
let mut editor =
MarkdownEditor::new(title, content, theme.clone(), highlight_fn, theme_gallery);
loop {
let size = terminal.size()?;
let area = Rect::new(0, 0, size.width, size.height);
terminal.draw(|f| {
editor.render(f, area);
})?;
if event::poll(std::time::Duration::from_millis(16))? {
let evt = event::read()?;
if let Event::Key(key) = evt {
let input = Input::from_keycode(key.code, key.modifiers);
match editor.handle_input(&input) {
EditorAction::Submit(content) => {
return Ok((Some(content), editor.selected_theme_id()));
}
EditorAction::Cancel => return Ok((None, editor.selected_theme_id())),
EditorAction::Continue => {}
}
}
}
}
}
pub fn open_markdown_editor(
title: &str,
content: &str,
theme: &EditorTheme,
highlight_fn: HighlightFn,
theme_gallery: Vec<ThemeGalleryItem>,
) -> io::Result<(Option<String>, Option<&'static str>)> {
terminal::enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let result = open_markdown_editor_on_terminal(
&mut terminal,
title,
content,
theme,
highlight_fn,
theme_gallery,
);
terminal::disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
result
}
pub fn open_markdown_editor_with_content(
title: &str,
initial_lines: &[String],
theme: &EditorTheme,
highlight_fn: HighlightFn,
theme_gallery: Vec<ThemeGalleryItem>,
) -> io::Result<(Option<String>, Option<&'static str>)> {
let content = initial_lines.join("\n");
open_markdown_editor(title, &content, theme, highlight_fn, theme_gallery)
}