src/Tools/jEdit/src/isabelle_navigator.scala
author wenzelm
Fri, 04 Apr 2025 10:58:39 +0200
changeset 82424 7fe17f5d1524
parent 82423 bcbbee58e7e9
child 82425 20f79e12ad20
permissions -rw-r--r--
tuned signature;

/*  Title:      Tools/jEdit/src/isabelle_navigator.scala
    Author:     Makarius

Navigate history of notable source positions.
*/

package isabelle.jedit


import org.gjt.sp.jedit.{View, Buffer, EditPane}


import isabelle._

object Isabelle_Navigator {
  object Pos {
    val none: Pos = new Pos(Document_ID.none, "", 0)
    def make(name: String, offset: Int): Pos = new Pos(Document_ID.make(), name, offset)

    def apply(buffer: Buffer): Pos =
      if (buffer == null) Pos.none else make(JEdit_Lib.buffer_name(buffer), 0)

    def apply(edit_pane: EditPane): Pos =
      if (edit_pane == null) none
      else {
        val buffer = edit_pane.getBuffer
        if (buffer != null && buffer.isLoaded && !buffer.isUntitled) {
          make(JEdit_Lib.buffer_name(buffer), edit_pane.getTextArea.getCaretPosition)
        }
        else none
      }

    def apply(view: View): Pos =
      if (view == null) none else apply(view.getEditPane)
  }

  final class Pos private(
    val id: Document_ID.Generic,
    val name: String,
    val offset: Int
  ) {
    def defined: Boolean = id != Document_ID.none

    override def toString: String =
      if (defined) "(offset " + offset + " of " + quote(name) + ")" else "Pos.none"

    override def hashCode: Int = id.hashCode
    override def equals(other: Any): Boolean =
      other match {
        case that: Pos => id == that.id
        case _ => false
      }

    def equiv(that: Pos): Boolean = name == that.name && offset == that.offset

    def convert(edit_name: String, edit: Text.Edit): Pos = {
      if (name == edit_name) {
        val offset1 = edit.convert(offset)
        if (offset == offset1) this else Pos.make(name, offset1)
      }
      else this
    }
  }

  object History {
    val limit: Int = 500
    val empty: History = new History(Linear_Set.empty)
  }

  class History(hist: Linear_Set[Pos]) {
    override def toString: String =
      size match {
        case 0 => "History.empty"
        case n => "History(" + n + ")"
      }
    def is_empty: Boolean = hist.isEmpty
    def size: Int = hist.size
    def iterator: Iterator[Pos] = hist.iterator

    def top: Pos = if (hist.isEmpty) Pos.none else hist.head
    def pop: History = if (hist.isEmpty) this else new History(hist.delete_after(None))

    def push(pos: Pos): History =
      if (!pos.defined || pos.equiv(top)) this
      else {
        val hist1 =
          if (hist.size < History.limit) hist
          else hist.delete_after(hist.prev(hist.last))
        new History(hist1.insert_after(None, pos))
      }

    def convert(name: String, edit: Text.Edit): History =
      new History(
        hist.foldLeft(hist) {
          case (h, pos) =>
            val prev = h.prev(pos)
            val pos0 = prev.getOrElse(Pos.none)
            val pos1 = pos.convert(name, edit)
            if (pos1.equiv(pos0)) h.delete_after(prev)
            else if (pos1.equiv(pos)) h
            else h.delete_after(prev).insert_after(prev, pos1)
        }
      )

    def close(names: Set[String]): History =
      new History(
        hist.foldLeft(hist) {
          case (h, pos) =>
            val prev = h.prev(pos)
            val pos0 = prev.getOrElse(Pos.none)
            if (names.contains(pos.name) || pos.equiv(pos0)) h.delete_after(prev)
            else h
        }
      )
  }

  final class State private[Isabelle_Navigator](view: View) {
  }
}

class Isabelle_Navigator {
  import Isabelle_Navigator.{Pos, History}

  // owned by GUI thread
  private var _bypass = false
  var _current: Pos = Pos.none
  var _backward = History.empty
  var _forward = History.empty

  override def toString: String = {
    val size = (if (_current.defined) 1 else 0) + _backward.size + _forward.size
    "Isabelle_Navigator(" + (if (size == 0) "" else size.toString) + ")"
  }

  private def convert(name: String, edit: Text.Edit): Unit = GUI_Thread.require {
    _current = _current.convert(name, edit)
    _backward = _backward.convert(name, edit)
    _forward = _forward.convert(name, edit)
  }

  private def close(names: Set[String]): Unit = GUI_Thread.require {
    if (names.contains(_current.name)) _current = Pos.none
    _backward = _backward.close(names)
    _forward = _forward.close(names)
  }

  private val buffer_listener =
    JEdit_Lib.buffer_listener((buffer, edit) => convert(JEdit_Lib.buffer_name(buffer), edit))

  def exit(buffers: IterableOnce[Buffer]): Unit = GUI_Thread.require {
    buffers.iterator.foreach(_.removeBufferListener(buffer_listener))
    close(buffers.iterator.map(JEdit_Lib.buffer_name).toSet)
  }

  def init(buffers: IterableOnce[Buffer]): Unit = GUI_Thread.require {
    exit(buffers)
    buffers.iterator.foreach(_.addBufferListener(buffer_listener))
  }

  def record(pos: Pos): Unit = GUI_Thread.require {
    if (!_bypass && pos.defined && !pos.equiv(_current)) {
      _backward = _backward.push(_current)
      _forward = History.empty
      _current = pos
    }
  }

  def record(buffer: Buffer): Unit = record(Pos(buffer))
  def record(edit_pane: EditPane): Unit = record(Pos(edit_pane))
  def record(view: View): Unit = record(Pos(view))

  def goto_current(view: View): Unit = GUI_Thread.require {
    if (_current.defined) {
      val b = _bypass
      try {
        _bypass = true
        PIDE.editor.goto_file(true, view, _current.name, offset = _current.offset)
      }
      finally { _bypass = b }
    }
  }

  def forward(view: View): Unit = GUI_Thread.require {
    if (!_forward.is_empty) {
      _backward = _backward.push(_current)
      _current = _forward.top
      _forward = _forward.pop
      goto_current(view)
    }
  }

  def backward(view: View): Unit = GUI_Thread.require {
    if (!_backward.is_empty) {
      val pos0 = _current
      val pos1 = Pos(view)

      def move(pos: Pos): Unit = {
        _forward = _forward.push(pos)
        _current = _backward.top
        _backward = _backward.pop
      }

      if (pos0.defined) move(pos0)
      if (pos1.defined && !pos1.equiv(pos0)) move(pos1)

      goto_current(view)
    }
  }
}