Skip to content

Commit

Permalink
Refactor autocompletion and its interface
Browse files Browse the repository at this point in the history
  • Loading branch information
voidcontext committed Sep 2, 2020
1 parent d5b9a9a commit 2ce0340
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 186 deletions.
12 changes: 4 additions & 8 deletions console4s/src/main/scala/com/gaborpihaj/console4s/Console.scala
Expand Up @@ -4,16 +4,14 @@ import cats.Show
import cats.effect.Sync
import cats.kernel.Eq
import cats.syntax.apply._
import com.gaborpihaj.console4s.{AutoCompletionConfig, AutoCompletionSource}
import com.gaborpihaj.console4s.AutoCompletion

trait Console[F[_]] {
def putStr(text: String): F[Unit]
def putStrLn(): F[Unit]
def putStrLn(text: String): F[Unit]
def readLine(prompt: String): F[String]
def readLine[Repr: Show: Eq](prompt: String, autocomplete: AutoCompletionSource[Repr])(
implicit cfg: AutoCompletionConfig[Repr]
): F[(String, Option[Repr])]
def readLine[Repr: Show: Eq](prompt: String, autocompletion: AutoCompletion[Repr]): F[(String, Option[Repr])]
def readInt(prompt: String): F[Int]
def readBool(prompt: String): F[Boolean]

Expand All @@ -36,10 +34,8 @@ object Console {
def readLine(prompt: String): F[String] =
lineReader.readLine(prompt)

def readLine[Repr: Show: Eq](prompt: String, autocomplete: AutoCompletionSource[Repr])(
implicit cfg: AutoCompletionConfig[Repr]
): F[(String, Option[Repr])] =
lineReader.readLine(prompt, autocomplete)
def readLine[Repr: Show: Eq](prompt: String, autocompletion: AutoCompletion[Repr]): F[(String, Option[Repr])] =
lineReader.readLine(prompt, autocompletion)

def readInt(prompt: String): F[Int] =
lineReader.readInt(prompt)
Expand Down
Expand Up @@ -8,9 +8,7 @@ trait InputReader

trait LineReader[F[_]] {
def readLine(prompt: String): F[String]
def readLine[Repr: Show: Eq](prompt: String, autocomplete: AutoCompletionSource[Repr])(
implicit cfg: AutoCompletionConfig[Repr]
): F[(String, Option[Repr])]
def readLine[Repr: Show: Eq](prompt: String, autocompletion: AutoCompletion[Repr]): F[(String, Option[Repr])]
def readInt(prompt: String): F[Int]
def readBool(prompt: String): F[Boolean]
}
Expand Down
@@ -1,20 +1,25 @@
package com.gaborpihaj.console4s

import cats.Eq
import com.gaborpihaj.console4s.AutoCompletionConfig.Direction
import com.gaborpihaj.console4s.AutoCompletion._

trait AutoCompletionSource[Repr] {
def candidates(fragment: String): List[(String, Repr)]
}

case class AutoCompletionConfig[Repr](
maxCandidates: Int,
strict: Boolean,
direction: Direction,
onResultChange: (Option[Repr], String => Unit) => Unit
case class AutoCompletion[Repr](
source: AutoCompletionSource[Repr],
config: AutoCompletionConfig[Repr]
)

object AutoCompletionConfig {
object AutoCompletion {
trait AutoCompletionSource[Repr] {
def candidates(fragment: String): List[(String, Repr)]
}

case class AutoCompletionConfig[Repr](
maxCandidates: Int,
strict: Boolean,
direction: Direction,
onResultChange: (Option[Repr], String => Unit) => Unit
)

sealed trait Direction
case object Up extends Direction
case object Down extends Direction
Expand All @@ -23,11 +28,12 @@ object AutoCompletionConfig {
implicit val eq: Eq[Direction] = Eq.fromUniversalEquals
}

implicit def defaultAutoCompletionConfig[Repr]: AutoCompletionConfig[Repr] =
def defaultAutoCompletionConfig[Repr]: AutoCompletionConfig[Repr] =
AutoCompletionConfig(
maxCandidates = 5,
strict = false,
direction = Up,
onResultChange = (_, _) => ()
)

}

This file was deleted.

@@ -0,0 +1,78 @@
package com.gaborpihaj.console4s.linereader

import cats.Show
import cats.instances.int._
import cats.instances.list._
import cats.instances.string._
import cats.instances.unit._
import cats.kernel.Eq
import cats.syntax.eq._
import cats.syntax.foldable._
import cats.syntax.show._
import com.gaborpihaj.console4s.AutoCompletion.{AutoCompletionConfig, Up}
import com.gaborpihaj.console4s.TerminalControl._
import com.gaborpihaj.console4s.linereader.LineReaderState.StateUpdate

private[linereader] object AutoCompletionHelper {

def updateCompletions[Repr: Show: Eq]: StateUpdate[Repr, String] =
StateUpdate.lift { (env, state) =>
env.autocompletion.fold(StateUpdate.pure[Repr, String]("")) { ac =>
val completions = ac.source.candidates(state.input).take(ac.config.maxCandidates)

for {
_ <- updateSelectedCompletion(completions)
out1 <- clearCompletionLines()
out2 <- printCompletionCandidates(completions)
} yield savePos() + out1 + out2 + restorePos()
}
}

private[this] def updateSelectedCompletion[Repr: Eq](candidates: List[(String, Repr)]): StateUpdate[Repr, Unit] =
StateUpdate.modify((_, state) =>
state.copy(
selectedCompletion = state.selectedCompletion
.flatMap(selected =>
candidates.zipWithIndex.find {
case ((str, repr), _) => str === selected._2 && repr === selected._3
}
)
.fold(candidates.headOption.map { case (str, repr) => (0, str, repr) }) {
case ((str, repr), index) => Option((index, str, repr))
}
)
)

private[this] def printCompletionCandidates[Repr: Show](
completions: List[(String, Repr)]
): StateUpdate[Repr, String] =
StateUpdate.inspect { (env, state) =>
env.autocompletion.fold("") { ac =>
completions.zipWithIndex
.foldLeft("") {
case (o, ((_, candidate), index)) =>
o + move(completionRow(ac.config, env.currentRow, index, completions.length), env.prompt.length() + 1) +
(
if (state.selectedCompletion.filter(_._1 === index).isDefined) bold() + candidate.show + sgrReset()
else candidate.show
)
}
}
}

private[this] def completionRow[Repr](
config: AutoCompletionConfig[Repr],
inputRow: Int,
index: Int,
completions: Int
) =
(if (config.direction === Up) inputRow - completions else inputRow + 1) + index

private[this] def clearCompletionLines[Repr](): StateUpdate[Repr, String] =
StateUpdate.ask.map { env =>
env.autocompletion.fold("") { ac =>
(1 to ac.config.maxCandidates).toList
.foldMap(i => move(env.currentRow + (if (ac.config.direction === Up) -i else i), 1) + clearLine())
}
}
}
Expand Up @@ -23,9 +23,9 @@ object LineReaderImpl {

def readLine[Repr: Show: Eq](
prompt: String,
autocomplete: AutoCompletionSource[Repr]
)(implicit cfg: AutoCompletionConfig[Repr]): F[(String, Option[Repr])] =
readLine(prompt, Option(cfg -> autocomplete), noFilter)
autocompletion: AutoCompletion[Repr]
): F[(String, Option[Repr])] =
readLine(prompt, Option(autocompletion), noFilter)

def readInt(prompt: String): F[Int] = readLine[String](prompt, None, intFilter).map(_._1.toInt)

Expand Down Expand Up @@ -85,14 +85,14 @@ object LineReaderImpl {

private def readLine[Repr: Show: Eq](
prompt: String,
autocomplete: Option[(AutoCompletionConfig[Repr], AutoCompletionSource[Repr])],
autocompletion: Option[AutoCompletion[Repr]],
filter: Chain[Int] => Boolean,
readWhile: Chain[Int] => Boolean = _ =!= Chain(13)
): F[(String, Option[Repr])] =
Sync[F].delay(write(prompt)) >>
readInput(
LineReaderState.empty,
Env(terminal.getCursorPosition()._1, prompt, filter, readWhile, autocomplete)
Env(terminal.getCursorPosition()._1, prompt, filter, readWhile, autocompletion)
)

private def readInput[Repr: Show: Eq](
Expand All @@ -111,16 +111,16 @@ object LineReaderImpl {
.foldLeft(state) { (state, byteSeq) =>
(for {
out1 <- handleKeypress[Repr](byteSeq)
out2 <- AutoCompletion.updateCompletions[Repr]
out2 <- AutoCompletionHelper.updateCompletions[Repr]
_ <- StateUpdate.now(write(out1 + out2))
} yield ())
.runS(env, state)
.value
}
)
.flatMap { state =>
env.autocomplete.fold(Sync[F].pure(state.result)) { ac =>
if (ac._1.strict && state.result._2.isEmpty) readInput(state, env)
env.autocompletion.fold(Sync[F].pure(state.result)) { ac =>
if (ac.config.strict && state.result._2.isEmpty) readInput(state, env)
else Sync[F].pure(state.result)
}
}
Expand Down Expand Up @@ -186,9 +186,7 @@ object LineReaderImpl {
column = state.selectedCompletion.fold(state.column)(_._2.length()),
completionResult = newState.selectedCompletion.map(_._3)
)
env.autocomplete.foreach {
case (config, _) => config.onResultChange(s.completionResult, write)
}
env.autocompletion.foreach(_.config.onResultChange(s.completionResult, write))
s -> (move(env.currentRow, readerStart) + clearLine() + s.input)

case Chain(27, 91, 65) => // Up
Expand All @@ -209,14 +207,13 @@ object LineReaderImpl {
): Option[(Int, String, Repr)] =
state.selectedCompletion.flatMap {
case old @ (index, _, _) =>
env.autocomplete.map {
case (config, source) =>
source
.candidates(state.input)
.take(config.maxCandidates)
.zipWithIndex
.map { case ((input, repr), i) => (i, input, repr) }
.applyOrElse(index + offset, (_: Int) => old)
env.autocompletion.map { ac =>
ac.source
.candidates(state.input)
.take(ac.config.maxCandidates)
.zipWithIndex
.map { case ((input, repr), i) => (i, input, repr) }
.applyOrElse(index + offset, (_: Int) => old)
}
}
}
Expand Up @@ -6,7 +6,7 @@ import cats.instances.option._
import cats.instances.unit._
import cats.syntax.apply._
import cats.syntax.flatMap._
import com.gaborpihaj.console4s.{AutoCompletionConfig, AutoCompletionSource}
import com.gaborpihaj.console4s.AutoCompletion

private[linereader] case class LineReaderState[Repr](
keys: Chain[ByteSeq],
Expand Down Expand Up @@ -51,7 +51,7 @@ private[linereader] object LineReaderState {
prompt: String,
filter: Chain[Int] => Boolean,
readWhile: Chain[Int] => Boolean,
autocomplete: Option[(AutoCompletionConfig[Repr], AutoCompletionSource[Repr])]
autocompletion: Option[AutoCompletion[Repr]]
)

def empty[Repr]: LineReaderState[Repr] = apply(Chain.empty, 0, "", None, None)
Expand All @@ -61,9 +61,9 @@ private[linereader] object LineReaderState {
def prependKeys(keys: ByteSeq): LineReaderState[Repr] = state.copy(keys = Chain.one(keys) ++ state.keys)
def withInput(input: String, env: Env[Repr], write: String => Unit): LineReaderState[Repr] = {
(
env.autocomplete,
env.autocompletion,
state.completionResult
).mapN { case ((config, _), _) => config.onResultChange(None, write) }
).mapN { case (ac, _) => ac.config.onResultChange(None, write) }
state.copy(input = input, completionResult = None)
}
def selectCompletion(index: Int, input: String, selected: Repr): LineReaderState[Repr] =
Expand Down

0 comments on commit 2ce0340

Please sign in to comment.