or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

changes.mdcharacter-utils.mdeditor-state.mdextensions.mdindex.mdrange-sets.mdselection.mdtext.mdtransactions.md
tile.json

changes.mddocs/

Change Management

Document change system with immutable change sets, position mapping, and change composition for collaborative editing.

Capabilities

ChangeSet Class

Immutable representation of document changes that can be applied to text documents.

/**
 * A change set represents a number of changes to a document. It stores the 
 * document length, and can update a document or be composed with other change sets.
 */
class ChangeSet extends ChangeDesc {
  /** Create a change set from a change specification */
  static of(changes: ChangeSpec, length: number, lineSep?: string): ChangeSet;
  
  /** Create an empty change set for a document of the given length */
  static empty(length: number): ChangeSet;
  
  /** Apply this change set to a document */
  apply(doc: Text): Text;
  
  /** Apply this change set to a JSON representation of a document */
  applyToJSON(json: any): any;
  
  /** Create the inverse of this change set */
  invert(doc: Text): ChangeSet;
  
  /** Compose this change set with another change set */
  compose(other: ChangeSet): ChangeSet;
  
  /** Map this change set through another change, producing a new change set */
  map(other: ChangeDesc, before?: boolean): ChangeSet;
  
  /** Iterate over the changed ranges */
  iterChanges(f: (fromA: number, toA: number, fromB: number, toB: number, inserted: string) => void, individual?: boolean): void;
  
  /** Get a JSON representation of this change set */
  toJSON(): any;
  
  /** Create a change set from a JSON representation */
  static fromJSON(json: any): ChangeSet;
}

Usage Examples:

import { ChangeSet, Text } from "@codemirror/state";

const doc = Text.of(["Hello, world!", "Second line"]);

// Create a change set
const changes = ChangeSet.of([
  { from: 7, to: 12, insert: "CodeMirror" },  // Replace "world" with "CodeMirror"
  { from: 25, insert: " - Added text" }       // Insert at end of second line
], doc.length);

// Apply changes to document
const newDoc = changes.apply(doc);
console.log(newDoc.toString()); // "Hello, CodeMirror!\nSecond line - Added text"

// Create inverse changes
const inverse = changes.invert(doc);
const restored = inverse.apply(newDoc);
console.log(restored.eq(doc)); // true - document is restored

ChangeDesc Class

Base class for change descriptions without the actual replacement text.

/**
 * A change description is a variant of change set that doesn't store the inserted text.
 * As such, it can't be applied, but is cheaper to store and manipulate.
 */
class ChangeDesc {
  /** The length of the document before the change */
  readonly length: number;
  
  /** The length of the document after the change */
  readonly newLength: number;
  
  /** False when there are actual changes in this set */
  readonly empty: boolean;
  
  /** Get a description of the inverted form of these changes */
  get invertedDesc(): ChangeDesc;
  
  /** Compute the combined effect of applying another set of changes after this one */
  composeDesc(other: ChangeDesc): ChangeDesc;
  
  /** Map this description over another set of changes */
  mapDesc(other: ChangeDesc, before?: boolean): ChangeDesc;
  
  /** Map a given position through these changes */
  mapPos(pos: number, assoc?: number, mode?: MapMode): number | null;
  
  /** Check whether these changes touch a given range */
  touchesRange(from: number, to?: number): boolean | "cover";
  
  /** Iterate over the unchanged parts left by these changes */
  iterGaps(f: (posA: number, posB: number, length: number) => void): void;
  
  /** Iterate over the ranges changed by these changes */
  iterChangedRanges(f: (fromA: number, toA: number, fromB: number, toB: number) => void, individual?: boolean): void;
}

Usage Examples:

const changes = ChangeSet.of([
  { from: 5, to: 10, insert: "replacement" },
  { from: 15, insert: "insertion" }
], 20);

console.log(changes.length);    // 20 (original document length)
console.log(changes.newLength); // 29 (new document length)
console.log(changes.empty);     // false (has changes)

// Map positions through changes
const mappedPos = changes.mapPos(8);  // Position 8 becomes 13 (after "replacement")
const mappedPos2 = changes.mapPos(17); // Position 17 becomes 31 (after both changes)

// Check if changes affect a range
const touches = changes.touchesRange(6, 9); // true (range overlaps with first change)
const covers = changes.touchesRange(6, 8);  // "cover" (range is completely covered)

// Iterate over unchanged parts
changes.iterGaps((posA, posB, length) => {
  console.log(`Unchanged: ${posA}-${posA + length} maps to ${posB}-${posB + length}`);
});

Change Specifications

Types and interfaces for specifying document changes.

/**
 * Change specifications can be provided in several formats
 */
type ChangeSpec = 
  | {from: number, to?: number, insert?: string | Text}
  | readonly {from: number, to?: number, insert?: string | Text}[]
  | ChangeSet;

/**
 * Individual change specification
 */
interface ChangeSpecification {
  /** Start position of the change */
  from: number;
  
  /** End position of the change (defaults to from for insertions) */
  to?: number;
  
  /** Text to insert at this position */
  insert?: string | Text;
}

Usage Examples:

// Single change specification
const singleChange: ChangeSpec = {
  from: 5,
  to: 10,
  insert: "replacement"
};

// Multiple changes
const multipleChanges: ChangeSpec = [
  { from: 0, to: 5, insert: "Hello" },
  { from: 10, insert: " world" },
  { from: 20, to: 25 } // Deletion (no insert)
];

