Compare commits

..

1 Commits

Author SHA1 Message Date
b1078f17eb
began writing it properly 2023-04-28 14:25:34 +01:00
20 changed files with 144 additions and 435 deletions

20
.replit
View File

@ -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"]

View File

@ -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.

View File

@ -1,9 +0,0 @@
{ pkgs }: {
deps = [
pkgs.rustc
pkgs.rustfmt
pkgs.cargo
pkgs.cargo-edit
pkgs.rust-analyzer
];
}

5
src/core.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod editor;
mod buffer;
mod languages;
mod config;
mod files;

View File

@ -1,37 +1,61 @@
use std::path::PathBuf; use std::error::Error;
use crate::core::languages::Language;
use crate::core::files;
#[derive(PartialEq)] enum BufferKind {
pub enum BufferKind {
Scratch,
Write,
Read, Read,
Write,
Scratch,
} }
impl BufferKind { impl BufferKind {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
BufferKind::Scratch => "*Scratch*", BufferKind::Read => "read",
BufferKind::Write => "Write", BufferKind::Write => "write",
BufferKind::Read => "Read", BufferKind::Scratch => "*scratch*",
} }
} }
} }
pub struct Buffer<'a> { // An file that is open in the editor
pub data: Vec<String>, pub struct Buffer {
pub path: PathBuf, kind: BufferKind,
pub kind: BufferKind, lines: Vec<String>,
pub name: &'a str, filename: Option<String>,
language: Language,
} }
impl<'a> Buffer<'a> { impl Buffer {
pub fn new(path: PathBuf, name: &'a str, kind: BufferKind) -> Self { pub fn new(path: Option<String>) -> Result<Self, Box<dyn Error>> {
// Return a buffer // If the file exists, read it into the buffer
Self { if let Some(path) = path {
data: vec![String::from("")], // Read the file
path, 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, kind,
name, lines,
filename: Some(path),
language,
})
} else {
Ok(Self {
kind: BufferKind::Scratch,
lines: Vec::new(),
filename: None,
language: Language::PlainText,
})
} }
} }
} }

19
src/core/config.rs Normal file
View File

@ -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.",
}
}
}

View File

@ -1,55 +1,22 @@
use std::path::PathBuf;
use crate::core::buffer::Buffer; use crate::core::buffer::Buffer;
use crate::core::buffer::BufferKind; use crate::core::config::Config;
use std::error::Error;
pub struct Config<'a> { pub struct Editor {
pub logo: &'a str, buffers: Vec<Buffer>,
pub friendly_name: &'a str, config: Config,
} }
impl<'a> Config<'a> { impl Editor {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
logo: "λ", buffers: Vec::new(),
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<Buffer<'a>>,
pub cursors: Vec<i32>,
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 {
config: Config::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<dyn Error>> {
self.buffers.push(Buffer::new(None)?);
Ok(())
}
} }

9
src/core/files.rs Normal file
View File

@ -0,0 +1,9 @@
use std::error::Error;
pub fn read_file(path: String) -> Result<Vec<String>, Box<dyn Error>> {
let contents: String = std::fs::read_to_string(path)?;
let lines: Vec<String> = contents.lines()
.map(|s| s.to_owned())
.collect();
Ok(lines)
}

31
src/core/languages.rs Normal file
View File

@ -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,
}
}
}

View File

@ -1,2 +0,0 @@
pub mod editor;
pub mod buffer;

View File

@ -1,25 +1,6 @@
mod core; mod core;
mod terminal;
mod tui;
use std::{env, path::PathBuf};
fn main() { fn main() {
// Collect command line arguments let mut editor = core::editor::Editor::new();
let args: Vec<String> = env::args().collect(); editor.attach_file().unwrap();
// 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 velocity
tui::ui::start(&mut screen, velocity);
} }

View File

@ -1 +0,0 @@
pub mod screen;

View File

@ -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<Self, ErrorKind> {
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<Self, ErrorKind> {
// 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);
}
}

View File

@ -1,2 +0,0 @@
pub mod statusbar;
pub mod welcome;

View File

@ -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(())
}
}

View File

@ -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 <file> to open a file and edit",
"Type :q! or <C-c> 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));
}
}
}

View File

@ -1,3 +0,0 @@
pub mod ui;
pub mod utils;
mod components;

View File

@ -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();
}

View File

@ -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()
}