Skip to content

Commit

Permalink
dropped f-bounded polymorphism, refactored to composition
Browse files Browse the repository at this point in the history
  • Loading branch information
lbialy authored and keynmol committed Nov 27, 2024
1 parent 2bb9801 commit a0baff3
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 107 deletions.
53 changes: 25 additions & 28 deletions modules/core/src/main/scala/InfiniscrollableState.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,35 @@

package cue4s

private[cue4s] trait InfiniscrollableState[A <: InfiniscrollableState[A]]:
self: A =>

def showing: Option[(List[Int], Int)]
def windowStart: Int
def windowSize: Int
private[cue4s] case class InfiniscrollableState(
showing: Option[(List[Int], Int)],
windowStart: Int,
windowSize: Int
):
def changeSelection(move: Int) =
showing match
case None => this // do nothing, no alternatives are showing
case a @ Some((filtered, showing)) =>
val position = filtered.indexOf(showing)

protected def changeSelection(move: Int): A
val newSelected =
(position + move).max(0).min(filtered.length - 1)

// implement as copy(windowStart = computeWindowStartAfterSearch)
// use it after any filtering operation
protected def resetWindow(): A
copy(showing = a.map(_ => (filtered, filtered(newSelected))))

protected def scrolledUpWindowStart: Int = (windowStart - 1).max(0)
def resetWindow() =
showing match
case None => this
case Some((filtered, selected)) =>
val position = filtered.indexOf(selected)
val newWindowStart =
(position - windowSize / 2).max(0).min(filtered.length - windowSize)

// implement as copy(windowStart = scrolledUpWindowStart)
protected def scrollUp: A
copy(windowStart = newWindowStart)

protected def scrolledDownWindowStart: Int = windowStart + 1
def scrollUp = copy(windowStart = (windowStart - 1).max(0))

// implement as copy(windowStart = scrolledDownWindowStart)
protected def scrollDown: A
def scrollDown = copy(windowStart = windowStart + 1)

def atTopScrollingPoint(position: Int) =
position > windowStart && position == windowStart + (windowSize / 2).min(2)
Expand All @@ -49,17 +56,7 @@ private[cue4s] trait InfiniscrollableState[A <: InfiniscrollableState[A]]:
def visibleEntries(filtered: List[Int]): List[Int] =
filtered.slice(windowStart, windowStart + windowSize)

protected def computeWindowStartAfterSearch: Int =
showing match
case None => 0
case Some((filtered, selected)) =>
val position = filtered.indexOf(selected)
val newWindowStart =
(position - windowSize / 2).max(0).min(filtered.length - windowSize)

newWindowStart

def up: A =
def up =
showing match
case None => this
case Some((filtered, selected)) =>
Expand All @@ -71,7 +68,7 @@ private[cue4s] trait InfiniscrollableState[A <: InfiniscrollableState[A]]:
end match
end up

