Compare commits

...

11 Commits

14 changed files with 458 additions and 50 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target
result/
*.json
tarpaulin-report.*

23
Makefile Normal file
View File

@ -0,0 +1,23 @@
rwildcard=$(foreach d,$(wildcard $(1:=/*)),$(call rwildcard,$d,$2) $(filter $(subst *,%,$2),$d))
rust_files=$(call rwildcard,src,*.rs)
test: $(rust-files)
cargo test
build: $(rust-files)
cargo build
tarpaulin-report.%: $(rust_files)
cargo tarpaulin --skip-clean --test --out $*
cover: tarpaulin-report.html
view-cover: tarpaulin-report.html
open $<
clean-tarpaulin:
rm -f tarpaulin-report.*
clean: clean-tarpaulin
cargo clean

View File

@ -44,6 +44,8 @@ table and between the sheets using the following keybinds:
* `l` and, ➡️ will move one cell to the right.
* `j`, ⬇️, and `Enter` will move one cell down.
* `k` ⬆️, will move one cell up.
* `d` will delete the contents of the selected cell leaving style untouched
* `D` will delete the contents of the selected cell including any style
**Sheet Navigation**
@ -66,8 +68,12 @@ will clear the numeric prefix if you want to cancel it.
**Other Keybindings**
* `Ctrl-r` will enter range selection mode
* `Ctrl-r` will enter range selection mode.
* `v` will enter range selection mode with the start of the range already selected.
* `Ctrl-s` will save the sheet.
* `Ctrl-c`, `y` Copy the cell or range contents.
* `Ctrl-v`, `p` Paste into the sheet.
* `Ctrl-Shift-C` Copy the cell or range formatted content.
* `q` will exit the application.
* `:` will enter CommandMode.
@ -129,12 +135,16 @@ will be discarded if you have not saved first.</aside>
### Range Select Mode
Range Select mode copies a range reference for use later. You can enter range
Range Select mode copies a range reference for use later or delete a range's contents. You can enter range
select mode from CellEdit mode with `CTRL-r`.
* `h`, `j`, `k`, `l` will navigate around the sheet.
* `Ctrl-n`, `Ctrl-p` will navigate between sheets.
* `Ctrl-c`, `y` Copy the cell or range contents.
* `Ctrl-Shift-C`, 'Y' Copy the cell or range formatted content.
* `The spacebar will select the start and end of the range respectively.
* `d` will delete the contents of the range leaving any style untouched
* `D` will delete the contents of the range including any style
When you have selected the end of the range you will exit range select mode and
the range reference will be placed into the cell contents you are editing.

Binary file not shown.

Binary file not shown.

View File

@ -37,7 +37,7 @@
rust-bin = pkgs.rust-bin;
devShells.default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ gnumake my-rust-bin rust-analyzer ];
nativeBuildInputs = with pkgs; [ gnumake my-rust-bin rust-analyzer cargo-tarpaulin ];
};
});
}

View File

@ -18,6 +18,64 @@ mod test;
const COL_PIXELS: f64 = 5.0;
#[derive(Debug, Clone)]
pub struct AddressRange<'book> {
pub start: &'book Address,
pub end: &'book Address,
}
impl<'book> AddressRange<'book> {
pub fn as_rows(&self) -> Vec<Vec<Address>> {
let (row_range, col_range) = self.get_ranges();
let mut rows = Vec::with_capacity(row_range.len());
for ri in row_range.iter() {
let mut row = Vec::with_capacity(col_range.len());
for ci in col_range.iter() {
row.push(Address { row: *ri, col: *ci });
}
rows.push(row);
}
rows
}
pub fn as_series(&self) -> Vec<Address> {
let (row_range, col_range) = self.get_ranges();
let mut rows = Vec::with_capacity(row_range.len() * col_range.len());
for ri in row_range.iter() {
for ci in col_range.iter() {
rows.push(Address { row: *ri, col: *ci });
}
}
rows
}
fn get_ranges(&self) -> (Vec<usize>, Vec<usize>) {
let row_range = if self.start.row <= self.end.row {
(self.start.row..=self.end.row)
.into_iter()
.collect::<Vec<usize>>()
} else {
let mut v = (self.start.row..=self.end.row)
.into_iter()
.collect::<Vec<usize>>();
v.reverse();
v
};
let col_range = if self.start.col <= self.end.col {
(self.start.col..=self.end.col)
.into_iter()
.collect::<Vec<usize>>()
} else {
let mut v = (self.start.col..=self.end.col)
.into_iter()
.collect::<Vec<usize>>();
v.reverse();
v
};
(row_range, col_range)
}
}
/// A spreadsheet book with some internal state tracking.
pub struct Book {
pub(crate) model: Model,
@ -96,7 +154,7 @@ impl Book {
Ok(&self.get_sheet()?.sheet_data)
}
/// Move to a specific sheel location in the current sheet
/// Move to a specific sheet location in the current sheet
pub fn move_to(&mut self, Address { row, col }: &Address) -> Result<()> {
// FIXME(zaphar): Check that this is safe first.
self.location.row = *row;
@ -104,6 +162,79 @@ impl Book {
Ok(())
}
/// Extend a cell to the rest of the range.
pub fn extend_to(&mut self, from: &Address, to: &Address) -> Result<()> {
for cell in (AddressRange {
start: from,
end: to,
})
.as_series()
.iter()
.skip(1)
{
let contents = self
.model
.extend_to(
self.current_sheet,
from.row as i32,
from.col as i32,
cell.row as i32,
cell.col as i32,
)
.map_err(|e| anyhow!(e))?;
self.model
.set_user_input(
self.current_sheet,
cell.row as i32,
cell.col as i32,
contents,
)
.map_err(|e| anyhow!(e))?;
}
self.evaluate();
Ok(())
}
pub fn clear_current_cell(&mut self) -> Result<()> {
self.clear_cell_contents(self.current_sheet as u32, self.location.clone())
}
pub fn clear_current_cell_all(&mut self) -> Result<()> {
self.clear_cell_all(self.current_sheet as u32, self.location.clone())
}
pub fn clear_cell_contents(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> {
Ok(self
.model
.cell_clear_contents(sheet, row as i32, col as i32)
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?)
}
pub fn clear_cell_range(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> {
for row in start.row..=end.row {
for col in start.col..=end.col {
self.clear_cell_contents(sheet, Address { row, col })?;
}
}
Ok(())
}
pub fn clear_cell_all(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> {
Ok(self
.model
.cell_clear_all(sheet, row as i32, col as i32)
.map_err(|s| anyhow!("Unable to clear cell contents {}", s))?)
}
pub fn clear_cell_range_all(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> {
for row in start.row..=end.row {
for col in start.col..=end.col {
self.clear_cell_all(sheet, Address { row, col })?;
}
}
Ok(())
}
/// Get a cells formatted content.
pub fn get_current_cell_rendered(&self) -> Result<String> {
Ok(self.get_cell_addr_rendered(&self.location)?)
@ -117,6 +248,14 @@ impl Book {
.map_err(|s| anyhow!("Unable to format cell {}", s))?)
}
/// Get a cells actual content unformatted as a string.
pub fn get_cell_addr_contents(&self, Address { row, col }: &Address) -> Result<String> {
Ok(self
.model
.get_cell_content(self.current_sheet, *row as i32, *col as i32)
.map_err(|s| anyhow!("Unable to format cell {}", s))?)
}
/// Get a cells actual content as a string.
pub fn get_current_cell_contents(&self) -> Result<String> {
Ok(self
@ -132,13 +271,13 @@ impl Book {
/// Update the current cell in a book.
/// This update won't be reflected until you call `Book::evaluate`.
pub fn edit_current_cell<S: Into<String>>(&mut self, value: S) -> Result<()> {
self.update_entry(&self.location.clone(), value)?;
self.update_cell(&self.location.clone(), value)?;
Ok(())
}
/// Update an entry in the current sheet for a book.
/// This update won't be reflected until you call `Book::evaluate`.
pub fn update_entry<S: Into<String>>(&mut self, location: &Address, value: S) -> Result<()> {
pub fn update_cell<S: Into<String>>(&mut self, location: &Address, value: S) -> Result<()> {
self.model
.set_user_input(
self.current_sheet,
@ -223,7 +362,7 @@ impl Book {
.enumerate()
.find(|(_idx, sheet)| sheet.name == name)
{
self.current_sheet =idx as u32;
self.current_sheet = idx as u32;
return true;
}
false
@ -242,7 +381,7 @@ impl Book {
}
self.current_sheet = next;
}
pub fn select_prev_sheet(&mut self) {
let len = self.model.workbook.worksheets.len() as u32;
let next = if self.current_sheet == 0 {
@ -253,7 +392,6 @@ impl Book {
self.current_sheet = next;
}
/// Select a sheet by id.
pub fn select_sheet_by_id(&mut self, id: u32) -> bool {
if let Some((idx, _sheet)) = self
@ -286,13 +424,14 @@ impl Book {
.worksheet_mut(self.current_sheet)
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?)
}
pub(crate) fn get_sheet_name_by_idx(&self, idx: usize) -> Result<&str> {
Ok(&self
.model
.workbook
.worksheet(idx as u32)
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?.name)
.map_err(|s| anyhow!("Invalid Worksheet: {}", s))?
.name)
}
pub(crate) fn get_sheet_by_idx_mut(&mut self, idx: usize) -> Result<&mut Worksheet> {
Ok(self
@ -307,7 +446,7 @@ impl Default for Book {
fn default() -> Self {
let mut book =
Book::new(Model::new_empty("default_name", "en", "America/New_York").unwrap());
book.update_entry(&Address { row: 1, col: 1 }, "").unwrap();
book.update_cell(&Address { row: 1, col: 1 }, "").unwrap();
book
}
}

View File

@ -36,7 +36,7 @@ fn test_book_default() {
#[test]
fn test_book_insert_cell_new_row() {
let mut book = Book::default();
book.update_entry(&Address { row: 2, col: 1 }, "1")
book.update_cell(&Address { row: 2, col: 1 }, "1")
.expect("failed to edit cell");
book.evaluate();
let WorksheetDimension {
@ -52,7 +52,7 @@ fn test_book_insert_cell_new_row() {
#[test]
fn test_book_insert_cell_new_column() {
let mut book = Book::default();
book.update_entry(&Address { row: 1, col: 2 }, "1")
book.update_cell(&Address { row: 1, col: 2 }, "1")
.expect("failed to edit cell");
let WorksheetDimension {
min_row,
@ -67,7 +67,7 @@ fn test_book_insert_cell_new_column() {
#[test]
fn test_book_insert_rows() {
let mut book = Book::default();
book.update_entry(&Address { row: 2, col: 2 }, "1")
book.update_cell(&Address { row: 2, col: 2 }, "1")
.expect("failed to edit cell");
book.move_to(&Address { row: 2, col: 2 })
.expect("Failed to move to location");
@ -85,7 +85,7 @@ fn test_book_insert_rows() {
#[test]
fn test_book_insert_columns() {
let mut book = Book::default();
book.update_entry(&Address { row: 2, col: 2 }, "1")
book.update_cell(&Address { row: 2, col: 2 }, "1")
.expect("failed to edit cell");
book.move_to(&Address { row: 2, col: 2 })
.expect("Failed to move to location");
@ -103,7 +103,7 @@ fn test_book_insert_columns() {
#[test]
fn test_book_col_size() {
let mut book = Book::default();
book.update_entry(&Address { row: 2, col: 2 }, "1")
book.update_cell(&Address { row: 2, col: 2 }, "1")
.expect("failed to edit cell");
book.set_col_size(1, 20).expect("Failed to set column size");
assert_eq!(20, book.get_col_size(1).expect("Failed to get column size"));

View File

@ -5,7 +5,7 @@ use slice_utils::{Measured, Peekable, Seekable, Span, StrCursor};
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
Write(Option<&'a str>),
InsertRow(usize),
InsertRows(usize),
InsertColumns(usize),
RenameSheet(Option<usize>, &'a str),
NewSheet(Option<&'a str>),
@ -155,7 +155,7 @@ fn try_consume_insert_row<'cmd, 'i: 'cmd>(
return Err("Invalid command: Did you mean to type `insert-rows <arg>`?");
}
let arg = input.span(0..).trim();
return Ok(Some(Cmd::InsertRow(if arg.is_empty() {
return Ok(Some(Cmd::InsertRows(if arg.is_empty() {
1
} else {
if let Ok(count) = arg.parse() {

View File

@ -1,7 +1,7 @@
//! Ui rendering logic
use std::{path::PathBuf, process::ExitCode};
use crate::book::Book;
use crate::book::{AddressRange, Book};
use anyhow::{anyhow, Result};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
@ -66,15 +66,23 @@ impl RangeSelection {
}
}
#[derive(Debug)]
pub enum ClipboardContents {
Cell(String),
Range(Vec<Vec<String>>),
}
#[derive(Debug)]
pub struct AppState<'ws> {
pub modality_stack: Vec<Modality>,
pub viewport_state: ViewportState,
pub command_state: TextState<'ws>,
pub numeric_prefix: Vec<char>,
pub char_queue: Vec<char>,
pub range_select: RangeSelection,
dirty: bool,
popup: Vec<String>,
clipboard: Option<ClipboardContents>,
}
impl<'ws> Default for AppState<'ws> {
@ -84,9 +92,11 @@ impl<'ws> Default for AppState<'ws> {
viewport_state: Default::default(),
command_state: Default::default(),
numeric_prefix: Default::default(),
char_queue: Default::default(),
range_select: Default::default(),
dirty: Default::default(),
popup: Default::default(),
clipboard: Default::default(),
}
}
}
@ -201,7 +211,11 @@ impl<'ws> Workspace<'ws> {
pub fn selected_range_to_string(&self) -> String {
let state = &self.state;
if let Some((start, end)) = state.range_select.get_range() {
let a1 = format!("{}{}", start.to_range_part(), format!(":{}", end.to_range_part()));
let a1 = format!(
"{}{}",
start.to_range_part(),
format!(":{}", end.to_range_part())
);
if let Some(range_sheet) = state.range_select.sheet {
if range_sheet != self.book.current_sheet {
return format!(
@ -215,7 +229,7 @@ impl<'ws> Workspace<'ws> {
}
return a1;
}
return String::new()
return String::new();
}
/// Move a row down in the current sheet.
@ -228,6 +242,12 @@ impl<'ws> Workspace<'ws> {
Ok(())
}
/// Move to the top row without changing columns
pub fn move_to_top(&mut self) -> Result<()> {
self.book.move_to(&Address { row: 1, col: self.book.location.col })?;
Ok(())
}
/// Move a row up in the current sheet.
pub fn move_up(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
@ -281,6 +301,8 @@ impl<'ws> Workspace<'ws> {
"* ENTER/RETURN: Go down one cell".to_string(),
"* TAB: Go over one cell".to_string(),
"* h,j,k,l: vim style navigation".to_string(),
"* d: clear cell contents leaving style untouched".to_string(),
"* D: clear cell contents including style".to_string(),
"* CTRl-r: Add a row".to_string(),
"* CTRl-c: Add a column".to_string(),
"* CTRl-l: Grow column width by 1".to_string(),
@ -295,6 +317,7 @@ impl<'ws> Workspace<'ws> {
"Edit Mode:".to_string(),
"* ENTER/RETURN: Exit edit mode and save changes".to_string(),
"* Ctrl-r: Enter Range Selection mode".to_string(),
"* v: Enter Range Selection mode with the start of the range already selected".to_string(),
"* ESC: Exit edit mode and discard changes".to_string(),
"Otherwise edit as normal".to_string(),
],
@ -308,6 +331,8 @@ impl<'ws> Workspace<'ws> {
"Range Selection Mode:".to_string(),
"* ESC: Exit command mode".to_string(),
"* h,j,k,l: vim style navigation".to_string(),
"* d: delete the contents of the range leaving style untouched".to_string(),
"* D: clear cell contents including style".to_string(),
"* Spacebar: Select start and end of range".to_string(),
"* CTRl-n: Next sheet. Starts over at beginning if at end.".to_string(),
"* CTRl-p: Previous sheet. Starts over at end if at beginning.".to_string(),
@ -356,7 +381,7 @@ impl<'ws> Workspace<'ws> {
return Ok(None);
}
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
self.enter_range_select_mode();
self.enter_range_select_mode(false);
return Ok(None);
}
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
@ -409,7 +434,7 @@ impl<'ws> Workspace<'ws> {
self.book.evaluate();
Ok(None)
}
Ok(Some(Cmd::InsertRow(count))) => {
Ok(Some(Cmd::InsertRows(count))) => {
self.book.insert_rows(self.book.location.row, count)?;
self.book.evaluate();
Ok(None)
@ -471,6 +496,30 @@ impl<'ws> Workspace<'ws> {
KeyCode::Char(d) if d.is_ascii_digit() => {
self.handle_numeric_prefix(d);
}
KeyCode::Char('D') => {
if let Some((start, end)) = self.state.range_select.get_range() {
self.book.clear_cell_range_all(
self.state
.range_select
.sheet
.unwrap_or_else(|| self.book.current_sheet),
start,
end,
)?;
}
}
KeyCode::Char('d') => {
if let Some((start, end)) = self.state.range_select.get_range() {
self.book.clear_cell_range(
self.state
.range_select
.sheet
.unwrap_or_else(|| self.book.current_sheet),
start,
end,
)?;
}
}
KeyCode::Char('h') => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_left()?;
@ -500,12 +549,7 @@ impl<'ws> Workspace<'ws> {
self.maybe_update_range_end();
}
KeyCode::Char(' ') | KeyCode::Enter => {
if self.state.range_select.start.is_none() {
self.state.range_select.start = Some(self.book.location.clone());
} else {
self.state.range_select.end = Some(self.book.location.clone());
self.exit_range_select_mode()?;
}
self.update_range_selection()?;
}
KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => {
self.state.range_select.reset_range_selection();
@ -523,6 +567,25 @@ impl<'ws> Workspace<'ws> {
})?;
self.state.range_select.sheet = Some(self.book.current_sheet);
}
KeyCode::Char('C')
if key
.modifiers
.contains(KeyModifiers::CONTROL | KeyModifiers::SHIFT) =>
{
// TODO(zaphar): Share the algorithm below between both copies
self.copy_range(true)?;
}
KeyCode::Char('Y') => self.copy_range(true)?,
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
self.copy_range(false)?;
}
KeyCode::Char('y') => self.copy_range(false)?,
KeyCode::Char('x') => {
if let (Some(from), Some(to)) = (self.state.range_select.start.as_ref(), self.state.range_select.end.as_ref()) {
self.book.extend_to(from, to)?;
}
self.exit_range_select_mode()?;
}
_ => {
// moop
}
@ -531,6 +594,53 @@ impl<'ws> Workspace<'ws> {
Ok(None)
}
fn copy_range(&mut self, formatted: bool) -> Result<(), anyhow::Error> {
self.update_range_selection()?;
match &self.state.range_select.get_range() {
Some((
start,
end,
)) => {
let mut rows = Vec::new();
for row in (AddressRange { start, end, }).as_rows() {
let mut cols = Vec::new();
for cell in row {
cols.push(if formatted {
self.book
.get_cell_addr_rendered(&cell)?
} else {
self.book
.get_cell_addr_contents(&cell)?
});
}
rows.push(cols);
}
self.state.clipboard = Some(ClipboardContents::Range(rows));
}
None => {
self.state.clipboard = Some(ClipboardContents::Cell(if formatted {
self.book
.get_current_cell_rendered()?
} else {
self.book
.get_current_cell_contents()?
}));
}
}
self.exit_range_select_mode()?;
Ok(())
}
fn update_range_selection(&mut self) -> Result<(), anyhow::Error> {
Ok(if self.state.range_select.start.is_none() {
self.state.range_select.start = Some(self.book.location.clone());
self.state.range_select.end = Some(self.book.location.clone());
} else {
self.state.range_select.end = Some(self.book.location.clone());
self.exit_range_select_mode()?;
})
}
fn maybe_update_range_end(&mut self) {
if self.state.range_select.start.is_some() {
self.state.range_select.end = Some(self.book.location.clone());
@ -555,8 +665,46 @@ impl<'ws> Workspace<'ws> {
KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => {
self.save_file()?;
}
KeyCode::Char('s') if key.modifiers != KeyModifiers::CONTROL => {
self.book.clear_current_cell()?;
self.text_area = reset_text_area(String::new());
self.enter_edit_mode();
}
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
self.enter_range_select_mode();
self.enter_range_select_mode(false);
}
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
self.state.clipboard = Some(ClipboardContents::Cell(
self.book.get_current_cell_contents()?,
));
}
KeyCode::Char('y') => {
self.state.clipboard = Some(ClipboardContents::Cell(
self.book.get_current_cell_contents()?,
));
}
KeyCode::Char('Y') => {
self.state.clipboard = Some(ClipboardContents::Cell(
self.book.get_current_cell_rendered()?,
));
}
KeyCode::Char('C')
if key
.modifiers
.contains(KeyModifiers::CONTROL | KeyModifiers::SHIFT) =>
{
self.state.clipboard = Some(ClipboardContents::Cell(
self.book.get_current_cell_rendered()?,
));
}
KeyCode::Char('v') if key.modifiers != KeyModifiers::CONTROL => {
self.enter_range_select_mode(true)
}
KeyCode::Char('p') if key.modifiers != KeyModifiers::CONTROL => {
self.paste_range()?;
}
KeyCode::Char('v') if key.modifiers == KeyModifiers::CONTROL => {
self.paste_range()?;
}
KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => {
self.enter_dialog_mode(self.render_help_text());
@ -567,6 +715,12 @@ impl<'ws> Workspace<'ws> {
Ok(())
})?;
}
KeyCode::Char('d') => {
self.book.clear_current_cell()?;
}
KeyCode::Char('D') => {
self.book.clear_current_cell_all()?;
}
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.book.select_prev_sheet();
@ -656,14 +810,56 @@ impl<'ws> Workspace<'ws> {
Ok(())
})?;
}
KeyCode::Char('g') => {
// TODO(zaphar): This really needs a better state machine.
if self.state.char_queue.first().map(|c| *c == 'g').unwrap_or(false) {
self.state.char_queue.pop();
self.move_to_top()?;
} else {
self.state.char_queue.push('g');
}
}
_ => {
// noop
self.state.char_queue.clear();
}
}
}
return Ok(None);
}
fn paste_range(&mut self) -> Result<(), anyhow::Error> {
match &self.state.clipboard {
Some(ClipboardContents::Cell(contents)) => {
self.book.edit_current_cell(contents)?;
self.book.evaluate();
}
Some(ClipboardContents::Range(ref rows)) => {
let Address { row, col } = self.book.location.clone();
let row_len = rows.len();
for ri in 0..row_len {
let columns = &rows[ri];
let col_len = columns.len();
for ci in 0..col_len {
self.book.update_cell(
&Address {
row: ri + row,
col: ci + col,
},
columns[ci].clone(),
)?;
}
}
self.book.evaluate();
}
None => {
// NOOP
}
}
self.state.clipboard = None;
Ok(())
}
fn run_with_prefix(
&mut self,
action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>,
@ -687,11 +883,15 @@ impl<'ws> Workspace<'ws> {
self.state.modality_stack.push(Modality::Dialog);
}
fn enter_range_select_mode(&mut self) {
fn enter_range_select_mode(&mut self, init_start: bool) {
self.state.range_select.sheet = Some(self.book.current_sheet);
self.state.range_select.original_sheet = Some(self.book.current_sheet);
self.state.range_select.original_location = Some(self.book.location.clone());
self.state.range_select.start = None;
if init_start {
self.state.range_select.start = Some(self.book.location.clone());
} else {
self.state.range_select.start = None;
}
self.state.range_select.end = None;
self.state.modality_stack.push(Modality::RangeSelect);
}

View File

@ -28,25 +28,40 @@ impl<'ws> Workspace<'ws> {
];
let mut rs: Vec<Box<dyn Fn(Rect, &mut Buffer, &mut Self)>> = vec![
Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| {
let tabs = Tabs::new(ws.book.get_sheet_names().iter().enumerate().map(|(idx, name)| format!("{} {}", name, idx)).collect::<Vec<String>>())
.select(Some(ws.book.current_sheet as usize));
let tabs = Tabs::new(
ws.book
.get_sheet_names()
.iter()
.enumerate()
.map(|(idx, name)| format!("{} {}", name, idx))
.collect::<Vec<String>>(),
)
.select(Some(ws.book.current_sheet as usize));
tabs.render(rect, buf);
}),
Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| {
let [text_rect, info_rect] = Layout::horizontal(vec![Constraint::Fill(1),Constraint::Fill(1)]).areas(rect);
let [text_rect, info_rect] =
Layout::horizontal(vec![Constraint::Fill(1), Constraint::Fill(1)]).areas(rect);
ws.text_area.render(text_rect, buf);
let hint = Paragraph::new(vec![
Line::from(""),
Line::from("ALT-h to toggle help dialog").centered()
Line::from("ALT-h to toggle help dialog").centered(),
]);
hint.render(info_rect, buf);
}),
Box::new(move |rect: Rect, buf: &mut Buffer, ws: &mut Self| {
let sheet_name = ws.book.get_sheet_name().unwrap_or("Unknown");
let table_block = Block::bordered().title_top(sheet_name);
let viewport = Viewport::new(&ws.book, &ws.state.range_select)
.with_selected(ws.book.location.clone())
.block(table_block);
let viewport = Viewport::new(
&ws.book,
if ws.state.modality() == &Modality::RangeSelect {
Some(&ws.state.range_select)
} else {
None
},
)
.with_selected(ws.book.location.clone())
.block(table_block);
StatefulWidget::render(viewport, rect, buf, &mut ws.state.viewport_state);
}),
];

View File

@ -14,7 +14,7 @@ fn test_viewport_get_visible_columns() {
let width = dbg!(dbg!(default_size) * 12 / 2);
let app_state = AppState::default();
let viewport =
Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 1, col: 17 });
Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { row: 1, col: 17 });
let cols = viewport
.get_visible_columns((width + 5) as u16, &mut state)
.expect("Failed to get visible columns");
@ -31,7 +31,7 @@ fn test_viewport_get_visible_rows() {
let height = 6;
let app_state = AppState::default();
let viewport =
Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 17, col: 1 });
Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { row: 17, col: 1 });
let rows = dbg!(viewport.get_visible_rows(height as u16, &mut state));
assert_eq!(height - 1, rows.len());
assert_eq!(
@ -51,7 +51,7 @@ fn test_viewport_visible_columns_after_length_change() {
let width = dbg!(dbg!(default_size) * 12 / 2);
{
let app_state = AppState::default();
let viewport = Viewport::new(&book, &app_state.range_select)
let viewport = Viewport::new(&book, Some(&app_state.range_select))
.with_selected(Address { row: 1, col: 17 });
let cols = viewport
.get_visible_columns((width + 5) as u16, &mut state)
@ -65,7 +65,7 @@ fn test_viewport_visible_columns_after_length_change() {
{
let app_state = AppState::default();
let viewport =
Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 1, col: 1 });
Viewport::new(&book, Some(&app_state.range_select)).with_selected(Address { row: 1, col: 1 });
let cols = viewport
.get_visible_columns((width + 5) as u16, &mut state)
.expect("Failed to get visible columns");

View File

@ -37,7 +37,7 @@ pub struct ViewportState {
pub struct Viewport<'ws> {
pub(crate) selected: Address,
book: &'ws Book,
range_selection: &'ws RangeSelection,
range_selection: Option<&'ws RangeSelection>,
block: Option<Block<'ws>>,
}
@ -47,7 +47,7 @@ pub(crate) const COLNAMES: [&'static str; 26] = [
];
impl<'ws> Viewport<'ws> {
pub fn new(book: &'ws Book, app_state: &'ws RangeSelection) -> Self {
pub fn new(book: &'ws Book, app_state: Option<&'ws RangeSelection>) -> Self {
Self {
book,
range_selection: app_state,
@ -160,7 +160,9 @@ impl<'ws> Viewport<'ws> {
.get_cell_addr_rendered(&Address { row: ri, col: *ci })
.unwrap();
let mut cell = Cell::new(Text::raw(content));
if let Some((start, end)) = &self.range_selection.get_range() {
if let Some((start, end)) =
&self.range_selection.map_or(None, |r| r.get_range())
{
if ri >= start.row
&& ri <= end.row
&& *ci >= start.col

View File

@ -35,7 +35,7 @@ fn test_insert_rows_cmd() {
let output = result.unwrap();
assert!(output.is_some());
let cmd = output.unwrap();
assert_eq!(cmd, Cmd::InsertRow(1));
assert_eq!(cmd, Cmd::InsertRows(1));
}
#[test]
@ -46,7 +46,7 @@ fn test_insert_rows_cmd_short() {
let output = result.unwrap();
assert!(output.is_some());
let cmd = output.unwrap();
assert_eq!(cmd, Cmd::InsertRow(1));
assert_eq!(cmd, Cmd::InsertRows(1));
}
#[test]
@ -200,7 +200,6 @@ fn construct_modified_key_event(code: KeyCode, mods: KeyModifiers) -> Event {
Event::Key(KeyEvent::new(code, mods))
}
// TODO(zaphar): Interaction testing for input.
#[test]
fn test_input_navitation_enter_key() {
let mut ws =
@ -425,3 +424,22 @@ fn test_range_copy_mode_from_edit_mode() {
.expect("Failed to handle 'Ctrl-r' key event");
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
}
#[test]
fn test_gg_movement() {
let mut ws =
Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook");
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
ws.handle_input(construct_key_event(KeyCode::Char('j')))
.expect("Failed to handle 'e' key event");
ws.handle_input(construct_key_event(KeyCode::Char('j')))
.expect("Failed to handle 'e' key event");
assert_eq!(ws.book.location, Address { row: 3, col: 1 });
ws.handle_input(construct_key_event(KeyCode::Char('l')))
.expect("Failed to handle 'e' key event");
ws.handle_input(construct_key_event(KeyCode::Char('g')))
.expect("Failed to handle 'e' key event");
ws.handle_input(construct_key_event(KeyCode::Char('g')))
.expect("Failed to handle 'e' key event");
assert_eq!(ws.book.location, Address { row: 1, col: 2 });
}