Implement Search Requests API (#953)

This commit is contained in:
Dmitry Bushev 2020-07-06 16:55:21 +03:00 committed by GitHub
parent 05146e23b7
commit 6ba038c800
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2143 additions and 467 deletions

View File

@ -87,6 +87,8 @@ jobs:
run: sbt -no-colors runtime/Benchmark/compile
- name: Check Language Server Benchmark Compilation
run: sbt -no-colors language-server/Benchmark/compile
- name: Check Searcher Benchmark Compilation
run: sbt -no-colors searcher/Benchmark/compile
- name: Build the Uberjar
run: sbt -no-colors runner/assembly
- name: Test the Uberjar

View File

@ -482,6 +482,7 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
"dev.zio" %% "zio-interop-cats" % zioInteropCatsVersion,
"commons-io" % "commons-io" % commonsIoVersion,
"com.beachape" %% "enumeratum-circe" % enumeratumCirceVersion,
"com.typesafe.slick" %% "slick-hikaricp" % slickVersion % Runtime,
"com.miguno.akka" %% "akka-mock-scheduler" % akkaMockSchedulerVersion % Test,
"org.mockito" %% "mockito-scala" % mockitoScalaVersion % Test
),
@ -608,13 +609,19 @@ lazy val searcher = project
.in(file("lib/scala/searcher"))
.configs(Test)
.settings(
libraryDependencies ++= akkaTest ++ Seq(
libraryDependencies ++= jmh ++ Seq(
"com.typesafe.slick" %% "slick" % slickVersion,
"org.xerial" % "sqlite-jdbc" % sqliteVersion,
"com.typesafe.slick" %% "slick-hikaricp" % slickVersion % Runtime,
"ch.qos.logback" % "logback-classic" % logbackClassicVersion % Test,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
)
)
.dependsOn(`json-rpc-server-test` % Test)
.configs(Benchmark)
.settings(
inConfig(Benchmark)(Defaults.testSettings),
fork in Benchmark := true
)
lazy val `interpreter-dsl` = (project in file("lib/scala/interpreter-dsl"))
.settings(
@ -681,8 +688,9 @@ lazy val `language-server` = (project in file("engine/language-server"))
"io.methvin" % "directory-watcher" % directoryWatcherVersion,
"com.beachape" %% "enumeratum-circe" % enumeratumCirceVersion,
"com.google.flatbuffers" % "flatbuffers-java" % flatbuffersVersion,
akkaTestkit % Test,
"commons-io" % "commons-io" % commonsIoVersion,
akkaTestkit % Test,
"com.typesafe.slick" %% "slick-hikaricp" % slickVersion % Runtime,
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
"org.scalacheck" %% "scalacheck" % scalacheckVersion % Test,
"org.graalvm.sdk" % "polyglot-tck" % graalVersion % "provided"
@ -875,7 +883,8 @@ lazy val runner = project
"com.monovore" %% "decline" % declineVersion,
"io.github.spencerpark" % "jupyter-jvm-basekernel" % jupyterJvmBasekernelVersion,
"org.jline" % "jline" % jlineVersion,
"org.typelevel" %% "cats-core" % catsVersion
"org.typelevel" %% "cats-core" % catsVersion,
"com.typesafe.slick" %% "slick-hikaricp" % slickVersion % Runtime
),
connectInput in run := true
)
@ -906,4 +915,3 @@ lazy val runner = project
.dependsOn(pkg)
.dependsOn(`language-server`)
.dependsOn(`polyglot-api`)

View File

