use crate::command::{Command, CommandAction, CommandRegistry, CommandScope};
use crate::file_buffer::FileBuffer;
use crate::EditorState;
use crate::helper::*;
use crate::tilde_range::parse_tilde_range;
use std::env;
use rand::Rng;
use copypasta::{ClipboardContext, ClipboardProvider};
use regex::Regex;
use std::cmp::Ordering;
use which::which;
use std::fs;
use std::path::PathBuf;
use std::process::Command as ShellCommand;
#[cfg(feature = "lua")]
use crate::lua;
pub fn register_all(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()); registry.add_command(join()); }
#[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(lua::eval()); registry.add_command(lua::script()); }
#[cfg(debug_assertions)]
registry.add_command(test_commands()); }
pub fn test_commands() -> Command {
Command {
name: "test",
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::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()
}
),
..Command::default()
}
}
pub fn command_list() -> Command {
Command {
name: "cmds",
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::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(", ")
}),
..Command::default()
}
}
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::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();
}
}),
..Command::default()
}
}
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::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()
}
}),
..Command::default()
}
}
pub fn about_sued() -> Command {
Command {
name: "about",
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::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>")
}),
..Command::default()
}
}
pub fn save() -> Command {
Command {
name: "save",
arguments: vec!["file_path..."],
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::new(|args: Vec<&str>, state: &mut EditorState| {
if args.len() <= 1 && state.buffer.file_path.is_none() {
return format!("save where?\ntry {}save filename", state.prefix);
}
let file_path = if args.get(1).is_some() {
args[1..]
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
.join(" ")
} else {
if let Some(file_path) = &state.buffer.file_path {
file_path.to_string()
} else {
return "file path invalid - you should never see this".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::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 {
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() -> 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::new(|args: Vec<&str>, state: &mut EditorState| {
if args.len() <= 1 {
return format!("open what?\ntry {}open file path", state.prefix);
}
let file_path = args[1..]
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
.join(" ");
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::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 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::new(|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::new(|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::new(|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::new(|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 join() -> Command {
Command {
name: "join",
arguments: vec!["range"],
description: "join lines in the buffer",
documentation: "this command joins a range of lines together onto the \
first line of the range\n\
run {~}join X~Y to join lines X through Y onto line X\n\
this command must take a range of at least two lines",
scope: CommandScope::Global,
action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
let range = match args.get(1).map(|&s| s) {
Some(n) => {
let n_range = match parse_tilde_range(n, state) {
Some(r) => r,
None => {
return format!("range is improperly formatted");
}
};
if n_range.1 <= n_range.0 {
return "range must be at least 2 lines long".to_string();
}
n_range
}
None => {
return format!("join what?\ntry {}join range", state.prefix);
}
};
let start = range.0 - 1;
let end = range.1 - 1;
let lines_to_join = state.buffer.contents[start..=end].to_vec();
let first_line = state.buffer.contents.get_mut(start).unwrap();
first_line.clear();
first_line.push_str(lines_to_join.iter()
.map(|s| s.trim_end())
.collect::<Vec<&str>>()
.join(" ").as_str());
state.buffer.contents.drain(start + 1..=end);
format!("joined lines {}~{}", range.0, range.1)
}),
}
}
pub fn shift() -> Command {
Command {
name: "shift",
arguments: vec!["start_range", "destination"],
description: "shift lines in the buffer",
documentation: "to be refactored",
scope: CommandScope::Global,
action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
let file_buffer = &mut state.buffer.contents;
if args.len() < 3 {
return "shift what?\ntry shift [start_range] [destination]".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 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::new(|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;
state.buffer.contents.drain(start_point - 1..=end_point - 1);
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::new(|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::new(|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::new(|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::new(|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::new(|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::new(|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::new(|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",
description: "do nothing",
documentation: "this command will effectively do nothing of value, \
simply displaying the current buffer contents joined \
by semicolons instead of newlines",
action: CommandAction::new(|_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)
},
),
..Command::default()
}
}
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::new(|_args: Vec<&str>, _state: &mut EditorState| {
"successfully flipped one bit".to_string()
}),
}
}