diff --git a/justfile b/justfile index 27432b6..0577467 100644 --- a/justfile +++ b/justfile @@ -22,16 +22,16 @@ output_name := 'fsharp_tools_rs.so' output := output_dir / output_name [linux] -_deploy dir: build - cp {{dir}}/libfsharp_tools_rs.so {{output}} +_deploy dir: + cp {{dir / 'libfsharp_tools_rs.so'}} {{output}} [macos] -_deploy dir: build - cp {{dir}}/libfsharp_tools_rs.dylib {{output}} +_deploy dir: + cp {{dir / 'libfsharp_tools_rs.dylib'}} {{output}} [windows] -_deploy dir: build - copy {{dir}}/fsharp_tools_rs.dll {{output}} +_deploy dir: + copy {{dir / 'fsharp_tools_rs.dll'}} {{output}} deploy-debug: test build (_deploy debug_dir) diff --git a/lib/src/error.rs b/lib/src/error.rs index bc2ef7a..ae0d6a9 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -1,7 +1,7 @@ use std::fmt::Display; #[derive(Debug)] -pub(crate) enum Error { +pub enum Error { FileError(String), LockError(String), IOError(std::io::Error), diff --git a/lib/src/lib.rs b/lib/src/lib.rs index a5f19b4..6e61de9 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -17,11 +17,225 @@ use error::{OptionToLuaError, ResultToLuaError}; use xmltree::{Element, EmitterConfig, XMLNode}; use std::fs::File; -use std::io::{BufRead, BufReader, BufWriter, Cursor, Read, Seek, Write}; +use std::io::{BufReader, BufWriter, Cursor, Read, Write}; use std::path::{Path, PathBuf}; const LINE_ENDING: &str = if cfg!(unix) { "\n" } else { "\r\n" }; +#[derive(Debug, Clone)] +pub struct Project<'a> { + content: String, + indent_string: Option<&'a str>, +} +impl<'a> Project<'a> { + fn read_file(file: impl Read) -> Result { + let mut file = BufReader::new(file); + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + Ok(buf) + } + + pub fn open_with_indent(file: impl Read, indent_string: &'a str) -> Result { + Ok(Self { + content: Self::read_file(file)?, + indent_string: Some(indent_string), + }) + } + + pub fn open(file: impl Read) -> Result { + Ok(Self { + content: Self::read_file(file)?, + indent_string: None, + }) + } + + pub fn write(&self, output: &mut impl Write) -> Result<(), Error> { + let mut writer = BufWriter::new(output); + writer.write_all(self.content.as_bytes())?; + + Ok(()) + } + + pub fn set_indent_string(&mut self, indent_string: &'a str) { + self.indent_string = Some(indent_string); + } + + fn parse_root(&self) -> Result { + let content = Cursor::new(&self.content); + Element::parse(content) + .map_err(|e| Error::FileError(format!("Failed to parse project: {}", e.to_string()))) + } + + pub fn fix_start_and_end(&self, original: &Self) -> Result { + let original_lines = original.content.lines().collect::>(); + + if original.content.is_empty() || self.content.is_empty() { + let content = "".to_string(); + return Ok(Project { + content, + indent_string: self.indent_string, + }); + } + + let mut output = self + .content + .lines() + .map(|x| x.to_string()) + .collect::>(); + + output[0] = original_lines[0].to_string(); + + let mut joined = output.join(LINE_ENDING); + if original.content.ends_with(LINE_ENDING) { + if !joined.ends_with(LINE_ENDING) { + joined.push_str(LINE_ENDING); + } + } else { + if joined.ends_with(LINE_ENDING) { + joined = joined + .strip_suffix(LINE_ENDING) + .map(|x| x.to_string()) + .unwrap_or(joined); + } + } + + Ok(Project { + content: joined, + indent_string: self.indent_string, + }) + } + + pub fn get_files(&self) -> Result, Error> { + fn get_item_groups(element: &Element) -> impl Iterator { + element.children.iter().filter_map(|node| { + let elem = node.as_element()?; + + if elem.name == "ItemGroup" { + Some(elem) + } else { + None + } + }) + } + + let root = self.parse_root()?; + let item_groups = get_item_groups(&root); + + let files = item_groups.flat_map(|ig| { + ig.children + .iter() + .filter_map(|node| node.as_element()) + .filter(|elem| elem.name == "Compile") + }); + + let mut paths: Vec<_> = files.map(|e| e.attributes["Include"].clone()).collect(); + + for p in paths.iter_mut() { + let (name, _) = p + .split_once('.') + .ok_or(Error::FileError(format!("missing extension for {}", p)))?; + + *p = name.to_owned(); + } + + Ok(paths) + } + + fn from_element(element: &Element, indent: Option<&'a str>) -> Result { + let data_to_write = { + let mut buffer = BufWriter::new(Vec::::new()); + + let config = EmitterConfig::new() + .perform_indent(true) + .line_separator(LINE_ENDING) + .indent_string(indent.unwrap_or(" ").to_string()); + + element + .write_with_config(&mut buffer, config) + .map_err(|e| Error::FileError(e.to_string()))?; + + let buffer = buffer + .into_inner() + .map_err(|e| Error::FileError(e.to_string()))?; + String::from_utf8(buffer).map_err(|e| Error::FileError(e.to_string()))? + }; + + cfg_if! { + if #[cfg(debug_assertions)] { + write_log("to_write", &data_to_write)?; + } + } + + Ok(Self { + content: data_to_write, + indent_string: indent, + }) + } + + pub fn with_files>(&self, file_names: &[T]) -> Result { + let mut root = self.parse_root()?; + + fn replace_files_in_group>(group: &mut Element, file_names: &[T]) { + group.children.retain(|n| { + if let Some(elem) = n.as_element() { + return elem.name != "Compile"; + } + true + }); + + let file_names = file_names + .iter() + .rev() + .filter(|s| s.as_ref().trim().len() > 0); + + for item in file_names { + let mut element = Element::new("Compile"); + element + .attributes + .insert("Include".into(), format!("{}.fs", item.as_ref())); + group.children.push(XMLNode::Element(element)); + } + + group.children.reverse(); + } + + for child in root.children.iter_mut() { + if let XMLNode::Element(elem) = child { + if elem.name == "ItemGroup" + && elem + .children + .iter() + .find(|n| n.as_element().map(|e| e.name == "Compile").is_some()) + .is_some() + { + replace_files_in_group(elem, file_names); + break; + } + } + } + + let output = Self::from_element(&root, self.indent_string)?; + + output.fix_start_and_end(self) + } + + pub fn derive_indent(&self) -> Option { + fn get_prefix(line: &str, prefix: char) -> String { + line.chars().take_while(|c| c == &prefix).collect() + } + + self.content.lines().find_map(|line| { + let first_char = line.chars().next(); + if first_char == Some(' ') { + Some(get_prefix(&line, ' ')) + } else if first_char == Some('\t') { + Some(get_prefix(&line, '\t')) + } else { + None + } + }) + } +} fn open_file_read(file_path: &str) -> Result { let file = File::open(file_path) @@ -40,47 +254,6 @@ fn open_file_write(file_path: &str) -> Result { .and_then(ExclusiveFileLock::new) } -fn parse_root(project: impl Read) -> Result { - Element::parse(project) - .map_err(|e| Error::FileError(format!("Failed to parse project: {}", e.to_string()))) -} - -fn get_item_groups(element: &Element) -> impl Iterator { - element.children.iter().filter_map(|node| { - let elem = node.as_element()?; - - if elem.name == "ItemGroup" { - Some(elem) - } else { - None - } - }) -} - -fn get_files_from_project(project: impl Read) -> Result, Error> { - let root = parse_root(project)?; - let item_groups = get_item_groups(&root); - - let files = item_groups.flat_map(|ig| { - ig.children - .iter() - .filter_map(|node| node.as_element()) - .filter(|elem| elem.name == "Compile") - }); - - let mut paths: Vec<_> = files.map(|e| e.attributes["Include"].clone()).collect(); - - for p in paths.iter_mut() { - let (name, _) = p - .split_once('.') - .ok_or(Error::FileError(format!("missing extension for {}", p)))?; - - *p = name.to_owned(); - } - - Ok(paths) -} - /// Takes the path to a file, then walks up the directory until it /// finds a fsproj file or hits max depth fn find_fsproj(file_path: &str, max_depth: i32) -> Option { @@ -122,144 +295,6 @@ fn find_fsproj(file_path: &str, max_depth: i32) -> Option { find_until(path, 0, max_depth).and_then(|path| Some(path.to_str()?.to_owned())) } -fn fix_start_and_end( - mut output_file: Output, - mut original_file: Original, -) -> Result -where - Output: Read + Seek, - Original: Read + Seek, -{ - fn read_lines(file: impl Read + Seek) -> Result, Error> { - BufReader::new(file) - .lines() - .map(|line| line.map_err(Error::file_error)) - .collect::>() - } - - output_file.rewind()?; - original_file.rewind()?; - - let original = { - let mut buf = String::new(); - BufReader::new(original_file) - .read_to_string(&mut buf) - .map_err(Error::file_error)?; - buf - }; - let original_lines = original.lines().collect::>(); - let mut output = read_lines(&mut output_file)?; - - if original.is_empty() || output.is_empty() { - return Ok("".to_string()); - } - - output[0] = original_lines[0].to_string(); - - let mut joined = output.join(LINE_ENDING); - if original.ends_with(LINE_ENDING) { - if !joined.ends_with(LINE_ENDING) { - joined.push_str(LINE_ENDING); - } - } else { - if joined.ends_with(LINE_ENDING) { - joined = joined - .strip_suffix(LINE_ENDING) - .map(|x| x.to_string()) - .unwrap_or(joined); - } - } - - Ok(joined) -} - -fn set_files_in_project>( - project: impl Read, - file_names: &[T], -) -> Result { - let mut root = parse_root(project)?; - - fn replace_files_in_group>(group: &mut Element, file_names: &[T]) { - group.children.retain(|n| { - if let Some(elem) = n.as_element() { - return elem.name != "Compile"; - } - true - }); - - let file_names = file_names - .iter() - .rev() - .filter(|s| s.as_ref().trim().len() > 0); - - for item in file_names { - let mut element = Element::new("Compile"); - element - .attributes - .insert("Include".into(), format!("{}.fs", item.as_ref())); - group.children.push(XMLNode::Element(element)); - } - - group.children.reverse(); - } - - for child in root.children.iter_mut() { - if let XMLNode::Element(elem) = child { - if elem.name == "ItemGroup" - && elem - .children - .iter() - .find(|n| n.as_element().map(|e| e.name == "Compile").is_some()) - .is_some() - { - replace_files_in_group(elem, file_names); - break; - } - } - } - - Ok(root) -} - -fn write_project(buf: &mut impl Write, element: &Element, indent: &str) -> Result<(), Error> { - let data_to_write = { - let mut buffer = BufWriter::new(Vec::::new()); - - let config = EmitterConfig::new() - .perform_indent(true) - .line_separator(LINE_ENDING) - .indent_string(indent.to_string()); - - element - .write_with_config(&mut buffer, config) - .map_err(|e| Error::FileError(e.to_string()))?; - - let buffer = buffer - .into_inner() - .map_err(|e| Error::FileError(e.to_string()))?; - String::from_utf8(buffer).map_err(|e| Error::FileError(e.to_string()))? - }; - - cfg_if! { - if #[cfg(debug_assertions)] { - write_log("to_write", &data_to_write)?; - } - } - - buf.write_all(data_to_write.as_bytes()) - .map_err(Error::IOError)?; - - Ok(()) -} - -fn write_project_to_string(element: &Element, indent: &str) -> Result { - // let mut file = open_file_write(file_path)?; - let mut buf = BufWriter::new(Vec::new()); - write_project(&mut buf, element, indent)?; - - String::from_utf8(buf.into_inner().unwrap()).map_err(Error::file_error) -} - #[cfg(debug_assertions)] fn write_log(name: &str, input: T) -> Result<(), Error> { let file_dir = if cfg!(unix) { @@ -292,26 +327,6 @@ fn get_file_name(file_path: &str) -> Option { .map(|x| x.to_owned()) } -/// Returns indent string -fn derive_file_indent_level(file: impl Read) -> Option { - let reader = BufReader::new(file); - - fn get_prefix(line: &str, prefix: char) -> String { - line.chars().take_while(|c| c == &prefix).collect() - } - - reader.lines().flatten().find_map(|line| { - let first_char = line.chars().next(); - if first_char == Some(' ') { - Some(get_prefix(&line, ' ')) - } else if first_char == Some('\t') { - Some(get_prefix(&line, '\t')) - } else { - None - } - }) -} - use mlua::prelude::*; #[mlua::lua_module(name = "fsharp_tools_rs")] @@ -331,9 +346,9 @@ fn module(lua: &Lua) -> LuaResult { "get_files_from_project", lua.create_function(|_, file_path: String| { let file = open_file_read(&file_path).to_lua_error()?; + let project = Project::open(file)?; - let result = get_files_from_project(file).to_lua_error()?; - Ok(result) + project.get_files().into_lua_err() })?, )?; @@ -341,33 +356,22 @@ fn module(lua: &Lua) -> LuaResult { "write_files_to_project", lua.create_function( |_, (file_path, files, indent): (String, Vec, Option)| { - let mut original = open_file_read(&file_path)?; - - let mut original_content = { - let mut buf = String::new(); - original.read_to_string(&mut buf)?; - Cursor::new(buf) - }; + let mut project = Project::open(open_file_read(&file_path)?)?; fn build_indent_string(size: u8) -> String { (0..size).map(|_| ' ').collect() } - let indent = derive_file_indent_level(&mut original_content) + let indent = project + .derive_indent() .or(indent.map(build_indent_string)) .unwrap_or(build_indent_string(2)); - original_content.rewind()?; - - let project = set_files_in_project(&mut original_content, &files)?; - original_content.rewind()?; - let output = Cursor::new(write_project_to_string(&project, &indent)?); - - drop(original); - let fixed = fix_start_and_end(output, original_content)?; + project.set_indent_string(indent.as_ref()); + let project = project.with_files(&files)?; let mut output_file = open_file_write(&file_path)?; - output_file.write_all(fixed.as_bytes())?; + project.write(&mut output_file)?; Ok(()) }, diff --git a/lib/src/tests/mod.rs b/lib/src/tests/mod.rs index 7a072e7..3828314 100644 --- a/lib/src/tests/mod.rs +++ b/lib/src/tests/mod.rs @@ -1,11 +1,10 @@ -use crate::{fix_start_and_end, write_project_to_string}; +use crate::{open_file_read, Project}; use pretty_assertions::assert_eq; use std::{ error::Error, io::Cursor, path::{Path, PathBuf}, }; -use xmltree::Element; type AnyResult = Result>; @@ -65,10 +64,10 @@ fn xml_parse() -> AnyResult<()> { .display() .to_string(); - let files = crate::get_files_from_project(crate::open_file_read(&with_version)?)?; + let project = Project::open(open_file_read(&with_version)?)?; assert_eq!( - files, + project.get_files()?, vec![ "One".to_string(), "Two".to_string(), @@ -78,10 +77,10 @@ fn xml_parse() -> AnyResult<()> { ] ); - let files = crate::get_files_from_project(crate::open_file_read(&without_version)?)?; + let project = Project::open(open_file_read(&without_version)?)?; assert_eq!( - files, + project.get_files()?, vec![ "One".to_string(), "Two".to_string(), @@ -100,24 +99,12 @@ fn set_files() -> AnyResult<()> { let expected_file = include_str!("files/projects/set_files_expected.fsproj"); - let expected = { - let src = expected_file; - let cursor = Cursor::new(src); - Element::parse(cursor) - } - .unwrap(); - let files = ["FileA", "FileB", "FileC"]; - let result = crate::set_files_in_project(original, &files)?; - - assert_eq!(result, expected); - - let result_string = write_project_to_string(&result, " ")?; + let project = Project::open_with_indent(original, " ")?; + let project = project.with_files(&files)?; - let fixed = fix_start_and_end(Cursor::new(result_string), Cursor::new(expected_file))?; - - core::assert_eq!(fixed, expected_file); + assert_eq!(project.content, expected_file); Ok(()) } @@ -133,7 +120,7 @@ fn get_file_name() { } #[test] -fn ignore_empty_lines() { +fn ignore_empty_lines() -> AnyResult<()> { let input = r#" @@ -155,16 +142,24 @@ fn ignore_empty_lines() { "# .as_bytes(); - let tree = - crate::set_files_in_project(input, &["a", "b", "", " ", " ", "c"]).unwrap(); + let project = Project::open_with_indent(input, " ")?.with_files(&[ + "a", + "b", + "", + " ", + " ", + "c", + ])?; let files = { let mut buf = Cursor::new(vec![]); - crate::write_project(&mut buf, &tree, " ").unwrap(); + project.write(&mut buf)?; buf.set_position(0); - crate::get_files_from_project(buf).unwrap() + Project::open_with_indent(&mut buf, " ")?.get_files()? }; assert_eq!(files, vec!["a", "b", "c"]); + + Ok(()) }