diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml
index 6824bf54890..566145a5b72 100644
--- a/.github/workflows/scala.yml
+++ b/.github/workflows/scala.yml
@@ -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
diff --git a/build.sbt b/build.sbt
index bfa6861f7ea..c4704c24e02 100644
--- a/build.sbt
+++ b/build.sbt
@@ -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(
- "com.typesafe.slick" %% "slick" % slickVersion,
- "org.xerial" % "sqlite-jdbc" % sqliteVersion,
- "org.scalatest" %% "scalatest" % scalatestVersion % Test
+ 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`)
-
diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md
index ff422b06f03..aaa51c2df6c 100644
--- a/docs/language-server/protocol-language-server.md
+++ b/docs/language-server/protocol-language-server.md
@@ -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)
@@ -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`
@@ -1720,11 +1703,11 @@ null
None
## Refactoring
-The language server also provides refactoring operations to restructure an
+The language server also provides refactoring operations to restructure an
internal body of code.
### `refactoring/renameProject`
-This request is sent from the project manager to the server to refactor project
+This request is sent from the project manager to the server to refactor project
name in an interpreter runtime.
- **Type:** Request
@@ -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"
+}
+```
diff --git a/engine/language-server/src/main/resources/logback.xml b/engine/language-server/src/main/resources/logback.xml
new file mode 100644
index 00000000000..6438014b07a
--- /dev/null
+++ b/engine/language-server/src/main/resources/logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level [%-15thread] %-36logger{36} %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala
index de5d8678799..be03c56d0de 100644
--- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala
+++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala
@@ -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,
diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala
index b08e183aa1a..d96b7a9029d 100644
--- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala
+++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala
@@ -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,
diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala
index ae0df2950a6..3a96a7c944a 100644
--- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala
+++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionControllerFactory.scala
@@ -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,
diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala
index e9c1433ba04..904ae69fc9a 100644
--- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala
+++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala
@@ -55,6 +55,9 @@ object JsonRpc {
.registerRequest(AttachVisualisation)
.registerRequest(DetachVisualisation)
.registerRequest(ModifyVisualisation)
+ .registerRequest(GetSuggestionsDatabase)
+ .registerRequest(GetSuggestionsDatabaseVersion)
+ .registerRequest(Completion)
.registerRequest(RenameProject)
.registerNotification(ForceReleaseCapability)
.registerNotification(GrantCapability)
diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/search/CompletionHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/search/CompletionHandler.scala
new file mode 100644
index 00000000000..fe6928de8af
--- /dev/null
+++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/search/CompletionHandler.scala
@@ -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))
+
+}
diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/search/GetSuggestionsDatabaseHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/search/GetSuggestionsDatabaseHandler.scala
new file mode 100644
index 00000000000..81f5ed5a2bf
--- /dev/null
+++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/search/GetSuggestionsDatabaseHandler.scala
@@ -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))
+
+}
diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/search/GetSuggestionsDatabaseVersionHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/search/GetSuggestionsDatabaseVersionHandler.scala
new file mode 100644
index 00000000000..eb96c8552dc
--- /dev/null
+++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/search/GetSuggestionsDatabaseVersionHandler.scala
@@ -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))
+
+}
diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchApi.scala
index 72fad69c604..635a4a2ddae 100644
--- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchApi.scala
+++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchApi.scala
@@ -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")
}
diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchProtocol.scala
index 6e7e4086bf0..fcb9155d04b 100644
--- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchProtocol.scala
+++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchProtocol.scala
@@ -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])
+
}
diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SuggestionsDatabaseEventsListener.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SuggestionsDatabaseEventsListener.scala
index f3041a19ec3..999abdfb51d 100644
--- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SuggestionsDatabaseEventsListener.scala
+++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SuggestionsDatabaseEventsListener.scala
@@ -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,41 +64,55 @@ final class SuggestionsDatabaseEventsListener(
context.become(withClients(clients - client.clientId))
case msg: Api.SuggestionsDatabaseUpdateNotification =>
- clients.foreach { clientId =>
- sessionRouter ! DeliverToJsonController(
- clientId,
- SuggestionsDatabaseUpdateNotification(msg.updates.map(toUpdate), 0)
- )
- }
+ applyDatabaseUpdates(msg)
+ .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
+ )
+ }
}
- 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
- )
- case Api.SuggestionsDatabaseUpdate.Remove(id) =>
- SuggestionsDatabaseUpdate.Remove(id)
+ 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
+ )
}
+ }
}
object SuggestionsDatabaseEventsListener {
@@ -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))
}
diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SuggestionsHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SuggestionsHandler.scala
new file mode 100644
index 00000000000..2ff47552e74
--- /dev/null
+++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SuggestionsHandler.scala
@@ -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))
+
+}
diff --git a/engine/language-server/src/test/resources/application.conf b/engine/language-server/src/test/resources/application.conf
index 41d847fc610..85d09bd278c 100644
--- a/engine/language-server/src/test/resources/application.conf
+++ b/engine/language-server/src/test/resources/application.conf
@@ -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"
diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/runtime/Suggestions.scala b/engine/language-server/src/test/scala/org/enso/languageserver/runtime/Suggestions.scala
new file mode 100644
index 00000000000..7bbcc9b3559
--- /dev/null
+++ b/engine/language-server/src/test/scala/org/enso/languageserver/runtime/Suggestions.scala
@@ -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)
+}
diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/runtime/SuggestionsDatabaseEventsListenerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/runtime/SuggestionsDatabaseEventsListenerTest.scala
new file mode 100644
index 00000000000..7d4aed3ce3b
--- /dev/null
+++ b/engine/language-server/src/test/scala/org/enso/languageserver/runtime/SuggestionsDatabaseEventsListenerTest.scala
@@ -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()
+ }
+}
diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/runtime/SuggestionsHandlerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/runtime/SuggestionsHandlerTest.scala
new file mode 100644
index 00000000000..f701b0fa381
--- /dev/null
+++ b/engine/language-server/src/test/scala/org/enso/languageserver/runtime/SuggestionsHandlerTest.scala
@@ -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()
+ }
+
+}
diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala
index be7e5cb9532..81635cabdd1 100644
--- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala
+++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala
@@ -15,30 +15,30 @@ 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 testContentRoot = Files.createTempDirectory(null).toRealPath()
- val testContentRootId = UUID.randomUUID()
+ val timeout: FiniteDuration = 3.seconds
+
+ val testSuggestionsDbPath = Files.createTempFile("suggestions", ".db")
+ val testContentRoot = Files.createTempDirectory(null).toRealPath()
+ val testContentRootId = UUID.randomUUID()
val config = Config(
Map(testContentRootId -> testContentRoot.toFile),
FileManagerConfig(timeout = 3.seconds),
@@ -48,6 +48,7 @@ class BaseServerTest extends JsonRpcServerTestKit {
val runtimeConnectorProbe = TestProbe()
testContentRoot.toFile.deleteOnExit()
+ testSuggestionsDbPath.toFile.deleteOnExit()
override def protocol: Protocol = JsonRpc.protocol
@@ -77,7 +78,10 @@ class BaseServerTest extends JsonRpcServerTestKit {
)
override def clientControllerFactory: ClientControllerFactory = {
- val zioExec = ZioExec(zio.Runtime.default)
+ 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,
diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/FileNotificationsTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/FileNotificationsTest.scala
index 5ef8a3d3915..061bad1a153 100644
--- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/FileNotificationsTest.scala
+++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/FileNotificationsTest.scala
@@ -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.
diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SearchJsonMessages.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SearchJsonMessages.scala
new file mode 100644
index 00000000000..45b61f7099a
--- /dev/null
+++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SearchJsonMessages.scala
@@ -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
+ }
+ """
+}
diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SuggestionsDatabaseEventsListenerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SuggestionsDatabaseEventsListenerTest.scala
index 803d7161682..55d085eea82 100644
--- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SuggestionsDatabaseEventsListenerTest.scala
+++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SuggestionsDatabaseEventsListenerTest.scala
@@ -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
- },
- {
- "name" : "b",
- "reprType" : "Any",
- "isSuspended" : false,
- "hasDefault" : true,
- "defaultValue" : "77"
- }
- ],
- "selfType" : "MyType",
- "returnType" : "IO",
- "scope" : {
- "start" : 12,
- "end" : 24
- }
- }
- ],
- "currentVersion" : 0
- }
- }
- """)
- }
-
- "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" : "Remove",
+ "id" : 2
+ },
{
- "type" : "Delete",
- "id" : 101
+ "type" : "Remove",
+ "id" : 3
}
],
- "currentVersion" : 0
+ "currentVersion" : 6
}
}
""")
+
}
}
- 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
- }
- """
- }
-
}
diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SuggestionsHandlerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SuggestionsHandlerTest.scala
new file mode 100644
index 00000000000..e5f09b5be78
--- /dev/null
+++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SuggestionsHandlerTest.scala
@@ -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
+ }
+ }
+ """)
+ }
+ }
+
+}
diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala
index b1892c2a0f4..6eacd4c074f 100644
--- a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala
+++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala
@@ -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
}
/**
diff --git a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/DocumentationCommentsTest.scala b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/DocumentationCommentsTest.scala
index 06d75448237..0ce567647ac 100644
--- a/engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/DocumentationCommentsTest.scala
+++ b/engine/runtime/src/test/scala/org/enso/compiler/test/pass/resolve/DocumentationCommentsTest.scala
@@ -215,7 +215,7 @@ class DocumentationCommentsTest extends CompilerTest with Inside {
| IO.println "foo"
| ## the return
| 0
- |
+ |
| f = case _ of
| ## case 1
| Bar -> 100
diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala
index 32d54f75706..1475a620495 100644
--- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala
+++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala
@@ -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)
diff --git a/lib/scala/searcher/src/bench/java/org/enso/searcher/sql/SuggestionsRepoBenchmark.java b/lib/scala/searcher/src/bench/java/org/enso/searcher/sql/SuggestionsRepoBenchmark.java
new file mode 100644
index 00000000000..a63345b6628
--- /dev/null
+++ b/lib/scala/searcher/src/bench/java/org/enso/searcher/sql/SuggestionsRepoBenchmark.java
@@ -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 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 scala.Option none() {
+ return (scala.Option) 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();
+ }
+}
diff --git a/lib/scala/searcher/src/bench/scala/org/enso/searcher/sql/SuggestionRandom.scala b/lib/scala/searcher/src/bench/scala/org/enso/searcher/sql/SuggestionRandom.scala
new file mode 100644
index 00000000000..94a8391f040
--- /dev/null
+++ b/lib/scala/searcher/src/bench/scala/org/enso/searcher/sql/SuggestionRandom.scala
@@ -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)
+}
diff --git a/lib/scala/searcher/src/main/resources/application.conf b/lib/scala/searcher/src/main/resources/application.conf
deleted file mode 100644
index 25f86080d49..00000000000
--- a/lib/scala/searcher/src/main/resources/application.conf
+++ /dev/null
@@ -1,8 +0,0 @@
-searcher {
- db {
- url = "jdbc:sqlite:searcher.db"
- driver = org.sqlite.JDBC
- connectionPool = disabled
- keepAliveConnection = true
- }
-}
diff --git a/lib/scala/searcher/src/main/resources/reference.conf b/lib/scala/searcher/src/main/resources/reference.conf
new file mode 100644
index 00000000000..e05cabcc6fa
--- /dev/null
+++ b/lib/scala/searcher/src/main/resources/reference.conf
@@ -0,0 +1,7 @@
+searcher {
+ db {
+ url = "jdbc:sqlite::memory:"
+ driver = "org.sqlite.JDBC"
+ connectionPool = "HikariCP"
+ }
+}
diff --git a/lib/scala/searcher/src/main/scala/org/enso/searcher/Database.scala b/lib/scala/searcher/src/main/scala/org/enso/searcher/Database.scala
new file mode 100644
index 00000000000..71ce876d355
--- /dev/null
+++ b/lib/scala/searcher/src/main/scala/org/enso/searcher/Database.scala
@@ -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
+}
diff --git a/lib/scala/searcher/src/main/scala/org/enso/searcher/Suggestion.scala b/lib/scala/searcher/src/main/scala/org/enso/searcher/Suggestion.scala
index 8970d28c792..deefab04f58 100644
--- a/lib/scala/searcher/src/main/scala/org/enso/searcher/Suggestion.scala
+++ b/lib/scala/searcher/src/main/scala/org/enso/searcher/Suggestion.scala
@@ -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
diff --git a/lib/scala/searcher/src/main/scala/org/enso/searcher/SuggestionEntry.scala b/lib/scala/searcher/src/main/scala/org/enso/searcher/SuggestionEntry.scala
new file mode 100644
index 00000000000..c184f41f410
--- /dev/null
+++ b/lib/scala/searcher/src/main/scala/org/enso/searcher/SuggestionEntry.scala
@@ -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)
diff --git a/lib/scala/searcher/src/main/scala/org/enso/searcher/SuggestionsRepo.scala b/lib/scala/searcher/src/main/scala/org/enso/searcher/SuggestionsRepo.scala
index c7e049597da..e77ca27511e 100644
--- a/lib/scala/searcher/src/main/scala/org/enso/searcher/SuggestionsRepo.scala
+++ b/lib/scala/searcher/src/main/scala/org/enso/searcher/SuggestionsRepo.scala
@@ -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]])]
}
diff --git a/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlDatabase.scala b/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlDatabase.scala
new file mode 100644
index 00000000000..11c017edfd7
--- /dev/null
+++ b/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlDatabase.scala
@@ -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("\\", "\\\\")
+}
diff --git a/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala b/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala
index 1efdbcfed8d..b7d9370c7be 100644
--- a/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala
+++ b/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/SqlSuggestionsRepo.scala
@@ -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))
+ }
+}
diff --git a/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/Tables.scala b/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/Tables.scala
index 8da773e6beb..2c7371ab1ac 100644
--- a/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/Tables.scala
+++ b/lib/scala/searcher/src/main/scala/org/enso/searcher/sql/Tables.scala
@@ -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(_))
diff --git a/lib/scala/searcher/src/test/resources/application.conf b/lib/scala/searcher/src/test/resources/application.conf
deleted file mode 100644
index f7d8aab96ae..00000000000
--- a/lib/scala/searcher/src/test/resources/application.conf
+++ /dev/null
@@ -1,6 +0,0 @@
-searcher.db {
- url = "jdbc:sqlite:file::memory:?cache=shared"
-}
-akka {
- loglevel = "ERROR"
-}
diff --git a/lib/scala/searcher/src/test/resources/logback-test.xml b/lib/scala/searcher/src/test/resources/logback-test.xml
index 433debfaba7..6774939257b 100644
--- a/lib/scala/searcher/src/test/resources/logback-test.xml
+++ b/lib/scala/searcher/src/test/resources/logback-test.xml
@@ -8,6 +8,9 @@
+
+
+
diff --git a/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala b/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala
index 991d0cb2e92..c2b893845b2 100644
--- a/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala
+++ b/lib/scala/searcher/src/test/scala/org/enso/searcher/sql/SuggestionsRepoTest.scala
@@ -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
}
}