Skip to content

feat(fallback): apply heuristics when doing fallbacks #111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 68 additions & 87 deletions src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,15 +341,30 @@ impl KdlDocument {
///
/// If the `v1-fallback` feature is enabled, this method will first try to
/// parse the string as a KDL v2 document, and, if that fails, it will try
/// to parse again as a KDL v1 document. If both fail, only the v2 parse
/// errors will be returned.
/// to parse again as a KDL v1 document. If both fail, a heuristic will be
/// applied to try and detect the "intended" KDL version, and that version's
/// error(s) will be returned.
pub fn parse(s: &str) -> Result<Self, KdlError> {
#[cfg(not(feature = "v1-fallback"))]
{
KdlDocument::parse_v2(s)
}
#[cfg(feature = "v1-fallback")]
{
let v2_res = KdlDocument::parse_v2(s);
if let Err(err) = v2_res {
let v1_res = KdlDocument::parse_v2(s);
if v1_res.is_err() && detect_v2(s) {
v2_res
} else if detect_v1(s) {
v1_res
} else {
// This does matter, because detection short-circuits.
v2_res
}
} else {
v2_res
}
KdlDocument::parse_v2(s).or_else(|e| KdlDocument::parse_v1(s).map_err(|_| e))
}
}
Expand Down Expand Up @@ -455,6 +470,55 @@ impl From<kdlv1::KdlDocument> for KdlDocument {
}
}

/// Applies heuristics to get an idea of whether the string might be intended to
/// be v2.
#[allow(unused)]
pub(crate) fn detect_v2(input: &str) -> bool {
for line in input.lines() {
if line.contains("kdl-version 2")
|| line.contains("#true")
|| line.contains("#false")
|| line.contains("#null")
|| line.contains("#inf")
|| line.contains("#-inf")
|| line.contains("#nan")
|| line.contains(" #\"")
|| line.contains("\"\"\"")
// Very very rough attempt at finding unquoted strings. We give up
// the first time we see a quoted one on a line.
|| (!line.contains('"') && line
.split_whitespace()
.skip(1)
.any(|x| {
x.chars()
.next()
.map(|d| !d.is_ascii_digit() && d != '-' && d != '+')
.unwrap_or_default()
}))
{
return true;
}
}
false
}

/// Applies heuristics to get an idea of whether the string might be intended to
/// be v2.
#[allow(unused)]
pub(crate) fn detect_v1(input: &str) -> bool {
input
.lines()
.next()
.map(|l| l.contains("kdl-version 1"))
.unwrap_or(false)
|| input.contains(" true")
|| input.contains(" false")
|| input.contains(" null")
|| input.contains("r#\"")
|| input.contains(" \"\n")
|| input.contains(" \"\r\n")
}

impl std::str::FromStr for KdlDocument {
type Err = KdlError;

Expand Down Expand Up @@ -1109,91 +1173,8 @@ 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 {
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 expected = 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(original)?, expected);
assert!(super::detect_v1(v1));
assert!(super::detect_v2(v2));
Ok(())
}
}
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@
//! version of `kdl-rs`, and so may be fairly heavy.
//! * `v1-fallback` - Implies `v1`. Makes it so the various `*::parse()` and
//! `FromStr` implementations try to parse their inputs as `v2`, and, if that
//! fails, try again with `v1`. Errors will only be reported as if the input was
//! `v2`. To manage this more precisely, you can use the `*::parse_v2` and
//! `*::parse_v1` methods.
//! fails, try again with `v1`. For `KdlDocument`, a heuristic will be applied
//! if both `v1` and `v2` parsers fail, to pick which error(s) to return. For
//! other types, only the `v2` parser's errors will be returned.
//!
//! ## Quirks
//!
Expand Down
Loading