Operations

Comprehensive guide to operation types that can be combined in transactions for model-based editors.

Overview

Operations are atomic changes to the document model. They can be grouped into transactions to ensure consistency and enable undo/redo functionality. Each operation type has specific properties and behaviors.

Operation characteristics:

  • Immutable - operations don't modify existing state
  • Invertible - each operation has an inverse
  • Composable - operations can be combined in transactions
  • Validatable - operations can be validated before application

Operation Interface

All operations follow a common interface:

interface Operation {
  type: string;
  path: Path;
  data?: any;
  inverse?: Operation; // Pre-computed inverse for efficiency
  metadata?: {
    source?: 'user' | 'programmatic';
    timestamp?: number;
    [key: string]: any;
  };
}

// Path represents position in document tree
type Path = number[];

// Example paths:
// [0] - first child of root
// [0, 1] - second child of first child
// [0, 1, 2] - third child of second child of first child

Insert Operations

Insert operations add content to the document at a specific path.

Delete Operations

Delete operations remove content from the document.

Format Operations

Format operations modify text styling without changing content.

Replace Operations

Replace operations combine delete and insert in a single operation.

Move Operations

Move operations relocate content within the document.

Node Structure Operations

Operations that modify node structure and relationships.

Composite Operations

Composite operations combine multiple operations for complex transformations.

Operation Inversion

Every operation must have an inverse for undo functionality.

class OperationInverter {
  invert(operation: Operation): Operation {
    switch (operation.type) {
      case 'insertText':
        return {
          type: 'deleteText',
          path: operation.path,
          length: operation.text.length
        };
      
      case 'deleteText':
        return {
          type: 'insertText',
          path: operation.path,
          text: operation.deletedContent || ''
        };
      
      case 'insertNode':
        return {
          type: 'deleteNode',
          path: operation.path
        };
      
      case 'deleteNode':
        return {
          type: 'insertNode',
          path: operation.path,
          node: operation.deletedNode
        };
      
      case 'applyFormat':
        return {
          type: 'removeFormat',
          path: operation.path,
          length: operation.length,
          format: operation.format
        };
      
      case 'removeFormat':
        return {
          type: 'applyFormat',
          path: operation.path,
          length: operation.length,
          format: operation.format,
          value: operation.previousValue
        };
      
      case 'replace':
        return {
          type: 'replace',
          path: operation.path,
          length: typeof operation.content === 'string' 
            ? operation.content.length 
            : 1,
          content: operation.deletedContent
        };
      
      case 'move':
        return {
          type: 'move',
          fromPath: operation.toPath,
          toPath: operation.fromPath
        };
      
      case 'splitNode':
        return {
          type: 'mergeNodes',
          path: operation.path,
          targetPath: [operation.path[0] + 1],
          position: operation.position
        };
      
      case 'mergeNodes':
        return {
          type: 'splitNode',
          path: operation.path,
          position: operation.position
        };
      
      case 'wrap':
        return {
          type: 'unwrap',
          path: operation.path,
          wrapperType: operation.wrapper.type,
          preservedWrapper: operation.wrapper
        };
      
      case 'unwrap':
        return {
          type: 'wrap',
          path: operation.path,
          wrapper: operation.preservedWrapper!
        };
      
      case 'updateAttributes':
        return {
          type: 'updateAttributes',
          path: operation.path,
          attributes: operation.previousAttributes || {},
          previousAttributes: operation.attributes
        };
      
      case 'setNodeType':
        return {
          type: 'setNodeType',
          path: operation.path,
          nodeType: operation.previousType!,
          previousType: operation.nodeType,
          attributes: operation.previousAttributes,
          previousAttributes: operation.attributes
        };
      
      default:
        throw new Error(`Unknown operation type: ${operation.type}`);
    }
  }
}

// Pre-compute inverse for efficiency
function createOperationWithInverse(operation: Operation): Operation {
  const inverter = new OperationInverter();
  return {
    ...operation,
    inverse: inverter.invert(operation)
  };
}