Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 196 additions & 74 deletions lively.ide/components/change-tracker.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion lively.ide/components/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ export class InteractiveComponentDescriptor extends ComponentDescriptor {
moduleId: this.moduleName,
exportName: this.componentName,
parentDocument: this._cachedComponent._changeTracker.projectionalRuntimeParentDocument([], {}),
resolveComponentDocument
resolveComponentDocument,
normalizeModuleId: (id, from) => this._cachedComponent._changeTracker.projectionalModuleId(id, from)
});
const invalidNames = parsed.diagnostics
.filter(diagnostic => diagnostic.kind === ComponentSourceDiagnosticKind.INVALID_NODE_NAME)
Expand Down
13 changes: 5 additions & 8 deletions lively.ide/components/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function getEligibleSourceEditorsFor (modId, modSource) {
});
return qualifiedEditors.filter(editor => {
if (typeof modSource !== 'string' || editor.textString !== modSource) return false;
if (editor.pluginFind?.(plugin => plugin.isComponentSourceEditorPlugin)?.sessions.size) return true;
if (editor.owner?.isBrowser && modSource) return !editor.owner.hasUnsavedChanges(modSource);
return true;
});
Expand Down Expand Up @@ -234,14 +235,10 @@ export function getComponentNode (parsedModuleContent, componentName) {
* @returns { object } The AST node of the component declaration
*/
export function findComponentDef (parsedModuleContent, componentName) {
return query.queryNodes(parsedModuleContent, `// VariableDeclaration [
@kind == "const"
&& /:declarations '*' [
VariableDeclarator [
/:id Identifier [ @name == "${componentName}"]
]
]
]`)[0];
return parsedModuleContent.body
.map(statement => statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement)
.find(declaration => declaration?.type === 'VariableDeclaration' && declaration.kind === 'const' &&
declaration.declarations.some(({ id }) => id.type === 'Identifier' && id.name === componentName));
}

/**
Expand Down
14 changes: 5 additions & 9 deletions lively.ide/components/reconciliation/component-transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,8 @@ export class ProjectionalComponentEditTransaction extends EditTransaction {
label = transaction?.commands?.length > 1
? 'component command batch'
: `component ${transaction?.command?.kind || 'command'}`,
afterReplay = null
afterReplay = null,
metadata = {}
} = {}) {
if (!(transaction instanceof PreparedComponentTransaction) &&
!(transaction instanceof CoalescedComponentTransaction)) {
Expand All @@ -515,7 +516,7 @@ export class ProjectionalComponentEditTransaction extends EditTransaction {
super({
kind: EditTransactionKind.COMPONENT_COMMAND,
label,
metadata: { componentTransactionId: transaction.id }
metadata: { ...metadata, componentTransactionId: transaction.id }
});
this.transaction = transaction;
this.adapters = adapters;
Expand Down Expand Up @@ -544,12 +545,8 @@ export class ProjectionalComponentEditTransaction extends EditTransaction {
/** Compact repeated property writes and retain other scalar operations in replay order. */
merge (next) {
const operations = [];
const targets = new Map();
for (const operation of [...this.transaction.runtimeChangeSet.operations,
...next.transaction.runtimeChangeSet.operations]) {
targets.set(operation.targetId,
next.adapters.runtimeContext.resolveMorph(operation.targetId) ||
this.adapters.runtimeContext.resolveMorph(operation.targetId));
const previous = operations[operations.length - 1];
if (previous?.targetId === operation.targetId && previous.property === operation.property &&
previous.valueSemantics === MorphicValueSemantics.REFERENCE &&
Expand All @@ -559,10 +556,9 @@ export class ProjectionalComponentEditTransaction extends EditTransaction {
}
const runtimeChangeSet = new MorphicChangeSet({ ...next.transaction.runtimeChangeSet, operations });
const transaction = new CoalescedComponentTransaction(this.transaction, next.transaction, runtimeChangeSet);
const runtimeContext = { ...next.adapters.runtimeContext, resolveMorph: id => targets.get(id) };
return new ProjectionalComponentEditTransaction(transaction,
next.adapters.forTransaction(transaction, runtimeContext), {
label: this.label, afterReplay: next.afterReplay
next.adapters.forTransaction(transaction), {
label: this.label, afterReplay: next.afterReplay, metadata: this.metadata
});
}

Expand Down
23 changes: 14 additions & 9 deletions lively.ide/components/reconciliation/derived-projector.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ export function projectDerivedComponentRename ({
beforeParentDocument,
afterParentDocument,
nodeId,
resolveComponentDocument = null
resolveComponentDocument = null,
normalizeModuleId = null
}) {
if (typeof source !== 'string') throw new Error('Derived rename projection requires source');
if (!(beforeParentDocument instanceof ComponentDocument) ||
Expand All @@ -104,7 +105,8 @@ export function projectDerivedComponentRename ({
exportName,
componentId,
parentDocument: beforeParentDocument,
resolveComponentDocument
resolveComponentDocument,
normalizeModuleId
});
if (!parsedBefore.supported || !parsedBefore.document.parentComponent) {
return unsupportedResult(source, [diagnostic(
Expand Down Expand Up @@ -193,7 +195,8 @@ export function projectDerivedComponentRename ({
exportName,
componentId,
parentDocument: afterParentDocument,
resolveComponentDocument
resolveComponentDocument,
normalizeModuleId
});
if (!parsedAfter.supported) {
return unsupportedResult(source, [diagnostic(
Expand Down Expand Up @@ -222,6 +225,7 @@ export function projectDerivedComponentStructure ({
beforeParentDocument,
afterParentDocument,
resolveComponentDocument = null,
normalizeModuleId = null,
restoredOverride = null,
introducedNodeId = null
}) {
Expand All @@ -242,7 +246,8 @@ export function projectDerivedComponentStructure ({
exportName,
componentId,
parentDocument: beforeParentDocument,
resolveComponentDocument
resolveComponentDocument,
normalizeModuleId
});
if (!parsedBefore.supported || !parsedBefore.document.parentComponent) {
return unsupportedResult(source, [diagnostic(
Expand Down Expand Up @@ -321,7 +326,7 @@ export function projectDerivedComponentStructure ({
}
let sourceAfter = applyChanges(source, changes);
const parseWithParent = parentDocument => parseComponentSource({
source: sourceAfter, moduleId, exportName, componentId, parentDocument, resolveComponentDocument
source: sourceAfter, moduleId, exportName, componentId, parentDocument, resolveComponentDocument, normalizeModuleId
});
let current = parseWithParent(beforeParentDocument);
const relocations = [];
Expand Down Expand Up @@ -364,7 +369,7 @@ export function projectDerivedComponentStructure ({
}
if (current.supported && restoredOverride) {
const restored = restoredOverrideSource(restoredOverride, afterParentDocument,
introducedNodeId, resolveComponentDocument);
introducedNodeId, resolveComponentDocument, normalizeModuleId);
if (restored) {
const owner = findComponentParent(current.document, introducedNodeId);
const insertion = owner && insertMovedNodeChange(sourceAfter, current.document,
Expand Down Expand Up @@ -403,7 +408,7 @@ export function projectDerivedComponentStructure ({
}

/** Reconcile the retained override against surviving descendants before reinserting it. */
function restoredOverrideSource (retained, afterParent, nodeId, resolveComponentDocument) {
function restoredOverrideSource (retained, afterParent, nodeId, resolveComponentDocument, normalizeModuleId) {
const { projection, parentDocument, nodeId: oldId } = retained;
const oldNode = findComponentNode(parentDocument, oldId);
const newNode = findComponentNode(afterParent, nodeId);
Expand All @@ -424,7 +429,7 @@ function restoredOverrideSource (retained, afterParent, nodeId, resolveComponent
componentId: projection.beforeDocument.componentId,
beforeParentDocument: parentDocument,
afterParentDocument: parent,
resolveComponentDocument
resolveComponentDocument, normalizeModuleId
});
if (!pruned.supported) throw new Error('Removed derived override could not be reconciled');
let source = pruned.sourceAfter;
Expand All @@ -438,7 +443,7 @@ function restoredOverrideSource (retained, afterParent, nodeId, resolveComponent
const renamed = projectDerivedComponentRename({ source, moduleId: document.moduleId,
exportName: document.exportName, componentId: document.componentId,
beforeParentDocument: parent, afterParentDocument: renamedParent,
nodeId: oldId, resolveComponentDocument });
nodeId: oldId, resolveComponentDocument, normalizeModuleId });
if (!renamed.supported) throw new Error('Removed derived override could not be renamed');
source = renamed.sourceAfter;
document = renamed.document;
Expand Down
59 changes: 34 additions & 25 deletions lively.ide/components/reconciliation/derived-runtime-projector.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,11 @@ function rememberRuntimeMorph (runtimeMorphs, morph) {
runtimeMorphs.set(morph.id, morph);
}

function assertCopyableIntroducedSubtree (document, node) {
if (node.provenance.kind !== ComponentNodeProvenanceKind.INHERITED ||
node.provenance.hasLocalOverrides || !isRuntimeVisible(document, node.id)) {
throw new Error(
`Cached derived runtime introduction for ${node.name} requires local synthesis`
);
}
visibleChildren(document, node).forEach(child => assertCopyableIntroducedSubtree(document, child));
/** Copy only when the base subtree already represents all derived-local state. */
function isCopyableIntroducedSubtree (document, node) {
return node.provenance.kind === ComponentNodeProvenanceKind.INHERITED &&
!node.provenance.hasLocalOverrides && isRuntimeVisible(document, node.id) &&
visibleChildren(document, node).every(child => isCopyableIntroducedSubtree(document, child));
}

function assertRuntimeSubtreeMatchesDocument (runtimeMorph, document, node) {
Expand All @@ -125,28 +122,33 @@ function operationMetadata (component) {
};
}

function introductionOperation (component, nodeId, runtimeMorphs, sourceMorph, runtimeRenames, restoredMorph) {
/** Prepare a detached copy or synthesized subtree before any transaction writes. */
function introductionOperation (component, nodeId, runtimeMorphs, sourceMorph, synthesizeIntroducedMorph, runtimeRenames, restoredMorph) {
const { beforeDocument, document: afterDocument } = component.projection;
const root = component.dependant._cachedComponent;
const node = findComponentNode(afterDocument, nodeId);
const parent = node && findComponentParent(afterDocument, nodeId);
const parentBefore = parent && findComponentNode(beforeDocument, parent.id);
const owner = parentBefore && resolveRuntimeNode(root, beforeDocument, parentBefore.id);
if (!node || !parent || !parentBefore || !owner) {
if (!node || !parent || !parentBefore || (root && !owner)) {
throw new Error(
`Cached derived component ${component.exportName} cannot resolve the introduced runtime owner`
);
}
assertRuntimeOwnerMatchesDocument(owner, beforeDocument, parentBefore);
if (!restoredMorph) assertCopyableIntroducedSubtree(afterDocument, node);
sourceMorph = restoredMorph || sourceMorph;
if (!sourceMorph || typeof sourceMorph.copy !== 'function') {
if (owner) assertRuntimeOwnerMatchesDocument(owner, beforeDocument, parentBefore);
const copyable = !restoredMorph && isCopyableIntroducedSubtree(afterDocument, node);
if (!restoredMorph && !copyable && !synthesizeIntroducedMorph) {
throw new Error(`Cached derived runtime introduction for ${node.name} requires local synthesis`);
}
if (copyable && typeof sourceMorph?.copy !== 'function') {
throw new Error(
`Cached derived component ${component.exportName} cannot copy the introduced runtime node`
);
}
const target = restoredMorph || sourceMorph.copy();
if (!target || (!restoredMorph && target === sourceMorph) || target.owner) {
const target = restoredMorph || (copyable
? sourceMorph.copy()
: synthesizeIntroducedMorph(component, nodeId));
if (!target || target === sourceMorph || target.owner) {
throw new Error(
`Cached derived component ${component.exportName} produced an invalid runtime copy`
);
Expand All @@ -164,7 +166,7 @@ function introductionOperation (component, nodeId, runtimeMorphs, sourceMorph, r
if (runtimeChild) renameCopiedDescendants(runtimeChild, child);
});
};
if (!restoredMorph) renameCopiedDescendants(target, node);
if (copyable) renameCopiedDescendants(target, node);
assertRuntimeSubtreeMatchesDocument(target, afterDocument, node);
const index = visibleChildren(afterDocument, parent)
.findIndex(({ id }) => id === nodeId);
Expand All @@ -173,15 +175,16 @@ function introductionOperation (component, nodeId, runtimeMorphs, sourceMorph, r
`Cached derived component ${component.exportName} cannot place the introduced runtime node`
);
}
const projectedNames = childrenOf(owner).map(({ name }) => name);
const projectedNames = (owner ? childrenOf(owner) : visibleChildren(beforeDocument, parentBefore))
.map(({ name }) => name);
projectedNames.splice(index, 0, target.name);
assertNamesMatch(projectedNames, afterDocument, parent);
rememberRuntimeMorph(runtimeMorphs, target);
rememberRuntimeMorph(runtimeMorphs, owner);
if (owner) rememberRuntimeMorph(runtimeMorphs, owner);
return new MoveMorph({
morphId: target.id,
from: detachedMorph(),
to: attachedMorph({ ownerId: owner.id, index }),
to: attachedMorph({ ownerId: owner?.id || `${afterDocument.componentId}:${parent.id}`, index }),
metadata: operationMetadata(component)
});
}
Expand Down Expand Up @@ -293,6 +296,7 @@ export function projectCachedDerivedRuntimeStructure ({
commandKind,
changeSetId,
sourceMorph = null,
synthesizeIntroducedMorph = null,
runtimeRenames = [],
restoredMorphs = new Map()
}) {
Expand All @@ -311,22 +315,27 @@ export function projectCachedDerivedRuntimeStructure ({
const operations = [];
const bindings = new Map();
for (const component of components) {
if (!component?.dependant?._cachedComponent) continue;
if (!component?.dependant) continue;
const beforeVisible = isRuntimeVisible(component.projection.beforeDocument, nodeId);
const afterVisible = isRuntimeVisible(component.projection.document, nodeId);
if (!beforeVisible && !afterVisible) continue;
const root = component.dependant?._cachedComponent;
// Retain an activated override's template even when its editor opens only before replay.
if (!root && !(commandKind === DerivedRuntimeStructureProjectionKind.INTRODUCE &&
!beforeVisible && afterVisible && synthesizeIntroducedMorph &&
!isCopyableIntroducedSubtree(component.projection.document,
findComponentNode(component.projection.document, nodeId)))) continue;
const operation = !beforeVisible && afterVisible
? introductionOperation(component, nodeId, runtimeMorphs, sourceMorph, runtimeRenames,
restoredMorphs.get(component.dependant))
? introductionOperation(component, nodeId, runtimeMorphs, sourceMorph, synthesizeIntroducedMorph,
runtimeRenames, restoredMorphs.get(component.dependant))
: afterVisible
? movementOperation(component, nodeId, runtimeMorphs)
: removalOperation(component, nodeId, runtimeMorphs);
if (operation) {
operations.push(operation);
const root = component.dependant._cachedComponent;
const target = runtimeMorphs.get(operation.morphId);
bindings.set(operation.morphId, {
component, target, detachedTargets: new WeakMap([[root, target]])
component, target, detachedTargets: new WeakMap(root ? [[root, target]] : [])
});
}
}
Expand Down
7 changes: 5 additions & 2 deletions lively.ide/components/reconciliation/shadow-projection.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ export function prepareShadowScalarProjection ({
bridgeCommands,
parentDocument = null,
resolveComponentDocument = null,
normalizeModuleId = null,
resolveModuleExports,
beforeDocument = null,
projectionId = 'shadow-component-projection',
Expand Down Expand Up @@ -557,7 +558,8 @@ export function prepareShadowScalarProjection ({
exportName,
componentId,
parentDocument,
resolveComponentDocument
resolveComponentDocument,
normalizeModuleId
});
if (!parsed.supported) {
return Object.freeze({
Expand Down Expand Up @@ -778,7 +780,8 @@ export function compareShadowProjectionToCurrentSource (shadowProjection, curren
exportName: document.exportName,
componentId: document.componentId,
parentDocument: document.sourceMetadata.parentDocument,
resolveComponentDocument: document.sourceMetadata.resolveComponentDocument
resolveComponentDocument: document.sourceMetadata.resolveComponentDocument,
normalizeModuleId: document.sourceMetadata.normalizeModuleId
});
if (!parsedCurrent.supported) {
return Object.freeze({
Expand Down
21 changes: 17 additions & 4 deletions lively.ide/components/reconciliation/source-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ export function parseComponentSource ({
exportName,
componentId = `${moduleId}#${exportName}`,
parentDocument = null,
resolveComponentDocument = null
resolveComponentDocument = null,
normalizeModuleId = null
}) {
if (typeof source !== 'string') throw new Error('Component source must be a string');
if (typeof moduleId !== 'string' || !moduleId) throw new Error('Component source requires a moduleId');
Expand All @@ -269,6 +270,9 @@ export function parseComponentSource ({
if (resolveComponentDocument !== null && typeof resolveComponentDocument !== 'function') {
throw new Error('Component source resolveComponentDocument must be a function');
}
if (normalizeModuleId !== null && typeof normalizeModuleId !== 'function') {
throw new Error('Component source normalizeModuleId must be a function');
}

const diagnostics = [];
let moduleAst;
Expand Down Expand Up @@ -317,14 +321,22 @@ export function parseComponentSource ({
const local = namespaceParent ? parentNode.object.name : parentName;
const binding = importBindingsOf(moduleAst).find(binding => binding.local === local);
let bindingModuleId = binding?.moduleId;
if (bindingModuleId?.startsWith('.')) {
try { bindingModuleId = new URL(bindingModuleId, moduleId).href; } catch (error) { bindingModuleId = null; }
let parentModuleId = parentDocument.moduleId;
if (!resolved && binding) {
try {
if (normalizeModuleId) {
bindingModuleId = normalizeModuleId(bindingModuleId, moduleId);
parentModuleId = normalizeModuleId(parentModuleId, moduleId);
} else if (bindingModuleId.startsWith('.')) {
bindingModuleId = new URL(bindingModuleId, moduleId).href;
}
} catch (error) { bindingModuleId = null; }
}
const matches = resolved
? resolved.moduleId === parentDocument.moduleId && resolved.exportName === parentDocument.exportName
: (parentNode?.type === 'Identifier' || (namespaceParent && binding?.kind === ComponentImportKind.NAMESPACE)) &&
(binding?.imported || parentName) === parentDocument.exportName &&
(!binding || bindingModuleId === parentDocument.moduleId);
(!binding || bindingModuleId === parentModuleId);
if (!matches) {
diagnostics.push(diagnostic(
ComponentSourceDiagnosticKind.PARENT_COMPONENT_MISMATCH,
Expand Down Expand Up @@ -919,6 +931,7 @@ export function parseComponentSource ({
opaqueSubmorphExpressions,
parentDocument,
resolveComponentDocument,
normalizeModuleId,
importBindings: importBindingsOf(moduleAst),
importDeclarationCount: moduleAst.body.filter(statement =>
statement.type === 'ImportDeclaration').length,
Expand Down
Loading
Loading