Fix the Suggestions Database Updates Handling (#1161)

Misc updates to the Suggestions database updates handling
algorithm
This commit is contained in:
Dmitry Bushev 2020-10-05 17:22:13 +03:00 committed by GitHub
parent 8e07e0347f
commit 3d65ffd3cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 439 additions and 415 deletions

View File

@ -90,11 +90,7 @@ final class SuggestionsHandler(
context.system.eventStream
.subscribe(self, classOf[Api.ExpressionValuesComputed])
context.system.eventStream
.subscribe(self, classOf[Api.SuggestionsDatabaseUpdateNotification])
context.system.eventStream
.subscribe(self, classOf[Api.SuggestionsDatabaseReIndexNotification])
context.system.eventStream
.subscribe(self, classOf[Api.SuggestionsDatabaseIndexUpdateNotification])
.subscribe(self, classOf[Api.SuggestionsDatabaseModuleUpdateNotification])
context.system.eventStream.subscribe(self, classOf[ProjectNameChangedEvent])
context.system.eventStream.subscribe(self, classOf[FileDeletedEvent])
context.system.eventStream
@ -143,24 +139,7 @@ final class SuggestionsHandler(
sender() ! CapabilityReleased
context.become(initialized(projectName, clients - client.clientId))
case msg: Api.SuggestionsDatabaseIndexUpdateNotification =>
applyIndexedModuleUpdates(msg.updates.toSeq)
.onComplete {
case Success(notification) =>
if (notification.updates.nonEmpty) {
clients.foreach { clientId =>
sessionRouter ! DeliverToJsonController(clientId, notification)
}
}
case Failure(ex) =>
log.error(
ex,
"Error applying suggestion database updates: {}",
msg.updates.map(_.file)
)
}
case msg: Api.SuggestionsDatabaseUpdateNotification =>
case msg: Api.SuggestionsDatabaseModuleUpdateNotification =>
applyDatabaseUpdates(msg)
.onComplete {
case Success(notification) =>
@ -173,25 +152,7 @@ final class SuggestionsHandler(
log.error(
ex,
"Error applying suggestion database updates: {}",
msg.updates
)
}
case msg: Api.SuggestionsDatabaseReIndexNotification =>
log.debug(s"ReIndex ${msg.moduleName} ${msg.updates.map(_.suggestion)}")
applyReIndexUpdates(msg.updates)
.onComplete {
case Success(notification) =>
if (notification.updates.nonEmpty) {
clients.foreach { clientId =>
sessionRouter ! DeliverToJsonController(clientId, notification)
}
}
case Failure(ex) =>
log.error(
ex,
"Error applying suggestion re-index updates: {}",
msg.updates
msg.file
)
}
@ -211,6 +172,20 @@ final class SuggestionsHandler(
}
SuggestionsDatabaseUpdateNotification(version, updates)
}
.onComplete {
case Success(notification) =>
if (notification.updates.nonEmpty) {
clients.foreach { clientId =>
sessionRouter ! DeliverToJsonController(clientId, notification)
}
}
case Failure(ex) =>
log.error(
ex,
"Error applying changes from computed values: {}",
updates
)
}
case GetSuggestionsDatabaseVersion =>
suggestionsRepo.currentVersion
@ -314,68 +289,6 @@ final class SuggestionsHandler(
}
}
private def applyIndexedModuleUpdates(
updates: Seq[Api.IndexedModule]
): Future[SuggestionsDatabaseUpdateNotification] = {
def createIndexedModuleUpdatesBatch(
contents: String,
file: java.io.File,
updates: Seq[Api.SuggestionsDatabaseUpdate.Add]
): Future[Seq[Api.SuggestionsDatabaseUpdate.Add]] =
fileVersionsRepo
.updateVersion(file, versionCalculator.evalDigest(contents))
.map(versionChanged => if (versionChanged) updates else Seq())
def getBatches =
Future
.traverse(updates) { indexed =>
createIndexedModuleUpdatesBatch(
indexed.contents,
indexed.file,
indexed.updates
)
}
.map(_.flatten)
for {
batch <- getBatches
update <- applyReIndexUpdates(batch)
} yield update
}
/**
* Handle the suggestions database re-index update.
*
* Function clears existing module suggestions from the database, inserts new
* suggestions and builds the notification containing combined removed and
* added suggestions.
*
* @param updates the list of updates after the full module re-index
* @return the API suggestions database update notification
*/
private def applyReIndexUpdates(
updates: Seq[Api.SuggestionsDatabaseUpdate.Add]
): Future[SuggestionsDatabaseUpdateNotification] = {
val added = updates.map(_.suggestion)
val modules = updates.map(_.suggestion.module).distinct
log.debug(s"Applying re-index updates; modules=$modules")
for {
(_, removedIds) <- suggestionsRepo.removeAllByModule(modules)
(version, addedIds) <- suggestionsRepo.insertAll(added)
} yield {
val updatesRemoved = removedIds.map(SuggestionsDatabaseUpdate.Remove)
val updatesAdded = (addedIds zip added).flatMap {
case (Some(id), suggestion) =>
Some(SuggestionsDatabaseUpdate.Add(id, suggestion))
case (None, suggestion) =>
log.error("Failed to insert re-index suggestion: {}", suggestion)
None
}
SuggestionsDatabaseUpdateNotification(
version,
updatesRemoved :++ updatesAdded
)
}
}
/**
* Handle the suggestions database update.
*
@ -386,28 +299,42 @@ final class SuggestionsHandler(
* @return the API suggestions database update notification
*/
private def applyDatabaseUpdates(
msg: Api.SuggestionsDatabaseUpdateNotification
msg: Api.SuggestionsDatabaseModuleUpdateNotification
): Future[SuggestionsDatabaseUpdateNotification] = {
val (added, removed) = msg.updates
.foldLeft((Seq[Suggestion](), Seq[Suggestion]())) {
case ((add, remove), msg: Api.SuggestionsDatabaseUpdate.Add) =>
(add :+ msg.suggestion, remove)
case ((add, remove), msg: Api.SuggestionsDatabaseUpdate.Remove) =>
(add, remove :+ msg.suggestion)
val (addCmds, removeCmds, cleanCmds) = msg.updates
.foldLeft(
(Vector[Suggestion](), Vector[Suggestion](), Vector[String]())
) {
case ((add, remove, clean), m: Api.SuggestionsDatabaseUpdate.Add) =>
(add :+ m.suggestion, remove, clean)
case ((add, remove, clean), m: Api.SuggestionsDatabaseUpdate.Remove) =>
(add, remove :+ m.suggestion, clean)
case ((add, remove, clean), m: Api.SuggestionsDatabaseUpdate.Clean) =>
(add, remove, clean :+ m.module)
}
val fileVersion = versionCalculator.evalDigest(msg.contents)
log.debug(
s"Applying suggestion updates; added=${added
.map(_.name)}; removed=${removed.map(_.name)}"
s"Applying suggestion updates: Add(${addCmds.map(_.name).mkString(",")}); Remove(${removeCmds
.map(_.name)
.mkString(",")}); Clean(${cleanCmds.mkString(",")})"
)
for {
(_, removedIds) <- suggestionsRepo.removeAll(removed)
(version, addedIds) <- suggestionsRepo.insertAll(added)
(_, cleanedIds) <- suggestionsRepo.removeAllByModule(cleanCmds)
(_, removedIds) <- suggestionsRepo.removeAll(removeCmds)
(version, addedIds) <- suggestionsRepo.insertAll(addCmds)
_ <- fileVersionsRepo.setVersion(msg.file, fileVersion)
} yield {
val updatesRemoved = removedIds.collect {
case Some(id) => SuggestionsDatabaseUpdate.Remove(id)
}
val updatesCleaned = cleanedIds.map(SuggestionsDatabaseUpdate.Remove)
val updatesRemoved =
(removedIds zip removeCmds).flatMap {
case (Some(id), _) =>
Some(SuggestionsDatabaseUpdate.Remove(id))
case (None, suggestion) =>
log.error("Failed to remove suggestion: {}", suggestion)
None
}
val updatesAdded =
(addedIds zip added).flatMap {
(addedIds zip addCmds).flatMap {
case (Some(id), suggestion) =>
Some(SuggestionsDatabaseUpdate.Add(id, suggestion))
case (None, suggestion) =>
@ -416,7 +343,7 @@ final class SuggestionsHandler(
}
SuggestionsDatabaseUpdateNotification(
version,
updatesRemoved :++ updatesAdded
updatesCleaned :++ updatesRemoved :++ updatesAdded
)
}
}

View File

@ -71,7 +71,9 @@ class SuggestionsHandlerSpec
expectMsg(CapabilityAcquired)
// receive updates
handler ! Api.SuggestionsDatabaseUpdateNotification(
handler ! Api.SuggestionsDatabaseModuleUpdateNotification(
new File("/tmp/foo"),
"",
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Add)
)
@ -105,7 +107,9 @@ class SuggestionsHandlerSpec
expectMsg(CapabilityAcquired)
// receive updates
handler ! Api.SuggestionsDatabaseUpdateNotification(
handler ! Api.SuggestionsDatabaseModuleUpdateNotification(
new File("/tmp/foo"),
"",
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Add) ++
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Remove)
)
@ -178,7 +182,7 @@ class SuggestionsHandlerSpec
"search entries by empty search query" taggedAs Retry in withDb {
(config, repo, _, _, handler) =>
Await.ready(repo.insertAll(Suggestions.all), Timeout)
val (_, inserted) = Await.result(repo.insertAll(Suggestions.all), Timeout)
handler ! SearchProtocol.Completion(
file = mkModulePath(config, "Foo", "Main.enso"),
position = Position(0, 0),
@ -187,7 +191,12 @@ class SuggestionsHandlerSpec
tags = None
)
expectMsg(SearchProtocol.CompletionResult(4L, Seq()))
expectMsg(
SearchProtocol.CompletionResult(
4L,
Seq(inserted(0).get, inserted(1).get)
)
)
}
"search entries by self type" taggedAs Retry in withDb {

View File

@ -1,5 +1,7 @@
package org.enso.languageserver.websocket.json
import java.io.File
import io.circe.literal._
import org.enso.languageserver.refactoring.ProjectNameChangedEvent
import org.enso.languageserver.search.Suggestions
@ -20,7 +22,9 @@ class SuggestionsHandlerEventsTest extends BaseServerTest with FlakySpec {
// add atom
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Api.SuggestionsDatabaseModuleUpdateNotification(
new File("/tmp/foo"),
"",
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.atom))
)
)
@ -56,7 +60,9 @@ class SuggestionsHandlerEventsTest extends BaseServerTest with FlakySpec {
// add method
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Api.SuggestionsDatabaseModuleUpdateNotification(
new File("/tmp/foo"),
"",
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.method))
)
)
@ -102,7 +108,9 @@ class SuggestionsHandlerEventsTest extends BaseServerTest with FlakySpec {
// add function
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Api.SuggestionsDatabaseModuleUpdateNotification(
new File("/tmp/foo"),
"",
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.function))
)
)
@ -142,7 +150,9 @@ class SuggestionsHandlerEventsTest extends BaseServerTest with FlakySpec {
// add local
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Api.SuggestionsDatabaseModuleUpdateNotification(
new File("/tmp/foo"),
"",
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.local))
)
)
@ -281,7 +291,9 @@ class SuggestionsHandlerEventsTest extends BaseServerTest with FlakySpec {
// remove items
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Api.SuggestionsDatabaseModuleUpdateNotification(
new File("/tmp/foo"),
"",
Seq(
Api.SuggestionsDatabaseUpdate.Remove(Suggestions.method),
Api.SuggestionsDatabaseUpdate.Remove(Suggestions.function)

View File

@ -28,8 +28,10 @@ import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}
)
sealed trait Suggestion {
def name: String
def externalId: Option[Suggestion.ExternalId]
def module: String
def name: String
def returnType: String
}
object Suggestion {

View File

@ -165,12 +165,8 @@ object Runtime {
name = "runtimeServerShutDown"
),
new JsonSubTypes.Type(
value = classOf[Api.SuggestionsDatabaseUpdateNotification],
name = "suggestionsDatabaseUpdateNotification"
),
new JsonSubTypes.Type(
value = classOf[Api.SuggestionsDatabaseReIndexNotification],
name = "suggestionsDatabaseReindexNotification"
value = classOf[Api.SuggestionsDatabaseModuleUpdateNotification],
name = "suggestionsDatabaseModuleUpdateNotification"
),
new JsonSubTypes.Type(
value = classOf[Api.InvalidateModulesIndexRequest],
@ -179,10 +175,6 @@ object Runtime {
new JsonSubTypes.Type(
value = classOf[Api.InvalidateModulesIndexResponse],
name = "invalidateModulesIndexResponse"
),
new JsonSubTypes.Type(
value = classOf[Api.SuggestionsDatabaseIndexUpdateNotification],
name = "suggestionsDatabaseIndexUpdateNotification"
)
)
)
@ -343,24 +335,37 @@ object Runtime {
new JsonSubTypes.Type(
value = classOf[SuggestionsDatabaseUpdate.Remove],
name = "suggestionsDatabaseUpdateRemove"
),
new JsonSubTypes.Type(
value = classOf[SuggestionsDatabaseUpdate.Clean],
name = "suggestionsDatabaseUpdateClean"
)
)
)
sealed trait SuggestionsDatabaseUpdate
object SuggestionsDatabaseUpdate {
/** Create or replace the database entry.
/**
* Create or replace the database entry.
*
* @param suggestion the new suggestion
*/
case class Add(suggestion: Suggestion) extends SuggestionsDatabaseUpdate
/** Remove the database entry.
/**
* Remove the database entry.
*
* @param suggestion the suggestion to remove
*/
case class Remove(suggestion: Suggestion)
extends SuggestionsDatabaseUpdate
/**
* Remove all module entries from the database.
*
* @param module the module name
*/
case class Clean(module: String) extends SuggestionsDatabaseUpdate
}
/**
@ -711,53 +716,24 @@ object Runtime {
case class ProjectRenamed(newName: String) extends ApiResponse
/**
* A notification about the change in the suggestions database.
* A notification about the changes in the suggestions database.
*
* @param updates the list of database updates
* @param file the module file path
* @param contents the module source
* @param updates the list of suggestions extracted from module
*/
case class SuggestionsDatabaseUpdateNotification(
case class SuggestionsDatabaseModuleUpdateNotification(
file: File,
contents: String,
updates: Seq[SuggestionsDatabaseUpdate]
) extends ApiNotification
/**
* A notification about the re-indexed module updates.
*
* @param moduleName the name of re-indexed module
* @param updates the list of database updates
*/
case class SuggestionsDatabaseReIndexNotification(
moduleName: String,
updates: Seq[SuggestionsDatabaseUpdate.Add]
) extends ApiNotification
/** A request to invalidate the indexed flag of the modules. */
case class InvalidateModulesIndexRequest() extends ApiRequest
/** Signals that the module indexes has been invalidated. */
case class InvalidateModulesIndexResponse() extends ApiResponse
/**
* An indexed module.
*
* @param file the module file path
* @param contents the module source
* @param updates the list of suggestions extracted from module
*/
case class IndexedModule(
file: File,
contents: String,
updates: Seq[SuggestionsDatabaseUpdate.Add]
) extends ApiNotification
/**
* A notification about new indexed modules.
*
* @param updates the list of suggestions database updates
*/
case class SuggestionsDatabaseIndexUpdateNotification(
updates: Iterable[IndexedModule]
) extends ApiNotification
private lazy val mapper = {
val factory = new CBORFactory()
val mapper = new ObjectMapper(factory) with ScalaObjectMapper

View File

@ -92,33 +92,40 @@ class EnsureCompiledJob(protected val files: Iterable[File])
ctx.executionService.getContext.getTopScope.getModules.asScala
ctx.executionService.getLogger
.finest(s"Modules in scope: ${modulesInScope.map(_.getName)}")
val updates = modulesInScope.flatMap { module =>
modulesInScope.foreach { module =>
compile(module)
analyzeModuleInScope(module)
}
sendIndexUpdateNotification(
Api.SuggestionsDatabaseIndexUpdateNotification(updates)
)
}
private def analyzeImport(
module: Module
)(implicit ctx: RuntimeContext): Unit = {
if (!module.isIndexed && module.getLiteralSource != null) {
if (
!module.isIndexed &&
module.getLiteralSource != null &&
module.getPath != null
) {
ctx.executionService.getLogger
.finest(s"Analyzing imported ${module.getName}")
val moduleName = module.getName.toString
val addedSuggestions = SuggestionBuilder(module.getLiteralSource)
.build(module.getName.toString, module.getIr)
.filter(isSuggestionGlobal)
sendReIndexNotification(moduleName, addedSuggestions)
val update = Api.SuggestionsDatabaseModuleUpdateNotification(
new File(module.getPath),
module.getLiteralSource.toString,
Api.SuggestionsDatabaseUpdate.Clean(moduleName) +:
addedSuggestions.map(Api.SuggestionsDatabaseUpdate.Add)
)
sendModuleUpdate(update)
module.setIndexed(true)
}
}
private def analyzeModuleInScope(module: Module)(implicit
ctx: RuntimeContext
): Option[Api.IndexedModule] = {
): Unit = {
try module.getSource
catch {
case e: IOException =>
@ -139,16 +146,14 @@ class EnsureCompiledJob(protected val files: Iterable[File])
val addedSuggestions = SuggestionBuilder(module.getLiteralSource)
.build(moduleName, module.getIr)
.filter(isSuggestionGlobal)
module.setIndexed(true)
Some(
Api.IndexedModule(
new File(module.getPath),
module.getSource.getCharacters.toString,
addedSuggestions.map(Api.SuggestionsDatabaseUpdate.Add)
)
val update = Api.SuggestionsDatabaseModuleUpdateNotification(
new File(module.getPath),
module.getLiteralSource.toString,
Api.SuggestionsDatabaseUpdate.Clean(moduleName) +:
addedSuggestions.map(Api.SuggestionsDatabaseUpdate.Add)
)
} else {
None
sendModuleUpdate(update)
module.setIndexed(true)
}
}
@ -165,17 +170,30 @@ class EnsureCompiledJob(protected val files: Iterable[File])
val addedSuggestions =
SuggestionBuilder(module.getLiteralSource)
.build(moduleName, module.getIr)
sendSuggestionsUpdateNotification(
removedSuggestions diff addedSuggestions,
addedSuggestions diff removedSuggestions
val update = Api.SuggestionsDatabaseModuleUpdateNotification(
new File(module.getPath),
module.getLiteralSource.toString,
removedSuggestions
.diff(addedSuggestions)
.map(Api.SuggestionsDatabaseUpdate.Remove) :++
addedSuggestions
.diff(removedSuggestions)
.map(Api.SuggestionsDatabaseUpdate.Add)
)
sendModuleUpdate(update)
} else {
ctx.executionService.getLogger
.finest(s"Analyzing not-indexed module ${module.getName}")
val addedSuggestions =
SuggestionBuilder(module.getLiteralSource)
.build(moduleName, module.getIr)
sendReIndexNotification(moduleName, addedSuggestions)
val update = Api.SuggestionsDatabaseModuleUpdateNotification(
new File(module.getPath),
module.getLiteralSource.toString,
Api.SuggestionsDatabaseUpdate.Clean(moduleName) +:
addedSuggestions.map(Api.SuggestionsDatabaseUpdate.Add)
)
sendModuleUpdate(update)
module.setIndexed(true)
}
}
@ -209,7 +227,7 @@ class EnsureCompiledJob(protected val files: Iterable[File])
val prevStage = module.getCompilationStage
module.compileScope(ctx.executionService.getContext).getModule
if (prevStage != module.getCompilationStage) {
ctx.executionService.getLogger.finer(
ctx.executionService.getLogger.finest(
s"Compiled ${module.getName} $prevStage->${module.getCompilationStage}"
)
}
@ -289,58 +307,18 @@ class EnsureCompiledJob(protected val files: Iterable[File])
}
/**
* Send notification about the suggestions database updates.
* Send notification about module updates.
*
* @param removed the list of suggestions to remove
* @param added the list of suggestions to add
* @param payload the module update
* @param ctx the runtime context
*/
private def sendSuggestionsUpdateNotification(
removed: Seq[Suggestion],
added: Seq[Suggestion]
private def sendModuleUpdate(
payload: Api.SuggestionsDatabaseModuleUpdateNotification
)(implicit ctx: RuntimeContext): Unit =
if (added.nonEmpty || removed.nonEmpty) {
ctx.endpoint.sendToClient(
Api.Response(
Api.SuggestionsDatabaseUpdateNotification(
removed.map(Api.SuggestionsDatabaseUpdate.Remove) :++
added.map(Api.SuggestionsDatabaseUpdate.Add)
)
)
)
if (payload.updates.nonEmpty) {
ctx.endpoint.sendToClient(Api.Response(payload))
}
/**
* Send notification about the re-indexed module updates.
*
* @param moduleName the name of re-indexed module
* @param added the list of suggestions to add
* @param ctx the runtime context
*/
private def sendReIndexNotification(
moduleName: String,
added: Seq[Suggestion]
)(implicit ctx: RuntimeContext): Unit = {
ctx.endpoint.sendToClient(
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
added.map(Api.SuggestionsDatabaseUpdate.Add)
)
)
)
}
private def sendIndexUpdateNotification(
msg: Api.SuggestionsDatabaseIndexUpdateNotification
)(implicit
ctx: RuntimeContext
): Unit = {
if (msg.updates.nonEmpty) {
ctx.endpoint.sendToClient(Api.Response(msg))
}
}
private def isSuggestionGlobal(suggestion: Suggestion): Boolean =
suggestion match {
case _: Suggestion.Atom => true

View File

@ -454,9 +454,11 @@ class RuntimeServerTest
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
contents,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
@ -557,9 +559,11 @@ class RuntimeServerTest
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
contents,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
@ -660,9 +664,11 @@ class RuntimeServerTest
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
contents,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
@ -761,9 +767,11 @@ class RuntimeServerTest
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
contents,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
@ -862,9 +870,11 @@ class RuntimeServerTest
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
contents,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
@ -946,9 +956,11 @@ class RuntimeServerTest
context.Main.Update.mainZ(contextId),
idMainUpdate,
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
contents,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
@ -1136,9 +1148,11 @@ class RuntimeServerTest
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
contents,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
@ -1270,9 +1284,11 @@ class RuntimeServerTest
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
contents,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
@ -1621,9 +1637,11 @@ class RuntimeServerTest
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
contents,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
@ -1877,6 +1895,7 @@ class RuntimeServerTest
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
val moduleName = "Test.Main"
val code =
"""from Builtins import all
|
@ -1899,7 +1918,7 @@ class RuntimeServerTest
contextId,
Api.StackItem
.ExplicitCall(
Api.MethodPointer("Test.Main", "Main", "main"),
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
@ -1909,9 +1928,11 @@ class RuntimeServerTest
context.receive(3) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
"Test.Main",
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
code,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
@ -1953,11 +1974,12 @@ class RuntimeServerTest
}
it should "support file modification operations with attached ids" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val metadata = new Metadata
val idMain = metadata.addItem(7, 2)
val code = metadata.appendToCode("main = 84")
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val idMain = metadata.addItem(7, 2)
val code = metadata.appendToCode("main = 84")
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
@ -1987,7 +2009,7 @@ class RuntimeServerTest
contextId,
Api.StackItem
.ExplicitCall(
Api.MethodPointer("Test.Main", "Main", "main"),
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
@ -2005,9 +2027,11 @@ class RuntimeServerTest
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
"Test.Main",
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
code,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
@ -2043,9 +2067,10 @@ class RuntimeServerTest
}
it should "send suggestion notifications when file is executed" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val idMain = context.Main.metadata.addItem(33, 47)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val idMain = context.Main.metadata.addItem(33, 47)
val idMainUpdate =
Api.Response(
Api.ExpressionValuesComputed(
@ -2076,7 +2101,7 @@ class RuntimeServerTest
// push main
val item1 = Api.StackItem.ExplicitCall(
Api.MethodPointer("Test.Main", "Main", "main"),
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
@ -2090,13 +2115,15 @@ class RuntimeServerTest
context.Main.Update.mainZ(contextId),
idMainUpdate,
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
"Test.Main",
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
context.Main.code,
List(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
Some(idMain),
"Test.Main",
moduleName,
"main",
List(Suggestion.Argument("this", "Any", false, false, None)),
"Main",
@ -2107,7 +2134,7 @@ class RuntimeServerTest
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Main",
moduleName,
"foo",
List(
Suggestion.Argument("this", "Any", false, false, None),
@ -2121,7 +2148,7 @@ class RuntimeServerTest
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(context.Main.idMainX),
"Test.Main",
moduleName,
"x",
"Any",
Suggestion
@ -2131,7 +2158,7 @@ class RuntimeServerTest
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(context.Main.idMainY),
"Test.Main",
moduleName,
"y",
"Any",
Suggestion
@ -2141,7 +2168,7 @@ class RuntimeServerTest
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(context.Main.idMainZ),
"Test.Main",
moduleName,
"z",
"Any",
Suggestion
@ -2151,7 +2178,7 @@ class RuntimeServerTest
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(context.Main.idFooY),
"Test.Main",
moduleName,
"y",
"Any",
Suggestion
@ -2161,7 +2188,7 @@ class RuntimeServerTest
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Local(
Some(context.Main.idFooZ),
"Test.Main",
moduleName,
"z",
"Any",
Suggestion
@ -2219,8 +2246,10 @@ class RuntimeServerTest
}
it should "send suggestion notifications when file is modified" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val newline = System.lineSeparator()
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
@ -2237,15 +2266,7 @@ class RuntimeServerTest
val mainFile = context.writeMain(code)
// Open the new file
context.send(
Api.Request(
Api.OpenFileNotification(
mainFile,
code,
false
)
)
)
context.send(Api.Request(Api.OpenFileNotification(mainFile, code, false)))
context.receiveNone shouldEqual None
context.consumeOut shouldEqual List()
@ -2257,7 +2278,7 @@ class RuntimeServerTest
contextId,
Api.StackItem
.ExplicitCall(
Api.MethodPointer("Test.Main", "Main", "main"),
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
@ -2267,13 +2288,15 @@ class RuntimeServerTest
context.receive(3) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
"Test.Main",
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
code,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Main",
moduleName,
"main",
Seq(Suggestion.Argument("this", "Any", false, false, None)),
"Main",
@ -2300,20 +2323,29 @@ class RuntimeServerTest
),
TextEdit(
model.Range(model.Position(2, 0), model.Position(2, 0)),
"Number.lucky = 42\n\n"
s"Number.lucky = 42$newline$newline"
)
)
)
)
)
val codeModified =
"""from Builtins import all
|
|Number.lucky = 42
|
|main = IO.println "I'm a modified!"
|""".stripMargin
context.receive(2) should contain theSameElementsAs Seq(
Api.Response(
Api.SuggestionsDatabaseUpdateNotification(
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
codeModified,
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Main",
moduleName,
"lucky",
Seq(Suggestion.Argument("this", "Any", false, false, None)),
"Number",
@ -2335,10 +2367,11 @@ class RuntimeServerTest
}
it should "recompute expressions without invalidation" in {
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val moduleName = "Test.Main"
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
@ -2354,7 +2387,7 @@ class RuntimeServerTest
// push main
val item1 = Api.StackItem.ExplicitCall(
Api.MethodPointer("Test.Main", "Main", "main"),
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
@ -2380,10 +2413,11 @@ class RuntimeServerTest
}
it should "recompute expressions invalidating all" in {
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val moduleName = "Test.Main"
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
@ -2399,7 +2433,7 @@ class RuntimeServerTest
// push main
val item1 = Api.StackItem.ExplicitCall(
Api.MethodPointer("Test.Main", "Main", "main"),
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
@ -2431,10 +2465,11 @@ class RuntimeServerTest
}
it should "recompute expressions invalidating some" in {
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val contents = context.Main.code
val mainFile = context.writeMain(contents)
val moduleName = "Test.Main"
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
@ -2450,7 +2485,7 @@ class RuntimeServerTest
context.receiveNone shouldEqual None
// push main
val item1 = Api.StackItem.ExplicitCall(
Api.MethodPointer("Test.Main", "Main", "main"),
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
@ -2849,8 +2884,9 @@ class RuntimeServerTest
}
it should "emit visualisation update when expression is evaluated" in {
val contents = context.Main.code
val mainFile = context.writeMain(context.Main.code)
val contents = context.Main.code
val mainFile = context.writeMain(context.Main.code)
val moduleName = "Test.Main"
val visualisationFile =
context.writeInSrcDir("Visualisation", context.Visualisation.code)
@ -2882,7 +2918,7 @@ class RuntimeServerTest
// push main
val item1 = Api.StackItem.ExplicitCall(
Api.MethodPointer("Test.Main", "Main", "main"),
Api.MethodPointer(moduleName, "Main", "main"),
None,
Vector()
)
@ -2895,40 +2931,37 @@ class RuntimeServerTest
context.Main.Update.mainY(contextId),
context.Main.Update.mainZ(contextId),
Api.Response(
Api.SuggestionsDatabaseIndexUpdateNotification(
Api.SuggestionsDatabaseModuleUpdateNotification(
visualisationFile,
context.Visualisation.code,
Seq(
Api.IndexedModule(
visualisationFile,
context.Visualisation.code,
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"encode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
"Visualisation",
"Any",
None
)
Api.SuggestionsDatabaseUpdate.Clean("Test.Visualisation"),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"encode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"incAndEncode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
"Visualisation",
"Any",
None
)
)
"Visualisation",
"Any",
None
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"incAndEncode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
"Visualisation",
"Any",
None
)
)
)
@ -3066,49 +3099,48 @@ class RuntimeServerTest
context.Main.Update.mainY(contextId),
context.Main.Update.mainZ(contextId),
Api.Response(
Api.SuggestionsDatabaseIndexUpdateNotification(
Api.SuggestionsDatabaseModuleUpdateNotification(
visualisationFile,
context.Visualisation.code,
Seq(
Api.IndexedModule(
visualisationFile,
context.Visualisation.code,
Seq(
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"encode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
"Visualisation",
"Any",
None
)
Api.SuggestionsDatabaseUpdate.Clean("Test.Visualisation"),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"encode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"incAndEncode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
"Visualisation",
"Any",
None
)
)
"Visualisation",
"Any",
None
)
),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,
"Test.Visualisation",
"incAndEncode",
List(
Suggestion.Argument("this", "Any", false, false, None),
Suggestion.Argument("x", "Any", false, false, None)
),
"Visualisation",
"Any",
None
)
)
)
)
),
Api.Response(
Api.SuggestionsDatabaseReIndexNotification(
moduleName,
Api.SuggestionsDatabaseModuleUpdateNotification(
mainFile,
contents,
Seq(
Api.SuggestionsDatabaseUpdate.Clean(moduleName),
Api.SuggestionsDatabaseUpdate.Add(
Suggestion.Method(
None,

View File

@ -112,6 +112,17 @@ class StdlibRuntimeServerTest
Iterator.continually(receive(timeout)).take(n).flatten.toList
}
def receiveAllUntil(
msg: Api.Response,
timeout: Long
): List[Api.Response] = {
Iterator
.continually(receive(timeout))
.takeWhile(received => received.isDefined && received != Some(msg))
.flatten
.toList
}
def consumeOut: List[String] = {
val result = out.toString
out.reset()
@ -169,22 +180,22 @@ class StdlibRuntimeServerTest
)
)
)
val response = context.receiveN(3, timeout = 30)
response.length shouldEqual 3
response should contain allOf (
Api.Response(requestId, Api.PushContextResponse(contextId)),
context.executionSuccessful(contextId)
val response =
context.receiveAllUntil(
context.executionSuccessful(contextId),
timeout = 30
)
response should contain (
Api.Response(requestId, Api.PushContextResponse(contextId))
)
response.collect {
val collected = response.collect {
case Api.Response(
None,
Api.SuggestionsDatabaseIndexUpdateNotification(xs)
Api.SuggestionsDatabaseModuleUpdateNotification(_, _, xs)
) =>
xs.nonEmpty shouldBe true
xs.flatMap(
_.updates.headOption.map(_.suggestion.module)
) should not contain "Test.Main"
} should have length 1
}
collected.nonEmpty shouldBe true
context.consumeOut shouldEqual List("Hello World!")
}

View File

@ -28,6 +28,12 @@ trait FileVersionsRepo[F[_]] {
*/
def updateVersion(file: File, digest: Array[Byte]): F[Boolean]
/** Update the versions in batch.
*
* @param versions files with corresponding digests
*/
def updateVersions(versions: Seq[(File, Array[Byte])]): F[Unit]
/** Remove the version record.
*
* @param file the file path

View File

@ -269,6 +269,7 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
private def removeQuery(suggestion: Suggestion): DBIO[Option[Long]] = {
val (raw, _) = toSuggestionRow(suggestion)
val selectQuery = Suggestions
.filter(_.module === raw.module)
.filter(_.kind === raw.kind)
.filter(_.name === raw.name)
.filter(_.scopeStartLine === raw.scopeStartLine)
@ -306,13 +307,18 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
private def removeAllByModuleQuery(
modules: Seq[String]
): DBIO[(Long, Seq[Long])] = {
val selectQuery = Suggestions.filter(_.module inSet modules)
val deleteQuery = for {
rows <- selectQuery.result
n <- selectQuery.delete
version <- if (n > 0) incrementVersionQuery else currentVersionQuery
} yield version -> rows.flatMap(_.id)
deleteQuery
if (modules.nonEmpty) {
val selectQuery = Suggestions.filter(_.module inSet modules)
for {
rows <- selectQuery.result
n <- selectQuery.delete
version <- if (n > 0) incrementVersionQuery else currentVersionQuery
} yield version -> rows.flatMap(_.id)
} else {
for {
version <- currentVersionQuery
} yield (version, Seq())
}
}
/** The query to remove a list of suggestions.
@ -418,6 +424,9 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
}
/** Create a search query by the provided parameters.
*
* Even if the module is specified, the response includes all available
* global symbols (atoms and method).
*
* @param module the module name search parameter
* @param selfType the selfType search parameter
@ -435,7 +444,8 @@ final class SqlSuggestionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
): Query[SuggestionsTable, SuggestionRow, Seq] = {
Suggestions
.filterOpt(module) {
case (row, value) => row.module === value
case (row, value) =>
row.scopeStartLine === ScopeColumn.EMPTY || row.module === value
}
.filterOpt(selfType) {
case (row, value) => row.selfType === value

View File

@ -30,6 +30,12 @@ final class SqlVersionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
override def updateVersion(file: File, digest: Array[Byte]): Future[Boolean] =
db.run(updateVersionQuery(file, digest))
/** @inheritdoc */
override def updateVersions(
versions: Seq[(File, Array[Byte])]
): Future[Unit] =
db.run(updateVersionsQuery(versions))
/** @inheritdoc */
override def remove(file: File): Future[Unit] =
db.run(removeQuery(file))
@ -77,11 +83,10 @@ final class SqlVersionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
): DBIO[Option[Array[Byte]]] = {
val upsertQuery = FileVersions
.insertOrUpdate(FileVersionRow(file.toString, version))
val query = for {
for {
version <- getVersionQuery(file)
_ <- upsertQuery
} yield version
query.transactionally
}
/** The query to update the version if it differs from the recorded version.
@ -102,6 +107,23 @@ final class SqlVersionsRepo(db: SqlDatabase)(implicit ec: ExecutionContext)
else DBIO.successful(None)
} yield !versionsEquals
/** The query to update the versions in batch.
*
* @param versions files with corresponding digests
*/
private def updateVersionsQuery(
versions: Seq[(File, Array[Byte])]
): DBIO[Unit] =
if (versions.nonEmpty) {
def upsertQuery(file: File, version: Array[Byte]) = FileVersions
.insertOrUpdate(FileVersionRow(file.toString, version))
DBIO.sequence(versions.map(Function.tupled(upsertQuery))) >>
DBIO.successful(())
} else {
DBIO.successful(())
}
/** The query to remove the version record.
*
* @param file the file path

View File

@ -108,6 +108,28 @@ class FileVersionsRepoTest extends AnyWordSpec with Matchers with RetrySpec {
b4 shouldBe false
}
"batch update digest" taggedAs Retry in withRepo { repo =>
val file1 = new File("/foo/1")
val file2 = new File("/foo/2")
val digest0 = nextDigest()
val digest1 = nextDigest()
val digest2 = nextDigest()
val input = Seq(file1 -> digest1, file2 -> digest2)
val action =
for {
_ <- repo.setVersion(file1, digest0)
_ <- repo.updateVersions(input)
v1 <- repo.getVersion(file1)
v2 <- repo.getVersion(file2)
} yield (v1, v2)
val (v1, v2) = Await.result(action, Timeout)
v1 shouldBe a[Some[_]]
v2 shouldBe a[Some[_]]
util.Arrays.equals(v1.get, digest1) shouldBe true
util.Arrays.equals(v2.get, digest2) shouldBe true
}
"delete digest" taggedAs Retry in withRepo { repo =>
val file = new File("/foo/bar")
val digest = nextDigest()

View File

@ -204,6 +204,23 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec {
inserted should contain theSameElementsAs removed
}
"remove all suggestions by empty module" taggedAs Retry in withRepo { repo =>
val action = for {
(_, idsIns) <- repo.insertAll(
Seq(
suggestion.atom,
suggestion.method,
suggestion.function,
suggestion.local
)
)
(_, idsRem) <- repo.removeAllByModule(Seq())
} yield (idsIns.flatten, idsRem)
val (_, removed) = Await.result(action, Timeout)
removed.isEmpty shouldBe true
}
"remove all suggestions" taggedAs Retry in withRepo { repo =>
val action = for {
(_, Seq(id1, _, _, id4)) <- repo.insertAll(
@ -492,15 +509,15 @@ class SuggestionsRepoTest extends AnyWordSpec with Matchers with RetrySpec {
"search suggestion by empty module" taggedAs Retry in withRepo { repo =>
val action = for {
_ <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
id1 <- repo.insert(suggestion.atom)
id2 <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
_ <- repo.insert(suggestion.local)
res <- repo.search(Some(""), None, None, None, None)
} yield res._2
} yield (res._2, Seq(id1, id2))
val res = Await.result(action, Timeout)
res.isEmpty shouldEqual true
val (res, globals) = Await.result(action, Timeout)
res should contain theSameElementsAs globals.flatten
}
"search suggestion by self type" taggedAs Retry in withRepo { repo =>