1use crate::{
2 AllowIncomplete, Args, IncompleteInput, InterpretResult, assignment::Assignment, evaluate,
3 interpret,
4};
5use rustc_hash::FxHashMap;
6use rustyline::EventHandler::Conditional;
7use rustyline::history::DefaultHistory;
8use rustyline::{
9 Cmd, ConditionalEventHandler, Editor, Event, EventContext, KeyEvent, Movement, RepeatCount,
10 error::ReadlineError,
11};
12use smol_str::SmolStr;
13use snix_eval::{GlobalsMap, SourceCode, Value};
14use snix_glue::snix_store_io::SnixStoreIO;
15use std::io::Write;
16use std::path::PathBuf;
17use std::process::Command;
18use std::rc::Rc;
19use std::{env, fs};
20use tracing::warn;
21
22fn state_dir() -> Option<PathBuf> {
23 let mut path = dirs::data_dir();
24 if let Some(p) = path.as_mut() {
25 p.push("snix")
26 }
27 path
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub(crate) enum ReplCommand<'a> {
32 Expr(&'a str),
33 Assign(Assignment<'a>),
34 Explain(&'a str),
35 Print(&'a str),
36 Quit,
37 Help,
38}
39
40impl<'a> ReplCommand<'a> {
41 const HELP: &'static str = "
42Welcome to the Snix REPL!
43
44The following commands are supported:
45
46 <expr> Evaluate a Nix language expression and print the result, along with its inferred type
47 <x> = <expr> Bind the result of an expression to a variable
48 :d <expr> Evaluate a Nix language expression and print a detailed description of the result
49 :p <expr> Evaluate a Nix language expression and print the result recursively
50 :q Exit the REPL
51 :?, :h Display this help text
52";
53
54 pub fn parse(input: &'a str) -> Self {
55 if input.starts_with(':') {
56 if let Some(without_prefix) = input.strip_prefix(":d ") {
57 return Self::Explain(without_prefix);
58 } else if let Some(without_prefix) = input.strip_prefix(":p ") {
59 return Self::Print(without_prefix);
60 }
61
62 let input = input.trim_end();
63 match input {
64 ":q" => return Self::Quit,
65 ":h" | ":?" => return Self::Help,
66 _ => {}
67 }
68 }
69
70 if let Some(assignment) = Assignment::parse(input) {
71 return Self::Assign(assignment);
72 }
73
74 Self::Expr(input)
75 }
76}
77
78pub struct CommandResult {
79 output: String,
80 continue_: bool,
81}
82
83impl CommandResult {
84 pub fn finalize<E: Write>(self, stdout: &mut E) -> bool {
85 write!(stdout, "{}", self.output).unwrap();
86 self.continue_
87 }
88
89 pub fn output(&self) -> &str {
90 &self.output
91 }
92}
93
94pub struct Repl<'a> {
95 multiline_input: Option<String>,
97 rl: Editor<(), DefaultHistory>,
98 env: FxHashMap<SmolStr, Value>,
100
101 io_handle: Rc<SnixStoreIO>,
102 args: &'a Args,
103 source_map: SourceCode,
104 globals: Option<Rc<GlobalsMap>>,
105}
106
107struct BufferEditor;
108impl ConditionalEventHandler for BufferEditor {
109 fn handle(
110 &self,
111 _event: &Event,
112 _repeat_count: RepeatCount,
113 _positive: bool,
114 ctx: &EventContext<'_>,
115 ) -> Option<Cmd> {
116 let tempdir = tempfile::tempdir()
117 .inspect_err(|e| {
118 warn!(err=%e, "failed to create tempdir for editing");
119 })
120 .ok()?;
121
122 let editor_cmd = env::var("VISUAL")
123 .or_else(|_| env::var("EDITOR"))
124 .unwrap_or_else(|_| String::from("nano"));
125
126 let path = tempdir.path().join("expression.nix");
127
128 fs::write(&path, ctx.line())
129 .inspect_err(|e| {
130 warn!(err=%e, "failed to write buffer to file");
131 })
132 .ok()?;
133
134 Command::new(&editor_cmd)
135 .arg(&path)
136 .status()
137 .inspect_err(|e| {
138 warn!(err=%e, "editor returned with error");
139 })
140 .ok()?;
141
142 let new_content = fs::read_to_string(&path)
143 .inspect_err(|e| {
144 warn!(err=%e, "failed to read back in edited buffer");
145 })
146 .ok()?;
147
148 Some(Cmd::Replace(Movement::WholeBuffer, Some(new_content)))
149 }
150}
151
152impl<'a> Repl<'a> {
153 pub fn new(io_handle: Rc<SnixStoreIO>, args: &'a Args) -> Self {
154 let mut rl =
155 Editor::<(), DefaultHistory>::new().expect("should be able to launch rustyline");
156
157 Self::binds(&mut rl);
159
160 Self {
161 multiline_input: None,
162 rl,
163 env: FxHashMap::default(),
164 io_handle,
165 args,
166 source_map: Default::default(),
167 globals: None,
168 }
169 }
170
171 fn binds(rl: &mut Editor<(), DefaultHistory>) {
172 rl.bind_sequence(KeyEvent::ctrl('o'), Conditional(Box::new(BufferEditor)));
173 }
174
175 pub fn run<O: Write + Clone + Send, E: Write + Clone + Send>(
176 &mut self,
177 stdout: &mut O,
178 stderr: &mut E,
179 ) {
180 if self.args.compile_only {
181 writeln!(
182 stderr,
183 "warning: `--compile-only` has no effect on REPL usage!"
184 )
185 .unwrap();
186 }
187
188 let history_path = match state_dir() {
189 Some(mut path) => {
192 let _ = std::fs::create_dir_all(&path);
193 path.push("history.txt");
194 let _ = self.rl.load_history(&path);
195 Some(path)
196 }
197
198 None => None,
199 };
200
201 loop {
202 let prompt = if self.multiline_input.is_some() {
203 " > "
204 } else {
205 "snix-repl> "
206 };
207
208 let readline = self.rl.readline(prompt);
209 match readline {
210 Ok(line) => {
211 if !self.send(stderr, line).finalize(stdout) {
212 break;
213 }
214 }
215 Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break,
216
217 Err(err) => {
218 writeln!(stderr, "error: {err}").unwrap();
219 break;
220 }
221 }
222 }
223
224 if let Some(path) = history_path {
225 self.rl.save_history(&path).unwrap();
226 }
227 }
228
229 pub fn send<E: Write + Clone + Send>(&mut self, stderr: &mut E, line: String) -> CommandResult {
232 if line.is_empty() {
233 return CommandResult {
234 output: String::new(),
235 continue_: true,
236 };
237 }
238
239 let input = if let Some(mi) = &mut self.multiline_input {
240 mi.push('\n');
241 mi.push_str(&line);
242 mi
243 } else {
244 &line
245 };
246
247 let res = match ReplCommand::parse(input) {
248 ReplCommand::Quit => {
249 return CommandResult {
250 output: String::new(),
251 continue_: false,
252 };
253 }
254 ReplCommand::Help => {
255 println!("{}", ReplCommand::HELP);
256 Ok(InterpretResult::empty_success(None))
257 }
258 ReplCommand::Expr(input) => interpret(
259 stderr,
260 Rc::clone(&self.io_handle),
261 input,
262 None,
263 self.args,
264 false,
265 AllowIncomplete::Allow,
266 Some(&self.env),
267 self.globals.clone(),
268 Some(self.source_map.clone()),
269 ),
270 ReplCommand::Assign(Assignment { ident, value }) => {
271 match evaluate(
272 stderr,
273 Rc::clone(&self.io_handle),
274 &value.to_string(), None,
276 self.args,
277 AllowIncomplete::Allow,
278 Some(&self.env),
279 self.globals.clone(),
280 Some(self.source_map.clone()),
281 ) {
282 Ok(result) => {
283 if let Some(value) = result.value {
284 self.env.insert(ident.into(), value);
285 }
286 Ok(InterpretResult::empty_success(Some(result.globals)))
287 }
288 Err(incomplete) => Err(incomplete),
289 }
290 }
291 ReplCommand::Explain(input) => interpret(
292 stderr,
293 Rc::clone(&self.io_handle),
294 input,
295 None,
296 self.args,
297 true,
298 AllowIncomplete::Allow,
299 Some(&self.env),
300 self.globals.clone(),
301 Some(self.source_map.clone()),
302 ),
303 ReplCommand::Print(input) => interpret(
304 stderr,
305 Rc::clone(&self.io_handle),
306 input,
307 None,
308 &Args {
309 strict: true,
310 ..(self.args.clone())
311 },
312 false,
313 AllowIncomplete::Allow,
314 Some(&self.env),
315 self.globals.clone(),
316 Some(self.source_map.clone()),
317 ),
318 };
319
320 match res {
321 Ok(InterpretResult {
322 output,
323 globals,
324 success: _,
325 }) => {
326 let _ = self.rl.add_history_entry(input);
327 self.multiline_input = None;
328 if globals.is_some() {
329 self.globals = globals;
330 }
331 CommandResult {
332 output,
333 continue_: true,
334 }
335 }
336 Err(IncompleteInput) => {
337 if self.multiline_input.is_none() {
338 self.multiline_input = Some(line);
339 }
340 CommandResult {
341 output: String::new(),
342 continue_: true,
343 }
344 }
345 }
346 }
347}