All Downloads are FREE. Search and download functionalities are using the official Maven repository.

polynote.messages.ContentEdit.scala Maven / Gradle / Ivy

The newest version!
package polynote.messages

import polynote.data.Rope
import scodec.Codec
import scodec.codecs.{Discriminated, Discriminator, byte}
import scodec.codecs.implicits._
import shapeless.cachedImplicit

import scala.collection.mutable.ListBuffer


sealed trait ContentEdit {
  def pos: Int

  def rebase(other: ContentEdits): ContentEdits = ContentEdits(ShortList(ContentEdit.rebaseAll(this, other.edits)._1))

  def applyTo(rope: Rope): Rope = this match {
    case Insert(pos, content) => rope.insertAt(pos, Rope(content))
    case Delete(pos, length)  => rope.delete(pos, length)
  }

  def nonEmpty: Boolean



  def action(onStr: String): String = this match {
    case Delete(pos, len) => s"""Delete "${onStr.substring(pos, pos + len)}" (at $pos)"""
    case Insert(pos, str) =>
      val before = if (pos > 0) s""""${onStr.substring(pos - 1, pos)}"""" else "beginning"
      val after = if (pos < onStr.length - 1) s""""${onStr.substring(pos, pos + 1)}"""" else "end"
      s"""Insert "$str" between $before and $after (at $pos)"""
  }
}

final case class Insert(pos: Int, content: String) extends ContentEdit {
  require(pos >= 0, "pos >= 0")

  def nonEmpty: Boolean = content.nonEmpty

  override def toString: String = s"""Insert($pos, "$content")"""
}

object Insert {
  implicit val insertTag: Discriminator[ContentEdit, Insert, Byte] = Discriminator(0)
}

final case class Delete(pos: Int, length: Int) extends ContentEdit {
  require(pos >= 0, "pos >= 0")
  require(length >= 0, "length >= 0")
  
  override def nonEmpty: Boolean = length != 0
}

object Delete {
  implicit val deleteTag: Discriminator[ContentEdit, Delete, Byte] = Discriminator(1)
}

object ContentEdit {
  implicit val discriminated: Discriminated[ContentEdit, Byte] = Discriminated(byte)
  implicit val codec: Codec[ContentEdit] = cachedImplicit

  /**
    * Rebase a onto b and b onto a
    * @param client Whether to use the client-side rebasing logic (should be false)
    * @return each edit rebased onto the other
    */
  //
  def rebase(a: ContentEdit, b: ContentEdit, client: Boolean = false): (List[ContentEdit], List[ContentEdit]) = (a, b) match {

    // equivalent edits cancel each other out on the server
    case (a, b) if a == b && !client => Nil -> Nil

    // equivalent edits are preserved on the client
    case (a, b) if a == b => List(a) -> List(a)

    // if A is before B, or A and B are at the same spot but (A is shorter than B or equal in length but lexically before B)
    // then A comes before B. (but is reverse for the client)
    case (winner @ Insert(posA, contentA), Insert(posB, contentB)) if
    (posA < posB) ||
      (posA == posB &&
        ((contentA.length < contentB.length || (contentA.length == contentB.length && contentA <= contentB)) && !client)) =>
      List(winner) -> List(Insert(posB + contentA.length, contentB))

    // Otherwise insert B comes before insert A
    case (Insert(posA, contentA), winner @ Insert(_, contentB)) =>
      List(Insert(posA + contentB.length, contentA)) -> List(winner)

    // If an insert comes before a delete, the delete moves forward by its length
    case (winner @ Insert(posA, contentA), Delete(posB, lengthB)) if posA <= posB =>
      List(winner) -> List(Delete(posB + contentA.length, lengthB))

    case (Delete(posA, lengthA), winner @ Insert(posB, contentB)) if posB <= posA =>
      List(Delete(posA + contentB.length, lengthA)) -> List(winner)

    // insert is in the middle of delete; delete has to split into before-insert and after-insert parts
    // and insert has to move back to where the delete started
    case (Insert(posA, contentA), Delete(posB, lengthB)) if posA < posB + lengthB =>
      val beforeLength = posA - posB
      List(Insert(posB, contentA)) -> List(Delete(posB, beforeLength), Delete(posB + contentA.length, lengthB - beforeLength))

    case (Delete(posA, lengthA), Insert(posB, contentB)) if posB < posA + lengthA =>
      val beforeLength = posB - posA
      List(Delete(posA, beforeLength), Delete(posA + contentB.length, lengthA - beforeLength)) -> List(Insert(posA, contentB))

    // insert is after delete - insert has to move back by deletion length
    case (Insert(posA, contentA), del @ Delete(_, lengthB)) =>
      List(Insert(posA - lengthB, contentA)) -> List(del)

    case (del @ Delete(_, lengthA), Insert(posB, contentB)) =>
      List(del) -> List(Insert(posB - lengthA, contentB))

    // delete A comes entirely before delete B;  move B back by A's length
    case (winner @ Delete(posA, lengthA), Delete(posB, lengthB)) if posA + lengthA <= posB =>
      List(winner) -> List(Delete(posB - lengthA, lengthB))

    // delete B comes entirely before delete A; move A back by B's length
    case (Delete(posA, lengthA), winner @ Delete(posB, lengthB)) if posB + lengthB <= posA =>
      List(Delete(posA - lengthB, lengthA)) -> List(winner)

    // B is entirely within A; A should shorten by B's length and B should disappear
    case (Delete(posA, lengthA), Delete(posB, lengthB)) if posB >= posA && posB + lengthB <= posA + lengthA =>
      List(Delete(posA, lengthA - lengthB)) -> Nil

    // A is entirely within B; opposite of above
    case (Delete(posA, lengthA), Delete(posB, lengthB)) if posA >= posB && posA + lengthA <= posB + lengthB =>
      Nil -> List(Delete(posB, lengthB - lengthA))

    // delete B starts in the middle of delete A; A should stop where B starts, and B should move to where A starts and shorten itself by the overlap
    case (Delete(posA, lengthA), Delete(posB, lengthB)) if posB > posA =>
      val overlap = posA + lengthA - posB
      List(Delete(posA, lengthA - overlap)) -> List(Delete(posA, lengthB - overlap))

    // delete A starts in the middle of B; opposite of above
    case (Delete(posA, lengthA), Delete(posB, lengthB)) if posA > posB =>
      val overlap = posB + lengthB - posA
      List(Delete(posB, lengthA - overlap)) -> List(Delete(posB, lengthB - overlap))
  }

