From 8a95cdf045b30a0dde1444a01f51a05afb026d20 Mon Sep 17 00:00:00 2001 From: Michael Bleuez Date: Tue, 8 Feb 2022 23:28:56 +0100 Subject: [PATCH] deno REPL mode --- CHANGELOG.md | 1 + README.md | 5 +- ressources/install_all_compilers_ci.sh | 3 + src/interpreters/JS_TS_deno.rs | 241 ++++++++++++++++++++++++- 4 files changed, 240 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8971d640..daac6a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## v1.2.1 - F# support - Fix multiline display in floating windows +- Deno brings REPL support for Javascript and TypeScript ## v1.2 - Live mode (a @ChristianChiarulli idea and partial realisation) diff --git a/README.md b/README.md index ce1a8eb7..6e25141f 100755 --- a/README.md +++ b/README.md @@ -435,7 +435,7 @@ println!("-> {}", alphabet); | Go | Bloc | No | | Haskell | Line | No | | Java | Bloc | No | -| JavaScript | Bloc | No | +| JavaScript | Bloc | Yes\*\* (Deno)| | Julia | Bloc | Yes\*\* | | Lua | Bloc | No | | Lua-nvim | Bloc | Yes\*\* | @@ -449,8 +449,7 @@ println!("-> {}", alphabet); | Rust | Bloc | No | | SageMath | Import | Yes\*\* | | Scala | Bloc | No | -| TypeScript | Bloc | No | - +| TypeScript | Bloc | Yes\*\* (Deno)| Want support for your language? Submit an [issue](https://github.com/michaelb/sniprun/issues/new?assignees=&labels=new-langage-support&template=support-for--language-.md&title=), or even better, [contribute](CONTRIBUTING.md), it's easy! diff --git a/ressources/install_all_compilers_ci.sh b/ressources/install_all_compilers_ci.sh index 789db8b0..c4e80641 100755 --- a/ressources/install_all_compilers_ci.sh +++ b/ressources/install_all_compilers_ci.sh @@ -18,3 +18,6 @@ sudo apt install gprolog sudo apt install dotnet ./ressources/go_install.sh export PATH=$PATH:$HOME/golang/go/bin/ + +# deno for typescript and javascript +curl -fsSL https://deno.land/x/install/install.sh | sh diff --git a/src/interpreters/JS_TS_deno.rs b/src/interpreters/JS_TS_deno.rs index 8d1224ae..22c1668a 100644 --- a/src/interpreters/JS_TS_deno.rs +++ b/src/interpreters/JS_TS_deno.rs @@ -4,12 +4,102 @@ pub struct JS_TS_deno { support_level: SupportLevel, data: DataHolder, code: String, + cache_dir: String, + current_output_id: u32, language_work_dir: String, main_file_path: String, } -impl ReplLikeInterpreter for JS_TS_deno {} +impl JS_TS_deno { + fn wait_out_file( + &self, + out_path: String, + err_path: String, + id: u32, + ) -> Result { + let end_mark = String::from("sniprun_finished_id=") + &id.to_string(); + let start_mark = String::from("sniprun_started_id=") + &id.to_string(); + + info!( + "searching for things between {:?} and {:?}", + start_mark, end_mark + ); + + let mut out_contents = String::new(); + let mut err_contents = String::new(); + + let mut pause = std::time::Duration::from_millis(50); + let start = std::time::Instant::now(); + loop { + std::thread::sleep(pause); + pause = pause.saturating_add(std::time::Duration::from_millis(50)); + + // timeout after 30s if no result found + if start.elapsed().as_secs() > 30 { + return Err(SniprunError::InterpreterLimitationError(String::from( + "reached the 30s timeout", + ))); + } + + //check for stderr first + if let Ok(mut file) = std::fs::File::open(&err_path) { + info!("errfile exists"); + out_contents.clear(); + let res = file.read_to_string(&mut err_contents); + if res.is_ok() { + // info!("errfile could be read : {:?}", err_contents); + if err_contents.contains(&end_mark) { + if let Some(index) = err_contents.rfind(&start_mark) { + let mut err_to_display = err_contents + [index + start_mark.len()..err_contents.len() - end_mark.len() - 1] + .to_owned(); + info!("err to display : {:?}", err_to_display); + if !err_to_display.trim().is_empty() { + info!("err found"); + if err_to_display.lines().count() > 2 { + let mut err_to_display_vec = + err_to_display.lines().skip(2).collect::>(); + err_to_display_vec.dedup(); + err_to_display = err_to_display_vec.join("\n"); + } + + return Err(SniprunError::RuntimeError(err_to_display)); + } + } + } + } + } + + //check for stdout + if let Ok(mut file) = std::fs::File::open(&out_path) { + info!("file exists"); + out_contents.clear(); + let res = file.read_to_string(&mut out_contents); + if res.is_ok() { + // info!("out {}", out_contents); + let relevant_content: String = out_contents + .lines() + .filter(|l| !l.contains("undefined")) + .collect::>() + .join("\n"); + info!("relevant {}", relevant_content); + info!("file could be read : {:?}", relevant_content); + // info!("file : {:?}", contents); + if relevant_content.contains(&end_mark) { + info!("out found"); + let index = relevant_content.rfind(&start_mark).unwrap(); + return Ok(relevant_content[index + start_mark.len() + ..relevant_content.len() - end_mark.len() - 1] + .to_owned()); + } + } + } + + info!("not found yet"); + } + } +} impl Interpreter for JS_TS_deno { fn new_with_level(data: DataHolder, support_level: SupportLevel) -> Box { @@ -24,11 +114,13 @@ impl Interpreter for JS_TS_deno { //pre-create string pointing to main file's and binary's path let mfp = lwd.clone() + "/main.ts"; Box::new(JS_TS_deno { + cache_dir: lwd.clone() + "/" + &Python3_fifo::get_nvim_pid(&data), data, support_level, code: String::new(), language_work_dir: lwd, main_file_path: mfp, + current_output_id: 0, }) } @@ -68,6 +160,13 @@ impl Interpreter for JS_TS_deno { SupportLevel::Bloc } + fn behave_repl_like_default() -> bool { + true + } + + fn has_repl_capability() -> bool { + true + } fn fetch_code(&mut self) -> Result<(), SniprunError> { //note: you probably don't have to modify, or even understand this function @@ -95,7 +194,7 @@ impl Interpreter for JS_TS_deno { } // now self.code contains the line or bloc of code wanted :-) - info!("Typescript self.code) = {}", self.code); + info!("javascript/typescript self.code) = {}", self.code); Ok(()) } @@ -105,11 +204,11 @@ impl Interpreter for JS_TS_deno { fn build(&mut self) -> Result<(), SniprunError> { //write code to file - let mut _file = File::create(&self.main_file_path).expect("failed to create file for typescript_original"); + let mut _file = + File::create(&self.main_file_path).expect("failed to create file for js_ts_deno"); // io errors can be ignored, or handled into a proper sniprunerror // if you panic, it should not be too dangerous for anyone - write(&self.main_file_path, &self.code) - .expect("unable to write to file for typescript_original"); + write(&self.main_file_path, &self.code).expect("unable to write to file for js_ts_deno"); Ok(()) } @@ -136,17 +235,136 @@ impl Interpreter for JS_TS_deno { } } +impl ReplLikeInterpreter for JS_TS_deno { + fn fetch_code_repl(&mut self) -> Result<(), SniprunError> { + if !self.read_previous_code().is_empty() { + // nothing to do, kernel already running + info!("Deno kernel already running"); + + if let Some(id) = self.get_pid() { + // there is a race condition here but honestly you'd have to + // trigger it on purpose + self.current_output_id = id + 1; + self.set_pid(self.current_output_id); + } else { + info!("Could not retrieve a previous id even if the kernel is running"); + info!("This was in saved code: {}", self.read_previous_code()); + return Err(SniprunError::CustomError( + "Sniprun failed to connect to the running kernel, please SnipReset".to_string(), + )); + } + + self.fetch_code()?; + Ok(()) + } else { + // launch everything + self.set_pid(0); + + let init_repl_cmd = self.data.sniprun_root_dir.clone() + "/ressources/init_repl.sh"; + info!( + "launching kernel : {:?} on {:?}", + init_repl_cmd, &self.cache_dir + ); + + match daemon() { + Ok(Fork::Child) => { + let _res = Command::new("bash") + .args(&[ + init_repl_cmd, + self.cache_dir.clone(), + String::from("deno"), + String::from("repl"), + String::from("-q"), + ]) + .output() + .unwrap(); + let pause = std::time::Duration::from_millis(36_000_000); + std::thread::sleep(pause); + + return Err(SniprunError::CustomError( + "Timeout expired for python3 REPL".to_owned(), + )); + } + Ok(Fork::Parent(_)) => {} + Err(_) => { + info!("JS_TS_deno could not fork itself to the background to launch the kernel") + } + }; + + let pause = std::time::Duration::from_millis(100); + std::thread::sleep(pause); + self.save_code("kernel_launched\n".to_owned()); + + Err(SniprunError::CustomError( + "Deno kernel launched, re-run your snippet".to_owned(), + )) + } + } + + fn add_boilerplate_repl(&mut self) -> Result<(), SniprunError> { + self.add_boilerplate()?; + let start_mark = String::from("\nconsole.log(\"sniprun_started_id=") + + &self.current_output_id.to_string() + + "\")\n"; + let end_mark = String::from("\nconsole.log(\"sniprun_finished_id=") + + &self.current_output_id.to_string() + + "\")\n"; + let start_mark_err = String::from("\nconsole.error(\"sniprun_started_id=") + + &self.current_output_id.to_string() + + "\")\n"; + let end_mark_err = String::from("\nconsole.error(\"sniprun_finished_id=") + + &self.current_output_id.to_string() + + "\")\n"; + + // Removing empty lines + // self.code = self + // .code + // .lines() + // .filter(|l| !l.trim().is_empty()) + // .collect::>() + // .join("\n"); + + let all_code = String::from("\n") + &self.code + "\n\n"; + self.code = start_mark + &start_mark_err + &all_code + &end_mark + &end_mark_err; + Ok(()) + } + + fn build_repl(&mut self) -> Result<(), SniprunError> { + self.build() + } + + fn execute_repl(&mut self) -> Result { + let send_repl_cmd = self.data.sniprun_root_dir.clone() + "/ressources/launcher_repl.sh"; + info!("running launcher {}", send_repl_cmd); + let res = Command::new(send_repl_cmd) + .arg(self.main_file_path.clone()) + .arg(self.cache_dir.clone() + "/fifo_repl/pipe_in") + .spawn(); + info!("cmd status: {:?}", res); + res.expect("could not run launcher"); + // info!("launcher launched : {:?}", res); + + let outfile = self.cache_dir.clone() + "/fifo_repl/out_file"; + let errfile = self.cache_dir.clone() + "/fifo_repl/err_file"; + info!("outfile : {:?}", outfile); + self.wait_out_file(outfile, errfile, self.current_output_id) + } +} + #[cfg(test)] mod test_ts_js_deno_original { use super::*; + use serial_test::serial; + #[test] - fn simple_print() { + #[serial(deno)] + fn simple_print() { let mut data = DataHolder::new(); //inspired from Rust syntax data.current_bloc = String::from("let message: string = 'Hi';\nconsole.log(message);"); let mut interpreter = JS_TS_deno::new(data); - let res = interpreter.run(); + let res = interpreter.run_at_level(SupportLevel::Bloc); // -> should panic if not an Ok() let string_result = res.unwrap(); @@ -154,4 +372,13 @@ mod test_ts_js_deno_original { // -> compare result with predicted assert_eq!(string_result, "Hi\n"); } + #[test] + #[serial(deno)] + fn print_quote() { + let mut data = DataHolder::new(); + data.current_bloc = String::from("let message: string = 'Hi';\nconsole.log(message);"); + let mut interpreter = Python3_fifo::new(data); + let res = interpreter.run_at_level_repl(SupportLevel::Bloc); + assert!(res.is_err()); + } }