Skip to content

Commit

Permalink
tree2: add sequence mutators (#17523)
Browse files Browse the repository at this point in the history
This PR implements sequence mutation methods. Tests will be added in a
follow-up PR when #17475 is complete.
  • Loading branch information
taylorsw04 committed Sep 29, 2023
1 parent 23324ff commit 61d83df
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 18 deletions.
16 changes: 14 additions & 2 deletions api-report/tree2.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ export function brandOpaque<T extends BrandedType<any, string>>(value: isAny<Val
// @alpha
export type ChangesetLocalId = Brand<number, "ChangesetLocalId">;

// @alpha
export type CheckTypesOverlap<T, TCheck> = [Extract<T, TCheck> extends never ? never : T][0];

// @alpha
export type ChildCollection = FieldKey | RootField;

Expand Down Expand Up @@ -1948,12 +1951,21 @@ export interface Sequence2<TTypes extends AllowedTypes> extends TreeField {
readonly asArray: readonly UnboxNodeUnion<TTypes>[];
at(index: number): UnboxNodeUnion<TTypes>;
boxedAt(index: number): TypedNodeUnion<TTypes>;
insertAt(index: number, value: FlexibleNodeContent<TTypes>[]): void;
insertAtEnd(value: FlexibleNodeContent<TTypes>[]): void;
insertAtStart(value: FlexibleNodeContent<TTypes>[]): void;
// (undocumented)
readonly length: number;
map<U>(callbackfn: (value: UnboxNodeUnion<TTypes>, index: number) => U): U[];
mapBoxed<U>(callbackfn: (value: TypedNodeUnion<TTypes>, index: number) => U): U[];
// (undocumented)
replaceRange(index: number, count: number, content: Iterable<FlexibleNodeContent<TTypes>>): void;
moveToEnd(sourceStart: number, sourceEnd: number): void;
moveToEnd<TTypesSource extends AllowedTypes>(sourceStart: number, sourceEnd: number, source: Sequence2<CheckTypesOverlap<TTypesSource, TTypes>>): void;
moveToIndex(index: number, sourceStart: number, sourceEnd: number): void;
moveToIndex<TTypesSource extends AllowedTypes>(index: number, sourceStart: number, sourceEnd: number, source: Sequence2<CheckTypesOverlap<TTypesSource, TTypes>>): void;
moveToStart(sourceStart: number, sourceEnd: number): void;
moveToStart<TTypesSource extends AllowedTypes>(sourceStart: number, sourceEnd: number, source: Sequence2<CheckTypesOverlap<TTypesSource, TTypes>>): void;
removeAt(index: number): void;
removeRange(start?: number, end?: number): void;
}

// @alpha (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,16 @@ export type FlexibleNodeContent<TTypes extends AllowedTypes> = SchemaAware.Allow
TTypes
>;

/**
* Type to ensures two types overlap in at least one way.
* It evaluates to the input type if this is true, and never otherwise.
* Examples:
* CheckTypesOverlap\<number | boolean, number | object\> = number | boolean
* CheckTypesOverlap\<number | boolean, string | object\> = never
* @alpha
*/
export type CheckTypesOverlap<T, TCheck> = [Extract<T, TCheck> extends never ? never : T][0];

/**
* {@link TreeField} that stores a sequence of children.
*
Expand Down Expand Up @@ -512,11 +522,120 @@ export interface Sequence<TTypes extends AllowedTypes> extends TreeField {

readonly length: number;

// TODO: more and/or better editing APIs. As is, this can't express moves.
replaceRange(
/**
* Inserts new item(s) at a specified location.
* @param index - The index at which to insert `value`.
* @param value - The content to insert.
* @throws Throws if any of the input indices are invalid.
*/
insertAt(index: number, value: FlexibleNodeContent<TTypes>[]): void;

/**
* Inserts new item(s) at the start of the sequence.
* @param value - The content to insert.
* @throws Throws if any of the input indices are invalid.
*/
insertAtStart(value: FlexibleNodeContent<TTypes>[]): void;

/**
* Inserts new item(s) at the end of the sequence.
* @param value - The content to insert.
* @throws Throws if any of the input indices are invalid.
*/
insertAtEnd(value: FlexibleNodeContent<TTypes>[]): void;

/**
* Removes the item at the specified location.
* @param index - The index at which to remove the item.
* @throws Throws if any of the input indices are invalid.
*/
removeAt(index: number): void;

/**
* Removes all items between the specified indices.
* @param start - The starting index of the range to remove (inclusive). Defaults to the start of the sequence.
* @param end - The ending index of the range to remove (exclusive).
* @throws Throws if any of the input indices are invalid.
* If `end` is not supplied or is greater than the length of the sequence, all items after `start` are deleted.
*/
removeRange(start?: number, end?: number): void;

/**
* Moves the specified items to the start of the sequence.
* @param sourceStart - The starting index of the range to move (inclusive).
* @param sourceEnd - The ending index of the range to move (exclusive)
* @throws Throws if any of the input indices are invalid.
* @remarks
* All indices are relative to the sequence excluding the nodes being moved.
*/
moveToStart(sourceStart: number, sourceEnd: number): void;

/**
* Moves the specified items to the start of the sequence.
* @param sourceStart - The starting index of the range to move (inclusive).
* @param sourceEnd - The ending index of the range to move (exclusive)
* @param source - The source sequence to move items out of.
* @throws Throws if the types of any of the items being moved are not allowed in the destination sequence or if the input indices are invalid.
* @remarks
* All indices are relative to the sequence excluding the nodes being moved.
*/
moveToStart<TTypesSource extends AllowedTypes>(
sourceStart: number,
sourceEnd: number,
source: Sequence<CheckTypesOverlap<TTypesSource, TTypes>>,
): void;

/**
* Moves the specified items to the end of the sequence.
* @param sourceStart - The starting index of the range to move (inclusive).
* @param sourceEnd - The ending index of the range to move (exclusive)
* @throws Throws if any of the input indices are invalid.
* @remarks
* All indices are relative to the sequence excluding the nodes being moved.
*/
moveToEnd(sourceStart: number, sourceEnd: number): void;

/**
* Moves the specified items to the end of the sequence.
* @param sourceStart - The starting index of the range to move (inclusive).
* @param sourceEnd - The ending index of the range to move (exclusive)
* @param source - The source sequence to move items out of.
* @throws Throws if the types of any of the items being moved are not allowed in the destination sequence or if the input indices are invalid.
* @remarks
* All indices are relative to the sequence excluding the nodes being moved.
*/
moveToEnd<TTypesSource extends AllowedTypes>(
sourceStart: number,
sourceEnd: number,
source: Sequence<CheckTypesOverlap<TTypesSource, TTypes>>,
): void;

/**
* Moves the specified items to the desired location within the sequence.
* @param index - The index to move the items to.
* @param sourceStart - The starting index of the range to move (inclusive).
* @param sourceEnd - The ending index of the range to move (exclusive)
* @throws Throws if any of the input indices are invalid.
* @remarks
* All indices are relative to the sequence excluding the nodes being moved.
*/
moveToIndex(index: number, sourceStart: number, sourceEnd: number): void;

/**
* Moves the specified items to the desired location within the sequence.
* @param index - The index to move the items to.
* @param sourceStart - The starting index of the range to move (inclusive).
* @param sourceEnd - The ending index of the range to move (exclusive)
* @param source - The source sequence to move items out of.
* @throws Throws if the types of any of the items being moved are not allowed in the destination sequence or if the input indices are invalid.
* @remarks
* All indices are relative to the sequence excluding the nodes being moved.
*/
moveToIndex<TTypesSource extends AllowedTypes>(
index: number,
count: number,
content: Iterable<FlexibleNodeContent<TTypes>>,
sourceStart: number,
sourceEnd: number,
source: Sequence<CheckTypesOverlap<TTypesSource, TTypes>>,
): void;

[boxedIterator](): IterableIterator<TypedNodeUnion<TTypes>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export {
TypedNode,
TypedNodeUnion,
boxedIterator,
CheckTypesOverlap,
} from "./editableTreeTypes";

export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ import {
SequenceFieldEditBuilder,
ValueFieldEditBuilder,
} from "../default-field-kinds";
import { compareSets, disposeSymbol, fail } from "../../util";
import {
assertValidIndex,
assertValidRangeIndices,
compareSets,
disposeSymbol,
fail,
} from "../../util";
import { AllowedTypes, FieldSchema } from "../typed-schema";
import { TreeStatus, treeStatusFromPath } from "../editable-tree";
import { Context } from "./context";
Expand All @@ -40,6 +46,7 @@ import {
TreeNode,
RequiredField,
boxedIterator,
CheckTypesOverlap,
} from "./editableTreeTypes";
import { makeTree } from "./lazyTree";
import {
Expand Down Expand Up @@ -246,26 +253,122 @@ export class LazySequence<TTypes extends AllowedTypes>
makePropertyEnumerableOwn(this, "asArray", LazySequence.prototype);
}

public get asArray(): readonly UnboxNodeUnion<TTypes>[] {
return this.map((x) => x);
}

private sequenceEditor(): SequenceFieldEditBuilder {
const fieldPath = this.getFieldPathForEditing();
const fieldEditor = this.context.editor.sequenceField(fieldPath);
return fieldEditor;
}

public replaceRange(
index: number,
count: number,
newContent: Iterable<FlexibleNodeContent<TTypes>>,
): void {
public insertAt(index: number, value: FlexibleNodeContent<TTypes>[]): void {
const fieldEditor = this.sequenceEditor();
const content = this.normalizeNewContent([...newContent]);

fieldEditor.delete(index, count);
const content = this.normalizeNewContent(Array.isArray(value) ? value : [value]);
assertValidIndex(index, this, true);
fieldEditor.insert(index, content);
}

public get asArray(): readonly UnboxNodeUnion<TTypes>[] {
return this.map((x) => x);
public insertAtStart(value: FlexibleNodeContent<TTypes>[]): void {
this.insertAt(0, value);
}

public insertAtEnd(value: FlexibleNodeContent<TTypes>[]): void {
this.insertAt(this.length, value);
}

public removeAt(index: number): void {
const fieldEditor = this.sequenceEditor();
fieldEditor.delete(index, 1);
}

public removeRange(start?: number, end?: number): void {
const fieldEditor = this.sequenceEditor();
const { length } = this;
const removeStart = start ?? 0;
const removeEnd = Math.min(length, end ?? length);
assertValidRangeIndices(removeStart, removeEnd, this);
fieldEditor.delete(removeStart, removeEnd - removeStart);
}

public moveToStart(sourceStart: number, sourceEnd: number): void;
public moveToStart<TTypesSource extends AllowedTypes>(
sourceStart: number,
sourceEnd: number,
source: Sequence<CheckTypesOverlap<TTypesSource, TTypes>>,
): void;
public moveToStart<TTypesSource extends AllowedTypes>(
sourceStart: number,
sourceEnd: number,
source?: Sequence<CheckTypesOverlap<TTypesSource, TTypes>>,
): void {
this._moveToIndex(0, sourceStart, sourceEnd, source);
}

public moveToEnd(sourceStart: number, sourceEnd: number): void;
public moveToEnd<TTypesSource extends AllowedTypes>(
sourceStart: number,
sourceEnd: number,
source: Sequence<CheckTypesOverlap<TTypesSource, TTypes>>,
): void;
public moveToEnd<TTypesSource extends AllowedTypes>(
sourceStart: number,
sourceEnd: number,
source?: Sequence<CheckTypesOverlap<TTypesSource, TTypes>>,
): void {
this._moveToIndex(this.length, sourceStart, sourceEnd, source);
}

public moveToIndex(index: number, sourceStart: number, sourceEnd: number): void;
public moveToIndex<TTypesSource extends AllowedTypes>(
index: number,
sourceStart: number,
sourceEnd: number,
source: Sequence<CheckTypesOverlap<TTypesSource, TTypes>>,
): void;
public moveToIndex<TTypesSource extends AllowedTypes>(
index: number,
sourceStart: number,
sourceEnd: number,
source?: Sequence<CheckTypesOverlap<TTypesSource, TTypes>>,
): void {
this._moveToIndex(index, sourceStart, sourceEnd, source);
}

private _moveToIndex<TTypesSource extends AllowedTypes>(
index: number,
sourceStart: number,
sourceEnd: number,
source?: Sequence<CheckTypesOverlap<TTypesSource, TTypes>>,
): void {
const sourceField = source !== undefined ? (this.isSameAs(source) ? this : source) : this;
assertValidRangeIndices(sourceStart, sourceEnd, sourceField);
if (this.schema.types !== undefined && sourceField !== this) {
for (let i = sourceStart; i < sourceEnd; i++) {
const sourceNode = sourceField.at(sourceStart);
if (!this.schema.types.has(sourceNode.schema.name)) {
throw new Error("Type in source sequence is not allowed in destination.");
}
}
}
const count = sourceEnd - sourceStart;
let destinationIndex = index;
if (sourceField === this) {
destinationIndex -= count;
}
assertValidIndex(destinationIndex, this, true);
// TODO: determine support for move across different sequence types
assert(source instanceof LazySequence, "Unsupported sequence implementation.");
const sourceFieldPath = (sourceField as LazySequence<TTypesSource>).getFieldPath();
const destinationFieldPath = this.getFieldPath();
this.context.editor.move(
sourceFieldPath,
sourceStart,
count,
destinationFieldPath,
destinationIndex,
);
}
}

Expand Down
1 change: 1 addition & 0 deletions experimental/dds/tree2/src/feature-libraries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export {
TreeNode,
getTreeContext,
boxedIterator,
CheckTypesOverlap,
} from "./editable-tree-2";

// Split into separate import and export for compatibility with API-Extractor.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ export class FieldSchema<Kind extends FieldKind = FieldKind, Types = AllowedType
*/
public constructor(public readonly kind: Kind, public readonly allowedTypes: Types) {}

// TODO:#5702 cache the result of this getter
public get types(): TreeTypeSet {
return allowedTypesToTypeSet(this.allowedTypes as unknown as AllowedTypes);
}
Expand Down
1 change: 1 addition & 0 deletions experimental/dds/tree2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ export {
LeafSchema,
MapSchema,
StructSchema,
CheckTypesOverlap,
} from "./feature-libraries";

export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe("LazyField", () => {
);
cursor.free();
assert.throws(
() => sequenceField.replaceRange(0, 1, []),
() => sequenceField.insertAt(0, [1]),
(e: Error) =>
validateAssertionError(
e,
Expand Down
1 change: 1 addition & 0 deletions experimental/dds/tree2/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export {
disposeSymbol,
IDisposable,
capitalize,
assertValidRangeIndices,
} from "./utils";
export { ReferenceCountedBase, ReferenceCounted } from "./referenceCounting";

Expand Down

0 comments on commit 61d83df

Please sign in to comment.