  // rebase edit onto all of edits, and all of edits onto edit
  def rebaseAll(edit: ContentEdit, edits: Seq[ContentEdit], client: Boolean = false): (List[ContentEdit], List[ContentEdit]) = {
    val rebasedOther = ListBuffer[ContentEdit]()
    val rebasedEdit = edits.foldLeft(List(edit)) {
      (as, b) =>
        var bs = List(b)  // tracks the other edit rebased onto `as`, which is `edit` as it rebases through `edits`.
        val rebasedAs = as.flatMap {
          a =>
            bs match {
              case Nil => List(a)
              case one :: Nil =>
                val rebased = rebase(a, one, client)
                bs = rebased._2
                rebased._1
              case more =>
                val rebased = rebaseAll(a, more, client)
                bs = rebased._1
                rebased._1
            }
        }
        rebasedOther ++= bs
        rebasedAs
    }
    rebasedEdit -> rebasedOther.toList
  }
}

/**
  * Represents a sequence of [[ContentEdit]]s. Each edit in the sequence must be based upon (or independent of) the
  * edits before it.
  */
final case class ContentEdits(edits: ShortList[ContentEdit]) extends AnyVal {
  def applyTo(rope: Rope): Rope = rope.withEdits(this)

  /**
    * Given another set of edits which would act upon the same content as this set of edits, produce a new set of edits
    * which would have the same effect as this set of edits but are based upon the given edits.
    */
  def rebase(other: ContentEdits, client: Boolean = false): ContentEdits = {
    // each edit in this set must be rebased to all the edits in the other set.
    // but we also have to track how the other edits are affected by each subsequent edit in this set, so that
    // the edits after it know how to rebase.
    val result = new ListBuffer[ContentEdit]
    val iter = edits.iterator.filter(_.nonEmpty)
    var otherEdits: List[ContentEdit] = other.edits

    while (iter.hasNext) {
      val edit = iter.next()
      val (rebasedEdit, rebasedOther) = ContentEdit.rebaseAll(edit, otherEdits, client)
      result ++= rebasedEdit
      otherEdits = rebasedOther
    }
    ContentEdits(ShortList(result.toList))
    //    ContentEdits(ShortList(edits.flatMap(_.rebase(other).edits)))
  }

  def rebaseBoth(other: ContentEdits, client: Boolean = false): (ContentEdits, List[ContentEdit]) = {
    // each edit in this set must be rebased to all the edits in the other set.
    // but we also have to track how the other edits are affected by each subsequent edit in this set, so that
    // the edits after it know how to rebase.
    val result = new ListBuffer[ContentEdit]
    val iter = edits.iterator.filter(_.nonEmpty)
    var otherEdits: List[ContentEdit] = other.edits

    while (iter.hasNext) {
      val edit = iter.next()
      val (rebasedEdit, rebasedOther) = ContentEdit.rebaseAll(edit, otherEdits, client)
      result ++= rebasedEdit
      otherEdits = rebasedOther
    }
    ContentEdits(ShortList(result.toList)) -> otherEdits
    //    ContentEdits(ShortList(edits.flatMap(_.rebase(other).edits)))
  }

  def rebase(other: ContentEdit): ContentEdits = rebase(ContentEdits(other))

  def size: Int = edits.size
}

object ContentEdits {
  def apply(edits: ContentEdit*): ContentEdits = ContentEdits(ShortList(edits.toList))
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy