Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Oneshot Visualization #1749

Merged
merged 5 commits into from
May 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/language-server/protocol-language-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ transport formats, please look [here](./protocol-architecture).
- [`executionContext/expressionUpdates`](#executioncontextexpressionupdates)
- [`executionContext/executionFailed`](#executioncontextexecutionfailed)
- [`executionContext/executionStatus`](#executioncontextexecutionstatus)
- [`executionContext/executeExpression`](#executioncontextexecuteexpression)
- [`executionContext/attachVisualisation`](#executioncontextattachvisualisation)
- [`executionContext/detachVisualisation`](#executioncontextdetachvisualisation)
- [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation)
Expand Down Expand Up @@ -1280,6 +1281,7 @@ destroying the context.
- [`executionContext/recompute`](#executioncontextrecompute)
- [`executionContext/push`](#executioncontextpush)
- [`executionContext/pop`](#executioncontextpop)
- [`executionContext/executeExpression`](#executioncontextexecuteexpression)
- [`executionContext/attachVisualisation`](#executioncontextattachvisualisation)
- [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation)
- [`executionContext/detachVisualisation`](#executioncontextdetachvisualisation)
Expand Down Expand Up @@ -2751,6 +2753,47 @@ Sent from the server to the client to inform about a status of execution.

None

### `executionContext/executeExpression`

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
- **Connection:** Protocol
- **Visibility:** Public

#### Parameters

```typescript
interface ExecuteExpressionRequest {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ import org.enso.languageserver.requesthandler.text._
import org.enso.languageserver.requesthandler.visualisation.{
AttachVisualisationHandler,
DetachVisualisationHandler,
ExecuteExpressionHandler,
ModifyVisualisationHandler
}
import org.enso.languageserver.runtime.ContextRegistryProtocol
import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.runtime.VisualisationApi.{
AttachVisualisation,
DetachVisualisation,
ExecuteExpression,
ModifyVisualisation
}
import org.enso.languageserver.search.SearchApi._
Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -333,6 +335,8 @@ class JsonConnectionController(
Completion -> search.CompletionHandler
.props(requestTimeout, suggestionsHandler),
Import -> search.ImportHandler.props(requestTimeout, suggestionsHandler),
ExecuteExpression -> ExecuteExpressionHandler
.props(rpcSession.clientId, requestTimeout, contextRegistry),
AttachVisualisation -> AttachVisualisationHandler
.props(rpcSession.clientId, requestTimeout, contextRegistry),
DetachVisualisation -> DetachVisualisationHandler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ object JsonRpc {
.registerRequest(ExecutionContextPush)
.registerRequest(ExecutionContextPop)
.registerRequest(ExecutionContextRecompute)
.registerRequest(ExecuteExpression)
.registerRequest(AttachVisualisation)
.registerRequest(DetachVisualisation)
.registerRequest(ModifyVisualisation)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.ExecuteExpression
import org.enso.languageserver.runtime.{
ContextRegistryProtocol,
RuntimeFailureMapper
}
import org.enso.languageserver.util.UnhandledLogging

import scala.concurrent.duration.FiniteDuration

/** 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 ExecuteExpressionHandler(
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(
ExecuteExpression,
id,
params: ExecuteExpression.Params
) =>
contextRegistry ! ContextRegistryProtocol.ExecuteExpression(
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(ExecuteExpression, id, Unused)
cancellable.cancel()
context.stop(self)

case error: ContextRegistryProtocol.Failure =>
replyTo ! ResponseError(Some(id), RuntimeFailureMapper.mapFailure(error))
cancellable.cancel()
context.stop(self)
}

}

object ExecuteExpressionHandler {

/** Creates configuration object used to create an
* [[ExecuteExpressionHandler]].
*
* @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 ExecuteExpressionHandler(clientId, timeout, runtime))

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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(
Expand All @@ -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) =>
Expand Down Expand Up @@ -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 =>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,20 +182,42 @@ final class ContextRegistry(
sender() ! AccessDenied
}

case ExecuteExpression(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 {
Expand All @@ -213,7 +235,6 @@ final class ContextRegistry(
context.actorOf(
DetachVisualisationHandler.props(config, timeout, runtime)
)

handler.forward(
Api.DetachVisualisation(contextId, visualisationId, expressionId)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,14 +307,53 @@ object ContextRegistryProtocol {
diagnostics: Seq[ExecutionDiagnostic]
)

/** 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
* @param expressionId an identifier of an expression which is visualised
* @param visualisationConfig a configuration object for properties of the
* visualisation
*/
case class ExecuteExpression(
clientId: ClientId,
visualisationId: UUID,
expressionId: UUID,
visualisationConfig: VisualisationConfiguration
) extends ToLogString {

/** @inheritdoc */
override def toLogString(shouldMask: Boolean): String =
"ExecuteExpression(" +
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`.
*
* @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
* visualisation
*/
case class AttachVisualisation(
clientId: ClientId,
Expand All @@ -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
Expand Down