#![allow(dead_code)]
use crate::tilde_range::parse_tilde_range;
use copypasta::{ClipboardContext, ClipboardProvider};
use rand::Rng;
use regex::Regex;
use std::cmp::Ordering;
use std::path::PathBuf;
use std::process::Command as ShellCommand;
use std::{env, fs, usize};
use which::which;
#[cfg(windows)]
fn is_root() -> bool {
use is_elevated;
is_elevated::is_elevated()
}
#[cfg(not(windows))]
fn is_root() -> bool {
use sudo;
sudo::check() == sudo::RunningAs::Root
}
use crate::command::{Command, CommandAction, CommandRegistry, CommandScope};
use crate::file_buffer::FileBuffer;
use crate::EditorState;
use std::sync::{Arc, Mutex};
pub fn register_commands(registry: &mut CommandRegistry) {
#[cfg(feature = "informational")]
{
registry.add_command(about_sued()); registry.add_command(command_list()); registry.add_command(help()); registry.add_command(document()); }
#[cfg(feature = "inputoutput")]
{
registry.add_command(save()); registry.add_command(open()); registry.add_command(reopen()); registry.add_command(show()); registry.add_command(copy()); registry.add_command(search()); }
#[cfg(feature = "cursor")]
{
registry.add_command(point()); registry.add_command(overtake()); registry.add_command(move_up()); registry.add_command(move_down()); }
#[cfg(feature = "transformations")]
{
registry.add_command(shift()); registry.add_command(indent()); registry.add_command(delete()); registry.add_command(substitute()); }
#[cfg(feature = "shell")]
{
registry.add_command(shell_command()); registry.add_command(shell_command_with_file());}
#[cfg(feature = "fun")]
{
registry.add_command(crash()); registry.add_command(nothing()); registry.add_command(butterfly()); }
#[cfg(feature = "lua")]
{
registry.add_command(eval()); registry.add_command(script()); }
#[cfg(debug_assertions)]
registry.add_command(test_commands()); }
pub fn test_commands() -> Command {
Command {
name: "test",
arguments: vec![],
description: "test every command in the registry",
documentation: "this command sorts every command available in the \
registry by alphabetical order and runs them all \
individually, each with a different input",
scope: CommandScope::REPLOnly,
action: CommandAction {
action: Arc::new(Mutex::new(|_args: Vec<&str>, state: &mut EditorState| {
let mut commands = state.registry.get_all_commands();
commands.sort();
let mut index = 0;
println!("{}", commands.len());
for command in commands {
if command.name == "test" {
println!("skipping over self - would cause infinite loop if locked");
continue;
}
index += 1;
println!("{}: {}", index, command.name);
let mut command_lock = command.action.action.lock().unwrap();
match command.name {
"bsod" => {
println!("not running bsod because why would you");
}
"show" | "delete" | "copy" | "indent" => {
state.buffer = FileBuffer::from(vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
]);
println!("testing ranged command {} with normal data", command.name);
command_lock(vec!["test", "1~3"], state);
state.buffer = FileBuffer::from(vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
]);
println!("testing ranged command {} with empty data", command.name);
command_lock(vec!["test"], state);
state.buffer = FileBuffer::from(vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
]);
println!("testing ranged command {} with invalid data", command.name);
command_lock(vec!["test", "invalid"], state);
}
"run" | "runhere" => {
println!(
"ignoring shell command {} because I know that works already",
command.name
);
}
_ => {
println!("testing command {} with text value", command.name);
command_lock(vec!["test", "testvalue"], state);
println!("testing command {} with number value", command.name);
command_lock(vec!["test", "123"], state);
println!("testing command {} with empty value", command.name);
command_lock(vec!["test"], state);
}
}
}
"done testing - all seems okay".to_string()
})),
},
}
}
pub fn startup_message() {
#[cfg(feature = "startup")]
let mut messages: Vec<&str> = vec![
"the shut up editor",
"the not standard text editor",
"it's pronounced \"soo ed\"",
"sued as in editor, not as in law",
"want a visual mode? ~runhere vim",
"what you get is what you get",
"what the frick is a config file",
"free software, hell yeah",
"put that mouse AWAY",
"it's got what plants crave!",
"it looks like you're editing text, would you like help?",
"more Easter eggs than there are features",
"write a Python script then ~runhere python",
#[cfg(feature = "lua")]
"write a Lua script then ~script this",
"it's a library, now, too",
"actually, real programmers use ~butterfly", "want features? edit the source code yourself",
"loop { let r = read()?; let e = eval(&r); println!(\"{e}\"); }",
#[cfg(feature = "lua")]
"~eval for i, line in ipairs(sued_buffer) do print(line) end",
"also try Astrion! it's got sued in it!", "no undo for you",
"in a world full of vscode, be an ed",
"notepad, but with less pad",
"yeah let's see sublime text do this",
"don't worry, you won't get emacs pinky here",
"i bet *your* text editor doesn't even ~runhere",
];
#[cfg(feature = "startup")]
let mut sudo_messages: Vec<&str> = vec![
"now i'm super sued!",
"i certainly hope you know what you're doing",
"this is concerning for several reasons",
"be careful while you're back there",
"why the frick do you need root access",
"please do not do that",
"no, this isn't how you're supposed to play the game",
"be grateful this is written in rust",
"freeman you fool!",
"you're doing it wrong",
"you might frick up your system, y'know",
"instead of root safeguards, we decided to mock you",
"aaaand now you're root, fantastic",
"the su in sued doesn't mean what you think it does",
];
#[cfg(feature = "startup")]
if is_root() {
messages.append(&mut sudo_messages);
}
#[cfg(feature = "startup")]
let message = messages[rand::thread_rng().gen_range(0..messages.len())];
let version = if cfg!(debug_assertions) {
let commit_hash = ShellCommand::new("git")
.args(&["rev-parse", "--short", "HEAD"])
.output()
.ok()
.and_then(|output| String::from_utf8(output.stdout).ok())
.unwrap_or_else(|| "unknown".to_string());
format!(
"{}-devel (c{})",
env!("CARGO_PKG_VERSION"),
commit_hash.trim()
)
} else {
env!("CARGO_PKG_VERSION").to_string()
};
let root_warning = if is_root() { " as root" } else { "" };
#[cfg(feature = "startup")]
{
if cfg!(debug_assertions) {
println!("sued{root_warning} v{version} - {message}\nthis is a development build, expect bugs and unexpected behaviour\ntype ~cmds or ~help for commands, otherwise just start typing");
} else {
println!("sued{root_warning} v{version} - {message}\ntype ~cmds or ~help for commands, otherwise just start typing");
}
}
#[cfg(not(feature = "startup"))]
{
if cfg!(debug_assertions) {
println!("sued{root_warning} v{version}\nthis is a development build, expect bugs and unexpected behaviour\ntype ~cmds or ~help for commands, otherwise just start typing");
} else {
println!("sued{root_warning} v{version}\ntype ~cmds or ~help for commands, otherwise just start typing");
}
}
}
pub fn command_list() -> Command {
Command {
name: "cmds",
arguments: vec![],
description: "list commands",
documentation: "this command only prints a list of command names \
separated by commas\n\
if you need detail, use {~}help instead",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(|_args: Vec<&str>, state: &mut EditorState| {
let mut commands = state.registry.get_all_commands();
commands.sort();
commands
.iter()
.map(|command| command.name)
.collect::<Vec<_>>()
.join(", ")
})),
},
}
}
pub fn document() -> Command {
Command {
name: "doc",
arguments: vec!["command"],
description: "retrieve the documentation for a command",
documentation: "this command simply retrieves the documentation for [command], \
without returning all the details that {~}help provides",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(|args: Vec<&str>, state: &mut EditorState| {
let command_name = args.get(1).map(|&s| s);
if let Some(name) = command_name {
let command = state.registry.get_command(name);
if command.is_none() {
return format!("{} is not a defined command", name);
}
let command = command.unwrap();
format!("documentation for {}:\n{}", name, command.documentation)
.replace("{~}", &state.prefix)
} else {
return "document what? try ~doc [command]".to_string();
}
})),
},
}
}
pub fn help() -> Command {
Command {
name: "help",
arguments: vec!["command"],
description: "list commands with descriptions",
documentation: "type {~}help on its own to list every command in a list, \
along with a description of each command\n\
type {~}help followed by a command name to get a \
description of that command\n\
if all you need is command names, use {~}cmds instead",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(|args: Vec<&str>, state: &mut EditorState| {
let command_name = args.get(1).map(|&s| s);
if let Some(name) = command_name {
let command = state.registry.get_command(name);
if command.is_none() {
return format!("{} is not a defined command", name);
}
let command = command.unwrap();
let arguments = if command.arguments.len() >= 1 {
command.arguments.join(", ")
} else {
"none".to_string()
};
let documentation = if command.documentation.len() > 0 {
format!("documentation:\n{}", command.documentation)
.replace("{~}", &state.prefix)
} else {
"this command has no documentation".to_string()
};
[
format!("{} - {}", command.name, command.description),
format!("arguments: {}", arguments),
format!("scope: {}", command.scope.to_string()),
documentation,
]
.join("\n")
} else {
let mut commands = state.registry.get_all_commands();
commands.sort();
let mut commands_string: String = "press up and down to navigate through command history\n\
type `{~}help [command]` to get information about a command\n\
all `range` arguments use tilde range syntax (X~, ~X, X~Y)\n\
key: command [arg1] [arg2] [...] - what the command does\n"
.to_string()
.replace("{~}", &state.prefix);
commands.iter().for_each(|command| {
if command.arguments.len() >= 1 {
let argues = format!("[{}]", command.arguments.join("] ["));
commands_string.push_str(&format!(
"{}{} {} - {}\n",
state.prefix, command.name, argues, command.description
));
} else {
commands_string.push_str(&format!(
"{}{} - {}\n",
state.prefix, command.name, command.description
));
}
});
commands_string.strip_suffix("\n").unwrap().to_string()
}
})),
},
}
}
pub fn about_sued() -> Command {
Command {
name: "about",
arguments: vec![],
description: "display about text",
documentation: "this command simply displays information about the sued \
text editor itself, with simple instructions and the \
author's name",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(|_args: Vec<&str>, _state: &mut EditorState| {
let version = if cfg!(debug_assertions) {
format!("{}-devel", env!("CARGO_PKG_VERSION"))
} else {
env!("CARGO_PKG_VERSION").to_string()
};
format!("this is sued, v{version}\n\
sued is a vector-oriented line editor, heavily inspired by the ed editor\n\
you can write text simply by typing, and use sued's extensive command set for editing\n\
editor commands are prefixed with a default prefix of ~, type ~help for a full list\n\
sued written by Arsalan \"Aeri\" Kazmi <sonicspeed848@gmail.com>")
})),
},
}
}
pub fn save() -> Command {
Command {
name: "save",
arguments: vec!["filename"],
description: "save the current file",
documentation: "if a file was previously opened (either with the {~}open \
command or passing it as an argument on startup) running \
{~}save without arguments will write the file contents \
to that file\n\
if [filename] is passed, it'll set the file path to that \
and then write the file contents to it, assuming nothing \
went wrong in the process",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(|args: Vec<&str>, state: &mut EditorState| {
let file_path = if let Some(name) = args.get(1) {
name.to_string()
} else {
match state.buffer.file_path {
Some(_) => state.buffer.file_path.clone().unwrap(),
None => return "no file to save\ntry ~save <filename>".to_string(),
}
};
if state.buffer.contents.is_empty() {
return "buffer empty - nothing to save".to_string();
}
let content = state.buffer.contents.join("\n");
let path = PathBuf::from(&file_path);
match fs::write(&path, content) {
Ok(_) => {
state.buffer.file_path = Some(file_path);
let file_size_display = get_file_size(&state);
format!("saved to {} with {}", &path.display(), file_size_display)
}
Err(error) => format!("couldn't save file to {}: {}", file_path, error),
}
})),
},
}
}
pub fn show() -> Command {
Command {
name: "show",
arguments: vec!["range", "line_numbers"],
description: "display the current file contents",
documentation: "with no arguments passed, this command will simply \
display the entire file to standard output\n\
if [range] is passed, the lines in that range will be \
displayed instead, for example {~}show 1~10 will display \
lines 1 through 10, inclusive\n\
if \"false\" is passed for [line_numbers] line numbers, \
will be omitted from the output",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(|args: Vec<&str>, state: &mut EditorState| {
let range = if args.len() >= 2 {
if let Some(range) = parse_tilde_range(&args[1], state) {
range
} else {
return format!("range is improperly formatted");
}
} else {
(1, state.buffer.contents.len())
};
let start_point: usize = range.0;
let end_point: usize = range.1;
let line_numbers: bool = match args.get(2) {
Some(arg) => arg.parse().unwrap_or(true),
None => true,
};
let mut listing: String = String::new();
if state.buffer.contents.is_empty() {
return format!("no buffer contents - nothing to show");
} else if let Err(_) = check_if_line_in_buffer(&state.buffer.contents, start_point)
{
return format!("invalid start point {}", start_point);
} else if let Err(_) = check_if_line_in_buffer(&state.buffer.contents, end_point) {
return format!("invalid end point {}", end_point);
} else {
let contents: Vec<String> =
state.buffer.contents[start_point - 1..end_point].to_vec();
let max_count_length: usize =
(start_point + contents.len() - 1).to_string().len();
for (index, line) in contents.iter().enumerate() {
let this_line = start_point + index - 1;
let mut sep = "│";
if this_line == state.cursor.y {
sep = "›";
}
if line_numbers {
let count: usize = start_point + index;
let count_padded: String =
format!("{:width$}", count, width = max_count_length);
listing.push_str(format!("{}{sep}{}\n", count_padded, line).as_str());
} else {
listing.push_str(format!("{}\n", line).as_str());
}
}
}
listing.strip_suffix("\n").unwrap().to_string()
})),
},
}
}
pub fn open_file(file_path: &str) -> Result<String, String> {
let path_exists = PathBuf::from(file_path).exists();
let file_exists = fs::read_to_string(file_path);
let is_dir = path_exists
&& match fs::metadata(file_path) {
Ok(metadata) => metadata.is_dir(),
Err(_) => false,
};
match file_exists {
Ok(contents) => {
return Ok(contents);
}
Err(e) => {
if is_dir {
println!("{} is a directory - will open as text", file_path);
let listings: Vec<String> = if let Ok(listings) = fs::read_dir(file_path) {
listings
.map(|f| f.unwrap().path().display().to_string())
.collect()
} else {
return Err(format!("failed to open directory {}", file_path));
};
return Ok(listings.join("\n"));
}
let error_specifier: &str;
match e.kind() {
std::io::ErrorKind::NotFound => {
error_specifier = "not found";
}
std::io::ErrorKind::PermissionDenied => {
error_specifier = "can't be opened";
}
std::io::ErrorKind::InvalidData => {
error_specifier = "is not text";
}
_ => {
error_specifier = "failed to open";
}
}
return Err(format!(
"file {} {}: {}",
file_path,
error_specifier,
e.to_string()
));
}
}
}
fn div_thousand(num: f32, repetitions: usize) -> f32 {
let mut n = num;
for _ in 0..repetitions {
n /= 1000.0;
}
n
}
pub fn get_file_size(state: &EditorState) -> String {
let file_size = state
.buffer
.contents
.iter()
.fold(0, |acc, line| acc + line.len());
const NL: usize = 0; const KB: usize = 10_u32.pow(3) as usize;
const MB: usize = 10_u32.pow(6) as usize;
const GB: usize = 10_u32.pow(9) as usize;
const OL: usize = usize::MAX; match file_size {
NL..KB => format!("{} bytes", file_size),
KB..MB => format!("{} KB", div_thousand(file_size as f32, 1)),
MB..GB => format!("{} MB", div_thousand(file_size as f32, 2)),
GB..OL => format!("{} GB", div_thousand(file_size as f32, 3)),
_ => "like, a lot of bytes".to_string(), }
}
pub fn open() -> Command {
Command {
name: "open",
arguments: vec!["file_path"],
description: "open a file or directory",
documentation: "loads the contents of the file at [file_path] into the \
buffer\n\
if a path to a dirctory is opened instead, it'll open the \
directory listing as text\n\
this command will set the buffer's file path for the \
{~}save command",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(|args: Vec<&str>, state: &mut EditorState| {
let file_path = match args.get(1) {
Some(arg) => arg,
None => "",
};
let contents = match open_file(file_path) {
Ok(contents) => contents,
Err(e) => return e,
};
state.buffer.contents = contents.split("\n").map(String::from).collect();
state.buffer.file_path = Some(file_path.to_string());
let file_size_display = get_file_size(&state);
format!("opened {} as text with {}", file_path, file_size_display)
})),
},
}
}
pub fn reopen() -> Command {
Command {
name: "reopen",
arguments: vec![],
description: "reopen the current file",
documentation: "reopens the file from the file path stored in the buffer\n\
if no file is open, this command will do nothing",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(|_args: Vec<&str>, state: &mut EditorState| {
if let Some(file_path) = &state.buffer.file_path {
let contents = match open_file(file_path) {
Ok(contents) => contents,
Err(e) => return e,
};
state.buffer.contents = contents.split("\n").map(String::from).collect();
let file_size_display = get_file_size(&state);
format!("opened {} as text with {}", file_path, file_size_display)
} else {
"no file path stored; try ~open-ing one".to_string()
}
})),
},
}
}
pub fn check_if_line_in_buffer(
file_buffer: &Vec<String>,
line_number: usize,
) -> Result<(), String> {
if line_number < 1 {
return Err(format!("invalid line {}", line_number));
}
if file_buffer.is_empty() {
return Err("no buffer contents".to_string());
}
if line_number <= file_buffer.len() {
return Ok(());
}
Err(format!("no line {}", line_number))
}
fn set_cursor_position(args: Vec<&str>, state: &mut EditorState, overtake: bool) -> String {
match state.buffer.contents.len() {
0 => return "no buffer contents".to_string(),
1 => return "buffer contains only one line".to_string(),
_ => (),
}
if args.len() < 2 {
return format!(
"point where? try {}point [position], between 1 and {}",
state.prefix,
state.buffer.contents.len()
);
}
let is_relative = args[1].starts_with("+") || args[1].starts_with("-");
let position = match args[1].parse::<isize>() {
Ok(pos) => {
if is_relative {
pos + state.cursor.y as isize + 1
} else {
pos
}
}
Err(e) => match args[1] {
"start" => 1,
"end" => state.buffer.contents.len() as isize + 1,
_ => return format!("invalid position {}: {}", args[1], e),
},
};
if let Err(msg) = check_if_line_in_buffer(&state.buffer.contents, position as usize) {
if position != state.buffer.contents.len() as isize + 1 {
return msg;
}
}
state.cursor.y = position as usize - 1;
let message: String;
if position == state.buffer.contents.len() as isize + 1 || position <= 1 {
message = format!("set cursor position to {position}");
} else {
if overtake {
let line_contents = state.buffer.contents[state.cursor.y].clone();
state.buffer.contents.remove(state.cursor.y);
state.existing_content = line_contents;
message = format!("overtook line {position} and set cursor position");
} else {
let max_length: usize = state.buffer.contents.len().to_string().len();
let line_number_padded = format!("{:width$}", state.cursor.y, width = max_length);
let output_line = format!(
"{}│{}",
line_number_padded,
state.buffer.contents[state.cursor.y - 1]
);
message = format!("set cursor position to {position}\n{output_line}");
}
}
message
}
pub fn point() -> Command {
Command {
name: "point",
arguments: vec!["position"],
description: "set the cursor position on the y axis non-destructively",
documentation: "this command will set the cursor position to a specific \
[position], setting the cursor to the specified line number
so that any text typed will be inserted at that point\n\
text entered after moving the cursor will be inserted\n\
into a new line before the cursor position, moving the \
line down without overwriting it\n\
after setting the cursor position, the line's contents \
will be displayed with the corresponding line number\n\
specify a relative position (+n or -n) to move the cursor \
down or up by n lines\n\
if no [position] is passed, it defaults to the end of the \
file buffer",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, state: &mut EditorState| {
set_cursor_position(args, state, false)
},
)),
},
}
}
pub fn overtake() -> Command {
Command {
name: "overtake",
arguments: vec!["position"],
description: "set the cursor position on the y axis destructively",
documentation: "this command sets the cursor position to a specific [position], \
like {~}point, but additionally deletes the line at [position], \
effectively overtaking the line (hence the name)\n\
after moving to the new line, the original line contents will be \
set in the line editor to be manually edited\n\
see {~}point for context and usage",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, state: &mut EditorState| {
set_cursor_position(args, state, true)
},
)),
},
}
}
pub fn move_up() -> Command {
Command {
name: "up",
arguments: vec!["position"],
description: "move the cursor up by position",
documentation: "see {~}point for more information, specifically the \
section about relative position",
scope: CommandScope::REPLOnly,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, state: &mut EditorState| {
let input = args.get(1).map(|&s| s).unwrap_or("");
let specifier = match input.parse::<usize>() {
Ok(n) => {
format!("-{}", n)
}
Err(_) => "-1".to_string(),
};
set_cursor_position(vec!["", &specifier], state, false)
},
)),
},
}
}
pub fn move_down() -> Command {
Command {
name: "down",
arguments: vec!["position"],
description: "move the cursor down by position",
documentation: "see {~}point for more information, specifically the \
section about relative position",
scope: CommandScope::REPLOnly,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, state: &mut EditorState| {
let input = args.get(1).map(|&s| s).unwrap_or("");
let specifier = match input.parse::<usize>() {
Ok(n) => {
format!("+{}", n)
}
Err(_) => "+1".to_string(),
};
set_cursor_position(vec!["", &specifier], state, false)
},
)),
},
}
}
pub fn shift() -> Command {
Command {
name: "shift",
arguments: vec!["start_range", "dest_range"],
description: "shift lines in the buffer",
documentation: "to be refactored",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, state: &mut EditorState| {
let file_buffer = &mut state.buffer.contents;
if args.len() < 3 {
return "shift what?\ntry shift [start_range] [dest_range]".to_string();
}
let source_range: (usize, usize) =
(args[1].parse().unwrap(), args[1].parse().unwrap());
let destination: usize = args[2].parse().unwrap();
let source_exists = check_if_line_in_buffer(file_buffer, source_range.0)
.is_ok()
&& check_if_line_in_buffer(file_buffer, source_range.1).is_ok();
if source_exists {
if destination < source_range.0 || destination > source_range.1 {
return format!("destination overlaps with source");
}
for line_number in source_range.0..=source_range.1 {
let source_index = line_number - 1;
let line = file_buffer.remove(source_index);
file_buffer.insert(destination - 1, line);
}
}
format!(
"shifted range {}~{} to {}",
source_range.0, source_range.1, destination
)
},
)),
},
}
}
pub fn del(file_buffer: &mut Vec<String>, line_number: usize) {
let is_in_buffer = check_if_line_in_buffer(file_buffer, line_number);
if is_in_buffer.is_ok() {
file_buffer.remove(line_number - 1);
}
}
pub fn delete() -> Command {
Command {
name: "delete",
arguments: vec!["range"],
description: "remove the given range",
documentation: "removes every line in [range] from the file buffer\n\
if only a numerical argument is passed, it'll delete just \
that one line
if a TRS range (X~, ~X, X~Y) is passed, it'll delete \
the specified range",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, state: &mut EditorState| {
if args.len() >= 2 {
let range: (usize, usize) = match parse_tilde_range(args[1], state) {
Some(range) => range,
None => return format!("range is improperly formatted"),
};
let start_point = range.0;
let end_point = range.1;
for line_number in start_point..=end_point {
del(&mut state.buffer.contents, line_number);
}
if start_point == end_point {
return format!("deleted line {}", start_point);
}
format!("deleted lines {}~{}", start_point, end_point)
} else {
return format!("delete what?\ntry ~delete start~end");
}
},
)),
},
}
}
pub fn copy() -> Command {
Command {
name: "copy",
arguments: vec!["range"],
description: "copy the given range to the system clipboard",
documentation: "copies the contents of the provided [range] to the \
system clipboard, or the whole file if no [range] is \
specified\n\
this command is not supported on mobile devices",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, state: &mut EditorState| {
if args.len() >= 2 {
let range: (usize, usize) = match parse_tilde_range(args[1], state) {
Some(range) => range,
None => return format!("range is improperly formatted"),
};
if state.buffer.contents.is_empty() {
return format!("no buffer contents");
}
#[cfg(any(target_os = "android", target_os = "ios"))]
{
return format!("~copy is unsupported on your device, sorry");
}
let mut clipboard_context = ClipboardContext::new().unwrap();
let file_contents = state.buffer.contents.join("\n");
let mut to_copy = file_contents;
match clipboard_context.get_contents() {
Ok(_) => {
let mut copy_message = String::new();
if range.0 == range.1 {
let line_number = range.0;
let is_in_buffer = check_if_line_in_buffer(
&state.buffer.contents,
line_number,
);
if is_in_buffer.is_ok() {
to_copy = state.buffer.contents[line_number - 1].clone();
copy_message = format!("copying line {}", line_number);
}
} else {
let is_in_buffer =
check_if_line_in_buffer(&state.buffer.contents, range.0)
.is_ok()
&& check_if_line_in_buffer(
&state.buffer.contents,
range.1,
)
.is_ok();
if is_in_buffer {
to_copy =
state.buffer.contents[range.0 - 1..range.1].join("\n");
copy_message =
format!("copying lines {} to {}", range.0, range.1);
}
}
clipboard_context.set_contents(to_copy).unwrap();
return copy_message;
}
Err(e) => return format!("copy failed, because {}", e),
}
} else {
format!("copy what?\ntry ~copy start~end")
}
},
)),
},
}
}
pub fn substitute() -> Command {
Command {
name: "substitute",
arguments: vec!["range", "pattern/replacement"],
description: "perform a regex `replace()` on the given range",
documentation: "performs a regular expression replacement on the chosen \
[range] - anything matched by [pattern] will be \
replaced with [replacement]",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, state: &mut EditorState| {
if args.len() < 3 {
return format!(
"substitute what?\ntry ~substitute [range] [pattern/replacement]"
);
}
let range = match parse_tilde_range(args[1], state) {
Some(range) => range,
None => return format!("range is improperly formatted"),
};
let selector = args[2..].join(" ");
let (pattern, replacement) = match selector.split_once('/') {
Some((pattern, replacement)) => (pattern, replacement),
None => {
return format!(
"substitute what?\ntry ~substitute [range] [pattern/replacement]"
)
}
};
for line_number in range.0..=range.1 {
let is_in_buffer =
check_if_line_in_buffer(&state.buffer.contents, line_number);
if is_in_buffer.is_ok() {
let index = line_number - 1;
let line = &mut state.buffer.contents[index];
match Regex::new(pattern) {
Ok(re) => {
let replaced_line =
re.replace_all(line, replacement).to_string();
*line = replaced_line;
}
Err(e) => {
let error_message = e.to_string();
let lines: Vec<String> =
error_message.lines().map(String::from).collect();
if let Some(error) = lines.last() {
return format!(
"substitute failed, because {}",
error.to_lowercase().replace("error: ", "")
);
} else {
return format!("substitute failed, for some reason");
}
}
}
} else {
return is_in_buffer.unwrap_err();
}
}
if range.0 == range.1 {
return format!(
"substituted '{pattern}' on line {} with '{replacement}'",
range.0
);
} else {
return format!(
"substituted '{pattern}' on lines {}~{} with '{replacement}'",
range.0, range.1
);
}
},
)),
},
}
}
pub fn search() -> Command {
Command {
name: "search",
arguments: vec!["term"],
description: "search for a term in the buffer",
documentation: "this command searches for [term] in the file buffer and \
prints every line that matches along with their line \
numbers",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, state: &mut EditorState| {
let file_buffer = &mut state.buffer.contents;
let term = match args.get(1).map(|&s| s) {
Some(term) => term,
None => return format!("search what?\ntry ~search term"),
};
let mut matches: Vec<String> = Vec::new();
let escaped_term = regex::escape(term);
let regex = Regex::new(escaped_term.as_str()).unwrap();
for (line_number, line) in file_buffer.iter().enumerate() {
if regex.is_match(line) {
matches.push(format!("line {}: {}", line_number + 1, line));
}
}
if matches.is_empty() {
return format!("no matches found for {}", term);
} else {
format!("{}", matches.join("\n"))
}
},
)),
},
}
}
pub fn shell_command() -> Command {
Command {
name: "run",
arguments: vec!["shell_command", "command_args..."],
description: "run a shell command",
documentation: "uses your system command-line shell to run a shell command \
and prints the captured standard output\n\
the command will prioritise binary files over built-in \
shell commands, so `/usr/bin/ls` will take precedence over \
a built-in `ls` command if the former exists",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, _state: &mut EditorState| {
let mut command_args = args;
if command_args.len() <= 1 {
return format!("run what?");
} else {
let fallback_shell = if cfg!(windows) {
if which("pwsh").is_ok() {
"pwsh"
} else {
"powershell"
}
} else {
"sh"
};
let shell = if env::var("SHELL").is_ok() {
env::var("SHELL").unwrap()
} else {
fallback_shell.to_string()
};
let arg = "-c";
let command = command_args[1];
match which(command) {
Ok(path) => println!("running {}", path.to_string_lossy()),
Err(_) => {
println!("{} wasn't found; trying to run it anyway", &command)
}
}
command_args.drain(0..1);
let cmd = ShellCommand::new(shell)
.arg(arg)
.arg(command_args.join(" "))
.output()
.expect("command failed");
let out = String::from_utf8(cmd.stdout)
.unwrap_or(String::from("no output"))
.trim()
.to_string();
if out.len() < 1 {
return String::from("no output");
}
return out;
}
},
)),
},
}
}
pub fn shell_command_with_file() -> Command {
Command {
name: "runhere",
arguments: vec!["shell_command", "command_args..."],
description: "run a shell command with the current buffer contents",
documentation: "uses your system command-line shell to run a shell command \
with the contents of the current buffer as an argument\n\
the command will create a temporary file with your file \
buffer contents and pass that file's name to the shell \
command\n\
any modifications made to the temporary file will be \
synchronised back with the file buffer when the command \
completes",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, state: &mut EditorState| {
let shell_command = match args.get(1) {
Some(command) => command,
None => return format!("run what?"),
};
let file_name = args.get(2);
let mut command_args = match args.get(3) {
Some(_) => args[3..]
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>(),
None => Vec::new(),
};
let mut edited_buffer = false;
if state.buffer.contents.is_empty() {
return format!("no buffer contents");
} else {
let temporary_file_name: String = if file_name.is_some() {
let file = file_name.unwrap();
if file.contains(".") {
format!("{}", file.replace(".", "-temp."))
} else {
format!("{}.temp", file)
}
} else {
let hex_string: String = (0..8)
.map(|_| {
let random_digit = rand::thread_rng().gen_range(0..16);
format!("{:x}", random_digit)
})
.collect();
format!("{}.temp", hex_string)
};
if fs::write(&temporary_file_name, state.buffer.contents.join("\n"))
.is_err()
{
return format!("couldn't write temporary file");
}
let fallback_shell = if cfg!(windows) {
if which("pwsh").is_ok() {
"pwsh"
} else {
"powershell"
}
} else {
"sh"
};
let shell = if env::var("SHELL").is_ok() {
env::var("SHELL").unwrap()
} else {
fallback_shell.to_string()
};
let arg = "-c";
match which(shell_command) {
Ok(path) => println!("running {}", path.to_string_lossy()),
Err(_) => {
println!("{} wasn't found; trying to run it anyway", &shell_command)
}
}
if command_args.len() >= 1 {
command_args.drain(0..1);
}
let constructed_command = format!(
"{} {} {}",
shell_command,
temporary_file_name.clone(),
command_args.join(" "),
);
ShellCommand::new(shell)
.arg(arg)
.arg(constructed_command)
.status()
.expect("command failed");
let contents = fs::read_to_string(&temporary_file_name).unwrap();
if contents != state.buffer.contents.join("\n") {
state.buffer.contents =
contents.lines().map(|s| s.to_string()).collect();
edited_buffer = true;
}
fs::remove_file(&temporary_file_name).unwrap_or_default();
}
if edited_buffer {
format!(
"finished running {} on file; changes synchronised",
shell_command
)
} else {
format!("finished running {} on file", shell_command)
}
},
)),
},
}
}
pub fn indent() -> Command {
Command {
name: "indent",
arguments: vec!["range", "indentation"],
description: "indent a range",
documentation: "this command inserts space characters before every line \
in [range] by [indentation] spaces, effectively indenting \
it\n\
passing a negative [indentation] will unindent",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, state: &mut EditorState| {
if args.len() < 2 {
return "indent what?\ntry ~indent [range] [indentation]".to_string();
}
if args.len() < 3 {
let range = match parse_tilde_range(args[1], state) {
Some(range) => range,
None => return format!("range is improperly formatted"),
};
if range.0 == range.1 {
return format!("indent line {} by how many spaces?\ntry ~indent [range] [indentation]", range.0);
}
return format!("indent lines {}~{} by how many spaces?\ntry ~indent [range] [indentation]", range.0, range.1);
}
let range: (usize, usize) = match parse_tilde_range(args[1], state) {
Some(range) => range,
None => return format!("range is improperly formatted"),
};
let indentation: isize = match args[2].parse() {
Ok(arg) => arg,
Err(_) => return format!("arg [indentation] must be isize"),
};
for line_number in range.0..=range.1 {
let is_in_buffer =
check_if_line_in_buffer(&mut state.buffer.contents, line_number);
if is_in_buffer.is_ok() {
let index = line_number - 1;
let line = &mut state.buffer.contents[index];
match indentation.cmp(&0) {
Ordering::Greater => {
let indented_line = format!(
"{:indent$}{}",
"",
line,
indent = indentation as usize
);
*line = indented_line;
}
Ordering::Less => {
let line_len = line.len() as isize;
let new_len = (line_len + indentation).max(0) as usize;
let indented_line = format!(
"{:indent$}",
&line[line_len as usize - new_len..],
indent = new_len
);
*line = indented_line;
}
_ => return "can't indent by no spaces".to_string(),
}
} else {
return is_in_buffer.unwrap_err();
}
}
if range.0 == range.1 {
format!("indented line {} by {} spaces", range.0, indentation)
} else {
format!(
"indented lines {}~{} by {} spaces",
range.0, range.1, indentation
)
}
},
)),
},
}
}
pub fn crash() -> Command {
Command {
name: "bsod",
arguments: vec![],
description: "'crash' the editor",
documentation: "this command resembles a blue screen of death and forces \
the editor to exit with a non-zero exit code\n\
this command is omitted from {~}test for obvious reasons",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |args: Vec<&str>, _state: &mut EditorState| {
let error_code = args.get(1).map(|&s| s).unwrap_or("USER_FRICKED_UP");
let hex_codes = if args.len() >= 2 {
args[2..]
.iter()
.map(|s| s.parse().unwrap())
.collect::<Vec<u32>>()
} else {
vec![]
};
let mut populated_hex_codes = [0x00000000; 4];
let num_values = hex_codes.len().min(4);
populated_hex_codes[..num_values].copy_from_slice(&hex_codes[..num_values]);
eprintln!(
"stop: {}: 0x{:08X} (0x{:08X},0x{:08X},0x{:08X})",
error_code.to_uppercase(),
populated_hex_codes[0],
populated_hex_codes[1],
populated_hex_codes[2],
populated_hex_codes[3],
);
std::process::exit(1);
},
)),
},
}
}
pub fn nothing() -> Command {
Command {
name: "nothing",
arguments: vec![],
description: "do nothing",
documentation: "this command will effectively do nothing of value, \
simply displaying the current buffer contents joined \
by semicolons instead of newlines",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(
move |_args: Vec<&str>, state: &mut EditorState| {
if state.buffer.contents.is_empty() {
return format!("no buffer contents");
}
let buffer_contents: String = state.buffer.contents.join("; ");
format!("doing nothing with {}", buffer_contents)
},
)),
},
}
}
pub fn butterfly() -> Command {
Command {
name: "butterfly",
arguments: vec![],
description: "it's what real programmers use!",
documentation: "if you don't understand this command, you're not a REAL \
programmer!\n\
in all seriousness, this command is a reference to the \
xkcd webcomic, specifically #378, \"Real Programmers\"\n\
you can read the comic at https://xkcd.com/378/",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(|_args: Vec<&str>, _state: &mut EditorState| {
"successfully flipped one bit".to_string()
})),
},
}
}
#[cfg(feature = "lua")]
pub fn evaluate_lua(code: String, state: &mut EditorState, feedback: bool) -> String {
let lua = state.lua.clone();
if let Err(e) = lua.globals().set("sued_buffer", state.buffer.contents.clone()) {
return format!("failed to set buffer contents as lua global: {}", e);
}
let mut is_error: bool = false;
let result = match state.lua.load(&code).eval::<mlua::Value>() {
Ok(value) => match value {
mlua::Value::Nil => "nil".to_string(),
mlua::Value::Boolean(b) => b.to_string(),
mlua::Value::Number(n) => n.to_string(),
mlua::Value::String(s) => {
match s.to_str() {
Ok(s) => s.to_string(),
Err(_) => format!("invalid string {:?}", s),
}
}
mlua::Value::Table(t) => {
let table = t.pairs::<String, String>();
let table_output = table.map(|result| match result {
Ok((_k, v)) => v,
Err(e) => format!("invalid table entry: {}", e),
});
format!("[{}]", table_output.collect::<Vec<String>>().join(", "))
},
mlua::Value::Error(e) => {
is_error = true;
format!("error: {}", e)
},
_ => "unsupported return".to_string(),
},
Err(e) => e.to_string(),
};
match lua.globals().get::<Vec<String>>("sued_buffer") {
Ok(buffer) => {
if buffer != state.buffer.contents {
println!("modifications synchronised");
state.buffer.contents = buffer;
}
},
Err(_) => println!("couldn't read back buffer contents"),
}
if !feedback {
if is_error {
return format!("error: {result}");
}
return "eval finished".to_string();
}
format!("=> {result}")
}
#[cfg(feature = "lua")]
pub fn eval() -> Command {
Command {
name: "eval",
arguments: vec!["lua_code"],
description: "evaluate lua",
documentation: "this command evaluates lua code from the arguments \
and displays the result, if any\n\
the buffer contents are added as a lua global, `sued_buffer` \
and like with ~runhere, any modifications made to the buffer \
are synchronised back",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(|args: Vec<&str>, state: &mut EditorState| {
if args.len() < 2 {
return format!(
"eval what? try {}eval [lua_code]\n\
if you want to run the buffer as lua, use {}script this instead\n\
or if you want to run a lua file, {}script [filename]",
state.prefix, state.prefix, state.prefix
);
}
let code = args[1..].join(" ");
evaluate_lua(code, state, true)
}))
}
}
}
#[cfg(feature = "lua")]
pub fn script() -> Command {
Command {
name: "script",
arguments: vec!["filename"],
description: "run a lua script",
documentation: "this command imports and runs a lua script at filename\n\
if `this` is specified instead of the filename, it'll \
treat the buffer contents as a lua script\n\
like ~eval, `sued_buffer` is added as a lua global and \
any modifications made to the buffer are synchronised back\n\
unlike ~eval, the return value is not printed unless \
it's an error",
scope: CommandScope::Global,
action: CommandAction {
action: Arc::new(Mutex::new(|args: Vec<&str>, state: &mut EditorState| {
if args.len() < 2 {
return format!(
"what script? try {}script [filename] or {}script this",
state.prefix, state.prefix
);
}
let filename = args[1..].join(" ");
let code = if filename == "this" {
state.buffer.contents.join("\n")
}
else {
match std::fs::read_to_string(&filename) {
Ok(contents) => contents,
Err(e) => {
match e.kind() {
std::io::ErrorKind::NotFound => {
return format!("{} doesn't exist\n\
if you want to run a lua expression, use {}eval [expr] instead",
&filename, state.prefix)
},
_ => {
return format!("failed to read file: {}", e)
}
}
}
}
};
evaluate_lua(code, state, true)
}))
}
}
}
pub fn split_pattern_replacement(combined_args: &str) -> Vec<&str> {
let mut pattern_replacement = Vec::new();
let mut start = 0;
let mut escaped = false;
for (i, c) in combined_args.char_indices() {
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '/' {
pattern_replacement.push(&combined_args[start..i]);
start = i + 1;
}
}
if start <= combined_args.len() {
pattern_replacement.push(&combined_args[start..]);
}
pattern_replacement
}