docs: help text, spelling

This commit is contained in:
Jeremy Wall 2024-12-04 20:02:08 -05:00
parent c62fd08043
commit bb5d81106e
5 changed files with 152 additions and 23 deletions

View File

@ -76,6 +76,9 @@ For the most part this should work the same way you expect a spreadsheet to work
* `Enter` will update the cell contents.
* `Esc` will cancel editing the cell and leave it unedited.
`Ctrl-r` will enter range select mode when editing a formula. You can navigate around the
sheet and hit space to select that cell in the sheet to set the start of the range. Navigate some more and hit space to set the end of the range.
You can find the functions we support documented here: [ironcalc docs](https://docs.ironcalc.com/functions/lookup-and-reference.html)
### Command Mode
@ -94,3 +97,15 @@ The currently supported commands are:
* `quit` Quits the application. `q` is a shorthand alias for this command.
<aside>Note that in the case of `quit` and `edit` that we do not currently prompt you if the current spreadsheet has not been saved yet. So your changes 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 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.
* ` ` the spacebar will select the start and end of the range respectively.
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.
<aside>We only support continuous ranges for the moment. Planned for discontinuous ranges still needs the interaction interface to be determined.</aside>

View File

@ -287,6 +287,13 @@ impl Book {
.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)
}
pub(crate) fn get_sheet_by_idx_mut(&mut self, idx: usize) -> Result<&mut Worksheet> {
Ok(self
.model

View File

@ -30,7 +30,7 @@ pub enum Modality {
CellEdit,
Command,
Dialog,
RangeCopy,
RangeSelect,
}
#[derive(Debug)]
@ -40,6 +40,8 @@ pub struct AppState<'ws> {
pub command_state: TextState<'ws>,
pub numeric_prefix: Vec<char>,
pub original_location: Option<Address>,
pub original_sheet: Option<u32>,
pub range_sheet: Option<u32>,
pub start_range: Option<Address>,
pub end_range: Option<Address>,
dirty: bool,
@ -54,6 +56,8 @@ impl<'ws> Default for AppState<'ws> {
command_state: Default::default(),
numeric_prefix: Default::default(),
original_location: Default::default(),
original_sheet: Default::default(),
range_sheet: Default::default(),
start_range: Default::default(),
end_range: Default::default(),
dirty: Default::default(),
@ -104,6 +108,19 @@ impl Address {
pub fn new(row: usize, col: usize) -> Self {
Self { row, col }
}
pub fn to_range_part(&self) -> String {
let count = if self.col == 26 {
1
} else {
(self.col / 26) + 1
};
format!(
"{}{}",
render::viewport::COLNAMES[(self.col - 1) % 26].repeat(count),
self.row
)
}
}
impl Default for Address {
@ -156,6 +173,33 @@ impl<'ws> Workspace<'ws> {
Ok(())
}
pub fn selected_range_to_string(&self) -> String {
let state = &self.state;
let start = state
.start_range
.as_ref()
.map(|addr| addr.to_range_part())
.unwrap_or_else(|| String::new());
let end = state
.end_range
.as_ref()
.map(|addr| format!(":{}", addr.to_range_part()))
.unwrap_or_else(|| String::new());
if let Some(range_sheet) = state.range_sheet {
if range_sheet != self.book.current_sheet {
return format!(
"{}!{}{}",
self.book
.get_sheet_name_by_idx(range_sheet as usize)
.expect("No such sheet index"),
start,
end
);
}
}
format!("{}:{}", start, end)
}
/// Move a row down in the current sheet.
pub fn move_down(&mut self) -> Result<()> {
let mut loc = self.book.location.clone();
@ -204,7 +248,7 @@ impl<'ws> Workspace<'ws> {
Modality::CellEdit => self.handle_edit_input(key)?,
Modality::Command => self.handle_command_input(key)?,
Modality::Dialog => self.handle_dialog_input(key)?,
Modality::RangeCopy => self.handle_range_copy_input(key)?,
Modality::RangeSelect => self.handle_range_select_input(key)?,
};
return Ok(result);
}
@ -225,13 +269,14 @@ impl<'ws> Workspace<'ws> {
"* CTRl-h: Shrink column width by 1".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(),
"* CTRl-?: Previous sheet. Starts over at end if at beginning.".to_string(),
"* ALT-h: Previous sheet. Starts over at end if at beginning.".to_string(),
"* q exit".to_string(),
"* Ctrl-S Save sheet".to_string(),
],
Modality::CellEdit => vec![
"Edit Mode:".to_string(),
"* ENTER/RETURN: Exit edit mode and save changes".to_string(),
"* Ctrl-r: Enter Range Selection mode".to_string(),
"* ESC: Exit edit mode and discard changes".to_string(),
"Otherwise edit as normal".to_string(),
],
@ -241,6 +286,14 @@ impl<'ws> Workspace<'ws> {
"* CTRL-?: Exit command mode".to_string(),
"* ENTER/RETURN: run command and exit command mode".to_string(),
],
Modality::RangeSelect => vec![
"Range Selection Mode:".to_string(),
"* ESC: Exit command mode".to_string(),
"* h,j,k,l: vim style navigation".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(),
],
_ => vec!["General help".to_string()],
}
}
@ -284,6 +337,15 @@ impl<'ws> Workspace<'ws> {
self.enter_dialog_mode(self.render_help_text());
return Ok(None);
}
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
self.enter_range_select_mode();
return Ok(None);
}
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
self.text_area.set_yank_text(self.selected_range_to_string());
self.text_area.paste();
return Ok(None);
}
KeyCode::Enter => self.exit_edit_mode(true)?,
KeyCode::Esc => self.exit_edit_mode(false)?,
_ => {
@ -371,19 +433,22 @@ impl<'ws> Workspace<'ws> {
self.state.numeric_prefix.push(digit);
}
fn handle_range_copy_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
fn handle_range_select_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Esc => {
self.state.reset_n_prefix();
}
KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => {
self.enter_dialog_mode(self.render_help_text());
return Ok(None);
}
KeyCode::Char(d) if d.is_ascii_digit() => {
self.handle_numeric_prefix(d);
}
KeyCode::Char('h') => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_left()?;
dbg!(&ws.book.location);
Ok(())
})?;
}
@ -396,7 +461,6 @@ impl<'ws> Workspace<'ws> {
KeyCode::Char('k') => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_up()?;
dbg!(&ws.book.location);
Ok(())
})?;
}
@ -408,12 +472,26 @@ impl<'ws> Workspace<'ws> {
}
KeyCode::Char(' ') => {
if self.state.start_range.is_none() {
self.state.start_range = dbg!(Some(self.book.location.clone()));
self.state.start_range = Some(self.book.location.clone());
} else {
self.state.end_range = dbg!(Some(self.book.location.clone()));
self.exit_range_copy_mode()?;
self.state.end_range = Some(self.book.location.clone());
self.exit_range_select_mode()?;
}
}
KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.book.select_next_sheet();
Ok(())
})?;
self.state.range_sheet = Some(self.book.current_sheet);
}
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.book.select_prev_sheet();
Ok(())
})?;
self.state.range_sheet = Some(self.book.current_sheet);
}
_ => {
// moop
}
@ -441,7 +519,7 @@ impl<'ws> Workspace<'ws> {
self.save_file()?;
}
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
self.enter_range_copy_mode();
self.enter_range_select_mode();
}
KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => {
self.enter_dialog_mode(self.render_help_text());
@ -549,7 +627,10 @@ impl<'ws> Workspace<'ws> {
return Ok(None);
}
fn run_with_prefix(&mut self, action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>) -> Result<(), anyhow::Error> {
fn run_with_prefix(
&mut self,
action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>,
) -> Result<(), anyhow::Error> {
for _ in 1..=self.state.get_n_prefix() {
action(self)?;
}
@ -569,14 +650,15 @@ impl<'ws> Workspace<'ws> {
self.state.modality_stack.push(Modality::Dialog);
}
fn enter_range_copy_mode(&mut self) {
fn enter_range_select_mode(&mut self) {
self.state.range_sheet = Some(self.book.current_sheet);
self.state.original_sheet = Some(self.book.current_sheet);
self.state.original_location = Some(self.book.location.clone());
self.state.start_range = None;
self.state.end_range = None;
self.state.modality_stack.push(Modality::RangeCopy);
self.state.modality_stack.push(Modality::RangeSelect);
}
fn enter_edit_mode(&mut self) {
self.state.modality_stack.push(Modality::CellEdit);
self.text_area
@ -601,14 +683,26 @@ impl<'ws> Workspace<'ws> {
Ok(())
}
fn exit_range_copy_mode(&mut self) -> Result<()> {
self.book.location = self.state.original_location.clone().expect("Missing original location after range copy");
fn exit_range_select_mode(&mut self) -> Result<()> {
self.book.current_sheet = self
.state
.original_sheet
.clone()
.expect("Missing original sheet");
self.book.location = self
.state
.original_location
.clone()
.expect("Missing original location after range copy");
self.state.original_location = None;
self.state.pop_modality();
if self.state.modality() == &Modality::CellEdit {
self.text_area.set_yank_text(self.selected_range_to_string());
self.text_area.paste();
}
Ok(())
}
fn exit_edit_mode(&mut self, keep: bool) -> Result<()> {
self.text_area.set_cursor_line_style(Style::default());
self.text_area.set_cursor_style(Style::default());

View File

@ -95,7 +95,7 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
Modality::CellEdit => "edit",
Modality::Command => "command",
Modality::Dialog => "",
Modality::RangeCopy => "range-copy",
Modality::RangeSelect => "range-copy",
})
.title_bottom(
Line::from(format!(

View File

@ -362,7 +362,7 @@ fn test_range_copy() {
let original_loc = ws.book.location.clone();
ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL))
.expect("Failed to handle 'Ctrl-r' key event");
assert_eq!(Some(&Modality::RangeCopy), ws.state.modality_stack.last());
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
assert_eq!(Some(original_loc.clone()), ws.state.original_location);
assert!(ws.state.start_range.is_none());
assert!(ws.state.end_range.is_none());
@ -390,7 +390,7 @@ fn test_range_copy() {
ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL))
.expect("Failed to handle 'Ctrl-r' key event");
assert_eq!(Some(&Modality::RangeCopy), ws.state.modality_stack.last());
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
assert_eq!(Some(original_loc_2.clone()), ws.state.original_location);
assert!(ws.state.start_range.is_none());
assert!(ws.state.end_range.is_none());
@ -412,3 +412,16 @@ fn test_range_copy() {
assert_eq!(original_loc_2, ws.book.location);
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
}
#[test]
fn test_range_copy_mode_from_edit_mode() {
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('e')))
.expect("Failed to handle 'e' key event");
assert_eq!(Some(&Modality::CellEdit), ws.state.modality_stack.last());
ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL))
.expect("Failed to handle 'Ctrl-r' key event");
assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last());
}