diff --git a/.replit b/.replit deleted file mode 100644 index c71897c..0000000 --- a/.replit +++ /dev/null @@ -1,20 +0,0 @@ -run = "cargo run" -hidden = ["target"] - -[packager] -language = "rust" - -[packager.features] -packageSearch = true - -[languages.rust] -pattern = "**/*.rs" - -[languages.rust.languageServer] -start = "rust-analyzer" - -[nix] -channel = "stable-22_05" - -[gitHubImport] -requiredFiles = [".replit", "replit.nix"] diff --git a/docs/design/introduction.md b/docs/design/introduction.md new file mode 100644 index 0000000..9727a6b --- /dev/null +++ b/docs/design/introduction.md @@ -0,0 +1,18 @@ +# Introduction +Velocity is a fast and hackable text editor made for programming. It is split into two main parts, the backend and the frontend. +The backend, written in performant Rust, is the core of the editor and handles text manipulation, files, buffers, views, etc. +The frontend, which can be written in any language, is the UI of the editor and handles rendering, keybinds, etc. + +## Editor +The `struct Editor` is the main class of the backend. It contains the current state of the editor, the views, and the configuration. +The `Editor` is the main way to interact with the backend, and is used by the frontend to manipulate the editor. + +## View +The `struct View` defines a way of viewing a buffer, it contains information such as the current cursor locations, the scroll position, and the buffer + +## Buffer +The `struct Buffer` contains the text of a file, and is the main way of interacting with the text. It contains a `Vec` of `Line`s, which contain a `Vec` of `char`s. +Buffers can be `BufferKind::Read`, meaning the file is read-only, or `BufferKind::Write` allowing edits to be made to the file, or can be `BufferKind::Scratch` which are not associated with a file. + +## Line +The `struct Line` contains a `Vec` of `char`s, and is the main way of interacting with the text. It contains a `Vec` of `Line`s, which contain a `Vec` of `char`s. \ No newline at end of file diff --git a/TODO.md b/docs/help/help.md similarity index 100% rename from TODO.md rename to docs/help/help.md diff --git a/replit.nix b/replit.nix deleted file mode 100644 index f0e8e08..0000000 --- a/replit.nix +++ /dev/null @@ -1,9 +0,0 @@ -{ pkgs }: { - deps = [ - pkgs.rustc - pkgs.rustfmt - pkgs.cargo - pkgs.cargo-edit - pkgs.rust-analyzer - ]; -} \ No newline at end of file diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..3b33b55 --- /dev/null +++ b/src/core.rs @@ -0,0 +1,5 @@ +pub mod editor; +mod buffer; +mod languages; +mod config; +mod files; \ No newline at end of file diff --git a/src/core/buffer.rs b/src/core/buffer.rs index 7433d3d..bae8373 100644 --- a/src/core/buffer.rs +++ b/src/core/buffer.rs @@ -1,37 +1,61 @@ -use std::path::PathBuf; +use std::error::Error; +use crate::core::languages::Language; +use crate::core::files; -#[derive(PartialEq)] -pub enum BufferKind { - Scratch, - Write, +enum BufferKind { Read, + Write, + Scratch, } impl BufferKind { pub fn as_str(&self) -> &str { match self { - BufferKind::Scratch => "*scratch*", - BufferKind::Write => "write", BufferKind::Read => "read", + BufferKind::Write => "write", + BufferKind::Scratch => "*scratch*", } } } -pub struct Buffer<'a> { - pub data: Vec, - pub path: PathBuf, - pub kind: BufferKind, - pub name: &'a str, +// An file that is open in the editor +pub struct Buffer { + kind: BufferKind, + lines: Vec, + filename: Option, + language: Language, } -impl<'a> Buffer<'a> { - pub fn new(path: PathBuf, name: &'a str, kind: BufferKind) -> Self { - // Return a buffer - Self { - data: vec![String::from("")], - path, - kind, - name, +impl Buffer { + pub fn new(path: Option) -> Result> { + // If the file exists, read it into the buffer + if let Some(path) = path { + // Read the file + let lines = files::read_file(path.clone())?; + + // Get the language + let language = Language::get_language(&path); + + // Check if the file is writeable + let kind = if std::path::Path::new(&path).is_file() { + BufferKind::Write + } else { + BufferKind::Read + }; + + Ok(Self { + kind, + lines, + filename: Some(path), + language, + }) + } else { + Ok(Self { + kind: BufferKind::Scratch, + lines: Vec::new(), + filename: None, + language: Language::PlainText, + }) } } -} +} \ No newline at end of file diff --git a/src/core/config.rs b/src/core/config.rs new file mode 100644 index 0000000..cef51b7 --- /dev/null +++ b/src/core/config.rs @@ -0,0 +1,19 @@ +const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); + +pub struct Config { + name: &'static str, + version: &'static str, + logo: &'static str, + motto: &'static str, +} + +impl Config { + pub fn new() -> Self { + Self { + name: "Velocity", + version: VERSION.unwrap_or("UNKNOWN"), + logo: "λ", + motto: "Hackable text editor for nerds.", + } + } +} \ No newline at end of file diff --git a/src/core/editor.rs b/src/core/editor.rs index e3ea4a4..2cc1d96 100644 --- a/src/core/editor.rs +++ b/src/core/editor.rs @@ -1,55 +1,22 @@ -use std::path::PathBuf; use crate::core::buffer::Buffer; -use crate::core::buffer::BufferKind; +use crate::core::config::Config; +use std::error::Error; -pub struct Config<'a> { - pub logo: &'a str, - pub friendly_name: &'a str, +pub struct Editor { + buffers: Vec, + config: Config, } -impl<'a> Config<'a> { +impl Editor { pub fn new() -> Self { Self { - logo: "λ", - friendly_name: "Velocity", - } - } -} - -#[allow(dead_code)] -pub enum Mode { - Normal, - Insert, - Select, - Command, -} - -impl Mode { - pub fn as_str(&self) -> &str { - match self { - Mode::Normal => "NORMAL", - Mode::Insert => "INSERT", - Mode::Select => "SELECT", - Mode::Command => "COMMAND", - } - } -} - -pub struct Editor<'a> { - pub config: Config<'a>, - pub buffer: Box>, - pub cursors: Vec, - pub mode: Mode, -} - -impl<'a> Editor<'a> { - pub fn new(path: PathBuf, buffer_name: &'a str) -> Self { - let buffer_kind = if path.to_str().unwrap().len() > 1 { BufferKind::Write } else { BufferKind::Scratch }; - Editor { + buffers: Vec::new(), config: Config::new(), - buffer: Box::new(Buffer::new(path, buffer_name, buffer_kind)), - cursors: Vec::from([0]), - mode: Mode::Normal, } } -} + + pub fn attach_file(&mut self) -> Result<(), Box> { + self.buffers.push(Buffer::new(None)?); + Ok(()) + } +} \ No newline at end of file diff --git a/src/core/files.rs b/src/core/files.rs new file mode 100644 index 0000000..c2fb01c --- /dev/null +++ b/src/core/files.rs @@ -0,0 +1,9 @@ +use std::error::Error; + +pub fn read_file(path: String) -> Result, Box> { + let contents: String = std::fs::read_to_string(path)?; + let lines: Vec = contents.lines() + .map(|s| s.to_owned()) + .collect(); + Ok(lines) +} \ No newline at end of file diff --git a/src/core/languages.rs b/src/core/languages.rs new file mode 100644 index 0000000..0ce4216 --- /dev/null +++ b/src/core/languages.rs @@ -0,0 +1,31 @@ +pub enum Language { + Rust, + Python, + Markdown, + PlainText, +} + +impl Language { + pub fn as_str(&self) -> &str { + match self { + Language::Rust => "rust", + Language::Python => "python", + Language::Markdown => "markdown", + Language::PlainText => "plain text", + } + } + + pub fn get_language(filename: &str) -> Self { + let extension = std::path::Path::new(filename) + .extension() + .and_then(|s| s.to_str()) + .unwrap_or(""); + + match extension { + "rs" => Language::Rust, + "py" => Language::Python, + "md" => Language::Markdown, + _ => Language::PlainText, + } + } +} \ No newline at end of file diff --git a/src/core/mod.rs b/src/core/mod.rs deleted file mode 100644 index e9335de..0000000 --- a/src/core/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod editor; -pub mod buffer; diff --git a/src/main.rs b/src/main.rs index 87e7cd1..b93e80d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,6 @@ mod core; -mod terminal; -mod tui; -use std::{env, path::PathBuf}; fn main() { - // Collect command line arguments - let args: Vec = env::args().collect(); - - // Collect the file path - let file_path = if args.len() > 1 { PathBuf::from(&args[1]) } else { PathBuf::from("") }; - - // Collect the file name - let file_name = file_path.clone(); - let file_name = if args.len() > 1 { file_name.file_name().unwrap().to_str().unwrap() } else { "" }; - - // Initalise a new editor - let velocity = core::editor::Editor::new(file_path, file_name); - - // Initalise a screen - let mut screen = terminal::screen::Screen::new().unwrap(); - - // Begin lambda - tui::ui::start(&mut screen, velocity); -} + let mut editor = core::editor::Editor::new(); + editor.attach_file().unwrap(); +} \ No newline at end of file diff --git a/src/terminal/mod.rs b/src/terminal/mod.rs deleted file mode 100644 index fdf5b6f..0000000 --- a/src/terminal/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod screen; \ No newline at end of file diff --git a/src/terminal/screen.rs b/src/terminal/screen.rs deleted file mode 100644 index 2d32374..0000000 --- a/src/terminal/screen.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crossterm::cursor::{Hide, MoveTo, Show}; -use crossterm::style::Print; -use crossterm::terminal; -use crossterm::{execute, ErrorKind}; -use std::io::stdout; - -// Struct for holding coordinates -#[derive(Copy, Clone)] -pub struct Coords { - pub x: usize, - pub y: usize, -} - -// Creating a coordinates from two values -impl Coords { - pub fn from(x: usize, y: usize) -> Self { - Self { x, y } - } -} - -// A cursor for writing to the terminal screen -#[derive(Copy, Clone)] -pub struct Cursor { - pub position: Coords, - pub hidden: bool, -} - -// When a cursor is created -impl Cursor { - pub fn new() -> Result { - let mut cursor = Self { - position: Coords::from(0, 0), - hidden: true, - }; - Cursor::move_to(&mut cursor, Coords::from(0, 0)); - Cursor::hide(&mut cursor); - Ok(cursor) - } -} - -// Cursor methods -impl Cursor { - pub fn move_to(&mut self, position: Coords) { - // Set the new position of the cursor - self.position = position; - - // Move the cursor to the desired posiition in the terminal - execute!(stdout(), MoveTo(position.x as u16, position.y as u16)).unwrap(); - } - - pub fn hide(&mut self) { - // Remember that the cursor is hidden - self.hidden = true; - - // Hide the cursor from the terminal screen - execute!(stdout(), Hide).unwrap(); - } - - pub fn show(&mut self) { - // Remember that the cursor isn't hidden - self.hidden = false; - - // Show the cursor to the terminal screen - execute!(stdout(), Show).unwrap(); - } -} - -// A struct for holding the size of the terminal -pub struct Size { - pub width: usize, - pub height: usize, -} - -// The terminal screen -pub struct Screen { - pub size: Size, - pub cursor: Cursor, -} - -// For when a new terminal screen is created -impl Screen { - pub fn new() -> Result { - // Get the size of the terminal - let size = terminal::size()?; - - // Define a new terminal screen struct - let mut screen = Self { - size: Size { - width: size.0 as usize, - height: size.1 as usize, - }, - cursor: Cursor::new().unwrap(), - }; - - // Empty the terminal screen - Screen::clear(); - - // Enter the terminal screen - screen.enter(); - - // Return a result containing the terminal - Ok(screen) - } -} - -// Terminal functions and methods for managing the terminal -impl Screen { - pub fn refresh(&mut self) -> Result<(), ErrorKind>{ - // Clear the screen - Screen::clear(); - - // Update the screen dimensions - let size = terminal::size()?; - self.size.width = size.0 as usize; - self.size.height = size.1 as usize; - - // Return Ok if was successful - Ok(()) - } - - pub fn enter(&mut self) { - // Hide the cursor - self.cursor.hide(); - - // Enter the terminal screen - terminal::enable_raw_mode().unwrap(); - execute!(stdout(), terminal::EnterAlternateScreen).unwrap(); - } - - pub fn exit(&mut self) { - // Show the cursor - self.cursor.show(); - - // Exit the terminal screen - execute!(stdout(), terminal::LeaveAlternateScreen).unwrap(); - terminal::disable_raw_mode().unwrap(); - } - - pub fn clear() { - // Clears the terminal screen - execute!(stdout(), terminal::Clear(terminal::ClearType::All)).unwrap(); - } - - pub fn write(text: &str) { - // Writes a line to a current cursor position - execute!(stdout(), Print(text)).unwrap(); - } - - pub fn write_at(&mut self, text: String, position: Coords) { - // Writes a line at a set of coordinates - self.cursor.move_to(position); - Screen::write(&text); - } -} diff --git a/src/tui/components/mod.rs b/src/tui/components/mod.rs deleted file mode 100644 index 3bd63cf..0000000 --- a/src/tui/components/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod statusbar; -pub mod welcome; \ No newline at end of file diff --git a/src/tui/components/statusbar.rs b/src/tui/components/statusbar.rs deleted file mode 100644 index ab9d8bd..0000000 --- a/src/tui/components/statusbar.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crossterm::style::Stylize; -use crate::core::editor::Editor; -use crate::terminal::screen::{Coords, Screen}; -use crate::tui::utils; - -pub fn draw(screen: &mut Screen, editor: &Editor) -> Result<(), ()> { - // Calculate where to draw the status bar - let status_height = screen.size.height - 2; - - // Get the editor logo from the config - let editor_logo = &utils::with_spaces(editor.config.logo) as &str; - // Get the current mode into a string - let mode_string = &utils::with_spaces(editor.mode.as_str()) as &str; - // Get the current open file name - let file_name = &utils::with_spaces(editor.buffer.name) as &str; - // Get the current buffer kind - let buffer_kind = &utils::with_spaces(editor.buffer.kind.as_str()) as &str; - - // Calculate the total length of all the status bar components - let total_length = editor_logo.len() + mode_string.len() + file_name.len() + buffer_kind.len() + 1; - - // If the screen isn't wide enough, panic as we can't draw the status bar - if screen.size.width < total_length { - Err(()) - - } else { - // Write the editor logo - screen.write_at( - editor_logo.yellow().bold().reverse().to_string(), - Coords::from(0, status_height), - ); - - // Calculate where to write the current mode - let x = editor_logo.len() - 1; - // Write the current mode - screen.write_at( - mode_string.green().bold().reverse().to_string(), - Coords::from(x, status_height), - ); - - let x = x + mode_string.len(); - // Draws the file name if it has a length, if not then it will draw the buffer type - if editor.buffer.name.len() > 0 { - screen.write_at( - file_name.magenta().bold().reverse().to_string(), - Coords::from(x, status_height), - ); - } else { - screen.write_at( - buffer_kind.blue().bold().reverse().to_string(), - Coords::from(x, status_height), - ); - } - - let x = if editor.buffer.name.len() > 0 { x + file_name.len() } else { x + buffer_kind.len() }; - screen.write_at( - " ".repeat(screen.size.width - x).reverse().to_string(), - Coords::from(x, status_height), - ); - - Ok(()) - } -} diff --git a/src/tui/components/welcome.rs b/src/tui/components/welcome.rs deleted file mode 100644 index 7021387..0000000 --- a/src/tui/components/welcome.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crossterm::style::Stylize; -use crate::core::editor::Editor; -use crate::terminal::screen::{Coords, Screen}; -use crate::tui::utils; - -pub fn draw(screen: &mut Screen, editor: &Editor) { - // The welcome message - let title = format!("{} {}", editor.config.logo, editor.config.friendly_name); - let message: [&str; 5] = [ - "Hackable text editor for nerds", - "", - "Type :help to open the README.md document", - "Type :o to open a file and edit", - "Type :q! or to quit the editor", - ]; - - // If the screen is big enough, we can draw - if screen.size.width > utils::longest_element_in_vec(message.to_vec()) && screen.size.height > message.len() + 4 { - // The starting y position in the centre of the screen - let mut y = (screen.size.height / 2) - (message.len() / 2) - 2; - - // Calculate where to place the title - let x = utils::calc_centred_x(screen.size.width, title.len()); - - // Write the title to the screen - screen.write_at(title.yellow().to_string(), Coords::from(x, y)); - - for line in message { - // Each line has different width so requires a different x position to center it - let x = utils::calc_centred_x(screen.size.width, line.len()) ; - - // For each line we move downwards so increment y - y += 1; - - // Write the line to the screen at position (x, y) - screen.write_at(line.to_string(), Coords::from(x, y)); - } - } -} diff --git a/src/tui/mod.rs b/src/tui/mod.rs deleted file mode 100644 index f3c65f4..0000000 --- a/src/tui/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod ui; -pub mod utils; -mod components; \ No newline at end of file diff --git a/src/tui/ui.rs b/src/tui/ui.rs deleted file mode 100644 index 18fc400..0000000 --- a/src/tui/ui.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::core::editor::Editor; -use crate::core::buffer::BufferKind; -use crate::terminal::screen::Screen; -use crate::tui::components; -use crossterm::event::{read, Event, KeyCode, KeyEvent, KeyModifiers}; - -pub fn start(screen: &mut Screen, editor: Editor) { - // Main screen loop, runs until the program exits - loop { - // Refresh the screen - screen.refresh().unwrap(); - - // Draw the welcome message only if it is a scratch buffer - if editor.buffer.kind == BufferKind::Scratch { - components::welcome::draw(screen, &editor); - }; - - // Draw the status bar - components::statusbar::draw(screen, &editor).unwrap(); - - // Check for any key presses - match read().unwrap() { - Event::Key(KeyEvent { - code: KeyCode::Char('q'), - modifiers: KeyModifiers::CONTROL, - .. - }) => break, - Event::Key(KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::CONTROL, - .. - }) => break, - _ => (), - } - } - - screen.exit(); -} diff --git a/src/tui/utils.rs b/src/tui/utils.rs deleted file mode 100644 index d6721f5..0000000 --- a/src/tui/utils.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Surrounds a &str with spaces -pub fn with_spaces(text: &str) -> String { - format!(" {} ", text) -} - -// Calculates the starting x coordinate for centred text -pub fn calc_centred_x(screen_width: usize, item_length: usize) -> usize { - (screen_width / 2) - (item_length / 2) -} - -// Returns the longest element in a vector -pub fn longest_element_in_vec(elements: Vec<&str>) -> usize { - elements.iter().max_by_key(|x: &&&str| x.len()).unwrap().len() -}