Command needs access to view information for certain changes.

This commit is contained in:
Blaž Hrastnik 2020-09-19 23:16:00 +09:00
parent 1303ffd94a
commit 48330ddb5f
7 changed files with 132 additions and 107 deletions

View file

@ -1,5 +1,4 @@
#![allow(unused)] #![allow(unused)]
pub mod commands;
pub mod graphemes; pub mod graphemes;
mod position; mod position;
mod selection; mod selection;
@ -11,7 +10,7 @@ pub use ropey::{Rope, RopeSlice};
pub use tendril::StrTendril as Tendril; pub use tendril::StrTendril as Tendril;
pub use position::Position; pub use position::Position;
pub use selection::Range as SelectionRange; pub use selection::Range;
pub use selection::Selection; pub use selection::Selection;
pub use syntax::Syntax; pub use syntax::Syntax;

View file

@ -1,5 +1,5 @@
use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes}; use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes};
use crate::{Position, Rope, RopeSlice, Selection, SelectionRange, Syntax}; use crate::{Position, Range, Rope, RopeSlice, Selection, Syntax};
use anyhow::Error; use anyhow::Error;
use std::path::PathBuf; use std::path::PathBuf;
@ -12,11 +12,12 @@ pub enum Mode {
/// A state represents the current editor state of a single buffer. /// A state represents the current editor state of a single buffer.
pub struct State { pub struct State {
// TODO: fields should be private but we need to refactor commands.rs first
/// Path to file on disk. /// Path to file on disk.
pub(crate) path: Option<PathBuf>, pub path: Option<PathBuf>,
pub(crate) doc: Rope, pub doc: Rope,
pub(crate) selection: Selection, pub selection: Selection,
pub(crate) mode: Mode, pub mode: Mode,
// //
pub syntax: Option<Syntax>, pub syntax: Option<Syntax>,
@ -189,7 +190,7 @@ impl State {
// } else { // } else {
let pos = self.move_pos(range.head, dir, granularity, count); let pos = self.move_pos(range.head, dir, granularity, count);
// }; // };
SelectionRange::new(pos, pos) Range::new(pos, pos)
}) })
} }
@ -201,7 +202,7 @@ impl State {
) -> Selection { ) -> Selection {
self.selection.transform(|range| { self.selection.transform(|range| {
let pos = self.move_pos(range.head, dir, granularity, count); let pos = self.move_pos(range.head, dir, granularity, count);
SelectionRange::new(range.anchor, pos) Range::new(range.anchor, pos)
}) })
} }
} }

View file

