sued/
commands.rs

1use crate::command::{Command, CommandAction, CommandRegistry, CommandScope};
2use crate::file_buffer::FileBuffer;
3use crate::EditorState;
4use crate::helper::*;
5use crate::tilde_range::parse_tilde_range;
6
7use std::env;
8use rand::Rng;
9use copypasta::{ClipboardContext, ClipboardProvider};
10use regex::Regex;
11use std::cmp::Ordering;
12use which::which;
13use std::fs;
14use std::path::PathBuf;
15use std::process::Command as ShellCommand;
16
17#[cfg(feature = "lua")]
18use crate::lua;
19
20/// Adds every `sued::Command` in this file to the `registry`.
21/// Used by `sued::CommandRegistry::instantiate` to initialise a new command
22/// registry with sued's commands.
23pub fn register_all(registry: &mut CommandRegistry) {
24    #[cfg(feature = "informational")]
25    {
26        registry.add_command(about_sued());     // `~about`
27        registry.add_command(command_list());   // `~cmds`
28        registry.add_command(help());           // `~help [cmd: string]`
29        registry.add_command(document());       // `~doc cmd: string`
30    }
31
32    #[cfg(feature = "inputoutput")]
33    {
34        registry.add_command(save());           // `~save [filename: string]`
35        registry.add_command(open());           // `~open filename: string`
36        registry.add_command(reopen());         // `~reopen`
37        registry.add_command(show());           // `~show [range: trs] [line_numbers: bool]`
38        registry.add_command(copy());           // `~copy [range: trs]`
39        registry.add_command(search());         // `~search keyword: string`
40    }
41
42    #[cfg(feature = "cursor")]
43    {
44        registry.add_command(point());          // `~point position: usize`
45        registry.add_command(overtake());       // `~overtake position: usize`
46        registry.add_command(move_up());        // `~up lines: isize`
47        registry.add_command(move_down());      // `~down lines: isize`
48    }
49
50    #[cfg(feature = "transformations")]
51    {
52        registry.add_command(shift());          // `~shift range: trs, destination: usize`
53        registry.add_command(indent());         // `~indent range: trs`
54        registry.add_command(delete());         // `~delete range: trs`
55        registry.add_command(substitute());     // `~substitute range: trs pattern: regex/replacement: string`
56        registry.add_command(join());           // `~join range: trs`
57    }
58
59    #[cfg(feature = "shell")]
60    {
61        registry.add_command(shell_command());  // `~run cmd: string`
62        registry.add_command(shell_command_with_file());// `~runhere cmd: string`
63    }
64
65    #[cfg(feature = "fun")]
66    {
67        registry.add_command(crash());          // `~bsod`
68        registry.add_command(nothing());        // `~nothing`
69        registry.add_command(butterfly());      // `~butterfly`
70    }
71
72    #[cfg(feature = "lua")]
73    {
74        registry.add_command(lua::eval());           // `~eval`
75        registry.add_command(lua::script());         // `~script`
76    }
77
78    #[cfg(debug_assertions)]
79    registry.add_command(test_commands());      // `~test`
80}
81
82/// Runs every `sued::Command` in this file to test them with various inputs.
83/// For `show`, `delete`, `copy` and `indent`, the inputs given are in Tilde
84/// Range Syntax.
85/// Skips over itself and `crash` ("bsod") because the former would cause an
86/// infinite loop and the latter would intentionally crash the program.
87/// Also skips over `shell_command` and `shell_command_with_file` ("run" and
88/// "runhere") because these commands launch shell proceses, and besides I
89/// already tested those commands on my own.
90///
91/// Can only be run in the REPL, is not available in sued as a library.
92pub fn test_commands() -> Command {
93    Command {
94        name: "test",
95        description: "test every command in the registry",
96        documentation: "this command sorts every command available in the \
97                        registry by alphabetical order and runs them all \
98                        individually, each with a different input",
99        scope: CommandScope::REPLOnly,
100        action: CommandAction::new(|_args: Vec<&str>, state: &mut EditorState| {
101                let mut commands = state.registry.get_all_commands();
102                commands.sort();
103                let mut index = 0;
104                println!("{}", commands.len());
105                for command in commands {
106                    if command.name == "test" {
107                        println!("skipping over self - would cause infinite loop if locked");
108                        continue;
109                    }
110                    index += 1;
111                    println!("{}: {}", index, command.name);
112                    let mut command_lock = command.action.action.lock().unwrap();
113                    match command.name {
114                        "bsod" => {
115                            println!("not running bsod because why would you");
116                        }
117                        "show" | "delete" | "copy" | "indent" => {
118                            state.buffer = FileBuffer::from(vec![
119                                "a".to_string(),
120                                "b".to_string(),
121                                "c".to_string(),
122                                "d".to_string(),
123                            ]);
124                            println!("testing ranged command {} with normal data", command.name);
125                            command_lock(vec!["test", "1~3"], state);
126
127                            state.buffer = FileBuffer::from(vec![
128                                "a".to_string(),
129                                "b".to_string(),
130                                "c".to_string(),
131                                "d".to_string(),
132                            ]);
133                            println!("testing ranged command {} with empty data", command.name);
134                            command_lock(vec!["test"], state);
135
136                            state.buffer = FileBuffer::from(vec![
137                                "a".to_string(),
138                                "b".to_string(),
139                                "c".to_string(),
140                                "d".to_string(),
141                            ]);
142                            println!("testing ranged command {} with invalid data", command.name);
143                            command_lock(vec!["test", "invalid"], state);
144                        }
145                        "run" | "runhere" => {
146                            println!(
147                                "ignoring shell command {} because I know that works already",
148                                command.name
149                            );
150                        }
151                        _ => {
152                            println!("testing command {} with text value", command.name);
153                            command_lock(vec!["test", "testvalue"], state);
154                            println!("testing command {} with number value", command.name);
155                            command_lock(vec!["test", "123"], state);
156                            println!("testing command {} with empty value", command.name);
157                            command_lock(vec!["test"], state);
158                        }
159                    }
160                }
161                "done testing - all seems okay".to_string()
162            }
163        ),
164        ..Command::default()
165    }
166}
167
168/// Displays and returns the list of commands that sued supports.
169/// Invoked with the `~` command.
170pub fn command_list() -> Command {
171    Command {
172        name: "cmds",
173        description: "list commands",
174        documentation: "this command only prints a list of command names \
175                        separated by commas\n\
176                        if you need detail, use {~}help instead",
177        scope: CommandScope::Global,
178        action: CommandAction::new(|_args: Vec<&str>, state: &mut EditorState| {
179            let mut commands = state.registry.get_all_commands();
180            commands.sort();
181            commands
182                .iter()
183                .map(|command| command.name)
184                .collect::<Vec<_>>()
185                .join(", ")
186        }),
187        ..Command::default()
188    }
189}
190
191pub fn document() -> Command {
192    Command {
193        name: "doc",
194        arguments: vec!["command"],
195        description: "retrieve the documentation for a command",
196        documentation: "this command simply retrieves the documentation for [command], \
197                        without returning all the details that {~}help provides",
198        scope: CommandScope::Global,
199        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
200            let command_name = args.get(1).map(|&s| s);
201            if let Some(name) = command_name {
202                let command = state.registry.get_command(name);
203                if command.is_none() {
204                    return format!("{} is not a defined command", name);
205                }
206                let command = command.unwrap();
207                format!("documentation for {}:\n{}", name, command.documentation)
208                    .replace("{~}", &state.prefix)
209            } else {
210                return "document what? try ~doc [command]".to_string();
211            }
212        }),
213        ..Command::default()
214    }
215}
216
217/// Displays a list of available commands and their descriptions.
218/// Invoked with the `~help` command.
219pub fn help() -> Command {
220    Command {
221        name: "help",
222        arguments: vec!["command"],
223        description: "list commands with descriptions",
224        documentation: "type {~}help on its own to list every command in a list, \
225                        along with a description of each command\n\
226                        type {~}help followed by a command name to get a \
227                        description of that command\n\
228                        if all you need is command names, use {~}cmds instead",
229        scope: CommandScope::Global,
230        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
231            let command_name = args.get(1).map(|&s| s);
232            if let Some(name) = command_name {
233                let command = state.registry.get_command(name);
234
235                if command.is_none() {
236                    return format!("{} is not a defined command", name);
237                }
238
239                let command = command.unwrap();
240
241                let arguments = if command.arguments.len() >= 1 {
242                    command.arguments.join(", ")
243                } else {
244                    "none".to_string()
245                };
246
247                let documentation = if command.documentation.len() > 0 {
248                    format!("documentation:\n{}", command.documentation)
249                        .replace("{~}", &state.prefix)
250                } else {
251                    "this command has no documentation".to_string()
252                };
253
254                [
255                    format!("{} - {}", command.name, command.description),
256                    format!("arguments: {}", arguments),
257                    format!("scope: {}", command.scope.to_string()),
258                    documentation,
259                ]
260                .join("\n")
261            } else {
262                let mut commands = state.registry.get_all_commands();
263                commands.sort();
264                let mut commands_string: String = "press up and down to navigate through command history\n\
265                                                    type `{~}help [command]` to get information about a command\n\
266                                                    all `range` arguments use tilde range syntax (X~, ~X, X~Y)\n\
267                                                    key: command [arg1] [arg2] [...] - what the command does\n"
268                                                    .to_string()
269                                                    .replace("{~}", &state.prefix);
270                commands.iter().for_each(|command| {
271                    if command.arguments.len() >= 1 {
272                        let argues = format!("[{}]", command.arguments.join("] ["));
273                        commands_string.push_str(&format!(
274                            "{}{} {} - {}\n",
275                            state.prefix, command.name, argues, command.description
276                        ));
277                    } else {
278                        commands_string.push_str(&format!(
279                            "{}{} - {}\n",
280                            state.prefix, command.name, command.description
281                        ));
282                    }
283                });
284                commands_string.strip_suffix("\n").unwrap().to_string()
285            }
286        }),
287        ..Command::default()
288    }
289}
290
291/// Displays the sued version number and information about the editor itself.
292/// Invoked with the `~about` command.
293pub fn about_sued() -> Command {
294    Command {
295        name: "about",
296        description: "display about text",
297        documentation: "this command simply displays information about the sued \
298                        text editor itself, with simple instructions and the \
299                        author's name",
300        scope: CommandScope::Global,
301        action: CommandAction::new(|_args: Vec<&str>, _state: &mut EditorState| {
302            let version = if cfg!(debug_assertions) {
303                format!("{}-devel", env!("CARGO_PKG_VERSION"))
304            } else {
305                env!("CARGO_PKG_VERSION").to_string()
306            };
307            format!("this is sued, v{version}\n\
308                        sued is a vector-oriented line editor, heavily inspired by the ed editor\n\
309                        you can write text simply by typing, and use sued's extensive command set for editing\n\
310                        editor commands are prefixed with a default prefix of ~, type ~help for a full list\n\
311                        sued written by Arsalan \"Aeri\" Kazmi <sonicspeed848@gmail.com>")
312        }),
313        ..Command::default()
314    }
315}
316
317/// Writes the `state.buffer.contents` to the `file_path`, if there are any
318/// buffer contents.
319/// Used to provide functionality for the `~save` command.
320pub fn save() -> Command {
321    Command {
322        name: "save",
323        arguments: vec!["file_path..."],
324        description: "save the current file",
325        documentation: "if a file was previously opened (either with the {~}open \
326                        command or passing it as an argument on startup) running \
327                        {~}save without arguments will write the file contents \
328                        to that file\n\
329                        if [filename] is passed, it'll set the file path to that \
330                        and then write the file contents to it, assuming nothing \
331                        went wrong in the process",
332        scope: CommandScope::Global,
333        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
334            if args.len() <= 1 && state.buffer.file_path.is_none() {
335                return format!("save where?\ntry {}save filename", state.prefix);
336            }
337
338            let file_path = if args.get(1).is_some() {
339                args[1..]
340                    .iter()
341                    .map(|s| s.to_string())
342                    .collect::<Vec<String>>()
343                    .join(" ")
344            } else {
345                if let Some(file_path) = &state.buffer.file_path {
346                    file_path.to_string()
347                } else {
348                    return "file path invalid - you should never see this".to_string();
349                }
350            };
351
352            if state.buffer.contents.is_empty() {
353                return "buffer empty - nothing to save".to_string();
354            }
355
356            let content = state.buffer.contents.join("\n");
357            let path = PathBuf::from(&file_path);
358
359            match fs::write(&path, content) {
360                Ok(_) => {
361                    state.buffer.file_path = Some(file_path);
362                    let file_size_display = get_file_size(&state);
363                    format!("saved to {} with {}", &path.display(), file_size_display)
364                }
365                Err(error) => format!("couldn't save file to {}: {}", file_path, error),
366            }
367        }),
368    }
369}
370
371/// Iterates over the `buffer_contents` and displays them one by one.
372/// If a range was specified, only iterate for that part.
373/// Used to provide functionality for the `~show` command.
374pub fn show() -> Command {
375    Command {
376        name: "show",
377        arguments: vec!["range", "line_numbers"],
378        description: "display the current file contents",
379        documentation: "with no arguments passed, this command will simply \
380                        display the entire file to standard output\n\
381                        if [range] is passed, the lines in that range will be \
382                        displayed instead, for example {~}show 1~10 will display \
383                        lines 1 through 10, inclusive\n\
384                        if \"false\" is passed for [line_numbers] line numbers, \
385                        will be omitted from the output",
386        scope: CommandScope::Global,
387        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
388            let range = if args.len() >= 2 {
389                if let Some(range) = parse_tilde_range(&args[1], state) {
390                    range
391                } else {
392                    return format!("range is improperly formatted");
393                }
394            } else {
395                (1, state.buffer.contents.len())
396            };
397            let start_point: usize = range.0;
398            let end_point: usize = range.1;
399            let line_numbers: bool = match args.get(2) {
400                Some(arg) => arg.parse().unwrap_or(true),
401                None => true,
402            };
403
404            let mut listing: String = String::new();
405
406            if state.buffer.contents.is_empty() {
407                return format!("no buffer contents - nothing to show");
408            } else if let Err(_) = check_if_line_in_buffer(&state.buffer.contents, start_point)
409            {
410                return format!("invalid start point {}", start_point);
411            } else if let Err(_) = check_if_line_in_buffer(&state.buffer.contents, end_point) {
412                return format!("invalid end point {}", end_point);
413            } else {
414                let contents: Vec<String> =
415                    state.buffer.contents[start_point - 1..end_point].to_vec();
416                let max_count_length: usize =
417                    (start_point + contents.len() - 1).to_string().len();
418                for (index, line) in contents.iter().enumerate() {
419                    let this_line = start_point + index - 1;
420                    let mut sep = "│";
421                    if this_line == state.cursor {
422                        sep = "›";
423                    }
424                    if line_numbers {
425                        let count: usize = start_point + index;
426                        let count_padded: String =
427                            format!("{:width$}", count, width = max_count_length);
428                        listing.push_str(format!("{}{sep}{}\n", count_padded, line).as_str());
429                    } else {
430                        listing.push_str(format!("{}\n", line).as_str());
431                    }
432                }
433            }
434
435            listing.strip_suffix("\n").unwrap().to_string()
436        }),
437    }
438}
439
440/// Verifies the `file_path`'s file existence, then returns a Result of the file contents as a `String`.
441/// If `file_path` is a directory, returns a Result of the directory listing as a `String`.
442/// If something goes wrong, it returns a Result of an error message as a `String`.
443/// Used for the `~open` command.
444pub fn open() -> Command {
445    Command {
446        name: "open",
447        arguments: vec!["file_path..."],
448        description: "open a file or directory",
449        documentation: "loads the contents of the file at [file_path] into the \
450                        buffer\n\
451                        if a path to a dirctory is opened instead, it'll open the \
452                        directory listing as text\n\
453                        this command will set the buffer's file path for the \
454                        {~}save command",
455        scope: CommandScope::Global,
456        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
457            if args.len() <= 1 {
458                return format!("open what?\ntry {}open file path", state.prefix);
459            }
460            
461            let file_path = args[1..]
462                .iter()
463                .map(|s| s.to_string())
464                .collect::<Vec<String>>()
465                .join(" ");
466
467            let contents = match open_file(&file_path) {
468                Ok(contents) => contents,
469                Err(e) => return e,
470            };
471            state.buffer.contents = contents.split("\n").map(String::from).collect();
472            state.buffer.file_path = Some(file_path.to_string());
473            let file_size_display = get_file_size(&state);
474            format!("opened {} as text with {}", file_path, file_size_display)
475        }),
476    }
477}
478
479pub fn reopen() -> Command {
480    Command {
481        name: "reopen",
482        arguments: vec![],
483        description: "reopen the current file",
484        documentation: "reopens the file from the file path stored in the buffer\n\
485                        if no file is open, this command will do nothing",
486        scope: CommandScope::Global,
487        action: CommandAction::new(|_args: Vec<&str>, state: &mut EditorState| {
488            if let Some(file_path) = &state.buffer.file_path {
489                let contents = match open_file(file_path) {
490                    Ok(contents) => contents,
491                    Err(e) => return e,
492                };
493                state.buffer.contents = contents.split("\n").map(String::from).collect();
494                let file_size_display = get_file_size(&state);
495                format!("opened {} as text with {}", file_path, file_size_display)
496            } else {
497                "no file path stored; try ~open-ing one".to_string()
498            }
499        }),
500    }
501}
502
503pub fn point() -> Command {
504    Command {
505        name: "point",
506        arguments: vec!["position"],
507        description: "set the cursor position on the y axis non-destructively",
508        documentation: "this command will set the cursor position to a specific \
509                        [position], setting the cursor to the specified line number 
510                        so that any text typed will be inserted at that point\n\
511                        text entered after moving the cursor will be inserted\n\
512                        into a new line before the cursor position, moving the \
513                        line down without overwriting it\n\
514                        after setting the cursor position, the line's contents \
515                        will be displayed with the corresponding line number\n\
516                        specify a relative position (+n or -n) to move the cursor \
517                        down or up by n lines\n\
518                        if no [position] is passed, it defaults to the end of the \
519                        file buffer",
520        scope: CommandScope::Global,
521        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
522            set_cursor_position(args, state, false)
523        }),
524    }
525}
526
527pub fn overtake() -> Command {
528    Command {
529        name: "overtake",
530        arguments: vec!["position"],
531        description: "set the cursor position on the y axis destructively",
532        documentation: "this command sets the cursor position to a specific [position], \
533                        like {~}point, but additionally deletes the line at [position], \
534                        effectively overtaking the line (hence the name)\n\
535                        after moving to the new line, the original line contents will be \
536                        set in the line editor to be manually edited\n\
537                        see {~}point for context and usage",
538        scope: CommandScope::Global,
539        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
540            set_cursor_position(args, state, true)
541        }),
542    }
543}
544
545pub fn move_up() -> Command {
546    Command {
547        name: "up",
548        arguments: vec!["position"],
549        description: "move the cursor up by position",
550        documentation: "see {~}point for more information, specifically the \
551                        section about relative position",
552        scope: CommandScope::REPLOnly,
553        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
554            let input = args.get(1).map(|&s| s).unwrap_or("");
555
556            let specifier = match input.parse::<usize>() {
557                Ok(n) => {
558                    format!("-{}", n)
559                }
560                Err(_) => "-1".to_string(),
561            };
562
563            set_cursor_position(vec!["", &specifier], state, false)
564        }),
565    }
566}
567
568pub fn move_down() -> Command {
569    Command {
570        name: "down",
571        arguments: vec!["position"],
572        description: "move the cursor down by position",
573        documentation: "see {~}point for more information, specifically the \
574                        section about relative position",
575        scope: CommandScope::REPLOnly,
576        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
577            let input = args.get(1).map(|&s| s).unwrap_or("");
578
579            let specifier = match input.parse::<usize>() {
580                Ok(n) => {
581                    format!("+{}", n)
582                }
583                Err(_) => "+1".to_string(),
584            };
585
586            set_cursor_position(vec!["", &specifier], state, false)
587        }),
588    }
589}
590
591/// Join `range` together onto the range's first line.
592/// Works similarly to Vim's `J` (`Shift-j`) command.
593/// Provides functionality for the `~join` command.
594pub fn join() -> Command {
595    Command {
596        name: "join",
597        arguments: vec!["range"],
598        description: "join lines in the buffer",
599        documentation: "this command joins a range of lines together onto the \
600                        first line of the range\n\
601                        run {~}join X~Y to join lines X through Y onto line X\n\
602                        this command must take a range of at least two lines",
603        scope: CommandScope::Global,
604        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
605            let range = match args.get(1).map(|&s| s) {
606                Some(n) => {
607                    let n_range = match parse_tilde_range(n, state) {
608                        Some(r) => r,
609                        None => {
610                            return format!("range is improperly formatted");
611                        }
612                    };
613
614                    if n_range.1 <= n_range.0 {
615                        return "range must be at least 2 lines long".to_string();
616                    }
617
618                    n_range
619                }
620                None => {
621                    return format!("join what?\ntry {}join range", state.prefix);
622                }
623            };
624
625            // Convert range to 0-indexing
626            let start = range.0 - 1;
627            let end = range.1 - 1;
628
629            let lines_to_join = state.buffer.contents[start..=end].to_vec();
630            let first_line = state.buffer.contents.get_mut(start).unwrap();
631
632            first_line.clear();
633            first_line.push_str(lines_to_join.iter()
634                .map(|s| s.trim_end())
635                .collect::<Vec<&str>>()
636                .join(" ").as_str());
637
638            state.buffer.contents.drain(start + 1..=end);
639
640            format!("joined lines {}~{}", range.0, range.1)
641        }),
642    }
643}
644
645/// Move the `source_line` with the `target_line` in the `file_buffer`.
646/// Provides functionality for the `~swap` command.
647pub fn shift() -> Command {
648    Command {
649        name: "shift",
650        arguments: vec!["start_range", "destination"],
651        description: "shift lines in the buffer",
652        documentation: "to be refactored",
653        scope: CommandScope::Global,
654        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
655            let file_buffer = &mut state.buffer.contents;
656
657            if args.len() < 3 {
658                return "shift what?\ntry shift [start_range] [destination]".to_string();
659            }
660
661            let source_range: (usize, usize) =
662                (args[1].parse().unwrap(), args[1].parse().unwrap());
663            let destination: usize = args[2].parse().unwrap();
664
665            let source_exists = check_if_line_in_buffer(file_buffer, source_range.0)
666                .is_ok()
667                && check_if_line_in_buffer(file_buffer, source_range.1).is_ok();
668            if source_exists {
669                if destination < source_range.0 || destination > source_range.1 {
670                    return format!("destination overlaps with source");
671                }
672
673                for line_number in source_range.0..=source_range.1 {
674                    let source_index = line_number - 1;
675                    let line = file_buffer.remove(source_index);
676                    file_buffer.insert(destination - 1, line);
677                }
678            }
679
680            format!(
681                "shifted range {}~{} to {}",
682                source_range.0, source_range.1, destination
683            )
684        }),
685    }
686}
687
688/// Remove the `line_number` from the `file_buffer`.
689/// Provides functionality for the `~delete` command.
690pub fn delete() -> Command {
691    Command {
692        name: "delete",
693        arguments: vec!["range"],
694        description: "remove the given range",
695        documentation: "removes every line in [range] from the file buffer\n\
696                        if only a numerical argument is passed, it'll delete just \
697                        that one line
698                        if a TRS range (X~, ~X, X~Y) is passed, it'll delete \
699                        the specified range",
700        scope: CommandScope::Global,
701        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
702            if args.len() >= 2 {
703                let range: (usize, usize) = match parse_tilde_range(args[1], state) {
704                    Some(range) => range,
705                    None => return format!("range is improperly formatted"),
706                };
707                let start_point = range.0;
708                let end_point = range.1;
709
710                state.buffer.contents.drain(start_point - 1..=end_point - 1);
711
712                if start_point == end_point {
713                    return format!("deleted line {}", start_point);
714                }
715                format!("deleted lines {}~{}", start_point, end_point)
716            } else {
717                return format!("delete what?\ntry ~delete start~end");
718            }
719        }),
720    }
721}
722
723/// Copy the provided `line_number` to the system clipboard.
724/// If no `line_number` is specified (it's not in the buffer), copy the whole buffer.
725/// Provides functionality for the `~copy` command.
726pub fn copy() -> Command {
727    Command {
728        name: "copy",
729        arguments: vec!["range"],
730        description: "copy the given range to the system clipboard",
731        documentation: "copies the contents of the provided [range] to the \
732                        system clipboard, or the whole file if no [range] is \
733                        specified\n\
734                        this command is not supported on mobile devices",
735        scope: CommandScope::Global,
736        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
737            if args.len() >= 2 {
738                let range: (usize, usize) = match parse_tilde_range(args[1], state) {
739                    Some(range) => range,
740                    None => return format!("range is improperly formatted"),
741                };
742
743                if state.buffer.contents.is_empty() {
744                    return format!("no buffer contents");
745                }
746                #[cfg(any(target_os = "android", target_os = "ios"))]
747                {
748                    return format!("~copy is unsupported on your device, sorry");
749                }
750                let mut clipboard_context = ClipboardContext::new().unwrap();
751                let file_contents = state.buffer.contents.join("\n");
752                let mut to_copy = file_contents;
753
754                match clipboard_context.get_contents() {
755                    Ok(_) => {
756                        let mut copy_message = String::new();
757                        if range.0 == range.1 {
758                            let line_number = range.0;
759                            let is_in_buffer = check_if_line_in_buffer(
760                                &state.buffer.contents,
761                                line_number,
762                            );
763                            if is_in_buffer.is_ok() {
764                                to_copy = state.buffer.contents[line_number - 1].clone();
765                                copy_message = format!("copying line {}", line_number);
766                            }
767                        } else {
768                            let is_in_buffer =
769                                check_if_line_in_buffer(&state.buffer.contents, range.0)
770                                    .is_ok()
771                                    && check_if_line_in_buffer(
772                                        &state.buffer.contents,
773                                        range.1,
774                                    )
775                                    .is_ok();
776                            if is_in_buffer {
777                                to_copy =
778                                    state.buffer.contents[range.0 - 1..range.1].join("\n");
779                                copy_message =
780                                    format!("copying lines {} to {}", range.0, range.1);
781                            }
782                        }
783                        clipboard_context.set_contents(to_copy).unwrap();
784                        return copy_message;
785                    }
786                    Err(e) => return format!("copy failed, because {}", e),
787                }
788            } else {
789                format!("copy what?\ntry ~copy start~end")
790            }
791        }),
792    }
793}
794
795/// Perform a regex `replace()` on `line_number`, with the `pattern` and `replacement`.
796/// Provides functionality for the `~substitute` command.
797pub fn substitute() -> Command {
798    Command {
799        name: "substitute",
800        arguments: vec!["range", "pattern/replacement"],
801        description: "perform a regex `replace()` on the given range",
802        documentation: "performs a regular expression replacement on the chosen \
803                        [range] - anything matched by [pattern] will be \
804                        replaced with [replacement]",
805        scope: CommandScope::Global,
806        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
807            if args.len() < 3 {
808                return format!(
809                    "substitute what?\ntry ~substitute [range] [pattern/replacement]"
810                );
811            }
812
813            let range = match parse_tilde_range(args[1], state) {
814                Some(range) => range,
815                None => return format!("range is improperly formatted"),
816            };
817            let selector = args[2..].join(" ");
818            let (pattern, replacement) = match selector.split_once('/') {
819                Some((pattern, replacement)) => (pattern, replacement),
820                None => {
821                    return format!(
822                        "substitute what?\ntry ~substitute [range] [pattern/replacement]"
823                    )
824                }
825            };
826
827            for line_number in range.0..=range.1 {
828                let is_in_buffer =
829                    check_if_line_in_buffer(&state.buffer.contents, line_number);
830                if is_in_buffer.is_ok() {
831                    let index = line_number - 1;
832                    let line = &mut state.buffer.contents[index];
833                    match Regex::new(pattern) {
834                        Ok(re) => {
835                            let replaced_line =
836                                re.replace_all(line, replacement).to_string();
837                            *line = replaced_line;
838                        }
839                        Err(e) => {
840                            let error_message = e.to_string();
841                            let lines: Vec<String> =
842                                error_message.lines().map(String::from).collect();
843
844                            if let Some(error) = lines.last() {
845                                return format!(
846                                    "substitute failed, because {}",
847                                    error.to_lowercase().replace("error: ", "")
848                                );
849                            } else {
850                                return format!("substitute failed, for some reason");
851                            }
852                        }
853                    }
854                } else {
855                    return is_in_buffer.unwrap_err();
856                }
857            }
858
859            if range.0 == range.1 {
860                return format!(
861                    "substituted '{pattern}' on line {} with '{replacement}'",
862                    range.0
863                );
864            } else {
865                return format!(
866                    "substituted '{pattern}' on lines {}~{} with '{replacement}'",
867                    range.0, range.1
868                );
869            }
870        }),
871    }
872}
873
874/// Searches for the given `term` in the `file_buffer` and prints matching lines.
875/// Provides functionality for the `~search` command.
876pub fn search() -> Command {
877    Command {
878        name: "search",
879        arguments: vec!["term"],
880        description: "search for a term in the buffer",
881        documentation: "this command searches for [term] in the file buffer and \
882                        prints every line that matches along with their line \
883                        numbers",
884        scope: CommandScope::Global,
885        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
886            let file_buffer = &mut state.buffer.contents;
887            let term = match args.get(1).map(|&s| s) {
888                Some(term) => term,
889                None => return format!("search what?\ntry ~search term"),
890            };
891
892            let mut matches: Vec<String> = Vec::new();
893
894            let escaped_term = regex::escape(term);
895
896            let regex = Regex::new(escaped_term.as_str()).unwrap();
897
898            for (line_number, line) in file_buffer.iter().enumerate() {
899                if regex.is_match(line) {
900                    matches.push(format!("line {}: {}", line_number + 1, line));
901                }
902            }
903
904            if matches.is_empty() {
905                return format!("no matches found for {}", term);
906            } else {
907                format!("{}", matches.join("\n"))
908            }
909        }),
910    }
911}
912
913/// Run a shell command with the OS shell, and fall back to a shell built-in if it fails.
914/// Provides functionality for the `~run` command.
915pub fn shell_command() -> Command {
916    Command {
917        name: "run",
918        arguments: vec!["shell_command", "command_args..."],
919        description: "run a shell command",
920        documentation: "uses your system command-line shell to run a shell command \
921                        and prints the captured standard output\n\
922                        the command will prioritise binary files over built-in \
923                        shell commands, so `/usr/bin/ls` will take precedence over \
924                        a built-in `ls` command if the former exists",
925        scope: CommandScope::Global,
926        action: CommandAction::new(|args: Vec<&str>, _state: &mut EditorState| {
927            let mut command_args = args;
928
929            if command_args.len() <= 1 {
930                return format!("run what?");
931            } else {
932                let fallback_shell = if cfg!(windows) {
933                    if which("pwsh").is_ok() {
934                        // Windows 10 and 11
935                        "pwsh"
936                    } else {
937                        // Windows <= 8
938                        "powershell"
939                    }
940                } else {
941                    // Literally every *nix system
942                    "sh"
943                };
944
945                // We will try to use the user's preferred shell first
946                let shell = if env::var("SHELL").is_ok() {
947                    env::var("SHELL").unwrap()
948                } else {
949                    fallback_shell.to_string()
950                };
951
952                let arg = "-c";
953                let command = command_args[1];
954
955                match which(command) {
956                    Ok(path) => println!("running {}", path.to_string_lossy()),
957                    Err(_) => {
958                        println!("{} wasn't found; trying to run it anyway", &command)
959                    }
960                }
961
962                command_args.drain(0..1);
963
964                let cmd = ShellCommand::new(shell)
965                    .arg(arg)
966                    .arg(command_args.join(" "))
967                    .output()
968                    .expect("command failed");
969
970                let out = String::from_utf8(cmd.stdout)
971                    .unwrap_or(String::from("no output"))
972                    .trim()
973                    .to_string();
974
975                if out.len() < 1 {
976                    return String::from("no output");
977                }
978                return out;
979            }
980        }),
981    }
982}
983
984/// Passes the current `buffer_contents` to `shell_command`.
985/// Provides functionality for the `~runhere` command.
986pub fn shell_command_with_file() -> Command {
987    Command {
988        name: "runhere",
989        arguments: vec!["shell_command", "command_args..."],
990        description: "run a shell command with the current buffer contents",
991        documentation: "uses your system command-line shell to run a shell command \
992                        with the contents of the current buffer as an argument\n\
993                        the command will create a temporary file with your file \
994                        buffer contents and pass that file's name to the shell \
995                        command\n\
996                        any modifications made to the temporary file will be \
997                        synchronised back with the file buffer when the command \
998                        completes",
999        scope: CommandScope::Global,
1000        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
1001            let shell_command = match args.get(1) {
1002                Some(command) => command,
1003                None => return format!("run what?"),
1004            };
1005            let file_name = args.get(2);
1006            let mut command_args = match args.get(3) {
1007                Some(_) => args[3..]
1008                    .iter()
1009                    .map(|s| s.to_string())
1010                    .collect::<Vec<String>>(),
1011                None => Vec::new(),
1012            };
1013
1014            let mut edited_buffer = false;
1015            if state.buffer.contents.is_empty() {
1016                return format!("no buffer contents");
1017            } else {
1018                let temporary_file_name: String = if file_name.is_some() {
1019                    let file = file_name.unwrap();
1020                    if file.contains(".") {
1021                        format!("{}", file.replace(".", "-temp."))
1022                    } else {
1023                        format!("{}.temp", file)
1024                    }
1025                } else {
1026                    /* Do we need a random hex string? No. Is it cool anyway? YES. */
1027                    let hex_string: String = (0..8)
1028                        .map(|_| {
1029                            let random_digit = rand::thread_rng().gen_range(0..16);
1030                            format!("{:x}", random_digit)
1031                        })
1032                        .collect();
1033
1034                    format!("{}.temp", hex_string)
1035                };
1036
1037                if fs::write(&temporary_file_name, state.buffer.contents.join("\n"))
1038                    .is_err()
1039                {
1040                    return format!("couldn't write temporary file");
1041                }
1042
1043                let fallback_shell = if cfg!(windows) {
1044                    if which("pwsh").is_ok() {
1045                        "pwsh"
1046                    } else {
1047                        "powershell"
1048                    }
1049                } else {
1050                    "sh"
1051                };
1052
1053                let shell = if env::var("SHELL").is_ok() {
1054                    env::var("SHELL").unwrap()
1055                } else {
1056                    fallback_shell.to_string()
1057                };
1058
1059                let arg = "-c";
1060
1061                match which(shell_command) {
1062                    Ok(path) => println!("running {}", path.to_string_lossy()),
1063                    Err(_) => {
1064                        println!("{} wasn't found; trying to run it anyway", &shell_command)
1065                    }
1066                }
1067
1068                if command_args.len() >= 1 {
1069                    command_args.drain(0..1);
1070                }
1071
1072                let constructed_command = format!(
1073                    "{} {} {}",
1074                    shell_command,
1075                    temporary_file_name.clone(),
1076                    command_args.join(" "),
1077                );
1078
1079                ShellCommand::new(shell)
1080                    .arg(arg)
1081                    .arg(constructed_command)
1082                    .status()
1083                    .expect("command failed");
1084
1085                let contents = fs::read_to_string(&temporary_file_name).unwrap();
1086
1087                if contents != state.buffer.contents.join("\n") {
1088                    state.buffer.contents =
1089                        contents.lines().map(|s| s.to_string()).collect();
1090                    edited_buffer = true;
1091                }
1092
1093                fs::remove_file(&temporary_file_name).unwrap_or_default();
1094            }
1095
1096            if edited_buffer {
1097                format!(
1098                    "finished running {} on file; changes synchronised",
1099                    shell_command
1100                )
1101            } else {
1102                format!("finished running {} on file", shell_command)
1103            }
1104        }),
1105    }
1106}
1107
1108/// Indent all lines of `range` by `indentation` spaces.
1109/// Used for the `~indent` command.
1110pub fn indent() -> Command {
1111    Command {
1112        name: "indent",
1113        arguments: vec!["range", "indentation"],
1114        description: "indent a range",
1115        documentation: "this command inserts space characters before every line \
1116                        in [range] by [indentation] spaces, effectively indenting \
1117                        it\n\
1118                        passing a negative [indentation] will unindent",
1119        scope: CommandScope::Global,
1120        action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
1121            if args.len() < 2 {
1122                return "indent what?\ntry ~indent [range] [indentation]".to_string();
1123            }
1124            if args.len() < 3 {
1125                let range = match parse_tilde_range(args[1], state) {
1126                    Some(range) => range,
1127                    None => return format!("range is improperly formatted"),
1128                };
1129                if range.0 == range.1 {
1130                    return format!("indent line {} by how many spaces?\ntry ~indent [range] [indentation]", range.0);
1131                }
1132                return format!("indent lines {}~{} by how many spaces?\ntry ~indent [range] [indentation]", range.0, range.1);
1133            }
1134
1135            let range: (usize, usize) = match parse_tilde_range(args[1], state) {
1136                Some(range) => range,
1137                None => return format!("range is improperly formatted"),
1138            };
1139            let indentation: isize = match args[2].parse() {
1140                Ok(arg) => arg,
1141                Err(_) => return format!("arg [indentation] must be isize"),
1142            };
1143
1144            for line_number in range.0..=range.1 {
1145                let is_in_buffer =
1146                    check_if_line_in_buffer(&mut state.buffer.contents, line_number);
1147                if is_in_buffer.is_ok() {
1148                    let index = line_number - 1;
1149                    let line = &mut state.buffer.contents[index];
1150                    match indentation.cmp(&0) {
1151                        Ordering::Greater => {
1152                            let indented_line = format!(
1153                                "{:indent$}{}",
1154                                "",
1155                                line,
1156                                indent = indentation as usize
1157                            );
1158                            *line = indented_line;
1159                        }
1160                        Ordering::Less => {
1161                            let line_len = line.len() as isize;
1162                            let new_len = (line_len + indentation).max(0) as usize;
1163                            let indented_line = format!(
1164                                "{:indent$}",
1165                                &line[line_len as usize - new_len..],
1166                                indent = new_len
1167                            );
1168                            *line = indented_line;
1169                        }
1170                        _ => return "can't indent by no spaces".to_string(),
1171                    }
1172                } else {
1173                    return is_in_buffer.unwrap_err();
1174                }
1175            }
1176
1177            if range.0 == range.1 {
1178                format!("indented line {} by {} spaces", range.0, indentation)
1179            } else {
1180                format!(
1181                    "indented lines {}~{} by {} spaces",
1182                    range.0, range.1, indentation
1183                )
1184            }
1185        }),
1186    }
1187}
1188
1189/// Displays a Blue Screen of Death-like error message.
1190/// Technically I don't need it, but it's funny.
1191pub fn crash() -> Command {
1192    Command {
1193        name: "bsod",
1194        arguments: vec![],
1195        description: "'crash' the editor",
1196        documentation: "this command resembles a blue screen of death and forces \
1197                        the editor to exit with a non-zero exit code\n\
1198                        this command is omitted from {~}test for obvious reasons",
1199        scope: CommandScope::Global,
1200        action: CommandAction::new(|args: Vec<&str>, _state: &mut EditorState| {
1201            let error_code = args.get(1).map(|&s| s).unwrap_or("USER_FRICKED_UP");
1202            let hex_codes = if args.len() >= 2 {
1203                args[2..]
1204                    .iter()
1205                    .map(|s| s.parse().unwrap())
1206                    .collect::<Vec<u32>>()
1207            } else {
1208                vec![]
1209            };
1210
1211            let mut populated_hex_codes = [0x00000000; 4];
1212            let num_values = hex_codes.len().min(4);
1213            populated_hex_codes[..num_values].copy_from_slice(&hex_codes[..num_values]);
1214
1215            eprintln!(
1216                "stop: {}: 0x{:08X} (0x{:08X},0x{:08X},0x{:08X})",
1217                error_code.to_uppercase(),
1218                populated_hex_codes[0],
1219                populated_hex_codes[1],
1220                populated_hex_codes[2],
1221                populated_hex_codes[3],
1222            );
1223            std::process::exit(1);
1224        }),
1225    }
1226}
1227
1228/// A nothing function that does nothing.
1229///
1230/// Used to provide functionality for the `~nothing` command.
1231pub fn nothing() -> Command {
1232    Command {
1233        name: "nothing",
1234        description: "do nothing",
1235        documentation: "this command will effectively do nothing of value, \
1236                        simply displaying the current buffer contents joined \
1237                        by semicolons instead of newlines",
1238        action: CommandAction::new(|_args: Vec<&str>, state: &mut EditorState| {
1239                if state.buffer.contents.is_empty() {
1240                    return format!("no buffer contents");
1241                }
1242                let buffer_contents: String = state.buffer.contents.join("; ");
1243                format!("doing nothing with {}", buffer_contents)
1244            },
1245        ),
1246        ..Command::default()
1247    }
1248}
1249
1250/// A joke command that "flips one bit".
1251///
1252/// References [xkcd's "Real Programmers" comic](https://xkcd.com/378/>).
1253pub fn butterfly() -> Command {
1254    Command {
1255        name: "butterfly",
1256        arguments: vec![],
1257        description: "it's what real programmers use!",
1258        documentation: "if you don't understand this command, you're not a REAL \
1259                        programmer!\n\
1260                        in all seriousness, this command is a reference to the \
1261                        xkcd webcomic, specifically #378, \"Real Programmers\"\n\
1262                        you can read the comic at https://xkcd.com/378/",
1263        scope: CommandScope::Global,
1264        action: CommandAction::new(|_args: Vec<&str>, _state: &mut EditorState| {
1265            // `nano`? Real programmers use `emacs`.
1266            // Hey. REAL programmers use `vim`.
1267            // Well, REAL programmers use `ed`.
1268            // No, REAL programmers use `cat`.
1269            // REAL programmers use a magnetized needle and a steady hand.
1270            /* Excuse me, but REAL programmers use butterflies.
1271                * They open their hands and let the delicate wings flap once.
1272                * The disturbances ripple outward, changing the flow of the
1273                eddy currents in the upper atmosphere.
1274                * These cause momentary pockets of higher-pressure air to form,
1275                which act as lenses that deflect incoming cosmic rays,
1276                focusing them to strike the drive platter and flip the
1277                desired bit.
1278            */
1279            "successfully flipped one bit".to_string()
1280            // Nice. 'Course, there's an Emacs command to do that.
1281            // Oh, yeah! Good old C-x M-c M-butterfly...
1282            // Dammit, Emacs.
1283        }),
1284    }
1285}