forked from mars/breed
Compare commits
17 Commits
d540f8ed7c
...
878f47c7e9
Author | SHA1 | Date |
---|---|---|
mars | 878f47c7e9 | |
mars | e9c8cd7a06 | |
mars | c914787f47 | |
mars | 60fd94ed34 | |
mars | 5f1e76e912 | |
mars | 18604f3ddd | |
mars | a40ef42ed6 | |
mars | 2d3c15e571 | |
mars | 547dea6ac0 | |
mars | b29891571b | |
mars | 46759c98c7 | |
mars | 37cfa90176 | |
mars | 8141f35cdc | |
mars | f04b0ecaab | |
mars | b7715532f8 | |
mars | 417294921a | |
mars | 2976fb4697 |
|
@ -7,4 +7,4 @@ strip = true # strip symbols from the binary
|
|||
opt-level = "z" # optimize for size
|
||||
lto = true # link time optimization
|
||||
codegen-units = 1 # decrease parallelization
|
||||
panic = "abort"
|
||||
# panic = "abort"
|
||||
|
|
|
@ -11,10 +11,14 @@ libc = "0.2.141"
|
|||
notify = "5"
|
||||
parking_lot = "0.12"
|
||||
ropey = "1.6"
|
||||
toml = "0.7"
|
||||
yacexits = "0.1.5"
|
||||
|
||||
[dependencies.syntect]
|
||||
version = "5"
|
||||
default-features = false
|
||||
features = ["default-syntaxes", "parsing", "regex-onig"]
|
||||
|
||||
[dependencies.toml]
|
||||
version = "0.7"
|
||||
default-features = false
|
||||
features = ["parse"]
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[toolchain]
|
||||
channel = "nightly"
|
140
src/actions.rs
140
src/actions.rs
|
@ -19,7 +19,7 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{Direction, InsertState, Mode, State};
|
||||
use crate::{Direction, InsertState, Mode, NormalState, State};
|
||||
|
||||
pub type Action = fn(&mut State);
|
||||
|
||||
|
@ -29,6 +29,23 @@ pub fn load_actions() -> HashMap<String, Action> {
|
|||
("move_line_down", move_line_down),
|
||||
("move_line_up", move_line_up),
|
||||
("move_char_right", move_char_right),
|
||||
("page_up", page_up),
|
||||
("page_down", page_down),
|
||||
("goto_line_start", goto_line_start),
|
||||
("goto_line_end_newline", goto_line_end_newline),
|
||||
("command_mode", command_mode),
|
||||
("normal_mode", normal_mode),
|
||||
("visual_mode", visual_mode),
|
||||
("insert_mode", insert_mode),
|
||||
("append_mode", append_mode),
|
||||
("insert_at_line_start", insert_at_line_start),
|
||||
("insert_at_line_end", insert_at_line_end),
|
||||
("open_below", open_below),
|
||||
("open_above", open_above),
|
||||
("undo", undo),
|
||||
("redo", redo),
|
||||
("delete_char_backward", delete_char_backward),
|
||||
("insert_newline", insert_newline),
|
||||
];
|
||||
|
||||
let mut actions = HashMap::with_capacity(list.len());
|
||||
|
@ -55,14 +72,58 @@ pub fn move_line_up(state: &mut State) {
|
|||
state.move_cursor(Direction::Up);
|
||||
}
|
||||
|
||||
pub fn page_up(state: &mut State) {
|
||||
state.set_error("page_up is unimplemented");
|
||||
}
|
||||
|
||||
pub fn page_down(state: &mut State) {
|
||||
state.set_error("page_down is unimplemented");
|
||||
}
|
||||
|
||||
pub fn goto_line_start(state: &mut State) {
|
||||
state.set_error("goto_line_start is unimplemented");
|
||||
}
|
||||
|
||||
pub fn goto_line_end_newline(state: &mut State) {
|
||||
state.set_error("goto_line_end_newline is unimplemented");
|
||||
}
|
||||
|
||||
pub fn command_mode(state: &mut State) {
|
||||
state.mode = Mode::Command(Default::default());
|
||||
}
|
||||
|
||||
pub fn normal_mode(state: &mut State) {
|
||||
match state.mode {
|
||||
Mode::Insert(InsertState { append: true }) => {
|
||||
state.move_cursor(Direction::Left);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
state.mode = Mode::Normal(NormalState::default());
|
||||
}
|
||||
|
||||
pub fn visual_mode(state: &mut State) {
|
||||
state.mode = Mode::Visual;
|
||||
}
|
||||
|
||||
pub fn insert_mode(state: &mut State) {
|
||||
state.mode = Mode::Insert(InsertState { append: false });
|
||||
}
|
||||
|
||||
pub fn append_mode(state: &mut State) {
|
||||
state.move_cursor(Direction::Right);
|
||||
state.mode = Mode::Insert(InsertState { append: true });
|
||||
}
|
||||
|
||||
pub fn insert_at_line_start(state: &mut State) {
|
||||
state.set_error("insert_at_line_start is unimplemented");
|
||||
}
|
||||
|
||||
pub fn insert_at_line_end(state: &mut State) {
|
||||
state.set_error("insert_at_line_end is unimplemented");
|
||||
}
|
||||
|
||||
pub fn open_below(state: &mut State) {
|
||||
state.cursor.line += 1;
|
||||
state.cursor.column = 0;
|
||||
|
@ -75,3 +136,80 @@ pub fn open_above(state: &mut State) {
|
|||
state.buffer.insert_char(state.cursor, '\n');
|
||||
state.mode = Mode::Insert(InsertState { append: false });
|
||||
}
|
||||
|
||||
pub fn undo(state: &mut State) {
|
||||
state.set_error("undo is unimplemented");
|
||||
}
|
||||
|
||||
pub fn redo(state: &mut State) {
|
||||
state.set_error("redo is unimplemented");
|
||||
}
|
||||
|
||||
pub fn delete_char_backward(state: &mut State) {
|
||||
state.move_cursor(Direction::Left);
|
||||
state.buffer.remove(state.cursor);
|
||||
}
|
||||
|
||||
pub fn delete_char_forward(state: &mut State) {
|
||||
state.buffer.remove(state.cursor);
|
||||
}
|
||||
|
||||
pub fn insert_newline(state: &mut State) {
|
||||
state.buffer.insert_char(state.cursor, '\n');
|
||||
state.cursor.line += 1;
|
||||
state.cursor.column = 0;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn state(src: &str) -> State {
|
||||
State::from_str(None, src).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backspace_empty() {
|
||||
let mut state = state("");
|
||||
delete_char_backward(&mut state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_on_newline() {
|
||||
let mut state = state("test\ntest\n");
|
||||
state.cursor.column = 4;
|
||||
append_mode(&mut state);
|
||||
assert_eq!(state.cursor.line, 1);
|
||||
assert_eq!(state.cursor.column, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_at_eof() {
|
||||
let mut state = state("test\n");
|
||||
state.cursor.column = 4;
|
||||
append_mode(&mut state);
|
||||
assert_eq!(state.cursor.line, 1);
|
||||
assert_eq!(state.cursor.column, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_line_up_preserve_column() {
|
||||
let mut state = state("test\n\ntest\n");
|
||||
state.cursor.line = 2;
|
||||
state.cursor.column = 3;
|
||||
move_line_up(&mut state);
|
||||
move_line_up(&mut state);
|
||||
assert_eq!(state.cursor.line, 0);
|
||||
assert_eq!(state.cursor.column, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_line_down_preserve_column() {
|
||||
let mut state = state("test\n\ntest\n");
|
||||
state.cursor.column = 3;
|
||||
move_line_down(&mut state);
|
||||
move_line_down(&mut state);
|
||||
assert_eq!(state.cursor.line, 2);
|
||||
assert_eq!(state.cursor.column, 3);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,15 +74,17 @@ impl Buffer {
|
|||
cols: u16,
|
||||
rows: u16,
|
||||
scroll: Cursor,
|
||||
cursor: Cursor,
|
||||
out: &mut impl Write,
|
||||
) -> crossterm::Result<u32> {
|
||||
self.parse();
|
||||
|
||||
let mut styles = self.styles.lock();
|
||||
let linenr_style = styles.get_scope("ui.linenr");
|
||||
let linenr_style = styles.get_scope("ui.linenr").clone();
|
||||
let linenr_width = self.text.len_lines().ilog10() + 1;
|
||||
let gutter_width = linenr_width + 1;
|
||||
let text_width = cols as usize - gutter_width as usize;
|
||||
let line_padding: String = std::iter::repeat(' ').take(text_width).collect();
|
||||
|
||||
out.queue(cursor::MoveTo(0, 0))?;
|
||||
|
||||
|
@ -94,22 +96,40 @@ impl Buffer {
|
|||
}
|
||||
|
||||
let row = row as usize + scroll.line;
|
||||
let is_selected = row == cursor.line;
|
||||
|
||||
let linenr_style = if is_selected {
|
||||
styles.get_scope("ui.linenr.selected").clone()
|
||||
} else {
|
||||
linenr_style
|
||||
};
|
||||
|
||||
let line_style = if is_selected {
|
||||
styles.get_scope("ui.cursorline.primary").clone()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let linenr = format!("{:width$} ", row, width = linenr_width as usize);
|
||||
linenr_style.print_styled(out, &linenr)?;
|
||||
|
||||
let lhs = scroll.column;
|
||||
let width = line.len_chars();
|
||||
let mut remaining = text_width;
|
||||
line_buf.clear();
|
||||
if lhs < width {
|
||||
let window = text_width.min(width - lhs);
|
||||
let rhs = lhs + window;
|
||||
let styled = &self.styled[row];
|
||||
for (style, range) in styled.iter() {
|
||||
for (chunk_style, range) in styled.iter() {
|
||||
let range = range.start.max(lhs)..range.end.min(rhs);
|
||||
if range.start < range.end {
|
||||
// this range is in view so display
|
||||
let content = line.slice(range.clone());
|
||||
let mut style = line_style.clone();
|
||||
style.apply(chunk_style);
|
||||
style.print_styled(&mut line_buf, content)?;
|
||||
remaining = text_width.saturating_sub(range.end);
|
||||
}
|
||||
|
||||
if range.end >= rhs {
|
||||
|
@ -119,6 +139,9 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
let padding = &line_padding[0..remaining];
|
||||
line_style.print_styled(&mut line_buf, padding)?;
|
||||
|
||||
out.write_all(&line_buf)?;
|
||||
out.queue(cursor::MoveToNextLine(1))?;
|
||||
}
|
||||
|
@ -199,11 +222,11 @@ impl Buffer {
|
|||
}
|
||||
|
||||
pub fn clamped_cursor(&self, cursor: Cursor) -> Cursor {
|
||||
let line_end = self.text.line(cursor.line).len_chars().saturating_sub(1);
|
||||
|
||||
Cursor {
|
||||
line: cursor.line,
|
||||
column: cursor
|
||||
.column
|
||||
.min(self.text.line(cursor.line).len_chars() - 1),
|
||||
column: cursor.column.min(line_end),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,17 +24,22 @@ use crossbeam_channel::{unbounded, Receiver, RecvError, Sender};
|
|||
use notify::{Event, EventKind, RecommendedWatcher, Watcher};
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::keybinds::Keybinds;
|
||||
use crate::theme::{StyleStore, Theme};
|
||||
|
||||
pub struct Config {
|
||||
/// Whether or not to wrap lines when moving the cursor left and right.
|
||||
pub move_linewrap: bool,
|
||||
|
||||
/// Key bindings.
|
||||
pub keybinds: Keybinds,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
move_linewrap: true,
|
||||
keybinds: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Marceline Cramer
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
use toml::{Table, Value};
|
||||
|
||||
use crate::actions::{load_actions, Action};
|
||||
|
||||
pub type Key = KeyCode;
|
||||
|
||||
pub type KeyMap = HashMap<Key, Keybind>;
|
||||
|
||||
pub struct Keybinds {
|
||||
pub normal: ModeKeys,
|
||||
pub insert: ModeKeys,
|
||||
pub visual: ModeKeys,
|
||||
}
|
||||
|
||||
impl Default for Keybinds {
|
||||
fn default() -> Self {
|
||||
use crate::actions::*;
|
||||
use KeyCode::*;
|
||||
|
||||
let basic_nav = &[
|
||||
(Esc, normal_mode as Action),
|
||||
(Left, move_char_left),
|
||||
(Down, move_line_down),
|
||||
(Up, move_line_up),
|
||||
(Right, move_char_right),
|
||||
(PageUp, page_up),
|
||||
(PageDown, page_down),
|
||||
(Home, goto_line_start),
|
||||
(End, goto_line_end_newline),
|
||||
];
|
||||
|
||||
let normalish_keys = &[
|
||||
(Char(':'), command_mode as Action),
|
||||
(Char('h'), move_char_left),
|
||||
(Char('j'), move_line_down),
|
||||
(Char('k'), move_line_up),
|
||||
(Char('l'), move_char_right),
|
||||
(Char('i'), insert_mode),
|
||||
(Char('a'), append_mode),
|
||||
(Char('I'), insert_at_line_start),
|
||||
(Char('A'), insert_at_line_end),
|
||||
(Char('o'), open_below),
|
||||
(Char('O'), open_above),
|
||||
(Char('u'), undo),
|
||||
(Char('U'), redo),
|
||||
]
|
||||
.iter()
|
||||
.chain(basic_nav);
|
||||
|
||||
let insert_keys = &[
|
||||
(Backspace, delete_char_backward as Action),
|
||||
(Delete, delete_char_forward),
|
||||
(Enter, insert_newline),
|
||||
]
|
||||
.iter()
|
||||
.chain(basic_nav);
|
||||
|
||||
Self {
|
||||
normal: normalish_keys.clone().into(),
|
||||
insert: insert_keys.clone().into(),
|
||||
visual: normalish_keys.clone().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ModeKeys {
|
||||
pub minor_modes: HashMap<Key, KeyMap>,
|
||||
pub map: KeyMap,
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for ModeKeys
|
||||
where
|
||||
T: IntoIterator<Item = &'a (Key, Action)>,
|
||||
{
|
||||
fn from(iter: T) -> Self {
|
||||
let minor_modes = HashMap::new();
|
||||
let mut map = KeyMap::new();
|
||||
for (key, action) in iter {
|
||||
map.insert(*key, Keybind::Action(*action));
|
||||
}
|
||||
|
||||
Self { minor_modes, map }
|
||||
}
|
||||
}
|
||||
|
||||
impl ModeKeys {
|
||||
pub fn apply_table(values: Table) -> Result<Self, String> {
|
||||
let mut keys = Self::default();
|
||||
|
||||
for (key, value) in values {
|
||||
let key = parse_key(&key)?;
|
||||
match value {
|
||||
Value::Table(table) => {
|
||||
let map = parse_key_map(table)?;
|
||||
keys.minor_modes.insert(key, map);
|
||||
}
|
||||
Value::String(keybind) => {
|
||||
let bind = Keybind::try_from(keybind.as_str())?;
|
||||
keys.map.insert(key, bind);
|
||||
}
|
||||
value => return Err(format!("Expected string or table but got {:?}", value)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Keybind {
|
||||
Action(Action),
|
||||
Command(String),
|
||||
}
|
||||
|
||||
impl TryFrom<Value> for Keybind {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: Value) -> Result<Self, String> {
|
||||
value
|
||||
.as_str()
|
||||
.ok_or_else(|| format!("Expected string but got {:?}", value))?
|
||||
.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Keybind {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(keybind: &str) -> Result<Self, String> {
|
||||
if keybind.starts_with(':') {
|
||||
Ok(Keybind::Command(keybind[1..].to_string()))
|
||||
} else {
|
||||
// TODO static once cell
|
||||
let actions = load_actions();
|
||||
actions
|
||||
.get(keybind)
|
||||
.map(|cb| Keybind::Action(cb.clone()))
|
||||
.ok_or_else(|| format!("Couldn't find action {:?}", keybind))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_key_map(values: Table) -> Result<KeyMap, String> {
|
||||
let mut map = KeyMap::with_capacity(values.len());
|
||||
|
||||
for (key, value) in values {
|
||||
let key = parse_key(&key)?;
|
||||
let bind = Keybind::try_from(value)?;
|
||||
map.insert(key, bind);
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
pub fn parse_key(key: &str) -> Result<Key, String> {
|
||||
use KeyCode::*;
|
||||
let key = match key {
|
||||
key if key.len() == 1 => Char(key.chars().nth(0).unwrap()),
|
||||
"backspace" => Backspace,
|
||||
"space" => Char(' '),
|
||||
"ret" => Enter,
|
||||
"minus" => Char('-'),
|
||||
"left" => Left,
|
||||
"right" => Right,
|
||||
"up" => Up,
|
||||
"down" => Down,
|
||||
"home" => Home,
|
||||
"end" => End,
|
||||
"pageup" => PageUp,
|
||||
"pagedown" => PageDown,
|
||||
"tab" => Tab,
|
||||
"del" => Delete,
|
||||
"ins" => Insert,
|
||||
"null" => Null,
|
||||
"esc" => Esc,
|
||||
key => return Err(format!("Unrecognized key name {:?}", key)),
|
||||
};
|
||||
|
||||
Ok(key)
|
||||
}
|
133
src/main.rs
133
src/main.rs
|
@ -31,17 +31,21 @@ use crossbeam_channel::Sender;
|
|||
use crossterm::{
|
||||
cursor,
|
||||
event::{Event, KeyCode, KeyEvent},
|
||||
terminal, Result, QueueableCommand,
|
||||
terminal, QueueableCommand, Result,
|
||||
};
|
||||
use keybinds::Keybind;
|
||||
use parking_lot::Mutex;
|
||||
use ropey::Rope;
|
||||
use yacexits::{exit, EX_DATAERR, EX_UNAVAILABLE};
|
||||
|
||||
mod actions;
|
||||
mod buffer;
|
||||
mod config;
|
||||
mod keybinds;
|
||||
mod theme;
|
||||
|
||||
use buffer::Buffer;
|
||||
use config::Config;
|
||||
use theme::StyleStore;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
|
@ -102,10 +106,12 @@ pub enum Direction {
|
|||
|
||||
pub struct State {
|
||||
pub styles: Arc<Mutex<StyleStore>>,
|
||||
pub config: Config,
|
||||
pub buffer: Buffer,
|
||||
pub cursor: Cursor,
|
||||
pub file: Option<OsString>,
|
||||
pub mode: Mode,
|
||||
pub last_saved: Rope,
|
||||
pub scroll: Cursor,
|
||||
pub size: (usize, usize),
|
||||
pub quit: bool,
|
||||
|
@ -116,15 +122,18 @@ impl State {
|
|||
pub fn from_str(file_name: Option<OsString>, text: &str) -> Result<Self> {
|
||||
let styles = Arc::new(Mutex::new(StyleStore::default()));
|
||||
let buffer = Buffer::from_str(styles.clone(), text);
|
||||
let last_saved = buffer.as_ref().clone();
|
||||
let (cols, rows) = terminal::size()?;
|
||||
let theme_tx = config::ThemeWatcher::spawn(styles.clone());
|
||||
|
||||
Ok(Self {
|
||||
styles,
|
||||
config: Default::default(),
|
||||
buffer,
|
||||
cursor: Cursor::default(),
|
||||
file: file_name,
|
||||
mode: Mode::default(),
|
||||
last_saved,
|
||||
scroll: Cursor::default(),
|
||||
size: (cols as usize, rows as usize),
|
||||
quit: false,
|
||||
|
@ -166,7 +175,9 @@ impl State {
|
|||
|
||||
// draw buffer
|
||||
let buffer_rows = if show_status_bar { rows - 1 } else { rows };
|
||||
let lr_width = self.buffer.draw(cols, buffer_rows, self.scroll, out)?;
|
||||
let lr_width = self
|
||||
.buffer
|
||||
.draw(cols, buffer_rows, self.scroll, self.cursor, out)?;
|
||||
|
||||
// draw cursor
|
||||
let cursor_pos = set_cursor_pos.unwrap_or_else(|| {
|
||||
|
@ -188,6 +199,13 @@ impl State {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the state to `Mode::Normal` with an error message.
|
||||
pub fn set_error(&mut self, error: impl ToString) {
|
||||
self.mode = Mode::Normal(NormalState {
|
||||
error: Some(error.to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn on_event(&mut self, event: Event) {
|
||||
match &self.mode {
|
||||
Mode::Normal(state) => self.on_normal_event(event, state.clone()),
|
||||
|
@ -200,28 +218,16 @@ impl State {
|
|||
fn on_normal_event(&mut self, event: Event, mut state: NormalState) {
|
||||
// reset the error from the last event
|
||||
state.error = None;
|
||||
self.mode = Mode::Normal(state);
|
||||
|
||||
match event {
|
||||
Event::Key(KeyEvent { code, .. }) => match code {
|
||||
KeyCode::Char(':') => {
|
||||
self.mode = Mode::Command(Default::default());
|
||||
Event::Key(KeyEvent { code, .. }) => {
|
||||
if let Some(keybind) = self.config.keybinds.visual.map.get(&code) {
|
||||
self.execute_keybind(keybind.clone());
|
||||
}
|
||||
KeyCode::Char('v') => {
|
||||
self.mode = Mode::Visual;
|
||||
}
|
||||
KeyCode::Char('i') => actions::insert_mode(self),
|
||||
KeyCode::Char('a') => actions::append_mode(self),
|
||||
KeyCode::Char('o') => actions::open_below(self),
|
||||
KeyCode::Char('O') => actions::open_above(self),
|
||||
code => self.on_any_key(code),
|
||||
},
|
||||
}
|
||||
event => self.on_any_event(event),
|
||||
}
|
||||
|
||||
match self.mode {
|
||||
Mode::Normal(_) => self.mode = Mode::Normal(state),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_command_event(&mut self, event: Event, mut state: CommandState) {
|
||||
|
@ -248,12 +254,16 @@ impl State {
|
|||
}
|
||||
KeyCode::Enter => {
|
||||
// TODO add to command history
|
||||
let result = self.queue_command(&state.buf);
|
||||
let result = self.execute_command(&state.buf);
|
||||
let error = result.err();
|
||||
self.mode = Mode::Normal(NormalState { error });
|
||||
return;
|
||||
}
|
||||
code => return self.on_any_key(code),
|
||||
KeyCode::Esc => {
|
||||
self.mode = Mode::default();
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
event => return self.on_any_event(event),
|
||||
}
|
||||
|
@ -263,43 +273,25 @@ impl State {
|
|||
|
||||
fn on_visual_event(&mut self, event: Event) {
|
||||
match event {
|
||||
Event::Key(KeyEvent { code, .. }) => match code {
|
||||
KeyCode::Esc => {
|
||||
self.mode = Mode::default();
|
||||
Event::Key(KeyEvent { code, .. }) => {
|
||||
if let Some(keybind) = self.config.keybinds.visual.map.get(&code) {
|
||||
self.execute_keybind(keybind.clone());
|
||||
}
|
||||
code => self.on_any_key(code),
|
||||
},
|
||||
}
|
||||
event => self.on_any_event(event),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_insert_event(&mut self, event: Event, state: InsertState) {
|
||||
fn on_insert_event(&mut self, event: Event, _state: InsertState) {
|
||||
match event {
|
||||
Event::Key(KeyEvent { code, .. }) => match code {
|
||||
KeyCode::Char(c) => {
|
||||
self.buffer.insert_char(self.cursor, c);
|
||||
self.move_cursor(Direction::Right)
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
self.move_cursor(Direction::Left);
|
||||
self.buffer.remove(self.cursor);
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
self.buffer.remove(self.cursor);
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
self.buffer.insert_char(self.cursor, '\n');
|
||||
self.cursor.line += 1;
|
||||
self.cursor.column = 0;
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
if state.append {
|
||||
self.move_cursor(Direction::Left);
|
||||
Event::Key(KeyEvent { code, .. }) => match self.config.keybinds.insert.map.get(&code) {
|
||||
Some(keybind) => self.execute_keybind(keybind.clone()),
|
||||
None => {
|
||||
if let KeyCode::Char(c) = code {
|
||||
self.buffer.insert_char(self.cursor, c);
|
||||
self.move_cursor(Direction::Right);
|
||||
}
|
||||
|
||||
self.mode = Mode::default();
|
||||
}
|
||||
code => self.on_any_key(code),
|
||||
},
|
||||
event => self.on_any_event(event),
|
||||
}
|
||||
|
@ -310,28 +302,15 @@ impl State {
|
|||
Event::Resize(cols, rows) => {
|
||||
self.size = (cols as usize, rows as usize);
|
||||
}
|
||||
Event::Key(KeyEvent { code, .. }) => self.on_any_key(code),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_any_key(&mut self, code: KeyCode) {
|
||||
match code {
|
||||
KeyCode::Esc => self.mode = Mode::default(),
|
||||
KeyCode::Char('h') | KeyCode::Left => actions::move_char_left(self),
|
||||
KeyCode::Char('j') | KeyCode::Down => actions::move_line_down(self),
|
||||
KeyCode::Char('k') | KeyCode::Up => actions::move_line_up(self),
|
||||
KeyCode::Char('l') | KeyCode::Right => actions::move_char_right(self),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_buffer(&mut self, file: OsString) -> Result<()> {
|
||||
self.last_saved = self.buffer.as_ref().clone();
|
||||
let out = self.buffer.as_ref().bytes().collect::<Vec<u8>>();
|
||||
let mut handle = OpenOptions::new().write(true).open(file)?;
|
||||
|
||||
handle.write_all(out.as_slice())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -349,7 +328,19 @@ impl State {
|
|||
self.write_buffer(handle).map_err(|err| format!("{}", err))
|
||||
}
|
||||
|
||||
fn queue_command(&mut self, command: &str) -> std::result::Result<(), String> {
|
||||
fn execute_keybind(&mut self, keybind: Keybind) {
|
||||
match keybind {
|
||||
Keybind::Action(action) => action(self),
|
||||
Keybind::Command(command) => {
|
||||
let result = self.execute_command(&command);
|
||||
let error = result.err();
|
||||
self.mode = Mode::Normal(NormalState { error });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_command(&mut self, command: &str) -> std::result::Result<(), String> {
|
||||
let command_parts = command.split(' ').collect::<Vec<&str>>();
|
||||
|
||||
let command = match command_parts.get(0) {
|
||||
|
@ -360,7 +351,14 @@ impl State {
|
|||
let args = &command_parts[1..];
|
||||
|
||||
match *command {
|
||||
"q" => self.quit = true,
|
||||
"q!" => self.quit = true,
|
||||
"q" => {
|
||||
if self.last_saved == *self.buffer.as_ref() {
|
||||
self.quit = true;
|
||||
} else {
|
||||
return Err("Buffer is unsaved (add ! to override)".into());
|
||||
}
|
||||
}
|
||||
"w" => return self.write_command(command, args),
|
||||
"wq" => {
|
||||
self.write_command(command, args)?;
|
||||
|
@ -382,7 +380,8 @@ impl State {
|
|||
}
|
||||
|
||||
fn move_cursor(&mut self, direction: Direction) {
|
||||
self.buffer.move_cursor(&mut self.cursor, direction, true);
|
||||
let wrap = self.config.move_linewrap;
|
||||
self.buffer.move_cursor(&mut self.cursor, direction, wrap);
|
||||
self.scroll_to_cursor();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,92 +1,30 @@
|
|||
"ui.linenr" = "gray"
|
||||
"ui.cursorline" = {}
|
||||
"ui.linenr" = "black"
|
||||
"ui.linenr.selected" = "white"
|
||||
|
||||
"error" = "love"
|
||||
"error" = "red"
|
||||
|
||||
"attribute" = "iris"
|
||||
|
||||
"type" = "foam"
|
||||
# "type.builtin" = ""
|
||||
|
||||
"constructor" = "foam"
|
||||
|
||||
"comment" = { fg = "muted", modifiers = ["italic"]}
|
||||
"constant" = "foam"
|
||||
"constant.builtin" = "rose"
|
||||
"constant.character" = "gold"
|
||||
"constant.character.escape" = "pine"
|
||||
"constant.numeric" = "gold"
|
||||
"entity.name" = "rose"
|
||||
"invalid" = "love"
|
||||
"keyword" = "pine"
|
||||
"keyword.operator" = "subtle"
|
||||
"label" = "foam"
|
||||
"operator" = "subtle"
|
||||
"punctuation" = "subtle"
|
||||
"string" = "gold"
|
||||
# "string.regexp" = ""
|
||||
# "string.special" = ""
|
||||
# "string.special.path" = ""
|
||||
# "string.special.url" = ""
|
||||
# "string.special.symbol" = ""
|
||||
"variable" = "text"
|
||||
"variable.builtin" = "love"
|
||||
"variable.parameter" = "iris"
|
||||
|
||||
"markup.heading.marker" = "muted"
|
||||
"markup.heading" = { fg = "iris", modifiers = ["bold"] }
|
||||
"markup.heading.1" = { fg = "iris", modifiers = ["bold"] }
|
||||
"markup.heading.2" = { fg = "foam", modifiers = ["bold"] }
|
||||
"markup.heading.3" = { fg = "rose", modifiers = ["bold"] }
|
||||
"markup.heading.4" = { fg = "gold", modifiers = ["bold"] }
|
||||
"markup.heading.5" = { fg = "pine", modifiers = ["bold"] }
|
||||
"markup.heading.6" = { fg = "foam", modifiers = ["bold"] }
|
||||
# "markup.heading.completion" = ""
|
||||
# "markup.heading.hover" = ""
|
||||
"markup.list" = "muted"
|
||||
# "markup.list.unnumbered" = ""
|
||||
# "markup.list.numbered" = ""
|
||||
"markup.bold" = { modifiers = ["bold"] }
|
||||
"markup.italic" = { modifiers = ["italic"] }
|
||||
"markup.strikethrough" = { modifiers = ["crossed_out"] }
|
||||
"markup.link" = "iris"
|
||||
"markup.link.url" = { fg = "iris", underline = { color = "iris", style = "line" } }
|
||||
"markup.link.label" = "subtle"
|
||||
"markup.link.text" = "text"
|
||||
"markup.quote" = "subtle"
|
||||
"markup.raw" = "subtle"
|
||||
# "markup.raw.inline" = {}
|
||||
# "markup.raw.inline.completion" = {}
|
||||
# "markup.raw.inline.hover" = {}
|
||||
# "markup.raw.block" = {}
|
||||
# "markup.normal" = ""
|
||||
# "markup.normal.completion" = ""
|
||||
# "markup.normal.hover" = ""
|
||||
|
||||
"diff" = "overlay"
|
||||
"diff.plus" = "foam"
|
||||
"diff.minus" = "love"
|
||||
"diff.delta" = "highlight_high"
|
||||
# "diff.delta.moved" = ""
|
||||
|
||||
[palette]
|
||||
base = "#191724"
|
||||
surface = "#1f1d2e"
|
||||
overlay = "#26233a"
|
||||
muted = "#6e6a86"
|
||||
subtle = "#908caa"
|
||||
text = "#e0def4"
|
||||
love = "#eb6f92"
|
||||
love_10 = "#311f30"
|
||||
gold = "#f6c177"
|
||||
gold_10 = "#30282c"
|
||||
rose = "#ebbcba"
|
||||
rose_10 = "#2f2834"
|
||||
pine = "#31748f"
|
||||
pine_10 = "#1a2030"
|
||||
foam = "#9ccfd8"
|
||||
foam_10 = "#252937"
|
||||
iris = "#c4a7e7"
|
||||
iris_10 = "#2b2539"
|
||||
highlight_low = "#21202e"
|
||||
highlight_med = "#403d52"
|
||||
highlight_high = "#524f67"
|
||||
"comment" = { fg = "black", modifiers = ["italics"] }
|
||||
"constant" = "yellow"
|
||||
"constant.character.escape" = "light-cyan"
|
||||
"constant.language" = "magenta"
|
||||
"constant.numeric" = "blue"
|
||||
"entity.name" = "blue"
|
||||
"entity.other.attribute-name" = "red"
|
||||
"entity.other.inherited-class" = "red"
|
||||
"invalid.deprecated" = "red"
|
||||
"invalid" = "red"
|
||||
"keyword" = "magenta"
|
||||
"keyword.control" = "magenta"
|
||||
"keyword.declaration" = "blue"
|
||||
"keyword.operator" = "blue"
|
||||
"meta.path" = "blue"
|
||||
"punctuation" = "gray"
|
||||
"punctuation.definition.string.begin" = "green"
|
||||
"punctuation.definition.string.end" = "green"
|
||||
"storage" = "magenta"
|
||||
"storage.type" = "magenta"
|
||||
"string" = "green"
|
||||
"support" = "yellow"
|
||||
"variable" = "green"
|
||||
"variable.language" = "magenta"
|
||||
|
|
Reference in New Issue