#![allow(unused)]
use crate::util;
use crate::Options;
use crate::CH_HEIGHT;
use crate::CH_WIDTH;
use crate::COMPONENT_NAME;
use cell::Cell;
use css_colors::rgba;
use css_colors::Color;
use css_colors::RGBA;
use line::Line;
use nalgebra::Point2;
use range::Range;
use sauron::html::attributes;
use sauron::jss::jss_ns;
use sauron::prelude::*;
use sauron::Node;
use std::iter::FromIterator;
use ultron_syntaxes_themes::TextHighlighter;
use ultron_syntaxes_themes::{Style, Theme};
#[allow(unused)]
use unicode_width::UnicodeWidthChar;
mod cell;
mod line;
mod range;
pub struct TextBuffer {
options: Options,
lines: Vec<Line>,
text_highlighter: TextHighlighter,
cursor: Point2<usize>,
#[allow(unused)]
selection_start: Option<Point2<usize>>,
#[allow(unused)]
selection_end: Option<Point2<usize>>,
focused_cell: Option<FocusCell>,
}
#[derive(Clone, Copy, Debug)]
struct FocusCell {
line_index: usize,
range_index: usize,
cell_index: usize,
cell: Option<Cell>,
}
impl TextBuffer {
pub fn from_str(options: Options, content: &str) -> Self {
let mut text_highlighter = TextHighlighter::default();
if let Some(theme_name) = &options.theme_name {
log::trace!("Selecting theme: {}", theme_name);
text_highlighter.select_theme(theme_name);
}
let mut this = Self {
lines: Self::highlight_content(
content,
&text_highlighter,
&options.syntax_token,
),
text_highlighter,
cursor: Point2::new(0, 0),
selection_start: None,
selection_end: None,
focused_cell: None,
options,
};
this.calculate_focused_cell();
this
}
pub fn clear(&mut self) {
self.lines.clear();
}
pub fn set_selection(&mut self, start: Point2<usize>, end: Point2<usize>) {
self.selection_start = Some(start);
self.selection_end = Some(end);
}
pub fn normalize_selection(
&self,
) -> Option<(Point2<usize>, Point2<usize>)> {
if let (Some(start), Some(end)) =
(self.selection_start, self.selection_end)
{
Some(util::normalize_points(start, end))
} else {
None
}
}
pub(crate) fn in_selection(
&self,
line_index: usize,
range_index: usize,
cell_index: usize,
) -> bool {
let x = self.lines[line_index]
.calc_range_cell_index_to_x(range_index, cell_index);
let y = line_index;
if let Some((start, end)) = self.normalize_selection() {
if self.options.use_block_mode {
x >= start.x && x <= end.x && y >= start.y && y <= end.y
} else {
if y > start.y && y < end.y {
true
} else {
let same_start_line = y == start.y;
let same_end_line = y == end.y;
if same_start_line && same_end_line {
x >= start.x && x <= end.x
} else if same_start_line {
x >= start.x
} else if same_end_line {
x <= end.x
} else {
false
}
}
}
} else {
false
}
}
pub(crate) fn selected_text(&self) -> Option<String> {
if let (Some(start), Some(_end)) =
(self.selection_start, self.selection_end)
{
let mut buffer = TextBuffer::from_str(Options::default(), "");
for (line_index, line) in self.lines.iter().enumerate() {
let y = line_index;
for (range_index, range) in line.ranges.iter().enumerate() {
for (cell_index, cell) in range.cells.iter().enumerate() {
let x = line.calc_range_cell_index_to_x(
range_index,
cell_index,
);
if self.in_selection(
line_index,
range_index,
cell_index,
) {
if self.options.use_block_mode {
buffer.insert_char(
x - start.x,
y - start.y,
cell.ch,
);
} else {
buffer.insert_char(x, y - start.y, cell.ch);
}
}
}
}
}
Some(buffer.to_string())
} else {
None
}
}
pub fn in_bounds(&self, point: Point2<i32>) -> bool {
let bound = self.bounds();
point.x >= 0 && point.y >= 0 && point.x <= bound.x && point.y <= bound.y
}
pub fn bounds(&self) -> Point2<i32> {
let total_lines = self.lines.len() as i32;
let max_column =
self.lines.iter().map(|line| line.width).max().unwrap_or(0) as i32;
Point2::new(max_column, total_lines)
}
pub fn set_options(&mut self, options: Options) {
self.options = options;
}
fn highlight_content(
content: &str,
text_highlighter: &TextHighlighter,
syntax_token: &str,
) -> Vec<Line> {
let (mut line_highlighter, syntax_set) =
text_highlighter.get_line_highlighter(syntax_token);
content
.lines()
.map(|line| {
let line_str = String::from_iter(line.chars());
let style_range: Vec<(Style, &str)> =
line_highlighter.highlight(&line_str, syntax_set);
let ranges: Vec<Range> = style_range
.into_iter()
.map(|(style, range_str)| {
let cells =
range_str.chars().map(Cell::from_char).collect();
Range::from_cells(cells, style)
})
.collect();
Line::from_ranges(ranges)
})
.collect()
}
fn calculate_focused_cell(&mut self) {
self.focused_cell = self.find_focused_cell();
}
fn find_focused_cell(&self) -> Option<FocusCell> {
let line_index = self.cursor.y;
if let Some(line) = self.lines.get(line_index) {
if let Some((range_index, cell_index)) =
line.calc_range_cell_index_position(self.cursor.x)
{
if let Some(range) = line.ranges.get(range_index) {
return Some(FocusCell {
line_index,
range_index,
cell_index,
cell: range.cells.get(cell_index).cloned(),
});
}
}
}
return None;
}
fn is_focused_line(&self, line_index: usize) -> bool {
if let Some(focused_cell) = self.focused_cell {
focused_cell.matched_line(line_index)
} else {
false
}
}
fn is_focused_range(&self, line_index: usize, range_index: usize) -> bool {
if let Some(focused_cell) = self.focused_cell {
focused_cell.matched_range(line_index, range_index)
} else {
false
}
}
fn is_focused_cell(
&self,
line_index: usize,
range_index: usize,
cell_index: usize,
) -> bool {
if let Some(focused_cell) = self.focused_cell {
focused_cell.matched(line_index, range_index, cell_index)
} else {
false
}
}
pub(crate) fn active_theme(&self) -> &Theme {
self.text_highlighter.active_theme()
}
pub(crate) fn gutter_background(&self) -> Option<RGBA> {
self.active_theme().settings.gutter.map(util::to_rgba)
}
pub(crate) fn gutter_foreground(&self) -> Option<RGBA> {
self.active_theme()
.settings
.gutter_foreground
.map(util::to_rgba)
}
pub(crate) fn theme_background(&self) -> Option<RGBA> {
self.active_theme().settings.background.map(util::to_rgba)
}
pub(crate) fn selection_background(&self) -> Option<RGBA> {
self.active_theme().settings.selection.map(util::to_rgba)
}
#[allow(unused)]
pub(crate) fn selection_foreground(&self) -> Option<RGBA> {
self.active_theme()
.settings
.selection_foreground
.map(util::to_rgba)
}
pub(crate) fn cursor_color(&self) -> Option<RGBA> {
self.active_theme().settings.caret.map(util::to_rgba)
}
fn numberline_wide(&self) -> usize {
if self.options.show_line_numbers {
self.lines.len().to_string().len()
} else {
0
}
}
pub(crate) fn numberline_padding_wide(&self) -> usize {
1
}
#[allow(unused)]
pub(crate) fn get_numberline_wide(&self) -> usize {
if self.options.show_line_numbers {
self.numberline_wide() + self.numberline_padding_wide()
} else {
0
}
}
pub fn view<MSG>(&self) -> Node<MSG> {
let class_ns = |class_names| {
attributes::class_namespaced(COMPONENT_NAME, class_names)
};
let class_number_wide =
format!("number_wide{}", self.numberline_wide());
let theme_background =
self.theme_background().unwrap_or(rgba(0, 0, 255, 1.0));
let code_attributes = [
class_ns("code"),
class_ns(&class_number_wide),
if self.options.use_background {
style! {background: theme_background.to_css()}
} else {
empty_attr()
},
];
let rendered_lines = self
.lines
.iter()
.enumerate()
.map(|(line_index, line)| line.view_line(&self, line_index));
if self.options.use_for_ssg {
div(code_attributes, rendered_lines)
} else {
pre(
[class_ns("code_wrapper")],
[code(code_attributes, rendered_lines)],
)
}
}
pub fn style(&self) -> String {
let selection_bg = self
.selection_background()
.unwrap_or(rgba(100, 100, 100, 0.5));
let cursor_color = self.cursor_color().unwrap_or(rgba(255, 0, 0, 1.0));
jss_ns! {COMPONENT_NAME,
".code_wrapper": {
margin: 0,
},
".code": {
position: "relative",
font_size: px(14),
cursor: "text",
display: "block",
min_width: "max-content",
},
".line_block": {
display: "block",
height: px(CH_HEIGHT),
},
".number__line": {
display: "flex",
height: px(CH_HEIGHT),
},
".number": {
flex: "none", text_align: "right",
background_color: "#002b36",
padding_right: px(CH_WIDTH * self.numberline_padding_wide() as u32),
height: px(CH_HEIGHT),
user_select: "none",
},
".number_wide1 .number": {
width: px(1 * CH_WIDTH),
},
".number_wide2 .number": {
width: px(2 * CH_WIDTH),
},
".number_wide3 .number": {
width: px(3 * CH_WIDTH),
},
".number_wide4 .number": {
width: px(4 * CH_WIDTH),
},
".number_wide5 .number": {
width: px(5 * CH_WIDTH),
},
".line": {
flex: "none", height: px(CH_HEIGHT),
overflow: "hidden",
display: "inline-block",
},
".filler": {
width: percent(100),
},
".line_focused": {
},
".range": {
flex: "none",
height: px(CH_HEIGHT),
overflow: "hidden",
display: "inline-block",
},
".line .ch": {
width: px(CH_WIDTH),
height: px(CH_HEIGHT),
font_stretch: "ultra-condensed",
font_variant_numeric: "slashed-zero",
font_kerning: "none",
font_size_adjust: "none",
font_optical_sizing: "none",
position: "relative",
overflow: "hidden",
align_items: "center",
line_height: 1,
display: "inline-block",
},
".line .ch::selection": {
"background-color": selection_bg.to_css(),
},
".ch.selected": {
background_color:selection_bg.to_css(),
},
".virtual_cursor": {
position: "absolute",
width: px(CH_WIDTH),
height: px(CH_HEIGHT),
background_color: cursor_color.to_css(),
},
".ch .cursor": {
position: "absolute",
left: 0,
width : px(CH_WIDTH),
height: px(CH_HEIGHT),
background_color: cursor_color.to_css(),
display: "inline",
animation: "cursor_blink-anim 1000ms step-end infinite",
},
".ch.wide2 .cursor": {
width: px(2 * CH_WIDTH),
},
".ch.wide3 .cursor": {
width: px(3 * CH_WIDTH),
},
".thin_cursor .cursor": {
width: px(2),
},
".block_cursor .cursor": {
width: px(CH_WIDTH),
},
".line .ch.wide2": {
width: px(2 * CH_WIDTH),
font_size: px(13),
},
".line .ch.wide3": {
width: px(3 * CH_WIDTH),
font_size: px(13),
},
"@keyframes cursor_blink-anim": {
"50%": {
background_color: "transparent",
border_color: "transparent",
},
"100%": {
background_color: cursor_color.to_css(),
border_color: "transparent",
},
},
}
}
}
impl TextBuffer {
pub(crate) fn total_lines(&self) -> usize {
self.lines.len()
}
pub(crate) fn is_in_virtual_position(&self) -> bool {
self.focused_cell.is_none()
}
pub(crate) fn rehighlight(&mut self) {
self.lines = Self::highlight_content(
&self.to_string(),
&self.text_highlighter,
&self.options.syntax_token,
);
}
#[allow(unused)]
pub(crate) fn line_width(&self, n: usize) -> Option<usize> {
self.lines.get(n).map(|l| l.width)
}
fn add_lines(&mut self, n: usize) {
for _i in 0..n {
self.lines.push(Line::default());
}
}
fn add_cell(&mut self, y: usize, n: usize) {
let ch = ' ';
for _i in 0..n {
self.lines[y].push_char(ch);
}
}
pub(crate) fn break_line(&mut self, x: usize, y: usize) {
if let Some(line) = self.lines.get_mut(y) {
let (range_index, col) = line
.calc_range_cell_index_position(x)
.unwrap_or(line.last_range_cell_index());
if let Some(range_bound) = line.ranges.get_mut(range_index) {
range_bound.recalc_width();
let mut other = range_bound.split_at(col);
other.recalc_width();
let mut rest =
line.ranges.drain(range_index + 1..).collect::<Vec<_>>();
rest.insert(0, other);
self.insert_line(y + 1, Line::from_ranges(rest));
} else {
self.insert_line(y, Line::default());
}
}
}
fn assert_chars(&self, ch: char) {
assert!(
ch != '\n',
"line breaks should have been pre-processed before this point"
);
assert!(
ch != '\t',
"tabs should have been pre-processed before this point"
);
}
pub fn insert_char(&mut self, x: usize, y: usize, ch: char) {
self.assert_chars(ch);
self.ensure_cell_exist(x, y);
let (range_index, cell_index) = self.lines[y]
.calc_range_cell_index_position(x)
.unwrap_or(self.lines[y].last_range_cell_index());
self.lines[y].insert_char(range_index, cell_index, ch);
}
pub(crate) fn insert_text(&mut self, x: usize, y: usize, text: &str) {
let mut new_line = y;
let mut new_col = x;
let lines: Vec<&str> = text.lines().collect();
let is_multi_line = lines.len() > 1;
if is_multi_line {
self.break_line(x, y);
}
for line in lines {
for ch in line.chars() {
let width = ch.width().unwrap_or_else(|| {
panic!("must have a unicode width for {:?}", ch)
});
self.insert_char(new_col, new_line, ch);
new_col += width;
}
new_col = 0;
new_line += 1;
self.break_line(new_col, new_line);
self.cursor.y = new_line;
}
}
pub fn replace_char(&mut self, x: usize, y: usize, ch: char) {
self.assert_chars(ch);
self.ensure_cell_exist(x + 1, y);
let (range_index, cell_index) = self.lines[y]
.calc_range_cell_index_position(x)
.expect("the range_index and cell_index must have existed at this point");
self.lines[y].replace_char(range_index, cell_index, ch);
}
pub(crate) fn delete_char(&mut self, x: usize, y: usize) {
if let Some(line) = self.lines.get_mut(y) {
if let Some((range_index, col)) =
line.calc_range_cell_index_position(x)
{
if let Some(range) = line.ranges.get_mut(range_index) {
if range.cells.get(col).is_some() {
range.cells.remove(col);
}
}
}
}
}
fn ensure_cell_exist(&mut self, x: usize, y: usize) {
self.ensure_line_exist(y);
let cell_gap = x.saturating_sub(self.lines[y].width);
if cell_gap > 0 {
self.add_cell(y, cell_gap);
}
}
fn ensure_line_exist(&mut self, y: usize) {
let line_gap = y.saturating_add(1).saturating_sub(self.total_lines());
if line_gap > 0 {
self.add_lines(line_gap);
}
}
fn insert_line(&mut self, line_index: usize, line: Line) {
self.ensure_line_exist(line_index.saturating_sub(1));
self.lines.insert(line_index, line);
}
pub(crate) fn get_position(&self) -> Point2<usize> {
self.cursor
}
}
impl TextBuffer {
pub(crate) fn command_insert_char(&mut self, ch: char) {
self.insert_char(self.cursor.x, self.cursor.y, ch);
let width = ch.width().expect("must have a unicode width");
self.move_x(width);
}
pub(crate) fn command_replace_char(&mut self, ch: char) {
self.replace_char(self.cursor.x, self.cursor.y, ch);
}
pub(crate) fn command_insert_text(&mut self, text: &str) {
use unicode_width::UnicodeWidthStr;
self.insert_text(self.cursor.x, self.cursor.y, text);
self.move_x(text.width());
}
pub(crate) fn move_left(&mut self) {
self.cursor.x = self.cursor.x.saturating_sub(1);
self.calculate_focused_cell();
}
pub(crate) fn move_left_start(&mut self) {
self.cursor.x = 0;
self.calculate_focused_cell();
}
pub(crate) fn move_right(&mut self) {
self.cursor.x = self.cursor.x.saturating_add(1);
self.calculate_focused_cell();
}
pub(crate) fn move_x(&mut self, x: usize) {
self.cursor.x = self.cursor.x.saturating_add(x);
self.calculate_focused_cell();
}
pub(crate) fn move_up(&mut self) {
self.cursor.y = self.cursor.y.saturating_sub(1);
self.calculate_focused_cell();
}
pub(crate) fn move_down(&mut self) {
self.cursor.y = self.cursor.y.saturating_add(1);
self.calculate_focused_cell();
}
pub(crate) fn set_position(&mut self, x: usize, y: usize) {
self.cursor.x = x;
self.cursor.y = y;
self.calculate_focused_cell();
}
pub(crate) fn command_break_line(&mut self) {
self.break_line(self.cursor.x, self.cursor.y);
self.move_left_start();
self.move_down();
}
pub(crate) fn command_delete_back(&mut self) {
if self.cursor.x > 0 {
self.delete_char(self.cursor.x.saturating_sub(1), self.cursor.y);
self.move_left();
}
}
pub(crate) fn command_delete_forward(&mut self) {
self.delete_char(self.cursor.x, self.cursor.y);
self.calculate_focused_cell();
}
}
impl ToString for TextBuffer {
fn to_string(&self) -> String {
self.lines
.iter()
.map(|line| line.text())
.collect::<Vec<_>>()
.join("\n")
}
}
impl FocusCell {
fn matched(
&self,
line_index: usize,
range_index: usize,
cell_index: usize,
) -> bool {
self.line_index == line_index
&& self.range_index == range_index
&& self.cell_index == cell_index
}
fn matched_line(&self, line_index: usize) -> bool {
self.line_index == line_index
}
fn matched_range(&self, line_index: usize, range_index: usize) -> bool {
self.line_index == line_index && self.range_index == range_index
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_ensure_line_exist() {
let mut buffer = TextBuffer::from_str(Options::default(), "");
buffer.ensure_line_exist(10);
assert!(buffer.lines.get(10).is_some());
assert_eq!(buffer.total_lines(), 11);
}
}