Compare commits
1 Commits
production
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
e74c5ec014 |
20
.replit
Normal file
20
.replit
Normal file
@ -0,0 +1,20 @@
|
||||
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"]
|
@ -1,18 +0,0 @@
|
||||
# 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.
|
9
replit.nix
Normal file
9
replit.nix
Normal file
@ -0,0 +1,9 @@
|
||||
{ pkgs }: {
|
||||
deps = [
|
||||
pkgs.rustc
|
||||
pkgs.rustfmt
|
||||
pkgs.cargo
|
||||
pkgs.cargo-edit
|
||||
pkgs.rust-analyzer
|
||||
];
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
pub mod editor;
|
||||
mod buffer;
|
||||
mod languages;
|
||||
mod config;
|
||||
mod files;
|
@ -1,61 +1,37 @@
|
||||
use std::error::Error;
|
||||
use crate::core::languages::Language;
|
||||
use crate::core::files;
|
||||
use std::path::PathBuf;
|
||||
|
||||
enum BufferKind {
|
||||
Read,
|
||||
Write,
|
||||
#[derive(PartialEq)]
|
||||
pub enum BufferKind {
|
||||
Scratch,
|
||||
Write,
|
||||
Read,
|
||||
}
|
||||
|
||||
impl BufferKind {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
BufferKind::Read => "read",
|
||||
BufferKind::Write => "write",
|
||||
BufferKind::Scratch => "*scratch*",
|
||||
BufferKind::Scratch => "*Scratch*",
|
||||
BufferKind::Write => "Write",
|
||||
BufferKind::Read => "Read",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An file that is open in the editor
|
||||
pub struct Buffer {
|
||||
kind: BufferKind,
|
||||
lines: Vec<String>,
|
||||
filename: Option<String>,
|
||||
language: Language,
|
||||
pub struct Buffer<'a> {
|
||||
pub data: Vec<String>,
|
||||
pub path: PathBuf,
|
||||
pub kind: BufferKind,
|
||||
pub name: &'a str,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn new(path: Option<String>) -> Result<Self, Box<dyn Error>> {
|
||||
// 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,
|
||||
})
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
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.",
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,55 @@
|
||||
use std::path::PathBuf;
|
||||
use crate::core::buffer::Buffer;
|
||||
use crate::core::config::Config;
|
||||
use std::error::Error;
|
||||
use crate::core::buffer::BufferKind;
|
||||
|
||||
pub struct Editor {
|
||||
buffers: Vec<Buffer>,
|
||||
config: Config,
|
||||
pub struct Config<'a> {
|
||||
pub logo: &'a str,
|
||||
pub friendly_name: &'a str,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
impl<'a> Config<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
buffers: Vec::new(),
|
||||
config: Config::new(),
|
||||
logo: "λ",
|
||||
friendly_name: "Velocity",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attach_file(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
self.buffers.push(Buffer::new(None)?);
|
||||
Ok(())
|
||||
#[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(),
|
||||
buffer: Box::new(Buffer::new(path, buffer_name, buffer_kind)),
|
||||
cursors: Vec::from([0]),
|
||||
mode: Mode::Normal,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
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)
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
2
src/core/mod.rs
Normal file
2
src/core/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod editor;
|
||||
pub mod buffer;
|
23
src/main.rs
23
src/main.rs
@ -1,6 +1,25 @@
|
||||
mod core;
|
||||
mod terminal;
|
||||
mod tui;
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
let mut editor = core::editor::Editor::new();
|
||||
editor.attach_file().unwrap();
|
||||
// Collect command line arguments
|
||||
let args: Vec<String> = 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 velocity
|
||||
tui::ui::start(&mut screen, velocity);
|
||||
}
|
1
src/terminal/mod.rs
Normal file
1
src/terminal/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod screen;
|
154
src/terminal/screen.rs
Normal file
154
src/terminal/screen.rs
Normal file
@ -0,0 +1,154 @@
|
||||
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);
|
||||
}
|
||||
}
|
2
src/tui/components/mod.rs
Normal file
2
src/tui/components/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod statusbar;
|
||||
pub mod welcome;
|
63
src/tui/components/statusbar.rs
Normal file
63
src/tui/components/statusbar.rs
Normal file
@ -0,0 +1,63 @@
|
||||
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(())
|
||||
}
|
||||
}
|
39
src/tui/components/welcome.rs
Normal file
39
src/tui/components/welcome.rs
Normal file
@ -0,0 +1,39 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
3
src/tui/mod.rs
Normal file
3
src/tui/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
mod components;
|
38
src/tui/ui.rs
Normal file
38
src/tui/ui.rs
Normal file
@ -0,0 +1,38 @@
|
||||
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();
|
||||
}
|
14
src/tui/utils.rs
Normal file
14
src/tui/utils.rs
Normal file
@ -0,0 +1,14 @@
|
||||
// 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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user