def down: A =
def down =
showing match
case None => this
case Some((filtered, selected)) =>
Expand Down
63 changes: 24 additions & 39 deletions modules/core/src/main/scala/InteractiveMultipleChoice.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ private[cue4s] class InteractiveMultipleChoice(
override def initialState = State(
text = "",
selected = preSelected.toSet,
showing = Some(altsWithIndex.map(_._2) -> 0),
all = altsWithIndex,
status = Status.Running,
windowStart = 0,
windowSize = windowSize
display = InfiniscrollableState(
showing = Some(altsWithIndex.map(_._2) -> 0),
windowStart = 0,
windowSize = windowSize
)
)

private lazy val altMapping = altsWithIndex.map(_.swap).toMap
Expand All @@ -57,11 +59,12 @@ private[cue4s] class InteractiveMultipleChoice(
lines += prompt.lab + " > ".cyan + st.text
lines += "Tab".bold + " to toggle, " + "Enter".bold + " to submit."

st.showing match
st.display.showing match
case None =>
lines += "no matches...".bold
case Some((filtered, selected)) =>
st.visibleEntries(filtered)
st.display
.visibleEntries(filtered)
.zipWithIndex
.foreach:
case (id, idx) =>
Expand All @@ -72,9 +75,9 @@ private[cue4s] class InteractiveMultipleChoice(
else
lines.addOne(
if id == selected then s"$alt".green
else if st.windowStart > 0 && idx == 0 then
else if st.display.windowStart > 0 && idx == 0 then
s"$alt".bold
else if filtered.size > st.windowSize && idx == st.windowSize - 1 &&
else if filtered.size > st.display.windowSize && idx == st.display.windowSize - 1 &&
filtered.indexOf(id) != filtered.size - 1
then s"$alt".bold
else s" $alt"
Expand Down Expand Up @@ -109,12 +112,12 @@ private[cue4s] class InteractiveMultipleChoice(
Next.Continue

case Event.Key(KeyEvent.UP) =>
stateTransition(_.up)
stateTransition(_.updateDisplay(_.up))
printPrompt()
Next.Continue

case Event.Key(KeyEvent.DOWN) =>
stateTransition(_.down)
stateTransition(_.updateDisplay(_.down))
printPrompt()
Next.Continue

Expand Down Expand Up @@ -166,33 +169,26 @@ private[cue4s] object InteractiveMultipleChoice:
case class State(
text: String,
selected: Set[Int],
showing: Option[(List[Int], Int)],
all: List[(String, Int)],
status: Status,
windowStart: Int,
windowSize: Int
) extends InfiniscrollableState[State]:
display: InfiniscrollableState
):

def updateDisplay(f: InfiniscrollableState => InfiniscrollableState) =
copy(display = f(display))

def finish = copy(status = Status.Finished(selected))

def cancel = copy(status = Status.Canceled)

override protected def scrollUp =
copy(windowStart = (windowStart - 1).max(0))

override protected def scrollDown = copy(windowStart = windowStart + 1)

override protected def resetWindow() =
copy(windowStart = computeWindowStartAfterSearch)

def addText(t: Char) =
changeText(text + t).resetWindow()
changeText(text + t).updateDisplay(_.resetWindow())

def trimText =
changeText(text.dropRight(1)).resetWindow()
changeText(text.dropRight(1)).updateDisplay(_.resetWindow())

def toggle =
showing match
display.showing match
case None => this
case Some((_, cursor)) =>
if selected(cursor) then copy(selected = selected - cursor)
Expand All @@ -205,34 +201,23 @@ private[cue4s] object InteractiveMultipleChoice:
.contains(newText.toLowerCase().trim())
)
if newFiltered.nonEmpty then
showing match
display.showing match
case None =>
val newShowing = newFiltered.headOption.map: (_, id) =>
newFiltered.map(_._2) -> id

copy(text = newText, showing = newShowing)
updateDisplay(_.copy(showing = newShowing)).copy(text = newText)

case Some((_, selected)) =>
val newShowing = newFiltered.headOption.map: (_, id) =>
val newSelected =
newFiltered.find(_._2 == selected).map(_._2).getOrElse(id)
newFiltered.map(_._2) -> newSelected

copy(text = newText, showing = newShowing)
else copy(showing = None, text = newText)
updateDisplay(_.copy(showing = newShowing)).copy(text = newText)
else updateDisplay(_.copy(showing = None)).copy(text = newText)
end if
end changeText

override protected def changeSelection(move: Int) =
showing match
case None => this // do nothing, no alternatives are showing
case a @ Some((filtered, showing)) =>
val position = filtered.indexOf(showing)

val newSelected =
(position + move).max(0).min(filtered.length - 1)

copy(showing = a.map(_ => (filtered, filtered(newSelected))))

end State
end InteractiveMultipleChoice
65 changes: 25 additions & 40 deletions modules/core/src/main/scala/InteractiveSingleChoice.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ private[cue4s] class InteractiveSingleChoice(

override def initialState = State(
text = "",
showing = Some(altsWithIndex.map(_._2) -> 0),
all = altsWithIndex,
status = Status.Running,
windowStart = 0,
windowSize = windowSize
display = InfiniscrollableState(
showing = Some(altsWithIndex.map(_._2) -> 0),
windowStart = 0,
windowSize = windowSize
)
)

override def handleEvent(event: Event): Next[String] =
Expand All @@ -48,11 +50,11 @@ private[cue4s] class InteractiveSingleChoice(
printPrompt()
Next.Continue
case Event.Key(KeyEvent.UP) =>
stateTransition(_.up)
stateTransition(_.updateDisplay(_.up))
printPrompt()
Next.Continue
case Event.Key(KeyEvent.DOWN) =>
stateTransition(_.down)
stateTransition(_.updateDisplay(_.down))
printPrompt()
Next.Continue

Expand Down Expand Up @@ -98,20 +100,23 @@ private[cue4s] class InteractiveSingleChoice(
// prompt question
lines += "· " + (prompt.lab + " > ").cyan + st.text

st.showing match
st.display.showing match
case None =>
lines += "no matches...".bold
case Some((filtered, selected)) =>
// Render only the visible window
st.visibleEntries(filtered)
st.display
.visibleEntries(filtered)
.zipWithIndex
.foreach:
case (id, idx) =>
val alt = altMapping(id)
lines.addOne(
if id == selected then s"$alt".green
else if st.windowStart > 0 && idx == 0 then s"$alt".bold
else if filtered.size > st.windowSize && idx == st.windowSize - 1 &&
else if st.display.windowStart > 0 && idx == 0 then
s"$alt".bold
else if filtered.size > st.display.windowSize &&
idx == st.display.windowSize - 1 &&
filtered.indexOf(id) != filtered.size - 1
then s"$alt".bold
else s" $alt".bold
Expand Down Expand Up @@ -141,21 +146,15 @@ private[cue4s] object InteractiveSingleChoice:

case class State(
text: String,
showing: Option[(List[Int], Int)],
all: List[(String, Int)],
status: Status,
windowStart: Int,
windowSize: Int
) extends InfiniscrollableState[State]:

override protected def scrollUp =
copy(windowStart = scrolledUpWindowStart)

override protected def scrollDown =
copy(windowStart = scrolledDownWindowStart)
display: InfiniscrollableState
):
def updateDisplay(f: InfiniscrollableState => InfiniscrollableState) =
copy(display = f(display))

def finish =
showing match
display.showing match
case None => this
case Some((_, selected)) =>
copy(status = Status.Finished(selected))
Expand All @@ -164,24 +163,10 @@ private[cue4s] object InteractiveSingleChoice:

def addText(t: Char) =
val newText = text + t
changeText(newText).resetWindow()
changeText(newText).updateDisplay(_.resetWindow())

def trimText =
changeText(text.dropRight(1)).resetWindow()

override protected def resetWindow() =
copy(windowStart = computeWindowStartAfterSearch)

override protected def changeSelection(move: Int) =
showing match
case None => this // do nothing, no alternatives are showing
case a @ Some((filtered, showing)) =>
val position = filtered.indexOf(showing)

val newSelected =
(position + move).max(0).min(filtered.length - 1)

copy(showing = a.map(_ => (filtered, filtered(newSelected))))
changeText(text.dropRight(1)).updateDisplay(_.resetWindow())

private def changeText(newText: String) =
val newFiltered = all.filter((alt, _) =>
Expand All @@ -190,21 +175,21 @@ private[cue4s] object InteractiveSingleChoice:
.contains(newText.toLowerCase().trim())
)
if newFiltered.nonEmpty then
showing match
display.showing match
case None =>
val newShowing = newFiltered.headOption.map: (_, id) =>
newFiltered.map(_._2) -> id

copy(text = newText, showing = newShowing)
updateDisplay(_.copy(showing = newShowing)).copy(text = newText)

case Some((_, selected)) =>
val newShowing = newFiltered.headOption.map: (_, id) =>
val newSelected =
newFiltered.find(_._2 == selected).map(_._2).getOrElse(id)
newFiltered.map(_._2) -> newSelected

copy(text = newText, showing = newShowing)
else copy(showing = None, text = newText)
updateDisplay(_.copy(showing = newShowing)).copy(text = newText)
else updateDisplay(_.copy(showing = None)).copy(text = newText)
end if
end changeText
end State
Expand Down

0 comments on commit a0baff3

Please sign in to comment.