Compare commits

42 Commits

Author SHA1 Message Date
d5d58694bb redo
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 43s
Undo / Redo are currently bound to Pageup/down. This is ideally temporary, I just need to figure out ctrl+r for redo. "u" is also bound to undo as it should be
2026-02-07 20:46:22 -07:00
c2e0661a45 solve #29 2026-02-07 20:37:01 -07:00
6ec7d90ac5 undo 2026-02-07 20:30:19 -07:00
53dcf2ffc9 mock up history
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 56s
2026-02-06 17:04:12 -07:00
86756a94ef deprecate old function 2026-02-06 16:55:50 -07:00
d242e1af21 apply api changes 2026-02-06 16:46:15 -07:00
abffe6073f setup history of file 2026-02-06 15:55:27 -07:00
d983995e8f solve failing test
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 53s
copy and pasting functions with ranges as arguments now works
2026-02-06 11:31:41 -07:00
045d1d6554 solve #36 by adding :fill command
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 53s
2026-01-28 16:29:16 -07:00
9691268d3d readme and create issue
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 38s
2026-01-27 16:53:46 -07:00
f3356c1398 fix #35, allows for strings to be returned from functions
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 44s
2026-01-27 16:07:23 -07:00
b3b2c59a36 fix #47
All checks were successful
Test Rust project / test (ubuntu-latest, stable) (push) Successful in 43s
2026-01-27 14:50:05 -07:00
077b53f6ff cleanup imports
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 41s
2026-01-27 13:50:00 -07:00
0c78e7834b fix #33 2026-01-27 13:49:46 -07:00
052828c89c auto run test
Some checks failed
Test Rust project / test (ubuntu-latest, stable) (push) Failing after 57s
2026-01-27 11:13:23 -07:00
626b8ff4eb code cleanup 2026-01-27 10:52:18 -07:00
69c7ebf24b make xlookup function like excel 2026-01-23 13:35:50 -07:00
8d3f003427 center column titles 2026-01-23 12:13:25 -07:00
1923bbe674 close #41 2026-01-23 11:37:13 -07:00
2076c42c80 add new keybinds to readme 2026-01-23 11:22:07 -07:00
91fb658f65 close #24 2026-01-23 11:17:43 -07:00
d82a3d263e solve #22 2026-01-23 10:41:47 -07:00
4452d3210e start on #47 2026-01-22 15:46:17 -07:00
da5e4921d8 readme cleanup 2026-01-22 14:28:38 -07:00
b5de08300a spelling 2026-01-22 14:20:32 -07:00
4ae7aec66e cleanup gnuplot command 2026-01-22 14:11:02 -07:00
3ff9e8683c fix #32 2026-01-22 13:52:10 -07:00
0c2588a299 example 2 line graph 2025-11-19 13:49:30 -07:00
0c527bb3cb add gnuplot plotting 2025-11-18 13:02:29 -07:00
6ca21b407d close #36 2025-11-18 12:33:34 -07:00
1323d15333 actually use the filetype 2025-11-18 10:11:01 -07:00
d69966bc01 fix csv (again) - added tests this time. Started on #27, #36 2025-11-18 10:09:29 -07:00
dae3b57f73 setup test for #33 2025-11-17 14:50:38 -07:00
ee1cac0522 fix #46 2025-11-17 14:39:30 -07:00
712d75b256 close #45 2025-11-17 13:42:17 -07:00
d5ccb9b8c6 close #42 2025-11-14 15:30:36 -07:00
a03794e69f prepwork for #41 2025-11-14 14:26:04 -07:00
98215e42af update 2025-11-14 10:45:02 -07:00
b8fd938120 close #38 2025-11-14 10:34:43 -07:00
3be92aea3c close #40 2025-11-14 09:46:57 -07:00
902af1311d reorganize file extension logic 2025-11-14 09:40:53 -07:00
9c44db0d92 clean up rendering function 2025-11-14 09:33:11 -07:00
14 changed files with 1305 additions and 424 deletions

View File

