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
20pub fn register_all(registry: &mut CommandRegistry) {
24 #[cfg(feature = "informational")]
25 {
26 registry.add_command(about_sued()); registry.add_command(command_list()); registry.add_command(help()); registry.add_command(document()); }
31
32 #[cfg(feature = "inputoutput")]
33 {
34 registry.add_command(save()); registry.add_command(open()); registry.add_command(reopen()); registry.add_command(show()); registry.add_command(copy()); registry.add_command(search()); }
41
42 #[cfg(feature = "cursor")]
43 {
44 registry.add_command(point()); registry.add_command(overtake()); registry.add_command(move_up()); registry.add_command(move_down()); }
49
50 #[cfg(feature = "transformations")]
51 {
52 registry.add_command(shift()); registry.add_command(indent()); registry.add_command(delete()); registry.add_command(substitute()); registry.add_command(join()); }
58
59 #[cfg(feature = "shell")]
60 {
61 registry.add_command(shell_command()); registry.add_command(shell_command_with_file());}
64
65 #[cfg(feature = "fun")]
66 {
67 registry.add_command(crash()); registry.add_command(nothing()); registry.add_command(butterfly()); }
71
72 #[cfg(feature = "lua")]
73 {
74 registry.add_command(lua::eval()); registry.add_command(lua::script()); }
77
78 #[cfg(debug_assertions)]
79 registry.add_command(test_commands()); }
81
82pub 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
168pub 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
217pub 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
291pub 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
317pub 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
371pub 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
440pub 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
591pub 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 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
645pub 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
688pub 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
723pub 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
795pub 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
874pub 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
913pub 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 "pwsh"
936 } else {
937 "powershell"
939 }
940 } else {
941 "sh"
943 };
944
945 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
984pub 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 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
1108pub 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
1189pub 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
1228pub 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
1250pub 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 "successfully flipped one bit".to_string()
1280 }),
1284 }
1285}