feat: preserve nested source projects across desktop and REST
This commit is contained in:
+196
-74
@@ -898,6 +898,10 @@ pub(crate) struct Declaration {
|
||||
fields: BTreeMap<String, Ast>,
|
||||
offset: usize,
|
||||
pub(crate) span: Span,
|
||||
/// The containing element's identity, independent of an authored parent field.
|
||||
pub(crate) lexical_owner: Option<Id>,
|
||||
/// Exact body interior, excluding the opening and closing braces.
|
||||
pub(crate) body: Span,
|
||||
pub(crate) id_insertion: Option<usize>,
|
||||
pub(crate) field_spans: BTreeMap<String, Span>,
|
||||
}
|
||||
@@ -952,6 +956,9 @@ pub(crate) enum EditKind {
|
||||
List(Vec<EditValue>),
|
||||
}
|
||||
pub(crate) struct EditDeclaration {
|
||||
pub(crate) span: Span,
|
||||
pub(crate) lexical_owner: Option<Id>,
|
||||
pub(crate) body: Span,
|
||||
pub(crate) header: BTreeMap<&'static str, Span>,
|
||||
pub(crate) fields: EditValue,
|
||||
}
|
||||
@@ -995,6 +1002,27 @@ pub(crate) fn edit_declaration(source: &str) -> Result<EditDeclaration, Box<Diag
|
||||
},
|
||||
)));
|
||||
}
|
||||
let mut declarations = edit_positions(source, unit)?;
|
||||
Ok(declarations.pop().expect("one declaration").1)
|
||||
}
|
||||
|
||||
/// Editing positions for every occurrence in lexical preorder. This is syntax
|
||||
/// analysis only: intermediate command edits may have duplicate identities or
|
||||
/// parent fields that disagree with their as-yet unmoved lexical owner. Callers
|
||||
/// retain occurrence provenance across such edits, then compile the final text.
|
||||
pub(crate) fn edit_declarations(
|
||||
source: &str,
|
||||
entry: bool,
|
||||
limits: ParseLimits,
|
||||
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
|
||||
let unit = parse_unit(source, entry, limits)?;
|
||||
edit_positions(source, unit)
|
||||
}
|
||||
|
||||
fn edit_positions(
|
||||
source: &str,
|
||||
unit: SourceUnit,
|
||||
) -> Result<Vec<(Id, EditDeclaration)>, Box<Diagnostic>> {
|
||||
let tokens = strict_tokens(source).map_err(|e| Box::new(diagnostic(source, e)))?;
|
||||
let mut ends: Vec<_> = tokens.iter().map(|token| token.span.end).collect();
|
||||
let mut stack = Vec::new();
|
||||
@@ -1055,21 +1083,48 @@ pub(crate) fn edit_declaration(source: &str) -> Result<EditDeclaration, Box<Diag
|
||||
tokens: &tokens,
|
||||
ends,
|
||||
};
|
||||
let body = tokens
|
||||
.iter()
|
||||
.position(|token| token.text(source) == "{")
|
||||
.expect("declaration body");
|
||||
let fields = EditValue {
|
||||
span: Span {
|
||||
start: tokens[body].span.start,
|
||||
end: positions.ends[body],
|
||||
},
|
||||
kind: EditKind::Record(positions.fields(&unit.declarations[0].fields)),
|
||||
};
|
||||
Ok(EditDeclaration {
|
||||
header: edit_header(source)?,
|
||||
fields,
|
||||
})
|
||||
unit.declarations
|
||||
.into_iter()
|
||||
.map(|declaration| {
|
||||
if declaration.id_insertion.is_some() {
|
||||
return Err(Box::new(diagnostic(
|
||||
source,
|
||||
Error {
|
||||
code: "invalid_source",
|
||||
offset: declaration.span.start,
|
||||
message: "Stabilize declaration identities before editing source.".into(),
|
||||
},
|
||||
)));
|
||||
}
|
||||
let roles: &[&str] = if matches!(declaration.kind, DeclKind::Node) {
|
||||
&["kind", "alias", "id_keyword", "id"]
|
||||
} else {
|
||||
&["kind", "alias", "name", "id_keyword", "id"]
|
||||
};
|
||||
let start = positions.token(declaration.span.start);
|
||||
let header = roles
|
||||
.iter()
|
||||
.zip(&tokens[start..])
|
||||
.map(|(role, token)| (*role, token.span))
|
||||
.collect();
|
||||
Ok((
|
||||
declaration.id,
|
||||
EditDeclaration {
|
||||
span: declaration.span,
|
||||
lexical_owner: declaration.lexical_owner,
|
||||
body: declaration.body,
|
||||
header,
|
||||
fields: EditValue {
|
||||
span: Span {
|
||||
start: declaration.body.start - 1,
|
||||
end: declaration.body.end + 1,
|
||||
},
|
||||
kind: EditKind::Record(positions.fields(&declaration.fields)),
|
||||
},
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
struct Parser<'a> {
|
||||
source: &'a str,
|
||||
@@ -1143,17 +1198,8 @@ impl Parser<'_> {
|
||||
self.take("{")?;
|
||||
let mut fields = BTreeMap::new();
|
||||
while self.text() != "}" {
|
||||
let key = self.text().to_owned();
|
||||
let offset = self.current().span.start;
|
||||
if !matches!(
|
||||
self.current().class,
|
||||
TokenClass::Keyword | TokenClass::Identifier
|
||||
) {
|
||||
return Err(self.error("Expected a field name or '}'."));
|
||||
}
|
||||
self.cursor += 1;
|
||||
let value = self.value()?;
|
||||
self.take(";")?;
|
||||
let (key, value, _) = self.field()?;
|
||||
if fields.insert(key.clone(), value).is_some() {
|
||||
return Err(Error {
|
||||
code: "invalid_source",
|
||||
@@ -1165,6 +1211,36 @@ impl Parser<'_> {
|
||||
self.take("}")?;
|
||||
Ok(fields)
|
||||
}
|
||||
fn field(&mut self) -> Result<(String, Ast, Span), Error> {
|
||||
let key = self.text().to_owned();
|
||||
if !matches!(
|
||||
self.current().class,
|
||||
TokenClass::Keyword | TokenClass::Identifier
|
||||
) {
|
||||
return Err(self.error("Expected a field name or '}'."));
|
||||
}
|
||||
self.cursor += 1;
|
||||
let start = self.current().span.start;
|
||||
let value = self.value()?;
|
||||
let end = self.tokens[self.cursor - 1].span.end;
|
||||
self.take(";")?;
|
||||
Ok((key, value, Span { start, end }))
|
||||
}
|
||||
|
||||
fn nested_header_kind(&self) -> Option<DeclKind> {
|
||||
let kind = declaration_kind(self.text())?;
|
||||
let alias = self.tokens.get(self.cursor + 1)?;
|
||||
let following = self.tokens.get(self.cursor + 2)?;
|
||||
// A node's ordinary `diagram @view;` field is not a declaration.
|
||||
if !crate::valid_alias(alias.text(self.source)) {
|
||||
return None;
|
||||
}
|
||||
if matches!(kind, DeclKind::Node) {
|
||||
matches!(following.text(self.source), "id" | "{").then_some(kind)
|
||||
} else {
|
||||
(following.class == TokenClass::String).then_some(kind)
|
||||
}
|
||||
}
|
||||
fn value(&mut self) -> Result<Ast, Error> {
|
||||
if self.value_depth >= 64 {
|
||||
return Err(self.error("Typed values may not nest more than 64 levels."));
|
||||
@@ -1301,15 +1377,54 @@ impl Parser<'_> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut declarations = Vec::new();
|
||||
let mut declarations: Vec<Declaration> = Vec::new();
|
||||
let mut includes = Vec::new();
|
||||
let mut graph_count = 0usize;
|
||||
while if entry {
|
||||
self.text() != "}"
|
||||
} else {
|
||||
!self.text().is_empty()
|
||||
} {
|
||||
// Declarations are a flat preorder with a stack of open body indices.
|
||||
// Semantic nesting can be deep without recursing through the Rust stack
|
||||
// or conflating declaration depth with the typed-value depth budget.
|
||||
let mut open: Vec<usize> = Vec::new();
|
||||
loop {
|
||||
let offset = self.current().span.start;
|
||||
if let Some(&owner) = open.last() {
|
||||
if self.text() == "}" {
|
||||
declarations[owner].body.end = offset;
|
||||
declarations[owner].span.end = self.current().span.end;
|
||||
self.cursor += 1;
|
||||
open.pop();
|
||||
continue;
|
||||
}
|
||||
if self.text().is_empty() {
|
||||
return Err(self.error("Expected a field, element declaration, or '}'."));
|
||||
}
|
||||
if self.text() == "include" {
|
||||
return Err(
|
||||
self.error("Includes must occur at document level, outside declarations.")
|
||||
);
|
||||
}
|
||||
if let Some(kind) = self.nested_header_kind() {
|
||||
if !matches!(declarations[owner].kind, DeclKind::Element(..))
|
||||
|| !matches!(kind, DeclKind::Element(..))
|
||||
{
|
||||
return Err(self
|
||||
.error("Only element declarations may nest inside an element body."));
|
||||
}
|
||||
} else {
|
||||
let (key, value, span) = self.field()?;
|
||||
let declaration = &mut declarations[owner];
|
||||
if declaration.fields.insert(key.clone(), value).is_some() {
|
||||
return Err(Error {
|
||||
code: "invalid_source",
|
||||
offset,
|
||||
message: format!("Duplicate field '{key}'."),
|
||||
});
|
||||
}
|
||||
declaration.field_spans.insert(key, span);
|
||||
continue;
|
||||
}
|
||||
} else if (entry && self.text() == "}") || (!entry && self.text().is_empty()) {
|
||||
break;
|
||||
}
|
||||
if self.text() == "include" {
|
||||
if !includes_allowed {
|
||||
return Err(self.error("An include requires a source bundle; single-string parsing never reads files."));
|
||||
@@ -1326,7 +1441,8 @@ impl Parser<'_> {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if self.text() == "description"
|
||||
if open.is_empty()
|
||||
&& self.text() == "description"
|
||||
&& let Some(mut entry_header) = header.take()
|
||||
{
|
||||
if entry_header.description.is_some() {
|
||||
@@ -1370,41 +1486,29 @@ impl Parser<'_> {
|
||||
};
|
||||
let id_insertion = (self.text() != "id").then_some(self.current().span.start);
|
||||
let id = self.optional_id()?;
|
||||
let fields = self.fields()?;
|
||||
let field_spans = fields
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
let start = value.offset();
|
||||
let mut depth = 0usize;
|
||||
let mut end = start;
|
||||
for token in
|
||||
self.tokens[self.tokens.partition_point(|t| t.span.start < start)..].iter()
|
||||
{
|
||||
match token.text(self.source) {
|
||||
";" if depth == 0 => break,
|
||||
"{" | "[" => depth += 1,
|
||||
"}" | "]" => depth = depth.saturating_sub(1),
|
||||
_ => {}
|
||||
}
|
||||
end = token.span.end;
|
||||
}
|
||||
(key.clone(), Span { start, end })
|
||||
})
|
||||
.collect();
|
||||
let body_start = self.current().span.end;
|
||||
self.take("{")?;
|
||||
let lexical_owner = open.last().map(|&owner| declarations[owner].id);
|
||||
declarations.push(Declaration {
|
||||
kind,
|
||||
alias,
|
||||
name,
|
||||
id,
|
||||
fields,
|
||||
fields: BTreeMap::new(),
|
||||
offset,
|
||||
span: Span {
|
||||
start: offset,
|
||||
end: self.tokens[self.cursor - 1].span.end,
|
||||
end: body_start,
|
||||
},
|
||||
lexical_owner,
|
||||
body: Span {
|
||||
start: body_start,
|
||||
end: body_start,
|
||||
},
|
||||
id_insertion,
|
||||
field_spans,
|
||||
field_spans: BTreeMap::new(),
|
||||
});
|
||||
open.push(declarations.len() - 1);
|
||||
}
|
||||
if entry {
|
||||
header.as_mut().expect("entry header").body.end = self.current().span.start;
|
||||
@@ -1449,8 +1553,19 @@ pub(crate) fn resolve_declarations(
|
||||
DeclKind::Element(kind, layer) => {
|
||||
let description =
|
||||
take_string(&mut fields, "description", "").map_err(&conversion_error)?;
|
||||
let parent =
|
||||
let explicit_parent = d.fields.contains_key("parent");
|
||||
let mut parent =
|
||||
take_optional_id(&mut fields, "parent").map_err(&conversion_error)?;
|
||||
if let Some(owner) = d.lexical_owner {
|
||||
if explicit_parent && parent != Some(owner) {
|
||||
return Err(Error {
|
||||
code: "conflicting_parent",
|
||||
offset: d.fields["parent"].offset(),
|
||||
message: "Explicit parent must equal the containing element; move the declaration to change its lexical parent.".into(),
|
||||
});
|
||||
}
|
||||
parent = Some(owner);
|
||||
}
|
||||
let mut initial = if kind == Kind::Port {
|
||||
if !fields.contains_key("contract") {
|
||||
return Err(conversion_error(
|
||||
@@ -1667,6 +1782,21 @@ pub fn analyze(source: &str) -> Analysis {
|
||||
.iter()
|
||||
.filter(|t| !matches!(t.class, TokenClass::Whitespace | TokenClass::Comment))
|
||||
.collect::<Vec<_>>();
|
||||
// Recover matched body bounds once. Scanning each aggregate's descendants
|
||||
// again would make tolerant navigation quadratic on deeply nested drafts.
|
||||
let mut body_ends = vec![None; significant.len()];
|
||||
let mut bodies = Vec::new();
|
||||
for (index, token) in significant.iter().enumerate() {
|
||||
match &source[token.span.start..token.span.end] {
|
||||
"{" => bodies.push(index),
|
||||
"}" => {
|
||||
if let Some(open) = bodies.pop() {
|
||||
body_ends[open] = Some(token.span.end);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut declarations = Vec::new();
|
||||
for (i, t) in significant.iter().enumerate() {
|
||||
let word = &source[t.span.start..t.span.end];
|
||||
@@ -1709,23 +1839,11 @@ pub fn analyze(source: &str) -> Analysis {
|
||||
let mut end = significant
|
||||
.get(j)
|
||||
.map_or(alias_token.span.end, |t| t.span.end);
|
||||
let mut depth = 0usize;
|
||||
for next in significant.iter().skip(j) {
|
||||
match &source[next.span.start..next.span.end] {
|
||||
"{" => depth += 1,
|
||||
"}" => {
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
end = next.span.end;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
end = next.span.end;
|
||||
if significant
|
||||
.get(j)
|
||||
.is_some_and(|t| &source[t.span.start..t.span.end] == "{")
|
||||
{
|
||||
end = body_ends[j].unwrap_or(source.len());
|
||||
}
|
||||
declarations.push(DeclarationSpan {
|
||||
alias: alias.into(),
|
||||
@@ -1744,6 +1862,10 @@ pub fn analyze(source: &str) -> Analysis {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "language_nesting_tests.rs"]
|
||||
mod nesting_tests;
|
||||
|
||||
pub(crate) mod legacy {
|
||||
//! Arcadia text: a readable, lossless representation of a valid OA project.
|
||||
//!
|
||||
|
||||
Reference in New Issue
Block a user