├── icon.png ├── src ├── core │ ├── mod.rs │ ├── debug.rs │ ├── prompt.rs │ └── exec.rs ├── edit │ ├── mod.rs │ ├── delete.rs │ ├── selection.rs │ ├── invert.rs │ ├── insert.rs │ └── buffer.rs ├── caret │ ├── mod.rs │ ├── position.rs │ ├── motion.rs │ └── movement.rs ├── state │ ├── mod.rs │ ├── mode.rs │ ├── cursor.rs │ ├── options.rs │ └── editor.rs ├── io │ ├── mod.rs │ ├── redraw.rs │ ├── key_state.rs │ ├── file.rs │ ├── key.rs │ ├── parse.rs │ └── graphics.rs └── main.rs ├── manifest ├── .gitignore ├── Cargo.toml ├── .travis.yml ├── README.md ├── LICENSE ├── TODO.md └── help.txt /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redox-os/sodium/HEAD/icon.png -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | /// Primitives for debugging. 2 | #[macro_use] 3 | pub mod debug; 4 | 5 | /// Executing commands. 6 | pub mod exec; 7 | 8 | /// The command prompt. 9 | pub mod prompt; 10 | -------------------------------------------------------------------------------- /manifest: -------------------------------------------------------------------------------- 1 | name=Sodium 2 | binary=/usr/bin/sodium 3 | icon=/ui/icons/sodium.png 4 | accept=*_REDOX 5 | accept=*.md 6 | accept=*.asm 7 | accept=*.rs 8 | accept=*.txt 9 | accept=*.list 10 | author=Ticki 11 | description=An editor inspired by Vim 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.lock 2 | *.bin 3 | *.gen 4 | *.list 5 | *.log 6 | *.o 7 | *.so 8 | *.pcap 9 | *.rlib 10 | *.vdi 11 | *.orig 12 | *.local 13 | *.img 14 | *.iso 15 | .directory 16 | .DS_Store 17 | libc/build/ 18 | setup/rust/ 19 | build 20 | target 21 | *.swp 22 | doc 23 | -------------------------------------------------------------------------------- /src/edit/mod.rs: -------------------------------------------------------------------------------- 1 | /// The text buffer. 2 | pub mod buffer; 3 | /// Delete text, defined by a motion. 4 | pub mod delete; 5 | /// Insertion of text. 6 | pub mod insert; 7 | /// "Invertion" of text. 8 | pub mod invert; 9 | /// Selection through motions. 10 | pub mod selection; 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sodium" 3 | version = "0.1.0" 4 | authors = ["Ticki"] 5 | description = """ 6 | Sodium: A modern vi-like editor 7 | """ 8 | 9 | [dependencies] 10 | orbclient = "0.3" 11 | 12 | [features] 13 | default = ["orbital"] 14 | orbital = [] 15 | ansi = [] 16 | -------------------------------------------------------------------------------- /src/caret/mod.rs: -------------------------------------------------------------------------------- 1 | /// Motions. 2 | /// 3 | /// A motion is a command defining some movement from point A to point B, these can be used in 4 | /// mulitple context, for example as argument for other commands. 5 | pub mod motion; 6 | /// Movement. 7 | pub mod movement; 8 | /// Calculations and bounding of positions. 9 | pub mod position; 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | sudo: required 5 | install: 6 | - sudo add-apt-repository -y ppa:zoogie/sdl2-snapshots 7 | - sudo apt-get update -qq 8 | - sudo apt-get install -qq libsdl2-dev 9 | script: 10 | - cargo build --features orbital --verbose 11 | - cargo test --features orbital --verbose 12 | notifications: 13 | email: false 14 | -------------------------------------------------------------------------------- /src/state/mod.rs: -------------------------------------------------------------------------------- 1 | /// Cursors. 2 | /// 3 | /// A cursor contains various non-global information about the editor state. You can switch between 4 | /// cursor, for reusing older editor states. 5 | pub mod cursor; 6 | /// The global editor state. 7 | pub mod editor; 8 | /// Editor modes. 9 | pub mod mode; 10 | /// Options and configuration of the editor. 11 | pub mod options; 12 | -------------------------------------------------------------------------------- /src/io/mod.rs: -------------------------------------------------------------------------------- 1 | /// Loading and writing files. 2 | pub mod file; 3 | /// Graphics and rendering. 4 | pub mod graphics; 5 | /// Key input and parsing. 6 | pub mod key; 7 | /// The "key state" of the editor. 8 | /// 9 | /// The key state contains information about the current state of modifiers. 10 | pub mod key_state; 11 | /// Parsing of input commands. 12 | pub mod parse; 13 | /// Partial redraws. 14 | pub mod redraw; 15 | -------------------------------------------------------------------------------- /src/io/redraw.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | #[derive(Clone)] 4 | /// A task for the renderer for redrawing 5 | pub enum RedrawTask { 6 | /// None redraw task. 7 | None, 8 | /// Redraw a range of lines. 9 | Lines(Range), 10 | /// Redraw the lines after a given line. 11 | LinesAfter(usize), 12 | /// Full screen redraw. 13 | Full, 14 | /// Status bar redraw. 15 | StatusBar, 16 | /// Move cursor. 17 | Cursor((usize, usize), (usize, usize)), 18 | } 19 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Sodium is a next generation Vi-like editor. 2 | 3 | #![feature(stmt_expr_attributes)] 4 | #![deny(missing_docs)] 5 | 6 | #[cfg(feature = "orbital")] 7 | extern crate orbclient; 8 | 9 | /// Core functionality. 10 | #[macro_use] 11 | pub mod core; 12 | /// Carret primitives. 13 | pub mod caret; 14 | /// Editing. 15 | pub mod edit; 16 | /// Input/Output 17 | pub mod io; 18 | /// State of the editor. 19 | pub mod state; 20 | 21 | fn main() { 22 | self::state::editor::Editor::init(); 23 | } 24 | -------------------------------------------------------------------------------- /src/core/debug.rs: -------------------------------------------------------------------------------- 1 | /// Write to console if the debug option is set (with newline) 2 | #[macro_export] 3 | macro_rules! debugln { 4 | ($e:expr, $($arg:tt)*) => ({ 5 | if $e.options.debug { 6 | println!($($arg)*); 7 | } 8 | }); 9 | } 10 | 11 | /// Write to console if the debug option is set 12 | #[macro_export] 13 | macro_rules! debug { 14 | ($e:expr, $($arg:tt)*) => ({ 15 | if $e.options.debug { 16 | print!($($arg)*); 17 | } 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Sodium: Vim 2.0 5 | 6 | **Sodium** is an editor inspired by Vim (but not a clone). It aims to be efficient, fast, and productive. 7 | 8 | [![Travis Build Status](https://travis-ci.org/redox-os/sodium.svg?branch=master)](https://travis-ci.org/redox-os/sodium) 9 | 10 | ### Library Requirements 11 | 12 | Sodium requires the sdl2 library in order to build. 13 | To install on Ubuntu, use the following command: `sudo apt-get install libsdl2-dev` 14 | 15 | ### Build 16 | 17 | Use `cargo run --features orbital` in order to build the program. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ticki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/edit/delete.rs: -------------------------------------------------------------------------------- 1 | use edit::buffer::TextBuffer; 2 | use io::redraw::RedrawTask; 3 | use state::cursor::Cursor; 4 | use state::editor::Editor; 5 | 6 | impl Editor { 7 | /// Delete a character. 8 | #[inline] 9 | pub fn delete(&mut self) { 10 | let &Cursor { x, y, .. } = self.cursor(); 11 | self.buffers.current_buffer_info_mut().dirty = true; 12 | if x == self.buffers.current_buffer()[y].len() { 13 | if y + 1 < self.buffers.current_buffer().len() { 14 | let s = self.buffers.current_buffer_mut().remove_line(y + 1); 15 | self.buffers.current_buffer_mut()[y].push_str(&s); 16 | self.redraw_task = RedrawTask::Lines(y..y + 1); 17 | } 18 | } else if x < self.buffers.current_buffer()[y].len() { 19 | self.buffers.current_buffer_mut()[y].remove(x); 20 | self.redraw_task = RedrawTask::LinesAfter(y); 21 | } 22 | 23 | self.hint(); 24 | } 25 | 26 | /// Backspace. 27 | #[inline] 28 | pub fn backspace(&mut self) { 29 | let previous = self.previous(1); 30 | self.buffers.current_buffer_info_mut().dirty = true; 31 | if let Some(p) = previous { 32 | self.goto(p); 33 | self.delete(); 34 | } else { 35 | self.status_bar.msg = "Can't delete file start".to_owned(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/edit/selection.rs: -------------------------------------------------------------------------------- 1 | use edit::buffer::TextBuffer; 2 | use state::editor::Editor; 3 | 4 | impl Editor { 5 | /// Remove from a given motion (row based), i.e. if the motion given is to another line, all 6 | /// the lines from the current one to the one defined by the motion are removed. If the motion 7 | /// defines a position on the same line, only the characters from the current position to the 8 | /// motion's position are removed. 9 | pub fn remove_rb<'a>(&mut self, (x, y): (isize, isize)) { 10 | if y == (self.y() as isize) { 11 | let (x, y) = self.bound((x as usize, y as usize), false); 12 | // Single line mode 13 | let (a, b) = if self.x() > x { 14 | (x, self.x()) 15 | } else { 16 | (self.x(), x) 17 | }; 18 | for _ in self.buffers.current_buffer_mut()[y].drain(a..b) {} 19 | } else { 20 | let (_, y) = self.bound((x as usize, y as usize), true); 21 | // Full line mode 22 | let (a, b) = if self.y() < y { 23 | (self.y(), y) 24 | } else { 25 | (y, self.y()) 26 | }; 27 | 28 | // TODO: Make this more idiomatic (drain) 29 | for _ in a..(b + 1) { 30 | if self.buffers.current_buffer().len() > 1 { 31 | self.buffers.current_buffer_mut().remove_line(a); 32 | } else { 33 | self.buffers.current_buffer_mut()[0] = String::new(); 34 | } 35 | } 36 | } 37 | 38 | self.hint(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/edit/invert.rs: -------------------------------------------------------------------------------- 1 | use state::editor::Editor; 2 | 3 | impl Editor { 4 | /// Invert n characters next to the cursor in the buffer. 5 | pub fn invert_chars(&mut self, n: usize) { 6 | for _ in 0..n { 7 | let (x, y) = self.pos(); 8 | let current = self.current(); 9 | 10 | if let Some(cur) = current { 11 | self.buffers.current_buffer_mut()[y].remove(x); 12 | self.buffers.current_buffer_mut()[y].insert(x, invert(cur)); 13 | } 14 | if let Some(m) = self.next(1) { 15 | self.goto(m); 16 | } 17 | } 18 | 19 | self.hint(); 20 | } 21 | } 22 | 23 | /// "Invert" a character, meaning that it gets swapped with it's counterpart, if no counterpart 24 | /// exists, swap the case of the character. 25 | pub fn invert(c: char) -> char { 26 | match c { 27 | '<' => '>', 28 | '>' => '<', 29 | '&' => '|', 30 | '*' => '/', 31 | '(' => ')', 32 | ')' => '(', 33 | '+' => '-', 34 | '-' => '+', 35 | ';' => ':', 36 | ':' => ';', 37 | '\\' => '/', 38 | '/' => '\\', 39 | ',' => '.', 40 | '.' => ',', 41 | '\'' => '"', 42 | '"' => '\'', 43 | '[' => ']', 44 | ']' => '[', 45 | '{' => '}', 46 | '}' => '{', 47 | '!' => '?', 48 | '?' => '!', 49 | a => { 50 | if a.is_lowercase() { 51 | a.to_uppercase().next().unwrap_or('?') 52 | } else { 53 | a.to_lowercase().next().unwrap_or('?') 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/io/key_state.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "orbital")] 2 | use io::key::Key; 3 | #[cfg(feature = "orbital")] 4 | use orbclient::KeyEvent; 5 | 6 | #[cfg(feature = "ansi")] 7 | use std::io::prelude::*; 8 | #[cfg(feature = "ansi")] 9 | use std::io::Stdin; 10 | 11 | /// Key state 12 | pub struct KeyState { 13 | /// Ctrl modifier. 14 | pub ctrl: bool, 15 | /// Alt modifier. 16 | pub alt: bool, 17 | /// Shift modifier. 18 | pub shift: bool, 19 | } 20 | 21 | impl KeyState { 22 | /// Create a new default key state. 23 | pub fn new() -> KeyState { 24 | KeyState { 25 | ctrl: false, 26 | alt: false, 27 | shift: false, 28 | } 29 | } 30 | 31 | /// Feed the keystate with a new key input. 32 | #[cfg(feature = "orbital")] 33 | pub fn feed(&mut self, k: KeyEvent) -> Option { 34 | use orbclient::{K_ALT, K_CTRL, K_LEFT_SHIFT, K_RIGHT_SHIFT}; 35 | 36 | let c = k.character; 37 | match c { 38 | '\0' => { 39 | // "I once lived here" - bug 40 | match k.scancode { 41 | K_ALT => self.alt = k.pressed, 42 | K_CTRL => self.ctrl = k.pressed, 43 | K_LEFT_SHIFT | K_RIGHT_SHIFT => self.shift = k.pressed, 44 | _ if k.pressed => { 45 | return Some(Key::from_event(k)); 46 | } 47 | _ => {} 48 | } 49 | } 50 | _ if k.pressed => { 51 | return Some(Key::from_event(k)); 52 | } 53 | _ => {} 54 | } 55 | 56 | None 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/io/file.rs: -------------------------------------------------------------------------------- 1 | use edit::buffer::{SplitBuffer, TextBuffer}; 2 | use state::editor::{Buffer, Editor}; 3 | use std::fs::File; 4 | use std::io::{Read, Write}; 5 | 6 | /// The status of a file IO operation. 7 | pub enum FileStatus { 8 | /// Oll fino. 9 | Ok, 10 | /// File not found. 11 | NotFound, 12 | /// Other error. 13 | Other, 14 | } 15 | 16 | impl Editor { 17 | /// Open a file. 18 | pub fn open(&mut self, path: &str) -> FileStatus { 19 | if let Some(mut file) = File::open(path).ok() { 20 | let mut con = String::new(); 21 | let _ = file.read_to_string(&mut con); 22 | 23 | if con.is_empty() { 24 | con.push('\n'); 25 | } 26 | 27 | let mut new_buffer: Buffer = SplitBuffer::from_str(&con).into(); 28 | new_buffer.title = Some(path.into()); 29 | 30 | let new_buffer_index = self.buffers.new_buffer(new_buffer); 31 | self.buffers.switch_to(new_buffer_index); 32 | self.hint(); 33 | FileStatus::Ok 34 | } else { 35 | FileStatus::NotFound 36 | } 37 | } 38 | 39 | /// Write the file. 40 | pub fn write<'a>(&'a mut self, path: &'a str) -> FileStatus { 41 | self.buffers.current_buffer_info_mut().title = Some(path.into()); 42 | if path == "" { 43 | return FileStatus::Other; 44 | } 45 | if let Some(mut file) = File::create(path).ok() { 46 | if file 47 | .write(self.buffers.current_buffer().to_string().as_bytes()) 48 | .is_ok() 49 | { 50 | FileStatus::Ok 51 | } else { 52 | FileStatus::Other 53 | } 54 | } else { 55 | FileStatus::NotFound 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [x] Make editor.pos method and use that instead of 2 | - [ ] Add word navigation 3 | - [ ] `.` command 4 | - [ ] More partial redrawing (register "is_modified") 5 | 6 | 7 | Known bugs: 8 | 9 | - [x] When using `t` with a char that isn't in the document, Sodium will crash. 10 | - [x] `dG` on the last line of the file deletes from the cursor to the end of the line, instead of the entire line. 11 | Not sure if intended. 12 | 13 | The bug causing these two bugs, is localised to be in position.rs. It resolves by returning a value one over bound x 14 | 15 | - [x] The x value is wrongly bounded. Reproduction: 16 | 1) Make two lines: 17 | - abc 18 | - abcdef 19 | 2) Go to the end of the first line. 20 | 3) Go one down. As you'll see you'll end up at d. That's right. 21 | 4) Now go two the end of the first line again. 22 | 5) Type 2l. 23 | 6) Now go one down 24 | 7) You'll end up on e, even though it should be d 25 | 26 | - [x] Crashes when: 27 | 1) Write abc on line 1 28 | 2) Press o to go to the next line 29 | 3) Go to normal mode 30 | 4) Press a and go to append mode 31 | 5) Type text 32 | 6) Out of bound (index) error 33 | 34 | - [x] When typing the first char in a line in normal insert mode, it wont go to the next char. 35 | 36 | - [x] The modifier keys are only working for one command 37 | Solutions: 38 | - Make a struct KeyState storing info on the modifiers active. Add a method `feed` which feeds the keystate with a key, updating it. This should Option, where a key should be returned iff the key entered was not a modifier 39 | 40 | - [ ] Crashes when ~ command is used on an empty line 41 | - [ ] `z` command is buggy. 42 | - [ ] `x` is buggy (when line length differ) 43 | 44 | Refactoring: 45 | - Organize into modules 46 | -------------------------------------------------------------------------------- /src/io/key.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "orbital")] 2 | use orbclient::{KeyEvent, K_BKSP, K_DOWN, K_ESC, K_LEFT, K_RIGHT, K_TAB, K_UP}; 3 | 4 | #[derive(Copy, Clone, PartialEq)] 5 | /// A key 6 | pub enum Key { 7 | /// Printable character. 8 | Char(char), 9 | // TODO: Space modifier? 10 | /// Backspace. 11 | Backspace, 12 | /// Escape. 13 | Escape, 14 | /// Left arrow key. 15 | Left, 16 | /// Right arrow key. 17 | Right, 18 | /// Up arrow key. 19 | Up, 20 | /// Down arrow key. 21 | Down, 22 | /// Tab. 23 | Tab, 24 | /// Null/unknown key. 25 | Null, 26 | /// Quit (close the window). 27 | Quit, 28 | /// Unknown key. 29 | Unknown(u8), 30 | } 31 | 32 | impl Key { 33 | /// Convern an Orbital key event to a `Key`. 34 | #[cfg(feature = "orbital")] 35 | pub fn from_event(k: KeyEvent) -> Key { 36 | if k.pressed { 37 | match k.scancode { 38 | K_BKSP => Key::Backspace, 39 | K_LEFT => Key::Left, 40 | K_RIGHT => Key::Right, 41 | K_UP => Key::Up, 42 | K_DOWN => Key::Down, 43 | K_TAB => Key::Tab, 44 | K_ESC => Key::Escape, 45 | s => match k.character { 46 | '\0' => Key::Unknown(s), 47 | c => Key::Char(c), 48 | }, 49 | } 50 | } else { 51 | Key::Null 52 | } 53 | } 54 | 55 | /// Convert a `Key` to its corresponding character. If no corresponding character exists, use 56 | /// the null character. 57 | pub fn to_char(self) -> char { 58 | match self { 59 | Key::Char(c) => c, 60 | _ => '\0', 61 | } 62 | } 63 | } 64 | 65 | #[derive(Copy, Clone, PartialEq)] 66 | /// A command, i.e. a key together with information on the modifiers. 67 | pub struct Cmd { 68 | /// The key associated with the command. 69 | pub key: Key, 70 | } 71 | -------------------------------------------------------------------------------- /src/state/mode.rs: -------------------------------------------------------------------------------- 1 | use edit::insert::InsertOptions; 2 | 3 | #[derive(Clone, PartialEq, Copy)] 4 | /// A mode. Modes determine which set of commands that will be used. Modes comes in two flavors: 5 | pub enum Mode { 6 | /// A primitive mode. In this mode type, absolutely none preprocessing of the commands are 7 | /// done. Therefore the instruction will just be a command, without any form of numeral 8 | /// parameter. This is useful for modes such as insert, where commands don't take numeral 9 | /// parameters. 10 | Primitive(PrimitiveMode), 11 | /// Command mode. In this mode type input is collected into instructions, which are commands 12 | /// having a numeral parameter. This numeral parameter is useful for a number of things, such 13 | /// as repeation, line number, etc. 14 | Command(CommandMode), 15 | } 16 | 17 | impl Mode { 18 | /// Convert the mode to string 19 | pub fn to_string(self) -> &'static str { 20 | use self::CommandMode::*; 21 | use self::Mode::*; 22 | use self::PrimitiveMode::*; 23 | match self { 24 | Command(Normal) => "Normal", 25 | Primitive(Insert(_)) => "Insert", 26 | Primitive(Prompt) => "Prompt", 27 | } 28 | } 29 | } 30 | 31 | #[derive(Clone, PartialEq, Copy)] 32 | /// A command mode 33 | pub enum CommandMode { 34 | // Visual(VisualOptions), 35 | /// Normal mode. The default mode, which can be used for most common commands and switching to 36 | /// other modes. 37 | Normal, 38 | } 39 | 40 | #[derive(Clone, PartialEq, Copy)] 41 | /// A primitive mode 42 | pub enum PrimitiveMode { 43 | /// Insert mode. In this text is inserted 44 | Insert(InsertOptions), 45 | /// Prompt. In the prompt the user can give the editor commands, which often are more 46 | /// "sentence-like", i.e. they're not like commands in normal mode for example. These commands 47 | /// can be used for a number of things, such as configurating Sodium, or enabling/disabling 48 | /// options. 49 | Prompt, 50 | } 51 | -------------------------------------------------------------------------------- /src/caret/position.rs: -------------------------------------------------------------------------------- 1 | use edit::buffer::TextBuffer; 2 | use state::editor::Editor; 3 | 4 | /// Convert a usize tuple to isize 5 | pub fn to_signed_pos((x, y): (usize, usize)) -> (isize, isize) { 6 | (x as isize, y as isize) 7 | } 8 | 9 | impl Editor { 10 | /// Get the position of the current cursor, bounded 11 | #[inline] 12 | pub fn pos(&self) -> (usize, usize) { 13 | let cursor = self.cursor(); 14 | self.bound((cursor.x, cursor.y), false) 15 | } 16 | 17 | #[inline] 18 | /// Get the X coordinate of the current cursor (bounded) 19 | pub fn x(&self) -> usize { 20 | self.pos().0 21 | } 22 | 23 | #[inline] 24 | /// Get the Y coordinate of the current cursor (bounded) 25 | pub fn y(&self) -> usize { 26 | self.pos().1 27 | } 28 | 29 | /// Convert a position value to a bounded position value 30 | #[inline] 31 | pub fn bound(&self, (x, mut y): (usize, usize), tight: bool) -> (usize, usize) { 32 | y = if y >= self.buffers.current_buffer().len() { 33 | self.buffers.current_buffer().len() - 1 34 | } else { 35 | y 36 | }; 37 | 38 | let ln = self.buffers.current_buffer()[y].len() + if tight { 0 } else { 1 }; 39 | if x >= ln { 40 | if ln == 0 { 41 | (0, y) 42 | } else { 43 | (ln - 1, y) 44 | } 45 | } else { 46 | (x, y) 47 | } 48 | } 49 | 50 | /// Bound horizontally, i.e. don't change the vertical axis only make sure that the horizontal 51 | /// axis is bounded. 52 | #[inline] 53 | pub fn bound_hor(&self, (x, y): (usize, usize), tight: bool) -> (usize, usize) { 54 | (self.bound((x, y), tight).0, y) 55 | } 56 | /// Bound vertically, i.e. don't change the horizontal axis only make sure that the vertical 57 | /// axis is bounded. 58 | #[inline] 59 | pub fn bound_ver(&self, (x, mut y): (usize, usize)) -> (usize, usize) { 60 | // Is this premature optimization? Yes, yes it is! 61 | y = if y > self.buffers.current_buffer().len() - 1 { 62 | self.buffers.current_buffer().len() - 1 63 | } else { 64 | y 65 | }; 66 | 67 | (x, y) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/state/cursor.rs: -------------------------------------------------------------------------------- 1 | use state::editor::Editor; 2 | use state::mode::{CommandMode, Mode}; 3 | 4 | #[derive(Clone)] 5 | /// A cursor, i.e. a state defining a mode, and a position. The cursor does not define the content 6 | /// of the current file. 7 | pub struct Cursor { 8 | /// The x coordinate of the cursor 9 | pub x: usize, 10 | /// The y coordinate of the cursor 11 | pub y: usize, 12 | /// The mode of the cursor 13 | pub mode: Mode, 14 | } 15 | 16 | impl Cursor { 17 | /// Create a new default cursor 18 | pub fn new() -> Cursor { 19 | Cursor { 20 | x: 0, 21 | y: 0, 22 | mode: Mode::Command(CommandMode::Normal), 23 | } 24 | } 25 | } 26 | 27 | impl Editor { 28 | /// Get the character under the cursor 29 | #[inline] 30 | pub fn current(&self) -> Option { 31 | let (x, y) = self.pos(); 32 | match self.buffers.current_buffer()[y].chars().nth(x) { 33 | Some(c) => Some(c), 34 | None => None, 35 | } 36 | } 37 | 38 | /// Get the current cursor 39 | #[inline] 40 | pub fn cursor(&self) -> &Cursor { 41 | let buffer = self.buffers.current_buffer_info(); 42 | buffer.cursors.get(buffer.current_cursor as usize).unwrap() 43 | } 44 | 45 | /// Get the current cursor mutably 46 | #[inline] 47 | pub fn cursor_mut(&mut self) -> &mut Cursor { 48 | let buffer = self.buffers.current_buffer_info_mut(); 49 | buffer 50 | .cursors 51 | .get_mut(buffer.current_cursor as usize) 52 | .unwrap() 53 | } 54 | 55 | /// Go to next cursor 56 | #[inline] 57 | pub fn next_cursor(&mut self) { 58 | let buffer = self.buffers.current_buffer_info_mut(); 59 | buffer.current_cursor = 60 | (buffer.current_cursor.wrapping_add(1)) % (buffer.cursors.len() as u8); 61 | } 62 | 63 | /// Go to previous cursor 64 | #[inline] 65 | pub fn prev_cursor(&mut self) { 66 | let buffer = self.buffers.current_buffer_info_mut(); 67 | if buffer.current_cursor != 0 { 68 | buffer.current_cursor -= 1; 69 | } else { 70 | buffer.current_cursor = (buffer.cursors.len() - 1) as u8; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/state/options.rs: -------------------------------------------------------------------------------- 1 | /// Editor options. 2 | pub struct Options { 3 | /// Autoindent on line breaks? 4 | pub autoindent: bool, 5 | /// Debug mode. 6 | pub debug: bool, 7 | /// Highlight. 8 | pub highlight: bool, 9 | /// Line marker (dimmed background of the current line). 10 | pub line_marker: bool, 11 | /// enables read-only mode 12 | pub readonly: bool, 13 | /// Enable linenumbers 14 | pub line_numbers: bool, 15 | } 16 | 17 | impl Options { 18 | /// Create new default options 19 | pub fn new() -> Self { 20 | Options { 21 | autoindent: true, 22 | debug: true, // TODO: Let this be `true` only in debug compilation cfg 23 | highlight: true, 24 | line_marker: true, 25 | readonly: false, 26 | line_numbers: false, 27 | } 28 | } 29 | 30 | /// Get the given option as a mutable reference 31 | pub fn get_mut(&mut self, name: &str) -> Option<&mut bool> { 32 | match name { 33 | "autoindent" | "ai" => Some(&mut self.autoindent), 34 | "debug" | "debug_mode" => Some(&mut self.debug), 35 | "highlight" | "hl" => Some(&mut self.highlight), 36 | "line_marker" | "linemarker" | "linemark" | "lm" => Some(&mut self.line_marker), 37 | "readonly" | "ro" => Some(&mut self.readonly), 38 | "line_numbers" | "ln" => Some(&mut self.line_numbers), 39 | _ => None, 40 | } 41 | } 42 | 43 | /// Get a given option 44 | pub fn get(&self, name: &str) -> Option { 45 | match name { 46 | "autoindent" | "ai" => Some(self.autoindent), 47 | "debug" | "debug_mode" => Some(self.debug), 48 | "highlight" | "hl" => Some(self.highlight), 49 | "line_marker" | "linemarker" | "linemark" | "lm" => Some(self.line_marker), 50 | "readonly" | "ro" => Some(self.readonly), 51 | "line_numbers" | "ln" => Some(self.line_numbers), 52 | _ => None, 53 | } 54 | } 55 | 56 | /// Set a given option (mark it as active) 57 | pub fn set(&mut self, name: &str) -> Result<(), ()> { 58 | match self.get_mut(name) { 59 | Some(x) => { 60 | *x = true; 61 | Ok(()) 62 | } 63 | None => Err(()), 64 | } 65 | } 66 | /// Unset a given option (mark it as inactive) 67 | pub fn unset(&mut self, name: &str) -> Result<(), ()> { 68 | match self.get_mut(name) { 69 | Some(x) => { 70 | *x = false; 71 | Ok(()) 72 | } 73 | None => Err(()), 74 | } 75 | } 76 | /// Toggle a given option 77 | pub fn toggle(&mut self, name: &str) -> Result<(), ()> { 78 | match self.get_mut(name) { 79 | Some(x) => { 80 | *x = !*x; 81 | Ok(()) 82 | } 83 | None => Err(()), 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/caret/motion.rs: -------------------------------------------------------------------------------- 1 | use caret::position::to_signed_pos; 2 | use edit::buffer::TextBuffer; 3 | use io::parse::Inst; 4 | use state::editor::Editor; 5 | 6 | impl Editor { 7 | /// Convert an instruction to a motion (new coordinate). Returns None if the instructions given 8 | /// either is invalid or has no movement. 9 | /// 10 | /// A motion is a namespace (i.e. non mode-specific set of commands), which represents 11 | /// movements. These are useful for commands which takes a motion as post-parameter, such as d. 12 | /// d deletes the text given by the motion following. Other commands can make use of motions, 13 | /// using this method. 14 | pub fn to_motion(&mut self, Inst(n, cmd): Inst) -> Option<(usize, usize)> { 15 | use io::key::Key::*; 16 | 17 | let y = self.y(); 18 | 19 | match cmd.key { 20 | Char('h') => Some(self.left(n.d())), 21 | Char('l') => Some(self.right(n.d(), true)), 22 | Char('j') => Some(self.down(n.d())), 23 | Char('k') => Some(self.up(n.d())), 24 | Char('g') => Some((0, n.or(1) - 1)), 25 | Char('G') => Some((0, self.buffers.current_buffer().len() - 1)), 26 | Char('L') => Some((self.buffers.current_buffer()[y].len() - 1, y)), 27 | Char('H') => Some((0, y)), 28 | Char('t') => { 29 | let ch = self.get_char(); 30 | 31 | if let Some(o) = self.next_ocur(ch, n.d()) { 32 | Some((o, y)) 33 | } else { 34 | None 35 | } 36 | } 37 | Char('f') => { 38 | let ch = self.get_char(); 39 | 40 | if let Some(o) = self.previous_ocur(ch, n.d()) { 41 | Some((o, y)) 42 | } else { 43 | None 44 | } 45 | } 46 | Char(c) => { 47 | self.status_bar.msg = format!("Motion not defined: '{}'", c); 48 | self.redraw_status_bar(); 49 | None 50 | } 51 | _ => { 52 | self.status_bar.msg = format!("Motion not defined"); 53 | None 54 | } 55 | } 56 | } 57 | /// Like to_motion() but does not bound to the text. Therefore it returns an isize, and in some 58 | /// cases it's a position which is out of bounds. This is useful when commands want to mesure 59 | /// the relative movement over the movement. 60 | pub fn to_motion_unbounded(&mut self, Inst(n, cmd): Inst) -> Option<(isize, isize)> { 61 | use io::key::Key::*; 62 | 63 | let y = self.y(); 64 | 65 | match cmd.key { 66 | Char('h') => Some(self.left_unbounded(n.d())), 67 | Char('l') => Some(self.right_unbounded(n.d())), 68 | Char('j') => Some(self.down_unbounded(n.d())), 69 | Char('k') => Some(self.up_unbounded(n.d())), 70 | Char('g') => Some((0, n.or(1) as isize - 1)), 71 | Char('G') => Some(( 72 | self.buffers.current_buffer()[y].len() as isize, 73 | self.buffers.current_buffer().len() as isize - 1, 74 | )), 75 | Char('L') => Some(to_signed_pos((self.buffers.current_buffer()[y].len(), y))), 76 | Char('H') => Some((0, y as isize)), 77 | Char('t') => { 78 | let ch = self.get_char(); 79 | 80 | if let Some(o) = self.next_ocur(ch, n.d()) { 81 | Some(to_signed_pos((o, y))) 82 | } else { 83 | None 84 | } 85 | } 86 | Char('f') => { 87 | let ch = self.get_char(); 88 | 89 | if let Some(o) = self.previous_ocur(ch, n.d()) { 90 | Some(to_signed_pos((o, y))) 91 | } else { 92 | None 93 | } 94 | } 95 | _ => None, 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/edit/insert.rs: -------------------------------------------------------------------------------- 1 | use edit::buffer::TextBuffer; 2 | use io::key::Key; 3 | use io::redraw::RedrawTask; 4 | use state::editor::Editor; 5 | 6 | #[derive(Clone, PartialEq, Copy)] 7 | /// The type of the insert mode 8 | pub enum InsertMode { 9 | /// Insert text (before the cursor) 10 | Insert, 11 | /// Replace text (on the cursor) 12 | Replace, 13 | } 14 | 15 | #[derive(Clone, PartialEq, Copy)] 16 | /// The insert options 17 | pub struct InsertOptions { 18 | /// The mode type 19 | pub mode: InsertMode, 20 | } 21 | 22 | impl Editor { 23 | /// Insert text under the current cursor. 24 | pub fn insert(&mut self, k: Key, InsertOptions { mode }: InsertOptions) { 25 | let (mut x, mut y) = self.pos(); 26 | self.buffers.current_buffer_info_mut().dirty = true; 27 | match (mode, k) { 28 | (InsertMode::Insert, Key::Char('\n')) => { 29 | let first_part = self.buffers.current_buffer()[y][..x].to_owned(); 30 | let second_part = self.buffers.current_buffer()[y][x..].to_owned(); 31 | 32 | self.buffers.current_buffer_mut()[y] = first_part; 33 | 34 | let nl = if self.options.autoindent { 35 | self.buffers.current_buffer().get_indent(y).to_owned() 36 | } else { 37 | String::new() 38 | }; 39 | let begin = nl.len(); 40 | 41 | self.buffers 42 | .current_buffer_mut() 43 | .insert_line(y + 1, nl + &second_part); 44 | 45 | self.redraw_task = RedrawTask::LinesAfter(y); 46 | self.goto((begin, y + 1)); 47 | } 48 | (InsertMode::Insert, Key::Backspace) => self.backspace(), 49 | (InsertMode::Insert, Key::Tab) => { 50 | for i in 0..4 { 51 | self.buffers.current_buffer_mut()[y].insert(x + i, ' '); 52 | } 53 | self.redraw_task = RedrawTask::Lines(y..y + 1); 54 | let right = self.right(4, false); 55 | self.goto(right); 56 | } 57 | (InsertMode::Insert, Key::Char(c)) => { 58 | self.buffers.current_buffer_mut()[y].insert(x, c); 59 | 60 | self.redraw_task = RedrawTask::Lines(y..y + 1); 61 | let right = self.right(1, false); 62 | self.goto(right); 63 | } 64 | (InsertMode::Replace, Key::Char(c)) => { 65 | if x == self.buffers.current_buffer()[y].len() { 66 | let next = self.next(1); 67 | if let Some(p) = next { 68 | self.goto(p); 69 | x = self.x(); 70 | y = self.y(); 71 | } 72 | } 73 | 74 | if self.buffers.current_buffer_mut().len() != y { 75 | if self.buffers.current_buffer()[y].len() == x { 76 | let next = self.next(1); 77 | if let Some(p) = next { 78 | self.goto(p); 79 | } 80 | } else { 81 | self.buffers.current_buffer_mut()[y].remove(x); 82 | self.buffers.current_buffer_mut()[y].insert(x, c); 83 | } 84 | } 85 | let next = self.next(1); 86 | if let Some(p) = next { 87 | self.goto(p); 88 | } 89 | self.redraw_task = RedrawTask::Lines(y..y + 1); 90 | } 91 | _ => {} 92 | } 93 | 94 | self.hint(); 95 | } 96 | 97 | /// Insert a string 98 | pub fn insert_str(&mut self, txt: String, opt: InsertOptions) { 99 | for c in txt.chars() { 100 | self.insert(Key::Char(c), opt); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /help.txt: -------------------------------------------------------------------------------- 1 | Sodium: A modern editor for the Redox OS 2 | ======================================== 3 | by Ticki et al. 4 | 5 | Sodium is a editor inspired by a various 6 | console based, keyboard-centric editor. 7 | The keybindings are loosly based on Vim. 8 | 9 | This is a small guide for using Sodium. 10 | 11 | Sodium consists of three different types 12 | of command sets. 13 | 14 | 1) Global commands. These are possible to 15 | use anywhere, in any mode. 16 | 2) Mode-specific commands. These are 17 | specific to a given mode. 18 | 3) Namespaces. Commands that can be 19 | invoked as input (after) certain 20 | commands. 21 | 22 | The modes are of two types: 23 | - Command mode: In command mode a command 24 | can be preceeded by a numeral. This 25 | numeral is called a parameter, and can 26 | mean various things for the command. 27 | - Primitive mode: In this type of mode 28 | the keys are given directly to the 29 | handler without any form of parsing. 30 | 31 | Global commands 32 | --------------- 33 | 34 | - [alt][space]: Go to the next cursor. 35 | - [alt]: Move a given motion. 36 | - [shift][space]: Go back to normal mode. 37 | 38 | Modes 39 | ----- 40 | 41 | # Normal 42 | 43 | This is the default mode. Normal mode 44 | provides various commands. Normal mode 45 | is intended for doing commands which 46 | are often invoked and commands used to 47 | change modes. 48 | 49 | The following commands are valid in 50 | normal mode: 51 | 52 | NOTE: means repeat command 53 | times, unless otherwise 54 | stated. 55 | 56 | Basic motion: 57 | - h : Go left 58 | - j : Go down 59 | - k : Go up 60 | - l : Go right 61 | - J : Go 15 down 62 | - K : Go 15 up 63 | - H : Go to the start of 64 | the line 65 | - L : Go to the end of the 66 | line 67 | 68 | Navigation: 69 | - g : Go to line 70 | - g : Do 71 | - G : Go to the end of the document 72 | - t : Go to the next occurence 73 | of 74 | - f : Go to the previous occurence 75 | of 76 | 77 | Scrolling: 78 | - z : Scroll 79 | - z : Scroll to line 80 | - Z : Scroll to cursor 81 | 82 | Cursor management: 83 | - b : Branch the cursor 84 | - B : Delete the current cursor 85 | - [space] : Go to the next cursor 86 | 87 | Editing: 88 | - i : Go to insert mode 89 | - a : Go to insert (append) mode 90 | - r : Replace the current char 91 | with 92 | - R : Go to replace mode 93 | - x : Delete char 94 | - X : Backspace char 95 | - d : Delete a given selection 96 | (given by ) 97 | - o : Insert a new line 98 | - ~ : Switch the character under the cursor 99 | with its counterpart (if it has one). 100 | For example a -> A 101 | ( -> ) 102 | / -> \ 103 | - ; : Go to prompt mode 104 | - . : Repeat the previous command 105 | 106 | 107 | # Insert 108 | 109 | Insert text before the cursor. 110 | 111 | # Replace 112 | 113 | Replace the text under the cursor. 114 | 115 | # Prompt 116 | 117 | Prompt mode is a mode where you can 118 | invoke commands like in Vi(m). In this 119 | mode you can for example set/unset options. 120 | 121 | Following commands are valid: 122 | 123 | - set