Skip to content

Commit de18cd0

Browse files
committed
Scaladoc Search Engine fixes
1 parent 3efe571 commit de18cd0

File tree

7 files changed

+102
-49
lines changed

7 files changed

+102
-49
lines changed

scaladoc-js/main/src/searchbar/Searchbar.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package dotty.tools.scaladoc
33
class Searchbar {
44
val pages = SearchbarGlobals.pages.toList.map(PageEntry.apply)
55
val parser = QueryParser()
6-
val searchEngine = SearchbarEngine(pages)
6+
val searchEngine = PageSearchEngine(pages)
77
val inkuireEngine = InkuireJSSearchEngine()
88
val component = SearchbarComponent(searchEngine, inkuireEngine, parser)
99
}

scaladoc-js/main/src/searchbar/SearchbarComponent.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import scala.util.chaining._
1313

1414
import java.net.URI
1515

16-
class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearchEngine, parser: QueryParser):
16+
class SearchbarComponent(engine: PageSearchEngine, inkuireEngine: InkuireJSSearchEngine, parser: QueryParser):
1717
val initialChunkSize = 5
1818
val resultsChunkSize = 20
1919
extension (p: PageEntry)
@@ -171,9 +171,9 @@ class SearchbarComponent(engine: SearchbarEngine, inkuireEngine: InkuireJSSearch
171171
clearResults()
172172
handleRecentQueries(query)
173173
parser.parse(query) match {
174-
case EngineMatchersQuery(matchers) =>
174+
case NameAndKindQuery(matchers) =>
175175
handleNewFluffQuery(matchers)
176-
case BySignature(signature) =>
176+
case SignatureQuery(signature) =>
177177
val loading = createLoadingAnimation
178178
val kindSeparator = createKindSeparator("inkuire")
179179
resultsDiv.appendChild(loading)
Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,24 @@
11
package dotty.tools.scaladoc
22

3+
case class MatchResult(score: Int, pageEntry: PageEntry, indices: Set[Int])
4+
35
sealed trait EngineQuery
4-
case class EngineMatchersQuery(matchers: List[Matchers]) extends EngineQuery
5-
case class BySignature(signature: String) extends EngineQuery
6+
case class NameAndKindQuery(name: Option[String], kind: Option[String]) extends EngineQuery:
7+
8+
case class SignatureQuery(signature: String) extends EngineQuery
69

710
case class Match(priority: Int, matchedIndexes: Set[Int]) // matchedIndexes - indexes of chars that got matched
811

9-
sealed trait Matchers extends Function1[PageEntry, Match]
12+
sealed trait Matchers:
13+
def matchEntry(p: PageEntry): Match
1014

11-
case class ByName(query: String) extends Matchers:
12-
val tokens = StringUtils.createCamelCaseTokens(query)
13-
def apply(p: PageEntry): Match = {
14-
val nameOption = Option(p.shortName.toLowerCase)
15-
//Edge case for empty query string
16-
if query == "" then Match(1, Set.empty)
17-
else {
18-
val (result, indexes) = p.shortName.toLowerCase.zipWithIndex.foldLeft((query.toLowerCase, Set.empty[Int])) {
19-
case ((pattern, indexes), (nextChar, index)) =>
20-
if !pattern.isEmpty then {
21-
if pattern.head.toString.equalsIgnoreCase(nextChar.toString) then (pattern.tail, indexes + index) else (pattern, indexes)
22-
} else ("", indexes)
23-
}
24-
if result.isEmpty then Match(p.shortName.size - query.size + 1, indexes) else Match(-1, Set.empty)
25-
}
26-
}
15+
/**
16+
* Searches matches PageEntry by name. It implements the following logic:
17+
* 1. When the query is empty, return successful match regardless of PageEntry.
18+
* 2.
19+
* @param query String to match in page names
20+
*/
21+
case class ByName(query: String) extends Matchers
2722

2823
case class ByKind(kind: String) extends Matchers:
2924
def apply(p: PageEntry): Match = Match(if p.kind.equalsIgnoreCase(kind) then 1 else -1, Set.empty)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package dotty.tools.scaladoc
2+
3+
import math.Ordering.Implicits.seqOrdering
4+
import org.scalajs.dom.Node
5+
6+
import scala.annotation.tailrec
7+
8+
class PageSearchEngine(pages: List[PageEntry]):
9+
def query(query: NameAndKindQuery): List[MatchResult] =
10+
matchPages(query)
11+
.filter {
12+
case MatchResult(score, _, _) => score >= 0
13+
}
14+
.sortBy {
15+
case MatchResult(score, _, _) => score
16+
}
17+
18+
private def matchPages(query: NameAndKindQuery): List[MatchResult] = query match {
19+
case NameAndKindQuery(None, None) => List.empty
20+
case NameAndKindQuery(None, Some(kind)) =>
21+
filterKind(pages, kind)
22+
.map(MatchResult(1, _, Set.empty))
23+
case NameAndKindQuery(Some(""), kind) =>
24+
kind.fold(pages)(filterKind(pages, _))
25+
case NameAndKindQuery(Some(nameSearch), kind) =>
26+
val kindFiltered = kind.fold(pages)(filterKind(pages, _))
27+
val prematchedPages = prematchPages(kindFiltered, nameSearch)
28+
prematchedPages.map(matchPage(_, nameSearch))
29+
}
30+
31+
private def matchPage(prematched: MatchResult, nameSearch: String): MatchResult =
32+
val searchTokens = StringUtils.createCamelCaseTokens(nameSearch)
33+
val pageTokens = page.tokens
34+
35+
@tailrec
36+
def findHighScoreMatch(searchTokens: List[String], pageTokens: List[String], scoreAcc: Int, indicesAcc: Set[Int]): (Int, Set[Int]) =
37+
val searchTokensLifted = searchTokens.lift
38+
val pageTokensLifted = pageTokens.lift
39+
40+
// Instead znajdź wszystkie poprawne podciągi match ujących tokenów OR leave it?
41+
@tailrec
42+
def matchTokens(searchTokenIndex: Int, pageTokenIndex: Int, acc: Set[(Int, Int)]): Set[(Int, Int)] = (searchTokensLifted(searchTokenIndex), pageTokensLifted(pageTokenIndex)) match
43+
case (None, _) || (_, None) => acc
44+
case (Some(searchHead :: _), Some(pageHead :: _)) =>
45+
if searchHead.head == pageHead.head then
46+
matchTokens(searchTokenIndex + 1, pageTokenIndex + 1, acc + (searchTokenIndex, pageTokenIndex))
47+
else
48+
matchTokens(searchTokenIndex + 1, pageTokenIndex + 1, acc)
49+
end matchTokens
50+
51+
val matchedTokens = matchTokens(0, 0, Set.empty)
52+
53+
54+
def findHighScoreMatch(searchTokenIndex: Int, pageTokenIndex: Int, positionAcc: Set[Int], scoreAcc: Int) =
55+
56+
57+
58+
private def filterKind(pages: List[PageEntry], kind: String): List[PageEntry] =
59+
pages.filter(_.kind == kind)
60+
61+
def prematchPages(pages: List[PageEntry], search: String): List[PageEntry]
62+
pages.map(page => MatchResult(1, page, getIndicesOfSearchLetters(page.shortName, search)))
63+
.filter(_.indices.nonEmpty)
64+
65+
private def getIndicesOfSearchLetters(pageName: String, search: String): Set[Int] =
66+
@tailrec
67+
def getIndicesAcc(nameIndex: Int, searchIndex: Int, acc: Set[Int]): Set[Int] =
68+
if nameIndex >= pageName.length then
69+
Set.empty
70+
else if searchIndex >= search.length then
71+
acc
72+
else if pageName(nameIndex) == search(searchIndex) then
73+
containsAllAcc(nameIndex + 1, searchIndex + 1, acc + nameIndex)
74+
else
75+
containsAllAcc(nameIndex + 1, searchIndex, acc)
76+
getIndicesAcc(0, 0)
77+
78+
}

scaladoc-js/main/src/searchbar/engine/QueryParser.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ class QueryParser:
2929
}
3030

