Skip to content

Commit

Permalink
Add evaluation for one-shot expressions (#1749)
Browse files Browse the repository at this point in the history
  • Loading branch information
4e6 authored May 21, 2021
1 parent 1bdf87c commit f34f8be
Show file tree
Hide file tree
Showing 11 changed files with 511 additions and 35 deletions.
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
Loading

0 comments on commit f34f8be

Please sign in to comment.