mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 17:41:53 +03:00
Renaming Variable or Function Support (#7515)
close #7389 Changelog: - add: `refactoring/renameSymbol` request to rename locals or module methods
This commit is contained in:
parent
f1c224e62e
commit
b7ab6911ff
@ -910,6 +910,7 @@
|
||||
- [Update to GraalVM 23.0.0][7176]
|
||||
- [Using official BigInteger support][7420]
|
||||
- [Allow users to give a project other than Upper_Snake_Case name][7397]
|
||||
- [Support renaming variable or function][7515]
|
||||
|
||||
[3227]: https://github.com/enso-org/enso/pull/3227
|
||||
[3248]: https://github.com/enso-org/enso/pull/3248
|
||||
@ -1040,6 +1041,7 @@
|
||||
[7291]: https://github.com/enso-org/enso/pull/7291
|
||||
[7420]: https://github.com/enso-org/enso/pull/7420
|
||||
[7397]: https://github.com/enso-org/enso/pull/7397
|
||||
[7515]: https://github.com/enso-org/enso/pull/7515
|
||||
|
||||
# Enso 2.0.0-alpha.18 (2021-10-12)
|
||||
|
||||
|
18
build.sbt
18
build.sbt
@ -272,6 +272,7 @@ lazy val enso = (project in file("."))
|
||||
`locking-test-helper`,
|
||||
`akka-native`,
|
||||
`version-output`,
|
||||
`refactoring-utils`,
|
||||
`engine-runner`,
|
||||
runtime,
|
||||
searcher,
|
||||
@ -787,6 +788,22 @@ lazy val `version-output` = (project in file("lib/scala/version-output"))
|
||||
}.taskValue
|
||||
)
|
||||
|
||||
lazy val `refactoring-utils` = project
|
||||
.in(file("lib/scala/refactoring-utils"))
|
||||
.configs(Test)
|
||||
.settings(
|
||||
frgaalJavaCompilerSetting,
|
||||
commands += WithDebugCommand.withDebug,
|
||||
version := "0.1",
|
||||
libraryDependencies ++= Seq(
|
||||
"junit" % "junit" % junitVersion % Test,
|
||||
"com.github.sbt" % "junit-interface" % junitIfVersion % Test
|
||||
)
|
||||
)
|
||||
.dependsOn(`runtime-parser`)
|
||||
.dependsOn(`text-buffer`)
|
||||
.dependsOn(testkit % Test)
|
||||
|
||||
lazy val `project-manager` = (project in file("lib/scala/project-manager"))
|
||||
.settings(
|
||||
(Compile / mainClass) := Some("org.enso.projectmanager.boot.ProjectManager")
|
||||
@ -1434,6 +1451,7 @@ lazy val `runtime-instrument-common` =
|
||||
"ENSO_TEST_DISABLE_IR_CACHE" -> "false"
|
||||
)
|
||||
)
|
||||
.dependsOn(`refactoring-utils`)
|
||||
.dependsOn(
|
||||
runtime % "compile->compile;test->test;runtime->runtime;bench->bench"
|
||||
)
|
||||
|
@ -125,6 +125,7 @@ transport formats, please look [here](./protocol-architecture).
|
||||
- [`heartbeat/init`](#heartbeatinit)
|
||||
- [Refactoring](#refactoring)
|
||||
- [`refactoring/renameProject`](#refactoringrenameproject)
|
||||
- [`refactoring/renameSymbol`](#refactoringrenamesymbol)
|
||||
- [Execution Management Operations](#execution-management-operations)
|
||||
- [Execution Management Example](#execution-management-example)
|
||||
- [Create Execution Context](#create-execution-context)
|
||||
@ -224,6 +225,9 @@ transport formats, please look [here](./protocol-architecture).
|
||||
- [`InvalidLibraryName`](#invalidlibraryname)
|
||||
- [`DependencyDiscoveryError`](#dependencydiscoveryerror)
|
||||
- [`InvalidSemverVersion`](#invalidsemverversion)
|
||||
- [`ExpressionNotFoundError`](#expressionnotfounderror)
|
||||
- [`FailedToApplyEdits`](#failedtoapplyedits)
|
||||
- [`RefactoringNotSupported`](#refactoringnotsupported)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
@ -3311,6 +3315,101 @@ null;
|
||||
|
||||
None
|
||||
|
||||
### `refactoring/renameSymbol`
|
||||
|
||||
Sent from the client to the server to rename a symbol in the program. The text
|
||||
edits required to perform the refactoring will be returned as a
|
||||
[`text/didChange`](#textdidchange) notification.
|
||||
|
||||
- **Type:** Request
|
||||
- **Direction:** Project Manager -> Server
|
||||
- **Connection:** Protocol
|
||||
- **Visibility:** Private
|
||||
|
||||
#### Supported refactorings
|
||||
|
||||
Refactorins supports only limited cases listed below.
|
||||
|
||||
##### Local definition
|
||||
|
||||
```text
|
||||
main =
|
||||
operator1 = 42
|
||||
^^^^^^^^^
|
||||
```
|
||||
|
||||
Expression id in the request should point to the left hand side symbol of the
|
||||
assignment.
|
||||
|
||||
##### Module method
|
||||
|
||||
```text
|
||||
function1 x = x
|
||||
^^^^^^^^^
|
||||
|
||||
main =
|
||||
operator1 = Main.function1 42
|
||||
```
|
||||
|
||||
Expression id in the request should point to the symbol defining the function.
|
||||
|
||||
Current limitations of the method renaming are:
|
||||
|
||||
- Methods defined on types are not supported, i.e.
|
||||
```text
|
||||
Main.function1 x = x
|
||||
```
|
||||
- Method calls where the self type is not specified will not be renamed, i.e.
|
||||
|
||||
```text
|
||||
function1 x = x
|
||||
|
||||
main =
|
||||
operator1 = function1 42
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
```typescript
|
||||
{
|
||||
/**
|
||||
* The qualified module name.
|
||||
*/
|
||||
module: string;
|
||||
|
||||
/**
|
||||
* The symbol to rename.
|
||||
*/
|
||||
expressionId: ExpressionId;
|
||||
|
||||
/**
|
||||
* The new name of the symbol. If the provided name is not a valid Enso
|
||||
* identifier (contains unsupported symbols, spaces, etc.), it will be normalized.
|
||||
* The final name will be returned in the response.
|
||||
*/
|
||||
newName: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### Result
|
||||
|
||||
```typescript
|
||||
{
|
||||
newName: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
- [`ModuleNotFoundError`](#modulenotfounderror) to signal that the requested
|
||||
module cannot be found.
|
||||
- [`ExpressionNotFoundError`](#expressionnotfounderror) to signal that the given
|
||||
expression cannot be found.
|
||||
- [`FailedToApplyEdits`](#failedtoapplyedits) to signal that the refactoring
|
||||
operation was not able to apply generated edits.
|
||||
- [`RefactoringNotSupported`](#refactoringnotsupported) to signal that the
|
||||
refactoring of the given expression is not supported.
|
||||
|
||||
## Execution Management Operations
|
||||
|
||||
The execution management portion of the language server API deals with exposing
|
||||
@ -5787,3 +5886,36 @@ message contains the invalid version in the payload.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `ExpressionNotFoundError`
|
||||
|
||||
Signals that the expression cannot be found by the provided id.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 9001,
|
||||
"message" : "Expression not found by id [<expression-id>]"
|
||||
}
|
||||
```
|
||||
|
||||
### `FailedToApplyEdits`
|
||||
|
||||
Signals that the refactoring operation was not able to apply generated edits.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 9002,
|
||||
"message" : "Failed to apply edits to module [<module-name>]"
|
||||
}
|
||||
```
|
||||
|
||||
### `RefactoringNotSupported`
|
||||
|
||||
Signals that the refactoring of the given expression is not supported.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 9003,
|
||||
"message" : "Refactoring not supported for expression [<expression-id>]"
|
||||
}
|
||||
```
|
||||
|
@ -226,6 +226,7 @@ class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
|
||||
fileManager,
|
||||
vcsManager,
|
||||
runtimeConnector,
|
||||
contentRootManagerWrapper,
|
||||
TimingsConfig.default().withAutoSave(6.seconds)
|
||||
),
|
||||
"buffer-registry"
|
||||
|
@ -32,7 +32,10 @@ import org.enso.languageserver.libraries.LibraryConfig
|
||||
import org.enso.languageserver.libraries.handler._
|
||||
import org.enso.languageserver.monitoring.MonitoringApi.{InitialPing, Ping}
|
||||
import org.enso.languageserver.monitoring.MonitoringProtocol
|
||||
import org.enso.languageserver.refactoring.RefactoringApi.RenameProject
|
||||
import org.enso.languageserver.refactoring.RefactoringApi.{
|
||||
RenameProject,
|
||||
RenameSymbol
|
||||
}
|
||||
import org.enso.languageserver.requesthandler._
|
||||
import org.enso.languageserver.requesthandler.capability._
|
||||
import org.enso.languageserver.requesthandler.io._
|
||||
@ -40,7 +43,10 @@ import org.enso.languageserver.requesthandler.monitoring.{
|
||||
InitialPingHandler,
|
||||
PingHandler
|
||||
}
|
||||
import org.enso.languageserver.requesthandler.refactoring.RenameProjectHandler
|
||||
import org.enso.languageserver.requesthandler.refactoring.{
|
||||
RenameProjectHandler,
|
||||
RenameSymbolHandler
|
||||
}
|
||||
import org.enso.languageserver.requesthandler.text._
|
||||
import org.enso.languageserver.requesthandler.visualization.{
|
||||
AttachVisualizationHandler,
|
||||
@ -76,6 +82,7 @@ import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.polyglot.runtime.Runtime.Api.ProgressNotification
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/** An actor handling communications between a single client and the language
|
||||
@ -573,6 +580,10 @@ class JsonConnectionController(
|
||||
requestTimeout,
|
||||
libraryConfig.localLibraryManager,
|
||||
libraryConfig.publishedLibraryCache
|
||||
),
|
||||
RenameSymbol -> RenameSymbolHandler.props(
|
||||
requestTimeout,
|
||||
runtimeConnector
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import org.enso.languageserver.capability.CapabilityApi.{
|
||||
import org.enso.languageserver.filemanager.FileManagerApi._
|
||||
import org.enso.languageserver.io.InputOutputApi._
|
||||
import org.enso.languageserver.monitoring.MonitoringApi.{InitialPing, Ping}
|
||||
import org.enso.languageserver.refactoring.RefactoringApi.RenameProject
|
||||
import org.enso.languageserver.refactoring.RefactoringApi._
|
||||
import org.enso.languageserver.runtime.ExecutionApi._
|
||||
import org.enso.languageserver.search.SearchApi._
|
||||
import org.enso.languageserver.runtime.VisualizationApi._
|
||||
@ -84,6 +84,7 @@ object JsonRpc {
|
||||
.registerRequest(Completion)
|
||||
.registerRequest(AICompletion)
|
||||
.registerRequest(RenameProject)
|
||||
.registerRequest(RenameSymbol)
|
||||
.registerRequest(ProjectInfo)
|
||||
.registerRequest(EditionsListAvailable)
|
||||
.registerRequest(EditionsResolve)
|
||||
|
@ -1,6 +1,8 @@
|
||||
package org.enso.languageserver.refactoring
|
||||
|
||||
import org.enso.jsonrpc.{HasParams, HasResult, Method, Unused}
|
||||
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused}
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
/** The refactoring JSON RPC API provided by the language server.
|
||||
* See [[https://github.com/luna/enso/blob/develop/docs/language-server/README.md]]
|
||||
@ -8,6 +10,8 @@ import org.enso.jsonrpc.{HasParams, HasResult, Method, Unused}
|
||||
*/
|
||||
object RefactoringApi {
|
||||
|
||||
type ExpressionId = UUID
|
||||
|
||||
case object RenameProject extends Method("refactoring/renameProject") {
|
||||
|
||||
case class Params(namespace: String, oldName: String, newName: String)
|
||||
@ -21,7 +25,39 @@ object RefactoringApi {
|
||||
new HasResult[this.type] {
|
||||
type Result = Unused.type
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case object RenameSymbol extends Method("refactoring/renameSymbol") {
|
||||
|
||||
case class Params(
|
||||
module: String,
|
||||
expressionId: ExpressionId,
|
||||
newName: String
|
||||
)
|
||||
|
||||
case class Result(newName: String)
|
||||
|
||||
implicit val hasParams: HasParams.Aux[this.type, RenameSymbol.Params] =
|
||||
new HasParams[this.type] {
|
||||
type Params = RenameSymbol.Params
|
||||
}
|
||||
|
||||
implicit val hasResult: HasResult.Aux[this.type, RenameSymbol.Result] =
|
||||
new HasResult[this.type] {
|
||||
type Result = RenameSymbol.Result
|
||||
}
|
||||
}
|
||||
|
||||
case class ExpressionNotFoundError(expressionId: UUID)
|
||||
extends Error(9001, s"Expression not found by id [$expressionId]")
|
||||
|
||||
case class FailedToApplyEdits(module: String)
|
||||
extends Error(9002, s"Failed to apply edits to module [$module]")
|
||||
|
||||
case class RefactoringNotSupported(expressionId: UUID)
|
||||
extends Error(
|
||||
9003,
|
||||
s"Refactoring not supported for expression [$expressionId]"
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
package org.enso.languageserver.refactoring
|
||||
|
||||
import org.enso.jsonrpc.Error
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
|
||||
object RenameFailureMapper {
|
||||
|
||||
/** Maps refactoring error into JSON RPC error.
|
||||
*
|
||||
* @param error refactoring error
|
||||
* @return JSON RPC error
|
||||
*/
|
||||
def mapFailure(error: Api.SymbolRenameFailed.Error): Error =
|
||||
error match {
|
||||
case error: Api.SymbolRenameFailed.ExpressionNotFound =>
|
||||
RefactoringApi.ExpressionNotFoundError(error.expressionId)
|
||||
|
||||
case error: Api.SymbolRenameFailed.FailedToApplyEdits =>
|
||||
RefactoringApi.FailedToApplyEdits(error.module)
|
||||
|
||||
case error: Api.SymbolRenameFailed.OperationNotSupported =>
|
||||
RefactoringApi.RefactoringNotSupported(error.expressionId)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package org.enso.languageserver.requesthandler.refactoring
|
||||
|
||||
import akka.actor.{Actor, ActorRef, Cancellable, Props}
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
import org.enso.jsonrpc._
|
||||
import org.enso.languageserver.refactoring.RefactoringApi.RenameSymbol
|
||||
import org.enso.languageserver.refactoring.RenameFailureMapper
|
||||
import org.enso.languageserver.requesthandler.RequestTimeout
|
||||
import org.enso.languageserver.runtime.ExecutionApi
|
||||
import org.enso.languageserver.util.UnhandledLogging
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
/** A request handler for `refactoring/renameSymbol` commands.
|
||||
*
|
||||
* @param timeout a request timeout
|
||||
* @param runtimeConnector a reference to the runtime connector
|
||||
*/
|
||||
class RenameSymbolHandler(
|
||||
timeout: FiniteDuration,
|
||||
runtimeConnector: ActorRef
|
||||
) extends Actor
|
||||
with LazyLogging
|
||||
with UnhandledLogging {
|
||||
|
||||
import context.dispatcher
|
||||
|
||||
override def receive: Receive = requestStage
|
||||
|
||||
private def requestStage: Receive = {
|
||||
case Request(RenameSymbol, id, params: RenameSymbol.Params) =>
|
||||
val payload =
|
||||
Api.RenameSymbol(params.module, params.expressionId, params.newName)
|
||||
runtimeConnector ! Api.Request(UUID.randomUUID(), payload)
|
||||
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 =>
|
||||
logger.error("Request [{}] timed out.", id)
|
||||
replyTo ! ResponseError(Some(id), Errors.RequestTimeout)
|
||||
context.stop(self)
|
||||
|
||||
case Api.Response(_, Api.SymbolRenamed(newName)) =>
|
||||
replyTo ! ResponseResult(
|
||||
RenameSymbol,
|
||||
id,
|
||||
RenameSymbol.Result(newName)
|
||||
)
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
|
||||
case Api.Response(_, Api.ModuleNotFound(moduleName)) =>
|
||||
replyTo ! ResponseError(
|
||||
Some(id),
|
||||
ExecutionApi.ModuleNotFoundError(moduleName)
|
||||
)
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
|
||||
case Api.Response(_, Api.SymbolRenameFailed(error)) =>
|
||||
replyTo ! ResponseError(Some(id), RenameFailureMapper.mapFailure(error))
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object RenameSymbolHandler {
|
||||
|
||||
/** Creates configuration object used to create a [[RenameSymbolHandler]].
|
||||
*
|
||||
* @param timeout request timeout
|
||||
* @param runtimeConnector reference to the runtime connector
|
||||
*/
|
||||
def props(
|
||||
timeout: FiniteDuration,
|
||||
runtimeConnector: ActorRef
|
||||
): Props =
|
||||
Props(
|
||||
new RenameSymbolHandler(timeout, runtimeConnector)
|
||||
)
|
||||
|
||||
}
|
@ -33,7 +33,7 @@ class ApplyEditHandler(
|
||||
private def requestStage: Receive = {
|
||||
case Request(ApplyEdit, id, params: ApplyEdit.Params) =>
|
||||
bufferRegistry ! TextProtocol.ApplyEdit(
|
||||
rpcSession.clientId,
|
||||
Some(rpcSession.clientId),
|
||||
params.edit,
|
||||
params.execute.getOrElse(true)
|
||||
)
|
||||
|
@ -277,5 +277,4 @@ object ExecutionApi {
|
||||
Encoder[ContextRegistryProtocol.ExecutionDiagnostic].apply(_)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ final class RuntimeFailureMapper(contentRootManager: ContentRootManager) {
|
||||
/** Convert the runtime failure message to the context registry protocol
|
||||
* representation.
|
||||
*
|
||||
* @param error the error message
|
||||
* @param result the api execution result
|
||||
* @return the registry protocol representation fo the diagnostic message
|
||||
*/
|
||||
def toProtocolFailure(
|
||||
|
@ -10,7 +10,12 @@ import org.enso.languageserver.capability.CapabilityProtocol.{
|
||||
ReleaseCapability
|
||||
}
|
||||
import org.enso.languageserver.data.{CanEdit, CapabilityRegistration, ClientId}
|
||||
import org.enso.languageserver.filemanager.{FileEvent, FileEventKind, Path}
|
||||
import org.enso.languageserver.filemanager.{
|
||||
ContentRootManager,
|
||||
FileEvent,
|
||||
FileEventKind,
|
||||
Path
|
||||
}
|
||||
import org.enso.languageserver.monitoring.MonitoringProtocol.{Ping, Pong}
|
||||
import org.enso.languageserver.session.JsonSession
|
||||
import org.enso.languageserver.text.BufferRegistry.{
|
||||
@ -43,6 +48,8 @@ import org.enso.languageserver.vcsmanager.VcsProtocol.{
|
||||
RestoreRepoResponse,
|
||||
SaveRepo
|
||||
}
|
||||
import org.enso.logger.masking.MaskedPath
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.text.ContentBasedVersioning
|
||||
|
||||
import java.util.UUID
|
||||
@ -81,13 +88,14 @@ import java.util.UUID
|
||||
* @param fileManager a file manager
|
||||
* @param vcsManager a VCS manager
|
||||
* @param runtimeConnector a gateway to the runtime
|
||||
* @param versionCalculator a content based version calculator
|
||||
* @param contentRootManager the content root manager
|
||||
* @param timingsConfig a config with timeout/delay values
|
||||
*/
|
||||
class BufferRegistry(
|
||||
fileManager: ActorRef,
|
||||
vcsManager: ActorRef,
|
||||
runtimeConnector: ActorRef,
|
||||
contentRootManager: ContentRootManager,
|
||||
timingsConfig: TimingsConfig
|
||||
)(implicit
|
||||
versionCalculator: ContentBasedVersioning
|
||||
@ -102,6 +110,7 @@ class BufferRegistry(
|
||||
super.preStart()
|
||||
|
||||
context.system.eventStream.subscribe(self, classOf[FileEvent])
|
||||
context.system.eventStream.subscribe(self, classOf[Api.FileEdit])
|
||||
}
|
||||
|
||||
override def receive: Receive = running(Map.empty)
|
||||
@ -212,6 +221,30 @@ class BufferRegistry(
|
||||
buffer ! msg
|
||||
}
|
||||
}
|
||||
|
||||
case msg: Api.FileEdit =>
|
||||
contentRootManager
|
||||
.findRelativePath(msg.path)
|
||||
.foreach {
|
||||
case Some(path) =>
|
||||
registry.get(path).foreach { buffer =>
|
||||
buffer ! ApplyEdit(
|
||||
None,
|
||||
FileEdit(
|
||||
path,
|
||||
msg.edits.toList,
|
||||
msg.oldVersion,
|
||||
msg.newVersion
|
||||
),
|
||||
execute = true
|
||||
)
|
||||
}
|
||||
case None =>
|
||||
logger.error(
|
||||
"Failed to resolve path [{}].",
|
||||
MaskedPath(msg.path.toPath)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def forwardMessageToVCS(
|
||||
@ -475,7 +508,7 @@ object BufferRegistry {
|
||||
* @param fileManager a file manager actor
|
||||
* @param vcsManager a VCS manager actor
|
||||
* @param runtimeConnector a gateway to the runtime
|
||||
* @param versionCalculator a content based version calculator
|
||||
* @param contentRootManager the content root manager
|
||||
* @param timingsConfig a config with timout/delay values
|
||||
* @return a configuration object
|
||||
*/
|
||||
@ -483,6 +516,7 @@ object BufferRegistry {
|
||||
fileManager: ActorRef,
|
||||
vcsManager: ActorRef,
|
||||
runtimeConnector: ActorRef,
|
||||
contentRootManager: ContentRootManager,
|
||||
timingsConfig: TimingsConfig
|
||||
)(implicit
|
||||
versionCalculator: ContentBasedVersioning
|
||||
@ -492,6 +526,7 @@ object BufferRegistry {
|
||||
fileManager,
|
||||
vcsManager,
|
||||
runtimeConnector,
|
||||
contentRootManager,
|
||||
timingsConfig
|
||||
)
|
||||
)
|
||||
|
@ -676,7 +676,7 @@ class CollaborativeBuffer(
|
||||
expressionValue: String,
|
||||
autoSave: Map[ClientId, (ContentVersion, Cancellable)]
|
||||
): Unit = {
|
||||
applyEdits(buffer, lockHolder, clientId, change) match {
|
||||
applyEdits(buffer, lockHolder, Some(clientId), change) match {
|
||||
case Left(failure) =>
|
||||
sender() ! failure
|
||||
|
||||
@ -705,7 +705,7 @@ class CollaborativeBuffer(
|
||||
buffer: Buffer,
|
||||
clients: Map[ClientId, JsonSession],
|
||||
lockHolder: Option[JsonSession],
|
||||
clientId: ClientId,
|
||||
clientId: Option[ClientId],
|
||||
change: FileEdit,
|
||||
execute: Boolean,
|
||||
autoSave: Map[ClientId, (ContentVersion, Cancellable)]
|
||||
@ -716,20 +716,21 @@ class CollaborativeBuffer(
|
||||
|
||||
case Right(modifiedBuffer) =>
|
||||
sender() ! ApplyEditSuccess
|
||||
val subscribers = clients.filterNot(_._1 == clientId).values
|
||||
val subscribers =
|
||||
clients.filterNot(kv => clientId.contains(kv._1)).values
|
||||
subscribers foreach { _.rpcController ! TextDidChange(List(change)) }
|
||||
runtimeConnector ! Api.Request(
|
||||
Api.EditFileNotification(
|
||||
buffer.fileWithMetadata.file,
|
||||
change.edits,
|
||||
execute
|
||||
clientId.foreach { _ =>
|
||||
runtimeConnector ! Api.Request(
|
||||
Api.EditFileNotification(
|
||||
buffer.fileWithMetadata.file,
|
||||
change.edits,
|
||||
execute
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
val newAutoSave: Map[ClientId, (ContentVersion, Cancellable)] =
|
||||
upsertAutoSaveTimer(
|
||||
autoSave,
|
||||
clientId,
|
||||
modifiedBuffer.version
|
||||
clientId.fold(autoSave)(
|
||||
upsertAutoSaveTimer(autoSave, _, modifiedBuffer.version)
|
||||
)
|
||||
context.become(
|
||||
collaborativeEditing(modifiedBuffer, clients, lockHolder, newAutoSave)
|
||||
@ -740,7 +741,7 @@ class CollaborativeBuffer(
|
||||
private def applyEdits(
|
||||
buffer: Buffer,
|
||||
lockHolder: Option[JsonSession],
|
||||
clientId: ClientId,
|
||||
clientId: Option[ClientId],
|
||||
change: FileEdit
|
||||
): Either[ApplyEditFailure, Buffer] = {
|
||||
for {
|
||||
@ -772,9 +773,10 @@ class CollaborativeBuffer(
|
||||
|
||||
private def validateAccess(
|
||||
lockHolder: Option[JsonSession],
|
||||
clientId: ClientId
|
||||
clientId: Option[ClientId]
|
||||
): Either[ApplyEditFailure, Unit] = {
|
||||
val hasLock = lockHolder.exists(_.clientId == clientId)
|
||||
val hasLock =
|
||||
lockHolder.exists(session => clientId.forall(_ == session.clientId))
|
||||
if (hasLock) {
|
||||
Right(())
|
||||
} else {
|
||||
|
@ -69,7 +69,11 @@ object TextProtocol {
|
||||
* @param edit a diff describing changes made to a file
|
||||
* @param execute whether to execute the program after applying the edits
|
||||
*/
|
||||
case class ApplyEdit(clientId: ClientId, edit: FileEdit, execute: Boolean)
|
||||
case class ApplyEdit(
|
||||
clientId: Option[ClientId],
|
||||
edit: FileEdit,
|
||||
execute: Boolean
|
||||
)
|
||||
|
||||
/** Signals the result of applying a series of edits.
|
||||
*/
|
||||
|
@ -198,6 +198,7 @@ class BaseServerTest
|
||||
fileManager,
|
||||
vcsManager,
|
||||
runtimeConnectorProbe.ref,
|
||||
contentRootManagerWrapper,
|
||||
timingsConfig
|
||||
)(
|
||||
Sha3_224VersionCalculator
|
||||
|
@ -0,0 +1,238 @@
|
||||
package org.enso.languageserver.websocket.json
|
||||
|
||||
import io.circe.literal._
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
class RefactoringTest extends BaseServerTest {
|
||||
|
||||
"refactoring/renameSymbol" should {
|
||||
|
||||
"return ok response after a successful renaming" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
val moduleName = "local.Unnamed.Main"
|
||||
val expressionId = new UUID(0, 1)
|
||||
val newName = "bar"
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "refactoring/renameSymbol",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"module": $moduleName,
|
||||
"expressionId": $expressionId,
|
||||
"newName": $newName
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
val requestId = runtimeConnectorProbe.receiveN(1).head match {
|
||||
case Api.Request(requestId, payload: Api.RenameSymbol) =>
|
||||
payload.module shouldEqual moduleName
|
||||
payload.expressionId shouldEqual expressionId
|
||||
payload.newName shouldEqual newName
|
||||
requestId
|
||||
case msg =>
|
||||
fail(s"Runtime connector received unexpected message: $msg")
|
||||
}
|
||||
runtimeConnectorProbe.lastSender ! Api.Response(
|
||||
requestId,
|
||||
Api.SymbolRenamed(newName)
|
||||
)
|
||||
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"newName": $newName
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"reply with ModuleNotFound error when the requested module not found" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
val moduleName = "local.Unnamed.Foo"
|
||||
val expressionId = new UUID(0, 1)
|
||||
val newName = "bar"
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "refactoring/renameSymbol",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"module": $moduleName,
|
||||
"expressionId": $expressionId,
|
||||
"newName": $newName
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
val requestId = runtimeConnectorProbe.receiveN(1).head match {
|
||||
case Api.Request(requestId, payload: Api.RenameSymbol) =>
|
||||
payload.module shouldEqual moduleName
|
||||
payload.expressionId shouldEqual expressionId
|
||||
payload.newName shouldEqual newName
|
||||
requestId
|
||||
case msg =>
|
||||
fail(s"Runtime connector received unexpected message: $msg")
|
||||
}
|
||||
runtimeConnectorProbe.lastSender ! Api.Response(
|
||||
requestId,
|
||||
Api.ModuleNotFound(moduleName)
|
||||
)
|
||||
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": 2005,
|
||||
"message": ${s"Module not found [$moduleName]"}
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"reply with ExpressionNotFound error when the requested expression not found" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
val moduleName = "local.Unnamed.Main"
|
||||
val expressionId = new UUID(0, 1)
|
||||
val newName = "bar"
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "refactoring/renameSymbol",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"module": $moduleName,
|
||||
"expressionId": $expressionId,
|
||||
"newName": $newName
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
val requestId = runtimeConnectorProbe.receiveN(1).head match {
|
||||
case Api.Request(requestId, payload: Api.RenameSymbol) =>
|
||||
payload.module shouldEqual moduleName
|
||||
payload.expressionId shouldEqual expressionId
|
||||
payload.newName shouldEqual newName
|
||||
requestId
|
||||
case msg =>
|
||||
fail(s"Runtime connector received unexpected message: $msg")
|
||||
}
|
||||
runtimeConnectorProbe.lastSender ! Api.Response(
|
||||
requestId,
|
||||
Api.SymbolRenameFailed(
|
||||
Api.SymbolRenameFailed.ExpressionNotFound(expressionId)
|
||||
)
|
||||
)
|
||||
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": 9001,
|
||||
"message": ${s"Expression not found by id [$expressionId]"}
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"reply with FailedToApplyEdits error when failed to apply edits" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
val moduleName = "local.Unnamed.Main"
|
||||
val expressionId = new UUID(0, 1)
|
||||
val newName = "bar"
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "refactoring/renameSymbol",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"module": $moduleName,
|
||||
"expressionId": $expressionId,
|
||||
"newName": $newName
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
val requestId = runtimeConnectorProbe.receiveN(1).head match {
|
||||
case Api.Request(requestId, payload: Api.RenameSymbol) =>
|
||||
payload.module shouldEqual moduleName
|
||||
payload.expressionId shouldEqual expressionId
|
||||
payload.newName shouldEqual newName
|
||||
requestId
|
||||
case msg =>
|
||||
fail(s"Runtime connector received unexpected message: $msg")
|
||||
}
|
||||
runtimeConnectorProbe.lastSender ! Api.Response(
|
||||
requestId,
|
||||
Api.SymbolRenameFailed(
|
||||
Api.SymbolRenameFailed.FailedToApplyEdits(moduleName)
|
||||
)
|
||||
)
|
||||
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": 9002,
|
||||
"message": ${s"Failed to apply edits to module [$moduleName]"}
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"reply with RefactoringNotSupported error when renaming unsupported expression" in {
|
||||
val client = getInitialisedWsClient()
|
||||
|
||||
val moduleName = "local.Unnamed.Main"
|
||||
val expressionId = new UUID(0, 1)
|
||||
val newName = "bar"
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "refactoring/renameSymbol",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"module": $moduleName,
|
||||
"expressionId": $expressionId,
|
||||
"newName": $newName
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
val requestId = runtimeConnectorProbe.receiveN(1).head match {
|
||||
case Api.Request(requestId, payload: Api.RenameSymbol) =>
|
||||
payload.module shouldEqual moduleName
|
||||
payload.expressionId shouldEqual expressionId
|
||||
payload.newName shouldEqual newName
|
||||
requestId
|
||||
case msg =>
|
||||
fail(s"Runtime connector received unexpected message: $msg")
|
||||
}
|
||||
runtimeConnectorProbe.lastSender ! Api.Response(
|
||||
requestId,
|
||||
Api.SymbolRenameFailed(
|
||||
Api.SymbolRenameFailed.OperationNotSupported(expressionId)
|
||||
)
|
||||
)
|
||||
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": 9003,
|
||||
"message": ${s"Refactoring not supported for expression [$expressionId]"}
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -5,8 +5,11 @@ import io.circe.literal._
|
||||
import org.enso.languageserver.event.{BufferClosed, JsonSessionTerminated}
|
||||
import org.enso.languageserver.filemanager.Path
|
||||
import org.enso.languageserver.session.JsonSession
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.testkit.FlakySpec
|
||||
import org.enso.text.editing.model
|
||||
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
|
||||
@ -16,6 +19,387 @@ class TextOperationsTest extends BaseServerTest with FlakySpec {
|
||||
|
||||
override def isFileWatcherEnabled = true
|
||||
|
||||
"BufferRegistry" must {
|
||||
"grant the canEdit capability if no one else holds it" in {
|
||||
// Interaction:
|
||||
// 1. Client 1 creates a file.
|
||||
// 2. Client 1 receives confirmation.
|
||||
// 3. Client 1 opens the created file.
|
||||
// 4. Client 1 receives the file contents and a canEdit capability.
|
||||
// 5. Client 1 releases the canEdit capability.
|
||||
// 6. Client 1 receives a confirmation.
|
||||
// 7. Client 2 opens the file.
|
||||
// 8. Client 2 receives the file contents and a canEdit capability.
|
||||
val client1 = getInitialisedWsClient()
|
||||
val client2 = getInitialisedWsClient()
|
||||
// 1
|
||||
client1.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/write",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "grant_can_edit.txt" ]
|
||||
},
|
||||
"contents": "123456789"
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// 2
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
|
||||
// 3
|
||||
client1.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/openFile",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "grant_can_edit.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// 4
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"writeCapability": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": { "path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": ["grant_can_edit.txt"]
|
||||
} }
|
||||
},
|
||||
"content": "123456789",
|
||||
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// 5
|
||||
client1.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/release",
|
||||
"id": 2,
|
||||
"params": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": { "path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": ["grant_can_edit.txt"]
|
||||
} }
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// 6
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
|
||||
// 7
|
||||
client2.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/openFile",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "grant_can_edit.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// 8
|
||||
client2.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"result": {
|
||||
"writeCapability": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": { "path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": ["grant_can_edit.txt"]
|
||||
} }
|
||||
},
|
||||
"content": "123456789",
|
||||
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"take canEdit capability away from clients when another client registers for it" in {
|
||||
val client1 = getInitialisedWsClient()
|
||||
val client2 = getInitialisedWsClient()
|
||||
val client3 = getInitialisedWsClient()
|
||||
client1.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/write",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "take_can_edit.txt" ]
|
||||
},
|
||||
"contents": "123456789"
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
|
||||
client1.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/openFile",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "take_can_edit.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"writeCapability": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": { "path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": ["take_can_edit.txt"]
|
||||
} }
|
||||
},
|
||||
"content": "123456789",
|
||||
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client2.expectNoMessage()
|
||||
client3.expectNoMessage()
|
||||
|
||||
client2.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/acquire",
|
||||
"id": 2,
|
||||
"params": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "take_can_edit.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/forceReleased",
|
||||
"params": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "take_can_edit.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client2.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
client3.expectNoMessage()
|
||||
|
||||
client3.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/acquire",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "take_can_edit.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
client1.expectNoMessage()
|
||||
client2.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/forceReleased",
|
||||
"params": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "take_can_edit.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client3.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"apply refactoring changes" in {
|
||||
val client = getInitialisedWsClient()
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/write",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "to_refactor.txt" ]
|
||||
},
|
||||
"contents": "123456789"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/openFile",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "to_refactor.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{
|
||||
"jsonrpc" : "2.0",
|
||||
"id" : 1,
|
||||
"result" : {
|
||||
"writeCapability" : {
|
||||
"method" : "text/canEdit",
|
||||
"registerOptions" : {
|
||||
"path" : {
|
||||
"rootId" : $testContentRootId,
|
||||
"segments" : [
|
||||
"to_refactor.txt"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"content" : "123456789",
|
||||
"currentVersion" : "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
val textEdits = Vector(
|
||||
model.TextEdit(
|
||||
model.Range(model.Position(0, 0), model.Position(0, 0)),
|
||||
"bar"
|
||||
),
|
||||
model.TextEdit(
|
||||
model.Range(model.Position(0, 12), model.Position(0, 12)),
|
||||
"foo"
|
||||
)
|
||||
)
|
||||
val fileEdit = Api.FileEdit(
|
||||
new File(testContentRoot.file, "to_refactor.txt"),
|
||||
textEdits,
|
||||
oldVersion = "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522",
|
||||
newVersion = "ebe55342f9c8b86857402797dd723fb4a2174e0b56d6ace0a6929ec3"
|
||||
)
|
||||
system.eventStream.publish(fileEdit)
|
||||
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc" : "2.0",
|
||||
"method" : "text/didChange",
|
||||
"params" : {
|
||||
"edits" : [
|
||||
{
|
||||
"path" : {
|
||||
"rootId" : $testContentRootId,
|
||||
"segments" : [
|
||||
"to_refactor.txt"
|
||||
]
|
||||
},
|
||||
"edits" : [
|
||||
{
|
||||
"range" : {
|
||||
"start" : {
|
||||
"line" : 0,
|
||||
"character" : 0
|
||||
},
|
||||
"end" : {
|
||||
"line" : 0,
|
||||
"character" : 0
|
||||
}
|
||||
},
|
||||
"text" : "bar"
|
||||
},
|
||||
{
|
||||
"range" : {
|
||||
"start" : {
|
||||
"line" : 0,
|
||||
"character" : 12
|
||||
},
|
||||
"end" : {
|
||||
"line" : 0,
|
||||
"character" : 12
|
||||
}
|
||||
},
|
||||
"text" : "foo"
|
||||
}
|
||||
],
|
||||
"oldVersion" : "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522",
|
||||
"newVersion" : "ebe55342f9c8b86857402797dd723fb4a2174e0b56d6ace0a6929ec3"
|
||||
}
|
||||
]
|
||||
}
|
||||
}""")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"text/openFile" must {
|
||||
"fail opening a file if it does not exist" taggedAs Flaky in {
|
||||
// Interaction:
|
||||
@ -294,263 +678,6 @@ class TextOperationsTest extends BaseServerTest with FlakySpec {
|
||||
}
|
||||
}
|
||||
|
||||
"grant the canEdit capability if no one else holds it" in {
|
||||
// Interaction:
|
||||
// 1. Client 1 creates a file.
|
||||
// 2. Client 1 receives confirmation.
|
||||
// 3. Client 1 opens the created file.
|
||||
// 4. Client 1 receives the file contents and a canEdit capability.
|
||||
// 5. Client 1 releases the canEdit capability.
|
||||
// 6. Client 1 receives a confirmation.
|
||||
// 7. Client 2 opens the file.
|
||||
// 8. Client 2 receives the file contents and a canEdit capability.
|
||||
val client1 = getInitialisedWsClient()
|
||||
val client2 = getInitialisedWsClient()
|
||||
// 1
|
||||
client1.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/write",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
},
|
||||
"contents": "123456789"
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// 2
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
|
||||
// 3
|
||||
client1.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/openFile",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// 4
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"writeCapability": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": { "path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": ["foo.txt"]
|
||||
} }
|
||||
},
|
||||
"content": "123456789",
|
||||
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// 5
|
||||
client1.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/release",
|
||||
"id": 2,
|
||||
"params": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": { "path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": ["foo.txt"]
|
||||
} }
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// 6
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
|
||||
// 7
|
||||
client2.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/openFile",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
// 8
|
||||
client2.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"result": {
|
||||
"writeCapability": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": { "path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": ["foo.txt"]
|
||||
} }
|
||||
},
|
||||
"content": "123456789",
|
||||
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"take canEdit capability away from clients when another client registers for it" in {
|
||||
val client1 = getInitialisedWsClient()
|
||||
val client2 = getInitialisedWsClient()
|
||||
val client3 = getInitialisedWsClient()
|
||||
client1.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/write",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
},
|
||||
"contents": "123456789"
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
|
||||
client1.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/openFile",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"writeCapability": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": { "path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": ["foo.txt"]
|
||||
} }
|
||||
},
|
||||
"content": "123456789",
|
||||
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client2.expectNoMessage()
|
||||
client3.expectNoMessage()
|
||||
|
||||
client2.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/acquire",
|
||||
"id": 2,
|
||||
"params": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
client1.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/forceReleased",
|
||||
"params": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client2.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
client3.expectNoMessage()
|
||||
|
||||
client3.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/acquire",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
client1.expectNoMessage()
|
||||
client2.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "capability/forceReleased",
|
||||
"params": {
|
||||
"method": "text/canEdit",
|
||||
"registerOptions": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client3.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"text/closeFile" must {
|
||||
|
||||
"fail when a client didn't open it before" in {
|
||||
|
@ -116,6 +116,10 @@ object Runtime {
|
||||
value = classOf[Api.VisualizationUpdate],
|
||||
name = "visualizationUpdate"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.FileEdit],
|
||||
name = "fileEdit"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.AttachVisualization],
|
||||
name = "attachVisualization"
|
||||
@ -152,6 +156,18 @@ object Runtime {
|
||||
value = classOf[Api.ProjectRenamed],
|
||||
name = "projectRenamed"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.RenameSymbol],
|
||||
name = "renameSymbol"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.SymbolRenamed],
|
||||
name = "symbolRenamed"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.SymbolRenameFailed],
|
||||
name = "symbolRenameFailed"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[Api.ContextNotExistError],
|
||||
name = "contextNotExistError"
|
||||
@ -1162,6 +1178,30 @@ object Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
/** A list of edits applied to a file.
|
||||
*
|
||||
* @param path the module file path
|
||||
* @param edits the list of text edits
|
||||
* @param oldVersion the current version of a buffer
|
||||
* @param newVersion the version of a buffer after applying all edits
|
||||
*/
|
||||
final case class FileEdit(
|
||||
path: File,
|
||||
edits: Vector[TextEdit],
|
||||
oldVersion: String,
|
||||
newVersion: String
|
||||
) extends ApiNotification
|
||||
with ToLogString {
|
||||
|
||||
override def toLogString(shouldMask: Boolean): String =
|
||||
"FileEdit(" +
|
||||
s"path=${MaskedPath(path.toPath).toLogString(shouldMask)}," +
|
||||
s"edits=${edits.mkString("[", ",", "]")}" +
|
||||
s"oldVersion=$oldVersion" +
|
||||
s"newVersion=$newVersion" +
|
||||
")"
|
||||
}
|
||||
|
||||
/** Envelope for an Api request.
|
||||
*
|
||||
* @param requestId the request identifier.
|
||||
@ -1604,6 +1644,74 @@ object Runtime {
|
||||
final case class ProjectRenamed(namespace: String, newName: String)
|
||||
extends ApiResponse
|
||||
|
||||
/** A request for symbol renaming.
|
||||
*
|
||||
* @param module the qualified module name
|
||||
* @param expressionId the symbol to rename
|
||||
* @param newName the new name of the symbol
|
||||
*/
|
||||
final case class RenameSymbol(
|
||||
module: String,
|
||||
expressionId: ExpressionId,
|
||||
newName: String
|
||||
) extends ApiRequest
|
||||
|
||||
/** Signals that the symbol has been renamed.
|
||||
*
|
||||
* @param newName the new name of the symbol
|
||||
*/
|
||||
final case class SymbolRenamed(newName: String) extends ApiResponse
|
||||
|
||||
/** Signals that the symbol rename has failed.
|
||||
*
|
||||
* @param error the error that happened
|
||||
*/
|
||||
final case class SymbolRenameFailed(error: SymbolRenameFailed.Error)
|
||||
extends ApiResponse
|
||||
|
||||
object SymbolRenameFailed {
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||
@JsonSubTypes(
|
||||
Array(
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[SymbolRenameFailed.ExpressionNotFound],
|
||||
name = "symbolRenameFailedExpressionNotFound"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[SymbolRenameFailed.FailedToApplyEdits],
|
||||
name = "symbolRenameFailedFailedToApplyEdits"
|
||||
),
|
||||
new JsonSubTypes.Type(
|
||||
value = classOf[SymbolRenameFailed.OperationNotSupported],
|
||||
name = "symbolRenameFailedOperationNotSupported"
|
||||
)
|
||||
)
|
||||
) sealed trait Error
|
||||
|
||||
/** Signals that an expression cannot be found by provided id.
|
||||
*
|
||||
* @param expressionId the id of expression
|
||||
*/
|
||||
final case class ExpressionNotFound(expressionId: ExpressionId)
|
||||
extends SymbolRenameFailed.Error
|
||||
|
||||
/** Signals that it was unable to apply edits to the current module contents.
|
||||
*
|
||||
* @param module the module name
|
||||
*/
|
||||
final case class FailedToApplyEdits(module: String)
|
||||
extends SymbolRenameFailed.Error
|
||||
|
||||
/** Signals that the renaming operation is not supported for the
|
||||
* provided expression.
|
||||
*
|
||||
* @param expressionId the id of expression
|
||||
*/
|
||||
final case class OperationNotSupported(expressionId: ExpressionId)
|
||||
extends SymbolRenameFailed.Error
|
||||
}
|
||||
|
||||
/** A notification about the changes in the suggestions database.
|
||||
*
|
||||
* @param module the module name
|
||||
|
@ -1,10 +1,10 @@
|
||||
package org.enso.interpreter.instrument.command
|
||||
|
||||
import org.enso.interpreter.instrument.execution.{Completion, RuntimeContext}
|
||||
import org.enso.polyglot.runtime.Runtime.{Api, ApiResponse}
|
||||
import org.enso.polyglot.runtime.Runtime.{Api, ApiNotification, ApiResponse}
|
||||
import org.enso.polyglot.runtime.Runtime.Api.RequestId
|
||||
|
||||
import scala.concurrent.{ExecutionContext}
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
/** Base command trait that encapsulates a function request. Uses
|
||||
* [[RuntimeContext]] to perform a request.
|
||||
@ -30,4 +30,9 @@ abstract class Command(maybeRequestId: Option[RequestId]) {
|
||||
ctx.endpoint.sendToClient(Api.Response(maybeRequestId, payload))
|
||||
}
|
||||
|
||||
protected def notify(
|
||||
payload: ApiNotification
|
||||
)(implicit ctx: RuntimeContext): Unit = {
|
||||
ctx.endpoint.sendToClient(Api.Response(None, payload))
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,9 @@ object CommandFactory {
|
||||
case payload: Api.RenameProject =>
|
||||
new RenameProjectCmd(request.requestId, payload)
|
||||
|
||||
case payload: Api.RenameSymbol =>
|
||||
new RenameSymbolCmd(request.requestId, payload)
|
||||
|
||||
case payload: Api.OpenFileNotification =>
|
||||
new OpenFileCmd(payload)
|
||||
case payload: Api.CloseFileNotification => new CloseFileCmd(payload)
|
||||
|
@ -0,0 +1,72 @@
|
||||
package org.enso.interpreter.instrument.command
|
||||
|
||||
import org.enso.interpreter.instrument.execution.RuntimeContext
|
||||
import org.enso.interpreter.instrument.job.{
|
||||
EnsureCompiledJob,
|
||||
ExecuteJob,
|
||||
RefactoringRenameJob
|
||||
}
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
|
||||
import java.io.File
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/** A command that orchestrates renaming of a symbol.
|
||||
*
|
||||
* @param maybeRequestId an option with request id
|
||||
* @param request a request for a service
|
||||
*/
|
||||
class RenameSymbolCmd(
|
||||
maybeRequestId: Option[Api.RequestId],
|
||||
request: Api.RenameSymbol
|
||||
) extends AsynchronousCommand(maybeRequestId) {
|
||||
|
||||
/** @inheritdoc */
|
||||
override def executeAsynchronously(implicit
|
||||
ctx: RuntimeContext,
|
||||
ec: ExecutionContext
|
||||
): Future[Unit] = {
|
||||
val moduleFile = ctx.executionService.getContext
|
||||
.findModule(request.module)
|
||||
.map(module => Seq(new File(module.getPath)))
|
||||
.orElseGet(() => Seq())
|
||||
|
||||
val ensureCompiledJob = ctx.jobProcessor.run(
|
||||
new EnsureCompiledJob(
|
||||
(ctx.state.pendingEdits.files ++ moduleFile).distinct,
|
||||
isCancellable = false
|
||||
)
|
||||
)
|
||||
val refactoringRenameJob = ctx.jobProcessor.run(
|
||||
new RefactoringRenameJob(
|
||||
maybeRequestId,
|
||||
request.module,
|
||||
request.expressionId,
|
||||
request.newName
|
||||
)
|
||||
)
|
||||
for {
|
||||
_ <- ensureCompiledJob
|
||||
refactoredFiles <- refactoringRenameJob
|
||||
_ <-
|
||||
if (refactoredFiles.isEmpty) Future.successful(())
|
||||
else reExecute(refactoredFiles)
|
||||
} yield ()
|
||||
}
|
||||
|
||||
private def reExecute(files: Seq[File])(implicit
|
||||
ctx: RuntimeContext,
|
||||
ec: ExecutionContext
|
||||
): Future[Unit] =
|
||||
for {
|
||||
_ <- ctx.jobProcessor.run(new EnsureCompiledJob(files))
|
||||
_ <- Future.sequence {
|
||||
ctx.contextManager.getAllContexts
|
||||
.collect {
|
||||
case (contextId, stack) if stack.nonEmpty =>
|
||||
ctx.jobProcessor.run(ExecuteJob(contextId, stack.toList))
|
||||
}
|
||||
}
|
||||
} yield ()
|
||||
}
|
@ -3,6 +3,7 @@ package org.enso.interpreter.instrument.execution
|
||||
import org.enso.interpreter.instrument.InterpreterContext
|
||||
import org.enso.interpreter.instrument.command.Command
|
||||
import org.enso.polyglot.RuntimeOptions
|
||||
import org.enso.text.Sha3_224VersionCalculator
|
||||
|
||||
import scala.concurrent.{ExecutionContext, ExecutionContextExecutor}
|
||||
|
||||
@ -48,14 +49,15 @@ class CommandExecutionEngine(interpreterContext: InterpreterContext)
|
||||
|
||||
private val runtimeContext =
|
||||
RuntimeContext(
|
||||
executionService = interpreterContext.executionService,
|
||||
contextManager = interpreterContext.contextManager,
|
||||
endpoint = interpreterContext.endpoint,
|
||||
truffleContext = interpreterContext.truffleContext,
|
||||
jobProcessor = jobExecutionEngine,
|
||||
jobControlPlane = jobExecutionEngine,
|
||||
locking = locking,
|
||||
state = executionState
|
||||
executionService = interpreterContext.executionService,
|
||||
contextManager = interpreterContext.contextManager,
|
||||
endpoint = interpreterContext.endpoint,
|
||||
truffleContext = interpreterContext.truffleContext,
|
||||
jobProcessor = jobExecutionEngine,
|
||||
jobControlPlane = jobExecutionEngine,
|
||||
locking = locking,
|
||||
state = executionState,
|
||||
versionCalculator = Sha3_224VersionCalculator
|
||||
)
|
||||
|
||||
/** @inheritdoc */
|
||||
|
@ -2,6 +2,7 @@ package org.enso.interpreter.instrument.execution
|
||||
|
||||
import org.enso.interpreter.instrument.InterpreterContext
|
||||
import org.enso.interpreter.instrument.job.{BackgroundJob, Job, UniqueJob}
|
||||
import org.enso.text.Sha3_224VersionCalculator
|
||||
|
||||
import java.util
|
||||
import java.util.{Collections, UUID}
|
||||
@ -50,14 +51,15 @@ final class JobExecutionEngine(
|
||||
|
||||
private val runtimeContext =
|
||||
RuntimeContext(
|
||||
executionService = interpreterContext.executionService,
|
||||
contextManager = interpreterContext.contextManager,
|
||||
endpoint = interpreterContext.endpoint,
|
||||
truffleContext = interpreterContext.truffleContext,
|
||||
jobProcessor = this,
|
||||
jobControlPlane = this,
|
||||
locking = locking,
|
||||
state = executionState
|
||||
executionService = interpreterContext.executionService,
|
||||
contextManager = interpreterContext.contextManager,
|
||||
endpoint = interpreterContext.endpoint,
|
||||
truffleContext = interpreterContext.truffleContext,
|
||||
jobProcessor = this,
|
||||
jobControlPlane = this,
|
||||
locking = locking,
|
||||
state = executionState,
|
||||
versionCalculator = Sha3_224VersionCalculator
|
||||
)
|
||||
|
||||
/** @inheritdoc */
|
||||
|
@ -19,4 +19,10 @@ trait PendingEdits {
|
||||
* @return the list of pending edits
|
||||
*/
|
||||
def dequeue(file: File): Seq[PendingEdit]
|
||||
|
||||
/** List files with pending edits.
|
||||
*
|
||||
* @return the list of files with pending edits
|
||||
*/
|
||||
def files: Seq[File]
|
||||
}
|
||||
|
@ -20,4 +20,8 @@ final class PendingFileEdits(
|
||||
/** @inheritdoc */
|
||||
override def dequeue(file: File): Seq[PendingEdit] =
|
||||
pending.remove(file).getOrElse(Seq())
|
||||
|
||||
/** @inheritdoc */
|
||||
override def files: Seq[File] =
|
||||
pending.keys.toSeq
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package org.enso.interpreter.instrument.execution
|
||||
import com.oracle.truffle.api.TruffleContext
|
||||
import org.enso.interpreter.instrument.{Endpoint, ExecutionContextManager}
|
||||
import org.enso.interpreter.service.ExecutionService
|
||||
import org.enso.text.ContentBasedVersioning
|
||||
|
||||
/** Contains suppliers of services that provide application specific
|
||||
* functionality.
|
||||
@ -16,6 +17,7 @@ import org.enso.interpreter.service.ExecutionService
|
||||
* @param jobControlPlane a job control plane
|
||||
* @param locking a locking service
|
||||
* @param state a state of the runtime
|
||||
* @param versionCalculator a content based version calculator
|
||||
*/
|
||||
case class RuntimeContext(
|
||||
executionService: ExecutionService,
|
||||
@ -25,5 +27,6 @@ case class RuntimeContext(
|
||||
jobProcessor: JobProcessor,
|
||||
jobControlPlane: JobControlPlane,
|
||||
locking: Locking,
|
||||
state: ExecutionState
|
||||
state: ExecutionState,
|
||||
versionCalculator: ContentBasedVersioning
|
||||
)
|
||||
|
@ -34,9 +34,16 @@ import scala.jdk.OptionConverters._
|
||||
/** A job that ensures that specified files are compiled.
|
||||
*
|
||||
* @param files a files to compile
|
||||
* @param isCancellable a flag indicating if the job is cancellable
|
||||
*/
|
||||
final class EnsureCompiledJob(protected val files: Iterable[File])
|
||||
extends Job[EnsureCompiledJob.CompilationStatus](List.empty, true, false) {
|
||||
final class EnsureCompiledJob(
|
||||
protected val files: Iterable[File],
|
||||
isCancellable: Boolean = true
|
||||
) extends Job[EnsureCompiledJob.CompilationStatus](
|
||||
List.empty,
|
||||
isCancellable,
|
||||
false
|
||||
) {
|
||||
|
||||
import EnsureCompiledJob.CompilationStatus
|
||||
|
||||
@ -282,7 +289,6 @@ final class EnsureCompiledJob(protected val files: Iterable[File])
|
||||
*
|
||||
* @param changeset the [[Changeset]] object capturing the previous
|
||||
* version of IR
|
||||
* @param ctx the runtime context
|
||||
* @return the list of cache invalidation commands
|
||||
*/
|
||||
private def buildCacheInvalidationCommands(
|
||||
@ -291,7 +297,7 @@ final class EnsureCompiledJob(protected val files: Iterable[File])
|
||||
): Seq[CacheInvalidation] = {
|
||||
val invalidateExpressionsCommand =
|
||||
CacheInvalidation.Command.InvalidateKeys(changeset.invalidated)
|
||||
val scopeIds = splitMeta(source.toString())._2.map(_._2)
|
||||
val scopeIds = splitMeta(source.toString)._2.map(_._2)
|
||||
val invalidateStaleCommand =
|
||||
CacheInvalidation.Command.InvalidateStale(scopeIds)
|
||||
Seq(
|
||||
@ -518,7 +524,7 @@ object EnsureCompiledJob {
|
||||
|
||||
/** The outcome of a compilation. */
|
||||
sealed trait CompilationStatus
|
||||
case object CompilationStatus {
|
||||
private case object CompilationStatus {
|
||||
|
||||
/** Compilation completed. */
|
||||
case object Success extends CompilationStatus
|
||||
|
@ -0,0 +1,204 @@
|
||||
package org.enso.interpreter.instrument.job
|
||||
|
||||
import org.enso.compiler.core.IR
|
||||
import org.enso.compiler.refactoring.IRUtils
|
||||
import org.enso.interpreter.instrument.execution.RuntimeContext
|
||||
import org.enso.interpreter.instrument.execution.model.PendingEdit
|
||||
import org.enso.interpreter.service.error.ModuleNotFoundException
|
||||
import org.enso.polyglot.runtime.Runtime.{Api, ApiNotification, ApiResponse}
|
||||
import org.enso.refactoring.RenameUtils
|
||||
import org.enso.refactoring.validation.MethodNameValidation
|
||||
import org.enso.text.editing.EditorOps
|
||||
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import java.util.logging.Level
|
||||
|
||||
/** A job responsible for refactoring renaming operation.
|
||||
*
|
||||
* @param maybeRequestId the original request id
|
||||
* @param moduleName the qualified module name
|
||||
* @param expressionId the symbol to rename
|
||||
* @param newName the new name of the symbol
|
||||
*/
|
||||
final class RefactoringRenameJob(
|
||||
maybeRequestId: Option[Api.RequestId],
|
||||
moduleName: String,
|
||||
expressionId: UUID,
|
||||
newName: String
|
||||
) extends Job[Seq[File]](
|
||||
List(),
|
||||
isCancellable = false,
|
||||
mayInterruptIfRunning = false
|
||||
) {
|
||||
|
||||
/** @inheritdoc */
|
||||
override def run(implicit ctx: RuntimeContext): Seq[File] = {
|
||||
val logger = ctx.executionService.getLogger
|
||||
val compilationLockTimestamp = ctx.locking.acquireReadCompilationLock()
|
||||
try {
|
||||
logger.log(
|
||||
Level.FINE,
|
||||
s"Renaming symbol [{0}]...",
|
||||
expressionId
|
||||
)
|
||||
val refactoredFile = applyRefactoringEdits()
|
||||
Seq(refactoredFile)
|
||||
} catch {
|
||||
case _: ModuleNotFoundException =>
|
||||
reply(Api.ModuleNotFound(moduleName))
|
||||
Seq()
|
||||
case ex: RefactoringRenameJob.ExpressionNotFound =>
|
||||
reply(
|
||||
Api.SymbolRenameFailed(
|
||||
Api.SymbolRenameFailed.ExpressionNotFound(ex.expressionId)
|
||||
)
|
||||
)
|
||||
Seq()
|
||||
case ex: RefactoringRenameJob.FailedToApplyEdits =>
|
||||
reply(
|
||||
Api.SymbolRenameFailed(
|
||||
Api.SymbolRenameFailed.FailedToApplyEdits(ex.module)
|
||||
)
|
||||
)
|
||||
Seq()
|
||||
case ex: RefactoringRenameJob.OperationNotSupported =>
|
||||
reply(
|
||||
Api.SymbolRenameFailed(
|
||||
Api.SymbolRenameFailed.OperationNotSupported(ex.expressionId)
|
||||
)
|
||||
)
|
||||
Seq()
|
||||
} finally {
|
||||
ctx.locking.releaseReadCompilationLock()
|
||||
logger.log(
|
||||
Level.FINEST,
|
||||
s"Kept read compilation lock [{0}] for {1} milliseconds.",
|
||||
Array(
|
||||
getClass.getSimpleName,
|
||||
System.currentTimeMillis() - compilationLockTimestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def applyRefactoringEdits()(implicit ctx: RuntimeContext): File = {
|
||||
val module = ctx.executionService.getContext
|
||||
.findModule(moduleName)
|
||||
.orElseThrow(() => new ModuleNotFoundException(moduleName))
|
||||
val newSymbolName = MethodNameValidation.normalize(newName)
|
||||
|
||||
val expression = IRUtils
|
||||
.findByExternalId(module.getIr, expressionId)
|
||||
.getOrElse(
|
||||
throw new RefactoringRenameJob.ExpressionNotFound(expressionId)
|
||||
)
|
||||
val local = getLiteral(expression)
|
||||
val methodDefinition = getMethodDefinition(expression)
|
||||
val symbol = local
|
||||
.orElse(methodDefinition)
|
||||
.getOrElse(
|
||||
throw new RefactoringRenameJob.OperationNotSupported(expressionId)
|
||||
)
|
||||
|
||||
def localUsages = local.flatMap(IRUtils.findLocalUsages(module.getIr, _))
|
||||
def methodDefinitionUsages = methodDefinition.flatMap(
|
||||
IRUtils.findModuleMethodUsages(module.getName, module.getIr, _)
|
||||
)
|
||||
|
||||
val usages = localUsages
|
||||
.orElse(methodDefinitionUsages)
|
||||
.getOrElse(Set())
|
||||
.concat(Set(symbol))
|
||||
.flatMap(_.location)
|
||||
.map(_.location)
|
||||
.toSeq
|
||||
val edits =
|
||||
RenameUtils.buildEdits(module.getLiteralSource, usages, newSymbolName)
|
||||
|
||||
val oldVersion =
|
||||
ctx.versionCalculator.evalVersion(module.getLiteralSource.toString)
|
||||
val newContents =
|
||||
EditorOps
|
||||
.applyEdits(module.getLiteralSource, edits)
|
||||
.getOrElse(
|
||||
throw new RefactoringRenameJob.FailedToApplyEdits(moduleName)
|
||||
)
|
||||
val newVersion = ctx.versionCalculator.evalVersion(newContents.toString)
|
||||
|
||||
val fileEdit = Api.FileEdit(
|
||||
new File(module.getPath),
|
||||
edits.toVector,
|
||||
oldVersion.toHexString,
|
||||
newVersion.toHexString
|
||||
)
|
||||
enqueuePendingEdits(fileEdit)
|
||||
notify(fileEdit)
|
||||
reply(Api.SymbolRenamed(newSymbolName))
|
||||
|
||||
fileEdit.path
|
||||
}
|
||||
|
||||
private def enqueuePendingEdits(fileEdit: Api.FileEdit)(implicit
|
||||
ctx: RuntimeContext
|
||||
): Unit = {
|
||||
val pendingEditsLockTimestamp = ctx.locking.acquirePendingEditsLock()
|
||||
try {
|
||||
val pendingEdits =
|
||||
fileEdit.edits.map(PendingEdit.ApplyEdit(_, execute = true))
|
||||
ctx.state.pendingEdits.enqueue(fileEdit.path, pendingEdits)
|
||||
} finally {
|
||||
ctx.locking.releasePendingEditsLock()
|
||||
ctx.executionService.getLogger.log(
|
||||
Level.FINEST,
|
||||
s"Kept pending edits lock [{0}] for {1} milliseconds.",
|
||||
Array(
|
||||
getClass.getSimpleName,
|
||||
System.currentTimeMillis() - pendingEditsLockTimestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def getLiteral(ir: IR): Option[IR.Name.Literal] =
|
||||
ir match {
|
||||
case literal: IR.Name.Literal => Some(literal)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
private def getMethodDefinition(ir: IR): Option[IR.Name] =
|
||||
ir match {
|
||||
case methodRef: IR.Name.MethodReference
|
||||
if methodRef.typePointer.isEmpty =>
|
||||
Some(methodRef.methodName)
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
|
||||
private def reply(
|
||||
payload: ApiResponse
|
||||
)(implicit ctx: RuntimeContext): Unit = {
|
||||
ctx.endpoint.sendToClient(Api.Response(maybeRequestId, payload))
|
||||
}
|
||||
|
||||
private def notify(
|
||||
payload: ApiNotification
|
||||
)(implicit ctx: RuntimeContext): Unit = {
|
||||
ctx.endpoint.sendToClient(Api.Response(None, payload))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object RefactoringRenameJob {
|
||||
|
||||
final private class ExpressionNotFound(val expressionId: IR.ExternalId)
|
||||
extends Exception(s"Expression was not found by id [$expressionId].")
|
||||
|
||||
final private class FailedToApplyEdits(val module: String)
|
||||
extends Exception(s"Failed to apply edits to module [$module]")
|
||||
|
||||
final private class OperationNotSupported(val expressionId: IR.ExternalId)
|
||||
extends Exception(
|
||||
s"Operation not supported for expression [$expressionId]"
|
||||
)
|
||||
}
|
@ -22,7 +22,6 @@ import org.enso.interpreter.instrument.{
|
||||
import org.enso.interpreter.runtime.Module
|
||||
import org.enso.interpreter.runtime.control.ThreadInterruptedException
|
||||
import org.enso.pkg.QualifiedName
|
||||
//import org.enso.polyglot.runtime.Runtime.Api.
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
|
||||
import java.util.logging.Level
|
||||
|
@ -0,0 +1,865 @@
|
||||
package org.enso.interpreter.test.instrument
|
||||
|
||||
import org.apache.commons.io.output.TeeOutputStream
|
||||
import org.enso.distribution.FileSystem
|
||||
import org.enso.distribution.locking.ThreadSafeFileLockManager
|
||||
import org.enso.interpreter.runtime.`type`.ConstantsGen
|
||||
import org.enso.interpreter.test.Metadata
|
||||
import org.enso.pkg.{Package, PackageManager}
|
||||
import org.enso.polyglot._
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.text.{ContentBasedVersioning, Sha3_224VersionCalculator}
|
||||
import org.enso.text.editing.model
|
||||
import org.enso.text.editing.model.TextEdit
|
||||
import org.graalvm.polyglot.Context
|
||||
import org.scalatest.BeforeAndAfterEach
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
import java.io.{ByteArrayOutputStream, File}
|
||||
import java.nio.file.{Files, Path, Paths}
|
||||
import java.util.UUID
|
||||
|
||||
@scala.annotation.nowarn("msg=multiarg infix syntax")
|
||||
class RuntimeRefactoringTest
|
||||
extends AnyFlatSpec
|
||||
with Matchers
|
||||
with BeforeAndAfterEach {
|
||||
|
||||
// === Test Utilities =======================================================
|
||||
|
||||
var context: TestContext = _
|
||||
|
||||
class TestContext(packageName: String) extends InstrumentTestContext {
|
||||
|
||||
val tmpDir: Path = Files.createTempDirectory("enso-test-packages")
|
||||
sys.addShutdownHook(FileSystem.removeDirectoryIfExists(tmpDir))
|
||||
val lockManager = new ThreadSafeFileLockManager(tmpDir.resolve("locks"))
|
||||
val runtimeServerEmulator =
|
||||
new RuntimeServerEmulator(messageQueue, lockManager)
|
||||
|
||||
val pkg: Package[File] =
|
||||
PackageManager.Default.create(tmpDir.toFile, packageName, "Enso_Test")
|
||||
val out: ByteArrayOutputStream = new ByteArrayOutputStream()
|
||||
val logOut: ByteArrayOutputStream = new ByteArrayOutputStream()
|
||||
val executionContext = new PolyglotContext(
|
||||
Context
|
||||
.newBuilder(LanguageInfo.ID)
|
||||
.allowExperimentalOptions(true)
|
||||
.allowAllAccess(true)
|
||||
.option(RuntimeOptions.PROJECT_ROOT, pkg.root.getAbsolutePath)
|
||||
.option(RuntimeOptions.LOG_LEVEL, "WARNING")
|
||||
.option(RuntimeOptions.INTERPRETER_SEQUENTIAL_COMMAND_EXECUTION, "true")
|
||||
.option(RuntimeOptions.ENABLE_PROJECT_SUGGESTIONS, "false")
|
||||
.option(RuntimeOptions.ENABLE_GLOBAL_SUGGESTIONS, "false")
|
||||
.option(RuntimeOptions.ENABLE_EXECUTION_TIMER, "false")
|
||||
.option(
|
||||
RuntimeOptions.DISABLE_IR_CACHES,
|
||||
InstrumentTestContext.DISABLE_IR_CACHE
|
||||
)
|
||||
.option(RuntimeServerInfo.ENABLE_OPTION, "true")
|
||||
.option(RuntimeOptions.INTERACTIVE_MODE, "true")
|
||||
.option(
|
||||
RuntimeOptions.LANGUAGE_HOME_OVERRIDE,
|
||||
Paths
|
||||
.get("../../test/micro-distribution/component")
|
||||
.toFile
|
||||
.getAbsolutePath
|
||||
)
|
||||
.option(RuntimeOptions.EDITION_OVERRIDE, "0.0.0-dev")
|
||||
.logHandler(new TeeOutputStream(logOut, System.err))
|
||||
.out(new TeeOutputStream(out, System.err))
|
||||
.serverTransport(runtimeServerEmulator.makeServerTransport)
|
||||
.build()
|
||||
)
|
||||
executionContext.context.initialize(LanguageInfo.ID)
|
||||
|
||||
def writeMain(contents: String): File =
|
||||
Files.write(pkg.mainFile.toPath, contents.getBytes).toFile
|
||||
|
||||
def readMain: String =
|
||||
Files.readString(pkg.mainFile.toPath)
|
||||
|
||||
def send(msg: Api.Request): Unit = runtimeServerEmulator.sendToRuntime(msg)
|
||||
|
||||
def consumeOut: List[String] = {
|
||||
val result = out.toString
|
||||
out.reset()
|
||||
result.linesIterator.toList
|
||||
}
|
||||
|
||||
def executionComplete(contextId: UUID): Api.Response =
|
||||
Api.Response(Api.ExecutionComplete(contextId))
|
||||
}
|
||||
|
||||
val versionCalculator: ContentBasedVersioning = Sha3_224VersionCalculator
|
||||
|
||||
override protected def beforeEach(): Unit = {
|
||||
context = new TestContext("Test")
|
||||
val Some(Api.Response(_, Api.InitializedNotification())) = context.receive
|
||||
}
|
||||
|
||||
override protected def afterEach(): Unit = {
|
||||
context.executionContext.context.close()
|
||||
context.runtimeServerEmulator.terminate()
|
||||
}
|
||||
|
||||
"RuntimeServer" should "rename operator in main body" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
|
||||
val metadata = new Metadata
|
||||
val idOperator1 = metadata.addItem(42, 9)
|
||||
val code =
|
||||
"""from Standard.Base import all
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = operator1 + 1
|
||||
| IO.println operator2
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// open file
|
||||
context.send(
|
||||
Api.Request(Api.OpenFileNotification(mainFile, contents))
|
||||
)
|
||||
context.receiveNone shouldEqual None
|
||||
|
||||
// push main
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(
|
||||
contextId,
|
||||
Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
|
||||
// rename operator1
|
||||
val newName = "foobarbaz"
|
||||
val expectedEdits = Vector(
|
||||
TextEdit(
|
||||
model.Range(model.Position(3, 4), model.Position(3, 13)),
|
||||
newName
|
||||
),
|
||||
TextEdit(
|
||||
model.Range(model.Position(4, 16), model.Position(4, 25)),
|
||||
newName
|
||||
)
|
||||
)
|
||||
val expectedFileEdit = Api.FileEdit(
|
||||
context.pkg.mainFile,
|
||||
expectedEdits,
|
||||
versionCalculator.evalVersion(contents).toHexString,
|
||||
versionCalculator
|
||||
.evalVersion(contents.replaceAll("operator1", newName))
|
||||
.toHexString
|
||||
)
|
||||
context.send(
|
||||
Api.Request(requestId, Api.RenameSymbol(moduleName, idOperator1, newName))
|
||||
)
|
||||
context.receiveNIgnoreStdLib(4) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.SymbolRenamed(newName)),
|
||||
Api.Response(None, expectedFileEdit),
|
||||
TestMessages.pending(contextId, idOperator1),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
}
|
||||
|
||||
it should "rename operator in lambda expression" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
|
||||
val metadata = new Metadata
|
||||
val idOperator1 = metadata.addItem(42, 9)
|
||||
val code =
|
||||
"""from Standard.Base import all
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = x-> operator1 + x
|
||||
| IO.println (operator2 1)
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// open file
|
||||
context.send(
|
||||
Api.Request(Api.OpenFileNotification(mainFile, contents))
|
||||
)
|
||||
context.receiveNone shouldEqual None
|
||||
|
||||
// push main
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(
|
||||
contextId,
|
||||
Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
|
||||
// rename operator1
|
||||
val newName = "foobarbaz"
|
||||
val expectedEdits = Vector(
|
||||
TextEdit(
|
||||
model.Range(model.Position(3, 4), model.Position(3, 13)),
|
||||
newName
|
||||
),
|
||||
TextEdit(
|
||||
model.Range(model.Position(4, 20), model.Position(4, 29)),
|
||||
newName
|
||||
)
|
||||
)
|
||||
val expectedFileEdit = Api.FileEdit(
|
||||
context.pkg.mainFile,
|
||||
expectedEdits,
|
||||
versionCalculator.evalVersion(contents).toHexString,
|
||||
versionCalculator
|
||||
.evalVersion(contents.replaceAll("operator1", newName))
|
||||
.toHexString
|
||||
)
|
||||
context.send(
|
||||
Api.Request(requestId, Api.RenameSymbol(moduleName, idOperator1, newName))
|
||||
)
|
||||
context.receiveNIgnoreStdLib(4) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.SymbolRenamed(newName)),
|
||||
Api.Response(None, expectedFileEdit),
|
||||
TestMessages.pending(contextId, idOperator1),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
}
|
||||
|
||||
it should "edit file after renaming local" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
|
||||
val metadata = new Metadata
|
||||
val symbolOperator1 = metadata.addItem(42, 9, "aa")
|
||||
val exprOperator1 = metadata.addItem(54, 2, "ab")
|
||||
val exprOperator2 = metadata.addItem(73, 13, "ac")
|
||||
val code =
|
||||
"""from Standard.Base import all
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = operator1 + 1
|
||||
| IO.println operator2
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// open file
|
||||
context.send(
|
||||
Api.Request(Api.OpenFileNotification(mainFile, contents))
|
||||
)
|
||||
context.receiveNone shouldEqual None
|
||||
|
||||
// push main
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(
|
||||
contextId,
|
||||
Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
context.receiveNIgnoreStdLib(5) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
TestMessages.update(contextId, exprOperator1, ConstantsGen.INTEGER),
|
||||
TestMessages.update(contextId, exprOperator2, ConstantsGen.INTEGER),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
|
||||
// rename operator1
|
||||
val newName = "foobarbaz"
|
||||
val expectedEdits = Vector(
|
||||
TextEdit(
|
||||
model.Range(model.Position(3, 4), model.Position(3, 13)),
|
||||
newName
|
||||
),
|
||||
TextEdit(
|
||||
model.Range(model.Position(4, 16), model.Position(4, 25)),
|
||||
newName
|
||||
)
|
||||
)
|
||||
val expectedFileEdit = Api.FileEdit(
|
||||
context.pkg.mainFile,
|
||||
expectedEdits,
|
||||
versionCalculator.evalVersion(contents).toHexString,
|
||||
versionCalculator
|
||||
.evalVersion(contents.replaceAll("operator1", newName))
|
||||
.toHexString
|
||||
)
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.RenameSymbol(moduleName, symbolOperator1, newName)
|
||||
)
|
||||
)
|
||||
context.receiveNIgnoreStdLib(5) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.SymbolRenamed(newName)),
|
||||
Api.Response(None, expectedFileEdit),
|
||||
TestMessages.pending(contextId, symbolOperator1, exprOperator2),
|
||||
TestMessages.update(
|
||||
contextId,
|
||||
exprOperator2,
|
||||
ConstantsGen.INTEGER,
|
||||
typeChanged = false
|
||||
),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
|
||||
// modify main
|
||||
context.send(
|
||||
Api.Request(
|
||||
Api.EditFileNotification(
|
||||
mainFile,
|
||||
Seq(
|
||||
TextEdit(
|
||||
model.Range(model.Position(3, 16), model.Position(3, 18)),
|
||||
"42"
|
||||
)
|
||||
),
|
||||
execute = true
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receiveN(4) should contain theSameElementsAs Seq(
|
||||
TestMessages.pending(contextId, exprOperator1, exprOperator2),
|
||||
TestMessages
|
||||
.update(
|
||||
contextId,
|
||||
exprOperator1,
|
||||
ConstantsGen.INTEGER,
|
||||
typeChanged = false
|
||||
),
|
||||
TestMessages
|
||||
.update(
|
||||
contextId,
|
||||
exprOperator2,
|
||||
ConstantsGen.INTEGER,
|
||||
typeChanged = false
|
||||
),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("43")
|
||||
}
|
||||
|
||||
it should "rename module method in main body" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
|
||||
val metadata = new Metadata
|
||||
val idFunction1 = metadata.addItem(31, 9)
|
||||
val code =
|
||||
"""from Standard.Base import all
|
||||
|
|
||||
|function1 x = x + 1
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = Main.function1 operator1
|
||||
| IO.println operator2
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// open file
|
||||
context.send(
|
||||
Api.Request(Api.OpenFileNotification(mainFile, contents))
|
||||
)
|
||||
context.receiveNone shouldEqual None
|
||||
|
||||
// push main
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(
|
||||
contextId,
|
||||
Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
|
||||
// rename operator1
|
||||
val newName = "function2"
|
||||
val expectedEdits = Vector(
|
||||
TextEdit(
|
||||
model.Range(model.Position(2, 0), model.Position(2, 9)),
|
||||
newName
|
||||
),
|
||||
TextEdit(
|
||||
model.Range(model.Position(6, 21), model.Position(6, 30)),
|
||||
newName
|
||||
)
|
||||
)
|
||||
val expectedFileEdit = Api.FileEdit(
|
||||
context.pkg.mainFile,
|
||||
expectedEdits,
|
||||
versionCalculator.evalVersion(contents).toHexString,
|
||||
versionCalculator
|
||||
.evalVersion(contents.replaceAll("function1", newName))
|
||||
.toHexString
|
||||
)
|
||||
context.send(
|
||||
Api.Request(requestId, Api.RenameSymbol(moduleName, idFunction1, newName))
|
||||
)
|
||||
context.receiveNIgnoreStdLib(4, 5) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.SymbolRenamed(newName)),
|
||||
Api.Response(None, expectedFileEdit),
|
||||
TestMessages.pending(contextId, idFunction1),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
}
|
||||
|
||||
it should "rename module method in lambda expression" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
|
||||
val metadata = new Metadata
|
||||
val idFunction1 = metadata.addItem(31, 9)
|
||||
val code =
|
||||
"""from Standard.Base import all
|
||||
|
|
||||
|function1 x = x + 1
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = x -> Main.function1 x
|
||||
| IO.println (operator2 operator1)
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// open file
|
||||
context.send(
|
||||
Api.Request(Api.OpenFileNotification(mainFile, contents))
|
||||
)
|
||||
context.receiveNone shouldEqual None
|
||||
|
||||
// push main
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(
|
||||
contextId,
|
||||
Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
|
||||
// rename operator1
|
||||
val newName = "function2"
|
||||
val expectedEdits = Vector(
|
||||
TextEdit(
|
||||
model.Range(model.Position(2, 0), model.Position(2, 9)),
|
||||
newName
|
||||
),
|
||||
TextEdit(
|
||||
model.Range(model.Position(6, 26), model.Position(6, 35)),
|
||||
newName
|
||||
)
|
||||
)
|
||||
val expectedFileEdit = Api.FileEdit(
|
||||
context.pkg.mainFile,
|
||||
expectedEdits,
|
||||
versionCalculator.evalVersion(contents).toHexString,
|
||||
versionCalculator
|
||||
.evalVersion(contents.replaceAll("function1", newName))
|
||||
.toHexString
|
||||
)
|
||||
context.send(
|
||||
Api.Request(requestId, Api.RenameSymbol(moduleName, idFunction1, newName))
|
||||
)
|
||||
context.receiveNIgnoreStdLib(4, 5) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.SymbolRenamed(newName)),
|
||||
Api.Response(None, expectedFileEdit),
|
||||
TestMessages.pending(contextId, idFunction1),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
}
|
||||
|
||||
it should "edit file after renaming module method" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
|
||||
val metadata = new Metadata
|
||||
val symbolFunction1 = metadata.addItem(31, 9, "aa")
|
||||
val exprOperator1 = metadata.addItem(75, 2, "ab")
|
||||
val exprOperator2 = metadata.addItem(94, 24, "ac")
|
||||
val code =
|
||||
"""from Standard.Base import all
|
||||
|
|
||||
|function1 x = x + 1
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = Main.function1 operator1
|
||||
| IO.println operator2
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// open file
|
||||
context.send(
|
||||
Api.Request(Api.OpenFileNotification(mainFile, contents))
|
||||
)
|
||||
context.receiveNone shouldEqual None
|
||||
|
||||
// push main
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(
|
||||
contextId,
|
||||
Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
context.receiveNIgnoreStdLib(5) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
TestMessages.update(contextId, exprOperator1, ConstantsGen.INTEGER),
|
||||
TestMessages.update(
|
||||
contextId,
|
||||
exprOperator2,
|
||||
ConstantsGen.INTEGER,
|
||||
Api.MethodCall(Api.MethodPointer(moduleName, moduleName, "function1"))
|
||||
),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
|
||||
// rename operator1
|
||||
val newName = "function2"
|
||||
val expectedEdits = Vector(
|
||||
TextEdit(
|
||||
model.Range(model.Position(2, 0), model.Position(2, 9)),
|
||||
newName
|
||||
),
|
||||
TextEdit(
|
||||
model.Range(model.Position(6, 21), model.Position(6, 30)),
|
||||
newName
|
||||
)
|
||||
)
|
||||
val expectedFileEdit = Api.FileEdit(
|
||||
context.pkg.mainFile,
|
||||
expectedEdits,
|
||||
versionCalculator.evalVersion(contents).toHexString,
|
||||
versionCalculator
|
||||
.evalVersion(contents.replaceAll("function1", newName))
|
||||
.toHexString
|
||||
)
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.RenameSymbol(moduleName, symbolFunction1, newName)
|
||||
)
|
||||
)
|
||||
context.receiveNIgnoreStdLib(5) should contain theSameElementsAs Seq(
|
||||
Api.Response(requestId, Api.SymbolRenamed(newName)),
|
||||
Api.Response(None, expectedFileEdit),
|
||||
TestMessages.pending(contextId, symbolFunction1, exprOperator2),
|
||||
TestMessages.update(
|
||||
contextId,
|
||||
exprOperator2,
|
||||
ConstantsGen.INTEGER,
|
||||
Api.MethodCall(Api.MethodPointer(moduleName, moduleName, "function2"))
|
||||
),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
|
||||
// modify main
|
||||
context.send(
|
||||
Api.Request(
|
||||
Api.EditFileNotification(
|
||||
mainFile,
|
||||
Seq(
|
||||
TextEdit(
|
||||
model.Range(model.Position(5, 16), model.Position(5, 18)),
|
||||
"42"
|
||||
)
|
||||
),
|
||||
execute = true
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receiveN(4) should contain theSameElementsAs Seq(
|
||||
TestMessages.pending(contextId, exprOperator1, exprOperator2),
|
||||
TestMessages
|
||||
.update(
|
||||
contextId,
|
||||
exprOperator1,
|
||||
ConstantsGen.INTEGER,
|
||||
typeChanged = false
|
||||
),
|
||||
TestMessages
|
||||
.update(
|
||||
contextId,
|
||||
exprOperator2,
|
||||
ConstantsGen.INTEGER,
|
||||
typeChanged = false,
|
||||
methodCall = Some(
|
||||
Api.MethodCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "function2")
|
||||
)
|
||||
)
|
||||
),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("43")
|
||||
}
|
||||
|
||||
it should "fail with ExpressionNotFound when renaming non-existent symbol" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
|
||||
val metadata = new Metadata
|
||||
val code =
|
||||
"""from Standard.Base import all
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = operator1 + 1
|
||||
| IO.println operator2
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// open file
|
||||
context.send(
|
||||
Api.Request(Api.OpenFileNotification(mainFile, contents))
|
||||
)
|
||||
context.receiveNone shouldEqual None
|
||||
|
||||
// push main
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(
|
||||
contextId,
|
||||
Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
|
||||
// rename operator1
|
||||
val randomExpressionId = UUID.randomUUID()
|
||||
val newName = "foobarbaz"
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.RenameSymbol(moduleName, randomExpressionId, newName)
|
||||
)
|
||||
)
|
||||
context.receiveNIgnoreStdLib(1) should contain theSameElementsAs Seq(
|
||||
Api.Response(
|
||||
requestId,
|
||||
Api.SymbolRenameFailed(
|
||||
Api.SymbolRenameFailed.ExpressionNotFound(randomExpressionId)
|
||||
)
|
||||
)
|
||||
)
|
||||
context.consumeOut shouldEqual List()
|
||||
}
|
||||
|
||||
it should "fail with OperationNotSupported when renaming an expression" in {
|
||||
val contextId = UUID.randomUUID()
|
||||
val requestId = UUID.randomUUID()
|
||||
val moduleName = "Enso_Test.Test.Main"
|
||||
|
||||
val metadata = new Metadata
|
||||
val exprOperator1 = metadata.addItem(54, 2)
|
||||
val code =
|
||||
"""from Standard.Base import all
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = operator1 + 1
|
||||
| IO.println operator2
|
||||
|""".stripMargin.linesIterator.mkString("\n")
|
||||
val contents = metadata.appendToCode(code)
|
||||
val mainFile = context.writeMain(contents)
|
||||
|
||||
// create context
|
||||
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
|
||||
context.receive shouldEqual Some(
|
||||
Api.Response(requestId, Api.CreateContextResponse(contextId))
|
||||
)
|
||||
|
||||
// open file
|
||||
context.send(
|
||||
Api.Request(Api.OpenFileNotification(mainFile, contents))
|
||||
)
|
||||
context.receiveNone shouldEqual None
|
||||
|
||||
// push main
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.PushContextRequest(
|
||||
contextId,
|
||||
Api.StackItem.ExplicitCall(
|
||||
Api.MethodPointer(moduleName, moduleName, "main"),
|
||||
None,
|
||||
Vector()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
context.receiveNIgnoreStdLib(4) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
TestMessages.update(contextId, exprOperator1, ConstantsGen.INTEGER),
|
||||
context.executionComplete(contextId)
|
||||
)
|
||||
context.consumeOut shouldEqual List("42")
|
||||
|
||||
// rename operator1
|
||||
val newName = "foobarbaz"
|
||||
context.send(
|
||||
Api.Request(
|
||||
requestId,
|
||||
Api.RenameSymbol(moduleName, exprOperator1, newName)
|
||||
)
|
||||
)
|
||||
context.receiveNIgnoreStdLib(1) should contain theSameElementsAs Seq(
|
||||
Api.Response(
|
||||
requestId,
|
||||
Api.SymbolRenameFailed(
|
||||
Api.SymbolRenameFailed.OperationNotSupported(exprOperator1)
|
||||
)
|
||||
)
|
||||
)
|
||||
context.consumeOut shouldEqual List()
|
||||
}
|
||||
}
|
@ -4256,7 +4256,7 @@ class RuntimeServerTest
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receiveNIgnoreStdLib(4) should contain theSameElementsAs Seq(
|
||||
context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
Api.Response(
|
||||
@ -4264,8 +4264,7 @@ class RuntimeServerTest
|
||||
contextId,
|
||||
Api.ExecutionResult.Failure("Module Unnamed.Main not found.", None)
|
||||
)
|
||||
),
|
||||
Api.Response(Api.BackgroundJobsStartedNotification())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -4305,7 +4304,7 @@ class RuntimeServerTest
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receiveNIgnoreStdLib(4) should contain theSameElementsAs Seq(
|
||||
context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
Api.Response(
|
||||
@ -4316,8 +4315,7 @@ class RuntimeServerTest
|
||||
Some(mainFile)
|
||||
)
|
||||
)
|
||||
),
|
||||
Api.Response(Api.BackgroundJobsStartedNotification())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -4357,7 +4355,7 @@ class RuntimeServerTest
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receiveNIgnoreStdLib(4) should contain theSameElementsAs Seq(
|
||||
context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
Api.Response(
|
||||
@ -4368,8 +4366,7 @@ class RuntimeServerTest
|
||||
Some(mainFile)
|
||||
)
|
||||
)
|
||||
),
|
||||
Api.Response(Api.BackgroundJobsStartedNotification())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -4479,7 +4476,7 @@ class RuntimeServerTest
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receiveNIgnoreStdLib(4) should contain theSameElementsAs Seq(
|
||||
context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
Api.Response(
|
||||
@ -4502,8 +4499,7 @@ class RuntimeServerTest
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Api.Response(Api.BackgroundJobsStartedNotification())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -4621,7 +4617,7 @@ class RuntimeServerTest
|
||||
)
|
||||
)
|
||||
)
|
||||
context.receiveNIgnoreStdLib(4) should contain theSameElementsAs Seq(
|
||||
context.receiveNIgnoreStdLib(3) should contain theSameElementsAs Seq(
|
||||
Api.Response(Api.BackgroundJobsStartedNotification()),
|
||||
Api.Response(requestId, Api.PushContextResponse(contextId)),
|
||||
Api.Response(
|
||||
@ -4652,8 +4648,7 @@ class RuntimeServerTest
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Api.Response(Api.BackgroundJobsStartedNotification())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,136 @@
|
||||
package org.enso.compiler.refactoring
|
||||
|
||||
import org.enso.compiler.core.IR
|
||||
import org.enso.compiler.data.BindingsMap
|
||||
import org.enso.compiler.pass.analyse.DataflowAnalysis
|
||||
import org.enso.compiler.pass.resolve.MethodCalls
|
||||
import org.enso.pkg.QualifiedName
|
||||
|
||||
trait IRUtils {
|
||||
|
||||
/** Find the node by external id.
|
||||
*
|
||||
* @param ir the syntax tree
|
||||
* @param externalId the external id to look for
|
||||
* @return the first node with the given external id in `ir`
|
||||
*/
|
||||
def findByExternalId(ir: IR, externalId: IR.ExternalId): Option[IR] = {
|
||||
ir.preorder.find(_.getExternalId.contains(externalId))
|
||||
}
|
||||
|
||||
/** Find usages of a local defined in the body block.
|
||||
*
|
||||
* @param ir the syntax tree
|
||||
* @param literal the literal name of the local
|
||||
* @return the list of usages of the given literal in the `ir`
|
||||
*/
|
||||
def findLocalUsages(
|
||||
ir: IR,
|
||||
literal: IR.Name.Literal
|
||||
): Option[Set[IR.Name.Literal]] = {
|
||||
for {
|
||||
usages <- findStaticUsages(ir, literal)
|
||||
} yield {
|
||||
usages.collect {
|
||||
case usage: IR.Name.Literal if usage.name == literal.name => usage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Find usages of a method defined on module.
|
||||
*
|
||||
* @param moduleName the qualified module name
|
||||
* @param ir the syntax tree
|
||||
* @param node the name of the method
|
||||
* @return the list of usages of the given method in the `ir`
|
||||
*/
|
||||
def findModuleMethodUsages(
|
||||
moduleName: QualifiedName,
|
||||
ir: IR,
|
||||
node: IR.Name
|
||||
): Option[Set[IR.Name.Literal]] =
|
||||
for {
|
||||
usages <- findDynamicUsages(ir, node)
|
||||
} yield {
|
||||
usages
|
||||
.collect {
|
||||
case usage: IR.Name.Literal
|
||||
if usage.isMethod && usage.name == node.name =>
|
||||
usage
|
||||
}
|
||||
.flatMap { symbol =>
|
||||
symbol.getMetadata(MethodCalls).flatMap { resolution =>
|
||||
resolution.target match {
|
||||
case BindingsMap.ResolvedMethod(module, _)
|
||||
if module.getName == moduleName =>
|
||||
Some(symbol)
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Find usages of a static dependency in the [[DataflowAnalysis]] metadata.
|
||||
*
|
||||
* @param ir the syntax tree
|
||||
* @param literal the name to look for
|
||||
* @return the list of usages of the given name in the `ir`
|
||||
*/
|
||||
private def findStaticUsages(
|
||||
ir: IR,
|
||||
literal: IR.Name.Literal
|
||||
): Option[Set[IR]] = {
|
||||
for {
|
||||
metadata <- ir.getMetadata(DataflowAnalysis)
|
||||
key = DataflowAnalysis.DependencyInfo.Type
|
||||
.Static(literal.getId, literal.getExternalId)
|
||||
dependents <- metadata.dependents.get(key)
|
||||
} yield {
|
||||
dependents
|
||||
.flatMap {
|
||||
case _: DataflowAnalysis.DependencyInfo.Type.Dynamic =>
|
||||
None
|
||||
case DataflowAnalysis.DependencyInfo.Type.Static(id, _) =>
|
||||
findById(ir, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Find usages of a dynamic dependency in the [[DataflowAnalysis]] metadata.
|
||||
*
|
||||
* @param ir the syntax tree
|
||||
* @param node the name to look for
|
||||
* @return the list of usages of the given name in the `ir`
|
||||
*/
|
||||
private def findDynamicUsages(
|
||||
ir: IR,
|
||||
node: IR.Name
|
||||
): Option[Set[IR]] = {
|
||||
for {
|
||||
metadata <- ir.getMetadata(DataflowAnalysis)
|
||||
key = DataflowAnalysis.DependencyInfo.Type.Dynamic(node.name, None)
|
||||
dependents <- metadata.dependents.get(key)
|
||||
} yield {
|
||||
dependents
|
||||
.flatMap {
|
||||
case _: DataflowAnalysis.DependencyInfo.Type.Dynamic =>
|
||||
None
|
||||
case DataflowAnalysis.DependencyInfo.Type.Static(id, _) =>
|
||||
findById(ir, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Find node by id.
|
||||
*
|
||||
* @param ir the syntax tree
|
||||
* @param id the identifier to look for
|
||||
* @return the `ir` node with the given identifier
|
||||
*/
|
||||
private def findById(ir: IR, id: IR.Identifier): Option[IR] = {
|
||||
ir.preorder.find(_.getId == id)
|
||||
}
|
||||
}
|
||||
|
||||
object IRUtils extends IRUtils
|
@ -0,0 +1,264 @@
|
||||
package org.enso.compiler.refactoring
|
||||
|
||||
import org.enso.compiler.core.IR
|
||||
import org.enso.interpreter.runtime
|
||||
import org.enso.interpreter.runtime.EnsoContext
|
||||
import org.enso.interpreter.test.InterpreterContext
|
||||
import org.enso.pkg.QualifiedName
|
||||
import org.enso.polyglot.{LanguageInfo, MethodNames}
|
||||
import org.scalatest.OptionValues
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
class IRUtilsTest extends AnyWordSpecLike with Matchers with OptionValues {
|
||||
private val ctx = new InterpreterContext()
|
||||
private val langCtx = ctx.ctx
|
||||
.getBindings(LanguageInfo.ID)
|
||||
.invokeMember(MethodNames.TopScope.LEAK_CONTEXT)
|
||||
.asHostObject[EnsoContext]()
|
||||
|
||||
implicit private class PreprocessModule(code: String) {
|
||||
|
||||
private val Module = QualifiedName(List("Unnamed"), "Test")
|
||||
|
||||
def preprocessModule(name: QualifiedName): IR.Module = {
|
||||
val module = new runtime.Module(
|
||||
name,
|
||||
null,
|
||||
code.stripMargin.linesIterator.mkString("\n")
|
||||
)
|
||||
langCtx.getCompiler.run(module)
|
||||
module.getIr
|
||||
}
|
||||
|
||||
def preprocessModule: IR.Module =
|
||||
preprocessModule(Module)
|
||||
|
||||
}
|
||||
|
||||
private def findUsagesOfLiteral(
|
||||
module: IR.Module,
|
||||
ir: IR
|
||||
): Option[Set[IR.Name.Literal]] = {
|
||||
ir match {
|
||||
case literal: IR.Name.Literal =>
|
||||
IRUtils.findLocalUsages(module, literal)
|
||||
case _ =>
|
||||
fail(s"Trying to find literal usages of [${ir.getClass}]: [$ir]")
|
||||
}
|
||||
}
|
||||
|
||||
private def findUsagesOfStaticMethod(
|
||||
moduleName: QualifiedName,
|
||||
module: IR.Module,
|
||||
ir: IR
|
||||
): Option[Set[IR.Name.Literal]] = {
|
||||
ir match {
|
||||
case methodRef: IR.Name.MethodReference
|
||||
if methodRef.typePointer.isEmpty =>
|
||||
IRUtils.findModuleMethodUsages(
|
||||
moduleName,
|
||||
module,
|
||||
methodRef.methodName
|
||||
)
|
||||
case _ =>
|
||||
fail(s"Trying to find method usages of [${ir.getClass}]: [$ir]")
|
||||
}
|
||||
}
|
||||
|
||||
"IRUtils" should {
|
||||
|
||||
"find usages of a literal in expression" in {
|
||||
val uuid1 = new UUID(0, 1)
|
||||
val code =
|
||||
s"""main =
|
||||
| operator1 = 41
|
||||
| operator2 = operator1 + 1
|
||||
| operator2
|
||||
|
|
||||
|
|
||||
|#### METADATA ####
|
||||
|[[{"index": {"value": 11}, "size": {"value": 9}}, "$uuid1"]]
|
||||
|[]
|
||||
|""".stripMargin
|
||||
|
||||
val module = code.preprocessModule
|
||||
val operator1 = IRUtils.findByExternalId(module, uuid1).get
|
||||
val usages = findUsagesOfLiteral(module, operator1)
|
||||
|
||||
usages.value.size shouldEqual 1
|
||||
usages.value.foreach {
|
||||
case _: IR.Name.Literal => succeed
|
||||
case ir => fail(s"Not a literal: $ir")
|
||||
}
|
||||
}
|
||||
|
||||
"find usages of a literal in a complex expression" in {
|
||||
val uuid1 = new UUID(0, 1)
|
||||
val code =
|
||||
s"""main =
|
||||
| operator1 = 41
|
||||
| operator2 = operator1 + operator1 + 1
|
||||
| operator2
|
||||
|
|
||||
|
|
||||
|#### METADATA ####
|
||||
|[[{"index": {"value": 11}, "size": {"value": 9}}, "$uuid1"]]
|
||||
|[]
|
||||
|""".stripMargin
|
||||
|
||||
val module = code.preprocessModule
|
||||
val operator1 = IRUtils.findByExternalId(module, uuid1).get
|
||||
val usages = findUsagesOfLiteral(module, operator1)
|
||||
|
||||
usages.value.size shouldEqual 2
|
||||
usages.value.foreach {
|
||||
case _: IR.Name.Literal => succeed
|
||||
case ir => fail(s"Not a literal: $ir")
|
||||
}
|
||||
}
|
||||
|
||||
"find usages of a literal in a lambda" in {
|
||||
val uuid1 = new UUID(0, 1)
|
||||
val code =
|
||||
s"""main =
|
||||
| operator1 = 41
|
||||
| operator2 = "".map (x -> x + operator1)
|
||||
| operator2
|
||||
|
|
||||
|
|
||||
|#### METADATA ####
|
||||
|[[{"index": {"value": 11}, "size": {"value": 9}}, "$uuid1"]]
|
||||
|[]
|
||||
|""".stripMargin
|
||||
|
||||
val module = code.preprocessModule
|
||||
val operator1 = IRUtils.findByExternalId(module, uuid1).get
|
||||
val usages = findUsagesOfLiteral(module, operator1)
|
||||
|
||||
usages.value.size shouldEqual 1
|
||||
usages.value.foreach {
|
||||
case _: IR.Name.Literal => succeed
|
||||
case ir => fail(s"Not a literal: $ir")
|
||||
}
|
||||
}
|
||||
|
||||
"find usages of a static method call in main body" in {
|
||||
val uuid1 = new UUID(0, 1)
|
||||
val moduleName = QualifiedName(List("Unnamed"), "Test")
|
||||
val code =
|
||||
s"""function1 x = x + 1
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = Test.function1 operator1
|
||||
| operator2
|
||||
|
|
||||
|
|
||||
|#### METADATA ####
|
||||
|[[{"index": {"value": 0}, "size": {"value": 9}}, "$uuid1"]]
|
||||
|[]
|
||||
|""".stripMargin
|
||||
|
||||
val module = code.preprocessModule(moduleName)
|
||||
val operator1 = IRUtils.findByExternalId(module, uuid1).get
|
||||
val usages = findUsagesOfStaticMethod(moduleName, module, operator1)
|
||||
|
||||
usages.value.size shouldEqual 1
|
||||
usages.value.foreach {
|
||||
case _: IR.Name.Literal => succeed
|
||||
case ir => fail(s"Not a literal: $ir")
|
||||
}
|
||||
}
|
||||
|
||||
"find usages of a static call in lambda" in {
|
||||
val uuid1 = new UUID(0, 1)
|
||||
val moduleName = QualifiedName(List("Unnamed"), "Test")
|
||||
val code =
|
||||
s"""function1 x = x
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = Test.function1 (x -> Test.function1 x)
|
||||
| operator2
|
||||
|
|
||||
|
|
||||
|#### METADATA ####
|
||||
|[[{"index": {"value": 0}, "size": {"value": 9}}, "$uuid1"]]
|
||||
|[]
|
||||
|""".stripMargin
|
||||
|
||||
val module = code.preprocessModule(moduleName)
|
||||
val operator1 = IRUtils.findByExternalId(module, uuid1).get
|
||||
val usages = findUsagesOfStaticMethod(moduleName, module, operator1)
|
||||
|
||||
usages.value.size shouldEqual 2
|
||||
usages.value.foreach {
|
||||
case _: IR.Name.Literal => succeed
|
||||
case ir => fail(s"Not a literal: $ir")
|
||||
}
|
||||
}
|
||||
|
||||
"find usages of a static method call in presence of an instance method" in {
|
||||
val uuid1 = new UUID(0, 1)
|
||||
val moduleName = QualifiedName(List("Unnamed"), "Test")
|
||||
val code =
|
||||
s"""function1 x = x
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = Test.function1 operator1
|
||||
| operator3 = operator2.function1
|
||||
|
|
||||
|
|
||||
|#### METADATA ####
|
||||
|[[{"index": {"value": 0}, "size": {"value": 9}}, "$uuid1"]]
|
||||
|[]
|
||||
|""".stripMargin
|
||||
|
||||
val module = code.preprocessModule(moduleName)
|
||||
val operator1 = IRUtils.findByExternalId(module, uuid1).get
|
||||
val usages = findUsagesOfStaticMethod(moduleName, module, operator1)
|
||||
|
||||
usages.value.size shouldEqual 1
|
||||
usages.value.foreach {
|
||||
case _: IR.Name.Literal => succeed
|
||||
case ir => fail(s"Not a literal: $ir")
|
||||
}
|
||||
}
|
||||
|
||||
"find usages of a static method call in presence of a type method" in {
|
||||
val uuid1 = new UUID(0, 1)
|
||||
val moduleName = QualifiedName(List("Unnamed"), "Test")
|
||||
val code =
|
||||
s"""function1 x = x
|
||||
|
|
||||
|type A
|
||||
| function1 x = x
|
||||
|
|
||||
|main =
|
||||
| operator1 = 41
|
||||
| operator2 = Test.function1 operator1
|
||||
| operator3 = A.function1
|
||||
|
|
||||
|
|
||||
|#### METADATA ####
|
||||
|[[{"index": {"value": 0}, "size": {"value": 9}}, "$uuid1"]]
|
||||
|[]
|
||||
|""".stripMargin
|
||||
|
||||
val module = code.preprocessModule(moduleName)
|
||||
val operator1 = IRUtils.findByExternalId(module, uuid1).get
|
||||
val usages = findUsagesOfStaticMethod(moduleName, module, operator1)
|
||||
|
||||
usages.value.size shouldEqual 1
|
||||
usages.value.foreach {
|
||||
case _: IR.Name.Literal => succeed
|
||||
case ir => fail(s"Not a literal: $ir")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -54,10 +54,6 @@ class SuggestionBuilderTest extends AnyWordSpecLike with Matchers {
|
||||
Vector()
|
||||
)
|
||||
|
||||
@annotation.nowarn
|
||||
def endOfLine(line: Int, character: Int): Suggestion.Position =
|
||||
Suggestion.Position(line + 1, 0)
|
||||
|
||||
"SuggestionBuilder" should {
|
||||
|
||||
"build method without explicit arguments" in {
|
||||
|
@ -0,0 +1,103 @@
|
||||
package org.enso.refactoring.validation;
|
||||
|
||||
public final class MethodNameValidation {
|
||||
|
||||
public static final String DEFAULT_NAME = "operator";
|
||||
|
||||
private static final char CHAR_UNDERSCORE = '_';
|
||||
private static final char CHAR_LOWERCASE_A = 'a';
|
||||
private static final char CHAR_LOWERCASE_Z = 'z';
|
||||
private static final char CHAR_UPPERCASE_A = 'A';
|
||||
private static final char CHAR_UPPERCASE_Z = 'Z';
|
||||
|
||||
/**
|
||||
* Normalize the name to make it a valid identifier of a method.
|
||||
*
|
||||
* @param name the name to normalize.
|
||||
* @return the normalized name.
|
||||
*/
|
||||
public static String normalize(String name) {
|
||||
if (name.isEmpty()) {
|
||||
return DEFAULT_NAME;
|
||||
}
|
||||
if (isAllowedFirstCharacter(Character.toLowerCase(name.charAt(0)))) {
|
||||
return toLowerSnakeCase(name);
|
||||
}
|
||||
return toLowerSnakeCase(DEFAULT_NAME + "_" + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if the provided name is a valid identifier of a method and {@code false}
|
||||
* otherwise.
|
||||
*/
|
||||
public static boolean isAllowedName(String name) {
|
||||
return !name.isEmpty()
|
||||
&& isAllowedFirstCharacter(name.charAt(0))
|
||||
&& name.chars().allMatch(MethodNameValidation::isAllowedNameCharacter);
|
||||
}
|
||||
|
||||
private static String toLowerSnakeCase(String name) {
|
||||
if (name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder(name.length());
|
||||
char[] chars = name.toCharArray();
|
||||
char previous = name.charAt(0);
|
||||
for (int i = 0; i < chars.length; i++) {
|
||||
char current = name.charAt(i);
|
||||
|
||||
if (current == CHAR_UNDERSCORE && previous == CHAR_UNDERSCORE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isLetterAscii(current) || Character.isDigit(current) || current == CHAR_UNDERSCORE) {
|
||||
if (Character.isUpperCase(current)
|
||||
&& (Character.isLowerCase(previous) || Character.isDigit(previous))) {
|
||||
result.append(CHAR_UNDERSCORE);
|
||||
}
|
||||
if (Character.isLowerCase(current) && Character.isDigit(previous)) {
|
||||
result.append(CHAR_UNDERSCORE);
|
||||
}
|
||||
result.append(Character.toLowerCase(current));
|
||||
previous = current;
|
||||
}
|
||||
|
||||
if (Character.isWhitespace(current) && previous != CHAR_UNDERSCORE) {
|
||||
result.append(CHAR_UNDERSCORE);
|
||||
previous = CHAR_UNDERSCORE;
|
||||
}
|
||||
}
|
||||
|
||||
char lastChar = result.charAt(result.length() - 1);
|
||||
if (lastChar == CHAR_UNDERSCORE) {
|
||||
result.setLength(result.length() - 1);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static boolean isAllowedFirstCharacter(int c) {
|
||||
return isLowerCaseAscii(c);
|
||||
}
|
||||
|
||||
private static boolean isAllowedNameCharacter(int c) {
|
||||
return isAlphanumericAscii(c) || c == CHAR_UNDERSCORE;
|
||||
}
|
||||
|
||||
private static boolean isAlphanumericAscii(int c) {
|
||||
return isLowerCaseAscii(c) || Character.isDigit(c);
|
||||
}
|
||||
|
||||
private static boolean isLetterAscii(int c) {
|
||||
return isLowerCaseAscii(c) || isUpperCaseAscii(c);
|
||||
}
|
||||
|
||||
private static boolean isLowerCaseAscii(int c) {
|
||||
return c >= CHAR_LOWERCASE_A && c <= CHAR_LOWERCASE_Z;
|
||||
}
|
||||
|
||||
private static boolean isUpperCaseAscii(int c) {
|
||||
return c >= CHAR_UPPERCASE_A && c <= CHAR_UPPERCASE_Z;
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package org.enso.refactoring
|
||||
|
||||
import org.enso.syntax.text.Location
|
||||
import org.enso.text.editing.model
|
||||
import org.enso.text.editing.{IndexedSource, TextEditor}
|
||||
import org.enso.text.editing.model.TextEdit
|
||||
|
||||
object RenameUtils {
|
||||
|
||||
/** Create a list of edits that should be made to rename the provided
|
||||
* occurrences of text.
|
||||
*
|
||||
* @param source the original source
|
||||
* @param occurrences the occurrences in the source that should be replaced
|
||||
* @param newText the text to replace in the provided locations
|
||||
* @return a list of text edits that should be applied to the original source
|
||||
* in order to replace the provided text occurrences with the new text.
|
||||
*/
|
||||
def buildEdits[A: IndexedSource: TextEditor](
|
||||
source: A,
|
||||
occurrences: Seq[Location],
|
||||
newText: String
|
||||
): Seq[TextEdit] = {
|
||||
val (_, _, builder) = occurrences
|
||||
.sortBy(_.start)
|
||||
.foldLeft((0, source, Vector.newBuilder[TextEdit])) {
|
||||
case ((offset, source, builder), location) =>
|
||||
val start =
|
||||
implicitly[IndexedSource[A]]
|
||||
.toPosition(location.start + offset, source)
|
||||
val end =
|
||||
implicitly[IndexedSource[A]]
|
||||
.toPosition(location.end + offset, source)
|
||||
val range = model.Range(start, end)
|
||||
|
||||
val newOffset = offset - location.length + newText.length
|
||||
val textEdit = TextEdit(range, newText)
|
||||
val newSource = implicitly[TextEditor[A]].edit(source, textEdit)
|
||||
(newOffset, newSource, builder += textEdit)
|
||||
}
|
||||
|
||||
builder.result()
|
||||
}
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
package org.enso.refactoring;
|
||||
|
||||
import org.enso.syntax.text.Location;
|
||||
import org.enso.text.buffer.Rope;
|
||||
import org.enso.text.editing.IndexedSource;
|
||||
import org.enso.text.editing.IndexedSource$;
|
||||
import org.enso.text.editing.JavaEditorAdapter;
|
||||
import org.enso.text.editing.RopeTextEditor$;
|
||||
import org.enso.text.editing.TextEditValidationFailure;
|
||||
import org.enso.text.editing.TextEditor;
|
||||
import org.enso.text.editing.model;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import scala.collection.immutable.Seq;
|
||||
import scala.collection.immutable.VectorBuilder;
|
||||
import scala.util.Either;
|
||||
|
||||
public class RenameUtilsTest {
|
||||
|
||||
private final IndexedSource<Rope> indexedSource;
|
||||
private final TextEditor<Rope> textEditor;
|
||||
|
||||
public RenameUtilsTest() {
|
||||
indexedSource = IndexedSource$.MODULE$.RopeIndexedSource();
|
||||
textEditor = RopeTextEditor$.MODULE$;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildEditsSingleLine() {
|
||||
Rope source = Rope.apply("foo a foo baz");
|
||||
|
||||
String newName = "quux";
|
||||
Seq<Location> occurrences = vec(new Location(0, 3), new Location(6, 9));
|
||||
Seq<model.TextEdit> edits =
|
||||
RenameUtils$.MODULE$.buildEdits(source, occurrences, newName, indexedSource, textEditor);
|
||||
|
||||
Assert.assertEquals(2, edits.length());
|
||||
|
||||
Either<TextEditValidationFailure, Rope> result = JavaEditorAdapter.applyEdits(source, edits);
|
||||
Assert.assertTrue(result.isRight());
|
||||
|
||||
Rope expected = Rope.apply("quux a quux baz");
|
||||
Assert.assertEquals(expected.toString(), result.toOption().get().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildEditsMultiLine() {
|
||||
Rope source = Rope.apply("foo a\nbaz foo");
|
||||
|
||||
String newName = "quux";
|
||||
Seq<Location> occurrences = vec(new Location(0, 3), new Location(10, 13));
|
||||
Seq<model.TextEdit> edits =
|
||||
RenameUtils$.MODULE$.buildEdits(source, occurrences, newName, indexedSource, textEditor);
|
||||
|
||||
Assert.assertEquals(2, edits.length());
|
||||
|
||||
Either<TextEditValidationFailure, Rope> result = JavaEditorAdapter.applyEdits(source, edits);
|
||||
Assert.assertTrue(result.isRight());
|
||||
|
||||
Rope expected = Rope.apply("quux a\nbaz quux");
|
||||
Assert.assertEquals(expected.toString(), result.toOption().get().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildEditsUnordered() {
|
||||
Rope source = Rope.apply("foo bar foo baz");
|
||||
|
||||
String newName = "quux";
|
||||
Seq<Location> occurrences = vec(new Location(8, 11), new Location(0, 3));
|
||||
Seq<model.TextEdit> edits =
|
||||
RenameUtils$.MODULE$.buildEdits(source, occurrences, newName, indexedSource, textEditor);
|
||||
|
||||
Assert.assertEquals(2, edits.length());
|
||||
|
||||
Either<TextEditValidationFailure, Rope> result = JavaEditorAdapter.applyEdits(source, edits);
|
||||
Assert.assertTrue(result.isRight());
|
||||
|
||||
Rope expected = Rope.apply("quux bar quux baz");
|
||||
Assert.assertEquals(expected.toString(), result.toOption().get().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildEditsUnorderedMultiLine() {
|
||||
Rope source = Rope.apply("foo a\nbaz foo");
|
||||
|
||||
String newName = "quux";
|
||||
Seq<Location> occurrences = vec(new Location(10, 13), new Location(0, 3));
|
||||
Seq<model.TextEdit> edits =
|
||||
RenameUtils$.MODULE$.buildEdits(source, occurrences, newName, indexedSource, textEditor);
|
||||
|
||||
Assert.assertEquals(2, edits.length());
|
||||
|
||||
Either<TextEditValidationFailure, Rope> result = JavaEditorAdapter.applyEdits(source, edits);
|
||||
Assert.assertTrue(result.isRight());
|
||||
|
||||
Rope expected = Rope.apply("quux a\nbaz quux");
|
||||
Assert.assertEquals(expected.toString(), result.toOption().get().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildEditsEmptyOccurrences() {
|
||||
Rope source = Rope.apply("foo bar foo baz");
|
||||
|
||||
String newName = "quux";
|
||||
Seq<Location> occurrences = vec();
|
||||
Seq<model.TextEdit> edits =
|
||||
RenameUtils$.MODULE$.buildEdits(source, occurrences, newName, indexedSource, textEditor);
|
||||
|
||||
Assert.assertEquals(0, edits.length());
|
||||
|
||||
Either<TextEditValidationFailure, Rope> result = JavaEditorAdapter.applyEdits(source, edits);
|
||||
Assert.assertTrue(result.isRight());
|
||||
Assert.assertEquals(source.toString(), result.toOption().get().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildEditsRemovingOccurrences() {
|
||||
Rope source = Rope.apply("foo bar foo baz");
|
||||
|
||||
String newName = "";
|
||||
Seq<Location> occurrences = vec(new Location(0, 3), new Location(8, 11));
|
||||
Seq<model.TextEdit> edits =
|
||||
RenameUtils$.MODULE$.buildEdits(source, occurrences, newName, indexedSource, textEditor);
|
||||
|
||||
Assert.assertEquals(2, edits.length());
|
||||
|
||||
Either<TextEditValidationFailure, Rope> result = JavaEditorAdapter.applyEdits(source, edits);
|
||||
Assert.assertTrue(result.isRight());
|
||||
|
||||
Rope expected = Rope.apply(" bar baz");
|
||||
Assert.assertEquals(expected.toString(), result.toOption().get().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildEditsRemovingOccurrencesMultiline() {
|
||||
Rope source = Rope.apply("foo xs\nfoo baz");
|
||||
|
||||
String newName = "";
|
||||
Seq<Location> occurrences = vec(new Location(0, 3), new Location(7, 10));
|
||||
Seq<model.TextEdit> edits =
|
||||
RenameUtils$.MODULE$.buildEdits(source, occurrences, newName, indexedSource, textEditor);
|
||||
|
||||
Assert.assertEquals(2, edits.length());
|
||||
|
||||
Either<TextEditValidationFailure, Rope> result = JavaEditorAdapter.applyEdits(source, edits);
|
||||
Assert.assertTrue(result.isRight());
|
||||
|
||||
Rope expected = Rope.apply(" xs\n baz");
|
||||
Assert.assertEquals(expected.toString(), result.toOption().get().toString());
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static <A> Seq<A> vec(A... elems) {
|
||||
VectorBuilder<A> builder = new VectorBuilder<>();
|
||||
|
||||
for (A elem : elems) {
|
||||
builder.addOne(elem);
|
||||
}
|
||||
|
||||
return builder.result();
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package org.enso.refactoring.validation;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class MethodNameValidationTest {
|
||||
|
||||
|
||||
public MethodNameValidationTest() {}
|
||||
|
||||
@Test
|
||||
public void isAllowedName() {
|
||||
Assert.assertFalse(MethodNameValidation.isAllowedName(""));
|
||||
Assert.assertFalse(MethodNameValidation.isAllowedName("@#$"));
|
||||
Assert.assertFalse(MethodNameValidation.isAllowedName("_foo"));
|
||||
Assert.assertFalse(MethodNameValidation.isAllowedName("42"));
|
||||
Assert.assertFalse(MethodNameValidation.isAllowedName("42_foo"));
|
||||
Assert.assertFalse(MethodNameValidation.isAllowedName("Foo"));
|
||||
Assert.assertFalse(MethodNameValidation.isAllowedName("foo_Bar"));
|
||||
Assert.assertFalse(MethodNameValidation.isAllowedName("foo bar"));
|
||||
|
||||
Assert.assertTrue(MethodNameValidation.isAllowedName("foo"));
|
||||
Assert.assertTrue(MethodNameValidation.isAllowedName("foo_bar"));
|
||||
Assert.assertTrue(MethodNameValidation.isAllowedName("foo_42"));
|
||||
Assert.assertTrue(MethodNameValidation.isAllowedName("foo42"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void normalizeName() {
|
||||
Assert.assertEquals("foo", MethodNameValidation.normalize("FOO"));
|
||||
Assert.assertEquals("foo_bar", MethodNameValidation.normalize("FOO_BAR"));
|
||||
Assert.assertEquals("foo42_bar", MethodNameValidation.normalize("FOO42BAR"));
|
||||
Assert.assertEquals("foo_bar", MethodNameValidation.normalize("FOO bar"));
|
||||
Assert.assertEquals("f_oo_bar", MethodNameValidation.normalize("fOoBar"));
|
||||
Assert.assertEquals("foo_42", MethodNameValidation.normalize("foo_42"));
|
||||
Assert.assertEquals("foo_bar", MethodNameValidation.normalize("foo_bar"));
|
||||
Assert.assertEquals("foo_bar", MethodNameValidation.normalize("foo__bar"));
|
||||
Assert.assertEquals("foo_bar", MethodNameValidation.normalize("foo$_$bar"));
|
||||
Assert.assertEquals(MethodNameValidation.DEFAULT_NAME, MethodNameValidation.normalize("!$%"));
|
||||
Assert.assertEquals(MethodNameValidation.DEFAULT_NAME, MethodNameValidation.normalize("!_%"));
|
||||
Assert.assertEquals(MethodNameValidation.DEFAULT_NAME + "_foo", MethodNameValidation.normalize("_foo"));
|
||||
Assert.assertEquals(MethodNameValidation.DEFAULT_NAME + "_foo", MethodNameValidation.normalize("__foo"));
|
||||
Assert.assertEquals(MethodNameValidation.DEFAULT_NAME + "_foo", MethodNameValidation.normalize("__foo__"));
|
||||
Assert.assertEquals(MethodNameValidation.DEFAULT_NAME + "_foo", MethodNameValidation.normalize(" foo "));
|
||||
Assert.assertEquals("foo_bar", MethodNameValidation.normalize("foo bar"));
|
||||
Assert.assertEquals("foo42", MethodNameValidation.normalize("foo42"));
|
||||
Assert.assertEquals("foo42_bar", MethodNameValidation.normalize("foo42bar"));
|
||||
Assert.assertEquals("foo_42_bar", MethodNameValidation.normalize("foo$ 42$bar"));
|
||||
Assert.assertEquals("foo_bar", MethodNameValidation.normalize("fooBar"));
|
||||
Assert.assertEquals("foo_bar", MethodNameValidation.normalize("FooBar"));
|
||||
Assert.assertEquals("foo42_bar", MethodNameValidation.normalize("Foo42Bar"));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user