From 8db7bf3114892a642f3f7209f2ee541b33e48e0e Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Thu, 20 May 2021 16:50:12 +0300 Subject: [PATCH 1/4] feat: executeVisualisation request --- .../protocol-language-server.md | 42 +++++++++ .../json/JsonConnectionController.scala | 6 +- .../protocol/json/JsonRpc.scala | 1 + .../ExecuteVisualisationHandler.scala | 90 +++++++++++++++++++ .../runtime/ContextEventsListener.scala | 46 +++++++++- .../runtime/ContextRegistry.scala | 31 +++++-- .../runtime/ContextRegistryProtocol.scala | 44 ++++++++- .../runtime/VisualisationApi.scala | 17 ++++ 8 files changed, 264 insertions(+), 13 deletions(-) create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteVisualisationHandler.scala diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index 67d3827145f4..2cbc7763a826 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -115,6 +115,7 @@ transport formats, please look [here](./protocol-architecture). - [`executionContext/expressionUpdates`](#executioncontextexpressionupdates) - [`executionContext/executionFailed`](#executioncontextexecutionfailed) - [`executionContext/executionStatus`](#executioncontextexecutionstatus) + - [`executionContext/executeVisualisation`](#executioncontextexecutevisualisation) - [`executionContext/attachVisualisation`](#executioncontextattachvisualisation) - [`executionContext/detachVisualisation`](#executioncontextdetachvisualisation) - [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation) @@ -1280,6 +1281,7 @@ destroying the context. - [`executionContext/recompute`](#executioncontextrecompute) - [`executionContext/push`](#executioncontextpush) - [`executionContext/pop`](#executioncontextpop) +- [`executionContext/executeVisualisation`](#executioncontextexecutevisualisation) - [`executionContext/attachVisualisation`](#executioncontextattachvisualisation) - [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation) - [`executionContext/detachVisualisation`](#executioncontextdetachvisualisation) @@ -2751,6 +2753,46 @@ Sent from the server to the client to inform about a status of execution. None +### `executionContext/executeVisualisation` + +This message allows the client to execute a visualisation on a given node, +potentially preprocessing it by some arbitrary Enso code. Unlike +[`executionContext/attachVisualisation`](#executioncontextattachvisualisation), +visualisation expression will be executed only once. + +- **Type:** Request +- **Direction:** Client -> Server +- **Connection:** Protocol +- **Visibility:** Public + +#### Parameters + +```typescript +interface ExecuteVisualisationRequest { + visualisationId: UUID; + expressionId: UUID; + visualisationConfig: VisualisationConfiguration; +} +``` + +#### Result + +```typescript +null; +``` + +#### Errors + +- [`AccessDeniedError`](#accessdeniederror) when the user does not hold the + `executionContext/canModify` capability for this context. +- [`ContextNotFoundError`](#contextnotfounderror) when context can not be found + by provided id. +- [`ModuleNotFoundError`](#modulenotfounderror) to signal that the module with + the visualisation cannot be found. +- [`VisualisationExpressionError`](#visualisationexpressionerror) to signal that + the expression specified in the `VisualisationConfiguration` cannot be + evaluated. + ### `executionContext/attachVisualisation` This message allows the client to attach a visualisation, potentially 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 eb7f6faa73ae..4f208139d4ac 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 @@ -38,6 +38,7 @@ import org.enso.languageserver.requesthandler.text._ import org.enso.languageserver.requesthandler.visualisation.{ AttachVisualisationHandler, DetachVisualisationHandler, + ExecuteVisualisationHandler, ModifyVisualisationHandler } import org.enso.languageserver.runtime.ContextRegistryProtocol @@ -45,6 +46,7 @@ import org.enso.languageserver.runtime.ExecutionApi._ import org.enso.languageserver.runtime.VisualisationApi.{ AttachVisualisation, DetachVisualisation, + ExecuteVisualisation, ModifyVisualisation } import org.enso.languageserver.search.SearchApi._ @@ -94,7 +96,7 @@ class JsonConnectionController( import context.dispatcher - implicit val timeout = Timeout(requestTimeout) + implicit val timeout: Timeout = Timeout(requestTimeout) override def receive: Receive = { case JsonRpcServer.WebConnect(webActor) => @@ -333,6 +335,8 @@ class JsonConnectionController( Completion -> search.CompletionHandler .props(requestTimeout, suggestionsHandler), Import -> search.ImportHandler.props(requestTimeout, suggestionsHandler), + ExecuteVisualisation -> ExecuteVisualisationHandler + .props(rpcSession.clientId, requestTimeout, contextRegistry), AttachVisualisation -> AttachVisualisationHandler .props(rpcSession.clientId, requestTimeout, contextRegistry), DetachVisualisation -> DetachVisualisationHandler 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 3ef78769fbcb..3c05e80327f5 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 @@ -52,6 +52,7 @@ object JsonRpc { .registerRequest(ExecutionContextPush) .registerRequest(ExecutionContextPop) .registerRequest(ExecutionContextRecompute) + .registerRequest(ExecuteVisualisation) .registerRequest(AttachVisualisation) .registerRequest(DetachVisualisation) .registerRequest(ModifyVisualisation) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteVisualisationHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteVisualisationHandler.scala new file mode 100644 index 000000000000..8625c3cfece1 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteVisualisationHandler.scala @@ -0,0 +1,90 @@ +package org.enso.languageserver.requesthandler.visualisation + +import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props} +import org.enso.jsonrpc._ +import org.enso.languageserver.data.ClientId +import org.enso.languageserver.requesthandler.RequestTimeout +import org.enso.languageserver.runtime.VisualisationApi.ExecuteVisualisation +import org.enso.languageserver.runtime.{ + ContextRegistryProtocol, + RuntimeFailureMapper +} +import org.enso.languageserver.util.UnhandledLogging + +import scala.concurrent.duration.FiniteDuration + +/** A request handler for `executionContext/executeVisualisation` command. + * + * @param clientId an unique identifier of the client + * @param timeout request timeout + * @param contextRegistry a reference to the context registry. + */ +class ExecuteVisualisationHandler( + clientId: ClientId, + timeout: FiniteDuration, + contextRegistry: ActorRef +) extends Actor + with ActorLogging + with UnhandledLogging { + + import context.dispatcher + + override def receive: Receive = requestStage + + private def requestStage: Receive = { + case Request( + ExecuteVisualisation, + id, + params: ExecuteVisualisation.Params + ) => + contextRegistry ! ContextRegistryProtocol.ExecuteVisualisation( + clientId, + params.visualisationId, + params.expressionId, + params.visualisationConfig + ) + val cancellable = + context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout) + context.become(responseStage(id, sender(), cancellable)) + } + + private def responseStage( + id: Id, + replyTo: ActorRef, + cancellable: Cancellable + ): Receive = { + case RequestTimeout => + log.error("Request [{}] timed out.", id) + replyTo ! ResponseError(Some(id), Errors.RequestTimeout) + context.stop(self) + + case ContextRegistryProtocol.VisualisationAttached => + replyTo ! ResponseResult(ExecuteVisualisation, id, Unused) + cancellable.cancel() + context.stop(self) + + case error: ContextRegistryProtocol.Failure => + replyTo ! ResponseError(Some(id), RuntimeFailureMapper.mapFailure(error)) + cancellable.cancel() + context.stop(self) + } + +} + +object ExecuteVisualisationHandler { + + /** Creates configuration object used to create an + * [[ExecuteVisualisationHandler]]. + * + * @param clientId an unique identifier of the client + * @param timeout request timeout + * @param runtime a reference to the context registry + */ + def props( + clientId: ClientId, + timeout: FiniteDuration, + runtime: ActorRef + ): Props = + Props(new ExecuteVisualisationHandler(clientId, timeout, runtime)) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala index fe1a30915127..49689a7f4dc2 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextEventsListener.scala @@ -4,6 +4,8 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props} import akka.pattern.pipe import org.enso.languageserver.data.Config import org.enso.languageserver.runtime.ContextRegistryProtocol.{ + DetachVisualisation, + RegisterOneshotVisualisation, VisualisationContext, VisualisationUpdate } @@ -60,9 +62,31 @@ final class ContextEventsListener( } } - override def receive: Receive = withState(Vector()) + override def receive: Receive = withState(Set(), Vector()) + + def withState( + oneshotVisualisations: Set[Api.VisualisationContext], + expressionUpdates: Vector[Api.ExpressionUpdate] + ): Receive = { + + case RegisterOneshotVisualisation( + contextId, + visualisationId, + expressionId + ) => + val visualisationContext = + Api.VisualisationContext( + visualisationId, + contextId, + expressionId + ) + context.become( + withState( + oneshotVisualisations + visualisationContext, + expressionUpdates + ) + ) - def withState(expressionUpdates: Vector[Api.ExpressionUpdate]): Receive = { case Api.VisualisationUpdate(ctx, data) if ctx.contextId == contextId => val payload = VisualisationUpdate( @@ -74,10 +98,24 @@ final class ContextEventsListener( data ) sessionRouter ! DeliverToBinaryController(rpcSession.clientId, payload) + if (oneshotVisualisations.contains(ctx)) { + context.parent ! DetachVisualisation( + rpcSession.clientId, + contextId, + ctx.visualisationId, + ctx.expressionId + ) + context.become( + withState( + oneshotVisualisations - ctx, + expressionUpdates + ) + ) + } case Api.ExpressionUpdates(`contextId`, apiUpdates) => context.become( - withState(expressionUpdates :++ apiUpdates) + withState(oneshotVisualisations, expressionUpdates :++ apiUpdates) ) case Api.ExecutionFailed(`contextId`, error) => @@ -115,7 +153,7 @@ final class ContextEventsListener( case RunExpressionUpdates if expressionUpdates.nonEmpty => runExpressionUpdates(expressionUpdates) - context.become(withState(Vector())) + context.become(withState(oneshotVisualisations, Vector())) case RunExpressionUpdates if expressionUpdates.isEmpty => } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala index dc9f72d76032..aab1bb40d7e9 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala @@ -182,20 +182,42 @@ final class ContextRegistry( sender() ! AccessDenied } + case ExecuteVisualisation(clientId, visualisationId, expressionId, cfg) => + val contextId = cfg.executionContextId + if (store.hasContext(clientId, contextId)) { + store.getListener(contextId).foreach { listener => + listener ! RegisterOneshotVisualisation( + contextId, + visualisationId, + expressionId + ) + } + val handler = + context.actorOf( + AttachVisualisationHandler.props(config, timeout, runtime) + ) + handler.forward( + Api.AttachVisualisation( + visualisationId, + expressionId, + convertVisualisationConfig(cfg) + ) + ) + } else { + sender() ! AccessDenied + } + case AttachVisualisation(clientId, visualisationId, expressionId, cfg) => if (store.hasContext(clientId, cfg.executionContextId)) { val handler = context.actorOf( AttachVisualisationHandler.props(config, timeout, runtime) ) - - val configuration = convertVisualisationConfig(cfg) - handler.forward( Api.AttachVisualisation( visualisationId, expressionId, - configuration + convertVisualisationConfig(cfg) ) ) } else { @@ -213,7 +235,6 @@ final class ContextRegistry( context.actorOf( DetachVisualisationHandler.props(config, timeout, runtime) ) - handler.forward( Api.DetachVisualisation(contextId, visualisationId, expressionId) ) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala index e62dc05dd270..103d15241168 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala @@ -307,6 +307,45 @@ object ContextRegistryProtocol { diagnostics: Seq[ExecutionDiagnostic] ) + /** Requests the language server to execute a oneshot visualisation on an + * expression specified by `expressionId`. + * + * @param clientId the requester id + * @param visualisationId an identifier of a visualisation + * @param expressionId an identifier of an expression which is visualised + * @param visualisationConfig a configuration object for properties of the + * visualisation + */ + case class ExecuteVisualisation( + clientId: ClientId, + visualisationId: UUID, + expressionId: UUID, + visualisationConfig: VisualisationConfiguration + ) extends ToLogString { + + /** @inheritdoc */ + override def toLogString(shouldMask: Boolean): String = + "ExecuteVisualisation(" + + s"clientId=$clientId," + + s"visualisationId=$visualisationId," + + s"expressionId=$expressionId,visualisationConfig=" + + visualisationConfig.toLogString(shouldMask) + + ")" + } + + /** Registers a oneshot visualisation that will be detached after the first + * execution. + * + * @param contextId execution context identifier + * @param visualisationId an identifier of a visualisation + * @param expressionId an identifier of an expression which is visualised + */ + case class RegisterOneshotVisualisation( + contextId: ContextId, + visualisationId: UUID, + expressionId: UUID + ) + /** Requests the language server to attach a visualisation to the expression * specified by `expressionId`. * @@ -314,7 +353,7 @@ object ContextRegistryProtocol { * @param visualisationId an identifier of a visualisation * @param expressionId an identifier of an expression which is visualised * @param visualisationConfig a configuration object for properties of the - * visualisation + * visualisation */ case class AttachVisualisation( clientId: ClientId, @@ -333,8 +372,7 @@ object ContextRegistryProtocol { ")" } - /** Signals that attaching a visualisation has succeeded. - */ + /** Signals that attaching a visualisation has succeeded. */ case object VisualisationAttached /** Requests the language server to detach a visualisation from the expression diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala index 593288a92f52..5af0cc696d9d 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala @@ -10,6 +10,23 @@ import org.enso.jsonrpc.{HasParams, HasResult, Method, Unused} */ object VisualisationApi { + case object ExecuteVisualisation + extends Method("executionContext/executeVisualisation") { + + case class Params( + visualisationId: UUID, + expressionId: UUID, + visualisationConfig: VisualisationConfiguration + ) + + implicit val hasParams = new HasParams[this.type] { + type Params = ExecuteVisualisation.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = Unused.type + } + } + case object AttachVisualisation extends Method("executionContext/attachVisualisation") { From 73960060a6804c3e2a56607c3a0cd9f49621d253 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Fri, 21 May 2021 13:21:36 +0300 Subject: [PATCH 2/4] test: visualisations --- .../runtime/ContextEventsListenerSpec.scala | 137 +++++++++++++++--- .../websocket/json/ContextRegistryTest.scala | 109 ++++++++++++++ .../json/ExecutionContextJsonMessages.scala | 22 +++ 3 files changed, 246 insertions(+), 22 deletions(-) diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/runtime/ContextEventsListenerSpec.scala b/engine/language-server/src/test/scala/org/enso/languageserver/runtime/ContextEventsListenerSpec.scala index 76852304e5a8..6f27d8e763d1 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/runtime/ContextEventsListenerSpec.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/runtime/ContextEventsListenerSpec.scala @@ -3,6 +3,7 @@ package org.enso.languageserver.runtime import java.io.File import java.nio.file.Files import java.util.UUID + import akka.actor.{ActorRef, ActorSystem} import akka.testkit.{ImplicitSender, TestKit, TestProbe} import org.apache.commons.io.FileUtils @@ -15,12 +16,14 @@ import org.enso.languageserver.data.{ } import org.enso.languageserver.event.InitializedEvent import org.enso.languageserver.runtime.ContextRegistryProtocol.{ + DetachVisualisation, ExecutionDiagnostic, ExecutionDiagnosticKind, ExecutionDiagnosticNotification, ExecutionFailedNotification, ExecutionFailure, ExpressionUpdatesNotification, + RegisterOneshotVisualisation, VisualisationContext, VisualisationEvaluationFailed, VisualisationUpdate @@ -61,12 +64,13 @@ class ContextEventsListenerSpec "ContextEventsListener" should { - "not send empty updates" taggedAs Retry in withDb { (_, _, _, router, _) => - router.expectNoMessage() + "not send empty updates" taggedAs Retry in withDb { + (_, _, _, router, _, _) => + router.expectNoMessage() } "send expression updates" taggedAs Retry in withDb { - (clientId, contextId, repo, router, listener) => + (clientId, contextId, repo, router, _, listener) => val (_, suggestionIds) = Await.result( repo.insertAll( Seq( @@ -120,7 +124,7 @@ class ContextEventsListenerSpec } "send dataflow error updates" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, _, listener) => listener ! Api.ExpressionUpdates( contextId, Set( @@ -159,7 +163,7 @@ class ContextEventsListenerSpec } "send runtime error updates" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, _, listener) => listener ! Api.ExpressionUpdates( contextId, Set( @@ -196,7 +200,7 @@ class ContextEventsListenerSpec } "send expression updates grouped" taggedAs Retry in withDb(0.seconds) { - (clientId, contextId, repo, router, listener) => + (clientId, contextId, repo, router, _, listener) => Await.result( repo.insertAll( Seq( @@ -267,16 +271,89 @@ class ContextEventsListenerSpec ) } + "register oneshot visualization" taggedAs Retry in withDb { + (clientId, contextId, _, router, registry, listener) => + val ctx = Api.VisualisationContext( + UUID.randomUUID(), + contextId, + UUID.randomUUID() + ) + + listener ! RegisterOneshotVisualisation( + ctx.contextId, + ctx.visualisationId, + ctx.expressionId + ) + + val data1 = Array[Byte](1, 2, 3) + listener ! Api.VisualisationUpdate(ctx, data1) + router.expectMsg( + DeliverToBinaryController( + clientId, + VisualisationUpdate( + VisualisationContext( + ctx.visualisationId, + ctx.contextId, + ctx.expressionId + ), + data1 + ) + ) + ) + registry.expectMsg( + DetachVisualisation( + clientId, + ctx.contextId, + ctx.visualisationId, + ctx.expressionId + ) + ) + + val data2 = Array[Byte](2, 3, 4) + listener ! Api.VisualisationUpdate(ctx, data2) + router.expectMsg( + DeliverToBinaryController( + clientId, + VisualisationUpdate( + VisualisationContext( + ctx.visualisationId, + ctx.contextId, + ctx.expressionId + ), + data2 + ) + ) + ) + registry.expectNoMessage() + } + "send visualization updates" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, registry, listener) => val ctx = Api.VisualisationContext( UUID.randomUUID(), contextId, UUID.randomUUID() ) - val data = Array[Byte](1, 2, 3) - listener ! Api.VisualisationUpdate(ctx, data) + val data1 = Array[Byte](1, 2, 3) + listener ! Api.VisualisationUpdate(ctx, data1) + router.expectMsg( + DeliverToBinaryController( + clientId, + VisualisationUpdate( + VisualisationContext( + ctx.visualisationId, + ctx.contextId, + ctx.expressionId + ), + data1 + ) + ) + ) + registry.expectNoMessage() + + val data2 = Array[Byte](2, 3, 4) + listener ! Api.VisualisationUpdate(ctx, data2) router.expectMsg( DeliverToBinaryController( clientId, @@ -286,14 +363,15 @@ class ContextEventsListenerSpec ctx.contextId, ctx.expressionId ), - data + data2 ) ) ) + registry.expectNoMessage() } "send execution failed notification" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, _, listener) => val message = "Test execution failed" listener ! Api.ExecutionFailed( contextId, @@ -315,7 +393,7 @@ class ContextEventsListenerSpec } "send execution update notification" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, _, listener) => val message = "Test execution failed" listener ! Api.ExecutionUpdate( contextId, @@ -343,7 +421,7 @@ class ContextEventsListenerSpec } "send visualisation evaluation failed notification" taggedAs Retry in withDb { - (clientId, contextId, _, router, listener) => + (clientId, contextId, _, router, _, listener) => val message = "Test visualisation evaluation failed" val visualisationId = UUID.randomUUID() val expressionId = UUID.randomUUID() @@ -384,21 +462,36 @@ class ContextEventsListenerSpec JsonSession(clientId, TestProbe().ref) def withDb( - test: (UUID, UUID, SuggestionsRepo[Future], TestProbe, ActorRef) => Any + test: ( + UUID, + UUID, + SuggestionsRepo[Future], + TestProbe, + TestProbe, + ActorRef + ) => Any ): Unit = withDb(100.millis)(test) def withDb(updatesSendRate: FiniteDuration)( - test: (UUID, UUID, SuggestionsRepo[Future], TestProbe, ActorRef) => Any + test: ( + UUID, + UUID, + SuggestionsRepo[Future], + TestProbe, + TestProbe, + ActorRef + ) => Any ): Unit = { val testContentRoot = Files.createTempDirectory(null).toRealPath() sys.addShutdownHook(FileUtils.deleteQuietly(testContentRoot.toFile)) - val config = newConfig(testContentRoot.toFile) - val clientId = UUID.randomUUID() - val contextId = UUID.randomUUID() - val router = TestProbe("session-router") - val repo = SqlSuggestionsRepo(config.directories.suggestionsDatabaseFile) - val listener = system.actorOf( + val config = newConfig(testContentRoot.toFile) + val clientId = UUID.randomUUID() + val contextId = UUID.randomUUID() + val router = TestProbe("session-router") + val contextRegistry = TestProbe("context-registry") + val repo = SqlSuggestionsRepo(config.directories.suggestionsDatabaseFile) + val listener = contextRegistry.childActorOf( ContextEventsListener.props( config, repo, @@ -419,7 +512,7 @@ class ContextEventsListenerSpec } Await.ready(repoInit, Timeout) - try test(clientId, contextId, repo, router, listener) + try test(clientId, contextId, repo, router, contextRegistry, listener) finally { system.stop(listener) repo.close() diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala index 754d48382628..3b3c9f192b24 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala @@ -453,6 +453,115 @@ class ContextRegistryTest extends BaseServerTest { client.expectJson(json.ok(3)) } + "successfully execute visualisation" in { + val client = getInitialisedWsClient() + + // create context + client.send(json.executionContextCreateRequest(1)) + val (requestId, contextId) = + runtimeConnectorProbe.receiveN(1).head match { + case Api.Request(requestId, Api.CreateContextRequest(contextId)) => + (requestId, contextId) + case msg => + fail(s"Unexpected message: $msg") + } + runtimeConnectorProbe.lastSender ! Api.Response( + requestId, + Api.CreateContextResponse(contextId) + ) + client.expectJson(json.executionContextCreateResponse(1, contextId)) + + // attach visualisation + val visualisationId = UUID.randomUUID() + val expressionId = UUID.randomUUID() + val config = + VisualisationConfiguration(contextId, "Test.Main", ".to_json.to_text") + client.send( + json.executionContextExecuteVisualisationRequest( + 2, + visualisationId, + expressionId, + config + ) + ) + val requestId2 = + runtimeConnectorProbe.receiveN(1).head match { + case Api.Request( + requestId, + Api.AttachVisualisation( + `visualisationId`, + `expressionId`, + _ + ) + ) => + requestId + case msg => + fail(s"Unexpected message: $msg") + } + runtimeConnectorProbe.lastSender ! Api.Response( + requestId2, + Api.VisualisationAttached() + ) + client.expectJson(json.ok(2)) + } + + "return ModuleNotFound error when executing visualisation" in { + val client = getInitialisedWsClient() + + // create context + client.send(json.executionContextCreateRequest(1)) + val (requestId, contextId) = + runtimeConnectorProbe.receiveN(1).head match { + case Api.Request(requestId, Api.CreateContextRequest(contextId)) => + (requestId, contextId) + case msg => + fail(s"Unexpected message: $msg") + } + runtimeConnectorProbe.lastSender ! Api.Response( + requestId, + Api.CreateContextResponse(contextId) + ) + client.expectJson(json.executionContextCreateResponse(1, contextId)) + + // attach visualisation + val visualisationId = UUID.randomUUID() + val expressionId = UUID.randomUUID() + val config = + VisualisationConfiguration(contextId, "Test.Main", ".to_json.to_text") + client.send( + json.executionContextExecuteVisualisationRequest( + 2, + visualisationId, + expressionId, + config + ) + ) + val requestId2 = + runtimeConnectorProbe.receiveN(1).head match { + case Api.Request( + requestId, + Api.AttachVisualisation( + `visualisationId`, + `expressionId`, + _ + ) + ) => + requestId + case msg => + fail(s"Unexpected message: $msg") + } + runtimeConnectorProbe.lastSender ! Api.Response( + requestId2, + Api.ModuleNotFound(config.visualisationModule) + ) + client.expectJson( + json.executionContextModuleNotFound( + 2, + config.visualisationModule + ) + ) + } + "successfully attach visualisation" in { val client = getInitialisedWsClient() diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala index 76e9518825d5..251f8e227344 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala @@ -93,6 +93,28 @@ object ExecutionContextJsonMessages { } """ + def executionContextExecuteVisualisationRequest( + reqId: Int, + visualisationId: Api.VisualisationId, + expressionId: Api.ExpressionId, + configuration: VisualisationConfiguration + ) = + json""" + { "jsonrpc": "2.0", + "method": "executionContext/executeVisualisation", + "id": $reqId, + "params": { + "visualisationId": $visualisationId, + "expressionId": $expressionId, + "visualisationConfig": { + "executionContextId": ${configuration.executionContextId}, + "visualisationModule": ${configuration.visualisationModule}, + "expression": ${configuration.expression} + } + } + } + """ + def executionContextAttachVisualisationRequest( reqId: Int, visualisationId: Api.VisualisationId, From cbe8cd20bb528be91366ac5e42927f52f0c8316b Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Fri, 21 May 2021 13:34:37 +0300 Subject: [PATCH 3/4] misc: [no-changelog] From 7663648d5c83c2a5484faee7e3ff8ea8807ca66a Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Fri, 21 May 2021 14:04:17 +0300 Subject: [PATCH 4/4] refactor: rename to execute expression --- .../protocol-language-server.md | 17 ++++++++-------- .../json/JsonConnectionController.scala | 6 +++--- .../protocol/json/JsonRpc.scala | 2 +- ...r.scala => ExecuteExpressionHandler.scala} | 20 +++++++++---------- .../runtime/ContextRegistry.scala | 2 +- .../runtime/ContextRegistryProtocol.scala | 8 ++++---- .../runtime/VisualisationApi.scala | 6 +++--- .../websocket/json/ContextRegistryTest.scala | 8 ++++---- .../json/ExecutionContextJsonMessages.scala | 4 ++-- 9 files changed, 37 insertions(+), 36 deletions(-) rename engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/{ExecuteVisualisationHandler.scala => ExecuteExpressionHandler.scala} (79%) diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index 2cbc7763a826..44124147e395 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -115,7 +115,7 @@ transport formats, please look [here](./protocol-architecture). - [`executionContext/expressionUpdates`](#executioncontextexpressionupdates) - [`executionContext/executionFailed`](#executioncontextexecutionfailed) - [`executionContext/executionStatus`](#executioncontextexecutionstatus) - - [`executionContext/executeVisualisation`](#executioncontextexecutevisualisation) + - [`executionContext/executeExpression`](#executioncontextexecuteexpression) - [`executionContext/attachVisualisation`](#executioncontextattachvisualisation) - [`executionContext/detachVisualisation`](#executioncontextdetachvisualisation) - [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation) @@ -1281,7 +1281,7 @@ destroying the context. - [`executionContext/recompute`](#executioncontextrecompute) - [`executionContext/push`](#executioncontextpush) - [`executionContext/pop`](#executioncontextpop) -- [`executionContext/executeVisualisation`](#executioncontextexecutevisualisation) +- [`executionContext/executeExpression`](#executioncontextexecuteexpression) - [`executionContext/attachVisualisation`](#executioncontextattachvisualisation) - [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation) - [`executionContext/detachVisualisation`](#executioncontextdetachvisualisation) @@ -2753,12 +2753,13 @@ Sent from the server to the client to inform about a status of execution. None -### `executionContext/executeVisualisation` +### `executionContext/executeExpression` -This message allows the client to execute a visualisation on a given node, -potentially preprocessing it by some arbitrary Enso code. Unlike -[`executionContext/attachVisualisation`](#executioncontextattachvisualisation), -visualisation expression will be executed only once. +This message allows the client to execute an arbitrary expression on a given +node. It behaves like oneshot +[`executionContext/attachVisualisation`](#executioncontextattachvisualisation) +visualisation request, meaning that the visualisation expression will be +executed only once. - **Type:** Request - **Direction:** Client -> Server @@ -2768,7 +2769,7 @@ visualisation expression will be executed only once. #### Parameters ```typescript -interface ExecuteVisualisationRequest { +interface ExecuteExpressionRequest { visualisationId: UUID; expressionId: UUID; visualisationConfig: VisualisationConfiguration; 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 4f208139d4ac..d32931ef0d71 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 @@ -38,7 +38,7 @@ import org.enso.languageserver.requesthandler.text._ import org.enso.languageserver.requesthandler.visualisation.{ AttachVisualisationHandler, DetachVisualisationHandler, - ExecuteVisualisationHandler, + ExecuteExpressionHandler, ModifyVisualisationHandler } import org.enso.languageserver.runtime.ContextRegistryProtocol @@ -46,7 +46,7 @@ import org.enso.languageserver.runtime.ExecutionApi._ import org.enso.languageserver.runtime.VisualisationApi.{ AttachVisualisation, DetachVisualisation, - ExecuteVisualisation, + ExecuteExpression, ModifyVisualisation } import org.enso.languageserver.search.SearchApi._ @@ -335,7 +335,7 @@ class JsonConnectionController( Completion -> search.CompletionHandler .props(requestTimeout, suggestionsHandler), Import -> search.ImportHandler.props(requestTimeout, suggestionsHandler), - ExecuteVisualisation -> ExecuteVisualisationHandler + ExecuteExpression -> ExecuteExpressionHandler .props(rpcSession.clientId, requestTimeout, contextRegistry), AttachVisualisation -> AttachVisualisationHandler .props(rpcSession.clientId, requestTimeout, contextRegistry), 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 3c05e80327f5..52f3d87dd8ab 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 @@ -52,7 +52,7 @@ object JsonRpc { .registerRequest(ExecutionContextPush) .registerRequest(ExecutionContextPop) .registerRequest(ExecutionContextRecompute) - .registerRequest(ExecuteVisualisation) + .registerRequest(ExecuteExpression) .registerRequest(AttachVisualisation) .registerRequest(DetachVisualisation) .registerRequest(ModifyVisualisation) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteVisualisationHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteExpressionHandler.scala similarity index 79% rename from engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteVisualisationHandler.scala rename to engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteExpressionHandler.scala index 8625c3cfece1..c9c94ac0ae69 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteVisualisationHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/visualisation/ExecuteExpressionHandler.scala @@ -4,7 +4,7 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props} import org.enso.jsonrpc._ import org.enso.languageserver.data.ClientId import org.enso.languageserver.requesthandler.RequestTimeout -import org.enso.languageserver.runtime.VisualisationApi.ExecuteVisualisation +import org.enso.languageserver.runtime.VisualisationApi.ExecuteExpression import org.enso.languageserver.runtime.{ ContextRegistryProtocol, RuntimeFailureMapper @@ -13,13 +13,13 @@ import org.enso.languageserver.util.UnhandledLogging import scala.concurrent.duration.FiniteDuration -/** A request handler for `executionContext/executeVisualisation` command. +/** A request handler for `executionContext/executeExpression` command. * * @param clientId an unique identifier of the client * @param timeout request timeout * @param contextRegistry a reference to the context registry. */ -class ExecuteVisualisationHandler( +class ExecuteExpressionHandler( clientId: ClientId, timeout: FiniteDuration, contextRegistry: ActorRef @@ -33,11 +33,11 @@ class ExecuteVisualisationHandler( private def requestStage: Receive = { case Request( - ExecuteVisualisation, + ExecuteExpression, id, - params: ExecuteVisualisation.Params + params: ExecuteExpression.Params ) => - contextRegistry ! ContextRegistryProtocol.ExecuteVisualisation( + contextRegistry ! ContextRegistryProtocol.ExecuteExpression( clientId, params.visualisationId, params.expressionId, @@ -59,7 +59,7 @@ class ExecuteVisualisationHandler( context.stop(self) case ContextRegistryProtocol.VisualisationAttached => - replyTo ! ResponseResult(ExecuteVisualisation, id, Unused) + replyTo ! ResponseResult(ExecuteExpression, id, Unused) cancellable.cancel() context.stop(self) @@ -71,10 +71,10 @@ class ExecuteVisualisationHandler( } -object ExecuteVisualisationHandler { +object ExecuteExpressionHandler { /** Creates configuration object used to create an - * [[ExecuteVisualisationHandler]]. + * [[ExecuteExpressionHandler]]. * * @param clientId an unique identifier of the client * @param timeout request timeout @@ -85,6 +85,6 @@ object ExecuteVisualisationHandler { timeout: FiniteDuration, runtime: ActorRef ): Props = - Props(new ExecuteVisualisationHandler(clientId, timeout, runtime)) + Props(new ExecuteExpressionHandler(clientId, timeout, runtime)) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala index aab1bb40d7e9..8f104b15b05e 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistry.scala @@ -182,7 +182,7 @@ final class ContextRegistry( sender() ! AccessDenied } - case ExecuteVisualisation(clientId, visualisationId, expressionId, cfg) => + case ExecuteExpression(clientId, visualisationId, expressionId, cfg) => val contextId = cfg.executionContextId if (store.hasContext(clientId, contextId)) { store.getListener(contextId).foreach { listener => diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala index 103d15241168..89cbdc3806b3 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/ContextRegistryProtocol.scala @@ -307,8 +307,8 @@ object ContextRegistryProtocol { diagnostics: Seq[ExecutionDiagnostic] ) - /** Requests the language server to execute a oneshot visualisation on an - * expression specified by `expressionId`. + /** Requests the language server to execute an expression provided in the + * `visualisationConfig` on an expression specified by `expressionId`. * * @param clientId the requester id * @param visualisationId an identifier of a visualisation @@ -316,7 +316,7 @@ object ContextRegistryProtocol { * @param visualisationConfig a configuration object for properties of the * visualisation */ - case class ExecuteVisualisation( + case class ExecuteExpression( clientId: ClientId, visualisationId: UUID, expressionId: UUID, @@ -325,7 +325,7 @@ object ContextRegistryProtocol { /** @inheritdoc */ override def toLogString(shouldMask: Boolean): String = - "ExecuteVisualisation(" + + "ExecuteExpression(" + s"clientId=$clientId," + s"visualisationId=$visualisationId," + s"expressionId=$expressionId,visualisationConfig=" + diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala index 5af0cc696d9d..3b680a3fb3b4 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/runtime/VisualisationApi.scala @@ -10,8 +10,8 @@ import org.enso.jsonrpc.{HasParams, HasResult, Method, Unused} */ object VisualisationApi { - case object ExecuteVisualisation - extends Method("executionContext/executeVisualisation") { + case object ExecuteExpression + extends Method("executionContext/executeExpression") { case class Params( visualisationId: UUID, @@ -20,7 +20,7 @@ object VisualisationApi { ) implicit val hasParams = new HasParams[this.type] { - type Params = ExecuteVisualisation.Params + type Params = ExecuteExpression.Params } implicit val hasResult = new HasResult[this.type] { type Result = Unused.type diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala index 3b3c9f192b24..3592571aa01e 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ContextRegistryTest.scala @@ -453,7 +453,7 @@ class ContextRegistryTest extends BaseServerTest { client.expectJson(json.ok(3)) } - "successfully execute visualisation" in { + "successfully execute expression" in { val client = getInitialisedWsClient() // create context @@ -477,7 +477,7 @@ class ContextRegistryTest extends BaseServerTest { val config = VisualisationConfiguration(contextId, "Test.Main", ".to_json.to_text") client.send( - json.executionContextExecuteVisualisationRequest( + json.executionContextExecuteExpressionRequest( 2, visualisationId, expressionId, @@ -505,7 +505,7 @@ class ContextRegistryTest extends BaseServerTest { client.expectJson(json.ok(2)) } - "return ModuleNotFound error when executing visualisation" in { + "return ModuleNotFound error when executing expression" in { val client = getInitialisedWsClient() // create context @@ -529,7 +529,7 @@ class ContextRegistryTest extends BaseServerTest { val config = VisualisationConfiguration(contextId, "Test.Main", ".to_json.to_text") client.send( - json.executionContextExecuteVisualisationRequest( + json.executionContextExecuteExpressionRequest( 2, visualisationId, expressionId, diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala index 251f8e227344..724ccbc6838b 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ExecutionContextJsonMessages.scala @@ -93,7 +93,7 @@ object ExecutionContextJsonMessages { } """ - def executionContextExecuteVisualisationRequest( + def executionContextExecuteExpressionRequest( reqId: Int, visualisationId: Api.VisualisationId, expressionId: Api.ExpressionId, @@ -101,7 +101,7 @@ object ExecutionContextJsonMessages { ) = json""" { "jsonrpc": "2.0", - "method": "executionContext/executeVisualisation", + "method": "executionContext/executeExpression", "id": $reqId, "params": { "visualisationId": $visualisationId,