mirror of
https://github.com/enso-org/enso.git
synced 2025-01-03 16:03:19 +03:00
Implement Search Requests API (#953)
This commit is contained in:
parent
05146e23b7
commit
6ba038c800
2
.github/workflows/scala.yml
vendored
2
.github/workflows/scala.yml
vendored
@ -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
|
||||
|
18
build.sbt
18
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(
|
||||
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`)
|
||||
|
||||
|
@ -27,7 +27,6 @@ transport formats, please look [here](./protocol-architecture).
|
||||
- [`SuggestionEntryArgument`](#suggestionentryargument)
|
||||
- [`SuggestionEntry`](#suggestionentry)
|
||||
- [`SuggestionEntryType`](#suggestionentrytype)
|
||||
- [`SuggestionsDatabaseEntry`](#suggestionsdatabaseentry)
|
||||
- [`SuggestionsDatabaseUpdate`](#suggestionsdatabaseupdate)
|
||||
- [`File`](#file)
|
||||
- [`DirectoryTree`](#directorytree)
|
||||
@ -141,6 +140,7 @@ transport formats, please look [here](./protocol-architecture).
|
||||
- [`CapabilityNotAcquired`](#capabilitynotacquired)
|
||||
- [`SessionNotInitialisedError`](#sessionnotinitialisederror)
|
||||
- [`SessionAlreadyInitialisedError`](#sessionalreadyinitialisederror)
|
||||
- [`SuggestionsDatabaseError`](#suggestionsdatabaseerror)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
@ -330,20 +330,6 @@ type SuggestionEntryType
|
||||
| Local;
|
||||
```
|
||||
|
||||
### `SuggestionsDatabaseEntry`
|
||||
The entry in the suggestions database.
|
||||
|
||||
#### Format
|
||||
|
||||
```typescript
|
||||
// The suggestions database entry.
|
||||
interface SuggestionsDatabaseEntry {
|
||||
// suggestion entry id;
|
||||
id: number;
|
||||
suggestion: Suggestion;
|
||||
}
|
||||
```
|
||||
|
||||
### `SuggestionsDatabaseUpdate`
|
||||
The update of the suggestions database.
|
||||
|
||||
@ -351,23 +337,20 @@ The update of the suggestions database.
|
||||
|
||||
```typescript
|
||||
// The kind of the suggestions database update.
|
||||
type SuggestionsDatabaseUpdateKind
|
||||
type SuggestionsDatabaseUpdate
|
||||
= Add
|
||||
| Update
|
||||
| Delete
|
||||
| Remove
|
||||
|
||||
interface SuggestionsDatabaseUpdate {
|
||||
interface Add {
|
||||
// suggestion entry id
|
||||
id: number;
|
||||
kind: SuggestionsDatabaseUpdateKind;
|
||||
name?: string;
|
||||
module?: string;
|
||||
arguments?: [SuggestionEntryArgument];
|
||||
selfType?: string;
|
||||
returnType?: string;
|
||||
documentation?: string;
|
||||
scope?: SuggestionEntryScope;
|
||||
// suggestion entry
|
||||
suggestion: SuggestionEntry;
|
||||
}
|
||||
|
||||
interface Remove {
|
||||
// suggestion entry id
|
||||
id: number;
|
||||
```
|
||||
|
||||
### `File`
|
||||
@ -2482,7 +2465,7 @@ null
|
||||
```typescript
|
||||
{
|
||||
// The list of suggestions database entries
|
||||
entries: [SuggestionsDatabaseEntry];
|
||||
entries: [SuggestionsDatabaseUpdate];
|
||||
// The version of received suggestions database
|
||||
currentVersion: number;
|
||||
}
|
||||
@ -2971,3 +2954,13 @@ Signals that session is already initialised.
|
||||
"message" : "Session already initialised"
|
||||
}
|
||||
```
|
||||
|
||||
### `SuggestionsDatabaseError`
|
||||
Signals about an error accessing the suggestions database.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 7001,
|
||||
"message" : "Suggestions database error"
|
||||
}
|
||||
```
|
||||
|
16
engine/language-server/src/main/resources/logback.xml
Normal file
16
engine/language-server/src/main/resources/logback.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<configuration>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} %-5level [%-15thread] %-36logger{36} %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="org.enso" level="TRACE"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -55,6 +55,9 @@ object JsonRpc {
|
||||
.registerRequest(AttachVisualisation)
|
||||
.registerRequest(DetachVisualisation)
|
||||
.registerRequest(ModifyVisualisation)
|
||||
.registerRequest(GetSuggestionsDatabase)
|
||||
.registerRequest(GetSuggestionsDatabaseVersion)
|
||||
.registerRequest(Completion)
|
||||
.registerRequest(RenameProject)
|
||||
.registerNotification(ForceReleaseCapability)
|
||||
.registerNotification(GrantCapability)
|
||||
|
@ -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))
|
||||
|
||||
}
|
@ -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))
|
||||
|
||||
}
|
@ -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))
|
||||
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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])
|
||||
|
||||
}
|
||||
|
@ -19,6 +19,10 @@ import org.enso.languageserver.runtime.SearchProtocol.{
|
||||
import org.enso.languageserver.session.SessionRouter.DeliverToJsonController
|
||||
import org.enso.languageserver.util.UnhandledLogging
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.searcher.{Suggestion, SuggestionsRepo}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* Event listener listens event stream for the suggestion database
|
||||
@ -26,13 +30,17 @@ import org.enso.polyglot.runtime.Runtime.Api
|
||||
* is a singleton and created per context registry.
|
||||
*
|
||||
* @param sessionRouter the session router
|
||||
* @param repo the suggestions repo
|
||||
*/
|
||||
final class SuggestionsDatabaseEventsListener(
|
||||
sessionRouter: ActorRef
|
||||
sessionRouter: ActorRef,
|
||||
repo: SuggestionsRepo[Future]
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with UnhandledLogging {
|
||||
|
||||
import context.dispatcher
|
||||
|
||||
override def preStart(): Unit = {
|
||||
context.system.eventStream
|
||||
.subscribe(self, classOf[Api.SuggestionsDatabaseUpdateNotification])
|
||||
@ -56,40 +64,54 @@ final class SuggestionsDatabaseEventsListener(
|
||||
context.become(withClients(clients - client.clientId))
|
||||
|
||||
case msg: Api.SuggestionsDatabaseUpdateNotification =>
|
||||
applyDatabaseUpdates(msg)
|
||||
.onComplete {
|
||||
case Success(notification) =>
|
||||
if (notification.updates.nonEmpty) {
|
||||
clients.foreach { clientId =>
|
||||
sessionRouter ! DeliverToJsonController(
|
||||
clientId,
|
||||
SuggestionsDatabaseUpdateNotification(msg.updates.map(toUpdate), 0)
|
||||
sessionRouter ! DeliverToJsonController(clientId, notification)
|
||||
}
|
||||
}
|
||||
case Failure(ex) =>
|
||||
log.error(
|
||||
ex,
|
||||
"Error applying suggestion database updates: {}",
|
||||
msg.updates
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def toUpdate(
|
||||
update: Api.SuggestionsDatabaseUpdate
|
||||
): SuggestionsDatabaseUpdate =
|
||||
update match {
|
||||
case Api.SuggestionsDatabaseUpdate.Add(id, suggestion) =>
|
||||
SuggestionsDatabaseUpdate.Add(id, suggestion)
|
||||
case Api.SuggestionsDatabaseUpdate.Modify(
|
||||
id,
|
||||
name,
|
||||
arguments,
|
||||
selfType,
|
||||
returnType,
|
||||
doc,
|
||||
scope
|
||||
) =>
|
||||
SuggestionsDatabaseUpdate.Modify(
|
||||
id,
|
||||
name,
|
||||
arguments,
|
||||
selfType,
|
||||
returnType,
|
||||
doc,
|
||||
scope
|
||||
private def applyDatabaseUpdates(
|
||||
msg: Api.SuggestionsDatabaseUpdateNotification
|
||||
): Future[SuggestionsDatabaseUpdateNotification] = {
|
||||
val (added, removed) = msg.updates
|
||||
.foldLeft((Seq[Suggestion](), Seq[Suggestion]())) {
|
||||
case ((add, remove), msg: Api.SuggestionsDatabaseUpdate.Add) =>
|
||||
(add :+ msg.suggestion, remove)
|
||||
case ((add, remove), msg: Api.SuggestionsDatabaseUpdate.Remove) =>
|
||||
(add, remove :+ msg.suggestion)
|
||||
}
|
||||
|
||||
for {
|
||||
(_, removedIds) <- repo.removeAll(removed)
|
||||
(version, addedIds) <- repo.insertAll(added)
|
||||
} yield {
|
||||
val updatesRemoved = removedIds.collect {
|
||||
case Some(id) => SuggestionsDatabaseUpdate.Remove(id)
|
||||
}
|
||||
val updatesAdded =
|
||||
(addedIds zip added).flatMap {
|
||||
case (Some(id), suggestion) =>
|
||||
Some(SuggestionsDatabaseUpdate.Add(id, suggestion))
|
||||
case (None, suggestion) =>
|
||||
log.error("failed to insert suggestion: {}", suggestion)
|
||||
None
|
||||
}
|
||||
SuggestionsDatabaseUpdateNotification(
|
||||
updatesRemoved ++ updatesAdded,
|
||||
version
|
||||
)
|
||||
case Api.SuggestionsDatabaseUpdate.Remove(id) =>
|
||||
SuggestionsDatabaseUpdate.Remove(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,8 +122,12 @@ object SuggestionsDatabaseEventsListener {
|
||||
* [[SuggestionsDatabaseEventsListener]].
|
||||
*
|
||||
* @param sessionRouter the session router
|
||||
* @param repo the suggestions repo
|
||||
*/
|
||||
def props(sessionRouter: ActorRef): Props =
|
||||
Props(new SuggestionsDatabaseEventsListener(sessionRouter))
|
||||
def props(
|
||||
sessionRouter: ActorRef,
|
||||
repo: SuggestionsRepo[Future]
|
||||
): Props =
|
||||
Props(new SuggestionsDatabaseEventsListener(sessionRouter, repo))
|
||||
|
||||
}
|
||||
|
@ -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))
|
||||
|
||||
}
|
@ -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"
|
||||
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -15,28 +15,28 @@ import org.enso.languageserver.filemanager.{
|
||||
FileSystem,
|
||||
ReceivesTreeUpdatesHandler
|
||||
}
|
||||
import org.enso.languageserver.io.{
|
||||
InputRedirectionController,
|
||||
ObservableOutputStream,
|
||||
ObservablePipedInputStream,
|
||||
OutputKind,
|
||||
OutputRedirectionController
|
||||
}
|
||||
import org.enso.languageserver.io._
|
||||
import org.enso.languageserver.protocol.json.{
|
||||
JsonConnectionControllerFactory,
|
||||
JsonRpc
|
||||
}
|
||||
import org.enso.languageserver.runtime.{
|
||||
ContextRegistry,
|
||||
SuggestionsDatabaseEventsListener
|
||||
SuggestionsDatabaseEventsListener,
|
||||
SuggestionsHandler
|
||||
}
|
||||
import org.enso.languageserver.session.SessionRouter
|
||||
import org.enso.languageserver.text.BufferRegistry
|
||||
import org.enso.searcher.sql.SqlSuggestionsRepo
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class BaseServerTest extends JsonRpcServerTestKit {
|
||||
|
||||
val timeout: FiniteDuration = 3.seconds
|
||||
|
||||
val testSuggestionsDbPath = Files.createTempFile("suggestions", ".db")
|
||||
val testContentRoot = Files.createTempDirectory(null).toRealPath()
|
||||
val testContentRootId = UUID.randomUUID()
|
||||
val config = Config(
|
||||
@ -48,6 +48,7 @@ class BaseServerTest extends JsonRpcServerTestKit {
|
||||
val runtimeConnectorProbe = TestProbe()
|
||||
|
||||
testContentRoot.toFile.deleteOnExit()
|
||||
testSuggestionsDbPath.toFile.deleteOnExit()
|
||||
|
||||
override def protocol: Protocol = JsonRpc.protocol
|
||||
|
||||
@ -78,6 +79,9 @@ class BaseServerTest extends JsonRpcServerTestKit {
|
||||
|
||||
override def clientControllerFactory: ClientControllerFactory = {
|
||||
val zioExec = ZioExec(zio.Runtime.default)
|
||||
val suggestionsRepo = SqlSuggestionsRepo()(system.dispatcher)
|
||||
Await.ready(suggestionsRepo.init, timeout)
|
||||
|
||||
val fileManager =
|
||||
system.actorOf(FileManager.props(config, new FileSystem, zioExec))
|
||||
val bufferRegistry =
|
||||
@ -97,7 +101,9 @@ class BaseServerTest extends JsonRpcServerTestKit {
|
||||
)
|
||||
|
||||
val suggestionsDatabaseEventsListener =
|
||||
system.actorOf(SuggestionsDatabaseEventsListener.props(sessionRouter))
|
||||
system.actorOf(
|
||||
SuggestionsDatabaseEventsListener.props(sessionRouter, suggestionsRepo)
|
||||
)
|
||||
|
||||
val capabilityRouter =
|
||||
system.actorOf(
|
||||
@ -108,11 +114,15 @@ class BaseServerTest extends JsonRpcServerTestKit {
|
||||
)
|
||||
)
|
||||
|
||||
val suggestionsHandler =
|
||||
system.actorOf(SuggestionsHandler.props(suggestionsRepo))
|
||||
|
||||
new JsonConnectionControllerFactory(
|
||||
bufferRegistry,
|
||||
capabilityRouter,
|
||||
fileManager,
|
||||
contextRegistry,
|
||||
suggestionsHandler,
|
||||
stdOutController,
|
||||
stdErrController,
|
||||
stdInController,
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
"""
|
||||
}
|
@ -1,32 +1,30 @@
|
||||
package org.enso.languageserver.websocket.json
|
||||
|
||||
import io.circe.literal._
|
||||
import org.enso.jsonrpc.test.FlakySpec
|
||||
import org.enso.languageserver.runtime.Suggestions
|
||||
import org.enso.languageserver.websocket.json.{SearchJsonMessages => json}
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.searcher.Suggestion
|
||||
import org.scalatest.BeforeAndAfter
|
||||
|
||||
class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
|
||||
class SuggestionsDatabaseEventsListenerTest
|
||||
extends BaseServerTest
|
||||
with BeforeAndAfter
|
||||
with FlakySpec {
|
||||
|
||||
lazy val client = getInitialisedWsClient()
|
||||
|
||||
"SuggestionsDatabaseEventListener" must {
|
||||
|
||||
"acquire and release capabilities" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
|
||||
client.expectJson(json.ok(0))
|
||||
|
||||
client.send(json.releaseSuggestionsDatabaseUpdatesCapability(1))
|
||||
client.expectJson(json.ok(1))
|
||||
}
|
||||
|
||||
"send suggestions database add atom notifications" in {
|
||||
val client = getInitialisedWsClient()
|
||||
"send suggestions database notifications" taggedAs Flaky in {
|
||||
|
||||
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
|
||||
client.expectJson(json.ok(0))
|
||||
|
||||
// add atom
|
||||
system.eventStream.publish(
|
||||
Api.SuggestionsDatabaseUpdateNotification(
|
||||
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.atom))
|
||||
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.atom))
|
||||
)
|
||||
)
|
||||
client.expectJson(json"""
|
||||
@ -36,7 +34,7 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
|
||||
"updates" : [
|
||||
{
|
||||
"type" : "Add",
|
||||
"id" : 0,
|
||||
"id" : 1,
|
||||
"suggestion" : {
|
||||
"type" : "atom",
|
||||
"name" : "MyType",
|
||||
@ -53,21 +51,15 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
|
||||
}
|
||||
}
|
||||
],
|
||||
"currentVersion" : 0
|
||||
"currentVersion" : 1
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"send suggestions database add method notifications" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
|
||||
client.expectJson(json.ok(0))
|
||||
|
||||
// add method
|
||||
system.eventStream.publish(
|
||||
Api.SuggestionsDatabaseUpdateNotification(
|
||||
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.method))
|
||||
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.method))
|
||||
)
|
||||
)
|
||||
client.expectJson(json"""
|
||||
@ -77,7 +69,7 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
|
||||
"updates" : [
|
||||
{
|
||||
"type" : "Add",
|
||||
"id" : 0,
|
||||
"id" : 2,
|
||||
"suggestion" : {
|
||||
"type" : "method",
|
||||
"name" : "foo",
|
||||
@ -99,25 +91,19 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
|
||||
],
|
||||
"selfType" : "MyType",
|
||||
"returnType" : "Number",
|
||||
"documentation" : "My doc"
|
||||
"documentation" : "Lovely"
|
||||
}
|
||||
}
|
||||
],
|
||||
"currentVersion" : 0
|
||||
"currentVersion" : 2
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"send suggestions database add function notifications" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
|
||||
client.expectJson(json.ok(0))
|
||||
|
||||
// add function
|
||||
system.eventStream.publish(
|
||||
Api.SuggestionsDatabaseUpdateNotification(
|
||||
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.function))
|
||||
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.function))
|
||||
)
|
||||
)
|
||||
client.expectJson(json"""
|
||||
@ -127,7 +113,7 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
|
||||
"updates" : [
|
||||
{
|
||||
"type" : "Add",
|
||||
"id" : 0,
|
||||
"id" : 3,
|
||||
"suggestion" : {
|
||||
"type" : "function",
|
||||
"name" : "print",
|
||||
@ -135,27 +121,21 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
|
||||
],
|
||||
"returnType" : "IO",
|
||||
"scope" : {
|
||||
"start" : 7,
|
||||
"end" : 10
|
||||
"start" : 9,
|
||||
"end" : 22
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"currentVersion" : 0
|
||||
"currentVersion" : 3
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"send suggestions database add local notifications" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
|
||||
client.expectJson(json.ok(0))
|
||||
|
||||
// add local
|
||||
system.eventStream.publish(
|
||||
Api.SuggestionsDatabaseUpdateNotification(
|
||||
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.local))
|
||||
Seq(Api.SuggestionsDatabaseUpdate.Add(Suggestions.local))
|
||||
)
|
||||
)
|
||||
client.expectJson(json"""
|
||||
@ -165,47 +145,29 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
|
||||
"updates" : [
|
||||
{
|
||||
"type" : "Add",
|
||||
"id" : 0,
|
||||
"id" : 4,
|
||||
"suggestion" : {
|
||||
"type" : "local",
|
||||
"name" : "x",
|
||||
"returnType" : "Number",
|
||||
"scope" : {
|
||||
"start" : 15,
|
||||
"end" : 17
|
||||
"start" : 34,
|
||||
"end" : 68
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"currentVersion" : 0
|
||||
"currentVersion" : 4
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"send suggestions database modify notifications" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
|
||||
client.expectJson(json.ok(0))
|
||||
|
||||
// remove items
|
||||
system.eventStream.publish(
|
||||
Api.SuggestionsDatabaseUpdateNotification(
|
||||
Seq(
|
||||
Api.SuggestionsDatabaseUpdate.Modify(
|
||||
id = 0,
|
||||
name = Some("foo"),
|
||||
arguments = Some(
|
||||
Seq(
|
||||
Suggestion.Argument("a", "Any", true, false, None),
|
||||
Suggestion.Argument("b", "Any", false, true, Some("77"))
|
||||
)
|
||||
),
|
||||
selfType = Some("MyType"),
|
||||
returnType = Some("IO"),
|
||||
documentation = None,
|
||||
scope = Some(Suggestion.Scope(12, 24))
|
||||
)
|
||||
Api.SuggestionsDatabaseUpdate.Remove(Suggestions.method),
|
||||
Api.SuggestionsDatabaseUpdate.Remove(Suggestions.function)
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -215,138 +177,20 @@ class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
|
||||
"params" : {
|
||||
"updates" : [
|
||||
{
|
||||
"type" : "Update",
|
||||
"id" : 0,
|
||||
"name" : "foo",
|
||||
"arguments" : [
|
||||
{
|
||||
"name" : "a",
|
||||
"reprType" : "Any",
|
||||
"isSuspended" : true,
|
||||
"hasDefault" : false,
|
||||
"defaultValue" : null
|
||||
"type" : "Remove",
|
||||
"id" : 2
|
||||
},
|
||||
{
|
||||
"name" : "b",
|
||||
"reprType" : "Any",
|
||||
"isSuspended" : false,
|
||||
"hasDefault" : true,
|
||||
"defaultValue" : "77"
|
||||
"type" : "Remove",
|
||||
"id" : 3
|
||||
}
|
||||
],
|
||||
"selfType" : "MyType",
|
||||
"returnType" : "IO",
|
||||
"scope" : {
|
||||
"start" : 12,
|
||||
"end" : 24
|
||||
}
|
||||
}
|
||||
],
|
||||
"currentVersion" : 0
|
||||
"currentVersion" : 6
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"send suggestions database remove notifications" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
|
||||
client.expectJson(json.ok(0))
|
||||
|
||||
system.eventStream.publish(
|
||||
Api.SuggestionsDatabaseUpdateNotification(
|
||||
Seq(Api.SuggestionsDatabaseUpdate.Remove(101))
|
||||
)
|
||||
)
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc" : "2.0",
|
||||
"method" : "search/suggestionsDatabaseUpdates",
|
||||
"params" : {
|
||||
"updates" : [
|
||||
{
|
||||
"type" : "Delete",
|
||||
"id" : 101
|
||||
}
|
||||
],
|
||||
"currentVersion" : 0
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
object suggestion {
|
||||
|
||||
val atom: Suggestion.Atom =
|
||||
Suggestion.Atom(
|
||||
name = "MyType",
|
||||
arguments = Seq(Suggestion.Argument("a", "Any", false, false, None)),
|
||||
returnType = "MyAtom",
|
||||
documentation = None
|
||||
)
|
||||
|
||||
val method: Suggestion.Method =
|
||||
Suggestion.Method(
|
||||
name = "foo",
|
||||
arguments = Seq(
|
||||
Suggestion.Argument("this", "MyType", false, false, None),
|
||||
Suggestion.Argument("foo", "Number", false, true, Some("42"))
|
||||
),
|
||||
selfType = "MyType",
|
||||
returnType = "Number",
|
||||
documentation = Some("My doc")
|
||||
)
|
||||
|
||||
val function: Suggestion.Function =
|
||||
Suggestion.Function(
|
||||
name = "print",
|
||||
arguments = Seq(),
|
||||
returnType = "IO",
|
||||
scope = Suggestion.Scope(7, 10)
|
||||
)
|
||||
|
||||
val local: Suggestion.Local =
|
||||
Suggestion.Local(
|
||||
name = "x",
|
||||
returnType = "Number",
|
||||
scope = Suggestion.Scope(15, 17)
|
||||
)
|
||||
}
|
||||
|
||||
object json {
|
||||
|
||||
def acquireSuggestionsDatabaseUpdatesCapability(reqId: Long) =
|
||||
json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/acquire",
|
||||
"id": $reqId,
|
||||
"params": {
|
||||
"method": "search/receivesSuggestionsDatabaseUpdates",
|
||||
"registerOptions": {}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def releaseSuggestionsDatabaseUpdatesCapability(reqId: Long) =
|
||||
json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/release",
|
||||
"id": $reqId,
|
||||
"params": {
|
||||
"method": "search/receivesSuggestionsDatabaseUpdates",
|
||||
"registerOptions": {}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def ok(reqId: Long) =
|
||||
json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": $reqId,
|
||||
"result": null
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
|
@ -0,0 +1,101 @@
|
||||
package org.enso.searcher.sql;
|
||||
|
||||
import org.enso.searcher.Suggestion;
|
||||
import org.openjdk.jmh.annotations.*;
|
||||
import org.openjdk.jmh.runner.Runner;
|
||||
import org.openjdk.jmh.runner.RunnerException;
|
||||
import org.openjdk.jmh.runner.options.Options;
|
||||
import org.openjdk.jmh.runner.options.OptionsBuilder;
|
||||
import scala.collection.immutable.Seq;
|
||||
import scala.concurrent.Await;
|
||||
import scala.concurrent.ExecutionContext;
|
||||
import scala.concurrent.duration.Duration;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@BenchmarkMode(Mode.AverageTime)
|
||||
@Fork(1)
|
||||
@Warmup(iterations = 5)
|
||||
@Measurement(iterations = 5)
|
||||
@OutputTimeUnit(TimeUnit.MICROSECONDS)
|
||||
@State(Scope.Benchmark)
|
||||
public class SuggestionsRepoBenchmark {
|
||||
|
||||
static final int DATABASE_SIZE = 1000000;
|
||||
static final Duration TIMEOUT = Duration.apply(3, TimeUnit.SECONDS);
|
||||
|
||||
final Path dbfile = Path.of(System.getProperty("java.io.tmpdir"), "bench-suggestions.db");
|
||||
final Seq<Suggestion.Kind> kinds = SuggestionRandom.nextKinds();
|
||||
|
||||
SqlSuggestionsRepo repo;
|
||||
|
||||
@Setup
|
||||
public void setup() throws TimeoutException, InterruptedException {
|
||||
repo = SqlSuggestionsRepo.apply(dbfile, ExecutionContext.global());
|
||||
if (Files.notExists(dbfile)) {
|
||||
System.out.println("initializing " + dbfile.toString() + " ...");
|
||||
Await.ready(repo.init(), TIMEOUT);
|
||||
System.out.println("inserting records...");
|
||||
int size = 0;
|
||||
while (size < DATABASE_SIZE) {
|
||||
size = insertBatch(20000);
|
||||
}
|
||||
System.out.println("created " + size + " records");
|
||||
}
|
||||
}
|
||||
|
||||
@TearDown
|
||||
public void tearDown() {
|
||||
repo.close();
|
||||
}
|
||||
|
||||
int insertBatch(int size) throws TimeoutException, InterruptedException {
|
||||
Suggestion[] stubs =
|
||||
Stream.generate(SuggestionRandom::nextSuggestion).limit(size).toArray(Suggestion[]::new);
|
||||
return (int) Await.result(repo.insertBatch(stubs), TIMEOUT);
|
||||
}
|
||||
|
||||
static <T> scala.Option<T> none() {
|
||||
return (scala.Option<T>) scala.None$.MODULE$;
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Object searchBaseline() throws TimeoutException, InterruptedException {
|
||||
return Await.result(repo.search(none(), none(), none()), TIMEOUT);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Object searchByReturnType() throws TimeoutException, InterruptedException {
|
||||
return Await.result(repo.search(none(), scala.Some.apply("MyType"), none()), TIMEOUT);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Object searchBySelfType() throws TimeoutException, InterruptedException {
|
||||
return Await.result(repo.search(scala.Some.apply("MyType"), none(), none()), TIMEOUT);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Object searchBySelfReturnTypes() throws TimeoutException, InterruptedException {
|
||||
return Await.result(
|
||||
repo.search(scala.Some.apply("SelfType"), scala.Some.apply("ReturnType"), none()), TIMEOUT);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public Object searchByAll() throws TimeoutException, InterruptedException {
|
||||
return Await.result(
|
||||
repo.search(
|
||||
scala.Some.apply("SelfType"), scala.Some.apply("ReturnType"), scala.Some.apply(kinds)),
|
||||
TIMEOUT);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws RunnerException {
|
||||
Options opt =
|
||||
new OptionsBuilder().include(SuggestionsRepoBenchmark.class.getSimpleName()).build();
|
||||
|
||||
new Runner(opt).run();
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
searcher {
|
||||
db {
|
||||
url = "jdbc:sqlite:searcher.db"
|
||||
driver = org.sqlite.JDBC
|
||||
connectionPool = disabled
|
||||
keepAliveConnection = true
|
||||
}
|
||||
}
|
7
lib/scala/searcher/src/main/resources/reference.conf
Normal file
7
lib/scala/searcher/src/main/resources/reference.conf
Normal file
@ -0,0 +1,7 @@
|
||||
searcher {
|
||||
db {
|
||||
url = "jdbc:sqlite::memory:"
|
||||
driver = "org.sqlite.JDBC"
|
||||
connectionPool = "HikariCP"
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
@ -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]])]
|
||||
}
|
||||
|
@ -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("\\", "\\\\")
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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(_))
|
||||
|
@ -1,6 +0,0 @@
|
||||
searcher.db {
|
||||
url = "jdbc:sqlite:file::memory:?cache=shared"
|
||||
}
|
||||
akka {
|
||||
loglevel = "ERROR"
|
||||
}
|
@ -8,6 +8,9 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="slick" level="INFO"/>
|
||||
<logger name="slick.compiler" level="INFO"/>
|
||||
|
||||
<root level="ERROR">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user