Compare commits
7 Commits
d983995e8f
...
full-copy-
| Author | SHA1 | Date | |
|---|---|---|---|
| d5d58694bb | |||
| c2e0661a45 | |||
| 6ec7d90ac5 | |||
| 53dcf2ffc9 | |||
| 86756a94ef | |||
| d242e1af21 | |||
| abffe6073f |
@@ -18,7 +18,10 @@ use ratatui::{
|
|||||||
use crate::app::{
|
use crate::app::{
|
||||||
clipboard::Clipboard,
|
clipboard::Clipboard,
|
||||||
error_msg::StatusMessage,
|
error_msg::StatusMessage,
|
||||||
logic::{calc::{Grid, get_header_size}, cell::CellType},
|
logic::{
|
||||||
|
calc::{Grid, get_header_size},
|
||||||
|
cell::CellType,
|
||||||
|
},
|
||||||
mode::Mode,
|
mode::Mode,
|
||||||
screen::ScreenSpace,
|
screen::ScreenSpace,
|
||||||
};
|
};
|
||||||
@@ -42,7 +45,7 @@ impl Widget for &App {
|
|||||||
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_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;
|
||||||
y1 += 1;
|
y1 += 1;
|
||||||
@@ -78,7 +81,7 @@ impl Widget for &App {
|
|||||||
let mut x_idx: usize = 0;
|
let mut x_idx: usize = 0;
|
||||||
let mut y_idx: usize = 0;
|
let mut y_idx: usize = 0;
|
||||||
if x != 0 {
|
if x != 0 {
|
||||||
x_idx = x as usize -1 + self.screen.scroll_x();
|
x_idx = x as usize - 1 + self.screen.scroll_x();
|
||||||
}
|
}
|
||||||
if y != 0 {
|
if y != 0 {
|
||||||
y_idx = y as usize - 1 + self.screen.scroll_y();
|
y_idx = y as usize - 1 + self.screen.scroll_y();
|
||||||
@@ -93,9 +96,9 @@ impl Widget for &App {
|
|||||||
/// Center the text "99 " -> " 99 "
|
/// Center the text "99 " -> " 99 "
|
||||||
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).into_iter().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))).into_iter().map(|_| ' ').collect::<String>();
|
||||||
format!("{l_margin}{text}{r_margin}")
|
format!("{l_margin}{text}{r_margin}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +217,7 @@ impl Widget for &App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If this is the row header column
|
// If this is the row header column
|
||||||
let area = if x==0 && y != 0 {
|
let area = if x == 0 && y != 0 {
|
||||||
Rect::new(x_off, y_off, row_header_width, cell_height)
|
Rect::new(x_off, y_off, row_header_width, cell_height)
|
||||||
} 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;
|
||||||
@@ -387,17 +390,20 @@ impl App {
|
|||||||
event::KeyCode::Enter => {
|
event::KeyCode::Enter => {
|
||||||
let v = editor.as_string();
|
let v = editor.as_string();
|
||||||
|
|
||||||
// try to insert as a float
|
let cursor = self.grid.cursor();
|
||||||
if let Ok(v) = v.parse::<f64>() {
|
self.grid.transact_on_grid(|grid| {
|
||||||
self.grid.set_cell_raw(self.grid.cursor(), Some(v));
|
// try to insert as a float
|
||||||
} else {
|
if let Ok(v) = v.parse::<f64>() {
|
||||||
// if you can't, then insert as a string
|
grid.set_cell_raw(cursor, Some(v));
|
||||||
if !v.is_empty() {
|
|
||||||
self.grid.set_cell_raw(self.grid.cursor(), Some(v));
|
|
||||||
} else {
|
} else {
|
||||||
self.grid.set_cell_raw::<CellType>(self.grid.cursor(), None);
|
// if you can't, then insert as a string
|
||||||
|
if !v.is_empty() {
|
||||||
|
grid.set_cell_raw(cursor, Some(v.to_owned()));
|
||||||
|
} else {
|
||||||
|
grid.set_cell_raw::<CellType>(cursor, None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
self.mode = Mode::Normal;
|
self.mode = Mode::Normal;
|
||||||
}
|
}
|
||||||
@@ -413,10 +419,23 @@ 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,25 +44,28 @@ impl Clipboard {
|
|||||||
// cursor
|
// cursor
|
||||||
let (cx, cy) = into.cursor();
|
let (cx, cy) = into.cursor();
|
||||||
|
|
||||||
// iterate thru the clipbaord's cells
|
let cursor = into.cursor();
|
||||||
for (x, row) in self.clipboard.iter().enumerate() {
|
into.transact_on_grid(|grid| {
|
||||||
for (y, cell) in row.iter().enumerate() {
|
// iterate thru the clipbaord's cells
|
||||||
let idx = (x + cx, y + cy);
|
for (x, row) in self.clipboard.iter().enumerate() {
|
||||||
|
for (y, cell) in row.iter().enumerate() {
|
||||||
|
let idx = (x + cx, y + cy);
|
||||||
|
|
||||||
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 {
|
||||||
|
// The cell at this location doesn't exist (empty)
|
||||||
|
grid.set_cell_raw::<CellType>(idx, None);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// The cell at this location doesn't exist (empty)
|
// translate = false
|
||||||
into.set_cell_raw::<CellType>(idx, None);
|
grid.set_cell_raw::<CellType>(idx, cell.clone());
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// translate = false
|
|
||||||
into.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();
|
||||||
// clone data into clipboard
|
|
||||||
for x in low_x..=hi_x {
|
from.transact_on_grid(|grid| {
|
||||||
let mut col = Vec::new();
|
// clone data into clipboard
|
||||||
for y in low_y..=hi_y {
|
for x in low_x..=hi_x {
|
||||||
let a = from.get_cell_raw(x, y);
|
let mut col = Vec::new();
|
||||||
col.push(a.clone());
|
for y in low_y..=hi_y {
|
||||||
from.set_cell_raw::<CellType>((x, y), None);
|
let a = grid.get_cell_raw(x, y);
|
||||||
|
col.push(a.clone());
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,5 +395,4 @@ fn copy_paste_range_in_function() {
|
|||||||
|
|
||||||
let a = app.grid.get_cell("B1").as_ref().expect("Should've been set by paste");
|
let a = app.grid.get_cell("B1").as_ref().expect("Should've been set by paste");
|
||||||
assert_eq!(a.to_string(), "=sum(A:A)");
|
assert_eq!(a.to_string(), "=sum(A:A)");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,21 @@ 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();
|
let row_header_width = LEN.to_string().len();
|
||||||
@@ -26,13 +27,109 @@ 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";
|
||||||
|
|
||||||
|
mod internal {
|
||||||
|
use crate::app::logic::{calc::LEN, cell::CellType};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CellGrid {
|
||||||
|
// a b c ...
|
||||||
|
// 0
|
||||||
|
// 1
|
||||||
|
// 2
|
||||||
|
// ...
|
||||||
|
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 {
|
pub struct Grid {
|
||||||
// a b c ...
|
/// Which grid in history are we currently on
|
||||||
// 0
|
current_grid: usize,
|
||||||
// 1
|
/// An array of grids, thru history
|
||||||
// 2
|
grid_history: Vec<CellGrid>,
|
||||||
// ...
|
|
||||||
cells: Vec<Vec<Option<CellType>>>,
|
|
||||||
/// (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,14 +154,17 @@ impl Grid {
|
|||||||
|
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
file.read_to_string(&mut buf)?;
|
file.read_to_string(&mut buf)?;
|
||||||
for (yi, line) in buf.lines().enumerate() {
|
|
||||||
let cells = Self::parse_csv_line(line);
|
|
||||||
|
|
||||||
for (xi, cell) in cells.into_iter().enumerate() {
|
grid.transact_on_grid(|grid| {
|
||||||
// This gets automatically duck-typed
|
for (yi, line) in buf.lines().enumerate() {
|
||||||
grid.set_cell_raw((xi, yi), cell);
|
let cells = Self::parse_csv_line(line);
|
||||||
|
|
||||||
|
for (xi, cell) in cells.into_iter().enumerate() {
|
||||||
|
// This gets automatically duck-typed
|
||||||
|
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,19 +201,15 @@ 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())
|
||||||
@@ -146,6 +231,40 @@ impl Grid {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_grid<'a>(&'a self) -> &'a CellGrid {
|
||||||
|
&self.grid_history[self.current_grid]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_grid_mut<'a>(&'a mut self) -> &'a mut CellGrid {
|
||||||
|
&mut 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,97 +371,55 @@ 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)) {
|
||||||
for x in 0..LEN {
|
self.transact_on_grid(|grid: &mut CellGrid| {
|
||||||
self.cells[x].insert(insertion_y, None);
|
grid.insert_row(insertion_y);
|
||||||
self.cells[x].pop();
|
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), (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 {
|
} else {
|
||||||
unimplemented!("Invalid variable wanted to be translated")
|
unimplemented!("Invalid variable wanted to be translated")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
grid.set_cell_raw((x, y), Some(cell));
|
||||||
}
|
}
|
||||||
)}) {
|
|
||||||
self.set_cell_raw((x,y), Some(cell));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn insert_row_below(&mut self, (x, y): (usize, usize)) {
|
|
||||||
self.insert_row_above((x,y+1));
|
|
||||||
}
|
|
||||||
pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) {
|
|
||||||
let mut v = Vec::with_capacity(LEN);
|
|
||||||
for _ in 0..LEN {
|
|
||||||
v.push(None);
|
|
||||||
}
|
|
||||||
self.cells.insert(insertion_x, v);
|
|
||||||
// keep the grid LEN
|
|
||||||
self.cells.pop();
|
|
||||||
for x in 0..LEN {
|
|
||||||
for y in 0..LEN {
|
|
||||||
if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| {
|
|
||||||
f.custom_translate_cell((0, 0), (1, 0), |rolling, old, new| {
|
|
||||||
if let Some((arg_x, _)) = Grid::parse_to_idx(old) {
|
|
||||||
// add 1 because of the insertion
|
|
||||||
if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) }
|
|
||||||
} else {
|
|
||||||
unimplemented!("Invalid variable wanted to be translated")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}) {
|
|
||||||
self.set_cell_raw((x, y), Some(cell));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_row_below(&mut self, (x, y): (usize, usize)) {
|
||||||
|
self.insert_row_above((x, y + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) {
|
||||||
|
self.transact_on_grid(|grid| {
|
||||||
|
grid.insert_column(insertion_x);
|
||||||
|
for x in 0..LEN {
|
||||||
|
for y in 0..LEN {
|
||||||
|
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| {
|
||||||
|
if let Some((arg_x, _)) = Grid::parse_to_idx(old) {
|
||||||
|
// add 1 because of the insertion
|
||||||
|
if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) }
|
||||||
|
} else {
|
||||||
|
unimplemented!("Invalid variable wanted to be translated")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
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.
|
||||||
@@ -381,10 +458,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}"));
|
||||||
}
|
}
|
||||||
@@ -394,7 +468,7 @@ impl Grid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn char_to_idx(i: &str) -> usize {
|
pub fn char_to_idx(i: &str) -> usize {
|
||||||
let x_idx = i
|
let x_idx = i
|
||||||
.chars()
|
.chars()
|
||||||
.filter(|f| f.is_alphabetic())
|
.filter(|f| f.is_alphabetic())
|
||||||
.enumerate()
|
.enumerate()
|
||||||
@@ -429,15 +503,13 @@ impl Grid {
|
|||||||
|
|
||||||
/// Helper for tests
|
/// Helper for tests
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
/// Don't ever remove this from being just a test-helper.
|
||||||
|
/// This function doesn't correctly use the undo/redo api, which would require doing
|
||||||
|
/// transactions on the grid instead of direct access.
|
||||||
pub fn set_cell<T: Into<CellType>>(&mut self, cell_id: &str, val: T) {
|
pub fn set_cell<T: Into<CellType>>(&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.get_grid_mut().set_cell_raw(loc, Some(val))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,7 +528,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 {
|
||||||
@@ -574,7 +646,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());
|
||||||
|
|
||||||
@@ -711,17 +783,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);
|
||||||
}
|
}
|
||||||
@@ -1072,7 +1144,7 @@ fn insert_row_above_3() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn cell_eval_depth() {
|
fn cell_eval_depth() {
|
||||||
use crate::app::mode::*;
|
use crate::app::mode::*;
|
||||||
let mut app= App::new();
|
let mut app = App::new();
|
||||||
|
|
||||||
app.grid.set_cell("A0", 1.);
|
app.grid.set_cell("A0", 1.);
|
||||||
app.grid.set_cell("A1", "=A0+$A$0".to_string());
|
app.grid.set_cell("A1", "=A0+$A$0".to_string());
|
||||||
@@ -1122,4 +1194,3 @@ fn return_string_from_fn() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ impl<'a> CallbackContext<'a> {
|
|||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
for x in start_idx..=end_idx {
|
for x in start_idx..=end_idx {
|
||||||
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_grid().get_cell_raw(x, y) {
|
||||||
buf.push(s);
|
buf.push(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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::{
|
||||||
@@ -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();
|
||||||
for (i, x) in (low_x..=hi_x).enumerate() {
|
g.transact_on_grid(|grid| {
|
||||||
for (j, y) in (low_y..=hi_y).enumerate() {
|
for (i, x) in (low_x..=hi_x).enumerate() {
|
||||||
g.set_cell_raw((i, j), app.grid.get_cell_raw(x, y).clone());
|
for (j, y) in (low_y..=hi_y).enumerate() {
|
||||||
|
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,22 +174,24 @@ impl Mode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "unknown"
|
return "unknown";
|
||||||
};
|
};
|
||||||
|
|
||||||
match args[0] {
|
match args[0] {
|
||||||
"f" | "fill" => {
|
"f" | "fill" => {
|
||||||
for (i, x) in (low_x..=hi_x).enumerate() {
|
app.grid.transact_on_grid(|grid| {
|
||||||
for (j, y) in (low_y..=hi_y).enumerate() {
|
for (i, x) in (low_x..=hi_x).enumerate() {
|
||||||
let arg = args.get(1)
|
for (j, y) in (low_y..=hi_y).enumerate() {
|
||||||
.map(|s| s.replace("xi", &i.to_string()))
|
let arg = args
|
||||||
.map(|s| s.replace("yi", &j.to_string()))
|
.get(1)
|
||||||
.map(|s| s.replace("x", &x.to_string()))
|
.map(|s| s.replace("xi", &i.to_string()))
|
||||||
.map(|s| s.replace("y", &y.to_string()))
|
.map(|s| s.replace("yi", &j.to_string()))
|
||||||
;
|
.map(|s| s.replace("x", &x.to_string()))
|
||||||
app.grid.set_cell_raw((x,y), arg);
|
.map(|s| s.replace("y", &y.to_string()));
|
||||||
|
grid.set_cell_raw((x, y), arg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
app.mode = Mode::Normal
|
app.mode = Mode::Normal
|
||||||
}
|
}
|
||||||
@@ -202,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");
|
||||||
@@ -217,10 +218,12 @@ impl Mode {
|
|||||||
let s = s.replace("$OUTPUT", "/tmp/output.png");
|
let s = s.replace("$OUTPUT", "/tmp/output.png");
|
||||||
let _ = fs::write("/tmp/plot.p", s);
|
let _ = fs::write("/tmp/plot.p", s);
|
||||||
|
|
||||||
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 {
|
||||||
@@ -276,7 +279,7 @@ impl Mode {
|
|||||||
// 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);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// edit cell
|
// edit cell
|
||||||
@@ -319,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());
|
||||||
@@ -398,7 +406,7 @@ impl Mode {
|
|||||||
let (_, y_height) = app.screen.get_screen_size(&app.vars);
|
let (_, y_height) = app.screen.get_screen_size(&app.vars);
|
||||||
let y_origin = app.screen.scroll_y();
|
let y_origin = app.screen.scroll_y();
|
||||||
|
|
||||||
app.grid.mv_cursor_to(x, y_origin+y_height);
|
app.grid.mv_cursor_to(x, y_origin + y_height);
|
||||||
app.mode = Mode::Normal;
|
app.mode = Mode::Normal;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -408,7 +416,7 @@ impl Mode {
|
|||||||
let (x_width, _) = app.screen.get_screen_size(&app.vars);
|
let (x_width, _) = app.screen.get_screen_size(&app.vars);
|
||||||
let x_origin = app.screen.scroll_x();
|
let x_origin = app.screen.scroll_x();
|
||||||
|
|
||||||
app.grid.mv_cursor_to(x_origin+x_width, y);
|
app.grid.mv_cursor_to(x_origin + x_width, y);
|
||||||
app.mode = Mode::Normal;
|
app.mode = Mode::Normal;
|
||||||
}
|
}
|
||||||
// Go to the left edge of the current window
|
// Go to the left edge of the current window
|
||||||
@@ -508,9 +516,7 @@ 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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,9 +525,7 @@ impl Chord {
|
|||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
buf.push(inital);
|
buf.push(inital);
|
||||||
|
|
||||||
Self {
|
Self { buf }
|
||||||
buf,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn backspace(&mut self) {
|
pub fn backspace(&mut self) {
|
||||||
|
|||||||
Reference in New Issue
Block a user