Compare commits
27 Commits
052828c89c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9de22b9680 | |||
| 3f336578f5 | |||
| 5fbff13428 | |||
| 7a23ee5bc0 | |||
| ea2e633d7d | |||
| d9f29434e9 | |||
| 4239844e0e | |||
| 5f9cd85faf | |||
| 74955032cc | |||
| 7b2eb751ab | |||
| b41a69781c | |||
| f654ce37a6 | |||
| d5d58694bb | |||
| c2e0661a45 | |||
| 6ec7d90ac5 | |||
| 1825460074 | |||
| 53dcf2ffc9 | |||
| 86756a94ef | |||
| d242e1af21 | |||
| abffe6073f | |||
| d983995e8f | |||
| 045d1d6554 | |||
| 9691268d3d | |||
| f3356c1398 | |||
| b3b2c59a36 | |||
| 077b53f6ff | |||
| 0c78e7834b |
@@ -2,8 +2,12 @@
|
|||||||
|
|
||||||
*New* Spreadsheet Calculator Improved
|
*New* Spreadsheet Calculator Improved
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keybinds and not many features.
|
Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keybinds and not many features.
|
||||||
|
|
||||||
|
[Checkout a demo file](readme/demo.nscim), download, then open with `neoscim demo.nscim`.
|
||||||
|
|
||||||
## Keybinds
|
## Keybinds
|
||||||
|
|
||||||
### Normal mode
|
### Normal mode
|
||||||
@@ -21,6 +25,8 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
|
|||||||
| `gg` | Go to beginning of the column |
|
| `gg` | Go to beginning of the column |
|
||||||
| `G` | Go to end of column |
|
| `G` | Go to end of column |
|
||||||
| `gG` | Go to end of the the visual column |
|
| `gG` | Go to end of the the visual column |
|
||||||
|
| `u`/`Page Down` | Undo[^undo] |
|
||||||
|
| `Page Up` | Redo[^undo] |
|
||||||
| `i`/`a` | Enter insert mode on current cell |
|
| `i`/`a` | Enter insert mode on current cell |
|
||||||
| `r` | Enter insert mode on current cell, deleting contents |
|
| `r` | Enter insert mode on current cell, deleting contents |
|
||||||
| `v` | Enter visual mode |
|
| `v` | Enter visual mode |
|
||||||
@@ -39,6 +45,8 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
|
|||||||
| n`G` | Jump to row "n" |
|
| n`G` | Jump to row "n" |
|
||||||
| nX | Press "X", "n" times |
|
| nX | Press "X", "n" times |
|
||||||
|
|
||||||
|
[^undo]: Page up/down keybinds are (probably) temporary until I can get ctrl+r to work for redo, which is the propper key for this action. See issue #25 for the status on this.
|
||||||
|
|
||||||
### Visual mode
|
### Visual mode
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
@@ -64,6 +72,7 @@ These commands operate on selections.
|
|||||||
| - | - |
|
| - | - |
|
||||||
| `:plot` | Plots the current selection to `plot.png` using gnuplot |
|
| `:plot` | Plots the current selection to `plot.png` using gnuplot |
|
||||||
| `:export <filename>` | Exports the current selection to a new file |
|
| `:export <filename>` | Exports the current selection to a new file |
|
||||||
|
| `:fill <value>` | (Aliased as `f`) Fill the selection with `<value>`, special variables `x`,`y`,`xi`, and `yi` are avaliable. Variables `x` and `y` are the global coordinates of the cell, and `xi` and `yi` are the local coordinates of the selection. |
|
||||||
|
|
||||||
## Math / Functions
|
## Math / Functions
|
||||||
|
|
||||||
|
|||||||
21
readme/demo.nscim
Normal file
21
readme/demo.nscim
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
,,,
|
||||||
|
Most of vim's movement works here as well,,,
|
||||||
|
,,,
|
||||||
|
,i,,
|
||||||
|
j,Movement,l,
|
||||||
|
,j,,
|
||||||
|
,,,
|
||||||
|
,"insert text with r, i, or a",,
|
||||||
|
,,,
|
||||||
|
1,,,
|
||||||
|
1,Text can overflow cells,,
|
||||||
|
1,But may get cut off if it's too long,,10
|
||||||
|
1,,,If it reaches another cell
|
||||||
|
1,,,
|
||||||
|
Total:,=sum(A:A),Sum A:A,
|
||||||
|
,=math::log2(B14),Do math on the output of another function,
|
||||||
|
,,,
|
||||||
|
,,,
|
||||||
|
,,11,Number.
|
||||||
|
,,=C18+1,Referencing the number and adding 1
|
||||||
|
,,,Copying the cell down will translate the reference
|
||||||
BIN
readme/screenshot.png
Normal file
BIN
readme/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
122
src/app/app.rs
122
src/app/app.rs
@@ -18,7 +18,10 @@ use ratatui::{
|
|||||||
use crate::app::{
|
use crate::app::{
|
||||||
clipboard::Clipboard,
|
clipboard::Clipboard,
|
||||||
error_msg::StatusMessage,
|
error_msg::StatusMessage,
|
||||||
logic::{self, calc::{Grid, get_header_size}, cell::CellType},
|
logic::{
|
||||||
|
calc::{Grid, LEN, get_header_size},
|
||||||
|
cell::CellType,
|
||||||
|
},
|
||||||
mode::Mode,
|
mode::Mode,
|
||||||
screen::ScreenSpace,
|
screen::ScreenSpace,
|
||||||
};
|
};
|
||||||
@@ -39,9 +42,11 @@ pub struct App {
|
|||||||
|
|
||||||
impl Widget for &App {
|
impl Widget for &App {
|
||||||
fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) {
|
fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) {
|
||||||
|
// let now = std::time::Instant::now();
|
||||||
|
|
||||||
let (x_max, y_max) = self.screen.how_many_cells_fit_in(&area, &self.vars);
|
let (x_max, y_max) = self.screen.how_many_cells_fit_in(&area, &self.vars);
|
||||||
|
|
||||||
let is_selected = |x: usize, y: usize| -> bool {
|
let is_visually_selected = |x: usize, y: usize| -> bool {
|
||||||
if let Mode::Visual((mut x1, mut y1)) | Mode::VisualCmd((mut x1, mut y1), _) = self.mode {
|
if let Mode::Visual((mut x1, mut y1)) | Mode::VisualCmd((mut x1, mut y1), _) = self.mode {
|
||||||
let (mut x2, mut y2) = self.grid.cursor();
|
let (mut x2, mut y2) = self.grid.cursor();
|
||||||
x1 += 1;
|
x1 += 1;
|
||||||
@@ -60,10 +65,40 @@ impl Widget for &App {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
for x in 0..x_max {
|
// cells that are related by reference to the cursor's cell
|
||||||
for y in 0..y_max {
|
// (inputs to formulas and such)
|
||||||
let mut display = String::new();
|
let cells_of_interest: Vec<(usize, usize)> = {
|
||||||
let mut style = Style::new().fg(Color::White);
|
let ctx = crate::app::logic::ctx::ExtractionContext::new();
|
||||||
|
let (x, y) = self.grid.cursor();
|
||||||
|
if let Some(cell) = self.grid.get_cell_raw(x, y) {
|
||||||
|
if let CellType::Equation(eq) = cell {
|
||||||
|
let _ = evalexpr::eval_with_context(&eq[1..], &ctx);
|
||||||
|
let vars = ctx.dump_vars();
|
||||||
|
|
||||||
|
let mut interest = Vec::new();
|
||||||
|
for var in vars {
|
||||||
|
if let Some(a) = Grid::parse_to_idx(&var) {
|
||||||
|
interest.push(a);
|
||||||
|
} else if let Some((start, end)) = Grid::range_as_indices(&var) {
|
||||||
|
// insert coords:
|
||||||
|
// (start, 0..len)
|
||||||
|
// ..
|
||||||
|
// (end, 0..len)
|
||||||
|
for x in start..=end {
|
||||||
|
for y in 0..=super::logic::calc::LEN {
|
||||||
|
interest.push((x,y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interest
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Custom width for the header of each row
|
// Custom width for the header of each row
|
||||||
let row_header_width = get_header_size() as u16;
|
let row_header_width = get_header_size() as u16;
|
||||||
@@ -72,6 +107,11 @@ impl Widget for &App {
|
|||||||
let cell_width = self.screen.get_cell_width(&self.vars) as u16;
|
let cell_width = self.screen.get_cell_width(&self.vars) as u16;
|
||||||
let cell_height = self.screen.get_cell_height(&self.vars) as u16;
|
let cell_height = self.screen.get_cell_height(&self.vars) as u16;
|
||||||
|
|
||||||
|
for x in 0..x_max {
|
||||||
|
for y in 0..y_max {
|
||||||
|
let mut display = String::new();
|
||||||
|
let mut style = Style::new();
|
||||||
|
|
||||||
// Minus 1 because of header cells,
|
// Minus 1 because of header cells,
|
||||||
// the grid is shifted over (1,1), so if you need
|
// the grid is shifted over (1,1), so if you need
|
||||||
// to index the grid, these are you values.
|
// to index the grid, these are you values.
|
||||||
@@ -84,6 +124,11 @@ impl Widget for &App {
|
|||||||
y_idx = y as usize - 1 + self.screen.scroll_y();
|
y_idx = y as usize - 1 + self.screen.scroll_y();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// don't render non-accessible cells
|
||||||
|
if x_idx > LEN-1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const ORANGE1: Color = Color::Rgb(200, 160, 0);
|
const ORANGE1: Color = Color::Rgb(200, 160, 0);
|
||||||
const ORANGE2: Color = Color::Rgb(180, 130, 0);
|
const ORANGE2: Color = Color::Rgb(180, 130, 0);
|
||||||
|
|
||||||
@@ -94,8 +139,8 @@ impl Widget for &App {
|
|||||||
fn center_text(text: &str, avaliable_space: i32) -> String {
|
fn center_text(text: &str, avaliable_space: i32) -> String {
|
||||||
let margin = avaliable_space - text.len() as i32;
|
let margin = avaliable_space - text.len() as i32;
|
||||||
let margin = margin / 2;
|
let margin = margin / 2;
|
||||||
let l_margin = (0..margin).into_iter().map(|_| ' ').collect::<String>();
|
let l_margin = (0..margin).map(|_| ' ').collect::<String>();
|
||||||
let r_margin = (0..(margin-(l_margin.len() as i32))).into_iter().map(|_| ' ').collect::<String>();
|
let r_margin = (0..(margin - (l_margin.len() as i32))).map(|_| ' ').collect::<String>();
|
||||||
format!("{l_margin}{text}{r_margin}")
|
format!("{l_margin}{text}{r_margin}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +156,7 @@ impl Widget for &App {
|
|||||||
|
|
||||||
let bg = if y_idx == self.grid.cursor().1 {
|
let bg = if y_idx == self.grid.cursor().1 {
|
||||||
Color::DarkGray
|
Color::DarkGray
|
||||||
} else if y_idx % 2 == 0 {
|
} else if y_idx.is_multiple_of(2) {
|
||||||
ORANGE1
|
ORANGE1
|
||||||
} else {
|
} else {
|
||||||
ORANGE2
|
ORANGE2
|
||||||
@@ -124,7 +169,7 @@ impl Widget for &App {
|
|||||||
|
|
||||||
let bg = if x_idx == self.grid.cursor().0 {
|
let bg = if x_idx == self.grid.cursor().0 {
|
||||||
Color::DarkGray
|
Color::DarkGray
|
||||||
} else if x_idx % 2 == 0 {
|
} else if x_idx.is_multiple_of(2) {
|
||||||
ORANGE1
|
ORANGE1
|
||||||
} else {
|
} else {
|
||||||
ORANGE2
|
ORANGE2
|
||||||
@@ -148,6 +193,9 @@ impl Widget for &App {
|
|||||||
Ok(val) => {
|
Ok(val) => {
|
||||||
display = val.to_string();
|
display = val.to_string();
|
||||||
style = Style::new()
|
style = Style::new()
|
||||||
|
.fg(Color::White)
|
||||||
|
// TODO This breaks dumb terminals like the windows
|
||||||
|
// terminal
|
||||||
.underline_color(Color::DarkGray)
|
.underline_color(Color::DarkGray)
|
||||||
.add_modifier(Modifier::UNDERLINED);
|
.add_modifier(Modifier::UNDERLINED);
|
||||||
}
|
}
|
||||||
@@ -163,11 +211,12 @@ impl Widget for &App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
// Allow for text in one cell to visually overflow into empty cells
|
// Allow for text in one cell to visually overflow into empty cells
|
||||||
suggest_upper_bound = Some(display.len() as u16);
|
suggest_upper_bound = Some(display.len() as u16);
|
||||||
// check for cells to the right, see if we should truncate the cell width
|
// check for cells to the right, see if we should truncate the cell width
|
||||||
for i in 1..(display.len() as f32 / cell_width as f32).ceil() as usize {
|
for i in 1..(display.len() as f32 / cell_width as f32).ceil() as usize {
|
||||||
if let Some(_) = self.grid.get_cell_raw(x_idx + i, y_idx) {
|
if self.grid.get_cell_raw(x_idx + i, y_idx).is_some() {
|
||||||
suggest_upper_bound = Some(cell_width * i as u16);
|
suggest_upper_bound = Some(cell_width * i as u16);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -179,18 +228,25 @@ impl Widget for &App {
|
|||||||
display.push('…');
|
display.push('…');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ===================================================
|
||||||
}
|
}
|
||||||
// Don't render blank cells
|
// Don't render blank cells
|
||||||
None => should_render = false,
|
None => should_render = false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_selected(x.into(), y.into()) {
|
if cells_of_interest.contains(&(x_idx, y_idx)) {
|
||||||
|
style = style.fg(Color::Yellow);
|
||||||
|
should_render = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_visually_selected(x.into(), y.into()) {
|
||||||
style = style.bg(Color::Blue);
|
style = style.bg(Color::Blue);
|
||||||
// Make it so that cells render when selected. This fixes issue #32
|
// Make it so that cells render when selected. This fixes issue #32
|
||||||
should_render = true;
|
should_render = true;
|
||||||
}
|
}
|
||||||
if (x_idx, y_idx) == self.grid.cursor() {
|
if (x_idx, y_idx) == self.grid.cursor() {
|
||||||
should_render = true;
|
should_render = true;
|
||||||
|
|
||||||
style = Style::new().fg(Color::Black).bg(Color::White);
|
style = Style::new().fg(Color::Black).bg(Color::White);
|
||||||
// modify the style of the cell you are editing
|
// modify the style of the cell you are editing
|
||||||
if let Mode::Insert(_) = self.mode {
|
if let Mode::Insert(_) = self.mode {
|
||||||
@@ -216,7 +272,7 @@ impl Widget for &App {
|
|||||||
} else if let Some(suggestion) = suggest_upper_bound {
|
} else if let Some(suggestion) = suggest_upper_bound {
|
||||||
let max_available_width = area.width - x_off;
|
let max_available_width = area.width - x_off;
|
||||||
// draw the biggest cell possible, without going OOB off the screen
|
// draw the biggest cell possible, without going OOB off the screen
|
||||||
let width = min(max_available_width, suggestion as u16);
|
let width = min(max_available_width, suggestion);
|
||||||
// Don't draw too small tho, we want full-sized cells, minium
|
// Don't draw too small tho, we want full-sized cells, minium
|
||||||
let width = max(cell_width, width);
|
let width = max(cell_width, width);
|
||||||
|
|
||||||
@@ -229,6 +285,9 @@ impl Widget for &App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// let ns = now.elapsed().as_nanos() as f64;
|
||||||
|
// eprintln!("Rendered in {}ms", ns/1_000_000.);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +333,6 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn file_name_display(&self) -> String {
|
fn file_name_display(&self) -> String {
|
||||||
let file_name_status = {
|
|
||||||
let mut file_name = "[No Name]";
|
let mut file_name = "[No Name]";
|
||||||
let mut icon = "";
|
let mut icon = "";
|
||||||
if let Some(file) = &self.file {
|
if let Some(file) = &self.file {
|
||||||
@@ -288,8 +346,6 @@ impl App {
|
|||||||
icon = "[+]";
|
icon = "[+]";
|
||||||
}
|
}
|
||||||
format!("{file_name}{icon}")
|
format!("{file_name}{icon}")
|
||||||
};
|
|
||||||
file_name_status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, frame: &mut Frame) {
|
fn draw(&self, frame: &mut Frame) {
|
||||||
@@ -320,6 +376,8 @@ impl App {
|
|||||||
let cmd_line_left = cmd_line_split[0];
|
let cmd_line_left = cmd_line_split[0];
|
||||||
let cmd_line_status = cmd_line_split[1];
|
let cmd_line_status = cmd_line_split[1];
|
||||||
let cmd_line_right = cmd_line_split[2];
|
let cmd_line_right = cmd_line_split[2];
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
let cmd_line_debug = cmd_line_split[3];
|
let cmd_line_debug = cmd_line_split[3];
|
||||||
// ======================================================
|
// ======================================================
|
||||||
|
|
||||||
@@ -354,7 +412,7 @@ impl App {
|
|||||||
event::KeyCode::Char(c) => chord.add_char(c),
|
event::KeyCode::Char(c) => chord.add_char(c),
|
||||||
event::KeyCode::Enter => {
|
event::KeyCode::Enter => {
|
||||||
// tmp is to get around reference issues.
|
// tmp is to get around reference issues.
|
||||||
let tmp = pos.clone();
|
let tmp = *pos;
|
||||||
Mode::process_cmd(self);
|
Mode::process_cmd(self);
|
||||||
self.mode = Mode::Visual(tmp)
|
self.mode = Mode::Visual(tmp)
|
||||||
}
|
}
|
||||||
@@ -384,17 +442,20 @@ impl App {
|
|||||||
event::KeyCode::Enter => {
|
event::KeyCode::Enter => {
|
||||||
let v = editor.as_string();
|
let v = editor.as_string();
|
||||||
|
|
||||||
|
let cursor = self.grid.cursor();
|
||||||
|
self.grid.transact_on_grid(|grid| {
|
||||||
// try to insert as a float
|
// try to insert as a float
|
||||||
if let Ok(v) = v.parse::<f64>() {
|
if let Ok(v) = v.parse::<f64>() {
|
||||||
self.grid.set_cell_raw(self.grid.cursor(), Some(v));
|
grid.set_cell_raw(cursor, Some(v));
|
||||||
} else {
|
} else {
|
||||||
// if you can't, then insert as a string
|
// if you can't, then insert as a string
|
||||||
if !v.is_empty() {
|
if !v.is_empty() {
|
||||||
self.grid.set_cell_raw(self.grid.cursor(), Some(v));
|
grid.set_cell_raw(cursor, Some(v.to_owned()));
|
||||||
} else {
|
} else {
|
||||||
self.grid.set_cell_raw::<CellType>(self.grid.cursor(), None);
|
grid.set_cell_raw::<CellType>(cursor, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
self.mode = Mode::Normal;
|
self.mode = Mode::Normal;
|
||||||
}
|
}
|
||||||
@@ -410,9 +471,24 @@ impl App {
|
|||||||
},
|
},
|
||||||
Mode::Normal => match event::read()? {
|
Mode::Normal => match event::read()? {
|
||||||
event::Event::Key(key_event) => match key_event.code {
|
event::Event::Key(key_event) => match key_event.code {
|
||||||
event::KeyCode::F(_) => todo!(),
|
event::KeyCode::F(_n) => {}
|
||||||
event::KeyCode::Char(c) => {
|
event::KeyCode::Char(c) => Mode::process_key(self, c),
|
||||||
Mode::process_key(self, c);
|
// Pretend that the arrow keys are vim movement keys
|
||||||
|
event::KeyCode::Left => Mode::process_key(self, 'h'),
|
||||||
|
event::KeyCode::Right => Mode::process_key(self, 'l'),
|
||||||
|
event::KeyCode::Up => Mode::process_key(self, 'k'),
|
||||||
|
event::KeyCode::Down => Mode::process_key(self, 'j'),
|
||||||
|
// Getting ctrl to work isn't going will right now. Use page keys for the time being.
|
||||||
|
event::KeyCode::PageUp => self.grid.redo(),
|
||||||
|
event::KeyCode::PageDown => self.grid.undo(),
|
||||||
|
event::KeyCode::Modifier(modifier_key_code) => {
|
||||||
|
if let event::ModifierKeyCode::LeftControl | event::ModifierKeyCode::RightControl =
|
||||||
|
modifier_key_code
|
||||||
|
{
|
||||||
|
// TODO my terminal (alacritty) isn't showing me ctrl presses. I know
|
||||||
|
// that they work tho, since ctrl+r works here in neovim.
|
||||||
|
// panic!("heard ctrl");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ pub struct Clipboard {
|
|||||||
|
|
||||||
impl Clipboard {
|
impl Clipboard {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self { clipboard: Vec::new(), last_paste_cell: (0, 0), momentum: (0, 1), source_cell: (0, 0) }
|
||||||
clipboard: Vec::new(),
|
|
||||||
last_paste_cell: (0, 0),
|
|
||||||
momentum: (0, 1),
|
|
||||||
source_cell: (0, 0),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Panics if clipboard is 0 length (if you call after you
|
/// Panics if clipboard is 0 length (if you call after you
|
||||||
@@ -49,6 +44,8 @@ impl Clipboard {
|
|||||||
// cursor
|
// cursor
|
||||||
let (cx, cy) = into.cursor();
|
let (cx, cy) = into.cursor();
|
||||||
|
|
||||||
|
let cursor = into.cursor();
|
||||||
|
into.transact_on_grid(|grid| {
|
||||||
// iterate thru the clipbaord's cells
|
// iterate thru the clipbaord's cells
|
||||||
for (x, row) in self.clipboard.iter().enumerate() {
|
for (x, row) in self.clipboard.iter().enumerate() {
|
||||||
for (y, cell) in row.iter().enumerate() {
|
for (y, cell) in row.iter().enumerate() {
|
||||||
@@ -56,18 +53,19 @@ impl Clipboard {
|
|||||||
|
|
||||||
if translate {
|
if translate {
|
||||||
if let Some(cell) = cell {
|
if let Some(cell) = cell {
|
||||||
let trans = cell.translate_cell(self.source_cell, into.cursor());
|
let trans = cell.translate_cell(self.source_cell, cursor);
|
||||||
into.set_cell_raw(idx, Some(trans));
|
grid.set_cell_raw(idx, Some(trans));
|
||||||
} else {
|
} else {
|
||||||
// The cell at this location doesn't exist (empty)
|
// The cell at this location doesn't exist (empty)
|
||||||
into.set_cell_raw::<CellType>(idx, None);
|
grid.set_cell_raw::<CellType>(idx, None);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// translate = false
|
// translate = false
|
||||||
into.set_cell_raw::<CellType>(idx, cell.clone());
|
grid.set_cell_raw::<CellType>(idx, cell.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let (lx, ly) = self.last_paste_cell;
|
let (lx, ly) = self.last_paste_cell;
|
||||||
self.momentum = (cx as i32 - lx as i32, cy as i32 - ly as i32);
|
self.momentum = (cx as i32 - lx as i32, cy as i32 - ly as i32);
|
||||||
@@ -109,16 +107,19 @@ impl Clipboard {
|
|||||||
|
|
||||||
// size the clipboard appropriately
|
// size the clipboard appropriately
|
||||||
self.clipboard.clear();
|
self.clipboard.clear();
|
||||||
|
|
||||||
|
from.transact_on_grid(|grid| {
|
||||||
// clone data into clipboard
|
// clone data into clipboard
|
||||||
for x in low_x..=hi_x {
|
for x in low_x..=hi_x {
|
||||||
let mut col = Vec::new();
|
let mut col = Vec::new();
|
||||||
for y in low_y..=hi_y {
|
for y in low_y..=hi_y {
|
||||||
let a = from.get_cell_raw(x, y);
|
let a = grid.get_cell_raw(x, y);
|
||||||
col.push(a.clone());
|
col.push(a.clone());
|
||||||
from.set_cell_raw::<CellType>((x, y), None);
|
grid.set_cell_raw::<CellType>((x, y), None);
|
||||||
}
|
}
|
||||||
self.clipboard.push(col);
|
self.clipboard.push(col);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
self.last_paste_cell = (low_x, low_y);
|
self.last_paste_cell = (low_x, low_y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,7 +354,7 @@ fn copy_paste_y_locked_var() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn issue_47() {
|
fn copy_paste_var_in_function() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
app.grid.set_cell("A0", 4.to_string());
|
app.grid.set_cell("A0", 4.to_string());
|
||||||
Mode::process_key(&mut app, 'j');
|
Mode::process_key(&mut app, 'j');
|
||||||
@@ -367,3 +368,31 @@ fn issue_47() {
|
|||||||
let a = app.grid.get_cell("A2").as_ref().expect("Should've been set by paste");
|
let a = app.grid.get_cell("A2").as_ref().expect("Should've been set by paste");
|
||||||
assert_eq!(a.to_string(), "=math::log2(A1)");
|
assert_eq!(a.to_string(), "=math::log2(A1)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_paste_range_in_function() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.grid.set_cell("A0", 1.to_string());
|
||||||
|
app.grid.set_cell("A1", 1.to_string());
|
||||||
|
app.grid.set_cell("A2", 1.to_string());
|
||||||
|
|
||||||
|
app.grid.set_cell("B0", "=sum(A:A)".to_string());
|
||||||
|
app.grid.mv_cursor_to(1, 0);
|
||||||
|
app.mode = super::mode::Mode::Chord(Chord::new('y'));
|
||||||
|
Mode::process_key(&mut app, 'y');
|
||||||
|
Mode::process_key(&mut app, 'l');
|
||||||
|
Mode::process_key(&mut app, 'p');
|
||||||
|
|
||||||
|
let a = app.grid.get_cell("C0").as_ref().expect("Should've been set by paste");
|
||||||
|
assert_eq!(a.to_string(), "=sum(B:B)");
|
||||||
|
|
||||||
|
// now copy the range the other direction
|
||||||
|
app.grid.mv_cursor_to(2, 0);
|
||||||
|
app.mode = super::mode::Mode::Chord(Chord::new('y'));
|
||||||
|
Mode::process_key(&mut app, 'y');
|
||||||
|
app.grid.mv_cursor_to(1, 1);
|
||||||
|
Mode::process_key(&mut app, 'p');
|
||||||
|
|
||||||
|
let a = app.grid.get_cell("B1").as_ref().expect("Should've been set by paste");
|
||||||
|
assert_eq!(a.to_string(), "=sum(A:A)");
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ impl Widget for &StatusMessage {
|
|||||||
let msg = if self.start.elapsed().as_secs() > 3 {
|
let msg = if self.start.elapsed().as_secs() > 3 {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
self.msg.clone().unwrap_or(String::new())
|
self.msg.clone().unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let style = match self.msg_type {
|
let style = match self.msg_type {
|
||||||
|
|||||||
@@ -2,37 +2,134 @@ use std::{
|
|||||||
cmp::{max, min},
|
cmp::{max, min},
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
path::PathBuf
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
use evalexpr::*;
|
use evalexpr::*;
|
||||||
|
|
||||||
use crate::app::{
|
use crate::app::logic::{
|
||||||
logic::{
|
calc::internal::CellGrid,
|
||||||
cell::{CSV_DELIMITER, CellType},
|
cell::{CSV_DELIMITER, CellType},
|
||||||
ctx,
|
ctx,
|
||||||
}, mode::Mode,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::app::app::App;
|
use crate::app::app::App;
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::app::mode::Mode;
|
||||||
|
|
||||||
pub fn get_header_size() -> usize {
|
pub fn get_header_size() -> usize {
|
||||||
let row_header_width = LEN.to_string().len();
|
LEN.to_string().len()
|
||||||
row_header_width
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const LEN: usize = 1001;
|
pub const LEN: usize = 1001;
|
||||||
pub const CSV_EXT: &str = "csv";
|
pub const CSV_EXT: &str = "csv";
|
||||||
pub const CUSTOM_EXT: &str = "nscim";
|
pub const CUSTOM_EXT: &str = "nscim";
|
||||||
|
|
||||||
pub struct Grid {
|
mod internal {
|
||||||
|
use crate::app::logic::{calc::LEN, cell::CellType};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CellGrid {
|
||||||
// a b c ...
|
// a b c ...
|
||||||
// 0
|
// 0
|
||||||
// 1
|
// 1
|
||||||
// 2
|
// 2
|
||||||
// ...
|
// ...
|
||||||
cells: Vec<Vec<Option<CellType>>>,
|
cells: Vec<Vec<Option<CellType>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CellGrid {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut a = Vec::with_capacity(LEN);
|
||||||
|
for _ in 0..LEN {
|
||||||
|
let mut b = Vec::with_capacity(LEN);
|
||||||
|
for _ in 0..LEN {
|
||||||
|
b.push(None)
|
||||||
|
}
|
||||||
|
a.push(b)
|
||||||
|
}
|
||||||
|
Self { cells: a }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_row(&mut self, y: usize) {
|
||||||
|
for x in 0..LEN {
|
||||||
|
self.cells[x].insert(y, None);
|
||||||
|
self.cells[x].pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_column(&mut self, x: usize) {
|
||||||
|
let mut v = Vec::with_capacity(LEN);
|
||||||
|
for _ in 0..LEN {
|
||||||
|
v.push(None);
|
||||||
|
}
|
||||||
|
// let clone = self.grid_history[self.current_grid].clone();
|
||||||
|
self.cells.insert(x, v);
|
||||||
|
// keep the grid LEN
|
||||||
|
self.cells.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cell_raw(&self, x: usize, y: usize) -> &Option<CellType> {
|
||||||
|
if x >= LEN || y >= LEN {
|
||||||
|
return &None;
|
||||||
|
}
|
||||||
|
&self.cells[x][y]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_cell_raw<T: Into<CellType>>(&mut self, (x, y): (usize, usize), val: Option<T>) {
|
||||||
|
// TODO check oob
|
||||||
|
self.cells[x][y] = val.map(|v| v.into());
|
||||||
|
}
|
||||||
|
/// Iterate over the entire grid and see where
|
||||||
|
/// the farthest modified cell is.
|
||||||
|
#[must_use]
|
||||||
|
pub fn max(&self) -> (usize, usize) {
|
||||||
|
let mut max_x = 0;
|
||||||
|
let mut max_y = 0;
|
||||||
|
|
||||||
|
for (xi, x) in self.cells.iter().enumerate() {
|
||||||
|
for (yi, cell) in x.iter().enumerate() {
|
||||||
|
if cell.is_some() {
|
||||||
|
if yi > max_y {
|
||||||
|
max_y = yi
|
||||||
|
}
|
||||||
|
if xi > max_x {
|
||||||
|
max_x = xi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(max_x, max_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_y_at_x(&self, x: usize) -> usize {
|
||||||
|
let mut max_y = 0;
|
||||||
|
if let Some(col) = self.cells.get(x) {
|
||||||
|
// we could do fancy things like .take_while(not null) but then
|
||||||
|
// we would have to deal with empty cells and stuff, which sounds
|
||||||
|
// boring. This will be fast "enough", considering the grid is
|
||||||
|
// probably only like 1k cells
|
||||||
|
for (yi, cell) in col.iter().enumerate() {
|
||||||
|
if cell.is_some() {
|
||||||
|
if yi > max_y {
|
||||||
|
max_y = yi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max_y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Grid {
|
||||||
|
/// Which grid in history are we currently on
|
||||||
|
current_grid: usize,
|
||||||
|
/// An array of grids, thru history
|
||||||
|
grid_history: Vec<CellGrid>,
|
||||||
/// (X, Y)
|
/// (X, Y)
|
||||||
selected_cell: (usize, usize),
|
selected_cell: (usize, usize),
|
||||||
/// Have unsaved modifications been made?
|
/// Have unsaved modifications been made?
|
||||||
@@ -47,20 +144,9 @@ impl std::fmt::Debug for Grid {
|
|||||||
|
|
||||||
impl Grid {
|
impl Grid {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut a = Vec::with_capacity(LEN);
|
let x = CellGrid::new();
|
||||||
for _ in 0..LEN {
|
|
||||||
let mut b = Vec::with_capacity(LEN);
|
|
||||||
for _ in 0..LEN {
|
|
||||||
b.push(None)
|
|
||||||
}
|
|
||||||
a.push(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self { current_grid: 0, grid_history: vec![x], selected_cell: (0, 0), dirty: false }
|
||||||
cells: a,
|
|
||||||
selected_cell: (0, 0),
|
|
||||||
dirty: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_from_file(file: &mut File) -> std::io::Result<Self> {
|
pub fn new_from_file(file: &mut File) -> std::io::Result<Self> {
|
||||||
@@ -68,6 +154,8 @@ impl Grid {
|
|||||||
|
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
file.read_to_string(&mut buf)?;
|
file.read_to_string(&mut buf)?;
|
||||||
|
|
||||||
|
grid.transact_on_grid(|grid| {
|
||||||
for (yi, line) in buf.lines().enumerate() {
|
for (yi, line) in buf.lines().enumerate() {
|
||||||
let cells = Self::parse_csv_line(line);
|
let cells = Self::parse_csv_line(line);
|
||||||
|
|
||||||
@@ -76,6 +164,7 @@ impl Grid {
|
|||||||
grid.set_cell_raw((xi, yi), cell);
|
grid.set_cell_raw((xi, yi), cell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// force dirty back off, we just read the data so it's gtg
|
// force dirty back off, we just read the data so it's gtg
|
||||||
grid.dirty = false;
|
grid.dirty = false;
|
||||||
@@ -112,25 +201,21 @@ impl Grid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut f = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?;
|
let mut f = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?;
|
||||||
let (mx, my) = self.max();
|
let (mx, my) = self.get_grid().max();
|
||||||
for y in 0..=my {
|
for y in 0..=my {
|
||||||
for x in 0..=mx {
|
for x in 0..=mx {
|
||||||
let cell = &self.cells[x][y];
|
let cell = &self.get_grid().get_cell_raw(x, y);
|
||||||
|
|
||||||
// newline after the cell, because it's end of line.
|
// newline after the cell, because it's end of line.
|
||||||
// else, just put a comma after the cell.
|
// else, just put a comma after the cell.
|
||||||
let is_last = x == mx;
|
let is_last = x == mx;
|
||||||
let delim = if is_last {
|
let delim = if is_last { '\n' } else { CSV_DELIMITER };
|
||||||
'\n'
|
|
||||||
} else {
|
|
||||||
CSV_DELIMITER
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = if let Some(cell) = cell {
|
let data = if let Some(cell) = cell {
|
||||||
if let Ok(val) = self.evaluate(&cell.to_string())
|
if let Ok(val) = self.evaluate(&cell.to_string())
|
||||||
&& resolve_values
|
&& resolve_values
|
||||||
{
|
{
|
||||||
format!("{}{}", val.to_string(), delim)
|
format!("{val}{delim}")
|
||||||
} else {
|
} else {
|
||||||
format!("{}{}", cell.escaped_csv_string(), delim)
|
format!("{}{}", cell.escaped_csv_string(), delim)
|
||||||
}
|
}
|
||||||
@@ -146,6 +231,37 @@ impl Grid {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_grid(&self) -> &CellGrid {
|
||||||
|
&self.grid_history[self.current_grid]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn undo(&mut self) {
|
||||||
|
self.current_grid = self.current_grid.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn redo(&mut self) {
|
||||||
|
self.current_grid = min(self.grid_history.len() - 1, self.current_grid + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transact_on_grid<F>(&mut self, mut action: F)
|
||||||
|
where
|
||||||
|
F: FnMut(&mut CellGrid),
|
||||||
|
{
|
||||||
|
// push on a new reality
|
||||||
|
let new = self.get_grid().clone();
|
||||||
|
self.grid_history.push(new);
|
||||||
|
self.current_grid += 1;
|
||||||
|
|
||||||
|
// delete the other fork of the history
|
||||||
|
for i in self.current_grid + 1..self.grid_history.len() {
|
||||||
|
self.grid_history.remove(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
action(&mut self.grid_history[self.current_grid]);
|
||||||
|
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn needs_to_be_saved(&self) -> bool {
|
pub fn needs_to_be_saved(&self) -> bool {
|
||||||
self.dirty
|
self.dirty
|
||||||
}
|
}
|
||||||
@@ -252,101 +368,84 @@ impl Grid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_row_above(&mut self, (_x, insertion_y): (usize, usize)) {
|
pub fn insert_row_above(&mut self, (_x, insertion_y): (usize, usize)) {
|
||||||
|
self.transact_on_grid(|grid: &mut CellGrid| {
|
||||||
|
grid.insert_row(insertion_y);
|
||||||
for x in 0..LEN {
|
for x in 0..LEN {
|
||||||
self.cells[x].insert(insertion_y, None);
|
|
||||||
self.cells[x].pop();
|
|
||||||
for y in 0..LEN {
|
for y in 0..LEN {
|
||||||
if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| {
|
if let Some(cell) = grid.get_cell_raw(x, y).as_ref().map(|f| {
|
||||||
f.custom_translate_cell((0, 0), (0, 1), |rolling, old, new| {
|
f.custom_translate_cell((0, 0), (0, 1), |rolling, old, new| {
|
||||||
if let Some((_, arg_y)) = Grid::parse_to_idx(old) {
|
if let Some((_, arg_y)) = Grid::parse_to_idx(old) {
|
||||||
if arg_y < insertion_y { rolling.to_owned() } else { rolling.replace(old, new) }
|
if arg_y < insertion_y { rolling.to_owned() } else { rolling.replace(old, new) }
|
||||||
|
} else if Grid::range_as_indices(old).is_some() {
|
||||||
|
// ranges are not changed when moved vertically
|
||||||
|
rolling.to_string()
|
||||||
} else {
|
} else {
|
||||||
unimplemented!("Invalid variable wanted to be translated")
|
#[cfg(debug_assertions)]
|
||||||
|
unimplemented!("Invalid variable wanted to be translated");
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
rolling.to_string()
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)}) {
|
}) {
|
||||||
self.set_cell_raw((x,y), Some(cell));
|
grid.set_cell_raw((x, y), Some(cell));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_row_below(&mut self, (x, y): (usize, usize)) {
|
pub fn insert_row_below(&mut self, (x, y): (usize, usize)) {
|
||||||
self.insert_row_above((x, y + 1));
|
self.insert_row_above((x, y + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) {
|
pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) {
|
||||||
let mut v = Vec::with_capacity(LEN);
|
self.transact_on_grid(|grid| {
|
||||||
for _ in 0..LEN {
|
grid.insert_column(insertion_x);
|
||||||
v.push(None);
|
|
||||||
}
|
|
||||||
self.cells.insert(insertion_x, v);
|
|
||||||
// keep the grid LEN
|
|
||||||
self.cells.pop();
|
|
||||||
for x in 0..LEN {
|
for x in 0..LEN {
|
||||||
for y in 0..LEN {
|
for y in 0..LEN {
|
||||||
if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| {
|
if let Some(cell) = grid.get_cell_raw(x, y).as_ref().map(|f| {
|
||||||
f.custom_translate_cell((0, 0), (1, 0), |rolling, old, new| {
|
f.custom_translate_cell((0, 0), (1, 0), |rolling, old, new| {
|
||||||
if let Some((arg_x, _)) = Grid::parse_to_idx(old) {
|
if let Some((arg_x, _)) = Grid::parse_to_idx(old) {
|
||||||
// add 1 because of the insertion
|
// add 1 because of the insertion
|
||||||
if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) }
|
if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) }
|
||||||
|
} else if let Some((start, end)) = Grid::range_as_indices(old) {
|
||||||
|
let mut range_start = Grid::num_to_char(start);
|
||||||
|
let mut range_end = Grid::num_to_char(end);
|
||||||
|
|
||||||
|
if start >= insertion_x {
|
||||||
|
range_start = Grid::num_to_char(start+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if end >= insertion_x {
|
||||||
|
range_end = Grid::num_to_char(end+1);
|
||||||
|
}
|
||||||
|
let new = format!("{range_start}:{range_end}");
|
||||||
|
|
||||||
|
rolling.replace(old, &new)
|
||||||
} else {
|
} else {
|
||||||
unimplemented!("Invalid variable wanted to be translated")
|
#[cfg(debug_assertions)]
|
||||||
|
unimplemented!("Invalid variable wanted to be translated");
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
rolling.to_string()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}) {
|
}) {
|
||||||
self.set_cell_raw((x, y), Some(cell));
|
grid.set_cell_raw((x, y), Some(cell));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_column_after(&mut self, (x, y): (usize, usize)) {
|
pub fn insert_column_after(&mut self, (x, y): (usize, usize)) {
|
||||||
self.insert_column_before((x + 1, y));
|
self.insert_column_before((x + 1, y));
|
||||||
}
|
}
|
||||||
/// Iterate over the entire grid and see where
|
|
||||||
/// the farthest modified cell is.
|
|
||||||
#[must_use]
|
|
||||||
fn max(&self) -> (usize, usize) {
|
|
||||||
let mut max_x = 0;
|
|
||||||
let mut max_y = 0;
|
|
||||||
|
|
||||||
for (xi, x) in self.cells.iter().enumerate() {
|
|
||||||
for (yi, cell) in x.iter().enumerate() {
|
|
||||||
if cell.is_some() {
|
|
||||||
if yi > max_y {
|
|
||||||
max_y = yi
|
|
||||||
}
|
|
||||||
if xi > max_x {
|
|
||||||
max_x = xi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(max_x, max_y)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_y_at_x(&self, x: usize) -> usize {
|
|
||||||
let mut max_y = 0;
|
|
||||||
if let Some(col) = self.cells.get(x) {
|
|
||||||
// we could do fancy things like .take_while(not null) but then
|
|
||||||
// we would have to deal with empty cells and stuff, which sounds
|
|
||||||
// boring. This will be fast "enough", considering the grid is
|
|
||||||
// probably only like 1k cells
|
|
||||||
for (yi, cell) in col.iter().enumerate() {
|
|
||||||
if cell.is_some() {
|
|
||||||
if yi > max_y {
|
|
||||||
max_y = yi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
max_y
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Only evaluates equations, such as `=10` or `=A1/C2`, not
|
/// Only evaluates equations, such as `=10` or `=A1/C2`, not
|
||||||
/// strings or numbers.
|
/// strings or numbers.
|
||||||
pub fn evaluate(&self, mut eq: &str) -> Result<f64, String> {
|
pub fn evaluate(&self, mut eq: &str) -> Result<CellType, String> {
|
||||||
if eq.starts_with('=') {
|
if eq.starts_with('=') {
|
||||||
eq = &eq[1..];
|
eq = &eq[1..];
|
||||||
} else {
|
} else {
|
||||||
@@ -354,17 +453,21 @@ impl Grid {
|
|||||||
return Err(format!("\"{eq}\" is not an equation"));
|
return Err(format!("\"{eq}\" is not an equation"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctx = ctx::CallbackContext::new(&self);
|
let ctx = ctx::CallbackContext::new(self);
|
||||||
|
|
||||||
let prep_for_return = |v: Value| {
|
let prep_for_return = |v: Value| {
|
||||||
if v.is_number() {
|
if v.is_number() {
|
||||||
if v.is_float() {
|
if v.is_float() {
|
||||||
let val = v.as_float().expect("Value lied about being a float");
|
let val = v.as_float().expect("Value lied about being a float");
|
||||||
return Ok(val);
|
return Ok(CellType::Number(val));
|
||||||
} else if v.is_int() {
|
} else if v.is_int() {
|
||||||
let i = v.as_int().expect("Value lied about being an int");
|
let val = v.as_int().expect("Value lied about being an int");
|
||||||
return Ok(i as f64);
|
return Ok(CellType::Number(val as f64));
|
||||||
}
|
}
|
||||||
|
} else if v.is_string() {
|
||||||
|
// ^^ This allows for functions to return a string
|
||||||
|
let s = v.as_string().expect("Value lied about being a String");
|
||||||
|
return Ok(CellType::String(s));
|
||||||
}
|
}
|
||||||
return Err("Result is NaN".to_string());
|
return Err("Result is NaN".to_string());
|
||||||
};
|
};
|
||||||
@@ -377,10 +480,7 @@ impl Grid {
|
|||||||
EvalexprError::VariableIdentifierNotFound(var_not_found) => {
|
EvalexprError::VariableIdentifierNotFound(var_not_found) => {
|
||||||
return Err(format!("\"{var_not_found}\" is not a variable"));
|
return Err(format!("\"{var_not_found}\" is not a variable"));
|
||||||
}
|
}
|
||||||
EvalexprError::TypeError {
|
EvalexprError::TypeError { expected: e, actual: a } => {
|
||||||
expected: e,
|
|
||||||
actual: a,
|
|
||||||
} => {
|
|
||||||
// IE: You put a string into a function that wants a float
|
// IE: You put a string into a function that wants a float
|
||||||
return Err(format!("Wanted {e:?}, got {a}"));
|
return Err(format!("Wanted {e:?}, got {a}"));
|
||||||
}
|
}
|
||||||
@@ -389,12 +489,37 @@ impl Grid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the indices of the range labels.
|
||||||
|
/// A:B -> (0,1)
|
||||||
|
pub fn range_as_indices(range: &str) -> Option<(usize, usize)> {
|
||||||
|
let v = range.split(':').collect::<Vec<&str>>();
|
||||||
|
if v.len() == 2 {
|
||||||
|
let start_col = v[0];
|
||||||
|
let end_col = v[1];
|
||||||
|
|
||||||
|
let start_idx = Grid::char_to_idx(start_col);
|
||||||
|
let end_idx = Grid::char_to_idx(end_col);
|
||||||
|
|
||||||
|
return Some((start_idx, end_idx));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn char_to_idx(i: &str) -> usize {
|
||||||
|
i
|
||||||
|
.chars()
|
||||||
|
.filter(|f| f.is_alphabetic())
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, c)| ((c.to_ascii_lowercase() as usize).saturating_sub(97)) + (26 * idx))
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse values in the format of A0, C10 ZZ99, etc, and
|
/// Parse values in the format of A0, C10 ZZ99, etc, and
|
||||||
/// turn them into an X,Y index.
|
/// turn them into an X,Y index.
|
||||||
pub fn parse_to_idx(i: &str) -> Option<(usize, usize)> {
|
pub fn parse_to_idx(i: &str) -> Option<(usize, usize)> {
|
||||||
let i = i.replace('$', "");
|
let i = i.replace('$', "");
|
||||||
|
|
||||||
let chars = i.chars().take_while(|c| c.is_alphabetic()).collect::<Vec<char>>();
|
let chars = i.chars().take_while(|c| c.is_alphabetic()).collect::<String>();
|
||||||
let nums = i.chars().skip(chars.len()).take_while(|c| c.is_numeric()).collect::<String>();
|
let nums = i.chars().skip(chars.len()).take_while(|c| c.is_numeric()).collect::<String>();
|
||||||
|
|
||||||
// At least half the arguments are gone
|
// At least half the arguments are gone
|
||||||
@@ -403,11 +528,7 @@ impl Grid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get the x index from the chars
|
// get the x index from the chars
|
||||||
let x_idx = chars
|
let x_idx = Self::char_to_idx(&chars);
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(idx, c)| (c.to_ascii_lowercase() as usize - 97) + (26 * idx))
|
|
||||||
.fold(0, |a, b| a + b);
|
|
||||||
|
|
||||||
// get the y index from the numbers
|
// get the y index from the numbers
|
||||||
if let Ok(y_idx) = nums.parse::<usize>() {
|
if let Ok(y_idx) = nums.parse::<usize>() {
|
||||||
@@ -419,15 +540,12 @@ impl Grid {
|
|||||||
|
|
||||||
/// Helper for tests
|
/// Helper for tests
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn set_cell<T: Into<CellType>>(&mut self, cell_id: &str, val: T) {
|
pub fn set_cell<T: Into<CellType> + Clone>(&mut self, cell_id: &str, val: T) {
|
||||||
if let Some(loc) = Self::parse_to_idx(cell_id) {
|
if let Some(loc) = Self::parse_to_idx(cell_id) {
|
||||||
self.set_cell_raw(loc, Some(val));
|
self.transact_on_grid(|grid| {
|
||||||
|
grid.set_cell_raw(loc, Some(val.clone()));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_cell_raw<T: Into<CellType>>(&mut self, (x, y): (usize, usize), val: Option<T>) {
|
|
||||||
// TODO check oob
|
|
||||||
self.cells[x][y] = val.map(|v| v.into());
|
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +564,7 @@ impl Grid {
|
|||||||
if x >= LEN || y >= LEN {
|
if x >= LEN || y >= LEN {
|
||||||
return &None;
|
return &None;
|
||||||
}
|
}
|
||||||
&self.cells[x][y]
|
self.get_grid().get_cell_raw(x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn num_to_char(idx: usize) -> String {
|
pub fn num_to_char(idx: usize) -> String {
|
||||||
@@ -463,7 +581,7 @@ impl Grid {
|
|||||||
}
|
}
|
||||||
word[1] = ((idx % 26) + 65) as u8 as char;
|
word[1] = ((idx % 26) + 65) as u8 as char;
|
||||||
|
|
||||||
word.iter().collect()
|
word.iter().filter(|a| !a.is_ascii_whitespace()).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,11 +617,11 @@ fn saving_csv() {
|
|||||||
// insure that the cells are there
|
// insure that the cells are there
|
||||||
let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set");
|
let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set");
|
||||||
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
||||||
assert_eq!(res, 11.0);
|
assert_eq!(res, (11.0).into());
|
||||||
assert_eq!(cell.escaped_csv_string(), "=A9+A$0");
|
assert_eq!(cell.escaped_csv_string(), "=A9+A$0");
|
||||||
let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set");
|
let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set");
|
||||||
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
||||||
assert_eq!(res, 121.0);
|
assert_eq!(res, (121.0).into());
|
||||||
assert_eq!(cell.escaped_csv_string(), "=A10^2");
|
assert_eq!(cell.escaped_csv_string(), "=A10^2");
|
||||||
|
|
||||||
// set saving the file
|
// set saving the file
|
||||||
@@ -542,11 +660,11 @@ fn saving_neoscim() {
|
|||||||
// insure that the cells are there
|
// insure that the cells are there
|
||||||
let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set");
|
let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set");
|
||||||
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
||||||
assert_eq!(res, 11.0);
|
assert_eq!(res, (11.0).into());
|
||||||
assert_eq!(cell.escaped_csv_string(), "=A9+A$0");
|
assert_eq!(cell.escaped_csv_string(), "=A9+A$0");
|
||||||
let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set");
|
let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set");
|
||||||
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
||||||
assert_eq!(res, 121.0);
|
assert_eq!(res, (121.0).into());
|
||||||
assert_eq!(cell.escaped_csv_string(), "=A10^2");
|
assert_eq!(cell.escaped_csv_string(), "=A10^2");
|
||||||
|
|
||||||
// set saving the file
|
// set saving the file
|
||||||
@@ -564,7 +682,7 @@ fn saving_neoscim() {
|
|||||||
fn cell_strings() {
|
fn cell_strings() {
|
||||||
let mut grid = Grid::new();
|
let mut grid = Grid::new();
|
||||||
|
|
||||||
assert!(&grid.cells[0][0].is_none());
|
assert!(&grid.get_grid().get_cell_raw(0, 0).is_none());
|
||||||
grid.set_cell("A0", "Hello".to_string());
|
grid.set_cell("A0", "Hello".to_string());
|
||||||
assert!(grid.get_cell("A0").is_some());
|
assert!(grid.get_cell("A0").is_some());
|
||||||
|
|
||||||
@@ -587,9 +705,10 @@ fn alphanumeric_indexing() {
|
|||||||
assert_eq!(Grid::parse_to_idx("A"), None);
|
assert_eq!(Grid::parse_to_idx("A"), None);
|
||||||
assert_eq!(Grid::parse_to_idx(":"), None);
|
assert_eq!(Grid::parse_to_idx(":"), None);
|
||||||
assert_eq!(Grid::parse_to_idx("="), None);
|
assert_eq!(Grid::parse_to_idx("="), None);
|
||||||
|
assert_eq!(Grid::parse_to_idx("A:A"), None);
|
||||||
|
|
||||||
assert_eq!(Grid::num_to_char(0).trim(), "A");
|
assert_eq!(Grid::num_to_char(0), "A");
|
||||||
assert_eq!(Grid::num_to_char(25).trim(), "Z");
|
assert_eq!(Grid::num_to_char(25), "Z");
|
||||||
assert_eq!(Grid::num_to_char(26), "AA");
|
assert_eq!(Grid::num_to_char(26), "AA");
|
||||||
assert_eq!(Grid::num_to_char(51), "AZ");
|
assert_eq!(Grid::num_to_char(51), "AZ");
|
||||||
assert_eq!(Grid::num_to_char(701), "ZZ");
|
assert_eq!(Grid::num_to_char(701), "ZZ");
|
||||||
@@ -607,31 +726,31 @@ fn valid_equations() {
|
|||||||
// cell math
|
// cell math
|
||||||
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
|
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 3.);
|
assert_eq!(res, (3.).into());
|
||||||
|
|
||||||
// divide floats
|
// divide floats
|
||||||
grid.set_cell("D0", "=5./2.".to_string());
|
grid.set_cell("D0", "=5./2.".to_string());
|
||||||
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
||||||
assert_eq!(res, 2.5);
|
assert_eq!(res, (2.5).into());
|
||||||
|
|
||||||
// Float / Int mix
|
// Float / Int mix
|
||||||
grid.set_cell("D0", "=5./2".to_string());
|
grid.set_cell("D0", "=5./2".to_string());
|
||||||
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
||||||
assert_eq!(res, 2.5);
|
assert_eq!(res, (2.5).into());
|
||||||
|
|
||||||
// divide "ints" (should become floats)
|
// divide "ints" (should become floats)
|
||||||
grid.set_cell("D0", "=5/2".to_string());
|
grid.set_cell("D0", "=5/2".to_string());
|
||||||
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
||||||
assert_eq!(res, 2.5);
|
assert_eq!(res, (2.5).into());
|
||||||
|
|
||||||
// Non-equation that should still be valid
|
// Non-equation that should still be valid
|
||||||
grid.set_cell("D0", "=10".to_string());
|
grid.set_cell("D0", "=10".to_string());
|
||||||
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
||||||
assert_eq!(res, 10.);
|
assert_eq!(res, (10.).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cell = output of Cell = value of Cells.
|
// Cell = output of Cell = value of Cells.
|
||||||
@@ -644,14 +763,16 @@ fn fn_of_fn() {
|
|||||||
grid.set_cell("C0", "=A0+B0".to_string());
|
grid.set_cell("C0", "=A0+B0".to_string());
|
||||||
grid.set_cell("D0", "=C0*2".to_string());
|
grid.set_cell("D0", "=C0*2".to_string());
|
||||||
|
|
||||||
if let Some(cell) = grid.get_cell("D0") {
|
let cell = grid.get_cell("D0");
|
||||||
|
assert!(cell.is_some());
|
||||||
|
|
||||||
|
if let Some(cell) = cell {
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
|
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 6.);
|
assert_eq!(res.unwrap(), (6.).into());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
panic!("Cell not found");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two cells that have a circular dependency to solve for a value
|
// Two cells that have a circular dependency to solve for a value
|
||||||
@@ -661,12 +782,14 @@ fn circular_reference_cells() {
|
|||||||
grid.set_cell("A0", "=B0".to_string());
|
grid.set_cell("A0", "=B0".to_string());
|
||||||
grid.set_cell("B0", "=A0".to_string());
|
grid.set_cell("B0", "=A0".to_string());
|
||||||
|
|
||||||
if let Some(cell) = grid.get_cell("A0") {
|
let cell = grid.get_cell("A0");
|
||||||
|
assert!(cell.is_some());
|
||||||
|
|
||||||
|
if let Some(cell) = cell {
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
panic!("Cell not found");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -684,7 +807,7 @@ fn invalid_equations() {
|
|||||||
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert!(res.is_ok_and(|v| v == 10.));
|
assert!(res.is_ok_and(|v| v == (10.).into()));
|
||||||
|
|
||||||
// Trailing comma in function call
|
// Trailing comma in function call
|
||||||
grid.set_cell("A0", 5.);
|
grid.set_cell("A0", 5.);
|
||||||
@@ -692,7 +815,7 @@ fn invalid_equations() {
|
|||||||
grid.set_cell("B0", "=avg(A0,A1,)".to_string());
|
grid.set_cell("B0", "=avg(A0,A1,)".to_string());
|
||||||
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert_eq!(res.unwrap(), 7.5);
|
assert_eq!(res.unwrap(), (7.5).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -700,17 +823,17 @@ fn grid_max() {
|
|||||||
let mut grid = Grid::new();
|
let mut grid = Grid::new();
|
||||||
|
|
||||||
grid.set_cell("A0", 1.);
|
grid.set_cell("A0", 1.);
|
||||||
let (mx, my) = grid.max();
|
let (mx, my) = grid.get_grid().max();
|
||||||
assert_eq!(mx, 0);
|
assert_eq!(mx, 0);
|
||||||
assert_eq!(my, 0);
|
assert_eq!(my, 0);
|
||||||
|
|
||||||
grid.set_cell("B0", 1.);
|
grid.set_cell("B0", 1.);
|
||||||
let (mx, my) = grid.max();
|
let (mx, my) = grid.get_grid().max();
|
||||||
assert_eq!(mx, 1);
|
assert_eq!(mx, 1);
|
||||||
assert_eq!(my, 0);
|
assert_eq!(my, 0);
|
||||||
|
|
||||||
grid.set_cell("B5", 1.);
|
grid.set_cell("B5", 1.);
|
||||||
let (mx, my) = grid.max();
|
let (mx, my) = grid.get_grid().max();
|
||||||
assert_eq!(mx, 1);
|
assert_eq!(mx, 1);
|
||||||
assert_eq!(my, 5);
|
assert_eq!(my, 5);
|
||||||
}
|
}
|
||||||
@@ -723,19 +846,19 @@ fn avg_function() {
|
|||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 5.);
|
assert_eq!(res.unwrap(), (5.).into());
|
||||||
|
|
||||||
grid.set_cell("A0", "=avg(5,10)".to_string());
|
grid.set_cell("A0", "=avg(5,10)".to_string());
|
||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 7.5);
|
assert_eq!(res.unwrap(), (7.5).into());
|
||||||
|
|
||||||
grid.set_cell("A0", "=avg(5,10,15)".to_string());
|
grid.set_cell("A0", "=avg(5,10,15)".to_string());
|
||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 10.);
|
assert_eq!(res.unwrap(), (10.).into());
|
||||||
|
|
||||||
grid.set_cell("A0", "=avg(foo)".to_string());
|
grid.set_cell("A0", "=avg(foo)".to_string());
|
||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
@@ -761,13 +884,13 @@ fn sum_function() {
|
|||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 5.);
|
assert_eq!(res.unwrap(), (5.).into());
|
||||||
|
|
||||||
grid.set_cell("A0", "=sum(5,10)".to_string());
|
grid.set_cell("A0", "=sum(5,10)".to_string());
|
||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 15.);
|
assert_eq!(res.unwrap(), (15.).into());
|
||||||
|
|
||||||
grid.set_cell("A0", "=sum(foo)".to_string());
|
grid.set_cell("A0", "=sum(foo)".to_string());
|
||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
@@ -797,7 +920,7 @@ fn xlookup_function() {
|
|||||||
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 31.);
|
assert_eq!(res.unwrap(), (31.).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -851,6 +974,19 @@ fn parse_csv() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_ranges() {
|
||||||
|
let mut grid = Grid::new();
|
||||||
|
|
||||||
|
grid.set_cell("A0", 2.);
|
||||||
|
grid.set_cell("A1", 1.);
|
||||||
|
// ASCII to number conversion needs to not overflow
|
||||||
|
grid.set_cell("B0", "=sum($:A)".to_string());
|
||||||
|
|
||||||
|
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
||||||
|
let _ = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ranges() {
|
fn ranges() {
|
||||||
let mut grid = Grid::new();
|
let mut grid = Grid::new();
|
||||||
@@ -862,13 +998,13 @@ fn ranges() {
|
|||||||
// range with numbers
|
// range with numbers
|
||||||
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 3.);
|
assert_eq!(res, (3.).into());
|
||||||
|
|
||||||
// use range output as input for other function
|
// use range output as input for other function
|
||||||
grid.set_cell("B1", "=B0*2".to_string());
|
grid.set_cell("B1", "=B0*2".to_string());
|
||||||
let cell = grid.get_cell("B1").as_ref().expect("Just set it");
|
let cell = grid.get_cell("B1").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 6.);
|
assert_eq!(res, (6.).into());
|
||||||
|
|
||||||
// use equation outputs as range input
|
// use equation outputs as range input
|
||||||
grid.set_cell("A2", "=C0+1".to_string());
|
grid.set_cell("A2", "=C0+1".to_string());
|
||||||
@@ -876,11 +1012,11 @@ fn ranges() {
|
|||||||
|
|
||||||
let cell = grid.get_cell("A2").as_ref().expect("Just set it");
|
let cell = grid.get_cell("A2").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 6.);
|
assert_eq!(res, (6.).into());
|
||||||
|
|
||||||
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 9.);
|
assert_eq!(res, (9.).into());
|
||||||
|
|
||||||
// use function outputs as range input
|
// use function outputs as range input
|
||||||
grid.set_cell("B1", 2.);
|
grid.set_cell("B1", 2.);
|
||||||
@@ -889,7 +1025,7 @@ fn ranges() {
|
|||||||
|
|
||||||
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
|
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 5.);
|
assert_eq!(res, (5.).into());
|
||||||
|
|
||||||
// use range outputs as range input
|
// use range outputs as range input
|
||||||
grid.set_cell("D0", "=sum(C:C)".to_string());
|
grid.set_cell("D0", "=sum(C:C)".to_string());
|
||||||
@@ -897,7 +1033,7 @@ fn ranges() {
|
|||||||
|
|
||||||
let cell = grid.get_cell("D0").as_ref().expect("Just set it");
|
let cell = grid.get_cell("D0").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 6.);
|
assert_eq!(res, (6.).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -982,6 +1118,66 @@ fn insert_col_before_3() {
|
|||||||
assert_eq!(cell.to_string(), "=B0*B1");
|
assert_eq!(cell.to_string(), "=B0*B1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_col_before_static_range() {
|
||||||
|
let mut grid = Grid::new();
|
||||||
|
|
||||||
|
grid.set_cell("A0", 2.);
|
||||||
|
grid.set_cell("A1", 2.);
|
||||||
|
grid.set_cell("B0", "=sum(A:A)".to_string());
|
||||||
|
|
||||||
|
grid.mv_cursor_to(1, 0);
|
||||||
|
grid.insert_column_before(grid.cursor());
|
||||||
|
|
||||||
|
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
|
||||||
|
assert_eq!(cell.to_string(), "=sum(A:A)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_col_before_move_range() {
|
||||||
|
let mut grid = Grid::new();
|
||||||
|
|
||||||
|
grid.set_cell("B0", 2.);
|
||||||
|
grid.set_cell("B1", 2.);
|
||||||
|
grid.set_cell("A0", "=sum(B:B)".to_string());
|
||||||
|
|
||||||
|
grid.mv_cursor_to(0, 0);
|
||||||
|
grid.insert_column_after(grid.cursor());
|
||||||
|
|
||||||
|
let cell = grid.get_cell("A0").as_ref().expect("Just set it");
|
||||||
|
assert_eq!(cell.to_string(), "=sum(C:C)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_row_before_static_range() {
|
||||||
|
let mut grid = Grid::new();
|
||||||
|
|
||||||
|
grid.set_cell("A0", 2.);
|
||||||
|
grid.set_cell("A1", 2.);
|
||||||
|
grid.set_cell("B0", "=sum(A:A)".to_string());
|
||||||
|
|
||||||
|
grid.mv_cursor_to(0, 0);
|
||||||
|
grid.insert_row_above(grid.cursor());
|
||||||
|
|
||||||
|
let cell = grid.get_cell("B1").as_ref().expect("Just set it");
|
||||||
|
assert_eq!(cell.to_string(), "=sum(A:A)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_row_before_move_range() {
|
||||||
|
let mut grid = Grid::new();
|
||||||
|
|
||||||
|
grid.set_cell("A0", 2.);
|
||||||
|
grid.set_cell("A1", 2.);
|
||||||
|
grid.set_cell("B0", "=sum(A:A)".to_string());
|
||||||
|
|
||||||
|
grid.mv_cursor_to(0, 0);
|
||||||
|
grid.insert_row_below(grid.cursor());
|
||||||
|
|
||||||
|
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
||||||
|
assert_eq!(cell.to_string(), "=sum(A:A)");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn insert_row_above_1() {
|
fn insert_row_above_1() {
|
||||||
let mut grid = Grid::new();
|
let mut grid = Grid::new();
|
||||||
@@ -1067,6 +1263,34 @@ fn cell_eval_depth() {
|
|||||||
assert_eq!(c.to_string(), "=A5+$A$0");
|
assert_eq!(c.to_string(), "=A5+$A$0");
|
||||||
|
|
||||||
let res = app.grid.evaluate(&c.to_string()).expect("Should evaluate");
|
let res = app.grid.evaluate(&c.to_string()).expect("Should evaluate");
|
||||||
assert_eq!(res, 7.);
|
assert_eq!(res, (7.).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn return_string_from_fn() {
|
||||||
|
let mut grid = Grid::new();
|
||||||
|
grid.set_cell("A0", "=if(2>1, \"A\", \"B\")".to_string()); // true, A
|
||||||
|
grid.set_cell("A1", "=if(1>2, \"A\", \"B\")".to_string()); // false, B
|
||||||
|
|
||||||
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
|
let res = grid.evaluate(&cell.to_string());
|
||||||
|
assert!(res.is_ok());
|
||||||
|
if let Ok(f) = res {
|
||||||
|
if let CellType::String(s) = f {
|
||||||
|
assert_eq!("A", s);
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell = grid.get_cell("A1").as_ref().expect("Just set the cell");
|
||||||
|
let res = grid.evaluate(&cell.to_string());
|
||||||
|
assert!(res.is_ok());
|
||||||
|
if let Ok(f) = res {
|
||||||
|
if let CellType::String(s) = f {
|
||||||
|
assert_eq!("B", s);
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ pub enum CellType {
|
|||||||
Equation(String),
|
Equation(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<CellType> for f64 {
|
impl From<f64> for CellType {
|
||||||
fn into(self) -> CellType {
|
fn from(value: f64) -> Self {
|
||||||
CellType::duck_type(self.to_string())
|
CellType::duck_type(value.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<CellType> for String {
|
impl From<String> for CellType {
|
||||||
fn into(self) -> CellType {
|
fn from(value: String) -> Self {
|
||||||
CellType::duck_type(self)
|
CellType::duck_type(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,14 +37,10 @@ impl CellType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// escape string of it has a comma
|
// escape string of it has a comma
|
||||||
if display.contains(CSV_DELIMITER) {
|
if display.contains(CSV_DELIMITER) { format!("\"{display}\"") } else { display }
|
||||||
format!("\"{display}\"")
|
|
||||||
} else {
|
|
||||||
display
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn duck_type<'a>(value: impl Into<String>) -> Self {
|
fn duck_type(value: impl Into<String>) -> Self {
|
||||||
let value = value.into();
|
let value = value.into();
|
||||||
|
|
||||||
if let Ok(parse) = value.parse::<f64>() {
|
if let Ok(parse) = value.parse::<f64>() {
|
||||||
@@ -56,14 +52,19 @@ impl CellType {
|
|||||||
|
|
||||||
/// `replace_fn` takes the string, the old value, and then the new value.
|
/// `replace_fn` takes the string, the old value, and then the new value.
|
||||||
/// It can be thought of as `echo $1 | sed s/$2/$3/g`
|
/// It can be thought of as `echo $1 | sed s/$2/$3/g`
|
||||||
pub fn custom_translate_cell(&self, from: (usize, usize), to: (usize, usize), replace_fn: impl Fn(&str, &str, &str) -> String) -> CellType {
|
pub fn custom_translate_cell(
|
||||||
|
&self,
|
||||||
|
from: (usize, usize),
|
||||||
|
to: (usize, usize),
|
||||||
|
replace_fn: impl Fn(&str, &str, &str) -> String,
|
||||||
|
) -> CellType {
|
||||||
match self {
|
match self {
|
||||||
// don't translate non-equations
|
// don't translate non-equations
|
||||||
CellType::Number(_) | CellType::String(_) => return self.clone(),
|
CellType::Number(_) | CellType::String(_) => return self.clone(),
|
||||||
CellType::Equation(eq) => {
|
CellType::Equation(eq) => {
|
||||||
// Populate the context
|
// Populate the context
|
||||||
let ctx = ExtractionContext::new();
|
let ctx = ExtractionContext::new();
|
||||||
let _ = eval_with_context(eq, &ctx);
|
let _ = eval_with_context(&eq[1..], &ctx);
|
||||||
|
|
||||||
let mut equation = eq.clone();
|
let mut equation = eq.clone();
|
||||||
// translate standard vars A0 -> A1
|
// translate standard vars A0 -> A1
|
||||||
@@ -73,7 +74,8 @@ impl CellType {
|
|||||||
let mut lock_y = false;
|
let mut lock_y = false;
|
||||||
|
|
||||||
if old_var.contains('$') {
|
if old_var.contains('$') {
|
||||||
let locations = old_var.char_indices().filter(|(_, c)| *c == '$').map(|(i, _)| i).collect::<Vec<usize>>();
|
let locations =
|
||||||
|
old_var.char_indices().filter(|(_, c)| *c == '$').map(|(i, _)| i).collect::<Vec<usize>>();
|
||||||
match locations.len() {
|
match locations.len() {
|
||||||
1 => {
|
1 => {
|
||||||
if locations[0] == 0 {
|
if locations[0] == 0 {
|
||||||
@@ -107,20 +109,11 @@ impl CellType {
|
|||||||
let x2 = x2 as i32;
|
let x2 = x2 as i32;
|
||||||
let y2 = y2 as i32;
|
let y2 = y2 as i32;
|
||||||
|
|
||||||
let dest_x = if lock_x {
|
let dest_x = if lock_x { src_x } else { (src_x as i32 + (x2 - x1)) as usize };
|
||||||
src_x as usize
|
|
||||||
} else {
|
|
||||||
(src_x as i32 + (x2 - x1)) as usize
|
|
||||||
};
|
|
||||||
|
|
||||||
let dest_y = if lock_y {
|
let dest_y = if lock_y { src_y } else { (src_y as i32 + (y2 - y1)) as usize };
|
||||||
src_y as usize
|
|
||||||
} else {
|
|
||||||
(src_y as i32 + (y2 - y1)) as usize
|
|
||||||
};
|
|
||||||
|
|
||||||
let alpha = Grid::num_to_char(dest_x);
|
let alpha = Grid::num_to_char(dest_x);
|
||||||
let alpha = alpha.trim();
|
|
||||||
|
|
||||||
// Persist the "$" locking
|
// Persist the "$" locking
|
||||||
let new_var = if lock_x {
|
let new_var = if lock_x {
|
||||||
@@ -136,6 +129,32 @@ impl CellType {
|
|||||||
// rolling = rolling.replace(&old_var, &new_var);
|
// rolling = rolling.replace(&old_var, &new_var);
|
||||||
} else {
|
} else {
|
||||||
// why you coping invalid stuff, nerd?
|
// why you coping invalid stuff, nerd?
|
||||||
|
//
|
||||||
|
// could be copying a range
|
||||||
|
if let Some(parts) = Grid::range_as_indices(&old_var) {
|
||||||
|
// how far is the movement?
|
||||||
|
let dx = to.0 as i32 - from.0 as i32;
|
||||||
|
|
||||||
|
let xs = parts.0 as i32;
|
||||||
|
let xe = parts.1 as i32;
|
||||||
|
|
||||||
|
// apply movement
|
||||||
|
let mut new_range_start = xs + dx;
|
||||||
|
let mut new_range_end = xe + dx;
|
||||||
|
|
||||||
|
// bottom out at 0
|
||||||
|
if new_range_start < 0 {
|
||||||
|
new_range_start = 0;
|
||||||
|
}
|
||||||
|
if new_range_end < 0 {
|
||||||
|
new_range_end = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert the index back into a letter and then submit it
|
||||||
|
let start = Grid::num_to_char(new_range_start as usize);
|
||||||
|
let end = Grid::num_to_char(new_range_end as usize);
|
||||||
|
equation = replace_fn(&equation, &old_var, &format!("{start}:{end}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return equation.into();
|
return equation.into();
|
||||||
@@ -158,3 +177,14 @@ impl Display for CellType {
|
|||||||
write!(f, "{d}")
|
write!(f, "{d}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for CellType {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Number(left), Self::Number(right)) => left == right,
|
||||||
|
(Self::String(left), Self::String(right)) => left == right,
|
||||||
|
(Self::Equation(left), Self::Equation(right)) => left == right,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,25 +16,11 @@ pub struct CallbackContext<'a> {
|
|||||||
|
|
||||||
impl<'a> CallbackContext<'a> {
|
impl<'a> CallbackContext<'a> {
|
||||||
fn expand_range(&self, range: &str) -> Option<Vec<&CellType>> {
|
fn expand_range(&self, range: &str) -> Option<Vec<&CellType>> {
|
||||||
let v = range.split(':').collect::<Vec<&str>>();
|
if let Some((start, end)) = Grid::range_as_indices(range) {
|
||||||
if v.len() == 2 {
|
|
||||||
let start_col = v[0];
|
|
||||||
let end_col = v[1];
|
|
||||||
|
|
||||||
let as_index = |s: &str| {
|
|
||||||
s.char_indices()
|
|
||||||
// .filter(|f| f.1 as u8 >= 97) // prevent sub with overflow errors
|
|
||||||
.map(|(idx, c)| (c.to_ascii_lowercase() as usize - 97) + (26 * idx))
|
|
||||||
.fold(0, |a, b| a + b)
|
|
||||||
};
|
|
||||||
|
|
||||||
let start_idx = as_index(start_col);
|
|
||||||
let end_idx = as_index(end_col);
|
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
for x in start_idx..=end_idx {
|
for x in start..=end {
|
||||||
for y in 0..=self.variables.max_y_at_x(x) {
|
for y in 0..=self.variables.get_grid().max_y_at_x(x) {
|
||||||
if let Some(s) = self.variables.get_cell_raw(x, y) {
|
if let Some(s) = self.variables.get_cell_raw(x, y) {
|
||||||
buf.push(s);
|
buf.push(s);
|
||||||
}
|
}
|
||||||
@@ -162,8 +148,8 @@ impl<'a> Context for CallbackContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(mut trail) = self.eval_breadcrumbs.write() {
|
if let Ok(mut trail) = self.eval_breadcrumbs.write() {
|
||||||
let find = trail.iter().filter(|id| *id == identifier).collect::<Vec<&String>>();
|
let find = trail.iter().filter(|id| *id == identifier).count();
|
||||||
if find.len() > 0 {
|
if find > 0 {
|
||||||
// recursion detected
|
// recursion detected
|
||||||
return None;
|
return None;
|
||||||
} else {
|
} else {
|
||||||
@@ -195,14 +181,21 @@ impl<'a> Context for CallbackContext<'a> {
|
|||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
match e {
|
match e {
|
||||||
EvalexprError::VariableIdentifierNotFound(_) => {
|
EvalexprError::VariableIdentifierNotFound(_err) => {
|
||||||
// If the variable isn't found, that's ~~probably~~ because
|
// If the variable isn't found, that's ~~probably~~ because
|
||||||
// of recursive reference, considering all references
|
// of recursive reference, considering all references
|
||||||
// are grabbed straight from the table.
|
// are grabbed straight from the table.
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
e => panic!("> Error {e}\n> Equation: '{eq}'"),
|
e => {
|
||||||
|
let msg = format!("> Error {e}\n> Equation: '{eq}'");
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
panic!("{msg}");
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
eprintln!("{msg}");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,8 +210,14 @@ impl<'a> Context for CallbackContext<'a> {
|
|||||||
CellType::Number(e) => vals.push(Value::Float(*e)),
|
CellType::Number(e) => vals.push(Value::Float(*e)),
|
||||||
CellType::String(s) => vals.push(Value::String(s.to_owned())),
|
CellType::String(s) => vals.push(Value::String(s.to_owned())),
|
||||||
CellType::Equation(eq) => {
|
CellType::Equation(eq) => {
|
||||||
if let Ok(val) = eval_with_context(&eq[1..], self) {
|
match eval_with_context(&eq[1..], self) {
|
||||||
vals.push(val);
|
Ok(val) => vals.push(val),
|
||||||
|
Err(_err) => {
|
||||||
|
// At this point we are getting an error because
|
||||||
|
// recursion protection made this equation return
|
||||||
|
// None. We now don't get any evaluation.
|
||||||
|
return None
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,10 +269,19 @@ impl ExtractionContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn dump_vars(&self) -> Vec<String> {
|
pub fn dump_vars(&self) -> Vec<String> {
|
||||||
if let Ok(r) = self.var_registry.read() { r.clone() } else { Vec::new() }
|
if let Ok(r) = self.var_registry.read() {
|
||||||
|
r.clone()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn dump_fns(&self) -> Vec<String> {
|
pub fn dump_fns(&self) -> Vec<String> {
|
||||||
if let Ok(r) = self.fn_registry.read() { r.clone() } else { Vec::new() }
|
if let Ok(r) = self.fn_registry.read() {
|
||||||
|
r.clone()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,8 +309,7 @@ impl Context for ExtractionContext {
|
|||||||
} else {
|
} else {
|
||||||
panic!("The RwLock should always be write-able")
|
panic!("The RwLock should always be write-able")
|
||||||
}
|
}
|
||||||
// Ok(Value::Int(1))
|
Ok(Value::Int(1))
|
||||||
unimplemented!("Extracting function identifier not implemented yet")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn are_builtin_functions_disabled(&self) -> bool {
|
fn are_builtin_functions_disabled(&self) -> bool {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use std::{
|
|||||||
cmp::{max, min},
|
cmp::{max, min},
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
fs,
|
fs,
|
||||||
path::PathBuf, process::Command,
|
path::PathBuf,
|
||||||
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@@ -15,7 +16,7 @@ use crate::app::{
|
|||||||
app::App,
|
app::App,
|
||||||
error_msg::StatusMessage,
|
error_msg::StatusMessage,
|
||||||
logic::{
|
logic::{
|
||||||
calc::{CSV_EXT, CUSTOM_EXT, Grid, LEN},
|
calc::{Grid, LEN},
|
||||||
cell::CellType,
|
cell::CellType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -86,7 +87,7 @@ impl Mode {
|
|||||||
path.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a")
|
path.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a")
|
||||||
));
|
));
|
||||||
|
|
||||||
if let None = app.file {
|
if app.file.is_none() {
|
||||||
app.file = Some(path)
|
app.file = Some(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,11 +154,13 @@ impl Mode {
|
|||||||
|
|
||||||
let mut save_range = |to: &str| {
|
let mut save_range = |to: &str| {
|
||||||
let mut g = Grid::new();
|
let mut g = Grid::new();
|
||||||
|
g.transact_on_grid(|grid| {
|
||||||
for (i, x) in (low_x..=hi_x).enumerate() {
|
for (i, x) in (low_x..=hi_x).enumerate() {
|
||||||
for (j, y) in (low_y..=hi_y).enumerate() {
|
for (j, y) in (low_y..=hi_y).enumerate() {
|
||||||
g.set_cell_raw((i, j), app.grid.get_cell_raw(x, y).clone());
|
grid.set_cell_raw((i, j), grid.get_cell_raw(x, y).clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
if let Err(_e) = g.save_to(to) {
|
if let Err(_e) = g.save_to(to) {
|
||||||
app.msg = StatusMessage::error("Failed to save file");
|
app.msg = StatusMessage::error("Failed to save file");
|
||||||
}
|
}
|
||||||
@@ -171,13 +174,30 @@ impl Mode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "unknown"
|
return "unknown";
|
||||||
};
|
};
|
||||||
|
|
||||||
match args[0] {
|
match args[0] {
|
||||||
|
"f" | "fill" => {
|
||||||
|
app.grid.transact_on_grid(|grid| {
|
||||||
|
for (i, x) in (low_x..=hi_x).enumerate() {
|
||||||
|
for (j, y) in (low_y..=hi_y).enumerate() {
|
||||||
|
let arg = args
|
||||||
|
.get(1)
|
||||||
|
.map(|s| s.replace("xi", &i.to_string()))
|
||||||
|
.map(|s| s.replace("yi", &j.to_string()))
|
||||||
|
.map(|s| s.replace("x", &x.to_string()))
|
||||||
|
.map(|s| s.replace("y", &y.to_string()));
|
||||||
|
grid.set_cell_raw((x, y), arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.mode = Mode::Normal
|
||||||
|
}
|
||||||
"export" => {
|
"export" => {
|
||||||
if let Some(arg1) = args.get(1) {
|
if let Some(arg1) = args.get(1) {
|
||||||
save_range(&arg1);
|
save_range(arg1);
|
||||||
} else {
|
} else {
|
||||||
app.msg = StatusMessage::error("export <path.csv>")
|
app.msg = StatusMessage::error("export <path.csv>")
|
||||||
}
|
}
|
||||||
@@ -187,11 +207,7 @@ impl Mode {
|
|||||||
// Use gnuplot to plot the selected data.
|
// Use gnuplot to plot the selected data.
|
||||||
// * Temp data will be stored in /tmp/
|
// * Temp data will be stored in /tmp/
|
||||||
// * Output will either be plot.png or a name that you pass in
|
// * Output will either be plot.png or a name that you pass in
|
||||||
let output_filename = if let Some(arg1) = args.get(1) {
|
let output_filename = if let Some(arg1) = args.get(1) { arg1 } else { "plot.png" };
|
||||||
arg1
|
|
||||||
} else {
|
|
||||||
"plot.png"
|
|
||||||
};
|
|
||||||
|
|
||||||
save_range("/tmp/plot.csv");
|
save_range("/tmp/plot.csv");
|
||||||
let plot = include_str!("../../template.gnuplot");
|
let plot = include_str!("../../template.gnuplot");
|
||||||
@@ -205,7 +221,9 @@ impl Mode {
|
|||||||
let cmd_res = Command::new("gnuplot").arg("/tmp/plot.p").output();
|
let cmd_res = Command::new("gnuplot").arg("/tmp/plot.p").output();
|
||||||
if let Err(err) = cmd_res {
|
if let Err(err) = cmd_res {
|
||||||
match err.kind() {
|
match err.kind() {
|
||||||
std::io::ErrorKind::NotFound => app.msg = StatusMessage::error("Error - Is gnuplot installed?"),
|
std::io::ErrorKind::NotFound => {
|
||||||
|
app.msg = StatusMessage::error("Error - Is gnuplot installed?")
|
||||||
|
}
|
||||||
_ => app.msg = StatusMessage::error(format!("{err}")),
|
_ => app.msg = StatusMessage::error(format!("{err}")),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -255,20 +273,20 @@ impl Mode {
|
|||||||
// Go to end of row
|
// Go to end of row
|
||||||
'$' => {
|
'$' => {
|
||||||
let (_, y) = app.grid.cursor();
|
let (_, y) = app.grid.cursor();
|
||||||
app.grid.mv_cursor_to(super::logic::calc::LEN, y);
|
app.grid.mv_cursor_to(super::logic::calc::LEN-1, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Go to bottom of column
|
// Go to bottom of column
|
||||||
'G' => {
|
'G' => {
|
||||||
let (x, _) = app.grid.cursor();
|
let (x, _) = app.grid.cursor();
|
||||||
app.grid.mv_cursor_to(x, super::logic::calc::LEN,);
|
app.grid.mv_cursor_to(x, super::logic::calc::LEN-1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// edit cell
|
// edit cell
|
||||||
'i' | 'a' => {
|
'i' | 'a' => {
|
||||||
let (x, y) = app.grid.cursor();
|
let (x, y) = app.grid.cursor();
|
||||||
|
|
||||||
let val = app.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or(String::new());
|
let val = app.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or_default();
|
||||||
|
|
||||||
app.mode = Mode::Insert(Chord::from(val));
|
app.mode = Mode::Insert(Chord::from(val));
|
||||||
}
|
}
|
||||||
@@ -304,6 +322,11 @@ impl Mode {
|
|||||||
app.mode = Mode::Command(Chord::new(':'))
|
app.mode = Mode::Command(Chord::new(':'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// undo
|
||||||
|
'u' => {
|
||||||
|
app.grid.undo();
|
||||||
|
}
|
||||||
|
// paste
|
||||||
'p' => {
|
'p' => {
|
||||||
app.clipboard.paste(&mut app.grid, true);
|
app.clipboard.paste(&mut app.grid, true);
|
||||||
app.grid.apply_momentum(app.clipboard.momentum());
|
app.grid.apply_momentum(app.clipboard.momentum());
|
||||||
@@ -454,14 +477,12 @@ impl Mode {
|
|||||||
let len = match &self {
|
let len = match &self {
|
||||||
Mode::Insert(edit) | Mode::VisualCmd(_, edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(),
|
Mode::Insert(edit) | Mode::VisualCmd(_, edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(),
|
||||||
Mode::Normal => {
|
Mode::Normal => {
|
||||||
let len = cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default();
|
cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default()
|
||||||
len
|
|
||||||
}
|
}
|
||||||
Mode::Visual(_) => 0,
|
Mode::Visual(_) => 0,
|
||||||
};
|
};
|
||||||
// min 20 chars, expand if needed
|
// min 20 chars, expand if needed
|
||||||
let len = max(len as u16 + 1, 20);
|
max(len as u16 + 1, 20)
|
||||||
len
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&self, f: &mut ratatui::Frame, area: prelude::Rect, cell: &Option<CellType>) {
|
pub fn render(&self, f: &mut ratatui::Frame, area: prelude::Rect, cell: &Option<CellType>) {
|
||||||
@@ -475,8 +496,7 @@ impl Mode {
|
|||||||
Mode::Chord(chord) => f.render_widget(chord, area),
|
Mode::Chord(chord) => f.render_widget(chord, area),
|
||||||
Mode::Normal => f.render_widget(
|
Mode::Normal => f.render_widget(
|
||||||
Paragraph::new({
|
Paragraph::new({
|
||||||
let cell = cell.as_ref().map(|f| f.to_string()).unwrap_or_default();
|
cell.as_ref().map(|f| f.to_string()).unwrap_or_default()
|
||||||
cell
|
|
||||||
}),
|
}),
|
||||||
area,
|
area,
|
||||||
),
|
),
|
||||||
@@ -493,20 +513,15 @@ pub struct Chord {
|
|||||||
impl From<String> for Chord {
|
impl From<String> for Chord {
|
||||||
fn from(value: String) -> Self {
|
fn from(value: String) -> Self {
|
||||||
let b = value.as_bytes().iter().map(|f| *f as char).collect();
|
let b = value.as_bytes().iter().map(|f| *f as char).collect();
|
||||||
Chord {
|
Chord { buf: b }
|
||||||
buf: b,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chord {
|
impl Chord {
|
||||||
pub fn new(inital: char) -> Self {
|
pub fn new(inital: char) -> Self {
|
||||||
let mut buf = Vec::new();
|
let buf = vec![inital];
|
||||||
buf.push(inital);
|
|
||||||
|
|
||||||
Self {
|
Self { buf }
|
||||||
buf,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn backspace(&mut self) {
|
pub fn backspace(&mut self) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::{collections::HashMap, sync::RwLock};
|
use std::{cmp::min, collections::HashMap, sync::RwLock};
|
||||||
|
|
||||||
use ratatui::prelude;
|
use ratatui::prelude;
|
||||||
|
|
||||||
use crate::app::logic::calc::{self, LEN};
|
use crate::app::logic::calc::LEN;
|
||||||
|
|
||||||
pub struct ScreenSpace {
|
pub struct ScreenSpace {
|
||||||
/// This is measured in cells.
|
/// This is measured in cells.
|
||||||
@@ -48,11 +48,11 @@ impl ScreenSpace {
|
|||||||
// ======= X =======
|
// ======= X =======
|
||||||
// screen seems to be 2 cells smaller than it should be
|
// screen seems to be 2 cells smaller than it should be
|
||||||
// this is probably related to issue #6
|
// this is probably related to issue #6
|
||||||
let x_cells = (screen_size.0 / self.get_cell_width(vars) as usize) -2;
|
let x_cells = (screen_size.0 / self.get_cell_width(vars)) -2;
|
||||||
// ======= Y =======
|
// ======= Y =======
|
||||||
// screen seems to be 2 cells smaller than it should be
|
// screen seems to be 2 cells smaller than it should be
|
||||||
// this is probably related to issue #6
|
// this is probably related to issue #6
|
||||||
let y_cells = (screen_size.1 / self.get_cell_height(vars) as usize) -2;
|
let y_cells = (screen_size.1 / self.get_cell_height(vars)) -2;
|
||||||
(x_cells,y_cells)
|
(x_cells,y_cells)
|
||||||
} else {
|
} else {
|
||||||
(0,0)
|
(0,0)
|
||||||
@@ -123,22 +123,12 @@ impl ScreenSpace {
|
|||||||
l.1 = area.height as usize;
|
l.1 = area.height as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// let width = (area.width as usize + calc::get_header_size() -1) / self.get_cell_width(vars);
|
let width = (area.width as usize / self.get_cell_width(vars)) + 1;
|
||||||
let width = area.width as usize / self.get_cell_width(vars);
|
|
||||||
let height = area.height as usize / self.get_cell_height(vars);
|
let height = area.height as usize / self.get_cell_height(vars);
|
||||||
|
|
||||||
let x_max =
|
let x_max = min(LEN-1, width);
|
||||||
if width > LEN {
|
let y_max = min(LEN-1, height);
|
||||||
LEN - 1
|
|
||||||
} else {
|
|
||||||
width
|
|
||||||
};
|
|
||||||
let y_max =
|
|
||||||
if height > LEN {
|
|
||||||
LEN - 1
|
|
||||||
} else {
|
|
||||||
height
|
|
||||||
};
|
|
||||||
(x_max as u16, y_max as u16)
|
(x_max as u16, y_max as u16)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +142,7 @@ fn fit_cells() {
|
|||||||
app.vars.insert("height".to_string(), 1.to_string());
|
app.vars.insert("height".to_string(), 1.to_string());
|
||||||
|
|
||||||
let (x,y) = app.screen.how_many_cells_fit_in(&prelude::Rect::new(0, 0, 181, 14), &app.vars);
|
let (x,y) = app.screen.how_many_cells_fit_in(&prelude::Rect::new(0, 0, 181, 14), &app.vars);
|
||||||
assert_eq!(x, 18);
|
assert_eq!(x, 19);
|
||||||
assert_eq!(y, 14);
|
assert_eq!(y, 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +156,7 @@ fn scroll() {
|
|||||||
// We have to check how many cells fit, because screen learns the width
|
// We have to check how many cells fit, because screen learns the width
|
||||||
// of the area by rumour here.
|
// of the area by rumour here.
|
||||||
let (x,y) = app.screen.how_many_cells_fit_in(&prelude::Rect::new(0, 0, 181, 14), &app.vars);
|
let (x,y) = app.screen.how_many_cells_fit_in(&prelude::Rect::new(0, 0, 181, 14), &app.vars);
|
||||||
assert_eq!(x, 18);
|
assert_eq!(x, 19);
|
||||||
assert_eq!(y, 14);
|
assert_eq!(y, 14);
|
||||||
|
|
||||||
// we aren't scrolled at all yet
|
// we aren't scrolled at all yet
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
#![allow(clippy::needless_return)]
|
||||||
|
#![allow(clippy::len_zero)]
|
||||||
|
#![allow(clippy::collapsible_if)]
|
||||||
|
#![allow(clippy::collapsible_else_if)]
|
||||||
|
#![allow(clippy::collapsible_match)]
|
||||||
|
#![allow(clippy::single_match)]
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
|
||||||
use std::env::args;
|
use std::env::args;
|
||||||
|
|||||||
Reference in New Issue
Block a user