feat: preserve nested source projects across desktop and REST

This commit is contained in:
2026-09-06 00:17:20 +03:00
parent d364d3c058
commit 4e8575bd11
32 changed files with 4283 additions and 417 deletions
+196 -74
View File
@@ -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.
//!