Skip to main content

indigo_core/mode/
command.rs

1use crate::{buffer::Buffer, editor::Editor, mode::prompt, window::WindowState};
2use anyhow::anyhow;
3use camino::Utf8PathBuf;
4use std::sync::Arc;
5
6#[cfg(feature = "arbitrary")]
7use arbitrary::Arbitrary;
8
9pub type Action = Command;
10
11#[derive(Clone, Debug)]
12pub enum Command {
13    Nop,
14    Echo { error: bool, message: Vec<String> },
15    Edit { path: Utf8PathBuf },
16    Write { path: Option<Utf8PathBuf> },
17    Quit { exit_code: Option<u8> },
18    QuitForce { exit_code: Option<u8> },
19    WriteQuit { exit_code: Option<u8> },
20    Readonly,
21    Panic,
22}
23
24#[cfg(feature = "arbitrary")]
25impl<'a> Arbitrary<'a> for Command {
26    fn arbitrary(_u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
27        // TODO: Uhhh I don't know what's most useful here
28        Ok(Self::Nop)
29    }
30}
31
32pub fn handle_actions(editor: &mut Editor, actions: &[Action]) {
33    for action in actions {
34        handle_action(editor, action);
35    }
36}
37
38pub fn handle_action(editor: &mut Editor, action: &Action) {
39    handle_command(editor, action.clone());
40}
41
42pub fn enter_command_mode(editor: &mut Editor) {
43    prompt::enter(editor, "", |editor, input| match parse_command(input) {
44        Ok(command) => handle_command(editor, command),
45        Err(error) => editor.message = Some(Err(error.to_string())),
46    });
47}
48
49fn parse_command(command: &str) -> anyhow::Result<Command> {
50    use bpaf::{ParseFailure, Parser, construct, long, positional, pure};
51
52    let echo = {
53        let error = long("error").switch();
54        let message = positional("MESSAGE").many();
55        construct!(Command::Echo { error, message })
56            .to_options()
57            .command("echo")
58    };
59
60    let edit = {
61        let path = positional("PATH");
62        construct!(Command::Edit { path })
63            .to_options()
64            .command("edit")
65            .long("e")
66    };
67
68    let write = {
69        let path = positional("PATH").optional();
70        construct!(Command::Write { path })
71            .to_options()
72            .command("write")
73            .long("w")
74    };
75
76    let quit = {
77        let exit_code = positional("EXIT_CODE").optional();
78        construct!(Command::Quit { exit_code })
79            .to_options()
80            .command("quit")
81            .long("q")
82    };
83
84    let quit_force = {
85        let exit_code = positional("EXIT_CODE").optional();
86        construct!(Command::QuitForce { exit_code })
87            .to_options()
88            .command("quit!")
89            .long("q!")
90    };
91
92    let write_quit = {
93        let exit_code = positional("EXIT_CODE").optional();
94        construct!(Command::WriteQuit { exit_code })
95            .to_options()
96            .command("writequit")
97            .long("wq")
98    };
99
100    let readonly = pure(Command::Readonly)
101        .to_options()
102        .command("readonly")
103        .long("ro");
104
105    let panic = pure(Command::Panic).to_options().command("panic");
106
107    let parser = construct!([
108        echo, edit, write, quit, quit_force, write_quit, readonly, panic
109    ])
110    .to_options();
111
112    let Ok(args) = shell_words::split(command) else {
113        anyhow::bail!("Invalid command");
114    };
115
116    parser.run_inner(args.as_slice()).map_err(|failure| {
117        let message = match failure {
118            ParseFailure::Stderr(doc) => doc.monochrome(true),
119            ParseFailure::Stdout(doc, full) => doc.monochrome(full),
120            ParseFailure::Completion(s) => s,
121        };
122        anyhow!("{message}")
123    })
124}
125
126fn handle_command(editor: &mut Editor, command: Command) {
127    match command {
128        Command::Nop => {}
129        Command::Echo { error, message } => {
130            if error {
131                editor.message = Some(Err(message.join(" ")));
132            } else {
133                editor.message = Some(Ok(message.join(" ")));
134            }
135        }
136        Command::Edit { path } => {
137            // TODO: Edit existing buffer for path if one exists.
138            if let Ok(buffer) = Buffer::open(&editor.fs, &path) {
139                // In Kakoune, windows are inextricably linked to buffers, so changing buffer means
140                // changing window, creating a new one if necessary.
141                // https://github.com/mawww/kakoune/blob/afc035ac0e17a6280edec511dfe8da1c0dd565ab/doc/pages/scopes.asciidoc?plain=1#L29-L30
142                let buffer_key = editor.insert_buffer(buffer);
143                let window_state = WindowState::new(buffer_key);
144                let window_key = editor.insert_window(window_state);
145                assert!(editor.focus_window(window_key));
146                // TODO: Delete previous window. It's no longer focused, so (at the time of writing)
147                // that means it's not displayed, it's just hanging out unused in memory.
148            } else {
149                editor.message = Some(Err(format!("Failed to open {path}")));
150            }
151        }
152        Command::Write { path } => {
153            let fs = Arc::clone(&editor.fs);
154            let result = if let Some(path) = path {
155                editor.focused_buffer_mut().save_as(&fs, path)
156            } else {
157                editor.focused_buffer_mut().save(&fs)
158            };
159            if result.is_err() {
160                editor.message = Some(Err(String::from("Failed to save")));
161            }
162        }
163        Command::Quit { exit_code } => {
164            let mut modified = None;
165            for (_, buffer) in editor.buffers() {
166                if buffer.is_modified().unwrap_or(false) {
167                    modified = Some(
168                        buffer
169                            .path()
170                            .map_or("a scratch buffer", |path| path.as_str()),
171                    );
172                    break;
173                }
174            }
175            if let Some(modified) = modified {
176                editor.message = Some(Err(format!("Unsaved changes in {modified}")));
177            } else {
178                editor.exit(exit_code.unwrap_or(0));
179            }
180        }
181        Command::QuitForce { exit_code } => {
182            editor.exit(exit_code.unwrap_or(0));
183        }
184        Command::WriteQuit { exit_code } => {
185            let fs = Arc::clone(&editor.fs);
186            if editor.focused_buffer_mut().save(&fs).is_ok() {
187                editor.exit(exit_code.unwrap_or(0));
188            } else {
189                editor.message = Some(Err(String::from("Failed to save")));
190            }
191        }
192        Command::Readonly => {
193            let new_readonly = !editor.focused_buffer().text.readonly;
194            editor.focused_buffer_mut().text.readonly = new_readonly;
195            if new_readonly {
196                editor.message = Some(Ok(String::from("Buffer is now readonly")));
197            } else {
198                editor.message = Some(Ok(String::from("Buffer is no longer readonly")));
199            }
200        }
201        Command::Panic => panic!(),
202    }
203}