pub mod highlight;
mod render;
use std::collections::HashSet;
pub use self::highlight::Language;
use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
use crate::scroll::ScrollState;
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum CodeBlockMessage {
ScrollUp,
ScrollDown,
PageUp(usize),
PageDown(usize),
Home,
End,
ScrollLeft,
ScrollRight,
SetCode(String),
SetLanguage(Language),
ToggleLineNumbers,
HighlightLine(usize),
UnhighlightLine(usize),
ClearHighlights,
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct CodeBlockState {
pub(crate) code: String,
pub(crate) language: Language,
pub(crate) scroll: ScrollState,
pub(crate) horizontal_offset: usize,
pub(crate) show_line_numbers: bool,
pub(crate) highlight_lines: HashSet<usize>,
pub(crate) title: Option<String>,
}
impl PartialEq for CodeBlockState {
fn eq(&self, other: &Self) -> bool {
self.code == other.code
&& self.language == other.language
&& self.scroll == other.scroll
&& self.horizontal_offset == other.horizontal_offset
&& self.show_line_numbers == other.show_line_numbers
&& self.highlight_lines == other.highlight_lines
&& self.title == other.title
}
}
impl CodeBlockState {
pub fn new() -> Self {
Self::default()
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = code.into();
self.scroll
.set_content_length(self.code.lines().count().max(1));
self
}
pub fn with_language(mut self, language: Language) -> Self {
self.language = language;
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_line_numbers(mut self, show: bool) -> Self {
self.show_line_numbers = show;
self
}
pub fn with_highlight_lines(mut self, lines: Vec<usize>) -> Self {
self.highlight_lines = lines.into_iter().collect();
self
}
pub fn code(&self) -> &str {
&self.code
}
pub fn set_code(&mut self, code: impl Into<String>) {
self.code = code.into();
self.scroll = ScrollState::new(self.code.lines().count().max(1));
}
pub fn line_count(&self) -> usize {
self.code.lines().count().max(1)
}
pub fn language(&self) -> &Language {
&self.language
}
pub fn set_language(&mut self, language: Language) {
self.language = language;
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn set_title(&mut self, title: Option<String>) {
self.title = title;
}
pub fn show_line_numbers(&self) -> bool {
self.show_line_numbers
}
pub fn set_show_line_numbers(&mut self, show: bool) {
self.show_line_numbers = show;
}
pub fn is_line_highlighted(&self, line: usize) -> bool {
self.highlight_lines.contains(&line)
}
pub fn add_highlight_line(&mut self, line: usize) {
self.highlight_lines.insert(line);
}
pub fn remove_highlight_line(&mut self, line: usize) {
self.highlight_lines.remove(&line);
}
pub fn clear_highlights(&mut self) {
self.highlight_lines.clear();
}
pub fn highlighted_lines(&self) -> &HashSet<usize> {
&self.highlight_lines
}
pub fn scroll_offset(&self) -> usize {
self.scroll.offset()
}
pub fn set_scroll_offset(&mut self, offset: usize) {
self.scroll.set_offset(offset);
}
pub fn horizontal_offset(&self) -> usize {
self.horizontal_offset
}
pub fn set_horizontal_offset(&mut self, offset: usize) {
self.horizontal_offset = offset;
}
pub fn update(&mut self, msg: CodeBlockMessage) -> Option<()> {
CodeBlock::update(self, msg)
}
}
pub struct CodeBlock;
impl Component for CodeBlock {
type State = CodeBlockState;
type Message = CodeBlockMessage;
type Output = ();
fn init() -> Self::State {
CodeBlockState::default()
}
fn handle_event(
_state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
let key = event.as_key()?;
let ctrl = key.modifiers.ctrl();
match key.code {
Key::Up | Key::Char('k') if !ctrl => Some(CodeBlockMessage::ScrollUp),
Key::Down | Key::Char('j') if !ctrl => Some(CodeBlockMessage::ScrollDown),
Key::Left | Key::Char('h') if !ctrl => Some(CodeBlockMessage::ScrollLeft),
Key::Right | Key::Char('l') if !ctrl => Some(CodeBlockMessage::ScrollRight),
Key::PageUp => Some(CodeBlockMessage::PageUp(10)),
Key::PageDown => Some(CodeBlockMessage::PageDown(10)),
Key::Char('u') if ctrl => Some(CodeBlockMessage::PageUp(10)),
Key::Char('d') if ctrl => Some(CodeBlockMessage::PageDown(10)),
Key::Char('g') if key.modifiers.shift() => Some(CodeBlockMessage::End),
Key::Home | Key::Char('g') => Some(CodeBlockMessage::Home),
Key::End => Some(CodeBlockMessage::End),
Key::Char('n') if !ctrl => Some(CodeBlockMessage::ToggleLineNumbers),
_ => None,
}
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
CodeBlockMessage::ScrollUp => {
state.scroll.scroll_up();
}
CodeBlockMessage::ScrollDown => {
state.scroll.scroll_down();
}
CodeBlockMessage::PageUp(n) => {
state.scroll.page_up(n);
}
CodeBlockMessage::PageDown(n) => {
state.scroll.page_down(n);
}
CodeBlockMessage::ScrollLeft => {
state.horizontal_offset = state.horizontal_offset.saturating_sub(1);
}
CodeBlockMessage::ScrollRight => {
let max_width = state.code.lines().map(|l| l.len()).max().unwrap_or(0);
if state.horizontal_offset < max_width {
state.horizontal_offset += 1;
}
}
CodeBlockMessage::Home => {
state.scroll.scroll_to_start();
state.horizontal_offset = 0;
}
CodeBlockMessage::End => {
state.scroll.scroll_to_end();
}
CodeBlockMessage::SetCode(code) => {
state.code = code;
state.scroll = ScrollState::new(state.code.lines().count().max(1));
state.horizontal_offset = 0;
}
CodeBlockMessage::SetLanguage(lang) => {
state.language = lang;
}
CodeBlockMessage::ToggleLineNumbers => {
state.show_line_numbers = !state.show_line_numbers;
}
CodeBlockMessage::HighlightLine(line) => {
state.highlight_lines.insert(line);
}
CodeBlockMessage::UnhighlightLine(line) => {
state.highlight_lines.remove(&line);
}
CodeBlockMessage::ClearHighlights => {
state.highlight_lines.clear();
}
}
None
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
render::render(
state,
ctx.frame,
ctx.area,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
#[cfg(test)]
mod tests;