From e7fac8ebcfcfac959f2f91c85f46d6ca66e7a3e5 Mon Sep 17 00:00:00 2001 From: Michael B Date: Sat, 20 Jul 2024 12:35:45 +0200 Subject: [PATCH] add plantuml --- CHANGELOG.md | 3 + Cargo.lock | 2 +- doc/sources/interpreters/Plantuml_original.md | 22 ++ lua/sniprun/display.lua | 4 +- ressources/install_all_compilers_ci.sh | 4 + src/interpreters/Plantuml_original.rs | 265 ++++++++++++++++++ 6 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 doc/sources/interpreters/Plantuml_original.md create mode 100644 src/interpreters/Plantuml_original.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a4b5870f..33fc4bc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## v1.3.15 +- Add PlantUML support (ascii output) + ## v1.3.14 - Improve Lua\_nvim's handling of 'local' requires in REPL mode diff --git a/Cargo.lock b/Cargo.lock index de3f02dc..0064595e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -795,7 +795,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "sniprun" -version = "1.3.14-beta" +version = "1.3.15-beta" dependencies = [ "close_fds", "dirs", diff --git a/doc/sources/interpreters/Plantuml_original.md b/doc/sources/interpreters/Plantuml_original.md new file mode 100644 index 00000000..6fc39e9f --- /dev/null +++ b/doc/sources/interpreters/Plantuml_original.md @@ -0,0 +1,22 @@ +## PlantUML original + +This interpreter relies on `plantuml`, which needs to be available on the $PATH + + + +This interpreter supports the following options: + +```lua +require'sniprun'.setup({ + interpreter_options = { + Plantuml_original = { + output_mode = "-tutxt", --# one of the options allowed by plantuml + compiler = "/home/user/my_custom_plantuml_install/plantuml" + } + } + } +}) +``` +You can add options to the 'compiler' key, but do not set "-pipe" (or it'll break output), and "-failfast2", "-nbthread auto" are already set. + + diff --git a/lua/sniprun/display.lua b/lua/sniprun/display.lua index aca3b5b7..c5649f85 100644 --- a/lua/sniprun/display.lua +++ b/lua/sniprun/display.lua @@ -29,7 +29,9 @@ function M.fw_open(row, column, message, ok, temp) vim.api.nvim_buf_set_lines(bufnr,h,h+1,false,{line}) vim.api.nvim_buf_add_highlight(bufnr, namespace_id, hl, h,0,-1) -- highlight lines in floating window end - M.fw_handle = vim.api.nvim_open_win(bufnr, false, {relative='win', width=w+1, height=h, bufpos=bp, focusable=false, style='minimal',border=M.borders}) + if h ~= 0 then + M.fw_handle = vim.api.nvim_open_win(bufnr, false, {relative='win', width=w+1, height=h, bufpos=bp, focusable=false, style='minimal',border=M.borders}) + end end function M.term_set_window_handle() diff --git a/ressources/install_all_compilers_ci.sh b/ressources/install_all_compilers_ci.sh index a41a95bf..51216ac9 100755 --- a/ressources/install_all_compilers_ci.sh +++ b/ressources/install_all_compilers_ci.sh @@ -74,6 +74,10 @@ then sudo apt-get install clojure fi +if ! command -v plantuml &> /dev/null +then + sudo apt-get install plantuml +fi if ! command -v go &> /dev/null diff --git a/src/interpreters/Plantuml_original.rs b/src/interpreters/Plantuml_original.rs new file mode 100644 index 00000000..e852b422 --- /dev/null +++ b/src/interpreters/Plantuml_original.rs @@ -0,0 +1,265 @@ +// Be sure to read the CONTRIBUTING.md file :-) + +#[derive(Clone)] +#[allow(non_camel_case_types)] +// For example, Rust_original is a good name for the first rust interpreter +pub struct Plantuml_original { + support_level: SupportLevel, + data: DataHolder, + code: String, + + language_work_dir: String, + main_file_path: String, + output_mode: String, +} + +//necessary boilerplate, you don't need to implement that if you want a Bloc support level +//interpreter (the easiest && most common) +impl ReplLikeInterpreter for Plantuml_original {} + +impl Interpreter for Plantuml_original { + fn new_with_level(data: DataHolder, support_level: SupportLevel) -> Box { + //create a subfolder in the cache folder + let lwd = data.work_dir.clone() + "/plantuml_original"; + let mut builder = DirBuilder::new(); + builder.recursive(true); + builder + .create(&lwd) + .expect("Could not create directory for example"); + + let mfp = lwd.clone() + "/main.uml"; + Box::new(Plantuml_original { + data, + support_level, + code: String::new(), + language_work_dir: lwd, + main_file_path: mfp, + output_mode: String::new(), + }) + } + + fn get_supported_languages() -> Vec { + vec![ + String::from("PlantUML"), // in 1st position of vector, used for info only + //':set ft?' in nvim to get the filetype of opened file + String::from("puml"), + String::from("uml"), + String::from("pu"), + String::from("iuml"), + String::from("plantuml"), + ] + } + + fn get_name() -> String { + // get your interpreter name + String::from("Plantuml_original") + } + + fn get_current_level(&self) -> SupportLevel { + self.support_level + } + fn set_current_level(&mut self, level: SupportLevel) { + self.support_level = level; + } + + fn get_data(&self) -> DataHolder { + self.data.clone() + } + + fn default_for_filetype() -> bool { + true + } + + fn behave_repl_like_default() -> bool { + false + } + + fn has_repl_capability() -> bool { + false + } + + fn get_max_support_level() -> SupportLevel { + //define the max level support of the interpreter (see readme for definitions) + SupportLevel::Bloc + } + + fn fetch_code(&mut self) -> Result<(), SniprunError> { + let nvim_instance = self.data.nvim_instance.clone().unwrap(); + let mut real_nvim_instance = nvim_instance.lock().unwrap(); + //note: you probably don't have to modify, or even understand this function + + //check if we not on boundary of block, if so replace self.code with whole bloc + let line_n = self.data.range[0]; + if self + .data + .current_line + .trim_start() + .to_lowercase() + .starts_with("@startuml") + { + let end_line = real_nvim_instance + .get_current_buf() + .unwrap() + .line_count(&mut real_nvim_instance) + .unwrap(); + let capped_end_line = std::cmp::min(line_n + 400, end_line); // in case there is a very long file, don't search for nothing up to line 500k + let it = line_n + 1..capped_end_line + 1; + + let mut code_bloc = vec![]; + for i in it { + let line_i = real_nvim_instance + .get_current_buf() + .unwrap() + .get_lines(&mut real_nvim_instance, i - 1, i, false) + .unwrap() + .join(""); + if line_i.trim_start().to_lowercase().starts_with("@enduml") { + //found end of bloc + info!("found end of bloc at line {}", i); + self.data.current_bloc = code_bloc.join("\n"); + } else { + info!("adding line {} to current bloc", i); + code_bloc.push(line_i.to_string()); + } + } + } + // + //add code from data to self.code + if !self + .data + .current_bloc + .replace(&[' ', '\t', '\n', '\r'][..], "") + .is_empty() + && self.support_level >= SupportLevel::Bloc + { + // if bloc is not pseudo empty and has Bloc current support level, + // add fetched code to self + self.code = self.data.current_bloc.clone(); + + // if there is only data on current line / or Line is the max support level + } else if !self.data.current_line.replace(" ", "").is_empty() + && self.support_level >= SupportLevel::Line + { + self.code = self.data.current_line.clone(); + } else { + // no code was retrieved + self.code = String::from(""); + } + + // now self.code contains the line or bloc of code wanted :-) + Ok(()) + } + + fn add_boilerplate(&mut self) -> Result<(), SniprunError> { + if !self.code.contains("@startuml") { + self.code = String::from("@startuml\n") + &self.code; + } + if !self.code.contains("@enduml") { + self.code = self.code.clone() + "\n@enduml\n"; + } + Ok(()) + } + + fn build(&mut self) -> Result<(), SniprunError> { + let mut _file = File::create(&self.main_file_path) + .expect("Failed to create file for plantuml_original"); + // 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 plantuml_original"); + + self.output_mode = String::from("-tutxt"); + + if let Some(config_value) = + Plantuml_original::get_interpreter_option(&self.data, "output_mode") + { + if let Some(config_value_valid_string) = config_value.as_str() { + self.output_mode = config_value_valid_string.to_string(); + } + } + let allowed_output_modes = [ + "-tutxt", + "-thtml", + "-tsvg", + "-tpng", + "-tpdf", + "-teps", + "-teps:text", + "-tlatex", + "-tlatex:nopreamble", + "-ttxt", + ]; + if !allowed_output_modes.contains(&self.output_mode.as_str()) { + return Err(SniprunError::CustomError(format!( + "invalid output mode {}, allowed modes are {:?}", + self.output_mode, allowed_output_modes + ))); + } + + let compiler = Plantuml_original::get_compiler_or(&self.data, "plantuml"); + //compile it (to the bin_path that already points to the rigth path) + let output = Command::new(compiler.split_whitespace().next().unwrap()) + .args(compiler.split_whitespace().skip(1)) + .arg("-o") + .arg(&self.language_work_dir) + .arg(&self.output_mode) + .arg("-nbthread") + .arg("auto") + .arg("-failfast2") + .arg(self.main_file_path.clone()) + .output() + .expect("Unable to start process"); + if output.status.success() { + //return stdout + return Ok(()); + } else { + // return stderr + return Err(SniprunError::CompilationError( + String::from_utf8(output.stderr).unwrap(), + )); + } + } + + fn execute(&mut self) -> Result { + let extension = String::from(".") + &self.output_mode[2..]; + let content = std::fs::read_to_string(&self.main_file_path.replace(".uml", &extension)); + if let Ok(content) = content { + Ok(content) + } else { + return Err(SniprunError::RuntimeError( + format!("could not read output file {}", &self.main_file_path.replace(".uml", &extension) ), + )); + } + } +} + +// You can add tests if you want to +#[cfg(test)] +mod test_language_original { + use super::*; + use serial_test::serial; + + #[test] + #[serial] // multiple test of the same interpreter should run sequentially: + // after all, they write and read to the same dir/files + fn simple_print() { + let mut data = DataHolder::new(); + + //inspired from Rust syntax + data.current_bloc = String::from("@startuml\nparticipant Bob"); + let mut interpreter = Plantuml_original::new(data); + let res = interpreter.run(); + + // -> should panic if not an Ok() + let string_result = res.unwrap(); + + // -> compare result with predicted + assert!(string_result.contains("Bob")); + } + + #[test] + #[serial] + fn another_test() { + //another test + } +}