Skip to content

Further enhancements of scala3doc searchbar #11267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/scaladoc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
with:
java-version: 11

- name: Compile and test scala3doc-js
run: ./project/scripts/sbt scaladoc-js/test

- name: Compile and test
run: ./project/scripts/sbt scaladoc/test

Expand Down
6 changes: 4 additions & 2 deletions scaladoc-js/resources/scaladoc-searchbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@
}

#scaladoc-searchbar {
position: absolute;
position: fixed;
top: 50px;
right: 40px;
z-index: 5;
width: calc(100% - 360px);
box-shadow: 0 2px 16px 0 rgba(0, 42, 76, 0.15);
font-size: 13px;
Expand Down Expand Up @@ -71,6 +72,7 @@
}

.scaladoc-searchbar-result {
background: white;
line-height: 32px;
padding-left: 10px;
padding-right: 10px;
Expand All @@ -80,7 +82,7 @@
margin-top: 10px;
}

.scaladoc-searchbar-result:hover {
.scaladoc-searchbar-result[selected] {
background-color: #d4edff;
}

Expand Down
7 changes: 7 additions & 0 deletions scaladoc-js/src/Globals.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@ import scala.scalajs.js.annotation.JSGlobalScope
@JSGlobalScope
object Globals extends js.Object {
val pathToRoot: String = js.native
}

object StringUtils {
def createCamelCaseTokens(s: String): List[String] =
if s.isEmpty then List.empty
else if s.tail.indexWhere(_.isUpper) == -1 then List(s)
else List(s.take(s.tail.indexWhere(_.isUpper) + 1)) ++ createCamelCaseTokens(s.drop(s.tail.indexWhere(_.isUpper) + 1))
}
17 changes: 7 additions & 10 deletions scaladoc-js/src/searchbar/PageEntry.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,15 @@ case class PageEntry(
description: String,
location: String,
shortName: String,
acronym: Option[String]
tokens: List[String]
)

object PageEntry {
private def createAcronym(s: String): Option[String] =
s.headOption.map(firstLetter => firstLetter.toString ++ s.tail.filter(_.isUpper))

def apply(jsObj: PageEntryJS): PageEntry = PageEntry(
jsObj.t,
jsObj.d,
jsObj.l,
jsObj.n.toLowerCase,
createAcronym(jsObj.n)
)
jsObj.t,
jsObj.d,
jsObj.l,
jsObj.n.toLowerCase,
StringUtils.createCamelCaseTokens(jsObj.n)
)
}
58 changes: 57 additions & 1 deletion scaladoc-js/src/searchbar/SearchbarComponent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class SearchbarComponent(val callback: (String) => List[PageEntry]):

wrapper.appendChild(resultA)
wrapper.appendChild(location)
wrapper.addEventListener("mouseover", {
case e: MouseEvent => handleHover(wrapper)
})
wrapper

def handleNewQuery(query: String) =
Expand Down Expand Up @@ -52,7 +55,10 @@ class SearchbarComponent(val callback: (String) => List[PageEntry]):
if (document.body.contains(rootDiv)) {
document.body.removeChild(rootDiv)
}
else document.body.appendChild(rootDiv)
else {
document.body.appendChild(rootDiv)
input.focus()
}

val element = createNestingDiv("search-content")(
createNestingDiv("search-container")(
Expand All @@ -69,6 +75,7 @@ class SearchbarComponent(val callback: (String) => List[PageEntry]):
val element = document.createElement("input").asInstanceOf[html.Input]
element.id = "scaladoc-searchbar-input"
element.addEventListener("input", (e) => handleNewQuery(e.target.asInstanceOf[html.Input].value))
element.autocomplete = "off"
element

private val resultsDiv: html.Div =
Expand All @@ -94,9 +101,58 @@ class SearchbarComponent(val callback: (String) => List[PageEntry]):
document.body.removeChild(element)
}
)
element.addEventListener("keydown", {
case e: KeyboardEvent =>
if e.keyCode == 40 then handleArrowDown()
else if e.keyCode == 38 then handleArrowUp()
else if e.keyCode == 13 then handleEnter()
})
element.id = "scaladoc-searchbar"
element.appendChild(input)
element.appendChild(resultsDiv)
element

private def handleArrowUp() = {
val selectedElement = resultsDiv.querySelector("[selected]")
if selectedElement != null then {
selectedElement.removeAttribute("selected")
val sibling = selectedElement.previousElementSibling
if sibling != null then {
sibling.setAttribute("selected", "")
resultsDiv.scrollTop = sibling.asInstanceOf[html.Element].offsetTop - (2 * sibling.asInstanceOf[html.Element].clientHeight)
}
}
}
private def handleArrowDown() = {
val selectedElement = resultsDiv.querySelector("[selected]")
if selectedElement != null then {
val sibling = selectedElement.nextElementSibling
if sibling != null then {
selectedElement.removeAttribute("selected")
sibling.setAttribute("selected", "")
resultsDiv.scrollTop = sibling.asInstanceOf[html.Element].offsetTop - (2 * sibling.asInstanceOf[html.Element].clientHeight)
}
} else {
val firstResult = resultsDiv.firstElementChild
if firstResult != null then {
firstResult.setAttribute("selected", "")
resultsDiv.scrollTop = firstResult.asInstanceOf[html.Element].offsetTop - (2 * firstResult.asInstanceOf[html.Element].clientHeight)
}
}
}
private def handleEnter() = {
val selectedElement = resultsDiv.querySelector("[selected] a").asInstanceOf[html.Element]
if selectedElement != null then {
selectedElement.click()
}
}

private def handleHover(elem: html.Element) = {
val selectedElement = resultsDiv.querySelector("[selected]")
if selectedElement != null then {
selectedElement.removeAttribute("selected")
}
elem.setAttribute("selected","")
}

