///|
fn modify_document(text : String) -> Result[Document, EditError] {
let document = parse(text, options={
..ParseOptions::default(),
allow_empty: true,
})
if document.diagnostics.is_empty() {
Ok(document)
} else {
Err(InvalidDocument(document.diagnostics))
}
}
///|
fn modify_options(options : FormatOptions) -> Result[Unit, EditError] {
if options.indent_size < 0 ||
options.indent_size > 16 ||
(options.eol != "\n" && options.eol != "\r\n" && options.eol != "\r") {
Err(
InvalidOptions("indent size must be 0..16 and EOL must be LF, CRLF or CR"),
)
} else {
Ok(())
}
}
///|
fn modify_value(
value : String,
options : FormatOptions,
) -> Result[String, EditError] {
let document = parse(value, options={
..ParseOptions::default(),
allow_comments: false,
allow_trailing_comma: false,
})
if !document.diagnostics.is_empty() {
return Err(InvalidValue(document.diagnostics))
}
let node = match document.root {
Some(node) => node
None => return Err(InvalidValue([]))
}
let source = edit_slice(value, node.offset, node.offset + node.length)
match format(source, options={ ..options, final_newline: false, }) {
Err(error) => Err(error)
Ok(edits) => apply_edits(source, edits)
}
}
///|
fn modify_key(node : Node) -> String {
match node.children[0].value {
String(key) => key
_ => ""
}
}
///|
fn modify_indent(text : String, position : Int) -> String {
let mut start = position
while start > 0 &&
text.unsafe_get(start - 1) != 10 &&
text.unsafe_get(start - 1) != 13 {
start -= 1
}
let mut end = start
while end < position &&
(text.unsafe_get(end) == 32 || text.unsafe_get(end) == 9) {
end += 1
}
edit_slice(text, start, end)
}
///|
fn modify_unit(options : FormatOptions) -> String {
if options.use_tabs {
"\t"
} else {
String::make(options.indent_size, ' ')
}
}
///|
fn modify_reindent(text : String, indent : String) -> String {
let out = StringBuilder()
let mut previous_cr = false
for c in text {
if previous_cr && c != '\n' {
out.write_string(indent)
}
out.write_string(c.to_string())
if c == '\n' {
out.write_string(indent)
}
previous_cr = c == '\r'
}
if previous_cr {
out.write_string(indent)
}
out.to_string()
}
///|
fn modify_comma(tokens : Array[Token], start : Int, end : Int) -> Token? {
for token in tokens {
if token.offset >= end {
break
}
if token.offset >= start && token.kind == Comma {
return Some(token)
}
}
None
}
///|
fn modify_finish(
text : String,
edits : Array[TextEdit],
) -> Result[Array[TextEdit], EditError] {
match apply_edits(text, edits) {
Err(error) => Err(error)
Ok(next) =>
match modify_document(next) {
Err(_) =>
Err(InvalidEdit("the requested change would produce invalid JSONC"))
Ok(_) => Ok(edits)
}
}
}
///|
fn modify_append(
text : String,
document : Document,
parent : Node,
entry : String,
options : FormatOptions,
) -> Result[Array[TextEdit], EditError] {
let close = parent.offset + parent.length - 1
let multiline = edit_slice(text, parent.offset, close).contains_char('\n') ||
edit_slice(text, parent.offset, close).contains_char('\r')
let count = parent.children.length()
let last_end = if count == 0 {
parent.offset + 1
} else {
let last = parent.children[count - 1]
last.offset + last.length
}
let trailing = modify_comma(document.tokens, last_end, close)
let edits : Array[TextEdit] = []
let mut position = close
let mut content = ""
if multiline {
let parent_indent = modify_indent(text, parent.offset)
let close_indent = modify_indent(text, close)
let before_close = close - close_indent.length()
let close_on_own_line = before_close == 0 ||
edit_slice(text, before_close - 1, before_close) == "\n" ||
edit_slice(text, before_close - 1, before_close) == "\r"
let child_indent = if count > 0 {
let first = parent.children[0]
let existing = modify_indent(text, first.offset)
if existing.length() > parent_indent.length() {
existing
} else {
parent_indent + modify_unit(options)
}
} else {
parent_indent + modify_unit(options)
}
if close_on_own_line {
position = before_close
}
let prefix = if close_on_own_line { "" } else { options.eol }
content = prefix +
child_indent +
modify_reindent(entry, child_indent) +
(if trailing is Some(_) { "," } else { "" }) +
options.eol +
(if close_on_own_line { "" } else { parent_indent })
} else {
content = (if count == 0 { "" } else { " " }) +
entry +
(if trailing is Some(_) { "," } else { "" })
}
if count > 0 && trailing is None {
if last_end == position {
content = "," + content
} else {
edits.push({ offset: last_end, length: 0, content: ",", })
}
}
edits.push({ offset: position, length: 0, content, })
modify_finish(text, edits)
}
///|
fn modify_remove_item(
text : String,
document : Document,
parent : Node,
index : Int,
) -> Result[Array[TextEdit], EditError] {
let item = parent.children[index]
let edits : Array[TextEdit] = [
{ offset: item.offset, length: item.length, content: "", },
]
let close = parent.offset + parent.length - 1
let comma = if index + 1 < parent.children.length() {
modify_comma(
document.tokens,
item.offset + item.length,
parent.children[index + 1].offset,
)
} else if index > 0 {
let previous = parent.children[index - 1]
modify_comma(
document.tokens,
previous.offset + previous.length,
item.offset,
)
} else {
modify_comma(document.tokens, item.offset + item.length, close)
}
if comma is Some(token) {
edits.push({ offset: token.offset, length: token.length, content: "", })
}
modify_finish(text, edits)
}
///|
fn modify_nested(
path : Array[PathSegment],
from : Int,
value : String,
) -> Result[String, EditError] {
if from == path.length() {
return Ok(value)
}
let child = match modify_nested(path, from + 1, value) {
Ok(child) => child
Err(error) => return Err(error)
}
match path[from] {
Key(key) => Ok("{" + key.to_json().stringify() + ": " + child + "}")
Index(index) =>
if index == 0 || index == -1 {
Ok("[" + child + "]")
} else {
Err(
InvalidPath(
"new arrays start at index 0; sparse creation is not supported",
),
)
}
}
}
///|
fn modify_empty(
text : String,
document : Document,
value : String,
options : FormatOptions,
) -> Result[Array[TextEdit], EditError] {
let mut newline = false
for token in document.tokens {
if token.kind == LineComment {
newline = true
} else if token.kind == LineBreak {
newline = false
}
}
let content = (if newline { options.eol } else { "" }) + value
modify_finish(text, [{ offset: text.length(), length: 0, content, }])
}
///|
fn modify_walk(
text : String,
document : Document,
current : Node,
path : Array[PathSegment],
depth : Int,
value : String?,
options : FormatOptions,
) -> Result[Array[TextEdit], EditError] {
if depth == path.length() {
let replacement = value.unwrap_or("")
if edit_slice(text, current.offset, current.offset + current.length) ==
replacement {
return Ok([])
}
return modify_finish(text, [
{ offset: current.offset, length: current.length, content: replacement, },
])
}
let last = depth + 1 == path.length()
match (current.kind, path[depth]) {
(ObjectNode, Key(key)) => {
let mut index = -1
for i, property in current.children {
if modify_key(property) == key {
index = i
break
}
}
if index >= 0 {
if last && value is None {
return modify_remove_item(text, document, current, index)
}
modify_walk(
text,
document,
current.children[index].children[1],
path,
depth + 1,
value,
options,
)
} else {
let supplied = match value {
None => return Ok([])
Some(v) => v
}
let nested = match modify_nested(path, depth + 1, supplied) {
Ok(v) => v
Err(e) => return Err(e)
}
let nested = match modify_value(nested, options) {
Ok(v) => v
Err(e) => return Err(e)
}
modify_append(
text,
document,
current,
key.to_json().stringify() + ": " + nested,
options,
)
}
}
(ArrayNode, Index(requested)) => {
let count = current.children.length()
let index = if requested == -1 { count } else { requested }
if index < 0 || index > count {
return Err(InvalidPath("array index is outside 0..length"))
}
if index < count {
if last && value is None {
return modify_remove_item(text, document, current, index)
}
modify_walk(
text,
document,
current.children[index],
path,
depth + 1,
value,
options,
)
} else {
let supplied = match value {
None => return Ok([])
Some(v) => v
}
let nested = match modify_nested(path, depth + 1, supplied) {
Ok(v) => v
Err(e) => return Err(e)
}
let nested = match modify_value(nested, options) {
Ok(v) => v
Err(e) => return Err(e)
}
modify_append(text, document, current, nested, options)
}
}
_ =>
Err(
InvalidPath(
"path segment does not match the existing object, array or scalar",
),
)
}
}
///|
/// Set or remove a JSONC path using source-coordinate edits. None removes;
/// missing removals are idempotent. Missing parents are created without replacing
/// existing scalar data. Index -1 and length append; sparse arrays are rejected.
pub fn modify(
text : String,
path : Array[PathSegment],
value : String?,
options? : FormatOptions = FormatOptions::default(),
) -> Result[Array[TextEdit], EditError] {
if path.length() > 128 {
return Err(InvalidPath("path depth exceeds 128"))
}
match modify_options(options) {
Err(e) => return Err(e)
Ok(_) => ()
}
let document = match modify_document(text) {
Ok(d) => d
Err(e) => return Err(e)
}
let prepared = match value {
None => None
Some(v) =>
match modify_value(v, options) {
Ok(v) => Some(v)
Err(e) => return Err(e)
}
}
match document.root {
Some(root) => modify_walk(text, document, root, path, 0, prepared, options)
None => {
let supplied = match prepared {
None => return Ok([])
Some(v) => v
}
let nested = match modify_nested(path, 0, supplied) {
Ok(v) => v
Err(e) => return Err(e)
}
let nested = match modify_value(nested, options) {
Ok(v) => v
Err(e) => return Err(e)
}
modify_empty(text, document, nested, options)
}
}
}
///|
/// Insert before an existing array element, or append at length. Unlike modify,
/// this shifts existing elements. The array must exist; indices are 0..length.
pub fn insert(
text : String,
path : Array[PathSegment],
index : Int,
value : String,
options? : FormatOptions = FormatOptions::default(),
) -> Result[Array[TextEdit], EditError] {
if path.length() > 128 {
return Err(InvalidPath("path depth exceeds 128"))
}
match modify_options(options) {
Err(e) => return Err(e)
Ok(_) => ()
}
let document = match modify_document(text) {
Ok(d) => d
Err(e) => return Err(e)
}
let root = match document.root {
Some(n) => n
None => return Err(InvalidPath("array does not exist"))
}
let parent = match find_node(root, path) {
Some(n) => n
None => return Err(InvalidPath("array does not exist"))
}
if parent.kind != ArrayNode {
return Err(InvalidPath("insert requires an array"))
}
let count = parent.children.length()
if index < 0 || index > count {
return Err(InvalidPath("insertion index is outside 0..length"))
}
let supplied = match modify_value(value, options) {
Ok(v) => v
Err(e) => return Err(e)
}
if index == count {
return modify_append(text, document, parent, supplied, options)
}
let position = if index == 0 {
parent.offset + 1
} else {
let previous = parent.children[index - 1]
match
modify_comma(
document.tokens,
previous.offset + previous.length,
parent.children[index].offset,
) {
Some(comma) => comma.offset + comma.length
None => return Err(InvalidEdit("array separator is missing"))
}
}
let multiline = edit_slice(text, parent.offset, parent.offset + parent.length).contains_char(
'\n',
) ||
edit_slice(text, parent.offset, parent.offset + parent.length).contains_char(
'\r',
)
let prefix = if multiline {
options.eol + modify_indent(text, parent.children[index].offset)
} else if index > 0 {
" "
} else {
""
}
let content = prefix +
modify_reindent(
supplied,
if multiline {
modify_indent(text, parent.children[index].offset)
} else {
""
},
) +
","
modify_finish(text, [{ offset: position, length: 0, content, }])
}