3131
def parse(query: String): EngineQuery = query match {
32-
case signatureRegex(signature) => BySignature(signature)
33-
case other => EngineMatchersQuery(parseMatchers(other))
32+
case signatureRegex(signature) => SignatureQuery(signature)
33+
case other => NameAndKindQuery(parseMatchers(other))
3434
}

scaladoc-js/main/src/searchbar/engine/SearchbarEngine.scala

Lines changed: 0 additions & 20 deletions
This file was deleted.

scaladoc-js/main/test/dotty/tools/scaladoc/QueryParserTest.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ class QueryParserTest:
2828

2929
@Test
3030
def queryParserTests() = {
31-
kinds.foreach(k => testCase(s"$k ", EngineMatchersQuery(List(ByKind(k), ByName("")))))
32-
testCase("trait", EngineMatchersQuery(List(ByName("trait"))))
33-
testCase("trait A", EngineMatchersQuery(List(ByKind("trait"), ByName("A"))))
34-
testCase("`trait A`", EngineMatchersQuery(List(ByName("trait A"))))
31+
kinds.foreach(k => testCase(s"$k ", NameAndKindQuery(List(ByKind(k), ByName("")))))
32+
testCase("trait", NameAndKindQuery(List(ByName("trait"))))
33+
testCase("trait A", NameAndKindQuery(List(ByKind("trait"), ByName("A"))))
34+
testCase("`trait A`", NameAndKindQuery(List(ByName("trait A"))))
3535
}

0 commit comments

Comments
 (0)