Implement Suggestions Updates API (#930)

This commit is contained in:
Dmitry Bushev 2020-06-26 19:52:42 +03:00 committed by GitHub
parent f0551f7693
commit 8ecc786be6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 916 additions and 155 deletions

View File

@ -626,6 +626,7 @@ lazy val `polyglot-api` = project
)
.dependsOn(pkg)
.dependsOn(`text-buffer`)
.dependsOn(`searcher`)
lazy val `language-server` = (project in file("engine/language-server"))
.settings(
@ -663,6 +664,7 @@ lazy val `language-server` = (project in file("engine/language-server"))
.dependsOn(`json-rpc-server`)
.dependsOn(`json-rpc-server-test` % Test)
.dependsOn(`text-buffer`)
.dependsOn(`searcher`)
lazy val runtime = (project in file("engine/runtime"))
.configs(Benchmark)

View File

@ -55,7 +55,7 @@ transport formats, please look [here](./protocol-architecture).
- [`file/receivesTreeUpdates`](#filereceivestreeupdates)
- [`executionContext/canModify`](#executioncontextcanmodify)
- [`executionContext/receivesUpdates`](#executioncontextreceivesupdates)
- [`search/receivesSuggestionsDatabaseUpdates`](#receivessuggestionsdatabaseupdates)
- [`search/receivesSuggestionsDatabaseUpdates`](#searchreceivessuggestionsdatabaseupdates)
- [File Management Operations](#file-management-operations)
- [`file/write`](#filewrite)
- [`file/read`](#fileread)
@ -162,6 +162,10 @@ An identifier used for execution contexts.
type ContextId = UUID;
```
```typescript
type SuggestionEntryId = number;
```
### `StackItem`
A representation of an executable position in code, used by the execution APIs.
@ -236,21 +240,19 @@ The argument of a [`SuggestionEntry`](#suggestionentry).
#### Format
``` idl
namespace org.enso.languageserver.protocol.binary;
```typescript
// The argument of an atom, method or function suggestion
table SuggestionEntryArgument {
interface SuggestionEntryArgument {
// The argument name
name: string (required);
name: string;
// The arguement type. String 'Any' is used to specify genric types
type: string (required);
type: string;
// Indicates whether the argument is lazy
isSuspended: bool (required);
isSuspended: bool;
// Indicates whether the argument has default value
hasDefault: bool (required);
hasDefault: bool;
// Optional default value
defaultValue: string;
defaultValue?: string;
}
```
@ -259,46 +261,58 @@ The language construct that can be returned as a suggestion.
#### Format
``` idl
namespace org.enso.languageserver.protocol.binary;
```typescript
// The definition scope
interface SuggestionEntryScope {
// The start of the definition scope
start: number;
// The end of the definition scope
end: number;
}
// A type of suggestion entries.
union SuggestionEntry {
type SuggestionEntry
// A value constructor
SuggestionEntryAtom,
= SuggestionEntryAtom
// A method defined on a type
SuggestionEntryMethod,
| SuggestionEntryMethod
// A function
SuggestionEntryFunction,
| SuggestionEntryFunction
// A local value
SuggestionEntryLocal
| SuggestionEntryLocal;
}
table SuggestionEntryAtom {
name: string (required);
arguments: [SuggestionEntryArgument] (required);
returnType: string (required);
documentation: string;
interface SuggestionEntryAtom {
name: string;
module: string;
arguments: [SuggestionEntryArgument];
returnType: string;
documentation?: string;
}
table SuggestionEntryMethod {
name: string (required);
arguments: [SuggestionEntryArgument] (required);
selfType: string (required);
returnType: string (required);
documentation: string;
interface SuggestionEntryMethod {
name: string;
module: string;
arguments: [SuggestionEntryArgument];
selfType: string;
returnType: string;
documentation?: string;
}
table SuggestionEntryFunction {
name: string (required);
arguments: [SuggestionEntryArgument] (required);
returnType: string (required);
documentation: string;
interface SuggestionEntryFunction {
name: string;
module: string;
arguments: [SuggestionEntryArgument];
returnType: string;
scope: SuggestionEntryScope;
}
table SuggestionEntryLocal {
name: string (required);
returnType: string (required);
interface SuggestionEntryLocal {
name: string;
module: string;
returnType: string;
scope: SuggestionEntryScope;
}
```
@ -307,16 +321,13 @@ The suggestion entry type that is used as a filter in search requests.
#### Format
``` idl
namespace org.enso.languageserver.protocol.binary;
```typescript
// The kind of a suggestion.
enum SuggestionEntryType : byte {
Atom,
Method,
Function,
Local
}
type SuggestionEntryType
= Atom
| Method
| Function
| Local;
```
### `SuggestionsDatabaseEntry`
@ -324,14 +335,12 @@ The entry in the suggestions database.
#### Format
``` idl
namespace org.enso.languageserver.protocol.binary;
```typescript
// The suggestions database entry.
table SuggestionsDatabaseEntry {
interface SuggestionsDatabaseEntry {
// suggestion entry id;
id: int64 (required);
suggestion: Suggestion (required);
id: number;
suggestion: Suggestion;
}
```
@ -340,38 +349,24 @@ The update of the suggestions database.
#### Format
``` idl
namespace org.enso.languageserver.protocol.binary;
```typescript
// The kind of the suggestions database update.
union SuggestionsDatabaseUpdate {
// Create or replace the database entry
SuggestionsDatabaseUpdateInsert,
// Update the entry fields
SuggestionsDatabaseUpdateModify,
// Remove the database Entry
SuggestionsDatabaseUpdateRemove
}
type SuggestionsDatabaseUpdateKind
= Add
| Update
| Delete
table SuggestionsDatabaseUpdateInsert {
interface SuggestionsDatabaseUpdate {
// suggestion entry id
id: int64 (required);
suggestion: SuggestionEntry (required);
}
table SuggestionsDatabaseUpdateModify {
// suggestion entry id
id: int64 (required);
name: string;
arguments: [SuggestionEntryArgument];
selfType: string;
returnType: string;
documentation: string;
}
table SuggestionsDatabaseUpdateRemove {
// suggestion entry id
id: int64 (required);
id: number;
kind: SuggestionsDatabaseUpdateKind;
name?: string;
module?: string;
arguments?: [SuggestionEntryArgument];
selfType?: string;
returnType?: string;
documentation?: string;
scope?: SuggestionEntryScope;
}
```
@ -895,7 +890,7 @@ This capability states that the client receives the search database updates for
a given execution context.
- **method:** `search/receivesSuggestionsDatabaseUpdates`
- **registerOptions:** `{ contextId: ContextId; }`
- **registerOptions:** `{}`
### Enables
- [`search/suggestionsDatabaseUpdate`](#suggestionsdatabaseupdate)
@ -2131,7 +2126,7 @@ None
### `executionContext/executionFailed`
Sent from the server to the client to inform about a failure during execution of
an execution context.
an execution context.
- **Type:** Notification
- **Direction:** Server -> Client
@ -2448,23 +2443,17 @@ Sent from client to the server to receive the full suggestions database.
- **Visibility:** Public
#### Parameters
``` idl
namespace org.enso.languageserver.protocol.binary;
table GetSuggestionsDatabaseCommand {
contextId: EnsoUUID (required);
}
```typescript
null
```
#### Result
``` idl
namespace org.enso.languageserver.protocol.binary;
table GetSuggestionsDatabaseReply {
```typescript
{
// The list of suggestions database entries
entries: [SuggestionsDatabaseEntry] (required);
entries: [SuggestionsDatabaseEntry];
// The version of received suggestions database
currentVersion: int64 (required);
currentVersion: number;
}
```
@ -2481,21 +2470,15 @@ database.
- **Visibility:** Public
#### Parameters
``` idl
namespace org.enso.languageserver.protocol.binary;
table GetSuggestionsDatabaseVersionCommand {
contextId: EnsoUUID (required);
}
```typescript
null
```
#### Result
``` idl
namespace org.enso.languageserver.protocol.binary;
table GetSuggestionsDatabaseVersionReply {
```typescript
{
// The version of the suggestions database
currentVersion: int64 (required);
currentVersion: number;
}
```
@ -2513,16 +2496,10 @@ database.
#### Parameters
``` idl
namespace org.enso.languageserver.protocol.binary;
table SuggestionsDatabaseUpdate {
// The context id
contextId: EnsoUUID (required);
// The list of database updates to apply
updates: [SuggestionsDatabaseUpdate] (required);
// The version of suggestions database after applying the updates
currentVersion: int64 (required);
```typescript
{
updates: [SuggestionsDatabaseUpdate];
currentVersion: number;
}
```
@ -2539,32 +2516,26 @@ Sent from client to the server to receive the autocomplete suggestion.
#### Parameters
``` idl
namespace org.enso.languageserver.protocol.binary;
table SearchCompletionCommand {
// The context id
contextId: EnsoUUID (required);
```typescript
{
// The edited file
file: Path (required);
file: Path;
// The cursor position
position: Position (required);
position: Position;
// Filter by methods with the provided self type
selfType: string;
selfType?: string;
// Filter by the return type
returnType: string;
returnType?: string;
// Filter by the suggestion types
tags: [SuggestionEntryType];
tags?: [SuggestionEntryType];
}
```
#### Result
``` idl
namespace org.enso.languageserver.protocol.binary;
table SearchCompletionReply {
results: [SuggestionEntryId] (required);
currentVersion: int64 (required);
```typescript
{
results: [SuggestionEntryId];
currentVersion: number;
}
```

View File

@ -27,7 +27,8 @@ import org.enso.languageserver.protocol.json.{
import org.enso.languageserver.runtime.{
ContextRegistry,
RuntimeConnector,
RuntimeKiller
RuntimeKiller,
SuggestionsDatabaseEventsListener
}
import org.enso.languageserver.session.SessionRouter
import org.enso.languageserver.text.BufferRegistry
@ -93,9 +94,16 @@ class MainModule(serverConfig: LanguageServerConfig) {
"file-event-registry"
)
lazy val suggestionsDatabaseEventsListener =
system.actorOf(SuggestionsDatabaseEventsListener.props(sessionRouter))
lazy val capabilityRouter =
system.actorOf(
CapabilityRouter.props(bufferRegistry, receivesTreeUpdatesHandler),
CapabilityRouter.props(
bufferRegistry,
receivesTreeUpdatesHandler,
suggestionsDatabaseEventsListener
),
"capability-router"
)

View File

@ -8,6 +8,7 @@ import org.enso.languageserver.capability.CapabilityProtocol.{
import org.enso.languageserver.data.{
CanEdit,
CapabilityRegistration,
ReceivesSuggestionsDatabaseUpdates,
ReceivesTreeUpdates
}
import org.enso.languageserver.monitoring.MonitoringProtocol.{Ping, Pong}
@ -20,10 +21,13 @@ import org.enso.languageserver.util.UnhandledLogging
* @param bufferRegistry the recipient of buffer capability requests
* @param receivesTreeUpdatesHandler the recipient of
* `receivesTreeUpdates` capability requests
* @param suggestionsDatabaseEventsListener the recipient of
* `receivesSuggestionsDatabaseUpdates` capability requests
*/
class CapabilityRouter(
bufferRegistry: ActorRef,
receivesTreeUpdatesHandler: ActorRef
receivesTreeUpdatesHandler: ActorRef,
suggestionsDatabaseEventsListener: ActorRef
) extends Actor
with ActorLogging
with UnhandledLogging {
@ -49,6 +53,18 @@ class CapabilityRouter(
CapabilityRegistration(ReceivesTreeUpdates(_))
) =>
receivesTreeUpdatesHandler.forward(msg)
case msg @ AcquireCapability(
_,
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
) =>
suggestionsDatabaseEventsListener.forward(msg)
case msg @ ReleaseCapability(
_,
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
) =>
suggestionsDatabaseEventsListener.forward(msg)
}
}
@ -59,12 +75,23 @@ object CapabilityRouter {
* Creates a configuration object used to create a [[CapabilityRouter]]
*
* @param bufferRegistry a buffer registry ref
* @param receivesTreeUpdatesHandler the recipient of `receivesTreeUpdates`
* capability requests
* @param suggestionsDatabaseEventsListener the recipient of
* `receivesSuggestionsDatabaseUpdates` capability requests
* @return a configuration object
*/
def props(
bufferRegistry: ActorRef,
receivesTreeUpdatesHandler: ActorRef
receivesTreeUpdatesHandler: ActorRef,
suggestionsDatabaseEventsListener: ActorRef
): Props =
Props(new CapabilityRouter(bufferRegistry, receivesTreeUpdatesHandler))
Props(
new CapabilityRouter(
bufferRegistry,
receivesTreeUpdatesHandler,
suggestionsDatabaseEventsListener
)
)
}

View File

@ -63,14 +63,22 @@ object Capability {
import io.circe.syntax._
implicit val encoder: Encoder[Capability] = {
case cap: CanEdit => cap.asJson
case cap: ReceivesTreeUpdates => cap.asJson
case cap: CanModify => cap.asJson
case cap: ReceivesUpdates => cap.asJson
case cap: CanEdit => cap.asJson
case cap: ReceivesTreeUpdates => cap.asJson
case cap: CanModify => cap.asJson
case cap: ReceivesUpdates => cap.asJson
case cap: ReceivesSuggestionsDatabaseUpdates => cap.asJson
}
}
case class ReceivesSuggestionsDatabaseUpdates()
extends Capability(ReceivesSuggestionsDatabaseUpdates.methodName)
object ReceivesSuggestionsDatabaseUpdates {
val methodName = "search/receivesSuggestionsDatabaseUpdates"
}
/**
* A capability registration object, used to identify acquired capabilities.
*
@ -99,14 +107,17 @@ object CapabilityRegistration {
def resolveOptions(
method: String,
json: Json
): Decoder.Result[Capability] = method match {
case CanEdit.methodName => json.as[CanEdit]
case ReceivesTreeUpdates.methodName => json.as[ReceivesTreeUpdates]
case CanModify.methodName => json.as[CanModify]
case ReceivesUpdates.methodName => json.as[ReceivesUpdates]
case _ =>
Left(DecodingFailure("Unrecognized capability method.", List()))
}
): Decoder.Result[Capability] =
method match {
case CanEdit.methodName => json.as[CanEdit]
case ReceivesTreeUpdates.methodName => json.as[ReceivesTreeUpdates]
case CanModify.methodName => json.as[CanModify]
case ReceivesUpdates.methodName => json.as[ReceivesUpdates]
case ReceivesSuggestionsDatabaseUpdates.methodName =>
json.as[ReceivesSuggestionsDatabaseUpdates]
case _ =>
Left(DecodingFailure("Unrecognized capability method.", List()))
}
for {
method <- json.downField(methodField).as[String]

View File

@ -39,7 +39,11 @@ import org.enso.languageserver.requesthandler.visualisation.{
DetachVisualisationHandler,
ModifyVisualisationHandler
}
import org.enso.languageserver.runtime.ContextRegistryProtocol
import org.enso.languageserver.runtime.{
ContextRegistryProtocol,
SearchApi,
SearchProtocol
}
import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.runtime.VisualisationApi.{
AttachVisualisation,
@ -170,6 +174,14 @@ class JsonConnectionController(
ExecutionContextExecutionFailed.Params(contextId, msg)
)
case SearchProtocol.SuggestionsDatabaseUpdateNotification(
updates,
version
) =>
webActor ! Notification(
SearchApi.SuggestionsDatabaseUpdates,
SearchApi.SuggestionsDatabaseUpdates.Params(updates, version)
)
case InputOutputProtocol.OutputAppended(output, outputKind) =>
outputKind match {
case StandardOutput =>
@ -189,7 +201,7 @@ class JsonConnectionController(
case InputOutputProtocol.WaitingForStandardInput =>
webActor ! Notification(InputOutputApi.WaitingForStandardInput, Unused)
case req @ Request(method, _, _) if (requestHandlers.contains(method)) =>
case req @ Request(method, _, _) if requestHandlers.contains(method) =>
val handler = context.actorOf(
requestHandlers(method),
s"request-handler-$method-${UUID.randomUUID()}"

View File

@ -21,6 +21,7 @@ import org.enso.languageserver.io.InputOutputApi.{
}
import org.enso.languageserver.monitoring.MonitoringApi.Ping
import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.runtime.SearchApi._
import org.enso.languageserver.runtime.VisualisationApi._
import org.enso.languageserver.session.SessionApi.InitProtocolConnection
import org.enso.languageserver.text.TextApi._
@ -71,5 +72,6 @@ object JsonRpc {
.registerNotification(StandardOutputAppended)
.registerNotification(StandardErrorAppended)
.registerNotification(WaitingForStandardInput)
.registerNotification(SuggestionsDatabaseUpdates)
}

View File

@ -0,0 +1,26 @@
package org.enso.languageserver.runtime
import org.enso.jsonrpc.{HasParams, Method}
import org.enso.languageserver.runtime.SearchProtocol.SuggestionsDatabaseUpdate
/**
* The execution JSON RPC API provided by the language server.
*
* @see `docs/language-server/protocol-language-server.md`
*/
object SearchApi {
case object SuggestionsDatabaseUpdates
extends Method("search/suggestionsDatabaseUpdates") {
case class Params(
updates: Seq[SuggestionsDatabaseUpdate],
currentVersion: Long
)
implicit val hasParams = new HasParams[this.type] {
type Params = SuggestionsDatabaseUpdates.Params
}
}
}

View File

@ -0,0 +1,160 @@
package org.enso.languageserver.runtime
import io.circe.generic.auto._
import io.circe.syntax._
import io.circe.{Decoder, Encoder, Json}
import org.enso.searcher.Suggestion
object SearchProtocol {
sealed trait SuggestionsDatabaseUpdate
object SuggestionsDatabaseUpdate {
/** Create or replace the database entry.
*
* @param id suggestion id
* @param suggestion the new suggestion
*/
case class Add(id: Long, 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
private object CodecField {
val Type = "type"
}
private object CodecType {
val Add = "Add"
val Delete = "Delete"
val Update = "Update"
}
implicit val decoder: Decoder[SuggestionsDatabaseUpdate] =
Decoder.instance { cursor =>
cursor.downField(CodecField.Type).as[String].flatMap {
case CodecType.Add =>
Decoder[SuggestionsDatabaseUpdate.Add].tryDecode(cursor)
case CodecType.Update =>
Decoder[SuggestionsDatabaseUpdate.Modify].tryDecode(cursor)
case CodecType.Delete =>
Decoder[SuggestionsDatabaseUpdate.Remove].tryDecode(cursor)
}
}
implicit val encoder: Encoder[SuggestionsDatabaseUpdate] =
Encoder.instance[SuggestionsDatabaseUpdate] {
case add: SuggestionsDatabaseUpdate.Add =>
Encoder[SuggestionsDatabaseUpdate.Add]
.apply(add)
.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))
}
private object SuggestionType {
val Atom = "atom"
val Method = "method"
val Function = "function"
val Local = "local"
}
implicit val suggestionEncoder: Encoder[Suggestion] =
Encoder.instance[Suggestion] {
case atom: Suggestion.Atom =>
Encoder[Suggestion.Atom]
.apply(atom)
.deepMerge(Json.obj(CodecField.Type -> SuggestionType.Atom.asJson))
.dropNullValues
case method: Suggestion.Method =>
Encoder[Suggestion.Method]
.apply(method)
.deepMerge(
Json.obj(CodecField.Type -> SuggestionType.Method.asJson)
)
.dropNullValues
case function: Suggestion.Function =>
Encoder[Suggestion.Function]
.apply(function)
.deepMerge(
Json.obj(CodecField.Type -> SuggestionType.Function.asJson)
)
.dropNullValues
case local: Suggestion.Local =>
Encoder[Suggestion.Local]
.apply(local)
.deepMerge(Json.obj(CodecField.Type -> SuggestionType.Local.asJson))
.dropNullValues
}
implicit val suggestionDecoder: Decoder[Suggestion] =
Decoder.instance { cursor =>
cursor.downField(CodecField.Type).as[String].flatMap {
case SuggestionType.Atom =>
Decoder[Suggestion.Atom].tryDecode(cursor)
case SuggestionType.Method =>
Decoder[Suggestion.Method].tryDecode(cursor)
case SuggestionType.Function =>
Decoder[Suggestion.Function].tryDecode(cursor)
case SuggestionType.Local =>
Decoder[Suggestion.Local].tryDecode(cursor)
}
}
}
case class SuggestionsDatabaseUpdateNotification(
updates: Seq[SuggestionsDatabaseUpdate],
currentVersion: Long
)
}

View File

@ -0,0 +1,107 @@
package org.enso.languageserver.runtime
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import org.enso.languageserver.capability.CapabilityProtocol.{
AcquireCapability,
CapabilityAcquired,
CapabilityReleased,
ReleaseCapability
}
import org.enso.languageserver.data.{
CapabilityRegistration,
ClientId,
ReceivesSuggestionsDatabaseUpdates
}
import org.enso.languageserver.runtime.SearchProtocol.{
SuggestionsDatabaseUpdate,
SuggestionsDatabaseUpdateNotification
}
import org.enso.languageserver.session.SessionRouter.DeliverToJsonController
import org.enso.languageserver.util.UnhandledLogging
import org.enso.polyglot.runtime.Runtime.Api
/**
* Event listener listens event stream for the suggestion database
* notifications from the runtime and sends updates to the client. The listener
* is a singleton and created per context registry.
*
* @param sessionRouter the session router
*/
final class SuggestionsDatabaseEventsListener(
sessionRouter: ActorRef
) extends Actor
with ActorLogging
with UnhandledLogging {
override def preStart(): Unit = {
context.system.eventStream
.subscribe(self, classOf[Api.SuggestionsDatabaseUpdateNotification])
}
override def receive: Receive = withClients(Set())
private def withClients(clients: Set[ClientId]): Receive = {
case AcquireCapability(
client,
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
) =>
sender() ! CapabilityAcquired
context.become(withClients(clients + client.clientId))
case ReleaseCapability(
client,
CapabilityRegistration(ReceivesSuggestionsDatabaseUpdates())
) =>
sender() ! CapabilityReleased
context.become(withClients(clients - client.clientId))
case msg: Api.SuggestionsDatabaseUpdateNotification =>
clients.foreach { clientId =>
sessionRouter ! DeliverToJsonController(
clientId,
SuggestionsDatabaseUpdateNotification(msg.updates.map(toUpdate), 0)
)
}
}
private def toUpdate(
update: Api.SuggestionsDatabaseUpdate
): SuggestionsDatabaseUpdate =
update match {
case Api.SuggestionsDatabaseUpdate.Add(id, suggestion) =>
SuggestionsDatabaseUpdate.Add(id, suggestion)
case Api.SuggestionsDatabaseUpdate.Modify(
id,
name,
arguments,
selfType,
returnType,
doc,
scope
) =>
SuggestionsDatabaseUpdate.Modify(
id,
name,
arguments,
selfType,
returnType,
doc,
scope
)
case Api.SuggestionsDatabaseUpdate.Remove(id) =>
SuggestionsDatabaseUpdate.Remove(id)
}
}
object SuggestionsDatabaseEventsListener {
/**
* Creates a configuration object used to create a
* [[SuggestionsDatabaseEventsListener]].
*
* @param sessionRouter the session router
*/
def props(sessionRouter: ActorRef): Props =
Props(new SuggestionsDatabaseEventsListener(sessionRouter))
}

View File

@ -141,4 +141,4 @@ table FileContentsReply {
}
//todo Split up the schema once Rust bugs will be resolved.
//todo Split up the schema once Rust bugs will be resolved.

View File

@ -26,7 +26,10 @@ import org.enso.languageserver.protocol.json.{
JsonConnectionControllerFactory,
JsonRpc
}
import org.enso.languageserver.runtime.ContextRegistry
import org.enso.languageserver.runtime.{
ContextRegistry,
SuggestionsDatabaseEventsListener
}
import org.enso.languageserver.session.SessionRouter
import org.enso.languageserver.text.BufferRegistry
@ -92,8 +95,18 @@ class BaseServerTest extends JsonRpcServerTestKit {
system.actorOf(
ContextRegistry.props(config, runtimeConnectorProbe.ref, sessionRouter)
)
lazy val capabilityRouter =
system.actorOf(CapabilityRouter.props(bufferRegistry, fileEventRegistry))
val suggestionsDatabaseEventsListener =
system.actorOf(SuggestionsDatabaseEventsListener.props(sessionRouter))
val capabilityRouter =
system.actorOf(
CapabilityRouter.props(
bufferRegistry,
fileEventRegistry,
suggestionsDatabaseEventsListener
)
)
new JsonConnectionControllerFactory(
bufferRegistry,

View File

@ -0,0 +1,352 @@
package org.enso.languageserver.websocket.json
import io.circe.literal._
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.searcher.Suggestion
class SuggestionsDatabaseEventsListenerTest extends BaseServerTest {
"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()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.atom))
)
)
client.expectJson(json"""
{ "jsonrpc" : "2.0",
"method" : "search/suggestionsDatabaseUpdates",
"params" : {
"updates" : [
{
"type" : "Add",
"id" : 0,
"suggestion" : {
"type" : "atom",
"name" : "MyType",
"arguments" : [
{
"name" : "a",
"reprType" : "Any",
"isSuspended" : false,
"hasDefault" : false,
"defaultValue" : null
}
],
"returnType" : "MyAtom"
}
}
],
"currentVersion" : 0
}
}
""")
}
"send suggestions database add method notifications" in {
val client = getInitialisedWsClient()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.method))
)
)
client.expectJson(json"""
{ "jsonrpc" : "2.0",
"method" : "search/suggestionsDatabaseUpdates",
"params" : {
"updates" : [
{
"type" : "Add",
"id" : 0,
"suggestion" : {
"type" : "method",
"name" : "foo",
"arguments" : [
{
"name" : "this",
"reprType" : "MyType",
"isSuspended" : false,
"hasDefault" : false,
"defaultValue" : null
},
{
"name" : "foo",
"reprType" : "Number",
"isSuspended" : false,
"hasDefault" : true,
"defaultValue" : "42"
}
],
"selfType" : "MyType",
"returnType" : "Number",
"documentation" : "My doc"
}
}
],
"currentVersion" : 0
}
}
""")
}
"send suggestions database add function notifications" in {
val client = getInitialisedWsClient()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.function))
)
)
client.expectJson(json"""
{ "jsonrpc" : "2.0",
"method" : "search/suggestionsDatabaseUpdates",
"params" : {
"updates" : [
{
"type" : "Add",
"id" : 0,
"suggestion" : {
"type" : "function",
"name" : "print",
"arguments" : [
],
"returnType" : "IO",
"scope" : {
"start" : 7,
"end" : 10
}
}
}
],
"currentVersion" : 0
}
}
""")
}
"send suggestions database add local notifications" in {
val client = getInitialisedWsClient()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Seq(Api.SuggestionsDatabaseUpdate.Add(0, suggestion.local))
)
)
client.expectJson(json"""
{ "jsonrpc" : "2.0",
"method" : "search/suggestionsDatabaseUpdates",
"params" : {
"updates" : [
{
"type" : "Add",
"id" : 0,
"suggestion" : {
"type" : "local",
"name" : "x",
"returnType" : "Number",
"scope" : {
"start" : 15,
"end" : 17
}
}
}
],
"currentVersion" : 0
}
}
""")
}
"send suggestions database modify notifications" in {
val client = getInitialisedWsClient()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
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))
)
)
)
)
client.expectJson(json"""
{ "jsonrpc" : "2.0",
"method" : "search/suggestionsDatabaseUpdates",
"params" : {
"updates" : [
{
"type" : "Update",
"id" : 0,
"name" : "foo",
"arguments" : [
{
"name" : "a",
"reprType" : "Any",
"isSuspended" : true,
"hasDefault" : false,
"defaultValue" : null
},
{
"name" : "b",
"reprType" : "Any",
"isSuspended" : false,
"hasDefault" : true,
"defaultValue" : "77"
}
],
"selfType" : "MyType",
"returnType" : "IO",
"scope" : {
"start" : 12,
"end" : 24
}
}
],
"currentVersion" : 0
}
}
""")
}
"send suggestions database remove notifications" in {
val client = getInitialisedWsClient()
client.send(json.acquireSuggestionsDatabaseUpdatesCapability(0))
client.expectJson(json.ok(0))
system.eventStream.publish(
Api.SuggestionsDatabaseUpdateNotification(
Seq(Api.SuggestionsDatabaseUpdate.Remove(101))
)
)
client.expectJson(json"""
{ "jsonrpc" : "2.0",
"method" : "search/suggestionsDatabaseUpdates",
"params" : {
"updates" : [
{
"type" : "Delete",
"id" : 101
}
],
"currentVersion" : 0
}
}
""")
}
}
object suggestion {
val atom: Suggestion.Atom =
Suggestion.Atom(
name = "MyType",
arguments = Seq(Suggestion.Argument("a", "Any", false, false, None)),
returnType = "MyAtom",
documentation = None
)
val method: Suggestion.Method =
Suggestion.Method(
name = "foo",
arguments = Seq(
Suggestion.Argument("this", "MyType", false, false, None),
Suggestion.Argument("foo", "Number", false, true, Some("42"))
),
selfType = "MyType",
returnType = "Number",
documentation = Some("My doc")
)
val function: Suggestion.Function =
Suggestion.Function(
name = "print",
arguments = Seq(),
returnType = "IO",
scope = Suggestion.Scope(7, 10)
)
val local: Suggestion.Local =
Suggestion.Local(
name = "x",
returnType = "Number",
scope = Suggestion.Scope(15, 17)
)
}
object json {
def acquireSuggestionsDatabaseUpdatesCapability(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"method": "capability/acquire",
"id": $reqId,
"params": {
"method": "search/receivesSuggestionsDatabaseUpdates",
"registerOptions": {}
}
}
"""
def releaseSuggestionsDatabaseUpdatesCapability(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"method": "capability/release",
"id": $reqId,
"params": {
"method": "search/receivesSuggestionsDatabaseUpdates",
"registerOptions": {}
}
}
"""
def ok(reqId: Long) =
json"""
{ "jsonrpc": "2.0",
"id": $reqId,
"result": null
}
"""
}
}

View File

@ -11,6 +11,7 @@ import com.fasterxml.jackson.module.scala.{
DefaultScalaModule,
ScalaObjectMapper
}
import org.enso.searcher.Suggestion
import org.enso.text.editing.model.TextEdit
import scala.util.Try
@ -150,6 +151,10 @@ object Runtime {
new JsonSubTypes.Type(
value = classOf[Api.RuntimeServerShutDown],
name = "runtimeServerShutDown"
),
new JsonSubTypes.Type(
value = classOf[Api.SuggestionsDatabaseUpdateNotification],
name = "suggestionsDatabaseUpdateNotification"
)
)
)
@ -297,6 +302,62 @@ object Runtime {
expression: String
)
/** A change in the suggestions database. */
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes(
Array(
new JsonSubTypes.Type(
value = classOf[SuggestionsDatabaseUpdate.Add],
name = "suggestionsDatabaseUpdateAdd"
),
new JsonSubTypes.Type(
value = classOf[SuggestionsDatabaseUpdate.Remove],
name = "suggestionsDatabaseUpdateRemove"
),
new JsonSubTypes.Type(
value = classOf[SuggestionsDatabaseUpdate.Modify],
name = "suggestionsDatabaseUpdateModify"
)
)
)
sealed trait SuggestionsDatabaseUpdate
object SuggestionsDatabaseUpdate {
/** Create or replace the database entry.
*
* @param id suggestion id
* @param suggestion the new suggestion
*/
case class Add(id: Long, 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
}
/**
* An event signaling a visualisation update.
*
@ -617,6 +678,15 @@ object Runtime {
*/
case class RuntimeServerShutDown() extends ApiResponse
/**
* A notification about the change in the suggestions database.
*
* @param updates the list of database updates
*/
case class SuggestionsDatabaseUpdateNotification(
updates: Seq[SuggestionsDatabaseUpdate]
) extends ApiNotification
private lazy val mapper = {
val factory = new CBORFactory()
val mapper = new ObjectMapper(factory) with ScalaObjectMapper