@@ -0,0 +1,21 @@
name: Test Rust project
on: [push]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
rust: [stable]
steps:
- uses: https://git.oliveratkinson.net/Oliver/setup-rust-action@master
with:
rust-version: ${{ matrix.rust }}
- uses: actions/checkout@master
- name: Run tests
run: cargo test --verbose
- name: Clippy
run: cargo clippy

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
/.vscode /.vscode
/*.csv /*.csv
/*.nscim /*.nscim
/plot.png

View File

@@ -2,23 +2,11 @@
*New* Spreadsheet Calculator Improved *New* Spreadsheet Calculator Improved
![Screenshot](readme/screenshot.png)
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.
## Improvements from sc-im
* Cell type (string, function, value) are all "the same" and use polymorphism to detect what you are trying to do.
The logic is more or less:
```cpp
if value.can_be_a_number() {
return value.as_number()
} else if value.starts_with('=') {
return value.as_equation()
} else {
return value.as_string()
}
```
* Keybinds are closer to vim keying (i/a start inserting into a cell)
[Checkout a demo file](readme/demo.nscim), download, then open with `neoscim demo.nscim`.
## Keybinds ## Keybinds
@@ -30,8 +18,13 @@ if value.can_be_a_number() {
| `k` | Move up | | `k` | Move up |
| `h` | Move left | | `h` | Move left |
| `l` | Move right | | `l` | Move right |
| `0` | Go to beginning of row | | `0` | Go to beginning of the row |
| `gg` | Go to beginning of column | | `g0` | Go to beginning of the visual row |
| `$` | Go to end of the row |
| `g$` | Go to end of the visual row |
| `gg` | Go to beginning of the column |
| `G` | Go to end of column |
| `gG` | Go to end of the the visual column |
| `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 |
@@ -67,6 +60,16 @@ if value.can_be_a_number() {
| `:q` | Quit program | | `:q` | Quit program |
| `:q!` | Quit program, even if the file isn't saved | | `:q!` | Quit program, even if the file isn't saved |
#### Visual mode commands
These commands operate on selections.
| Commmand | Description |
| - | - |
| `:plot` | Plots the current selection to `plot.png` using gnuplot |
| `: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
### Math ### Math
@@ -84,11 +87,11 @@ if value.can_be_a_number() {
| Identifier | Arg Qty | Argument Types | Description | | Identifier | Arg Qty | Argument Types | Description |
| - | - | - | - | | - | - | - | - |
| `avg` | >= 1 | Numeric | Returns the average of the arguments | | `avg` | 1 | Numeric | Returns the average of the arguments |
| `sum` | >= 1 | Numbers | Returns the sum of the arguments | | `sum` | 1 | Numbers | Returns the sum of the arguments |
| `xlookup` | 3 | Range, Number/String, Range | Searches for the number/string in the first range, returning the item at the same index in the second range | | `xlookup` | 3 | Number/String, Range, Range | Searches for the number/string in the first range, returning the item at the same index in the second range |
| `min` | >= 1 | Numeric | Returns the minimum of the arguments | | `min` | 1 | Numeric | Returns the minimum of the arguments |
| `max` | >= 1 | Numeric | Returns the maximum of the arguments | | `max` | 1 | Numeric | Returns the maximum of the arguments |
| `len` | 1 | String/Tuple | Returns the character length of a string, or the amount of elements in a tuple (not recursively) | | `len` | 1 | String/Tuple | Returns the character length of a string, or the amount of elements in a tuple (not recursively) |
| `floor` | 1 | Numeric | Returns the largest integer less than or equal to a number | | `floor` | 1 | Numeric | Returns the largest integer less than or equal to a number |
| `round` | 1 | Numeric | Returns the nearest integer to a number. Rounds half-way cases away from 0.0 | | `round` | 1 | Numeric | Returns the nearest integer to a number. Rounds half-way cases away from 0.0 |
@@ -128,7 +131,7 @@ if value.can_be_a_number() {
| `str::to_lowercase` | 1 | String | Returns the lower-case version of the string | | `str::to_lowercase` | 1 | String | Returns the lower-case version of the string |
| `str::to_uppercase` | 1 | String | Returns the upper-case version of the string | | `str::to_uppercase` | 1 | String | Returns the upper-case version of the string |
| `str::trim` | 1 | String | Strips whitespace from the start and the end of the string | | `str::trim` | 1 | String | Strips whitespace from the start and the end of the string |
| `str::from` | >= 0 | Any | Returns passed value as string | | `str::from` | 0 | Any | Returns passed value as string |
| `str::substring` | 3 | String, Int, Int | Returns a substring of the first argument, starting at the second argument and ending at the third argument. If the last argument is omitted, the substring extends to the end of the string | | `str::substring` | 3 | String, Int, Int | Returns a substring of the first argument, starting at the second argument and ending at the third argument. If the last argument is omitted, the substring extends to the end of the string |
| `bitand` | 2 | Int | Computes the bitwise and of the given integers | | `bitand` | 2 | Int | Computes the bitwise and of the given integers |
| `bitor` | 2 | Int | Computes the bitwise or of the given integers | | `bitor` | 2 | Int | Computes the bitwise or of the given integers |
@@ -141,6 +144,19 @@ if value.can_be_a_number() {
> Any time a function takes a variable amount of inputs, a range can be specified: `avg(B:B)`. > Any time a function takes a variable amount of inputs, a range can be specified: `avg(B:B)`.
## Improvements from sc-im
* Cell type (string, function, value) are all "the same" and use polymorphism to detect what you are trying to do.
If the cell can be a number (parsed into a float) it will be, if not, then if the string starts with "=", then treats it as an equation, otherwise, it's a string.
* Cell references move with copy / paste
* =A0: Will translate
* =$A0: Will translate Y only (numbers)
* =A$0: Will translate X only (letters)
* =$A$0: No translation
* Keybinds are closer to vim keying (i/a start inserting into a cell)
## FAQ: ## FAQ:
* Every number is a float (or will end up as a float) * Every number is a float (or will end up as a float)

10
bash_complitions Normal file
View File

@@ -0,0 +1,10 @@
# This is only WIP, idk how to make it only take
# 1 argument yet
_complete() {
local cur="${COMP_WORDS[COMP_CWORD]}"
# Only suggest *.csv and *.neoscim
COMPREPLY=($(compgen -f -X '!*.@(csv|nscim)' -- "$cur"))
}
complete -F _complete sc

21
readme/demo.nscim Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,13 +1,29 @@
use std::{ use std::{
cmp::{max, min}, collections::HashMap, io, path::PathBuf cmp::{max, min},
collections::HashMap,
fs, io,
path::PathBuf,
time::SystemTime,
}; };
use ratatui::{ use ratatui::{
DefaultTerminal, Frame, crossterm::event, layout::{self, Constraint, Layout, Rect}, prelude, style::{Color, Modifier, Style}, widgets::{Paragraph, Widget} DefaultTerminal, Frame,
crossterm::event,
layout::{self, Constraint, Layout, Rect},
prelude,
style::{Color, Modifier, Style},
widgets::{Paragraph, Widget},
}; };
use crate::app::{ use crate::app::{
clipboard::Clipboard, error_msg::StatusMessage, logic::{calc::Grid, cell::CellType}, mode::Mode, screen::ScreenSpace clipboard::Clipboard,
error_msg::StatusMessage,
logic::{
calc::{Grid, get_header_size},
cell::CellType,
},
mode::Mode,
screen::ScreenSpace,
}; };
pub struct App { pub struct App {
@@ -15,6 +31,7 @@ pub struct App {
pub grid: Grid, pub grid: Grid,
pub mode: Mode, pub mode: Mode,
pub file: Option<PathBuf>, pub file: Option<PathBuf>,
file_modified_date: SystemTime,
pub msg: StatusMessage, pub msg: StatusMessage,
pub vars: HashMap<String, String>, pub vars: HashMap<String, String>,
pub screen: ScreenSpace, pub screen: ScreenSpace,
@@ -28,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)) = 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;
@@ -49,8 +66,12 @@ impl Widget for &App {
for x in 0..x_max { for x in 0..x_max {
for y in 0..y_max { for y in 0..y_max {
let mut display = String::new(); let mut display = String::new();
let mut style = Style::new().fg(Color::White); let mut style = Style::new();
// Custom width for the header of each row
let row_header_width = get_header_size() as u16;
// ^^ Feels like it oculd be static but evaluating string lens doesn't work at
// compile time. Thus cannot be static.
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;
@@ -66,16 +87,21 @@ impl Widget for &App {
y_idx = y as usize - 1 + self.screen.scroll_y(); y_idx = y as usize - 1 + self.screen.scroll_y();
} }
if is_selected(x.into(), y.into()) {
style = style.fg(Color::LightMagenta).bg(Color::Blue);
}
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);
let mut should_render = true; let mut should_render = true;
let mut suggest_upper_bound = None; let mut suggest_upper_bound = None;
/// Center the text "99 " -> " 99 "
fn center_text(text: &str, avaliable_space: i32) -> String {
let margin = avaliable_space - text.len() as i32;
let margin = margin / 2;
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>();
format!("{l_margin}{text}{r_margin}")
}
match (x == 0, y == 0) { match (x == 0, y == 0) {
// 0,0 vi mode // 0,0 vi mode
(true, true) => { (true, true) => {
@@ -84,7 +110,7 @@ impl Widget for &App {
} }
// row names // row names
(true, false) => { (true, false) => {
display = y_idx.to_string(); display = center_text(&y_idx.to_string(), row_header_width as i32);
let bg = if y_idx == self.grid.cursor().1 { let bg = if y_idx == self.grid.cursor().1 {
Color::DarkGray Color::DarkGray
@@ -97,7 +123,7 @@ impl Widget for &App {
} }
// column names // column names
(false, true) => { (false, true) => {
display = Grid::num_to_char(x_idx); display = center_text(&Grid::num_to_char(x_idx), cell_width as i32);
let bg = if x_idx == self.grid.cursor().0 { let bg = if x_idx == self.grid.cursor().0 {
Color::DarkGray Color::DarkGray
@@ -113,14 +139,21 @@ impl Widget for &App {
(false, false) => { (false, false) => {
match self.grid.get_cell_raw(x_idx, y_idx) { match self.grid.get_cell_raw(x_idx, y_idx) {
Some(cell) => { Some(cell) => {
// Render in different colors based on type of contents
match cell { match cell {
CellType::Number(c) => display = c.to_string(), CellType::Number(c) => display = c.to_string(),
CellType::String(s) => display = s.to_owned(), CellType::String(s) => {
display = s.to_owned();
style = Style::new().fg(Color::LightMagenta)
}
CellType::Equation(e) => { CellType::Equation(e) => {
match self.grid.evaluate(e) { match self.grid.evaluate(e) {
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);
} }
@@ -136,6 +169,7 @@ impl Widget for &App {
} }
} }
// 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 {
@@ -144,9 +178,23 @@ impl Widget for &App {
break; break;
} }
} }
if let Some(bound) = suggest_upper_bound {
let bound = bound as usize;
if bound < display.len() {
display.truncate(bound - 2);
display.push('…');
} }
}
}
// Don't render blank cells
None => should_render = false, None => should_render = false,
} }
if is_selected(x.into(), y.into()) {
style = style.bg(Color::Blue);
// Make it so that cells render when selected. This fixes issue #32
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);
@@ -157,11 +205,21 @@ impl Widget for &App {
} }
} }
} }
if should_render { if should_render {
let x_off = area.x + (x * cell_width); let mut x_off = area.x + (x * cell_width);
let y_off = area.y + (y * cell_height); let y_off = area.y + (y * cell_height);
let area = if let Some(suggestion) = suggest_upper_bound { // Adjust for the fact that the first column
// is smaller, since it is just headers
if x > 0 {
x_off = x_off - (cell_width - row_header_width);
}
// If this is the row header column
let area = if x == 0 && y != 0 {
Rect::new(x_off, y_off, row_header_width, cell_height)
} 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 as u16);
@@ -192,13 +250,24 @@ impl App {
screen: ScreenSpace::new(), screen: ScreenSpace::new(),
marks: HashMap::new(), marks: HashMap::new(),
clipboard: Clipboard::new(), clipboard: Clipboard::new(),
file_modified_date: SystemTime::now(),
} }
} }
pub fn new_with_file(file: impl Into<PathBuf> + Clone) -> std::io::Result<Self> { pub fn new_with_file(file: impl Into<PathBuf> + Clone) -> std::io::Result<Self> {
let mut app = Self::new(); let mut app = Self::new();
app.file = Some(file.clone().into()); app.file = Some(file.clone().into());
app.grid = Grid::new_from_file(file.into())?;
let mut file = fs::OpenOptions::new().read(true).open(file.into())?;
let metadata = file.metadata()?;
// Not all systems support this, apparently.
if let Ok(time) = metadata.modified() {
app.file_modified_date = time;
} else {
// Default is to just assume it was modified when we opened it.
}
app.grid = Grid::new_from_file(&mut file)?;
Ok(app) Ok(app)
} }
@@ -210,27 +279,7 @@ impl App {
Ok(()) Ok(())
} }
fn draw(&self, frame: &mut Frame) { fn file_name_display(&self) -> String {
let layout = Layout::default()
.direction(layout::Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.split(frame.area());
let cmd_line = layout[0];
let body = layout[1];
let len = match &self.mode {
Mode::Insert(edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(),
Mode::Normal => {
let (x, y) = self.grid.cursor();
let cell = self.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string().len()).unwrap_or_default();
cell
}
Mode::Visual(_) => 0,
};
// min 20 chars, expand if needed
let len = max(len as u16 + 1, 20);
let file_name_status = { let file_name_status = {
let mut file_name = "[No Name]"; let mut file_name = "[No Name]";
let mut icon = ""; let mut icon = "";
@@ -246,39 +295,46 @@ impl App {
} }
format!("{file_name}{icon}") format!("{file_name}{icon}")
}; };
file_name_status
}
fn draw(&self, frame: &mut Frame) {
let (x, y) = self.grid.cursor();
let current_cell = self.grid.get_cell_raw(x, y);
let len = self.mode.chars_to_display(current_cell);
let file_name_status = self.file_name_display();
// layout
// ======================================================
let layout = Layout::default()
.direction(layout::Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.split(frame.area());
let cmd_line = layout[0];
let body = layout[1];
let cmd_line_split = Layout::default() let cmd_line_split = Layout::default()
.direction(layout::Direction::Horizontal) .direction(layout::Direction::Horizontal)
.constraints([Constraint::Length(len), Constraint::Length(file_name_status.len() as u16 + 1), Constraint::Percentage(50), Constraint::Percentage(50)]) .constraints([
Constraint::Length(len),
Constraint::Length(file_name_status.len() as u16 + 1),
Constraint::Percentage(50),
Constraint::Percentage(50),
])
.split(cmd_line); .split(cmd_line);
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];
let cmd_line_debug = cmd_line_split[3]; let cmd_line_debug = cmd_line_split[3];
// ======================================================
match &self.mode { self.mode.render(frame, cmd_line_left, current_cell);
Mode::Insert(editor) => {
frame.render_widget(editor, cmd_line_left);
}
Mode::Command(editor) => {
frame.render_widget(editor, cmd_line_left);
}
Mode::Chord(chord) => frame.render_widget(chord, cmd_line_left),
Mode::Normal => frame.render_widget(
Paragraph::new({
let (x, y) = self.grid.cursor();
let cell = self.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or_default();
cell
}),
cmd_line_left,
),
Mode::Visual(_) => {}
}
frame.render_widget(self, body); frame.render_widget(self, body);
frame.render_widget(&self.msg, cmd_line_right); frame.render_widget(&self.msg, cmd_line_right);
frame.render_widget(Paragraph::new(file_name_status), cmd_line_status); frame.render_widget(Paragraph::new(file_name_status), cmd_line_status);
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
frame.render_widget( frame.render_widget(
Paragraph::new(format!( Paragraph::new(format!(
@@ -297,6 +353,21 @@ impl App {
fn handle_events(&mut self) -> io::Result<()> { fn handle_events(&mut self) -> io::Result<()> {
match &mut self.mode { match &mut self.mode {
Mode::VisualCmd(pos, chord) => match event::read()? {
event::Event::Key(key) => match key.code {
event::KeyCode::Esc => self.mode = Mode::Visual(*pos),
event::KeyCode::Backspace => chord.backspace(),
event::KeyCode::Char(c) => chord.add_char(c),
event::KeyCode::Enter => {
// tmp is to get around reference issues.
let tmp = pos.clone();
Mode::process_cmd(self);
self.mode = Mode::Visual(tmp)
}
_ => {}
},
_ => {}
},
Mode::Chord(chord) => match event::read()? { Mode::Chord(chord) => match event::read()? {
event::Event::Key(key) => match key.code { event::Event::Key(key) => match key.code {
event::KeyCode::Esc => self.mode = Mode::Normal, event::KeyCode::Esc => self.mode = Mode::Normal,
@@ -319,17 +390,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;
} }
@@ -345,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");
} }
},
_ => {} _ => {}
}, },
_ => {} _ => {}
@@ -366,20 +453,13 @@ impl App {
} }
Mode::Command(editor) => match event::read()? { Mode::Command(editor) => match event::read()? {
event::Event::Key(key) => match key.code { event::Event::Key(key) => match key.code {
event::KeyCode::Esc => { event::KeyCode::Esc => self.mode = Mode::Normal,
// just cancel the operation event::KeyCode::Backspace => editor.backspace(),
self.mode = Mode::Normal; event::KeyCode::Char(c) => editor.add_char(c),
}
event::KeyCode::Enter => { event::KeyCode::Enter => {
Mode::process_cmd(self); Mode::process_cmd(self);
self.mode = Mode::Normal; self.mode = Mode::Normal;
} }
event::KeyCode::Backspace => {
editor.backspace();
}
event::KeyCode::Char(c) => {
editor.add_char(c);
}
_ => {} _ => {}
}, },
_ => {} _ => {}

View File

@@ -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,24 +44,28 @@ 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
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() {
let idx = (x + cx, y + cy); 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 { } else {
// cell doesn't exist, no need to translate // 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);
@@ -108,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);
} }
} }
@@ -296,3 +298,101 @@ fn copy_paste_vars_translate() {
let a = app.grid.get_cell("A0").as_ref().expect("Should've been set by paste"); let a = app.grid.get_cell("A0").as_ref().expect("Should've been set by paste");
assert_eq!(a.to_string(), "=A1"); assert_eq!(a.to_string(), "=A1");
} }
#[test]
fn copy_paste_double_locked_var() {
let mut app = App::new();
app.grid.set_cell("A0", 0.);
app.grid.set_cell("A1", "=$A$0".to_string());
// Copy A0
app.grid.mv_cursor_to(0, 1);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
app.grid.mv_cursor_to(1, 0);
Mode::process_key(&mut app, 'p');
let c = app.grid.get_cell("B0").as_ref().expect("Just set it");
assert_eq!(c.to_string(), "=$A$0");
}
#[test]
fn copy_paste_x_locked_var() {
let mut app = App::new();
app.grid.set_cell("A0", 0.);
app.grid.set_cell("A1", "=$A0".to_string());
// Copy A0
app.grid.mv_cursor_to(0, 1);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
app.grid.mv_cursor_to(1, 2);
Mode::process_key(&mut app, 'p');
let c = app.grid.get_cell("B2").as_ref().expect("Just set it");
assert_eq!(c.to_string(), "=$A1");
}
#[test]
fn copy_paste_y_locked_var() {
let mut app = App::new();
app.grid.set_cell("A0", 0.);
app.grid.set_cell("A1", "=A$0".to_string());
// Copy A0
app.grid.mv_cursor_to(0, 1);
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
app.grid.mv_cursor_to(1, 2);
Mode::process_key(&mut app, 'p');
let c = app.grid.get_cell("B2").as_ref().expect("Just set it");
assert_eq!(c.to_string(), "=B$0");
}
#[test]
fn copy_paste_var_in_function() {
let mut app = App::new();
app.grid.set_cell("A0", 4.to_string());
Mode::process_key(&mut app, 'j');
app.grid.set_cell("A1", "=math::log2(A0)".to_string());
app.mode = super::mode::Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
Mode::process_key(&mut app, 'j');
Mode::process_key(&mut app, 'p');
let a = app.grid.get_cell("A2").as_ref().expect("Should've been set by paste");
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)");
}

View File

@@ -1,32 +1,135 @@
use std::{ use std::{
cmp::{max, min}, cmp::{max, min},
fs, 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 const LEN: usize = 1000; pub fn get_header_size() -> usize {
let row_header_width = LEN.to_string().len();
row_header_width
}
pub struct Grid { pub const LEN: usize = 1001;
pub const CSV_EXT: &str = "csv";
pub const CUSTOM_EXT: &str = "nscim";
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?
@@ -41,28 +144,18 @@ 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); Self { current_grid: 0, grid_history: vec![x], selected_cell: (0, 0), dirty: false }
for _ in 0..LEN {
b.push(None)
}
a.push(b)
} }
Self { pub fn new_from_file(file: &mut File) -> std::io::Result<Self> {
cells: a,
selected_cell: (0, 0),
dirty: false,
}
}
pub fn new_from_file(path: impl Into<PathBuf>) -> std::io::Result<Self> {
let mut grid = Self::new(); let mut grid = Self::new();
let mut file = fs::OpenOptions::new().read(true).open(path.into())?;
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);
@@ -71,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;
@@ -83,51 +177,53 @@ impl Grid {
pub fn save_to(&mut self, path: impl Into<PathBuf>) -> std::io::Result<()> { pub fn save_to(&mut self, path: impl Into<PathBuf>) -> std::io::Result<()> {
let mut path = path.into(); let mut path = path.into();
const CSV: &str = "csv";
const CUSTOM_EXT: &str = "nscim";
let resolve_values; let resolve_values;
match path.extension() { match path.extension() {
Some(ext) => match ext.to_str() { Some(ext) => match ext.to_str() {
Some(CSV) => { Some(CSV_EXT) => {
resolve_values = true; resolve_values = true;
} }
Some(CUSTOM_EXT) => { Some(CUSTOM_EXT) => {
resolve_values = true; resolve_values = false;
} }
_ => { _ => {
resolve_values = false; // File as an extension but isn't ours.
path.add_extension(CUSTOM_EXT); // Save as csv-like
resolve_values = true;
} }
}, },
None => { None => {
// File has no extension. Save it as our file type
resolve_values = false; resolve_values = false;
path.add_extension(CUSTOM_EXT); path.add_extension(CUSTOM_EXT);
} }
} }
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.
// else, just put a comma after the cell.
let is_last = x == mx;
let delim = if is_last { '\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
{ {
val.to_string() format!("{}{}", val.to_string(), delim)
} else { } else {
cell.escaped_csv_string() format!("{}{}", cell.escaped_csv_string(), delim)
} }
} else { } else {
CSV_DELIMITER.to_string() delim.to_string()
}; };
write!(f, "{data}")?; write!(f, "{data}")?;
} }
write!(f, "\n")?;
} }
f.flush()?; f.flush()?;
@@ -135,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
} }
@@ -241,38 +371,36 @@ 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 { } else {
unimplemented!("Invalid variable wanted to be translated") unimplemented!("Invalid variable wanted to be translated")
} }
} })
)}) { }) {
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
@@ -282,60 +410,20 @@ impl Grid {
} }
}) })
}) { }) {
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 {
@@ -349,11 +437,15 @@ impl Grid {
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());
}; };
@@ -366,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}"));
} }
@@ -378,10 +467,22 @@ impl Grid {
} }
} }
pub fn char_to_idx(i: &str) -> usize {
let x_idx = i
.chars()
.filter(|f| f.is_alphabetic())
.enumerate()
.map(|(idx, c)| ((c.to_ascii_lowercase() as usize).saturating_sub(97)) + (26 * idx))
.fold(0, |a, b| a + b);
x_idx
}
/// 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 chars = i.chars().take_while(|c| c.is_alphabetic()).collect::<Vec<char>>(); let i = i.replace('$', "");
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
@@ -390,11 +491,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>() {
@@ -406,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;
} }
@@ -433,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 {
@@ -460,12 +555,98 @@ impl Default for Grid {
} }
} }
#[test]
fn saving_csv() {
// setup grid
// This should be 1..10 in the A column, then
// 1^2..10^2 in the B column.
let mut app = App::new();
app.grid.set_cell("A0", 1.);
app.grid.set_cell("B0", "=A0^2".to_string());
app.grid.set_cell("A1", "=A0+A$0".to_string());
app.grid.set_cell("B1", "=A1^2".to_string());
app.grid.mv_cursor_to(0, 1);
app.mode = Mode::Visual(app.grid.cursor());
Mode::process_key(&mut app, 'l');
Mode::process_key(&mut app, 'y');
app.mode = Mode::Normal;
app.grid.mv_cursor_to(0, 2);
for _ in 0..10 {
Mode::process_key(&mut app, 'p');
}
// setup done
// insure that the cells are there
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");
assert_eq!(res, (11.0).into());
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 res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
assert_eq!(res, (121.0).into());
assert_eq!(cell.escaped_csv_string(), "=A10^2");
// set saving the file
let filename = "/tmp/file.csv";
app.grid.save_to(filename).expect("This will only work on linux systems");
let mut file = fs::OpenOptions::new().read(true).open(filename).expect("Just wrote the file");
let mut buf = String::new();
file.read_to_string(&mut buf).expect("Just opened the file");
let line = buf.lines().skip(10).next();
assert_eq!(line, Some("11,121"));
}
#[test]
fn saving_neoscim() {
// setup grid
// This should be 1..10 in the A column, then
// 1^2..10^2 in the B column.
let mut app = App::new();
app.grid.set_cell("A0", 1.);
app.grid.set_cell("B0", "=A0^2".to_string());
app.grid.set_cell("A1", "=A0+A$0".to_string());
app.grid.set_cell("B1", "=A1^2".to_string());
app.grid.mv_cursor_to(0, 1);
app.mode = Mode::Visual(app.grid.cursor());
Mode::process_key(&mut app, 'l');
Mode::process_key(&mut app, 'y');
app.mode = Mode::Normal;
app.grid.mv_cursor_to(0, 2);
for _ in 0..10 {
Mode::process_key(&mut app, 'p');
}
// setup done
// insure that the cells are there
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");
assert_eq!(res, (11.0).into());
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 res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
assert_eq!(res, (121.0).into());
assert_eq!(cell.escaped_csv_string(), "=A10^2");
// set saving the file
let filename = format!("/tmp/file.{CUSTOM_EXT}");
app.grid.save_to(&filename).expect("This will only work on linux systems");
let mut file = fs::OpenOptions::new().read(true).open(filename).expect("Just wrote the file");
let mut buf = String::new();
file.read_to_string(&mut buf).expect("Just opened the file");
let line = buf.lines().skip(10).next();
assert_eq!(line, Some("=A9+A$0,=A10^2"));
}
// Do cells hold strings? // Do cells hold strings?
#[test] #[test]
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());
@@ -476,6 +657,9 @@ fn cell_strings() {
#[test] #[test]
fn alphanumeric_indexing() { fn alphanumeric_indexing() {
assert_eq!(Grid::parse_to_idx("A0"), Some((0, 0))); assert_eq!(Grid::parse_to_idx("A0"), Some((0, 0)));
assert_eq!(Grid::parse_to_idx("$A0"), Some((0, 0)));
assert_eq!(Grid::parse_to_idx("A$0"), Some((0, 0)));
assert_eq!(Grid::parse_to_idx("$A$0"), Some((0, 0)));
assert_eq!(Grid::parse_to_idx("AA0"), Some((26, 0))); assert_eq!(Grid::parse_to_idx("AA0"), Some((26, 0)));
assert_eq!(Grid::parse_to_idx("A1"), Some((0, 1))); assert_eq!(Grid::parse_to_idx("A1"), Some((0, 1)));
assert_eq!(Grid::parse_to_idx("A10"), Some((0, 10))); assert_eq!(Grid::parse_to_idx("A10"), Some((0, 10)));
@@ -485,6 +669,7 @@ 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).trim(), "A");
assert_eq!(Grid::num_to_char(25).trim(), "Z"); assert_eq!(Grid::num_to_char(25).trim(), "Z");
@@ -505,31 +690,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.
@@ -546,7 +731,7 @@ fn fn_of_fn() {
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"); panic!("Cell not found");
@@ -582,7 +767,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.);
@@ -590,7 +775,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]
@@ -598,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);
} }
@@ -621,19 +806,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");
@@ -659,13 +844,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");
@@ -691,11 +876,11 @@ fn xlookup_function() {
grid.set_cell("A1", "Sarah".to_string()); grid.set_cell("A1", "Sarah".to_string());
grid.set_cell("C0", 31.); grid.set_cell("C0", 31.);
grid.set_cell("C1", 41.); grid.set_cell("C1", 41.);
grid.set_cell("B0", "=xlookup(A:A,\"Bobby\",C:C)".to_string()); grid.set_cell("B0", "=xlookup(\"Bobby\",A:A,C:C)".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!(res.is_ok()); assert!(res.is_ok());
assert_eq!(res.unwrap(), 31.); assert_eq!(res.unwrap(), (31.).into());
} }
#[test] #[test]
@@ -749,6 +934,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();
@@ -760,13 +958,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());
@@ -774,11 +972,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.);
@@ -787,7 +985,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());
@@ -795,13 +993,21 @@ 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]
fn recursive_ranges() { fn recursive_ranges() {
// recursive ranges causes weird behavior let mut grid = Grid::new();
// todo!();
grid.set_cell("A1", 1.);
grid.set_cell("B1", 2.);
grid.set_cell("B0", "=sum(A:A)".to_string());
grid.set_cell("A0", "=sum(B:B)".to_string());
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
let res = grid.evaluate(&cell.to_string());
assert!(res.is_err());
} }
#[test] #[test]
@@ -934,3 +1140,57 @@ fn insert_row_above_3() {
let cell = grid.get_cell("B1").as_ref().expect("Just set it"); let cell = grid.get_cell("B1").as_ref().expect("Just set it");
assert_eq!(cell.to_string(), "=A1"); assert_eq!(cell.to_string(), "=A1");
} }
#[test]
fn cell_eval_depth() {
use crate::app::mode::*;
let mut app = App::new();
app.grid.set_cell("A0", 1.);
app.grid.set_cell("A1", "=A0+$A$0".to_string());
app.grid.mv_cursor_to(0, 1);
app.mode = Mode::Chord(Chord::new('y'));
Mode::process_key(&mut app, 'y');
Mode::process_key(&mut app, 'j');
app.mode = Mode::Chord(Chord::new('5'));
Mode::process_key(&mut app, 'p');
assert_eq!(app.grid.cursor(), (0, 7));
let c = app.grid.get_cell("A6").as_ref().expect("Just set it");
assert_eq!(c.to_string(), "=A5+$A$0");
let res = app.grid.evaluate(&c.to_string()).expect("Should evaluate");
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!();
}
}
}

View File

@@ -38,9 +38,9 @@ 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}\"{CSV_DELIMITER}") format!("\"{display}\"")
} else { } else {
format!("{display}{CSV_DELIMITER}") display
} }
} }
@@ -54,19 +54,55 @@ impl CellType {
} }
} }
/// `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`
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) => {
// extract all the variables // 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 rolling = eq.clone(); let mut equation = eq.clone();
// translate standard vars A0 -> A1 // translate standard vars A0 -> A1
// extract all the variables
for old_var in ctx.dump_vars() { for old_var in ctx.dump_vars() {
let mut lock_x = false;
let mut lock_y = false;
if old_var.contains('$') {
let locations = old_var
.char_indices()
.filter(|(_, c)| *c == '$').map(|(i, _)| i)
.collect::<Vec<usize>>();
match locations.len() {
1 => {
if locations[0] == 0 {
// locking the X axis (A,B,C...)
lock_x = true;
} else {
// inside the string somewhere, gonna assume this means to lock Y (1,2,3...)
lock_y = true;
}
}
2 => {
// Ignore this variable all together, effectively lockng X & Y
continue;
}
_ => {
// There are 0 or >2 "$" in this string.
//
// Could probably optimize the code or something so you only go over the string
// once, instead of contains() then getting the indexes of where it is.
// You could then put your no-$ code here.
}
}
}
if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) { if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) {
// Use i32s instead of usize in case of negative numbers
let (x1, y1) = from; let (x1, y1) = from;
let x1 = x1 as i32; let x1 = x1 as i32;
let y1 = y1 as i32; let y1 = y1 as i32;
@@ -74,21 +110,72 @@ 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 = (src_x as i32 + (x2 - x1)) as usize; let dest_x = if lock_x {
let dest_y = (src_y as i32 + (y2 - y1)) as usize; src_x as usize
} else {
(src_x as i32 + (x2 - x1)) as usize
};
let dest_y = if lock_y {
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(); let alpha = alpha.trim();
let new_var = format!("{alpha}{dest_y}");
// Persist the "$" locking
let new_var = if lock_x {
format!("${alpha}{dest_y}")
} else if lock_y {
format!("{alpha}${dest_y}")
} else {
format!("{alpha}{dest_y}")
};
// swap out vars // swap out vars
rolling = replace_fn(&rolling, &old_var, &new_var); equation = replace_fn(&equation, &old_var, &new_var);
// 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 old_var.contains(':') {
let parts = old_var.split(':').collect::<Vec<&str>>();
// This means the var was formatted as X:X
if parts.len() == 2 {
// how far is the movement?
let dx = to.0 as i32 - from.0 as i32;
let range_start = parts[0];
let range_end = parts[1];
// get the letters as numbers
let xs = Grid::char_to_idx(range_start) as i32;
let xe = Grid::char_to_idx(range_end) 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.trim(), end.trim()));
} }
} }
return rolling.into(); }
}
return equation.into();
} }
} }
} }
@@ -108,3 +195,15 @@ 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,
}
}
}

View File

@@ -6,7 +6,8 @@ use crate::app::logic::{calc::Grid, cell::CellType};
pub struct CallbackContext<'a> { pub struct CallbackContext<'a> {
variables: &'a Grid, variables: &'a Grid,
eval_depth: RwLock<usize>, eval_breadcrumbs: RwLock<Vec<String>>,
compute_cache: RwLock<HashMap<String, Value>>,
functions: HashMap<String, Function<DefaultNumericTypes>>, functions: HashMap<String, Function<DefaultNumericTypes>>,
/// True if builtin functions are disabled. /// True if builtin functions are disabled.
@@ -23,7 +24,7 @@ impl<'a> CallbackContext<'a> {
let as_index = |s: &str| { let as_index = |s: &str| {
s.char_indices() s.char_indices()
// .filter(|f| f.1 as u8 >= 97) // prevent sub with overflow errors // .filter(|f| f.1 as u8 >= 97) // prevent sub with overflow errors
.map(|(idx, c)| (c.to_ascii_lowercase() as usize - 97) + (26 * idx)) .map(|(idx, c)| ((c.to_ascii_lowercase() as usize).saturating_sub(97)) + (26 * idx))
.fold(0, |a, b| a + b) .fold(0, |a, b| a + b)
}; };
@@ -33,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);
} }
} }
@@ -103,13 +104,13 @@ impl<'a> CallbackContext<'a> {
functions.insert( functions.insert(
"xlookup".to_string(), "xlookup".to_string(),
Function::new(|arg| { Function::new(|arg| {
let expected = vec![ValueType::Tuple, ValueType::String, ValueType::Tuple]; let expected = vec![ValueType::String, ValueType::Tuple, ValueType::Tuple];
if arg.is_tuple() { if arg.is_tuple() {
let args = arg.as_tuple()?; let args = arg.as_tuple()?;
if args.len() == 3 { if args.len() == 3 {
let lookup_array = &args[0]; let lookup_value = &args[0];
let lookup_value = &args[1]; let lookup_array = &args[1];
let return_array = &args[2]; let return_array = &args[2];
if lookup_array.is_tuple() && return_array.is_tuple() { if lookup_array.is_tuple() && return_array.is_tuple() {
@@ -139,10 +140,11 @@ impl<'a> CallbackContext<'a> {
pub fn new(grid: &'a Grid) -> Self { pub fn new(grid: &'a Grid) -> Self {
Self { Self {
eval_depth: RwLock::new(0),
variables: grid, variables: grid,
functions: Self::get_functions(), functions: Self::get_functions(),
without_builtin_functions: false, without_builtin_functions: false,
eval_breadcrumbs: RwLock::new(Vec::new()),
compute_cache: RwLock::new(HashMap::new()),
} }
} }
} }
@@ -151,30 +153,49 @@ impl<'a> Context for CallbackContext<'a> {
type NumericTypes = DefaultNumericTypes; type NumericTypes = DefaultNumericTypes;
fn get_value(&self, identifier: &str) -> Option<Value<Self::NumericTypes>> { fn get_value(&self, identifier: &str) -> Option<Value<Self::NumericTypes>> {
const RECURSION_DEPTH_LIMIT: usize = 20;
// check cache
if let Ok(cc) = self.compute_cache.read() {
if let Some(hit) = cc.get(identifier) {
return Some(hit.clone());
}
}
if let Ok(mut trail) = self.eval_breadcrumbs.write() {
let find = trail.iter().filter(|id| *id == identifier).count();
if find > 0 {
// recursion detected
return None;
} else {
trail.push(identifier.to_owned(), );
}
}
let pre_return = |v: Value| {
if let Ok(mut cc) = self.compute_cache.write() {
cc.insert(identifier.to_owned(), v.clone());
}
Some(v)
};
if let Some(v) = self.variables.get_cell(identifier) { if let Some(v) = self.variables.get_cell(identifier) {
match v { match v {
CellType::Number(n) => return Some(Value::Float(n.to_owned())), CellType::Number(n) => {
CellType::String(s) => return Some(Value::String(s.to_owned())), return pre_return(Value::Float(n.to_owned()));
},
CellType::String(s) => {
return pre_return(Value::String(s.to_owned()));
},
CellType::Equation(eq) => { CellType::Equation(eq) => {
if let Ok(mut depth) = self.eval_depth.write() {
*depth += 1;
if *depth > RECURSION_DEPTH_LIMIT {
return None;
}
} else {
// It would be unsafe to continue to process without knowing how
// deep we've gone.
return None;
}
// remove the equals sign from the beginning, as that // remove the equals sign from the beginning, as that
// tries to set variables with our evaluation lib // tries to set variables with our evaluation lib
match eval_with_context(&eq[1..], self) { match eval_with_context(&eq[1..], self) {
Ok(e) => return Some(e), Ok(e) => {
return pre_return(e)
},
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.
@@ -196,16 +217,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(mut depth) = self.eval_depth.write() { match eval_with_context(&eq[1..], self) {
*depth += 1; Ok(val) => vals.push(val),
if *depth > RECURSION_DEPTH_LIMIT { Err(_err) => {
return None; // At this point we are getting an error because
} // recursion protection made this equation return
} else { // None. We now don't get any evaluation.
return None; return None
} },
if let Ok(val) = eval_with_context(&eq[1..], self) {
vals.push(val);
} }
} }
} }
@@ -239,8 +258,6 @@ impl<'a> Context for CallbackContext<'a> {
} }
} }
/// DOES NOT EVALUATE EQUATIONS!! /// DOES NOT EVALUATE EQUATIONS!!
/// ///
/// This is used as a pseudo-context, just used for /// This is used as a pseudo-context, just used for
@@ -265,6 +282,7 @@ impl ExtractionContext {
Vec::new() 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() { if let Ok(r) = self.fn_registry.read() {
r.clone() r.clone()
@@ -280,7 +298,9 @@ impl Context for ExtractionContext {
fn get_value(&self, identifier: &str) -> Option<Value<Self::NumericTypes>> { fn get_value(&self, identifier: &str) -> Option<Value<Self::NumericTypes>> {
if let Ok(mut registry) = self.var_registry.write() { if let Ok(mut registry) = self.var_registry.write() {
registry.push(identifier.to_owned()); registry.push(identifier.to_owned());
} else { panic!("The RwLock should always be write-able") } } else {
panic!("The RwLock should always be write-able")
}
Some(Value::Int(1)) Some(Value::Int(1))
} }
@@ -293,9 +313,10 @@ impl Context for ExtractionContext {
let _ = argument; let _ = argument;
if let Ok(mut registry) = self.fn_registry.write() { if let Ok(mut registry) = self.fn_registry.write() {
registry.push(identifier.to_owned()) registry.push(identifier.to_owned())
} else { panic!("The RwLock should always be write-able") } } else {
// Ok(Value::Int(1)) panic!("The RwLock should always be write-able")
todo!(); }
Ok(Value::Int(1))
} }
fn are_builtin_functions_disabled(&self) -> bool { fn are_builtin_functions_disabled(&self) -> bool {

View File

@@ -1,4 +1,10 @@
use std::{cmp::min, fmt::Display, path::PathBuf}; use std::{
cmp::{max, min},
fmt::Display,
fs,
path::PathBuf,
process::Command,
};
use ratatui::{ use ratatui::{
prelude, prelude,
@@ -9,7 +15,10 @@ use ratatui::{
use crate::app::{ use crate::app::{
app::App, app::App,
error_msg::StatusMessage, error_msg::StatusMessage,
logic::calc::LEN, logic::{
calc::{Grid, LEN},
cell::CellType,
},
}; };
pub enum Mode { pub enum Mode {
@@ -18,6 +27,7 @@ pub enum Mode {
Normal, Normal,
Command(Chord), Command(Chord),
Visual((usize, usize)), Visual((usize, usize)),
VisualCmd((usize, usize), Chord),
} }
impl Display for Mode { impl Display for Mode {
@@ -28,6 +38,7 @@ impl Display for Mode {
Mode::Chord(_) => write!(f, "CHORD"), Mode::Chord(_) => write!(f, "CHORD"),
Mode::Command(_) => write!(f, "COMMAND"), Mode::Command(_) => write!(f, "COMMAND"),
Mode::Visual(_) => write!(f, "VISUAL"), Mode::Visual(_) => write!(f, "VISUAL"),
Mode::VisualCmd(_, _) => write!(f, "V-CMD"),
} }
} }
} }
@@ -38,6 +49,7 @@ impl Mode {
// Where you are typing // Where you are typing
Mode::Insert(_) => Style::new().fg(Color::White).bg(Color::Blue), Mode::Insert(_) => Style::new().fg(Color::White).bg(Color::Blue),
Mode::Command(_) => Style::new().fg(Color::Black).bg(Color::Magenta), Mode::Command(_) => Style::new().fg(Color::Black).bg(Color::Magenta),
Mode::VisualCmd(_, _) => Style::new().fg(Color::Black).bg(Color::Yellow),
Mode::Chord(_) => Style::new().fg(Color::Black).bg(Color::LightBlue), Mode::Chord(_) => Style::new().fg(Color::Black).bg(Color::LightBlue),
// Movement-based modes // Movement-based modes
Mode::Visual(_) => Style::new().fg(Color::Yellow), Mode::Visual(_) => Style::new().fg(Color::Yellow),
@@ -59,13 +71,21 @@ impl Mode {
"w" => { "w" => {
// first try the passed argument as file // first try the passed argument as file
if let Some(arg) = args.get(1) { if let Some(arg) = args.get(1) {
if let Err(e) = app.grid.save_to(arg) { let path: PathBuf = arg.into();
// TODO Check if the file we are writing to exists, since
// this code path already knows that we are writing to a new file.
// We might be accidentally overwriting something else.
if let Err(e) = app.grid.save_to(&path) {
app.msg = StatusMessage::error(format!("{e}")); app.msg = StatusMessage::error(format!("{e}"));
} else { } else {
// file saving was a success, adopt the provided file // file saving was a success, adopt the provided file
// if we don't already have one (this is how vim works) // if we don't already have one (this is how vim works)
let path: PathBuf = arg.into(); app.msg = StatusMessage::info(format!(
app.msg = StatusMessage::info(format!("Saved file {}", path.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a"))); "Saved file {}",
path.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a")
));
if let None = app.file { if let None = app.file {
app.file = Some(path) app.file = Some(path)
@@ -76,7 +96,10 @@ impl Mode {
if let Err(e) = app.grid.save_to(file) { if let Err(e) = app.grid.save_to(file) {
app.msg = StatusMessage::error(format!("{e}")); app.msg = StatusMessage::error(format!("{e}"));
} else { } else {
app.msg = StatusMessage::info(format!("Saved file {}", file.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a"))); app.msg = StatusMessage::info(format!(
"Saved file {}",
file.file_name().map(|f| f.to_str().unwrap_or("n/a")).unwrap_or("n/a")
));
} }
// you need to provide a file from *somewhere* // you need to provide a file from *somewhere*
} else { } else {
@@ -113,6 +136,105 @@ impl Mode {
_ => {} _ => {}
} }
} }
if let Mode::VisualCmd(pos, editor) = &mut app.mode {
let cmd = &editor.as_string()[1..];
let args = cmd.split_ascii_whitespace().collect::<Vec<&str>>();
if args.is_empty() {
return;
}
// These values are going to be used in probably all
// the commands related to ranges, we will just write
// logic here first, once.
let (x1, y1) = pos;
let (x1, y1) = (*x1, *y1);
let (x2, y2) = app.grid.cursor();
let (low_x, hi_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
let (low_y, hi_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) };
let mut save_range = |to: &str| {
let mut g = Grid::new();
g.transact_on_grid(|grid| {
for (i, x) in (low_x..=hi_x).enumerate() {
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) {
app.msg = StatusMessage::error("Failed to save file");
}
};
let get_project_name = || {
if let Some(file) = &app.file {
if let Some(name) = file.file_name() {
if let Some(name) = name.to_str() {
return name;
}
}
}
return "unknown";
};
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" => {
if let Some(arg1) = args.get(1) {
save_range(&arg1);
} else {
app.msg = StatusMessage::error("export <path.csv>")
}
app.mode = Mode::Normal
}
"plot" => {
// Use gnuplot to plot the selected data.
// * Temp data will be stored in /tmp/
// * Output will either be plot.png or a name that you pass in
let output_filename = if let Some(arg1) = args.get(1) { arg1 } else { "plot.png" };
save_range("/tmp/plot.csv");
let plot = include_str!("../../template.gnuplot");
let s = plot.replace("$FILE", "/tmp/plot.csv");
let s = s.replace("$TITLE", get_project_name());
let s = s.replace("$XLABEL", "hard-coded x");
let s = s.replace("$YLABEL", "hard-coded y");
let s = s.replace("$OUTPUT", "/tmp/output.png");
let _ = fs::write("/tmp/plot.p", s);
let cmd_res = Command::new("gnuplot").arg("/tmp/plot.p").output();
if let Err(err) = cmd_res {
match err.kind() {
std::io::ErrorKind::NotFound => {
app.msg = StatusMessage::error("Error - Is gnuplot installed?")
}
_ => app.msg = StatusMessage::error(format!("{err}")),
};
} else {
let _ = fs::copy("/tmp/output.png", output_filename);
app.msg = StatusMessage::info(format!("Created {output_filename}. Artifacts are in /tmp"));
}
app.mode = Mode::Normal
}
_ => {}
}
}
} }
pub fn process_key(app: &mut App, key: char) { pub fn process_key(app: &mut App, key: char) {
@@ -148,6 +270,18 @@ impl Mode {
app.grid.mv_cursor_to(0, y); app.grid.mv_cursor_to(0, y);
return; return;
} }
// Go to end of row
'$' => {
let (_, y) = app.grid.cursor();
app.grid.mv_cursor_to(super::logic::calc::LEN, y);
return;
}
// Go to bottom of column
'G' => {
let (x, _) = app.grid.cursor();
app.grid.mv_cursor_to(x, super::logic::calc::LEN);
return;
}
// edit cell // edit cell
'i' | 'a' => { 'i' | 'a' => {
let (x, y) = app.grid.cursor(); let (x, y) = app.grid.cursor();
@@ -181,7 +315,18 @@ impl Mode {
app.grid.insert_row_above(app.grid.cursor()); app.grid.insert_row_above(app.grid.cursor());
} }
'v' => app.mode = Mode::Visual(app.grid.cursor()), 'v' => app.mode = Mode::Visual(app.grid.cursor()),
':' => app.mode = Mode::Command(Chord::new(':')), ':' => {
if let Self::Visual(pos) = app.mode {
app.mode = Mode::VisualCmd(pos, Chord::new(':'));
} else {
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());
@@ -255,6 +400,34 @@ impl Mode {
app.grid.mv_cursor_to(x, 0); app.grid.mv_cursor_to(x, 0);
app.mode = Mode::Normal; app.mode = Mode::Normal;
} }
// Go to the bottom of the current window
("g", 'G') => {
let (x, _) = app.grid.cursor();
let (_, y_height) = app.screen.get_screen_size(&app.vars);
let y_origin = app.screen.scroll_y();
app.grid.mv_cursor_to(x, y_origin + y_height);
app.mode = Mode::Normal;
return;
}
// Go to the right edge of the current window
("g", '$') => {
let (_, y) = app.grid.cursor();
let (x_width, _) = app.screen.get_screen_size(&app.vars);
let x_origin = app.screen.scroll_x();
app.grid.mv_cursor_to(x_origin + x_width, y);
app.mode = Mode::Normal;
}
// Go to the left edge of the current window
("g", '0') => {
let (_, y) = app.grid.cursor();
let x_origin = app.screen.scroll_x();
app.grid.mv_cursor_to(x_origin, y);
app.mode = Mode::Normal;
return;
}
// center screen to cursor // center screen to cursor
("z", 'z') => { ("z", 'z') => {
app.screen.center_x(app.grid.cursor(), &app.vars); app.screen.center_x(app.grid.cursor(), &app.vars);
@@ -293,9 +466,45 @@ impl Mode {
} }
} }
} }
// IDK why it works but it does. Keystrokes are process somewhere else? // Keys are process in the handle_event method in App for these
Mode::Insert(_chord) => {}, Mode::Insert(_chord) => {}
Mode::Command(_chord) => {}, Mode::Command(_chord) => {}
Mode::VisualCmd(_pos, _chord) => {}
}
}
pub fn chars_to_display(&self, cell: &Option<CellType>) -> u16 {
let len = match &self {
Mode::Insert(edit) | Mode::VisualCmd(_, edit) | Mode::Command(edit) | Mode::Chord(edit) => edit.len(),
Mode::Normal => {
let len = cell.as_ref().map(|f| f.to_string().len()).unwrap_or_default();
len
}
Mode::Visual(_) => 0,
};
// min 20 chars, expand if needed
let len = max(len as u16 + 1, 20);
len
}
pub fn render(&self, f: &mut ratatui::Frame, area: prelude::Rect, cell: &Option<CellType>) {
match &self {
Mode::Insert(editor) => {
f.render_widget(editor, area);
}
Mode::Command(editor) => {
f.render_widget(editor, area);
}
Mode::Chord(chord) => f.render_widget(chord, area),
Mode::Normal => f.render_widget(
Paragraph::new({
let cell = cell.as_ref().map(|f| f.to_string()).unwrap_or_default();
cell
}),
area,
),
Mode::Visual(_) => {}
Mode::VisualCmd(_, editor) => f.render_widget(editor, area),
} }
} }
} }
@@ -307,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,
}
} }
} }
@@ -318,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) {

View File

@@ -5,9 +5,12 @@ use ratatui::prelude;
use crate::app::logic::calc::LEN; use crate::app::logic::calc::LEN;
pub struct ScreenSpace { pub struct ScreenSpace {
/// This is measured in cells /// This is measured in cells.
/// This is the top-left cell index.
scroll: (usize, usize), scroll: (usize, usize),
/// In chars
default_cell_len: usize, default_cell_len: usize,
/// In chars
default_cell_hight: usize, default_cell_hight: usize,
/// This is measured in chars /// This is measured in chars
last_seen_screen_size: RwLock<(usize, usize)> last_seen_screen_size: RwLock<(usize, usize)>
@@ -17,37 +20,49 @@ impl ScreenSpace {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
scroll: (0, 0), scroll: (0, 0),
default_cell_len: 10, default_cell_len: 9,
default_cell_hight: 1, default_cell_hight: 1,
last_seen_screen_size: RwLock::new((0,0)) last_seen_screen_size: RwLock::new((0,0))
} }
} }
pub fn center_x(&mut self, (cursor_x, _): (usize, usize), vars: &HashMap<String, String>) { pub fn center_x(&mut self, (cursor_x, _): (usize, usize), vars: &HashMap<String, String>) {
if let Ok(screen_size) = self.last_seen_screen_size.read() { let (x_cells, _) = self.get_screen_size(vars);
let x_cells = (screen_size.0 / self.get_cell_width(vars) as usize) -2;
let x_center = self.scroll_x() + (x_cells/2); let x_center = self.scroll_x() + (x_cells/2);
let delta = cursor_x as isize - x_center as isize; let delta = cursor_x as isize - x_center as isize;
self.scroll.0 = self.scroll.0.saturating_add_signed(delta); self.scroll.0 = self.scroll.0.saturating_add_signed(delta);
} }
}
pub fn center_y(&mut self, (_, cursor_y): (usize, usize), vars: &HashMap<String, String>) { pub fn center_y(&mut self, (_, cursor_y): (usize, usize), vars: &HashMap<String, String>) {
if let Ok(screen_size) = self.last_seen_screen_size.read() { let (_, y_cells) = self.get_screen_size(vars);
let y_cells = (screen_size.1 / self.get_cell_height(vars) as usize) -2;
let y_center = self.scroll_y() + (y_cells/2); let y_center = self.scroll_y() + (y_cells/2);
let delta = cursor_y as isize - y_center as isize; let delta = cursor_y as isize - y_center as isize;
self.scroll.1 = self.scroll.1.saturating_add_signed(delta); self.scroll.1 = self.scroll.1.saturating_add_signed(delta);
} }
}
pub fn scroll_based_on_cursor_location(&mut self, (cursor_x, cursor_y): (usize, usize), vars: &HashMap<String, String>) { /// In chars
pub fn get_screen_size(&self, vars: &HashMap<String, String>) -> (usize, usize) {
if let Ok(screen_size) = self.last_seen_screen_size.read() { if let Ok(screen_size) = self.last_seen_screen_size.read() {
// ======= 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) as usize) -2;
// ======= Y =======
// screen seems to be 2 cells smaller than it should be
// this is probably related to issue #6
let y_cells = (screen_size.1 / self.get_cell_height(vars) as usize) -2;
(x_cells,y_cells)
} else {
(0,0)
}
}
pub fn scroll_based_on_cursor_location(&mut self, (cursor_x, cursor_y): (usize, usize), vars: &HashMap<String, String>) {
let (x_cells, y_cells) = self.get_screen_size(vars);
let lower_x = self.scroll_x(); let lower_x = self.scroll_x();
let upper_x = self.scroll_x() + x_cells; let upper_x = self.scroll_x() + x_cells;
@@ -60,10 +75,7 @@ impl ScreenSpace {
self.scroll.0 = self.scroll.0.saturating_add(delta); self.scroll.0 = self.scroll.0.saturating_add(delta);
} }
// ======= Y =======
// screen seems to be 2 cells smaller than it should be
// this is probably related to issue #6
let y_cells = (screen_size.1 / self.get_cell_height(vars) as usize) -2;
let lower_y = self.scroll_y(); let lower_y = self.scroll_y();
let upper_y = self.scroll_y() + y_cells; let upper_y = self.scroll_y() + y_cells;
@@ -77,14 +89,15 @@ impl ScreenSpace {
self.scroll.1 = self.scroll.1.saturating_add(delta); self.scroll.1 = self.scroll.1.saturating_add(delta);
} }
} }
}
pub fn scroll_x(&self) -> usize { pub fn scroll_x(&self) -> usize {
self.scroll.0 self.scroll.0
} }
pub fn scroll_y(&self) -> usize{ pub fn scroll_y(&self) -> usize{
self.scroll.1 self.scroll.1
} }
pub fn get_cell_height(&self, vars: &HashMap<String, String>) -> usize { pub fn get_cell_height(&self, vars: &HashMap<String, String>) -> usize {
if let Some(h) = vars.get("height") { if let Some(h) = vars.get("height") {
if let Ok(p) = h.parse::<usize>() { if let Ok(p) = h.parse::<usize>() {
@@ -93,6 +106,7 @@ impl ScreenSpace {
} }
return self.default_cell_hight return self.default_cell_hight
} }
pub fn get_cell_width(&self, vars: &HashMap<String, String>) -> usize { pub fn get_cell_width(&self, vars: &HashMap<String, String>) -> usize {
if let Some(h) = vars.get("length") { if let Some(h) = vars.get("length") {
if let Ok(p) = h.parse::<usize>() { if let Ok(p) = h.parse::<usize>() {
@@ -100,19 +114,34 @@ impl ScreenSpace {
} }
} }
self.default_cell_len self.default_cell_len
} }
pub fn how_many_cells_fit_in(&self, area: &prelude::Rect, vars: &HashMap<String, String>) -> (u16, u16) { pub fn how_many_cells_fit_in(&self, area: &prelude::Rect, vars: &HashMap<String, String>) -> (u16, u16) {
if let Ok(mut l) = self.last_seen_screen_size.write() { if let Ok(mut l) = self.last_seen_screen_size.write() {
l.0 = area.width as usize; l.0 = area.width as usize;
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);
let height = area.height as usize / self.get_cell_height(vars);
let x_max = let x_max =
if area.width as usize / self.get_cell_width(vars) > LEN { LEN - 1 } else { area.width as usize / self.get_cell_width(vars)}; if width > LEN {
LEN - 1
} else {
width
};
let y_max = let y_max =
if area.height as usize / self.get_cell_height(vars) > LEN { LEN - 1 } else { area.height as usize / self.get_cell_height(vars)}; if height > LEN {
LEN - 1
} else {
height
};
(x_max as u16, y_max as u16) (x_max as u16, y_max as u16)
} }
} }
#[test] #[test]

18
template.gnuplot Normal file
View File

@@ -0,0 +1,18 @@
datafile = '$FILE'
set datafile separator ','
set title '$TITLE'
set key autotitle columnhead
set xlabel "$XLABEL"
set ylabel "$YLABEL"
set style line 1 linewidth 2 linecolor 1 pointtype 7 pointsize 1.5
set autoscale
set grid
set term png size 1280, 720
set output '$OUTPUT'
plot datafile using 1:2 with linespoints linestyle 1
# plot datafile using 1:2 with linespoints linestyle 1, datafile using 1:3, etc...
replot