@ -27,7 +27,6 @@ transport formats, please look [here](./protocol-architecture).
- [`SuggestionEntryArgument`](#suggestionentryargument)
- [`SuggestionEntry`](#suggestionentry)
- [`SuggestionEntryType`](#suggestionentrytype)
- [`SuggestionsDatabaseEntry`](#suggestionsdatabaseentry)
- [`SuggestionsDatabaseUpdate`](#suggestionsdatabaseupdate)
- [`File`](#file)
- [`DirectoryTree`](#directorytree)
@ -141,6 +140,7 @@ transport formats, please look [here](./protocol-architecture).
- [`CapabilityNotAcquired`](#capabilitynotacquired)
- [`SessionNotInitialisedError`](#sessionnotinitialisederror)
- [`SessionAlreadyInitialisedError`](#sessionalreadyinitialisederror)
- [`SuggestionsDatabaseError`](#suggestionsdatabaseerror)
<!-- /MarkdownTOC -->
@ -330,20 +330,6 @@ type SuggestionEntryType
| Local;
```
### `SuggestionsDatabaseEntry`
The entry in the suggestions database.
#### Format
```typescript
// The suggestions database entry.
interface SuggestionsDatabaseEntry {
// suggestion entry id;
id: number;
suggestion: Suggestion;
}
```
### `SuggestionsDatabaseUpdate`
The update of the suggestions database.
@ -351,23 +337,20 @@ The update of the suggestions database.
```typescript
// The kind of the suggestions database update.
type SuggestionsDatabaseUpdateKind
type SuggestionsDatabaseUpdate
= Add
| Update
| Delete
| Remove
interface SuggestionsDatabaseUpdate {
interface Add {
// suggestion entry id
id: number;
kind: SuggestionsDatabaseUpdateKind;
name?: string;
module?: string;
arguments?: [SuggestionEntryArgument];
selfType?: string;
returnType?: string;
documentation?: string;
scope?: SuggestionEntryScope;
// suggestion entry
suggestion: SuggestionEntry;
}
interface Remove {
// suggestion entry id
id: number;
```
### `File`
@ -2482,7 +2465,7 @@ null
```typescript
{
// The list of suggestions database entries
entries: [SuggestionsDatabaseEntry];
entries: [SuggestionsDatabaseUpdate];
// The version of received suggestions database
currentVersion: number;
}
@ -2971,3 +2954,13 @@ Signals that session is already initialised.
"message" : "Session already initialised"
}
```
### `SuggestionsDatabaseError`
Signals about an error accessing the suggestions database.
```typescript
"error" : {
"code" : 7001,
"message" : "Suggestions database error"
}
```

View File

@ -0,0 +1,16 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%-15thread] %-36logger{36} %msg%n</pattern>
</encoder>
</appender>
<logger name="org.enso" level="TRACE"/>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@ -24,16 +24,12 @@ import org.enso.languageserver.protocol.json.{
JsonConnectionControllerFactory,
JsonRpc
}
import org.enso.languageserver.runtime.{
ContextRegistry,
RuntimeConnector,
RuntimeKiller,
SuggestionsDatabaseEventsListener
}
import org.enso.languageserver.runtime._
import org.enso.languageserver.session.SessionRouter
import org.enso.languageserver.text.BufferRegistry
import org.enso.languageserver.util.binary.BinaryEncoder
import org.enso.polyglot.{LanguageInfo, RuntimeOptions, RuntimeServerInfo}
import org.enso.searcher.sql.SqlSuggestionsRepo
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.io.MessageEndpoint
@ -70,6 +66,12 @@ class MainModule(serverConfig: LanguageServerConfig) {
implicit val materializer = SystemMaterializer.get(system)
val suggestionsRepo = {
val repo = SqlSuggestionsRepo()(system.dispatcher)
repo.init
repo
}
lazy val sessionRouter =
system.actorOf(SessionRouter.props(), "session-router")
@ -95,7 +97,12 @@ class MainModule(serverConfig: LanguageServerConfig) {
)
lazy val suggestionsDatabaseEventsListener =
system.actorOf(SuggestionsDatabaseEventsListener.props(sessionRouter))
system.actorOf(
SuggestionsDatabaseEventsListener.props(sessionRouter, suggestionsRepo)
)
lazy val suggestionsHandler =
system.actorOf(SuggestionsHandler.props(suggestionsRepo))
lazy val capabilityRouter =
system.actorOf(
@ -176,6 +183,7 @@ class MainModule(serverConfig: LanguageServerConfig) {
capabilityRouter,
fileManager,
contextRegistry,
suggestionsHandler,
stdOutController,
stdErrController,
stdInController,

View File

@ -41,6 +41,11 @@ import org.enso.languageserver.runtime.{
SearchProtocol
}
import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.runtime.SearchApi.{
Completion,
GetSuggestionsDatabase,
GetSuggestionsDatabaseVersion
}
import org.enso.languageserver.runtime.VisualisationApi.{
AttachVisualisation,
DetachVisualisation,
@ -67,6 +72,7 @@ import scala.concurrent.duration._
* @param capabilityRouter a router that dispatches capability requests
* @param fileManager performs operations with file system
* @param contextRegistry a router that dispatches execution context requests
* @param suggestionsHandler a reference to the suggestions requests handler
* @param requestTimeout a request timeout
*/
class JsonConnectionController(
@ -75,6 +81,7 @@ class JsonConnectionController(
val capabilityRouter: ActorRef,
val fileManager: ActorRef,
val contextRegistry: ActorRef,
val suggestionsHandler: ActorRef,
val stdOutController: ActorRef,
val stdErrController: ActorRef,
val stdInController: ActorRef,
@ -259,6 +266,12 @@ class JsonConnectionController(
.props(requestTimeout, contextRegistry, rpcSession),
ExecutionContextRecompute -> executioncontext.RecomputeHandler
.props(requestTimeout, contextRegistry, rpcSession),
GetSuggestionsDatabaseVersion -> search.GetSuggestionsDatabaseVersionHandler
.props(requestTimeout, suggestionsHandler),
GetSuggestionsDatabase -> search.GetSuggestionsDatabaseHandler
.props(requestTimeout, suggestionsHandler),
Completion -> search.CompletionHandler
.props(requestTimeout, suggestionsHandler),
AttachVisualisation -> AttachVisualisationHandler
.props(rpcSession.clientId, requestTimeout, contextRegistry),
DetachVisualisation -> DetachVisualisationHandler
@ -288,6 +301,7 @@ object JsonConnectionController {
* @param capabilityRouter a router that dispatches capability requests
* @param fileManager performs operations with file system
* @param contextRegistry a router that dispatches execution context requests
* @param suggestionsHandler a reference to the suggestions requests handler
* @param requestTimeout a request timeout
* @return a configuration object
*/
@ -297,6 +311,7 @@ object JsonConnectionController {
capabilityRouter: ActorRef,
fileManager: ActorRef,
contextRegistry: ActorRef,
suggestionsHandler: ActorRef,
stdOutController: ActorRef,
stdErrController: ActorRef,
stdInController: ActorRef,
@ -310,6 +325,7 @@ object JsonConnectionController {
capabilityRouter,
fileManager,
contextRegistry,
suggestionsHandler,
stdOutController,
stdErrController,
stdInController,

View File

@ -17,6 +17,7 @@ class JsonConnectionControllerFactory(
capabilityRouter: ActorRef,
fileManager: ActorRef,
contextRegistry: ActorRef,
suggestionsHandler: ActorRef,
stdOutController: ActorRef,
stdErrController: ActorRef,
stdInController: ActorRef,
@ -38,6 +39,7 @@ class JsonConnectionControllerFactory(
capabilityRouter,
fileManager,
contextRegistry,
suggestionsHandler,
stdOutController,
stdErrController,
stdInController,

View File

@ -55,6 +55,9 @@ object JsonRpc {
.registerRequest(AttachVisualisation)
.registerRequest(DetachVisualisation)
.registerRequest(ModifyVisualisation)
.registerRequest(GetSuggestionsDatabase)
.registerRequest(GetSuggestionsDatabaseVersion)
.registerRequest(Completion)
.registerRequest(RenameProject)
.registerNotification(ForceReleaseCapability)
.registerNotification(GrantCapability)

View File

@ -0,0 +1,92 @@
package org.enso.languageserver.requesthandler.search
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Status}
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc._
import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.runtime.SearchApi.{
Completion,
SuggestionsDatabaseError
}
import org.enso.languageserver.runtime.SearchProtocol
import org.enso.languageserver.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration
/**
* A request handler for `search/completion` command.
*
* @param timeout request timeout
* @param suggestionsHandler a reference to the suggestions handler
*/
class CompletionHandler(
timeout: FiniteDuration,
suggestionsHandler: ActorRef
) extends Actor
with ActorLogging
with UnhandledLogging {
import context.dispatcher
override def receive: Receive = requestStage
private def requestStage: Receive = {
case Request(
Completion,
id,
Completion.Params(module, pos, selfType, returnType, tags)
) =>
suggestionsHandler ! SearchProtocol.Completion(
module,
pos,
selfType,
returnType,
tags
)
val cancellable =
context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout)
context.become(responseStage(id, sender(), cancellable))
}
private def responseStage(
id: Id,
replyTo: ActorRef,
cancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
log.error(ex, "Search completion error")
replyTo ! ResponseError(Some(id), SuggestionsDatabaseError)
cancellable.cancel()
context.stop(self)
case RequestTimeout =>
log.error(s"Request $id timed out")
replyTo ! ResponseError(Some(id), ServiceError)
context.stop(self)
case SearchProtocol.CompletionResult(version, results) =>
replyTo ! ResponseResult(
Completion,
id,
Completion.Result(results, version)
)
cancellable.cancel()
context.stop(self)
}
}
object CompletionHandler {
/**
* Creates configuration object used to create a [[CompletionHandler]].
*
* @param timeout request timeout
* @param suggestionsHandler a reference to the suggestions handler
*/
def props(
timeout: FiniteDuration,
suggestionsHandler: ActorRef
): Props =
Props(new CompletionHandler(timeout, suggestionsHandler))
}

View File

@ -0,0 +1,83 @@
package org.enso.languageserver.requesthandler.search
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Status}
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc._
import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.runtime.SearchApi.{
GetSuggestionsDatabase,
SuggestionsDatabaseError
}
import org.enso.languageserver.runtime.SearchProtocol
import org.enso.languageserver.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration
/**
* A request handler for `search/getSuggestionsDatabase` command.
*
* @param timeout request timeout
* @param suggestionsHandler a reference to the suggestions handler
*/
class GetSuggestionsDatabaseHandler(
timeout: FiniteDuration,
suggestionsHandler: ActorRef
) extends Actor
with ActorLogging
with UnhandledLogging {
import context.dispatcher
override def receive: Receive = requestStage
private def requestStage: Receive = {
case Request(GetSuggestionsDatabase, id, _) =>
suggestionsHandler ! SearchProtocol.GetSuggestionsDatabase
val cancellable =
context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout)
context.become(responseStage(id, sender(), cancellable))
}
private def responseStage(
id: Id,
replyTo: ActorRef,
cancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
log.error(ex, "GetSuggestionsDatabase error")
replyTo ! ResponseError(Some(id), SuggestionsDatabaseError)
cancellable.cancel()
context.stop(self)
case RequestTimeout =>
log.error(s"Request $id timed out")
replyTo ! ResponseError(Some(id), ServiceError)
context.stop(self)
case SearchProtocol.GetSuggestionsDatabaseResult(updates, version) =>
replyTo ! ResponseResult(
GetSuggestionsDatabase,
id,
GetSuggestionsDatabase.Result(updates, version)
)
cancellable.cancel()
context.stop(self)
}
}
object GetSuggestionsDatabaseHandler {
/**
* Creates configuration object used to create a
* [[GetSuggestionsDatabaseHandler]].
*
* @param timeout request timeout
* @param suggestionsHandler a reference to the suggestions handler
*/
def props(
timeout: FiniteDuration,
suggestionsHandler: ActorRef
): Props =
Props(new GetSuggestionsDatabaseHandler(timeout, suggestionsHandler))
}

View File

@ -0,0 +1,83 @@
package org.enso.languageserver.requesthandler.search
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Status}
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc._
import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.runtime.SearchApi.{
GetSuggestionsDatabaseVersion,
SuggestionsDatabaseError
}
import org.enso.languageserver.runtime.SearchProtocol
import org.enso.languageserver.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration
/**
* A request handler for `search/getSuggestionsDatabaseVersion` command.
*
* @param timeout request timeout
* @param suggestionsHandler a reference to the suggestions handler
*/
class GetSuggestionsDatabaseVersionHandler(
timeout: FiniteDuration,
suggestionsHandler: ActorRef
) extends Actor
with ActorLogging
with UnhandledLogging {
import context.dispatcher
override def receive: Receive = requestStage
private def requestStage: Receive = {
case Request(GetSuggestionsDatabaseVersion, id, _) =>
suggestionsHandler ! SearchProtocol.GetSuggestionsDatabaseVersion
val cancellable =
context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout)
context.become(responseStage(id, sender(), cancellable))
}
private def responseStage(
id: Id,
replyTo: ActorRef,
cancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
log.error(ex, "GetSuggestionsDatabaseVersion error")
replyTo ! ResponseError(Some(id), SuggestionsDatabaseError)
cancellable.cancel()
context.stop(self)
case RequestTimeout =>
log.error(s"Request $id timed out")
replyTo ! ResponseError(Some(id), ServiceError)
context.stop(self)
case SearchProtocol.GetSuggestionsDatabaseVersionResult(version) =>
replyTo ! ResponseResult(
GetSuggestionsDatabaseVersion,
id,
GetSuggestionsDatabaseVersion.Result(version)
)
cancellable.cancel()
context.stop(self)
}
}
object GetSuggestionsDatabaseVersionHandler {
/**
* Creates configuration object used to create a
* [[GetSuggestionsDatabaseVersionHandler]].
*
* @param timeout request timeout
* @param suggestionsHandler a reference to the suggestions handler
*/
def props(
timeout: FiniteDuration,
suggestionsHandler: ActorRef
): Props =
Props(new GetSuggestionsDatabaseVersionHandler(timeout, suggestionsHandler))
}

View File

@ -1,7 +1,12 @@
package org.enso.languageserver.runtime
import org.enso.jsonrpc.{HasParams, Method}
import org.enso.languageserver.runtime.SearchProtocol.SuggestionsDatabaseUpdate
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused}
import org.enso.languageserver.runtime.SearchProtocol.{
SuggestionId,
SuggestionKind,
SuggestionsDatabaseUpdate
}
import org.enso.text.editing.model.Position
/**
* The execution JSON RPC API provided by the language server.
@ -23,4 +28,55 @@ object SearchApi {
}
}
case object GetSuggestionsDatabase
extends Method("search/getSuggestionsDatabase") {
case class Result(
entries: Seq[SuggestionsDatabaseUpdate],
currentVersion: Long
)
implicit val hasParams = new HasParams[this.type] {
type Params = Unused.type
}
implicit val hasResult = new HasResult[this.type] {
type Result = GetSuggestionsDatabase.Result
}
}
case object GetSuggestionsDatabaseVersion
extends Method("search/getSuggestionsDatabaseVersion") {
case class Result(version: Long)
implicit val hasParams = new HasParams[this.type] {
type Params = Unused.type
}
implicit val hasResult = new HasResult[this.type] {
type Result = GetSuggestionsDatabaseVersion.Result
}
}
case object Completion extends Method("search/completion") {
case class Params(
module: String,
position: Position,
selfType: Option[String],
returnType: Option[String],
tags: Option[Seq[SuggestionKind]]
)
case class Result(results: Seq[SuggestionId], currentVersion: Long)
implicit val hasParams = new HasParams[this.type] {
type Params = Completion.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = Completion.Result
}
}
case object SuggestionsDatabaseError
extends Error(7001, "Suggestions database error")
}

View File

@ -1,48 +1,32 @@
package org.enso.languageserver.runtime
import enumeratum._
import io.circe.generic.auto._
import io.circe.syntax._
import io.circe.{Decoder, Encoder, Json}
import org.enso.searcher.Suggestion
import org.enso.text.editing.model.Position
object SearchProtocol {
type SuggestionId = Long
sealed trait SuggestionsDatabaseUpdate
object SuggestionsDatabaseUpdate {
/** Create or replace the database entry.
*
* @param id suggestion id
* @param id the suggestion id
* @param suggestion the new suggestion
*/
case class Add(id: Long, suggestion: Suggestion)
case class Add(id: SuggestionId, suggestion: Suggestion)
extends SuggestionsDatabaseUpdate
/** Remove the database entry.
*
* @param id the suggestion id
*/
case class Remove(id: Long) extends SuggestionsDatabaseUpdate
/** Modify the database entry.
*
* @param id the suggestion id
* @param name the new suggestion name
* @param arguments the new suggestion arguments
* @param selfType the new self type of the suggestion
* @param returnType the new return type of the suggestion
* @param documentation the new documentation string
* @param scope the suggestion scope
*/
case class Modify(
id: Long,
name: Option[String],
arguments: Option[Seq[Suggestion.Argument]],
selfType: Option[String],
returnType: Option[String],
documentation: Option[String],
scope: Option[Suggestion.Scope]
) extends SuggestionsDatabaseUpdate
case class Remove(id: SuggestionId) extends SuggestionsDatabaseUpdate
private object CodecField {
@ -53,9 +37,7 @@ object SearchProtocol {
val Add = "Add"
val Delete = "Delete"
val Update = "Update"
val Remove = "Remove"
}
implicit val decoder: Decoder[SuggestionsDatabaseUpdate] =
@ -64,10 +46,7 @@ object SearchProtocol {
case CodecType.Add =>
Decoder[SuggestionsDatabaseUpdate.Add].tryDecode(cursor)
case CodecType.Update =>
Decoder[SuggestionsDatabaseUpdate.Modify].tryDecode(cursor)
case CodecType.Delete =>
case CodecType.Remove =>
Decoder[SuggestionsDatabaseUpdate.Remove].tryDecode(cursor)
}
}
@ -80,16 +59,10 @@ object SearchProtocol {
.deepMerge(Json.obj(CodecField.Type -> CodecType.Add.asJson))
.dropNullValues
case modify: SuggestionsDatabaseUpdate.Modify =>
Encoder[SuggestionsDatabaseUpdate.Modify]
.apply(modify)
.deepMerge(Json.obj(CodecField.Type -> CodecType.Update.asJson))
.dropNullValues
case remove: SuggestionsDatabaseUpdate.Remove =>
Encoder[SuggestionsDatabaseUpdate.Remove]
.apply(remove)
.deepMerge(Json.obj(CodecField.Type -> CodecType.Delete.asJson))
.deepMerge(Json.obj(CodecField.Type -> CodecType.Remove.asJson))
}
private object SuggestionType {
@ -152,9 +125,106 @@ object SearchProtocol {
}
}
/** The type of a suggestion. */
sealed trait SuggestionKind extends EnumEntry
object SuggestionKind
extends Enum[SuggestionKind]
with CirceEnum[SuggestionKind] {
/** An atom suggestion. */
case object Atom extends SuggestionKind
/** A method suggestion. */
case object Method extends SuggestionKind
/** A function suggestion. */
case object Function extends SuggestionKind
/** Local binding suggestion. */
case object Local extends SuggestionKind
override val values = findValues
/** Create API kind from the [[Suggestion.Kind]]
*
* @param kind the suggestion kind
* @return the API kind
*/
def apply(kind: Suggestion.Kind): SuggestionKind =
kind match {
case Suggestion.Kind.Atom => Atom
case Suggestion.Kind.Method => Method
case Suggestion.Kind.Function => Function
case Suggestion.Kind.Local => Local
}
/** Convert from API kind to [[Suggestion.Kind]]
*
* @param kind the API kind
* @return the suggestion kind
*/
def toSuggestion(kind: SuggestionKind): Suggestion.Kind =
kind match {
case Atom => Suggestion.Kind.Atom
case Method => Suggestion.Kind.Method
case Function => Suggestion.Kind.Function
case Local => Suggestion.Kind.Local
}
}
/** A notification about changes in the suggestions database.
*
* @param updates the list of database updates
* @param currentVersion current version of the suggestions database
*/
case class SuggestionsDatabaseUpdateNotification(
updates: Seq[SuggestionsDatabaseUpdate],
currentVersion: Long
)
/** The request to receive contents of the suggestions database. */
case object GetSuggestionsDatabase
/** The reply to the [[GetSuggestionsDatabase]] request.
*
* @param entries the entries of the suggestion database
* @param currentVersion current version of the suggestions database
*/
case class GetSuggestionsDatabaseResult(
entries: Seq[SuggestionsDatabaseUpdate],
currentVersion: Long
)
/** The request to receive the current version of the suggestions database. */
case object GetSuggestionsDatabaseVersion
/** The reply to the [[GetSuggestionsDatabaseVersion]] request.
*
* @param version current version of the suggestions database
*/
case class GetSuggestionsDatabaseVersionResult(version: Long)
/** The completion request.
*
* @param module the edited module
* @param position the cursor position
* @param selfType filter entries matching the self type
* @param returnType filter entries matching the return type
* @param tags filter entries by suggestion type
*/
case class Completion(
module: String,
position: Position,
selfType: Option[String],
returnType: Option[String],
tags: Option[Seq[SuggestionKind]]
)
/** Te reply to the [[Completion]] request.
*
* @param currentVersion current version of the suggestions database
* @param results the list of suggestion ids matched the search query
*/
case class CompletionResult(currentVersion: Long, results: Seq[SuggestionId])
}

View File

@ -19,6 +19,10 @@ import org.enso.languageserver.runtime.SearchProtocol.{
import org.enso.languageserver.session.SessionRouter.DeliverToJsonController
import org.enso.languageserver.util.UnhandledLogging
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.searcher.{Suggestion, SuggestionsRepo}
import scala.concurrent.Future
import scala.util.{Failure, Success}
/**
* Event listener listens event stream for the suggestion database
@ -26,13 +30,17 @@ import org.enso.polyglot.runtime.Runtime.Api
* is a singleton and created per context registry.
*
* @param sessionRouter the session router
* @param repo the suggestions repo
*/
final class SuggestionsDatabaseEventsListener(
sessionRouter: ActorRef
sessionRouter: ActorRef,
repo: SuggestionsRepo[Future]
) extends Actor
with ActorLogging
with UnhandledLogging {
import context.dispatcher
override def preStart(): Unit = {
context.system.eventStream
.subscribe(self, classOf[Api.SuggestionsDatabaseUpdateNotification])
@ -56,40 +64,54 @@ final class SuggestionsDatabaseEventsListener(
context.become(withClients(clients - client.clientId))
case msg: Api.SuggestionsDatabaseUpdateNotification =>
applyDatabaseUpdates(msg)
.onComplete {
case Success(notification) =>
if (notification.updates.nonEmpty) {
clients.foreach { clientId =>
sessionRouter ! DeliverToJsonController(
clientId,
SuggestionsDatabaseUpdateNotification(msg.updates.map(toUpdate), 0)
sessionRouter ! DeliverToJsonController(clientId, notification)
}
}
case Failure(ex) =>
log.error(
ex,
"Error applying suggestion database updates: {}",
msg.updates
)
}
}
private def toUpdate(
update: Api.SuggestionsDatabaseUpdate
): SuggestionsDatabaseUpdate =
update match {
case Api.SuggestionsDatabaseUpdate.Add(id, suggestion) =>
SuggestionsDatabaseUpdate.Add(id, suggestion)
case Api.SuggestionsDatabaseUpdate.Modify(
id,
name,
arguments,
selfType,
returnType,
doc,
scope
) =>
SuggestionsDatabaseUpdate.Modify(
id,
name,
arguments,
selfType,
returnType,
doc,
scope
private def applyDatabaseUpdates(
msg: Api.SuggestionsDatabaseUpdateNotification
): 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)
}
for {
(_, removedIds) <- repo.removeAll(removed)
(version, addedIds) <- repo.insertAll(added)
} yield {
val updatesRemoved = removedIds.collect {
case Some(id) => SuggestionsDatabaseUpdate.Remove(id)
}
val updatesAdded =
(addedIds zip added).flatMap {
case (Some(id), suggestion) =>
Some(SuggestionsDatabaseUpdate.Add(id, suggestion))
case (None, suggestion) =>
log.error("failed to insert suggestion: {}", suggestion)
None
}
SuggestionsDatabaseUpdateNotification(
updatesRemoved ++ updatesAdded,
version
)
case Api.SuggestionsDatabaseUpdate.Remove(id) =>
SuggestionsDatabaseUpdate.Remove(id)
}
}
}
@ -100,8 +122,12 @@ object SuggestionsDatabaseEventsListener {
* [[SuggestionsDatabaseEventsListener]].
*
* @param sessionRouter the session router
* @param repo the suggestions repo
*/
def props(sessionRouter: ActorRef): Props =
Props(new SuggestionsDatabaseEventsListener(sessionRouter))
def props(
sessionRouter: ActorRef,
repo: SuggestionsRepo[Future]
): Props =
Props(new SuggestionsDatabaseEventsListener(sessionRouter, repo))
}

View File

@ -0,0 +1,65 @@
package org.enso.languageserver.runtime
import akka.actor.{Actor, ActorLogging, Props}
import akka.pattern.pipe
import org.enso.languageserver.runtime.SearchProtocol._
import org.enso.languageserver.util.UnhandledLogging
import org.enso.searcher.{SuggestionEntry, SuggestionsRepo}
import scala.concurrent.Future
/**
* The handler of search requests.
*
* Handler initializes the database and responds to the search requests.
*
* @param repo the suggestions repo
*/
final class SuggestionsHandler(repo: SuggestionsRepo[Future])
extends Actor
with ActorLogging
with UnhandledLogging {
import context.dispatcher
override def receive: Receive = {
case GetSuggestionsDatabaseVersion =>
repo.currentVersion
.map(GetSuggestionsDatabaseVersionResult)
.pipeTo(sender())
case GetSuggestionsDatabase =>
repo.getAll
.map(Function.tupled(toGetSuggestionsDatabaseResult))
.pipeTo(sender())
case Completion(_, _, selfType, returnType, tags) =>
val kinds = tags.map(_.map(SuggestionKind.toSuggestion))
repo
.search(selfType, returnType, kinds)
.map(CompletionResult.tupled)
.pipeTo(sender())
}
private def toGetSuggestionsDatabaseResult(
version: Long,
entries: Seq[SuggestionEntry]
): GetSuggestionsDatabaseResult = {
val updates = entries.map(entry =>
SuggestionsDatabaseUpdate.Add(entry.id, entry.suggestion)
)
GetSuggestionsDatabaseResult(updates, version)
}
}
object SuggestionsHandler {
/**
* Creates a configuration object used to create a [[SuggestionsHandler]].
*
* @param repo the suggestions repo
*/
def props(repo: SuggestionsRepo[Future]): Props =
Props(new SuggestionsHandler(repo))
}

View File

@ -1,6 +1,4 @@
akka.http.server.idle-timeout = infinite
akka.http.server.remote-address-header = on
akka.http.server.websocket.periodic-keep-alive-max-idle = 1 second
akka {
loglevel = "ERROR"
}
akka.loglevel = "ERROR"

View File

@ -0,0 +1,44 @@
package org.enso.languageserver.runtime
import org.enso.searcher.Suggestion
/** Suggestion instances used in tests. */
object Suggestions {
val atom: Suggestion.Atom =
Suggestion.Atom(
name = "MyType",
arguments = Vector(Suggestion.Argument("a", "Any", false, false, None)),
returnType = "MyAtom",
documentation = None
)
val method: Suggestion.Method =
Suggestion.Method(
name = "foo",
arguments = Vector(
Suggestion.Argument("this", "MyType", false, false, None),
Suggestion.Argument("foo", "Number", false, true, Some("42"))
),
selfType = "MyType",
returnType = "Number",
documentation = Some("Lovely")
)
val function: Suggestion.Function =
Suggestion.Function(
name = "print",
arguments = Vector(),
returnType = "IO",
scope = Suggestion.Scope(9, 22)
)
val local: Suggestion.Local =
Suggestion.Local(
name = "x",
returnType = "Number",
scope = Suggestion.Scope(34, 68)
)
val all = Seq(atom, method, function, local)
}

View File

@ -0,0 +1,148 @@
package org.enso.languageserver.runtime
import java.nio.file.Files
import java.util.UUID
import akka.actor.{ActorRef, ActorSystem}
import akka.testkit.{ImplicitSender, TestKit, TestProbe}
import org.enso.jsonrpc.test.RetrySpec
import org.enso.languageserver.capability.CapabilityProtocol.{
AcquireCapability,
CapabilityAcquired
}
import org.enso.languageserver.data.{
CapabilityRegistration,
ReceivesSuggestionsDatabaseUpdates
}
import org.enso.languageserver.session.JsonSession
import org.enso.languageserver.session.SessionRouter.DeliverToJsonController
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.searcher.SuggestionsRepo
import org.enso.searcher.sql.SqlSuggestionsRepo
import org.scalatest.BeforeAndAfterAll
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
class SuggestionsDatabaseEventsListenerTest
extends TestKit(ActorSystem("TestSystem"))
with ImplicitSender
with AnyWordSpecLike
with Matchers
with BeforeAndAfterAll
with RetrySpec {
import system.dispatcher
val Timeout: FiniteDuration = 5.seconds
override def afterAll(): Unit = {
TestKit.shutdownActorSystem(system)
}
"SuggestionsHandler" should {
"subscribe to notification updates" taggedAs Retry() in withDb {
(router, repo) =>
val handler = newEventsListener(router.ref, repo)
val clientId = UUID.randomUUID()
handler ! AcquireCapability(
newJsonSession(clientId),
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
)
expectMsg(CapabilityAcquired)
}
"receive runtime updates" taggedAs Retry() in withDb { (router, repo) =>
val handler = newEventsListener(router.ref, repo)
val clientId = UUID.randomUUID()
// acquire capability
handler ! AcquireCapability(
newJsonSession(clientId),
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
)
expectMsg(CapabilityAcquired)
// receive updates
handler ! Api.SuggestionsDatabaseUpdateNotification(
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Add)
)
val updates = Suggestions.all.zipWithIndex.map {
case (suggestion, ix) =>
SearchProtocol.SuggestionsDatabaseUpdate.Add(ix + 1L, suggestion)
}
router.expectMsg(
DeliverToJsonController(
clientId,
SearchProtocol.SuggestionsDatabaseUpdateNotification(updates, 4L)
)
)
// check database entries exist
val (_, records) = Await.result(repo.getAll, Timeout)
records.map(_.suggestion) should contain theSameElementsAs Suggestions.all
}
"apply runtime updates in correct order" taggedAs Retry() in withDb {
(router, repo) =>
val handler = newEventsListener(router.ref, repo)
val clientId = UUID.randomUUID()
// acquire capability
handler ! AcquireCapability(
newJsonSession(clientId),
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
)
expectMsg(CapabilityAcquired)
// receive updates
handler ! Api.SuggestionsDatabaseUpdateNotification(
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Add) ++
Suggestions.all.map(Api.SuggestionsDatabaseUpdate.Remove)
)
val updates = Suggestions.all.zipWithIndex.map {
case (suggestion, ix) =>
SearchProtocol.SuggestionsDatabaseUpdate.Add(ix + 1L, suggestion)
}
router.expectMsg(
DeliverToJsonController(
clientId,
SearchProtocol.SuggestionsDatabaseUpdateNotification(updates, 4L)
)
)
// check that database entries removed
val (_, all) = Await.result(repo.getAll, Timeout)
all.map(_.suggestion) should contain theSameElementsAs Suggestions.all
}
}
def newEventsListener(
router: ActorRef,
repo: SuggestionsRepo[Future]
): ActorRef =
system.actorOf(SuggestionsDatabaseEventsListener.props(router, repo))
def newJsonSession(clientId: UUID): JsonSession =
JsonSession(clientId, TestProbe().ref)
def withDb(
test: (TestProbe, SuggestionsRepo[Future]) => Any
): Unit = {
val dbPath = Files.createTempFile("suggestions", ".db")
system.registerOnTermination(Files.deleteIfExists(dbPath))
val repo = SqlSuggestionsRepo()
Await.ready(repo.init, Timeout)
val router = TestProbe("sessionRouterProbe")
try test(router, repo)
finally repo.close()
}
}

View File

@ -0,0 +1,146 @@
package org.enso.languageserver.runtime
import java.nio.file.Files
import akka.actor.{ActorRef, ActorSystem}
import akka.testkit.{ImplicitSender, TestKit}
import org.enso.jsonrpc.test.RetrySpec
import org.enso.searcher.SuggestionsRepo
import org.enso.searcher.sql.SqlSuggestionsRepo
import org.enso.text.editing.model.Position
import org.scalatest.BeforeAndAfterAll
import org.scalatest.wordspec.AnyWordSpecLike
import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
class SuggestionsHandlerTest
extends TestKit(ActorSystem("TestSystem"))
with ImplicitSender
with AnyWordSpecLike
with BeforeAndAfterAll
with RetrySpec {
import system.dispatcher
val Timeout: FiniteDuration = 10.seconds
override def afterAll(): Unit = {
TestKit.shutdownActorSystem(system)
}
"SuggestionsHandler" should {
"get initial suggestions database version" in withDb { repo =>
val handler = newSuggestionsHandler(repo)
handler ! SearchProtocol.GetSuggestionsDatabaseVersion
expectMsg(SearchProtocol.GetSuggestionsDatabaseVersionResult(0))
}
"get suggestions database version" in withDb { repo =>
val handler = newSuggestionsHandler(repo)
Await.ready(repo.insert(Suggestions.atom), Timeout)
handler ! SearchProtocol.GetSuggestionsDatabaseVersion
expectMsg(SearchProtocol.GetSuggestionsDatabaseVersionResult(1))
}
"get initial suggestions database" in withDb { repo =>
val handler = newSuggestionsHandler(repo)
handler ! SearchProtocol.GetSuggestionsDatabase
expectMsg(SearchProtocol.GetSuggestionsDatabaseResult(Seq(), 0))
}
"get suggestions database" in withDb { repo =>
val handler = newSuggestionsHandler(repo)
Await.ready(repo.insert(Suggestions.atom), Timeout)
handler ! SearchProtocol.GetSuggestionsDatabase
expectMsg(
SearchProtocol.GetSuggestionsDatabaseResult(
Seq(
SearchProtocol.SuggestionsDatabaseUpdate.Add(1L, Suggestions.atom)
),
1
)
)
}
"search entries by empty search query" taggedAs Retry() in withDb { repo =>
val handler = newSuggestionsHandler(repo)
Await.ready(repo.insertAll(Suggestions.all), Timeout)
handler ! SearchProtocol.Completion(
module = "Test.Main",
position = Position(0, 0),
selfType = None,
returnType = None,
tags = None
)
expectMsg(SearchProtocol.CompletionResult(4L, Seq()))
}
"search entries by self type" taggedAs Retry() in withDb { repo =>
val handler = newSuggestionsHandler(repo)
val (_, Seq(_, methodId, _, _)) =
Await.result(repo.insertAll(Suggestions.all), Timeout)
handler ! SearchProtocol.Completion(
module = "Test.Main",
position = Position(0, 0),
selfType = Some("MyType"),
returnType = None,
tags = None
)
expectMsg(SearchProtocol.CompletionResult(4L, Seq(methodId).flatten))
}
"search entries by return type" taggedAs Retry() in withDb { repo =>
val handler = newSuggestionsHandler(repo)
val (_, Seq(_, _, functionId, _)) =
Await.result(repo.insertAll(Suggestions.all), Timeout)
handler ! SearchProtocol.Completion(
module = "Test.Main",
position = Position(0, 0),
selfType = None,
returnType = Some("IO"),
tags = None
)
expectMsg(SearchProtocol.CompletionResult(4L, Seq(functionId).flatten))
}
"search entries by tags" taggedAs Retry() in withDb { repo =>
val handler = newSuggestionsHandler(repo)
val (_, Seq(_, _, _, localId)) =
Await.result(repo.insertAll(Suggestions.all), Timeout)
handler ! SearchProtocol.Completion(
module = "Test.Main",
position = Position(0, 0),
selfType = None,
returnType = None,
tags = Some(Seq(SearchProtocol.SuggestionKind.Local))
)
expectMsg(SearchProtocol.CompletionResult(4L, Seq(localId).flatten))
}
}
def newSuggestionsHandler(repo: SuggestionsRepo[Future]): ActorRef =
system.actorOf(SuggestionsHandler.props(repo))
def withDb(test: SuggestionsRepo[Future] => Any): Unit = {
val dbPath = Files.createTempFile("suggestions", ".db")
system.registerOnTermination(Files.deleteIfExists(dbPath))
val repo = SqlSuggestionsRepo()
Await.ready(repo.init, Timeout)
try test(repo)
finally repo.close()
}
}

View File

@ -15,28 +15,28 @@ import org.enso.languageserver.filemanager.{
FileSystem,
ReceivesTreeUpdatesHandler
}
import org.enso.languageserver.io.{
InputRedirectionController,
ObservableOutputStream,
ObservablePipedInputStream,
OutputKind,
OutputRedirectionController
}
import org.enso.languageserver.io._
import org.enso.languageserver.protocol.json.{
JsonConnectionControllerFactory,
JsonRpc
}
import org.enso.languageserver.runtime.{
ContextRegistry,
SuggestionsDatabaseEventsListener
SuggestionsDatabaseEventsListener,
SuggestionsHandler
}
import org.enso.languageserver.session.SessionRouter
import org.enso.languageserver.text.BufferRegistry
import org.enso.searcher.sql.SqlSuggestionsRepo
import scala.concurrent.Await
import scala.concurrent.duration._
class BaseServerTest extends JsonRpcServerTestKit {
val timeout: FiniteDuration = 3.seconds
val testSuggestionsDbPath = Files.createTempFile("suggestions", ".db")
val testContentRoot = Files.createTempDirectory(null).toRealPath()
val testContentRootId = UUID.randomUUID()
val config = Config(
@ -48,6 +48,7 @@ class BaseServerTest extends JsonRpcServerTestKit {
val runtimeConnectorProbe = TestProbe()
testContentRoot.toFile.deleteOnExit()
testSuggestionsDbPath.toFile.deleteOnExit()
override def protocol: Protocol = JsonRpc.protocol
@ -78,6 +79,9 @@ class BaseServerTest extends JsonRpcServerTestKit {
override def clientControllerFactory: ClientControllerFactory = {
val zioExec = ZioExec(zio.Runtime.default)
val suggestionsRepo = SqlSuggestionsRepo()(system.dispatcher)
Await.ready(suggestionsRepo.init, timeout)
val fileManager =
system.actorOf(FileManager.props(config, new FileSystem, zioExec))
val bufferRegistry =
@ -97,7 +101,9 @@ class BaseServerTest extends JsonRpcServerTestKit {
)
val suggestionsDatabaseEventsListener =
system.actorOf(SuggestionsDatabaseEventsListener.props(sessionRouter))
system.actorOf(
SuggestionsDatabaseEventsListener.props(sessionRouter, suggestionsRepo)
)
val capabilityRouter =
system.actorOf(
@ -108,11 +114,15 @@ class BaseServerTest extends JsonRpcServerTestKit {
)
)
val suggestionsHandler =
system.actorOf(SuggestionsHandler.props(suggestionsRepo))
new JsonConnectionControllerFactory(
bufferRegistry,
capabilityRouter,
fileManager,
contextRegistry,
suggestionsHandler,
stdOutController,
stdErrController,
stdInController,

View File

@ -3,16 +3,17 @@ package org.enso.languageserver.websocket.json
import java.io.File
import io.circe.literal._
import org.enso.jsonrpc.test.RetrySpec
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.text.editing.model.{Position, Range, TextEdit}
class FileNotificationsTest extends BaseServerTest {
class FileNotificationsTest extends BaseServerTest with RetrySpec {
def file(name: String): File = new File(testContentRoot.toFile, name)
"text operations" should {
"notify runtime about operations with files" in {
"notify runtime about operations with files" taggedAs Retry() in {
// Interaction:
// 1. Client 1 creates a file.
// 2. Client 1 opens the file.

View File

@ -0,0 +1,71 @@
package org.enso.languageserver.websocket.json
import io.circe.literal._
object SearchJsonMessages {
def acquireSuggestionsDatabaseUpdatesCapability(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"method": "capability/acquire",
"id": $reqId,
"params": {
"method": "search/receivesSuggestionsDatabaseUpdates",
"registerOptions": {}
}
}
"""
def releaseSuggestionsDatabaseUpdatesCapability(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"method": "capability/release",
"id": $reqId,
"params": {
"method": "search/receivesSuggestionsDatabaseUpdates",
"registerOptions": {}
}
}
"""
def getSuggestionsDatabaseVersion(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"method": "search/getSuggestionsDatabaseVersion",
"id": $reqId,
"params": null
}
"""
def getSuggestionsDatabase(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"method": "search/getSuggestionsDatabase",
"id": $reqId,
"params": null
}
"""
def completion(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"method": "search/completion",
"id": $reqId,
"params": {
"module": "Test.Main",
"position": {
"line": 0,
"character": 0
}
}
}
"""
def ok(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"id": $reqId,
"result": null
}
"""
}

View File

@ -1,32 +1,30 @@
package org.enso.languageserver.websocket.json
import io.circe.literal._
import org.enso.jsonrpc.test.FlakySpec
import org.enso.languageserver.runtime.Suggestions
import org.enso.languageserver.websocket.json.{SearchJsonMessages => json}
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.searcher.Suggestion
import org.scalatest.BeforeAndAfter
class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
class SuggestionsDatabaseEventsListenerTest
extends BaseServerTest
with BeforeAndAfter
with FlakySpec {
lazy val client = getInitialisedWsClient()
"SuggestionsDatabaseEventListener" must {
"acquire and release capabilities" in {
val client = getInitialisedWsClient()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
client.send(json.releaseSuggestionsDatabaseUpdatesCapability(1))
client.expectJson(json.ok(1))
}
"send suggestions database add atom notifications" in {
val client = getInitialisedWsClient()
"send suggestions database notifications" taggedAs Flaky in {
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
// add atom
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.atom))
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.atom))
)
)
client.expectJson(json"""
@ -36,7 +34,7 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
"updates" : [
{
"type" : "Add",
"id" : 0,
"id" : 1,
"suggestion" : {
"type" : "atom",
"name" : "MyType",
@ -53,21 +51,15 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
}
}
],
"currentVersion" : 0
"currentVersion" : 1
}
}
""")
}
"send suggestions database add method notifications" in {
val client = getInitialisedWsClient()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
// add method
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.method))
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.method))
)
)
client.expectJson(json"""
@ -77,7 +69,7 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
"updates" : [
{
"type" : "Add",
"id" : 0,
"id" : 2,
"suggestion" : {
"type" : "method",
"name" : "foo",
@ -99,25 +91,19 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
],
"selfType" : "MyType",
"returnType" : "Number",
"documentation" : "My doc"
"documentation" : "Lovely"
}
}
],
"currentVersion" : 0
"currentVersion" : 2
}
}
""")
}
"send suggestions database add function notifications" in {
val client = getInitialisedWsClient()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
// add function
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.function))
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.function))
)
)
client.expectJson(json"""
@ -127,7 +113,7 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
"updates" : [
{
"type" : "Add",
"id" : 0,
"id" : 3,
"suggestion" : {
"type" : "function",
"name" : "print",
@ -135,27 +121,21 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
],
"returnType" : "IO",
"scope" : {
"start" : 7,
"end" : 10
"start" : 9,
"end" : 22
}
}
}
],
"currentVersion" : 0
"currentVersion" : 3
}
}
""")
}
"send suggestions database add local notifications" in {
val client = getInitialisedWsClient()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
// add local
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.local))
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.local))
)
)
client.expectJson(json"""
@ -165,47 +145,29 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
"updates" : [
{
"type" : "Add",
"id" : 0,
"id" : 4,
"suggestion" : {
"type" : "local",
"name" : "x",
"returnType" : "Number",
"scope" : {
"start" : 15,
"end" : 17
"start" : 34,
"end" : 68
}
}
}
],
"currentVersion" : 0
"currentVersion" : 4
}
}
""")
}
"send suggestions database modify notifications" in {
val client = getInitialisedWsClient()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
// remove items
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Seq(
Api.SuggestionsDatabaseUpdate.Modify(
id = 0,
name = Some("foo"),
arguments = Some(
Seq(
Suggestion.Argument("a", "Any", true, false, None),
Suggestion.Argument("b", "Any", false, true, Some("77"))
)
),
selfType = Some("MyType"),
returnType = Some("IO"),
documentation = None,
scope = Some(Suggestion.Scope(12, 24))
)
Api.SuggestionsDatabaseUpdate.Remove(Suggestions.method),
Api.SuggestionsDatabaseUpdate.Remove(Suggestions.function)
)
)
)
@ -215,138 +177,20 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
"params" : {
"updates" : [
{
"type" : "Update",
"id" : 0,
"name" : "foo",
"arguments" : [
{
"name" : "a",
"reprType" : "Any",
"isSuspended" : true,
"hasDefault" : false,
"defaultValue" : null
"type" : "Remove",
"id" : 2
},
{
"name" : "b",
"reprType" : "Any",
"isSuspended" : false,
"hasDefault" : true,
"defaultValue" : "77"
"type" : "Remove",
"id" : 3
}
],
"selfType" : "MyType",
"returnType" : "IO",
"scope" : {
"start" : 12,
"end" : 24
}
}
],
"currentVersion" : 0
"currentVersion" : 6
}
}
""")
}
"send suggestions database remove notifications" in {
val client = getInitialisedWsClient()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Seq(Api.SuggestionsDatabaseUpdate.Remove(101))
)
)
client.expectJson(json"""
{ "jsonrpc" : "2.0",
"method" : "search/suggestionsDatabaseUpdates",
"params" : {
"updates" : [
{
"type" : "Delete",
"id" : 101
}
],
"currentVersion" : 0
}
}
""")
}
}
object suggestion {
val atom: Suggestion.Atom =
Suggestion.Atom(
name = "MyType",
arguments = Seq(Suggestion.Argument("a", "Any", false, false, None)),
returnType = "MyAtom",
documentation = None
)
val method: Suggestion.Method =
Suggestion.Method(
name = "foo",
arguments = Seq(
Suggestion.Argument("this", "MyType", false, false, None),
Suggestion.Argument("foo", "Number", false, true, Some("42"))
),
selfType = "MyType",
returnType = "Number",
documentation = Some("My doc")
)
val function: Suggestion.Function =
Suggestion.Function(
name = "print",
arguments = Seq(),
returnType = "IO",
scope = Suggestion.Scope(7, 10)
)
val local: Suggestion.Local =
Suggestion.Local(
name = "x",
returnType = "Number",
scope = Suggestion.Scope(15, 17)
)
}
object json {
def acquireSuggestionsDatabaseUpdatesCapability(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"method": "capability/acquire",
"id": $reqId,
"params": {
"method": "search/receivesSuggestionsDatabaseUpdates",
"registerOptions": {}
}
}
"""
def releaseSuggestionsDatabaseUpdatesCapability(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"method": "capability/release",
"id": $reqId,
"params": {
"method": "search/receivesSuggestionsDatabaseUpdates",
"registerOptions": {}
}
}
"""
def ok(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"id": $reqId,
"result": null
}
"""
}
}

View File

@ -0,0 +1,56 @@
package org.enso.languageserver.websocket.json
import io.circe.literal._
import org.enso.languageserver.websocket.json.{SearchJsonMessages => json}
class SuggestionsHandlerTest extends BaseServerTest {
"SuggestionsHandler" must {
"get initial suggestions database version" in {
val client = getInitialisedWsClient()
client.send(json.getSuggestionsDatabaseVersion(0))
client.expectJson(json"""
{ "jsonrpc" : "2.0",
"id" : 0,
"result" : {
"version" : 0
}
}
""")
}
"get initial suggestions database" in {
val client = getInitialisedWsClient()
client.send(json.getSuggestionsDatabase(0))
client.expectJson(json"""
{ "jsonrpc" : "2.0",
"id" : 0,
"result" : {
"entries" : [
],
"currentVersion" : 0
}
}
""")
}
"reply to completion request" in {
val client = getInitialisedWsClient()
client.send(json.completion(0))
client.expectJson(json"""
{ "jsonrpc" : "2.0",
"id" : 0,
"result" : {
"results" : [
],
"currentVersion" : 0
}
}
""")
}
}
}

View File

@ -321,10 +321,6 @@ object Runtime {
new JsonSubTypes.Type(
value = classOf[SuggestionsDatabaseUpdate.Remove],
name = "suggestionsDatabaseUpdateRemove"
),
new JsonSubTypes.Type(
value = classOf[SuggestionsDatabaseUpdate.Modify],
name = "suggestionsDatabaseUpdateModify"
)
)
)
@ -333,37 +329,16 @@ object Runtime {
/** Create or replace the database entry.
*
* @param id suggestion id
* @param suggestion the new suggestion
*/
case class Add(id: Long, suggestion: Suggestion)
extends SuggestionsDatabaseUpdate
case class Add(suggestion: Suggestion) extends SuggestionsDatabaseUpdate
/** Remove the database entry.
*
* @param id the suggestion id
* @param suggestion the suggestion to remove
*/
case class Remove(id: Long) extends SuggestionsDatabaseUpdate
/** Modify the database entry.
*
* @param id the suggestion id
* @param name the new suggestion name
* @param arguments the new suggestion arguments
* @param selfType the new self type of the suggestion
* @param returnType the new return type of the suggestion
* @param documentation the new documentation string
* @param scope the suggestion scope
*/
case class Modify(
id: Long,
name: Option[String],
arguments: Option[Seq[Suggestion.Argument]],
selfType: Option[String],
returnType: Option[String],
documentation: Option[String],
scope: Option[Suggestion.Scope]
) extends SuggestionsDatabaseUpdate
case class Remove(suggestion: Suggestion)
extends SuggestionsDatabaseUpdate
}
/**

View File

@ -247,7 +247,7 @@ class ProjectManagementApiSpec
""")
}
"start the Language Server if not running" in {
"start the Language Server if not running" taggedAs Flaky in {
//given
val projectName = "to-remove"
implicit val client = new WsTestClient(address)

View File

@ -0,0 +1,101 @@
package org.enso.searcher.sql;
import org.enso.searcher.Suggestion;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import scala.collection.immutable.Seq;
import scala.concurrent.Await;
import scala.concurrent.ExecutionContext;
import scala.concurrent.duration.Duration;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Stream;
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class SuggestionsRepoBenchmark {
static final int DATABASE_SIZE = 1000000;
static final Duration TIMEOUT = Duration.apply(3, TimeUnit.SECONDS);
final Path dbfile = Path.of(System.getProperty("java.io.tmpdir"), "bench-suggestions.db");
final Seq<Suggestion.Kind> kinds = SuggestionRandom.nextKinds();
SqlSuggestionsRepo repo;
@Setup
public void setup() throws TimeoutException, InterruptedException {
repo = SqlSuggestionsRepo.apply(dbfile, ExecutionContext.global());
if (Files.notExists(dbfile)) {
System.out.println("initializing " + dbfile.toString() + " ...");
Await.ready(repo.init(), TIMEOUT);
System.out.println("inserting records...");
int size = 0;
while (size < DATABASE_SIZE) {
size = insertBatch(20000);
}
System.out.println("created " + size + " records");
}
}
@TearDown
public void tearDown() {
repo.close();
}
int insertBatch(int size) throws TimeoutException, InterruptedException {
Suggestion[] stubs =
Stream.generate(SuggestionRandom::nextSuggestion).limit(size).toArray(Suggestion[]::new);
return (int) Await.result(repo.insertBatch(stubs), TIMEOUT);
}
static <T> scala.Option<T> none() {
return (scala.Option<T>) scala.None$.MODULE$;
}
@Benchmark
public Object searchBaseline() throws TimeoutException, InterruptedException {
return Await.result(repo.search(none(), none(), none()), TIMEOUT);
}
@Benchmark
public Object searchByReturnType() throws TimeoutException, InterruptedException {
return Await.result(repo.search(none(), scala.Some.apply("MyType"), none()), TIMEOUT);
}
@Benchmark
public Object searchBySelfType() throws TimeoutException, InterruptedException {
return Await.result(repo.search(scala.Some.apply("MyType"), none(), none()), TIMEOUT);
}
@Benchmark
public Object searchBySelfReturnTypes() throws TimeoutException, InterruptedException {
return Await.result(
repo.search(scala.Some.apply("SelfType"), scala.Some.apply("ReturnType"), none()), TIMEOUT);
}
@Benchmark
public Object searchByAll() throws TimeoutException, InterruptedException {
return Await.result(
repo.search(
scala.Some.apply("SelfType"), scala.Some.apply("ReturnType"), scala.Some.apply(kinds)),
TIMEOUT);
}
public static void main(String[] args) throws RunnerException {
Options opt =
new OptionsBuilder().include(SuggestionsRepoBenchmark.class.getSimpleName()).build();
new Runner(opt).run();
}
}

View File

@ -0,0 +1,73 @@
package org.enso.searcher.sql
import org.enso.searcher.Suggestion
import scala.util.Random
object SuggestionRandom {
def nextKinds(): Seq[Suggestion.Kind] =
Set.fill(1)(nextKind()).toSeq
def nextSuggestion(): Suggestion = {
nextKind() match {
case Suggestion.Kind.Atom => nextSuggestionAtom()
case Suggestion.Kind.Method => nextSuggestionMethod()
case Suggestion.Kind.Function => nextSuggestionFunction()
case Suggestion.Kind.Local => nextSuggestionLocal()
}
}
def nextSuggestionAtom(): Suggestion.Atom =
Suggestion.Atom(
name = nextString(),
arguments = Seq(),
returnType = nextString(),
documentation = optional(nextString())
)
def nextSuggestionMethod(): Suggestion.Method =
Suggestion.Method(
name = nextString(),
arguments = Seq(),
selfType = nextString(),
returnType = nextString(),
documentation = optional(nextString())
)
def nextSuggestionFunction(): Suggestion.Function =
Suggestion.Function(
name = nextString(),
arguments = Seq(),
returnType = nextString(),
scope = nextScope()
)
def nextSuggestionLocal(): Suggestion.Local =
Suggestion.Local(
name = nextString(),
returnType = nextString(),
scope = nextScope()
)
def nextScope(): Suggestion.Scope =
Suggestion.Scope(
start = Random.nextInt(Int.MaxValue),
end = Random.nextInt(Int.MaxValue)
)
def nextKind(): Suggestion.Kind =
Random.nextInt(4) match {
case 0 => Suggestion.Kind.Atom
case 1 => Suggestion.Kind.Method
case 2 => Suggestion.Kind.Function
case 3 => Suggestion.Kind.Local
case x => throw new NoSuchElementException(s"nextKind: $x")
}
def nextString(): String =
Random.nextString(Random.nextInt(26) + 5)
def optional[A](f: => A): Option[A] =
Option.when(Random.nextBoolean())(f)
}

View File

@ -1,8 +0,0 @@
searcher {
db {
url = "jdbc:sqlite:searcher.db"
driver = org.sqlite.JDBC
connectionPool = disabled
keepAliveConnection = true
}
}

View File

@ -0,0 +1,7 @@
searcher {
db {
url = "jdbc:sqlite::memory:"
driver = "org.sqlite.JDBC"
connectionPool = "HikariCP"
}
}

View File

@ -0,0 +1,21 @@
package org.enso.searcher
/** The database queries
*
* @tparam F the type of the query
* @tparam G the type of the result
*/
trait Database[F[_], G[_]] {
/** Run the database query.
*
* @param query the query to run
*/
def run[A](query: F[A]): G[A]
/** Run the database query in one transaction */
def transaction[A](query: F[A]): G[A]
/** Close the database. */
def close(): Unit
}

View File

@ -4,6 +4,23 @@ package org.enso.searcher
sealed trait Suggestion
object Suggestion {
/** The type of a suggestion. */
sealed trait Kind
object Kind {
/** The atom suggestion. */
case object Atom extends Kind
/** The method suggestion. */
case object Method extends Kind
/** The function suggestion. */
case object Function extends Kind
/** The suggestion of a local value. */
case object Local extends Kind
}
/** An argument of an atom or a function.
*
* @param name the argument name

View File

@ -0,0 +1,8 @@
package org.enso.searcher
/** The entry in the suggestions database.
*
* @param id the suggestion id
* @param suggestion the suggestion
*/
case class SuggestionEntry(id: Long, suggestion: Suggestion)

View File

@ -1,16 +1,35 @@
package org.enso.searcher.sql
import org.enso.searcher.Suggestion
package org.enso.searcher
/** The object for accessing the suggestions database. */
trait SuggestionsRepo[F[_]] {
/** Find suggestions by the return type.
/** Initialize the repo. */
def init: F[Unit]
/** Clean the repo. */
def clean: F[Unit]
/** Get current version of the repo. */
def currentVersion: F[Long]
/** Get all suggestions.
*
* @param returnType the return type of a suggestion
* @return the list of suggestions
* @return the current database version and the list of suggestions
*/
def findBy(returnType: String): F[Seq[Suggestion]]
def getAll: F[(Long, Seq[SuggestionEntry])]
/** Search suggestion by various parameters.
*
* @param selfType the selfType search parameter
* @param returnType the returnType search parameter
* @param kinds the list suggestion kinds to search
* @return the current database version and the list of found suggestion ids
*/
def search(
selfType: Option[String],
returnType: Option[String],
kinds: Option[Seq[Suggestion.Kind]]
): F[(Long, Seq[Long])]
/** Select the suggestion by id.
*
@ -24,5 +43,26 @@ trait SuggestionsRepo[F[_]] {
* @param suggestion the suggestion to insert
* @return the id of an inserted suggestion
*/
def insert(suggestion: Suggestion): F[Long]
def insert(suggestion: Suggestion): F[Option[Long]]
/** Insert a list of suggestions
*
* @param suggestions the suggestions to insert
* @return the current database version and a list of inserted suggestion ids
*/
def insertAll(suggestions: Seq[Suggestion]): F[(Long, Seq[Option[Long]])]
/** Remove the suggestion.
*
* @param suggestion the suggestion to remove
* @return the id of removed suggestion
*/
def remove(suggestion: Suggestion): F[Option[Long]]
/** Remove a list of suggestions.
*
* @param suggestions the suggestions to remove
* @return the current database version and a list of removed suggestion ids
*/
def removeAll(suggestions: Seq[Suggestion]): F[(Long, Seq[Option[Long]])]
}

View File

@ -0,0 +1,58 @@
package org.enso.searcher.sql
import com.typesafe.config.{Config, ConfigFactory}
import org.enso.searcher.Database
import slick.dbio.DBIO
import slick.jdbc.SQLiteProfile
import slick.jdbc.SQLiteProfile.api._
import scala.concurrent.Future
/** Ths SQL database that runs Slick [[DBIO]] queries resulting in a [[Future]].
*
* @param config the configuration
*/
final private[sql] class SqlDatabase(config: Option[Config] = None)
extends Database[DBIO, Future] {
val db = SQLiteProfile.backend.Database
.forConfig(SqlDatabase.configPath, config.orNull)
/** @inheritdoc */
override def run[A](query: DBIO[A]): Future[A] =
db.run(query)
/** @inheritdoc */
override def transaction[A](query: DBIO[A]): Future[A] =
db.run(query.transactionally)
/** @inheritdoc */
override def close(): Unit =
db.close()
}
object SqlDatabase {
private val configPath: String =
"searcher.db"
/** Create [[SqlDatabase]] instance.
*
* @param filename the database file path
* @return new sql database instance
*/
def apply(filename: String): SqlDatabase = {
val config = ConfigFactory
.parseString(s"""$configPath.url = "${jdbcUrl(filename)}"""")
.withFallback(ConfigFactory.load())
new SqlDatabase(Some(config))
}
/** Create JDBC URL from the file path. */
private def jdbcUrl(filename: String): String =
s"jdbc:sqlite:${escapePath(filename)}"
/** Escape Windows path. */
private def escapePath(path: String): String =
path.replace("\\", "\\\\")
}

View File

@ -1,53 +1,265 @@
package org.enso.searcher.sql
import org.enso.searcher.Suggestion
import java.nio.file.Path
import org.enso.searcher.{Suggestion, SuggestionEntry, SuggestionsRepo}
import slick.jdbc.SQLiteProfile.api._
import scala.concurrent.ExecutionContext
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
/** The object for accessing the suggestions database. */
final class SqlSuggestionsRepo(implicit ec: ExecutionContext)
extends SuggestionsRepo[DBIO] {
final class SqlSuggestionsRepo private (db: SqlDatabase)(implicit
ec: ExecutionContext
) extends SuggestionsRepo[Future] {
/** The query returning the arguments joined with the corresponding
* suggestions. */
* suggestions.
*/
private val joined: Query[
(Rep[Option[ArgumentsTable]], SuggestionsTable),
(Option[ArgumentRow], SuggestionRow),
Seq
] =
arguments
.joinRight(suggestions)
Arguments
.joinRight(Suggestions)
.on(_.suggestionId === _.id)
/** @inheritdoc **/
override def findBy(returnType: String): DBIO[Seq[Suggestion]] = {
/** @inheritdoc */
override def init: Future[Unit] =
db.run(initQuery)
/** @inheritdoc */
override def clean: Future[Unit] =
db.run(cleanQuery)
/** @inheritdoc */
override def getAll: Future[(Long, Seq[SuggestionEntry])] =
db.run(getAllQuery)
/** @inheritdoc */
override def search(
selfType: Option[String],
returnType: Option[String],
kinds: Option[Seq[Suggestion.Kind]]
): Future[(Long, Seq[Long])] =
db.run(searchQuery(selfType, returnType, kinds))
/** @inheritdoc */
override def select(id: Long): Future[Option[Suggestion]] =
db.run(selectQuery(id))
/** @inheritdoc */
override def insert(suggestion: Suggestion): Future[Option[Long]] =
db.run(insertQuery(suggestion))
/** @inheritdoc */
override def insertAll(
suggestions: Seq[Suggestion]
): Future[(Long, Seq[Option[Long]])] =
db.run(insertAllQuery(suggestions))
/** @inheritdoc */
override def remove(suggestion: Suggestion): Future[Option[Long]] =
db.run(removeQuery(suggestion))
/** @inheritdoc */
override def removeAll(
suggestions: Seq[Suggestion]
): Future[(Long, Seq[Option[Long]])] =
db.run(removeAllQuery(suggestions))
/** @inheritdoc */
override def currentVersion: Future[Long] =
db.run(currentVersionQuery)
/** Close the database. */
def close(): Unit =
db.close()
/** Insert suggestions in a batch. */
private[sql] def insertBatch(suggestions: Array[Suggestion]): Future[Int] =
db.run(insertBatchQuery(suggestions))
/** The query to initialize the repo. */
private def initQuery: DBIO[Unit] =
(Suggestions.schema ++ Arguments.schema ++ Versions.schema).createIfNotExists
/** The query to clean the repo. */
private def cleanQuery: DBIO[Unit] =
for {
_ <- Suggestions.delete
_ <- Arguments.delete
_ <- Versions.delete
} yield ()
/** Get all suggestions. */
private def getAllQuery: DBIO[(Long, Seq[SuggestionEntry])] = {
val query = for {
(argument, suggestion) <- joined
if suggestion.returnType === returnType
} yield (argument, suggestion)
query.result.map(joinedToSuggestion)
suggestions <- joined.result.map(joinedToSuggestionEntries)
version <- currentVersionQuery
} yield (version, suggestions)
query.transactionally
}
/** @inheritdoc **/
override def select(id: Long): DBIO[Option[Suggestion]] = {
/** The query to search suggestion by various parameters.
*
* @param selfType the selfType search parameter
* @param returnType the returnType search parameter
* @param kinds the list suggestion kinds to search
* @return the list of suggestion ids
*/
private def searchQuery(
selfType: Option[String],
returnType: Option[String],
kinds: Option[Seq[Suggestion.Kind]]
): DBIO[(Long, Seq[Long])] = {
val searchAction =
if (selfType.isEmpty && returnType.isEmpty && kinds.isEmpty) {
DBIO.successful(Seq())
} else {
val query = searchQueryBuilder(selfType, returnType, kinds).map(_.id)
query.result
}
val query = for {
results <- searchAction
version <- currentVersionQuery
} yield (version, results)
query.transactionally
}
/** The query to select the suggestion by id.
*
* @param id the id of a suggestion
* @return return the suggestion
*/
private def selectQuery(id: Long): DBIO[Option[Suggestion]] = {
val query = for {
(argument, suggestion) <- joined
if suggestion.id === id
} yield (argument, suggestion)
query.result.map(coll => joinedToSuggestion(coll).headOption)
query.result.map(coll => joinedToSuggestions(coll).headOption)
}
/** @inheritdoc **/
override def insert(suggestion: Suggestion): DBIO[Long] = {
/** The query to insert the suggestion
*
* @param suggestion the suggestion to insert
* @return the id of an inserted suggestion
*/
private def insertQuery(suggestion: Suggestion): DBIO[Option[Long]] = {
val (suggestionRow, args) = toSuggestionRow(suggestion)
for {
id <- suggestions.returning(suggestions.map(_.id)) += suggestionRow
_ <- arguments ++= args.map(toArgumentRow(id, _))
val query = for {
id <- Suggestions.returning(Suggestions.map(_.id)) += suggestionRow
_ <- Arguments ++= args.zipWithIndex.map {
case (argument, ix) => toArgumentRow(id, ix, argument)
}
_ <- incrementVersionQuery
} yield id
query.transactionally.asTry.map {
case Failure(_) => None
case Success(id) => Some(id)
}
}
private def joinedToSuggestion(
/** The query to insert a list of suggestions
*
* @param suggestions the suggestions to insert
* @return the list of inserted suggestion ids
*/
private def insertAllQuery(
suggestions: Seq[Suggestion]
): DBIO[(Long, Seq[Option[Long]])] = {
val query = for {
ids <- DBIO.sequence(suggestions.map(insertQuery))
version <- currentVersionQuery
} yield (version, ids)
query.transactionally
}
/** The query to remove the suggestion.
*
* @param suggestion the suggestion to remove
* @return the id of removed suggestion
*/
private def removeQuery(suggestion: Suggestion): DBIO[Option[Long]] = {
val (raw, _) = toSuggestionRow(suggestion)
val selectQuery = Suggestions
.filter(_.kind === raw.kind)
.filter(_.name === raw.name)
.filter(_.scopeStart === raw.scopeStart)
.filter(_.scopeEnd === raw.scopeEnd)
val deleteQuery = for {
rows <- selectQuery.result
n <- selectQuery.delete
_ <- if (n > 0) incrementVersionQuery else DBIO.successful(())
} yield rows.flatMap(_.id).headOption
deleteQuery.transactionally
}
/** The query to remove a list of suggestions.
*
* @param suggestions the suggestions to remove
* @return the list of removed suggestion ids
*/
private def removeAllQuery(
suggestions: Seq[Suggestion]
): DBIO[(Long, Seq[Option[Long]])] = {
val query = for {
ids <- DBIO.sequence(suggestions.map(removeQuery))
version <- currentVersionQuery
} yield (version, ids)
query.transactionally
}
/** The query to get current version of the repo. */
private def currentVersionQuery: DBIO[Long] = {
for {
versionOpt <- Versions.result.headOption
} yield versionOpt.flatMap(_.id).getOrElse(0L)
}
/** The query to increment the current version of the repo. */
private def incrementVersionQuery: DBIO[Long] = {
val increment = for {
version <- Versions.returning(Versions.map(_.id)) += VersionRow(None)
_ <- Versions.filterNot(_.id === version).delete
} yield version
increment.transactionally
}
/** The query to insert suggestions in a batch. */
private def insertBatchQuery(
suggestions: Array[Suggestion]
): DBIO[Int] = {
val rows = suggestions.map(toSuggestionRow)
for {
_ <- (Suggestions ++= rows.map(_._1)).asTry
size <- Suggestions.length.result
} yield size
}
/** Create a search query by the provided parameters. */
private def searchQueryBuilder(
selfType: Option[String],
returnType: Option[String],
kinds: Option[Seq[Suggestion.Kind]]
): Query[SuggestionsTable, SuggestionRow, Seq] = {
Suggestions
.filterOpt(selfType) {
case (row, value) => row.selfType === value
}
.filterOpt(returnType) {
case (row, value) => row.returnType === value
}
.filterOpt(kinds) {
case (row, value) => row.kind inSet value.map(SuggestionKind(_))
}
}
/** Convert the rows of suggestions joined with arguments to a list of
* suggestions.
*/
private def joinedToSuggestions(
coll: Seq[(Option[ArgumentRow], SuggestionRow)]
): Seq[Suggestion] = {
coll
@ -58,6 +270,21 @@ final class SqlSuggestionsRepo(implicit ec: ExecutionContext)
.toSeq
}
/** Convert the rows of suggestions joined with arguments to a list of
* suggestion entries.
*/
private def joinedToSuggestionEntries(
coll: Seq[(Option[ArgumentRow], SuggestionRow)]
): Seq[SuggestionEntry] = {
coll
.groupBy(_._2)
.view
.mapValues(_.flatMap(_._1))
.map(Function.tupled(toSuggestionEntry))
.toSeq
}
/** Convert the suggestion to a row in the suggestions table. */
private def toSuggestionRow(
suggestion: Suggestion
): (SuggestionRow, Seq[Suggestion.Argument]) =
@ -67,11 +294,11 @@ final class SqlSuggestionsRepo(implicit ec: ExecutionContext)
id = None,
kind = SuggestionKind.ATOM,
name = name,
selfType = None,
selfType = SelfTypeColumn.EMPTY,
returnType = returnType,
documentation = doc,
scopeStart = None,
scopeEnd = None
scopeStart = ScopeColumn.EMPTY,
scopeEnd = ScopeColumn.EMPTY
)
row -> args
case Suggestion.Method(name, args, selfType, returnType, doc) =>
@ -79,11 +306,11 @@ final class SqlSuggestionsRepo(implicit ec: ExecutionContext)
id = None,
kind = SuggestionKind.METHOD,
name = name,
selfType = Some(selfType),
selfType = selfType,
returnType = returnType,
documentation = doc,
scopeStart = None,
scopeEnd = None
scopeStart = ScopeColumn.EMPTY,
scopeEnd = ScopeColumn.EMPTY
)
row -> args
case Suggestion.Function(name, args, returnType, scope) =>
@ -91,11 +318,11 @@ final class SqlSuggestionsRepo(implicit ec: ExecutionContext)
id = None,
kind = SuggestionKind.FUNCTION,
name = name,
selfType = None,
selfType = SelfTypeColumn.EMPTY,
returnType = returnType,
documentation = None,
scopeStart = Some(scope.start),
scopeEnd = Some(scope.end)
scopeStart = scope.start,
scopeEnd = scope.end
)
row -> args
case Suggestion.Local(name, returnType, scope) =>
@ -103,22 +330,25 @@ final class SqlSuggestionsRepo(implicit ec: ExecutionContext)
id = None,
kind = SuggestionKind.LOCAL,
name = name,
selfType = None,
selfType = SelfTypeColumn.EMPTY,
returnType = returnType,
documentation = None,
scopeStart = Some(scope.start),
scopeEnd = Some(scope.end)
scopeStart = scope.start,
scopeEnd = scope.end
)
row -> Seq()
}
/** Convert the argument to a row in the arguments table. */
private def toArgumentRow(
suggestionId: Long,
index: Int,
argument: Suggestion.Argument
): ArgumentRow =
ArgumentRow(
id = None,
suggestionId = suggestionId,
index = index,
name = argument.name,
tpe = argument.reprType,
isSuspended = argument.isSuspended,
@ -126,6 +356,14 @@ final class SqlSuggestionsRepo(implicit ec: ExecutionContext)
defaultValue = argument.defaultValue
)
/** Convert the database rows to a suggestion entry. */
private def toSuggestionEntry(
suggestion: SuggestionRow,
arguments: Seq[ArgumentRow]
): SuggestionEntry =
SuggestionEntry(suggestion.id.get, toSuggestion(suggestion, arguments))
/** Convert the databaes rows to a suggestion. */
private def toSuggestion(
suggestion: SuggestionRow,
arguments: Seq[ArgumentRow]
@ -134,38 +372,36 @@ final class SqlSuggestionsRepo(implicit ec: ExecutionContext)
case SuggestionKind.ATOM =>
Suggestion.Atom(
name = suggestion.name,
arguments = arguments.map(toArgument),
arguments = arguments.sortBy(_.index).map(toArgument),
returnType = suggestion.returnType,
documentation = suggestion.documentation
)
case SuggestionKind.METHOD =>
Suggestion.Method(
name = suggestion.name,
arguments = arguments.map(toArgument),
selfType = suggestion.selfType.get,
arguments = arguments.sortBy(_.index).map(toArgument),
selfType = suggestion.selfType,
returnType = suggestion.returnType,
documentation = suggestion.documentation
)
case SuggestionKind.FUNCTION =>
Suggestion.Function(
name = suggestion.name,
arguments = arguments.map(toArgument),
arguments = arguments.sortBy(_.index).map(toArgument),
returnType = suggestion.returnType,
scope =
Suggestion.Scope(suggestion.scopeStart.get, suggestion.scopeEnd.get)
scope = Suggestion.Scope(suggestion.scopeStart, suggestion.scopeEnd)
)
case SuggestionKind.LOCAL =>
Suggestion.Local(
name = suggestion.name,
returnType = suggestion.returnType,
scope =
Suggestion.Scope(suggestion.scopeStart.get, suggestion.scopeEnd.get)
scope = Suggestion.Scope(suggestion.scopeStart, suggestion.scopeEnd)
)
case k =>
throw new NoSuchElementException(s"Unknown suggestion kind: $k")
}
/** Convert the database row to the suggestion argument. */
private def toArgument(row: ArgumentRow): Suggestion.Argument =
Suggestion.Argument(
name = row.name,
@ -175,3 +411,23 @@ final class SqlSuggestionsRepo(implicit ec: ExecutionContext)
defaultValue = row.defaultValue
)
}
object SqlSuggestionsRepo {
/** Create the suggestions repo.
*
* @return the suggestions repo backed up by SQL database.
*/
def apply()(implicit ec: ExecutionContext): SqlSuggestionsRepo = {
new SqlSuggestionsRepo(new SqlDatabase())
}
/** Create the suggestions repo.
*
* @param path the path to the database file.
* @return the suggestions repo backed up by SQL database.
*/
def apply(path: Path)(implicit ec: ExecutionContext): SqlSuggestionsRepo = {
new SqlSuggestionsRepo(SqlDatabase(path.toString))
}
}

View File

@ -1,5 +1,6 @@
package org.enso.searcher.sql
import org.enso.searcher.Suggestion
import slick.jdbc.SQLiteProfile.api._
import scala.annotation.nowarn
@ -8,6 +9,7 @@ import scala.annotation.nowarn
*
* @param id the id of an argument
* @param suggestionId the id of the suggestion
* @param index the argument position in the arguments list
* @param name the argument name
* @param tpe the argument type
* @param isSuspended is the argument lazy
@ -17,6 +19,7 @@ import scala.annotation.nowarn
case class ArgumentRow(
id: Option[Long],
suggestionId: Long,
index: Int,
name: String,
tpe: String,
isSuspended: Boolean,
@ -39,13 +42,19 @@ case class SuggestionRow(
id: Option[Long],
kind: Byte,
name: String,
selfType: Option[String],
selfType: String,
returnType: String,
documentation: Option[String],
scopeStart: Option[Int],
scopeEnd: Option[Int]
scopeStart: Int,
scopeEnd: Int
)
/** A row in the versions table.
*
* @param id the row id
*/
case class VersionRow(id: Option[Long])
/** The type of a suggestion. */
object SuggestionKind {
@ -53,6 +62,31 @@ object SuggestionKind {
val METHOD: Byte = 1
val FUNCTION: Byte = 2
val LOCAL: Byte = 3
/** Create a database suggestion kind.
*
* @param kind the suggestion kind
* @return the representation of the suggestion kind in the database
*/
def apply(kind: Suggestion.Kind): Byte =
kind match {
case Suggestion.Kind.Atom => ATOM
case Suggestion.Kind.Method => METHOD
case Suggestion.Kind.Function => FUNCTION
case Suggestion.Kind.Local => LOCAL
}
}
object ScopeColumn {
/** A constant representing an empty value in the scope column. */
val EMPTY: Int = -1
}
object SelfTypeColumn {
/** A constant representing en empty value in the self type column. */
val EMPTY: String = "\u0500"
}
/** The schema of the arguments table. */
@ -62,17 +96,27 @@ final class ArgumentsTable(tag: Tag)
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def suggestionId = column[Long]("suggestion_id")
def index = column[Int]("index")
def name = column[String]("name")
def tpe = column[String]("type")
def isSuspended = column[Boolean]("is_suspended", O.Default(false))
def hasDefault = column[Boolean]("has_default", O.Default(false))
def defaultValue = column[Option[String]]("default_value")
def * =
(id.?, suggestionId, name, tpe, isSuspended, hasDefault, defaultValue) <>
(
id.?,
suggestionId,
index,
name,
tpe,
isSuspended,
hasDefault,
defaultValue
) <>
(ArgumentRow.tupled, ArgumentRow.unapply)
def suggestion =
foreignKey("suggestion_fk", suggestionId, suggestions)(
foreignKey("suggestion_fk", suggestionId, Suggestions)(
_.id,
onUpdate = ForeignKeyAction.Restrict,
onDelete = ForeignKeyAction.Cascade
@ -87,11 +131,11 @@ final class SuggestionsTable(tag: Tag)
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def kind = column[Byte]("kind")
def name = column[String]("name")
def selfType = column[Option[String]]("self_type")
def selfType = column[String]("self_type")
def returnType = column[String]("return_type")
def documentation = column[Option[String]]("documentation")
def scopeStart = column[Option[Int]]("scope_start")
def scopeEnd = column[Option[Int]]("scope_end")
def scopeStart = column[Int]("scope_start", O.Default(ScopeColumn.EMPTY))
def scopeEnd = column[Int]("scope_end", O.Default(ScopeColumn.EMPTY))
def * =
(
id.?,
@ -105,10 +149,30 @@ final class SuggestionsTable(tag: Tag)
) <>
(SuggestionRow.tupled, SuggestionRow.unapply)
def selfTypeIdx = index("self_type_idx", selfType)
def returnTypeIdx = index("return_type_idx", name)
def selfTypeIdx = index("suggestions_self_type_idx", selfType)
def returnTypeIdx = index("suggestions_return_type_idx", returnType)
def name_idx = index("suggestions_name_idx", name)
// NOTE: unique index should not contain nullable columns because SQLite
// teats NULLs as distinct values.
def uniqueIdx =
index(
"suggestion_unique_idx",
(kind, name, selfType, scopeStart, scopeEnd),
unique = true
)
}
object arguments extends TableQuery(new ArgumentsTable(_))
/** The schema of the versions table. */
@nowarn("msg=multiarg infix syntax")
final class VersionsTable(tag: Tag) extends Table[VersionRow](tag, "version") {
object suggestions extends TableQuery(new SuggestionsTable(_))
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def * = id.? <> (VersionRow.apply, VersionRow.unapply)
}
object Arguments extends TableQuery(new ArgumentsTable(_))
object Suggestions extends TableQuery(new SuggestionsTable(_))
object Versions extends TableQuery(new VersionsTable(_))

View File

@ -1,6 +0,0 @@
searcher.db {
url = "jdbc:sqlite:file::memory:?cache=shared"
}
akka {
loglevel = "ERROR"
}

View File

@ -8,6 +8,9 @@
</encoder>
</appender>
<logger name="slick" level="INFO"/>
<logger name="slick.compiler" level="INFO"/>
<root level="ERROR">
<appender-ref ref="STDOUT"/>
</root>

View File

@ -1,11 +1,9 @@
package org.enso.searcher.sql
import org.enso.jsonrpc.test.FlakySpec
import org.enso.searcher.Suggestion
import org.scalatest.BeforeAndAfterAll
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import slick.jdbc.SQLiteProfile.api._
import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll}
import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
@ -13,51 +11,279 @@ import scala.concurrent.duration._
class SuggestionsRepoTest
extends AnyWordSpec
with FlakySpec
with Matchers
with BeforeAndAfter
with BeforeAndAfterAll {
val Timeout: FiniteDuration = 3.seconds
val Timeout: FiniteDuration = 10.seconds
val db = Database.forConfig("searcher.db")
val repo = new SqlSuggestionsRepo()
val repo = SqlSuggestionsRepo()
override def beforeAll(): Unit = {
Await.ready(
db.run((suggestions.schema ++ arguments.schema).createIfNotExists),
Timeout
)
Await.ready(repo.init, Timeout)
}
override def afterAll(): Unit = {
db.close()
repo.close()
}
"SuggestionsDBIO" should {
before {
Await.ready(repo.clean, Timeout)
}
"select suggestion by id" taggedAs Flaky in {
"SuggestionsRepo" should {
"get all suggestions" in {
val action =
for {
id <- db.run(repo.insert(suggestion.atom))
res <- db.run(repo.select(id))
_ <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
_ <- repo.insert(suggestion.local)
all <- repo.getAll
} yield all._2
val suggestions = Await.result(action, Timeout).map(_.suggestion)
suggestions should contain theSameElementsAs Seq(
suggestion.atom,
suggestion.method,
suggestion.function,
suggestion.local
)
}
"fail to insert duplicate suggestion" in {
val action =
for {
id1 <- repo.insert(suggestion.atom)
id2 <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
_ <- repo.insert(suggestion.function)
_ <- repo.insert(suggestion.local)
_ <- repo.insert(suggestion.local)
all <- repo.getAll
} yield (id1, id2, all._2)
val (id1, id2, all) = Await.result(action, Timeout)
id1 shouldBe a[Some[_]]
id2 shouldBe a[None.type]
all.map(_.suggestion) should contain theSameElementsAs Seq(
suggestion.atom,
suggestion.method,
suggestion.function,
suggestion.local
)
}
"fail to insertAll duplicate suggestion" in {
val action =
for {
(v1, ids) <- repo.insertAll(Seq(suggestion.local, suggestion.local))
(v2, all) <- repo.getAll
} yield (v1, v2, ids, all)
val (v1, v2, ids, all) = Await.result(action, Timeout)
v1 shouldEqual v2
ids.flatten.length shouldEqual 1
all.map(_.suggestion) should contain theSameElementsAs Seq(
suggestion.local
)
}
"select suggestion by id" in {
val action =
for {
Some(id) <- repo.insert(suggestion.atom)
res <- repo.select(id)
} yield res
Await.result(action, Timeout) shouldEqual Some(suggestion.atom)
}
"find suggestion by returnType" taggedAs Flaky in {
"remove suggestion" in {
val action =
for {
_ <- db.run(repo.insert(suggestion.local))
_ <- db.run(repo.insert(suggestion.method))
_ <- db.run(repo.insert(suggestion.function))
res <- db.run(repo.findBy("MyType"))
} yield res
id1 <- repo.insert(suggestion.atom)
id2 <- repo.remove(suggestion.atom)
} yield (id1, id2)
Await.result(action, Timeout) should contain theSameElementsAs Seq(
suggestion.local,
suggestion.function
val (id1, id2) = Await.result(action, Timeout)
id1 shouldEqual id2
}
"get version" in {
val action = repo.currentVersion
Await.result(action, Timeout) shouldEqual 0L
}
"change version after insert" in {
val action = for {
v1 <- repo.currentVersion
_ <- repo.insert(suggestion.atom)
v2 <- repo.currentVersion
} yield (v1, v2)
val (v1, v2) = Await.result(action, Timeout)
v1 should not equal v2
}
"not change version after failed insert" in {
val action = for {
v1 <- repo.currentVersion
_ <- repo.insert(suggestion.atom)
v2 <- repo.currentVersion
_ <- repo.insert(suggestion.atom)
v3 <- repo.currentVersion
} yield (v1, v2, v3)
val (v1, v2, v3) = Await.result(action, Timeout)
v1 should not equal v2
v2 shouldEqual v3
}
"change version after remove" in {
val action = for {
v1 <- repo.currentVersion
_ <- repo.insert(suggestion.local)
v2 <- repo.currentVersion
_ <- repo.remove(suggestion.local)
v3 <- repo.currentVersion
} yield (v1, v2, v3)
val (v1, v2, v3) = Await.result(action, Timeout)
v1 should not equal v2
v2 should not equal v3
}
"not change version after failed remove" in {
val action = for {
v1 <- repo.currentVersion
_ <- repo.insert(suggestion.local)
v2 <- repo.currentVersion
_ <- repo.remove(suggestion.local)
v3 <- repo.currentVersion
_ <- repo.remove(suggestion.local)
v4 <- repo.currentVersion
} yield (v1, v2, v3, v4)
val (v1, v2, v3, v4) = Await.result(action, Timeout)
v1 should not equal v2
v2 should not equal v3
v3 shouldEqual v4
}
"search suggestion by empty query" in {
val action = for {
_ <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
_ <- repo.insert(suggestion.local)
res <- repo.search(None, None, None)
} yield res._2
val res = Await.result(action, Timeout)
res.isEmpty shouldEqual true
}
"search suggestion by self type" in {
val action = for {
_ <- repo.insert(suggestion.atom)
id2 <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
_ <- repo.insert(suggestion.local)
res <- repo.search(Some("Main"), None, None)
} yield (id2, res._2)
val (id, res) = Await.result(action, Timeout)
res should contain theSameElementsAs Seq(id).flatten
}
"search suggestion by return type" in {
val action = for {
_ <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
id3 <- repo.insert(suggestion.function)
id4 <- repo.insert(suggestion.local)
res <- repo.search(None, Some("MyType"), None)
} yield (id3, id4, res._2)
val (id1, id2, res) = Await.result(action, Timeout)
res should contain theSameElementsAs Seq(id1, id2).flatten
}
"search suggestion by kind" in {
val kinds = Seq(Suggestion.Kind.Atom, Suggestion.Kind.Local)
val action = for {
id1 <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
id4 <- repo.insert(suggestion.local)
res <- repo.search(None, None, Some(kinds))
} yield (id1, id4, res._2)
val (id1, id2, res) = Await.result(action, Timeout)
res should contain theSameElementsAs Seq(id1, id2).flatten
}
"search suggestion by empty kinds" in {
val action = for {
_ <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
_ <- repo.insert(suggestion.local)
res <- repo.search(None, None, Some(Seq()))
} yield res._2
val res = Await.result(action, Timeout)
res.isEmpty shouldEqual true
}
"search suggestion by return type and kind" in {
val kinds = Seq(Suggestion.Kind.Atom, Suggestion.Kind.Local)
val action = for {
_ <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
id4 <- repo.insert(suggestion.local)
res <- repo.search(None, Some("MyType"), Some(kinds))
} yield (id4, res._2)
val (id, res) = Await.result(action, Timeout)
res should contain theSameElementsAs Seq(id).flatten
}
"search suggestion by self and return types" in {
val action = for {
_ <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
_ <- repo.insert(suggestion.local)
res <- repo.search(Some("Main"), Some("MyType"), None)
} yield res._2
val res = Await.result(action, Timeout)
res.isEmpty shouldEqual true
}
"search suggestion by all parameters" in {
val kinds = Seq(
Suggestion.Kind.Atom,
Suggestion.Kind.Method,
Suggestion.Kind.Function
)
val action = for {
_ <- repo.insert(suggestion.atom)
_ <- repo.insert(suggestion.method)
_ <- repo.insert(suggestion.function)
_ <- repo.insert(suggestion.local)
res <- repo.search(Some("Main"), Some("MyType"), Some(kinds))
} yield res._2
val res = Await.result(action, Timeout)
res.isEmpty shouldEqual true
}
}