handleNewQuery("")
35 changes: 18 additions & 17 deletions scaladoc-js/src/searchbar/engine/Matchers.scala
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
package dotty.tools.scaladoc

enum Matchers extends Function1[PageEntry, Int]:
case ByName(query: String)
case ByKind(kind: String)
sealed trait Matchers extends Function1[PageEntry, Int]

def apply(p: PageEntry): Int = this match {
case ByName(query) => {
val nameOption = Option(p.shortName)
val acronym = p.acronym
//Edge case for empty query string
if query == "" then 1
else {
val results = List(
nameOption.filter(_.contains(query.toLowerCase)).fold(-1)(_.size - query.size),
acronym.filter(_.contains(query)).fold(-1)(_.size - query.size + 1)
)
if results.forall(_ == -1) then -1 else results.filter(_ != -1).min
}
case class ByName(query: String) extends Matchers:
val tokens = StringUtils.createCamelCaseTokens(query)
def apply(p: PageEntry): Int = {
val nameOption = Option(p.shortName.toLowerCase)
//Edge case for empty query string
if query == "" then 1
else {
val results = List(
nameOption.filter(_.contains(query.toLowerCase)).fold(-1)(_.size - query.size),
if p.tokens.size >= tokens.size && p.tokens.zip(tokens).forall( (token, query) => token.startsWith(query))
then p.tokens.size - tokens.size + 1
else -1
//acronym.filter(_.contains(query)).fold(-1)(_.size - query.size + 1)
)
if results.forall(_ == -1) then -1 else results.filter(_ != -1).min
}
case ByKind(kind) => p.fullName.split(" ").headOption.filter(_.equalsIgnoreCase(kind)).fold(-1)(_ => 1)
}

case class ByKind(kind: String) extends Matchers:
def apply(p: PageEntry): Int = p.fullName.split(" ").headOption.filter(_.equalsIgnoreCase(kind)).fold(-1)(_ => 1)
6 changes: 3 additions & 3 deletions scaladoc-js/src/searchbar/engine/QueryParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class QueryParser:
val escapedRegex = raw"`(.*)`".r

def parse(query: String): List[Matchers] = query match {
case escapedRegex(rest) => List(Matchers.ByName(rest))
case kindRegex(kind, rest) => List(Matchers.ByKind(kind)) ++ parse(rest)
case restRegex(name) => List(Matchers.ByName(name))
case escapedRegex(rest) => List(ByName(rest))
case kindRegex(kind, rest) => List(ByKind(kind)) ++ parse(rest)
case restRegex(name) => List(ByName(name))
case _ => List()
}
79 changes: 79 additions & 0 deletions scaladoc-js/test/dotty/dokka/MatchersTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package dotty.tools.scaladoc

import org.junit.{Test, Assert}
import org.junit.Assert._

class MatchersTest:
private val kinds = Seq(
"class",
"trait",
"enum",
"object",
"def",
"val",
"var",
"package",
"given",
"type"
)
private val names = Seq(
"NullPointerException",
"NPException",
"Seq",
"SeqOps",
"writeBytes",
"lessOrEqual",
"testFuzzySearch1",
"testF",
"testFS"
)
private val pages = for {
kind <- kinds
name <- names
} yield PageEntry(
s"$kind $name",
"",
"",
s"$name",
StringUtils.createCamelCaseTokens(name)
)

private def result(matchers: List[Matchers]) = {
pages.map { p =>
p -> matchers.map(_(p))
}.filterNot { (page, results) =>
results.exists(_ == -1)
}.map((page, results) => page)
}

@Test
def testByKind = kinds.foreach { kind =>
val res = result(List(ByKind(kind)))
val expected = pages.filter(p => p.fullName.startsWith(kind)).toSet
assertEquals(
s"Matchers test error: for kind: $kind should match $expected but matched $res",
expected,
res.toSet
)
}

private def byNameTestCase(query: String, expectedMatch: String*) = expectedMatch.foreach { expMatch =>
assertTrue(
s"Matchers test error: for query: $query expected $expMatch",
result(List(ByName(query))).exists(p => p.shortName.contains(expMatch))
)
}

@Test
def testByName = {
names.foreach(n => byNameTestCase(n, n))
byNameTestCase("NPE", "NPException", "NullPointerException")
byNameTestCase("NullPE", "NullPointerException")
byNameTestCase("tFuzzS", "testFuzzySearch1")
byNameTestCase("SO", "SeqOps")
byNameTestCase("teFS", "testFS")
byNameTestCase("writeBy", "writeBytes")
byNameTestCase("seQ", "Seq")
byNameTestCase("lOrEqu", "lessOrEqual")
byNameTestCase("teF", "testFS", "testF")
}
35 changes: 35 additions & 0 deletions scaladoc-js/test/dotty/dokka/QueryParserTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dotty.tools.scaladoc

import org.junit.{Test, Assert}
import org.junit.Assert._

class QueryParserTest:
val queryParser = QueryParser()
val kinds = Seq(
"class",
"trait",
"enum",
"object",
"def",
"val",
"var",
"package",
"given",
"type"
)
private def testCase(query: String, result: List[Matchers]) = {
val parsed = queryParser.parse(query)
assertEquals(
s"Query parser test error: for query: $query expected $result but found $parsed",
parsed,
result
)
}

@Test
def queryParserTests() = {
kinds.foreach(k => testCase(s"$k ", List(ByKind(k), ByName(""))))
testCase("trait", List(ByName("trait")))
testCase("trait A", List(ByKind("trait"), ByName("A")))
testCase("`trait A`", List(ByName("trait A")))
}