// Pure insertion
const insertion: ChangeSpec = {
  from: 5,
  insert: "inserted text"
};

// Pure deletion  
const deletion: ChangeSpec = {
  from: 5,
  to: 10
};

// Create change sets from specifications
const changeSet1 = ChangeSet.of(singleChange, 30);
const changeSet2 = ChangeSet.of(multipleChanges, 30);

Position Mapping

System for tracking position changes through document modifications.

/**
 * Distinguishes different ways in which positions can be mapped
 */
enum MapMode {
  /** Map a position to a valid new position, even when its context was deleted */
  Simple,
  
  /** Return null if deletion happens across the position */
  TrackDel,
  
  /** Return null if the character before the position is deleted */
  TrackBefore,
  
  /** Return null if the character after the position is deleted */
  TrackAfter
}

/**
 * Map a position through changes
 * @param pos Position to map
 * @param assoc Association direction (-1 for before, 1 for after, 0 for default)
 * @param mode Mapping mode
 */
mapPos(pos: number, assoc?: number, mode?: MapMode): number | null;

Usage Examples:

const changes = ChangeSet.of([
  { from: 5, to: 10, insert: "NEW" }, // Replace 5 characters with 3
  { from: 15, insert: "+" }           // Insert 1 character
], 20);

// Simple mapping (default)
console.log(changes.mapPos(3));  // 3 (before first change)
console.log(changes.mapPos(7));  // 7 (mapped to middle of replacement)
console.log(changes.mapPos(12)); // 10 (after first change, adjusted for length difference)
console.log(changes.mapPos(16)); // 14 (after both changes)

// Track deletions
console.log(changes.mapPos(7, 0, MapMode.TrackDel)); // null (position was deleted)
console.log(changes.mapPos(3, 0, MapMode.TrackDel)); // 3 (position not deleted)

// Association affects boundary behavior
console.log(changes.mapPos(5, -1)); // 5 (stay before insertion)
console.log(changes.mapPos(5, 1));  // 8 (move after insertion)

Change Composition

Methods for combining and manipulating change sets.

/**
 * Compose this change set with another change set
 * @param other The change set to compose with
 */
compose(other: ChangeSet): ChangeSet;

/**
 * Map this change set through another change
 * @param other The change to map through
 * @param before Whether to map as if this change happened before other
 */
map(other: ChangeDesc, before?: boolean): ChangeSet;

/**
 * Create the inverse of this change set
 * @param doc The document this change set was applied to
 */
invert(doc: Text): ChangeSet;

Usage Examples:

const doc = Text.of(["Original text"]);

// First change: replace "Original" with "Modified"
const change1 = ChangeSet.of([
  { from: 0, to: 8, insert: "Modified" }
], doc.length);

// Second change: append " content"  
const change2 = ChangeSet.of([
  { from: 13, insert: " content" }
], change1.newLength);

// Compose changes
const composed = change1.compose(change2);
const result = composed.apply(doc);
console.log(result.toString()); // "Modified text content"

// Map changes through each other (for collaborative editing)
const parallel1 = ChangeSet.of([{ from: 9, insert: "extra " }], doc.length);
const parallel2 = ChangeSet.of([{ from: 0, to: 8, insert: "New" }], doc.length);

const mapped1 = parallel1.map(parallel2, false); // Map parallel1 after parallel2
const mapped2 = parallel2.map(parallel1, true);  // Map parallel2 before parallel1

// Invert changes
const inverted = change1.invert(doc);
const restored = inverted.apply(change1.apply(doc));
console.log(restored.eq(doc)); // true

Change Iteration

Methods for examining the content of changes.

/**
 * Iterate over the changed ranges, providing both old and new content
 * @param f Callback function receiving change information
 * @param individual Whether to report adjacent changes separately
 */
iterChanges(f: (fromA: number, toA: number, fromB: number, toB: number, inserted: string) => void, individual?: boolean): void;

/**
 * Iterate over the ranges changed by these changes (ChangeDesc version)
 * @param f Callback function receiving position information
 * @param individual Whether to report adjacent changes separately  
 */
iterChangedRanges(f: (fromA: number, toA: number, fromB: number, toB: number) => void, individual?: boolean): void;

/**
 * Iterate over the unchanged parts left by these changes
 * @param f Callback function receiving gap information
 */
iterGaps(f: (posA: number, posB: number, length: number) => void): void;

Usage Examples:

const changes = ChangeSet.of([
  { from: 5, to: 10, insert: "HELLO" },
  { from: 15, to: 17, insert: "XX" }
], 25);

// Iterate over changes with content
changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
  console.log(`Change: ${fromA}-${toA} -> ${fromB}-${toB}, inserted: "${inserted}"`);
});
// Output:
// Change: 5-10 -> 5-10, inserted: "HELLO"  
// Change: 15-17 -> 20-22, inserted: "XX"

// Iterate over changed ranges (no content)
changes.iterChangedRanges((fromA, toA, fromB, toB) => {
  console.log(`Range: ${fromA}-${toA} -> ${fromB}-${toB}`);
});

// Iterate over unchanged gaps
changes.iterGaps((posA, posB, length) => {
  console.log(`Gap: ${posA} -> ${posB}, length: ${length}`);
});
// Shows the parts of the document that weren't changed

Types

/**
 * Interface for objects that have a length and can have changes applied
 */
interface Changeable {
  readonly length: number;
}

/**
 * Result of applying a change to a document  
 */
interface ChangeResult<T> {
  doc: T;
  changes: ChangeSet;
}