mirror of https://github.com/kdl-org/kdl-rs.git
feat(v1): add v2 -> v1 translation and fix translations to not autoformat
This commit is contained in:
parent
ec73cdfa05
commit
b332eed4a0
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ rust-version = "1.70.0"
|
|||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["span"]
|
||||
default = ["span", "v1"]
|
||||
span = []
|
||||
v1-fallback = ["v1"]
|
||||
v1 = ["kdlv1"]
|
||||
|
|
|
|||
169
src/document.rs
169
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<Self, KdlError> {
|
||||
#[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<Self, KdlError> {
|
||||
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<Self, KdlError> {
|
||||
|
|
@ -365,9 +370,74 @@ impl KdlDocument {
|
|||
#[cfg(feature = "v1")]
|
||||
pub fn v1_to_v2(s: &str) -> Result<String, KdlError> {
|
||||
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<String, KdlError> {
|
||||
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 {
|
||||
|
|
|
|||
232
src/entry.rs
232
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::<String>()
|
||||
.replacen('\"', "\"\"\"\n", 1)
|
||||
.chars()
|
||||
.rev()
|
||||
.collect::<String>()
|
||||
}
|
||||
} 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<kdlv1::KdlEntry, kdlv1::KdlError> = 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::<String>()
|
||||
.replacen('\"', "\"\"\"\n", 1)
|
||||
.chars()
|
||||
.rev()
|
||||
.collect::<String>()
|
||||
}
|
||||
} 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::<String>();
|
||||
let prefix = prefix.chars().rev().collect::<String>();
|
||||
// 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")]
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ impl From<kdlv1::KdlIdentifier> 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
63
src/node.rs
63
src/node.rs
|
|
@ -353,11 +353,70 @@ impl KdlNode {
|
|||
let ret: Result<kdlv1::KdlNode, kdlv1::KdlError> = 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<kdlv1::KdlNode> 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::<String>();
|
||||
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<kdlv1::KdlNode> 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()),
|
||||
|
|
|
|||
Loading…
Reference in New Issue