From 8ecc786be6975b09271231d692aacd53d19618f2 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Fri, 26 Jun 2020 19:52:42 +0300 Subject: [PATCH] Implement Suggestions Updates API (#930) --- build.sbt | 2 + .../protocol-language-server.md | 235 +++++------- .../enso/languageserver/boot/MainModule.scala | 12 +- .../capability/CapabilityRouter.scala | 33 +- .../enso/languageserver/data/Capability.scala | 35 +- .../json/JsonConnectionController.scala | 16 +- .../protocol/json/JsonRpc.scala | 2 + .../languageserver/runtime/SearchApi.scala | 26 ++ .../runtime/SearchProtocol.scala | 160 ++++++++ .../SuggestionsDatabaseEventsListener.scala | 107 ++++++ .../src/main/schema/binary_protocol.fbs | 2 +- .../websocket/json/BaseServerTest.scala | 19 +- ...uggestionsDatabaseEventsListenerTest.scala | 352 ++++++++++++++++++ .../org/enso/polyglot/runtime/Runtime.scala | 70 ++++ 14 files changed, 916 insertions(+), 155 deletions(-) create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchApi.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchProtocol.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/runtime/SuggestionsDatabaseEventsListener.scala create mode 100644 engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SuggestionsDatabaseEventsListenerTest.scala diff --git a/build.sbt b/build.sbt index 48e05f6da63..e7a20e187e0 100644 --- a/build.sbt +++ b/build.sbt @@ -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) diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index c604d8c03f0..cd026130278 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -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; } ``` diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala index 23931dbd274..e6772a1244f 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/boot/MainModule.scala @@ -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" ) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/capability/CapabilityRouter.scala b/engine/language-server/src/main/scala/org/enso/languageserver/capability/CapabilityRouter.scala index 6349cbe4e89..9beaa0febb1 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/capability/CapabilityRouter.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/capability/CapabilityRouter.scala @@ -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 + ) + ) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/data/Capability.scala b/engine/language-server/src/main/scala/org/enso/languageserver/data/Capability.scala index 986092edb96..4155e68a699 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/data/Capability.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/data/Capability.scala @@ -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] diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala index 0c5ee5fb2e4..704be498ec4 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala @@ -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()}" diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala index e39b4fc6bb3..fd51c04b14e 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala @@ -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) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchApi.scala new file mode 100644 index 00000000000..72fad69c604 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchApi.scala @@ -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 + } + } + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchProtocol.scala new file mode 100644 index 00000000000..6e7e4086bf0 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SearchProtocol.scala @@ -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 + ) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SuggestionsDatabaseEventsListener.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SuggestionsDatabaseEventsListener.scala new file mode 100644 index 00000000000..f3041a19ec3 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/SuggestionsDatabaseEventsListener.scala @@ -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)) + +} diff --git a/engine/language-server/src/main/schema/binary_protocol.fbs b/engine/language-server/src/main/schema/binary_protocol.fbs index 301e922c117..d9300cd8aee 100644 --- a/engine/language-server/src/main/schema/binary_protocol.fbs +++ b/engine/language-server/src/main/schema/binary_protocol.fbs @@ -141,4 +141,4 @@ table FileContentsReply { } -//todo Split up the schema once Rust bugs will be resolved. \ No newline at end of file +//todo Split up the schema once Rust bugs will be resolved. diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala index 1c2afacf307..0e7d23d7a09 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/BaseServerTest.scala @@ -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, diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SuggestionsDatabaseEventsListenerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SuggestionsDatabaseEventsListenerTest.scala new file mode 100644 index 00000000000..803d7161682 --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/SuggestionsDatabaseEventsListenerTest.scala @@ -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 + } + """ + } + +} diff --git a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala index 5f24f96e96c..a489a206612 100644 --- a/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala +++ b/engine/polyglot-api/src/main/scala/org/enso/polyglot/runtime/Runtime.scala @@ -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