feat: preserve nested source projects across desktop and REST
This commit is contained in:
@@ -4,6 +4,8 @@ use crate::{Id, Kind, Project, language, persistence::DecodeResult, validation};
|
||||
use language::{Diagnostic, ParseLimits, Span};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
#[path = "source_project/source_structure.rs"]
|
||||
mod source_structure;
|
||||
|
||||
/// A portable slash-separated path, checked at every bundle boundary.
|
||||
pub type LogicalPath = String;
|
||||
@@ -88,6 +90,11 @@ pub struct IncludeLocation {
|
||||
pub struct SourceIndex {
|
||||
/// Includes the project header itself under the project UUID.
|
||||
pub declarations: BTreeMap<Id, SourceLocation>,
|
||||
/// Interior of each declaration body, excluding its opening/closing braces.
|
||||
pub declaration_bodies: BTreeMap<Id, SourceLocation>,
|
||||
/// Authored element nesting, distinct from an explicit semantic parent field.
|
||||
/// Flat declarations have no entry; the project is not a lexical element owner.
|
||||
pub lexical_owners: BTreeMap<Id, Id>,
|
||||
/// Complete value spans for top-level declaration fields.
|
||||
pub fields: BTreeMap<(Id, String), SourceLocation>,
|
||||
pub includes: Vec<IncludeLocation>,
|
||||
@@ -363,6 +370,8 @@ fn compile_once(
|
||||
let mut alias_locations: BTreeMap<String, SourceLocation> = BTreeMap::new();
|
||||
let mut project_header = None;
|
||||
let mut declaration_locations = BTreeMap::new();
|
||||
let mut declaration_bodies = BTreeMap::new();
|
||||
let mut lexical_owners = BTreeMap::new();
|
||||
let mut fields = BTreeMap::new();
|
||||
let mut includes = Vec::new();
|
||||
let mut insertions: Edits = BTreeMap::new();
|
||||
@@ -461,6 +470,16 @@ fn compile_once(
|
||||
aliases.insert(declaration.alias.clone(), declaration.id);
|
||||
alias_locations.insert(declaration.alias.clone(), location.clone());
|
||||
declaration_locations.insert(declaration.id, location);
|
||||
declaration_bodies.insert(
|
||||
declaration.id,
|
||||
SourceLocation {
|
||||
path: path.clone(),
|
||||
span: declaration.body,
|
||||
},
|
||||
);
|
||||
if let Some(owner) = declaration.lexical_owner {
|
||||
lexical_owners.insert(declaration.id, owner);
|
||||
}
|
||||
for (key, span) in &declaration.field_spans {
|
||||
fields.insert(
|
||||
(declaration.id, key.clone()),
|
||||
@@ -532,6 +551,8 @@ fn compile_once(
|
||||
let header = project_header.expect("validated entry header");
|
||||
let index = SourceIndex {
|
||||
declarations: declaration_locations,
|
||||
declaration_bodies,
|
||||
lexical_owners,
|
||||
fields,
|
||||
includes,
|
||||
project_header: SourceLocation {
|
||||
@@ -847,63 +868,65 @@ pub fn update_bundle_with_identity_map(
|
||||
.collect();
|
||||
let mut bundle = current.bundle.clone();
|
||||
let mut edits: Edits = BTreeMap::new();
|
||||
for (id, old_text) in &before {
|
||||
let location = ¤t.index.declarations[id];
|
||||
let new_text = identities.get(id).and_then(|id| after.get(id));
|
||||
if new_text != Some(old_text) {
|
||||
let source = &bundle.files[&location.path][location.span.start..location.span.end];
|
||||
let mut local = Vec::new();
|
||||
if let Some(new_text) = new_text {
|
||||
let parse = |text| {
|
||||
language::edit_declaration(text).map_err(|error| {
|
||||
vec![problem(
|
||||
&bundle,
|
||||
&location.path,
|
||||
location.span,
|
||||
"source_edit",
|
||||
error.message,
|
||||
)]
|
||||
})
|
||||
};
|
||||
let actual = parse(source)?;
|
||||
let previous = parse(old_text)?;
|
||||
let next = parse(new_text)?;
|
||||
let update = SyntaxUpdate {
|
||||
source,
|
||||
before: old_text,
|
||||
after: new_text,
|
||||
old_aliases: &old_aliases,
|
||||
new_aliases: &new_aliases,
|
||||
};
|
||||
update.header(&actual.header, &previous.header, &next.header, &mut local);
|
||||
update.value(
|
||||
&actual.fields,
|
||||
Some(&previous.fields),
|
||||
&next.fields,
|
||||
&mut local,
|
||||
);
|
||||
} else {
|
||||
erase_syntax(
|
||||
source,
|
||||
Span {
|
||||
start: 0,
|
||||
end: source.len(),
|
||||
},
|
||||
&mut local,
|
||||
);
|
||||
let mut original_order = BTreeMap::new();
|
||||
for (path, source) in &bundle.files {
|
||||
let declarations = language::edit_declarations(source, path == &bundle.entry, limits.model)
|
||||
.map_err(|error| {
|
||||
vec![SourceDiagnostic {
|
||||
path: path.clone(),
|
||||
diagnostic: *error,
|
||||
related: Vec::new(),
|
||||
}]
|
||||
})?;
|
||||
original_order.insert(
|
||||
path.clone(),
|
||||
declarations.iter().map(|(id, _)| *id).collect(),
|
||||
);
|
||||
for (id, actual) in declarations {
|
||||
let old_text = &before[&id];
|
||||
// Deletion is structural: removing a parent's complete span here
|
||||
// would overlap child edits and lose independently moved descendants.
|
||||
let Some(new_text) = identities.get(&id).and_then(|id| after.get(id)) else {
|
||||
continue;
|
||||
};
|
||||
if new_text == old_text {
|
||||
continue;
|
||||
}
|
||||
edits
|
||||
.entry(location.path.clone())
|
||||
.or_default()
|
||||
.extend(local.into_iter().map(|(span, text)| {
|
||||
(
|
||||
Span {
|
||||
start: span.start + location.span.start,
|
||||
end: span.end + location.span.start,
|
||||
},
|
||||
text,
|
||||
)
|
||||
}));
|
||||
let parse = |text| {
|
||||
language::edit_declaration(text).map_err(|error| {
|
||||
vec![problem(
|
||||
&bundle,
|
||||
path,
|
||||
actual.span,
|
||||
"source_edit",
|
||||
error.message,
|
||||
)]
|
||||
})
|
||||
};
|
||||
let mut previous = parse(old_text)?;
|
||||
let mut next = parse(new_text)?;
|
||||
if actual.lexical_owner.is_some()
|
||||
&& let language::EditKind::Record(fields) = &actual.fields.kind
|
||||
&& !fields.contains_key("parent")
|
||||
{
|
||||
// The lexical structure is the authored definition of parent.
|
||||
// Keep it implicit while the structural phase moves this record.
|
||||
for fields in [&mut previous.fields, &mut next.fields] {
|
||||
if let language::EditKind::Record(fields) = &mut fields.kind {
|
||||
fields.remove("parent");
|
||||
}
|
||||
}
|
||||
}
|
||||
let update = SyntaxUpdate {
|
||||
source,
|
||||
before: old_text,
|
||||
after: new_text,
|
||||
old_aliases: &old_aliases,
|
||||
new_aliases: &new_aliases,
|
||||
};
|
||||
let local = edits.entry(path.clone()).or_default();
|
||||
update.header(&actual.header, &previous.header, &next.header, local);
|
||||
update.value(&actual.fields, Some(&previous.fields), &next.fields, local);
|
||||
}
|
||||
}
|
||||
if old.id != project.id || old.alias != project.alias || old.name != project.name {
|
||||
@@ -975,84 +998,16 @@ pub fn update_bundle_with_identity_map(
|
||||
}
|
||||
}
|
||||
|
||||
let defaults = default_ownership(project);
|
||||
let mut assigned: BTreeMap<_, _> = current
|
||||
.index
|
||||
.declarations
|
||||
.iter()
|
||||
.filter_map(|(id, location)| identities.get(id).map(|new| (*new, location.path.clone())))
|
||||
.collect();
|
||||
let mut chosen_defaults = BTreeMap::new();
|
||||
let mut used_paths: BTreeSet<_> = bundle
|
||||
.files
|
||||
.keys()
|
||||
.map(|path| path.to_lowercase())
|
||||
.collect();
|
||||
let mut additions: BTreeMap<LogicalPath, String> = BTreeMap::new();
|
||||
let aliases: BTreeMap<_, _> = project.identities().into_iter().collect();
|
||||
let mut new_ids: Vec<_> = after
|
||||
.keys()
|
||||
.filter(|id| !origins.contains_key(*id))
|
||||
.copied()
|
||||
.collect();
|
||||
new_ids.sort_by_key(|id| declaration_order(project, *id, &aliases));
|
||||
for id in new_ids {
|
||||
let mut parent = semantic_owner(project, id);
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut inherited = None;
|
||||
while let Some(owner) = parent {
|
||||
if !seen.insert(owner) {
|
||||
break;
|
||||
}
|
||||
if let Some(path) = assigned.get(&owner) {
|
||||
inherited = Some(path.clone());
|
||||
break;
|
||||
}
|
||||
parent = semantic_owner(project, owner);
|
||||
}
|
||||
let path = inherited.unwrap_or_else(|| {
|
||||
chosen_defaults
|
||||
.entry(defaults[&id].clone())
|
||||
.or_insert_with(|| {
|
||||
let preferred = &defaults[&id];
|
||||
if bundle.files.contains_key(preferred) {
|
||||
preferred.clone()
|
||||
} else {
|
||||
unique_path(preferred, &mut used_paths)
|
||||
}
|
||||
})
|
||||
.clone()
|
||||
});
|
||||
assigned.insert(id, path.clone());
|
||||
additions
|
||||
.entry(path)
|
||||
.or_default()
|
||||
.push_str(&unindent(&after[&id]));
|
||||
}
|
||||
let mut new_includes = String::new();
|
||||
for (path, text) in additions {
|
||||
if !bundle.files.contains_key(&path) {
|
||||
bundle.files.insert(path.clone(), String::new());
|
||||
new_includes.push_str(&format!(" include {};\n", quote(&path)));
|
||||
}
|
||||
let at = if path == bundle.entry {
|
||||
current.index.project_body.span.end
|
||||
} else {
|
||||
bundle.files[&path].len()
|
||||
};
|
||||
edits
|
||||
.entry(path)
|
||||
.or_default()
|
||||
.push((Span { start: at, end: at }, format!("\n{text}")));
|
||||
}
|
||||
if !new_includes.is_empty() {
|
||||
let at = current.index.project_body.span.end;
|
||||
edits
|
||||
.entry(bundle.entry.clone())
|
||||
.or_default()
|
||||
.push((Span { start: at, end: at }, format!("\n{new_includes}")));
|
||||
}
|
||||
apply_edits(&mut bundle, edits)?;
|
||||
source_structure::update(
|
||||
&mut bundle,
|
||||
current,
|
||||
project,
|
||||
identities,
|
||||
&original_order,
|
||||
&after,
|
||||
limits,
|
||||
)?;
|
||||
let result = compile_bundle(&bundle, limits)?;
|
||||
if &result.decoded.project != project {
|
||||
return Err(vec![problem(
|
||||
|
||||
Reference in New Issue
Block a user