diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12a5736..0850ad5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,4 +43,4 @@ jobs: - name: Clippy run: cargo clippy --all -- -D warnings - name: Run tests - run: cargo test --all --verbose + run: cargo test --features span --features --v1 --all --verbose diff --git a/Cargo.toml b/Cargo.toml index c672be6..9e19ab9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ rust-version = "1.70.0" edition = "2021" [features] -default = ["span"] +default = ["span", "v1"] span = [] v1-fallback = ["v1"] v1 = ["kdlv1"] diff --git a/src/document.rs b/src/document.rs index f52930e..d9268b5 100644 --- a/src/document.rs +++ b/src/document.rs @@ -2,7 +2,7 @@ use miette::SourceSpan; use std::fmt::Display; -use crate::{FormatConfig, KdlError, KdlNode, KdlValue}; +use crate::{FormatConfig, KdlError, KdlNode, KdlNodeFormat, KdlValue}; /// Represents a KDL /// [`Document`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#document). @@ -344,15 +344,20 @@ impl KdlDocument { pub fn parse(s: &str) -> Result { #[cfg(not(feature = "v1-fallback"))] { - crate::v2_parser::try_parse(crate::v2_parser::document, s) + KdlDocument::parse_v2(s) } #[cfg(feature = "v1-fallback")] { - crate::v2_parser::try_parse(crate::v2_parser::document, s) - .or_else(|e| KdlDocument::parse_v1(s).map_err(|_| e)) + KdlDocument::parse_v2(s).or_else(|e| KdlDocument::parse_v1(s).map_err(|_| e)) } } + /// Parses a KDL v2 string into a document. + #[cfg(feature = "v1")] + pub fn parse_v2(s: &str) -> Result { + crate::v2_parser::try_parse(crate::v2_parser::document, s) + } + /// Parses a KDL v1 string into a document. #[cfg(feature = "v1")] pub fn parse_v1(s: &str) -> Result { @@ -365,9 +370,74 @@ impl KdlDocument { #[cfg(feature = "v1")] pub fn v1_to_v2(s: &str) -> Result { let mut doc = KdlDocument::parse_v1(s)?; - doc.autoformat(); + doc.ensure_v2(); Ok(doc.to_string()) } + + /// Takes a KDL v2 document string and returns the same document, but + /// autoformatted into valid KDL v2 syntax. + #[cfg(feature = "v1")] + pub fn v2_to_v1(s: &str) -> Result { + let mut doc = KdlDocument::parse_v2(s)?; + doc.ensure_v1(); + Ok(doc.to_string()) + } + + /// Makes sure this document is in v2 format. + #[cfg(feature = "v1")] + pub fn ensure_v2(&mut self) { + // No need to touch KdlDocumentFormat, probably. In the longer term, + // we'll want to make sure to parse out whitespace and comments and make + // sure they're actually compliant, but this is good enough for now. + for node in self.nodes_mut().iter_mut() { + node.ensure_v2(); + } + } + + /// Makes sure this document is in v1 format. + #[cfg(feature = "v1")] + pub fn ensure_v1(&mut self) { + // No need to touch KdlDocumentFormat, probably. In the longer term, + // we'll want to make sure to parse out whitespace and comments and make + // sure they're actually compliant, but this is good enough for now. + + // the last node in v1 docs/children has to have a semicolon. + let mut iter = self.nodes_mut().iter_mut().rev(); + let last = iter.next(); + let penult = iter.next(); + if let Some(last) = last { + if let Some(fmt) = last.format_mut() { + if !fmt.trailing.contains(";") + && fmt + .trailing + .chars() + .any(|c| crate::v2_parser::NEWLINES.iter().any(|nl| nl.contains(c))) + { + fmt.terminator = ";".into(); + } + } else { + let maybe_indent = { + if let Some(penult) = penult { + if let Some(fmt) = penult.format() { + fmt.leading.clone() + } else { + "".into() + } + } else { + "".into() + } + }; + last.format = Some(KdlNodeFormat { + leading: maybe_indent, + terminator: "\n".into(), + ..Default::default() + }) + } + } + for node in self.nodes_mut().iter_mut() { + node.ensure_v1(); + } + } } #[cfg(feature = "v1")] @@ -956,10 +1026,95 @@ inline { time; to; live "our" "dreams"; "y;all" } Ok(()) } - #[ignore = "Formatting is still seriously broken, and this is gonna need some extra love."] #[cfg(feature = "v1")] #[test] - fn v1_to_v2() -> miette::Result<()> { + fn v1_v2_conversions() -> miette::Result<()> { + let v1 = r##" +// If you'd like to override the default keybindings completely, be sure to change "keybinds" to "keybinds clear-defaults=true" +keybinds { + normal { + // uncomment this and adjust key if using copy_on_select=false + // bind "Alt c" { Copy; } + } + locked { + bind "Ctrl g" { SwitchToMode "Normal"; } + } + resize { + bind "Ctrl n" { SwitchToMode "Normal"; } + bind "h" "Left" { Resize "Increase Left"; } + bind "j" "Down" { Resize "Increase Down"; } + bind "k" "Up" { Resize "Increase Up"; } + bind "l" "Right" { Resize "Increase Right"; } + bind "H" { Resize "Decrease Left"; } + bind "J" { Resize "Decrease Down"; } + bind "K" { Resize "Decrease Up"; } + bind "L" { Resize "Decrease Right"; } + bind "=" "+" { Resize "Increase"; } + bind "-" { Resize "Decrease"; } + } +} +// Plugin aliases - can be used to change the implementation of Zellij +// changing these requires a restart to take effect +plugins { + tab-bar location="zellij:tab-bar" + status-bar location="zellij:status-bar" + welcome-screen location="zellij:session-manager" { + welcome_screen true + } + filepicker location="zellij:strider" { + cwd "\/" + } +} +mouse_mode false +mirror_session true +"##; + let v2 = r##" +// If you'd like to override the default keybindings completely, be sure to change "keybinds" to "keybinds clear-defaults=true" +keybinds { + normal { + // uncomment this and adjust key if using copy_on_select=false + // bind "Alt c" { Copy; } + } + locked { + bind "Ctrl g" { SwitchToMode Normal; } + } + resize { + bind "Ctrl n" { SwitchToMode Normal; } + bind h Left { Resize "Increase Left"; } + bind j Down { Resize "Increase Down"; } + bind k Up { Resize "Increase Up"; } + bind l Right { Resize "Increase Right"; } + bind H { Resize "Decrease Left"; } + bind J { Resize "Decrease Down"; } + bind K { Resize "Decrease Up"; } + bind L { Resize "Decrease Right"; } + bind "=" + { Resize Increase; } + bind - { Resize Decrease; } + } +} +// Plugin aliases - can be used to change the implementation of Zellij +// changing these requires a restart to take effect +plugins { + tab-bar location=zellij:tab-bar + status-bar location=zellij:status-bar + welcome-screen location=zellij:session-manager { + welcome_screen #true + } + filepicker location=zellij:strider { + cwd "/" + } +} +mouse_mode #false +mirror_session #true +"##; + pretty_assertions::assert_eq!(KdlDocument::v1_to_v2(v1)?, v2, "Converting a v1 doc to v2"); + pretty_assertions::assert_eq!(KdlDocument::v2_to_v1(v2)?, v1, "Converting a v2 doc to v1"); + Ok(()) + } + + #[cfg(feature = "v1")] + #[test] + fn v2_to_v1() -> miette::Result<()> { let original = r##" // If you'd like to override the default keybindings completely, be sure to change "keybinds" to "keybinds clear-defaults=true" keybinds { diff --git a/src/entry.rs b/src/entry.rs index 9f5ddbe..d37c78c 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -171,9 +171,8 @@ impl KdlEntry { /// Auto-formats this entry. pub fn autoformat(&mut self) { - // TODO once MSRV allows: + // TODO once MSRV allows (1.80.0): //self.format.take_if(|f| !f.autoformat_keep); - let old_fmt = self.format.clone(); if !self .format .as_ref() @@ -182,68 +181,13 @@ impl KdlEntry { { self.format = None; } else { - let value_repr = old_fmt.map(|x| { - match &self.value { - KdlValue::String(val) => { - // cleanup. I don't _think_ this should have any whitespace, - // but just in case. - let s = x.value_repr.trim(); - // convert raw strings to new format - let s = s.strip_prefix('r').unwrap_or(s); - let s = if crate::value::is_plain_ident(val) { - val.to_string() - } else if s - .find(|c| v2_parser::NEWLINES.iter().any(|nl| nl.contains(c))) - .is_some() - { - // Multiline string. Need triple quotes if they're not there already. - if s.contains("\"\"\"") { - // We're probably good. This could be more precise, but close enough. - s.to_string() - } else { - // `"` -> `"""` but also extra newlines need to be - // added because v2 strips the first and last ones. - let s = s.replacen('\"', "\"\"\"\n", 1); - s.chars() - .rev() - .collect::() - .replacen('\"', "\"\"\"\n", 1) - .chars() - .rev() - .collect::() - } - } else if !s.starts_with('#') { - // `/` is no longer an escaped char in v2. - s.replace("\\/", "/") - } else { - // We're all good! Let's move on. - s.to_string() - }; - s - } - // These have `#` prefixes now. The regular Display impl will - // take care of that. - KdlValue::Bool(_) | KdlValue::Null => format!("{}", self.value), - // These should be fine as-is? - KdlValue::Integer(_) | KdlValue::Float(_) => x.value_repr, - } + #[cfg(feature = "v1")] + self.ensure_v2(); + self.format = self.format.take().map(|f| KdlEntryFormat { + value_repr: f.value_repr, + leading: f.leading, + ..Default::default() }); - - if let Some(value_repr) = value_repr.as_ref() { - self.format = Some( - self.format - .clone() - .map(|mut x| { - x.value_repr = value_repr.into(); - x - }) - .unwrap_or_else(|| KdlEntryFormat { - value_repr: value_repr.into(), - leading: " ".into(), - ..Default::default() - }), - ) - } } if let Some(name) = &mut self.name { @@ -275,6 +219,168 @@ impl KdlEntry { let ret: Result = s.parse(); ret.map(|x| x.into()).map_err(|e| e.into()) } + + /// Makes sure this entry is in v2 format. + #[cfg(feature = "v1")] + pub fn ensure_v2(&mut self) { + let value_repr = self.format.as_ref().map(|x| { + match &self.value { + KdlValue::String(val) => { + // cleanup. I don't _think_ this should have any whitespace, + // but just in case. + let s = x.value_repr.trim(); + // convert raw strings to new format + let s = s.strip_prefix('r').unwrap_or(s); + let s = if crate::value::is_plain_ident(val) { + val.into() + } else if s + .find(|c| v2_parser::NEWLINES.iter().any(|nl| nl.contains(c))) + .is_some() + { + // Multiline string. Need triple quotes if they're not there already. + if s.contains("\"\"\"") { + // We're probably good. This could be more precise, but close enough. + s.to_string() + } else { + // `"` -> `"""` but also extra newlines need to be + // added because v2 strips the first and last ones. + let s = s.replacen('\"', "\"\"\"\n", 1); + s.chars() + .rev() + .collect::() + .replacen('\"', "\"\"\"\n", 1) + .chars() + .rev() + .collect::() + } + } else if !s.starts_with('#') { + // `/` is no longer an escaped char in v2. + s.replace("\\/", "/") + } else { + // We're all good! Let's move on. + s.to_string() + }; + s + } + // These have `#` prefixes now. The regular Display impl will + // take care of that. + KdlValue::Bool(_) | KdlValue::Null => format!("{}", self.value), + // These should be fine as-is? + KdlValue::Integer(_) | KdlValue::Float(_) => x.value_repr.clone(), + } + }); + + if let Some(value_repr) = value_repr.as_ref() { + self.format = Some( + self.format + .clone() + .map(|mut x| { + x.value_repr = value_repr.into(); + x + }) + .unwrap_or_else(|| KdlEntryFormat { + value_repr: value_repr.into(), + leading: " ".into(), + ..Default::default() + }), + ) + } + } + + /// Makes sure this entry is in v1 format. + #[cfg(feature = "v1")] + pub fn ensure_v1(&mut self) { + let value_repr = self.format.as_ref().map(|x| { + match &self.value { + KdlValue::String(val) => { + // cleanup. I don't _think_ this should have any whitespace, + // but just in case. + let s = x.value_repr.trim(); + // convert raw strings to v1 format + let s = if s.starts_with("#") { + format!("r{s}") + } else { + s.to_string() + }; + let s = if crate::value::is_plain_ident(val) + && !s.starts_with('\"') + && !s.starts_with("r#") + { + format!("\"{val}\"") + } else if s + .find(|c| v2_parser::NEWLINES.iter().any(|nl| nl.contains(c))) + .is_some() + { + // Multiline string. Let's make sure it's v1. + if s.contains("\"\"\"") { + let prefix = s + .chars() + .rev() + .skip_while(|c| c == &'"') + .take_while(|c| { + v2_parser::NEWLINES.iter().any(|nl| nl.contains(*c)) + }) + .collect::(); + let prefix = prefix.chars().rev().collect::(); + // Sigh. Yeah. I didn't promise this would be _efficient_. + let mut s = s; + for nl in v2_parser::NEWLINES { + s = s.replace(&format!("{nl}{prefix}"), nl); + } + // And now we strips the beginning and ending newlines. + // Finally, replace `"""` with `"`. + s + } else { + // It's already a v1 string + s + } + } else if !s.starts_with("r#") { + // `/` is an escaped char in v2 + let s = s.replace("\\/", "/"); // Maneuvering. Will fix in a sec. + s.replace('/', "\\/") + } else { + // We're all good! Let's move on. + s.to_string() + }; + s + } + // No more # prefix for these + KdlValue::Bool(b) => b.to_string(), + KdlValue::Null => "null".to_string(), + // These should be fine as-is? + KdlValue::Integer(_) | KdlValue::Float(_) => x.value_repr.clone(), + } + }); + + if let Some(value_repr) = value_repr.as_ref() { + self.format = Some( + self.format + .clone() + .map(|mut x| { + x.value_repr = value_repr.into(); + x + }) + .unwrap_or_else(|| KdlEntryFormat { + value_repr: value_repr.into(), + leading: " ".into(), + ..Default::default() + }), + ) + } else { + let v1_val = match self.value() { + KdlValue::String(s) => kdlv1::KdlValue::String(s.clone()), + KdlValue::Integer(i) => kdlv1::KdlValue::Base10(*i as i64), + KdlValue::Float(f) => kdlv1::KdlValue::Base10Float(*f), + KdlValue::Bool(b) => kdlv1::KdlValue::Bool(*b), + KdlValue::Null => kdlv1::KdlValue::Null, + }; + self.format = Some(KdlEntryFormat { + value_repr: v1_val.to_string(), + leading: " ".into(), + ..Default::default() + }) + } + } } #[cfg(feature = "v1")] diff --git a/src/identifier.rs b/src/identifier.rs index 06a7fa0..f7767a5 100644 --- a/src/identifier.rs +++ b/src/identifier.rs @@ -121,7 +121,7 @@ impl From for KdlIdentifier { value: value.value().into(), repr: value.repr().map(|x| x.into()), #[cfg(feature = "span")] - span: SourceSpan::new(value.span().offset().into(), value.span().len()), + span: (value.span().offset(), value.span().len()).into(), } } } diff --git a/src/node.rs b/src/node.rs index ec1e737..6bfc41a 100644 --- a/src/node.rs +++ b/src/node.rs @@ -353,11 +353,70 @@ impl KdlNode { let ret: Result = s.parse(); ret.map(|x| x.into()).map_err(|e| e.into()) } + + /// Makes sure this node is in v2 format. + #[cfg(feature = "v1")] + pub fn ensure_v2(&mut self) { + self.ty = self.ty.take().map(|ty| ty.value().into()); + let v2_name: KdlIdentifier = self.name.value().into(); + self.name = v2_name; + for entry in self.iter_mut() { + entry.ensure_v2(); + } + self.children = self.children.take().map(|mut doc| { + doc.ensure_v2(); + doc + }); + } + + /// Makes sure this node is in v1 format. + #[cfg(feature = "v1")] + pub fn ensure_v1(&mut self) { + self.ty = self.ty.take().map(|ty| { + let v1_name: kdlv1::KdlIdentifier = ty.value().into(); + v1_name.into() + }); + let v1_name: kdlv1::KdlIdentifier = self.name.value().into(); + self.name = v1_name.into(); + for entry in self.iter_mut() { + entry.ensure_v1(); + } + self.children = self.children.take().map(|mut children| { + children.ensure_v1(); + children + }); + } } #[cfg(feature = "v1")] impl From for KdlNode { fn from(value: kdlv1::KdlNode) -> Self { + let terminator = value + .trailing() + .map(|t| if t.contains(";") { ";" } else { "\n" }) + .unwrap_or("\n"); + let trailing = value.trailing().map(|t| { + if t.contains(";") { + t.replace(';', "") + } else { + let t = t.replace("\r\n", "\n"); + let t = t + .chars() + .map(|c| { + if v2_parser::NEWLINES.iter().any(|nl| nl.contains(c)) { + '\n' + } else { + c + } + }) + .collect::(); + if terminator == ";" { + t + } else { + t.replacen('\n', "", 1) + } + } + }); KdlNode { ty: value.ty().map(|x| x.clone().into()), name: value.name().clone().into(), @@ -370,8 +429,8 @@ impl From for KdlNode { after_ty: "".into(), before_children: value.before_children().unwrap_or("").into(), before_terminator: "".into(), - terminator: "".into(), - trailing: value.trailing().unwrap_or("").into(), + terminator: terminator.into(), + trailing: trailing.unwrap_or_else(|| "".into()), }), #[cfg(feature = "span")] span: SourceSpan::new(value.span().offset().into(), value.span().len()),