@ -1,4 +1,4 @@
use crate::{Rope, Selection, SelectionRange, State, Tendril}; use crate::{Range, Rope, Selection, State, Tendril};
/// (from, to, replacement) /// (from, to, replacement)
pub type Change = (usize, usize, Option<Tendril>); pub type Change = (usize, usize, Option<Tendril>);
@ -387,7 +387,7 @@ impl Transaction {
/// Generate a transaction with a change per selection range. /// Generate a transaction with a change per selection range.
pub fn change_by_selection<F>(state: &State, f: F) -> Self pub fn change_by_selection<F>(state: &State, f: F) -> Self
where where
F: Fn(&SelectionRange) -> Change, F: Fn(&Range) -> Change,
{ {
Self::change(state, state.selection.ranges().iter().map(f)) Self::change(state, state.selection.ranges().iter().map(f))
} }

View file

@ -1,32 +1,42 @@
use crate::graphemes; use helix_core::{
use crate::selection::{Range, Selection}; graphemes,
use crate::state::{Direction, Granularity, Mode, State}; state::{Direction, Granularity, Mode, State},
use crate::transaction::{ChangeSet, Transaction}; ChangeSet, Range, Selection, Tendril, Transaction,
use crate::Tendril; };
use crate::editor::View;
/// A command is a function that takes the current state and a count, and does a side-effect on the /// A command is a function that takes the current state and a count, and does a side-effect on the
/// state (usually by creating and applying a transaction). /// state (usually by creating and applying a transaction).
pub type Command = fn(state: &mut State, count: usize); pub type Command = fn(view: &mut View, count: usize);
pub fn move_char_left(state: &mut State, count: usize) { pub fn move_char_left(view: &mut View, count: usize) {
// TODO: use a transaction // TODO: use a transaction
let selection = state.move_selection(Direction::Backward, Granularity::Character, count); let selection = view
state.selection = selection; .state
.move_selection(Direction::Backward, Granularity::Character, count);
view.state.selection = selection;
} }
pub fn move_char_right(state: &mut State, count: usize) { pub fn move_char_right(view: &mut View, count: usize) {
// TODO: use a transaction // TODO: use a transaction
state.selection = state.move_selection(Direction::Forward, Granularity::Character, count); view.state.selection =
view.state
.move_selection(Direction::Forward, Granularity::Character, count);
} }
pub fn move_line_up(state: &mut State, count: usize) { pub fn move_line_up(view: &mut View, count: usize) {
// TODO: use a transaction // TODO: use a transaction
state.selection = state.move_selection(Direction::Backward, Granularity::Line, count); view.state.selection = view
.state
.move_selection(Direction::Backward, Granularity::Line, count);
} }
pub fn move_line_down(state: &mut State, count: usize) { pub fn move_line_down(view: &mut View, count: usize) {
// TODO: use a transaction // TODO: use a transaction
state.selection = state.move_selection(Direction::Forward, Granularity::Line, count); view.state.selection = view
.state
.move_selection(Direction::Forward, Granularity::Line, count);
} }
// avoid select by default by having a visual mode switch that makes movements into selects // avoid select by default by having a visual mode switch that makes movements into selects
@ -37,21 +47,22 @@ pub fn move_line_down(state: &mut State, count: usize) {
// lastly, if it was append mode we shift cursor by 1? // lastly, if it was append mode we shift cursor by 1?
// inserts at the start of each selection // inserts at the start of each selection
pub fn insert_mode(state: &mut State, _count: usize) { pub fn insert_mode(view: &mut View, _count: usize) {
state.mode = Mode::Insert; view.state.mode = Mode::Insert;
state.selection = state view.state.selection = view
.state
.selection .selection
.transform(|range| Range::new(range.to(), range.from())) .transform(|range| Range::new(range.to(), range.from()))
} }
// inserts at the end of each selection // inserts at the end of each selection
pub fn append_mode(state: &mut State, _count: usize) { pub fn append_mode(view: &mut View, _count: usize) {
state.mode = Mode::Insert; view.state.mode = Mode::Insert;
// TODO: as transaction // TODO: as transaction
let text = &state.doc.slice(..); let text = &view.state.doc.slice(..);
state.selection = state.selection.transform(|range| { view.state.selection = view.state.selection.transform(|range| {
// TODO: to() + next char // TODO: to() + next char
Range::new( Range::new(
range.from(), range.from(),
@ -78,63 +89,63 @@ fn selection_lines(state: &State) -> Vec<usize> {
} }
// I inserts at the start of each line with a selection // I inserts at the start of each line with a selection
pub fn prepend_to_line(state: &mut State, _count: usize) { pub fn prepend_to_line(view: &mut View, _count: usize) {
state.mode = Mode::Insert; view.state.mode = Mode::Insert;
let lines = selection_lines(state); let lines = selection_lines(&view.state);
let positions = lines let positions = lines
.into_iter() .into_iter()
.map(|index| { .map(|index| {
// adjust all positions to the start of the line. // adjust all positions to the start of the line.
state.doc.line_to_char(index) view.state.doc.line_to_char(index)
}) })
.map(|pos| Range::new(pos, pos)); .map(|pos| Range::new(pos, pos));
let selection = Selection::new(positions.collect(), 0); let selection = Selection::new(positions.collect(), 0);
let transaction = Transaction::new(state).with_selection(selection); let transaction = Transaction::new(&mut view.state).with_selection(selection);
transaction.apply(state); transaction.apply(&mut view.state);
// TODO: need to store into history if successful // TODO: need to store into history if successful
} }
// A inserts at the end of each line with a selection // A inserts at the end of each line with a selection
pub fn append_to_line(state: &mut State, _count: usize) { pub fn append_to_line(view: &mut View, _count: usize) {
state.mode = Mode::Insert; view.state.mode = Mode::Insert;
let lines = selection_lines(state); let lines = selection_lines(&view.state);
let positions = lines let positions = lines
.into_iter() .into_iter()
.map(|index| { .map(|index| {
// adjust all positions to the end of the line. // adjust all positions to the end of the line.
let line = state.doc.line(index); let line = view.state.doc.line(index);
let line_start = state.doc.line_to_char(index); let line_start = view.state.doc.line_to_char(index);
line_start + line.len_chars() - 1 line_start + line.len_chars() - 1
}) })
.map(|pos| Range::new(pos, pos)); .map(|pos| Range::new(pos, pos));
let selection = Selection::new(positions.collect(), 0); let selection = Selection::new(positions.collect(), 0);
let transaction = Transaction::new(state).with_selection(selection); let transaction = Transaction::new(&mut view.state).with_selection(selection);
transaction.apply(state); transaction.apply(&mut view.state);
// TODO: need to store into history if successful // TODO: need to store into history if successful
} }
// o inserts a new line after each line with a selection // o inserts a new line after each line with a selection
pub fn open_below(state: &mut State, _count: usize) { pub fn open_below(view: &mut View, _count: usize) {
state.mode = Mode::Insert; view.state.mode = Mode::Insert;
let lines = selection_lines(state); let lines = selection_lines(&view.state);
let positions: Vec<_> = lines let positions: Vec<_> = lines
.into_iter() .into_iter()
.map(|index| { .map(|index| {
// adjust all positions to the end of the line. // adjust all positions to the end of the line.
let line = state.doc.line(index); let line = view.state.doc.line(index);
let line_start = state.doc.line_to_char(index); let line_start = view.state.doc.line_to_char(index);
line_start + line.len_chars() line_start + line.len_chars()
}) })
.collect(); .collect();
@ -155,51 +166,51 @@ pub fn open_below(state: &mut State, _count: usize) {
0, 0,
); );
let transaction = Transaction::change(state, changes).with_selection(selection); let transaction = Transaction::change(&view.state, changes).with_selection(selection);
transaction.apply(state); transaction.apply(&mut view.state);
// TODO: need to store into history if successful // TODO: need to store into history if successful
} }
// O inserts a new line before each line with a selection // O inserts a new line before each line with a selection
pub fn normal_mode(state: &mut State, _count: usize) { pub fn normal_mode(view: &mut View, _count: usize) {
// TODO: if leaving append mode, move cursor back by 1 // TODO: if leaving append mode, move cursor back by 1
state.mode = Mode::Normal; view.state.mode = Mode::Normal;
} }
// TODO: insert means add text just before cursor, on exit we should be on the last letter. // TODO: insert means add text just before cursor, on exit we should be on the last letter.
pub fn insert_char(state: &mut State, c: char) { pub fn insert_char(view: &mut View, c: char) {
let c = Tendril::from_char(c); let c = Tendril::from_char(c);
let transaction = Transaction::insert(&state, c); let transaction = Transaction::insert(&view.state, c);
transaction.apply(state); transaction.apply(&mut view.state);
// TODO: need to store into history if successful // TODO: need to store into history if successful
} }
// TODO: handle indent-aware delete // TODO: handle indent-aware delete
pub fn delete_char_backward(state: &mut State, count: usize) { pub fn delete_char_backward(view: &mut View, count: usize) {
let text = &state.doc.slice(..); let text = &view.state.doc.slice(..);
let transaction = Transaction::change_by_selection(state, |range| { let transaction = Transaction::change_by_selection(&view.state, |range| {
( (
graphemes::nth_prev_grapheme_boundary(text, range.head, count), graphemes::nth_prev_grapheme_boundary(text, range.head, count),
range.head, range.head,
None, None,
) )
}); });
transaction.apply(state); transaction.apply(&mut view.state);
// TODO: need to store into history if successful // TODO: need to store into history if successful
} }
pub fn delete_char_forward(state: &mut State, count: usize) { pub fn delete_char_forward(view: &mut View, count: usize) {
let text = &state.doc.slice(..); let text = &view.state.doc.slice(..);
let transaction = Transaction::change_by_selection(state, |range| { let transaction = Transaction::change_by_selection(&view.state, |range| {
( (
graphemes::nth_next_grapheme_boundary(text, range.head, count), graphemes::nth_next_grapheme_boundary(text, range.head, count),
range.head, range.head,
None, None,
) )
}); });
transaction.apply(state); transaction.apply(&mut view.state);
// TODO: need to store into history if successful // TODO: need to store into history if successful
} }

View file

@ -1,4 +1,4 @@
use crate::{keymap, theme::Theme, Args}; use crate::{commands, keymap, theme::Theme, Args};
use helix_core::{ use helix_core::{
state::coords_at_pos, state::coords_at_pos,
state::Mode, state::Mode,
@ -31,10 +31,15 @@ type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
static EX: smol::Executor = smol::Executor::new(); static EX: smol::Executor = smol::Executor::new();
pub struct View {
pub state: State,
pub first_line: u16,
pub size: (u16, u16),
}
pub struct Editor { pub struct Editor {
terminal: Terminal, terminal: Terminal,
state: Option<State>, view: Option<View>,
first_line: u16,
size: (u16, u16), size: (u16, u16),
surface: Surface, surface: Surface,
cache: Surface, cache: Surface,
@ -52,8 +57,7 @@ impl Editor {
let mut editor = Editor { let mut editor = Editor {
terminal, terminal,
state: None, view: None,
first_line: 0,
size, size,
surface: Surface::empty(area), surface: Surface::empty(area),
cache: Surface::empty(area), cache: Surface::empty(area),
@ -75,7 +79,14 @@ impl Editor {
.as_mut() .as_mut()
.unwrap() .unwrap()
.configure(self.theme.scopes()); .configure(self.theme.scopes());
self.state = Some(state);
let view = View {
state,
first_line: 0,
size: self.size,
};
self.view = Some(view);
Ok(()) Ok(())
} }
@ -83,8 +94,8 @@ impl Editor {
use tui::backend::Backend; use tui::backend::Backend;
use tui::style::Color; use tui::style::Color;
// TODO: ideally not mut but highlights require it because of cursor cache // TODO: ideally not mut but highlights require it because of cursor cache
match &mut self.state { match &mut self.view {
Some(state) => { Some(view) => {
let area = Rect::new(0, 0, self.size.0, self.size.1); let area = Rect::new(0, 0, self.size.0, self.size.1);
let mut stdout = stdout(); let mut stdout = stdout();
self.surface.reset(); // reset is faster than allocating new empty surface self.surface.reset(); // reset is faster than allocating new empty surface
@ -97,18 +108,18 @@ impl Editor {
let viewport = Rect::new(offset, 0, self.size.0, self.size.1 - 1); // - 1 for statusline let viewport = Rect::new(offset, 0, self.size.0, self.size.1 - 1); // - 1 for statusline
// TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|) // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, pos|)
let source_code = state.doc().to_string(); let source_code = view.state.doc().to_string();
let last_line = std::cmp::min( let last_line = std::cmp::min(
(self.first_line + viewport.height - 1) as usize, (view.first_line + viewport.height - 1) as usize,
state.doc().len_lines() - 1, view.state.doc().len_lines() - 1,
); );
let range = { let range = {
// calculate viewport byte ranges // calculate viewport byte ranges
let start = state.doc().line_to_byte(self.first_line.into()); let start = view.state.doc().line_to_byte(view.first_line.into());
let end = state.doc().line_to_byte(last_line) let end = view.state.doc().line_to_byte(last_line)
+ state.doc().line(last_line).len_bytes(); + view.state.doc().line(last_line).len_bytes();
start..end start..end
}; };
@ -117,7 +128,8 @@ impl Editor {
// TODO: cache highlight results // TODO: cache highlight results
// TODO: only recalculate when state.doc is actually modified // TODO: only recalculate when state.doc is actually modified
let highlights: Vec<_> = state let highlights: Vec<_> = view
.state
.syntax .syntax
.as_mut() .as_mut()
.unwrap() .unwrap()
@ -141,10 +153,10 @@ impl Editor {
HighlightEvent::Source { start, end } => { HighlightEvent::Source { start, end } => {
// TODO: filter out spans out of viewport for now.. // TODO: filter out spans out of viewport for now..
let start = state.doc().byte_to_char(start); let start = view.state.doc().byte_to_char(start);
let end = state.doc().byte_to_char(end); let end = view.state.doc().byte_to_char(end);
let text = state.doc().slice(start..end); let text = view.state.doc().slice(start..end);
use helix_core::graphemes::{grapheme_width, RopeGraphemes}; use helix_core::graphemes::{grapheme_width, RopeGraphemes};
@ -191,7 +203,7 @@ impl Editor {
let mut line = 0; let mut line = 0;
let style = self.theme.get("ui.linenr"); let style = self.theme.get("ui.linenr");
for i in self.first_line..(last_line as u16) { for i in view.first_line..(last_line as u16) {
self.surface self.surface
.set_stringn(0, line, format!("{:>5}", i + 1), 5, style); // lavender .set_stringn(0, line, format!("{:>5}", i + 1), 5, style); // lavender
line += 1; line += 1;
@ -223,7 +235,7 @@ impl Editor {
// } // }
// statusline // statusline
let mode = match state.mode() { let mode = match view.state.mode() {
Mode::Insert => "INS", Mode::Insert => "INS",
Mode::Normal => "NOR", Mode::Normal => "NOR",
}; };
@ -235,7 +247,7 @@ impl Editor {
let text_color = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac let text_color = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
self.surface self.surface
.set_string(1, self.size.1 - 1, mode, text_color); .set_string(1, self.size.1 - 1, mode, text_color);
if let Some(path) = state.path() { if let Some(path) = view.state.path() {
self.surface self.surface
.set_string(6, self.size.1 - 1, path.to_string_lossy(), text_color); .set_string(6, self.size.1 - 1, path.to_string_lossy(), text_color);
} }
@ -247,19 +259,19 @@ impl Editor {
std::mem::swap(&mut self.surface, &mut self.cache); std::mem::swap(&mut self.surface, &mut self.cache);
// set cursor shape // set cursor shape
match state.mode() { match view.state.mode() {
Mode::Insert => write!(stdout, "\x1B[6 q"), Mode::Insert => write!(stdout, "\x1B[6 q"),
Mode::Normal => write!(stdout, "\x1B[2 q"), Mode::Normal => write!(stdout, "\x1B[2 q"),
}; };
// render the cursor // render the cursor
let pos = state.selection().cursor(); let pos = view.state.selection().cursor();
let coords = coords_at_pos(&state.doc().slice(..), pos); let coords = coords_at_pos(&view.state.doc().slice(..), pos);
execute!( execute!(
stdout, stdout,
cursor::MoveTo( cursor::MoveTo(
coords.col as u16 + viewport.x, coords.col as u16 + viewport.x,
coords.row as u16 - self.first_line + viewport.y, coords.row as u16 - view.first_line + viewport.y,
) )
); );
} }
@ -295,29 +307,29 @@ impl Editor {
break; break;
} }
Some(Ok(Event::Key(event))) => { Some(Ok(Event::Key(event))) => {
if let Some(state) = &mut self.state { if let Some(view) = &mut self.view {
match state.mode() { match view.state.mode() {
Mode::Insert => { Mode::Insert => {
match event { match event {
KeyEvent { KeyEvent {
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
} => helix_core::commands::normal_mode(state, 1), } => commands::normal_mode(view, 1),
KeyEvent { KeyEvent {
code: KeyCode::Backspace, code: KeyCode::Backspace,
.. ..
} => helix_core::commands::delete_char_backward(state, 1), } => commands::delete_char_backward(view, 1),
KeyEvent { KeyEvent {
code: KeyCode::Delete, code: KeyCode::Delete,
.. ..
} => helix_core::commands::delete_char_forward(state, 1), } => commands::delete_char_forward(view, 1),
KeyEvent { KeyEvent {
code: KeyCode::Char(c), code: KeyCode::Char(c),
.. ..
} => helix_core::commands::insert_char(state, c), } => commands::insert_char(view, c),
KeyEvent { KeyEvent {
code: KeyCode::Enter, code: KeyCode::Enter,
.. ..
} => helix_core::commands::insert_char(state, '\n'), } => commands::insert_char(view, '\n'),
_ => (), // skip _ => (), // skip
} }
// TODO: simplistic ensure cursor in view for now // TODO: simplistic ensure cursor in view for now
@ -329,7 +341,7 @@ impl Editor {
// TODO: handle modes and sequences (`gg`) // TODO: handle modes and sequences (`gg`)
if let Some(command) = keymap.get(&event) { if let Some(command) = keymap.get(&event) {
// TODO: handle count other than 1 // TODO: handle count other than 1
command(state, 1); command(view, 1);
// TODO: simplistic ensure cursor in view for now // TODO: simplistic ensure cursor in view for now
self.ensure_cursor_in_view(); self.ensure_cursor_in_view();
@ -350,10 +362,10 @@ impl Editor {
} }
fn ensure_cursor_in_view(&mut self) { fn ensure_cursor_in_view(&mut self) {
if let Some(state) = &mut self.state { if let Some(view) = &mut self.view {
let cursor = state.selection().cursor(); let cursor = view.state.selection().cursor();
let line = state.doc().char_to_line(cursor) as u16; let line = view.state.doc().char_to_line(cursor) as u16;
let document_end = self.first_line + self.size.1.saturating_sub(1) - 1; let document_end = view.first_line + self.size.1.saturating_sub(1) - 1;
let padding = 5u16; let padding = 5u16;
@ -361,10 +373,10 @@ impl Editor {
if line > document_end.saturating_sub(padding) { if line > document_end.saturating_sub(padding) {
// scroll down // scroll down
self.first_line += line - (document_end.saturating_sub(padding)); view.first_line += line - (document_end.saturating_sub(padding));
} else if line < self.first_line + padding { } else if line < view.first_line + padding {
// scroll up // scroll up
self.first_line = line.saturating_sub(padding); view.first_line = line.saturating_sub(padding);
} }
} }
} }

View file

@ -1,9 +1,9 @@
use crate::commands::{self, Command};
use crossterm::{ use crossterm::{
event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers}, event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers},
execute, execute,
style::Print, style::Print,
}; };
use helix_core::commands::{self, Command};
use std::collections::HashMap; use std::collections::HashMap;
// Kakoune-inspired: // Kakoune-inspired:

View file

@ -1,6 +1,8 @@
#![allow(unused)] #![allow(unused)]
#[macro_use] #[macro_use]
mod macros; mod macros;
mod commands;
mod editor; mod editor;
mod keymap; mod keymap;
mod theme; mod theme;