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

View File

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

View File

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

View File

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

View File

@ -67,10 +67,18 @@ object Capability {
case cap: ReceivesTreeUpdates => cap.asJson case cap: ReceivesTreeUpdates => cap.asJson
case cap: CanModify => cap.asJson case cap: CanModify => cap.asJson
case cap: ReceivesUpdates => 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. * A capability registration object, used to identify acquired capabilities.
* *
@ -99,11 +107,14 @@ object CapabilityRegistration {
def resolveOptions( def resolveOptions(
method: String, method: String,
json: Json json: Json
): Decoder.Result[Capability] = method match { ): Decoder.Result[Capability] =
method match {
case CanEdit.methodName => json.as[CanEdit] case CanEdit.methodName => json.as[CanEdit]
case ReceivesTreeUpdates.methodName => json.as[ReceivesTreeUpdates] case ReceivesTreeUpdates.methodName => json.as[ReceivesTreeUpdates]
case CanModify.methodName => json.as[CanModify] case CanModify.methodName => json.as[CanModify]
case ReceivesUpdates.methodName => json.as[ReceivesUpdates] case ReceivesUpdates.methodName => json.as[ReceivesUpdates]
case ReceivesSuggestionsDatabaseUpdates.methodName =>
json.as[ReceivesSuggestionsDatabaseUpdates]
case _ => case _ =>
Left(DecodingFailure("Unrecognized capability method.", List())) Left(DecodingFailure("Unrecognized capability method.", List()))
} }

View File

@ -39,7 +39,11 @@ import org.enso.languageserver.requesthandler.visualisation.{
DetachVisualisationHandler, DetachVisualisationHandler,
ModifyVisualisationHandler 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.ExecutionApi._
import org.enso.languageserver.runtime.VisualisationApi.{ import org.enso.languageserver.runtime.VisualisationApi.{
AttachVisualisation, AttachVisualisation,
@ -170,6 +174,14 @@ class JsonConnectionController(
ExecutionContextExecutionFailed.Params(contextId, msg) ExecutionContextExecutionFailed.Params(contextId, msg)
) )
case SearchProtocol.SuggestionsDatabaseUpdateNotification(
updates,
version
) =>
webActor ! Notification(
SearchApi.SuggestionsDatabaseUpdates,
SearchApi.SuggestionsDatabaseUpdates.Params(updates, version)
)
case InputOutputProtocol.OutputAppended(output, outputKind) => case InputOutputProtocol.OutputAppended(output, outputKind) =>
outputKind match { outputKind match {
case StandardOutput => case StandardOutput =>
@ -189,7 +201,7 @@ class JsonConnectionController(
case InputOutputProtocol.WaitingForStandardInput => case InputOutputProtocol.WaitingForStandardInput =>
webActor ! Notification(InputOutputApi.WaitingForStandardInput, Unused) 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( val handler = context.actorOf(
requestHandlers(method), requestHandlers(method),
s"request-handler-$method-${UUID.randomUUID()}" 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.monitoring.MonitoringApi.Ping
import org.enso.languageserver.runtime.ExecutionApi._ import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.runtime.SearchApi._
import org.enso.languageserver.runtime.VisualisationApi._ import org.enso.languageserver.runtime.VisualisationApi._
import org.enso.languageserver.session.SessionApi.InitProtocolConnection import org.enso.languageserver.session.SessionApi.InitProtocolConnection
import org.enso.languageserver.text.TextApi._ import org.enso.languageserver.text.TextApi._
@ -71,5 +72,6 @@ object JsonRpc {
.registerNotification(StandardOutputAppended) .registerNotification(StandardOutputAppended)
.registerNotification(StandardErrorAppended) .registerNotification(StandardErrorAppended)
.registerNotification(WaitingForStandardInput) .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

@ -26,7 +26,10 @@ import org.enso.languageserver.protocol.json.{
JsonConnectionControllerFactory, JsonConnectionControllerFactory,
JsonRpc JsonRpc
} }
import org.enso.languageserver.runtime.ContextRegistry import org.enso.languageserver.runtime.{
ContextRegistry,
SuggestionsDatabaseEventsListener
}
import org.enso.languageserver.session.SessionRouter import org.enso.languageserver.session.SessionRouter
import org.enso.languageserver.text.BufferRegistry import org.enso.languageserver.text.BufferRegistry
@ -92,8 +95,18 @@ class BaseServerTest extends JsonRpcServerTestKit {
system.actorOf( system.actorOf(
ContextRegistry.props(config, runtimeConnectorProbe.ref, sessionRouter) 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( new JsonConnectionControllerFactory(
bufferRegistry, 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, DefaultScalaModule,
ScalaObjectMapper ScalaObjectMapper
} }
import org.enso.searcher.Suggestion
import org.enso.text.editing.model.TextEdit import org.enso.text.editing.model.TextEdit
import scala.util.Try import scala.util.Try
@ -150,6 +151,10 @@ object Runtime {
new JsonSubTypes.Type( new JsonSubTypes.Type(
value = classOf[Api.RuntimeServerShutDown], value = classOf[Api.RuntimeServerShutDown],
name = "runtimeServerShutDown" name = "runtimeServerShutDown"
),
new JsonSubTypes.Type(
value = classOf[Api.SuggestionsDatabaseUpdateNotification],
name = "suggestionsDatabaseUpdateNotification"
) )
) )
) )
@ -297,6 +302,62 @@ object Runtime {
expression: String 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. * An event signaling a visualisation update.
* *
@ -617,6 +678,15 @@ object Runtime {
*/ */
case class RuntimeServerShutDown() extends ApiResponse 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 = { private lazy val mapper = {
val factory = new CBORFactory() val factory = new CBORFactory()
val mapper = new ObjectMapper(factory) with ScalaObjectMapper val mapper = new ObjectMapper(factory) with ScalaObjectMapper