Module file operations through execution server (#660)

This commit is contained in:
Marcin Kostrzewa 2020-04-20 14:33:51 +02:00 committed by GitHub
parent 75f25b66db
commit e2d901fb68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 536 additions and 164 deletions

View File

@ -99,7 +99,8 @@ lazy val enso = (project in file("."))
`project-manager`,
graph,
runner,
`language-server`
`language-server`,
`text-buffer`
)
.settings(Global / concurrentRestrictions += Tags.exclusive(Exclusive))
@ -271,6 +272,17 @@ lazy val `parser-service` = (project in file("lib/parser-service"))
mainClass := Some("org.enso.ParserServiceMain")
)
lazy val `text-buffer` = project
.in(file("lib/text-buffer"))
.configs(Test)
.settings(
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % catsVersion,
"org.scalatest" %% "scalatest" % "3.2.0-M2" % Test,
"org.scalacheck" %% "scalacheck" % "1.14.0" % Test
)
)
lazy val graph = (project in file("lib/graph/"))
.dependsOn(logger.jvm)
.configs(Test)
@ -478,6 +490,7 @@ lazy val `polyglot-api` = project
)
)
.dependsOn(pkg)
.dependsOn(`text-buffer`)
lazy val `language-server` = (project in file("engine/language-server"))
.settings(
@ -513,6 +526,7 @@ lazy val `language-server` = (project in file("engine/language-server"))
.dependsOn(`polyglot-api`)
.dependsOn(`json-rpc-server`)
.dependsOn(`json-rpc-server-test` % Test)
.dependsOn(`text-buffer`)
lazy val runtime = (project in file("engine/runtime"))
.configs(Benchmark)
@ -589,6 +603,7 @@ lazy val runtime = (project in file("engine/runtime"))
.dependsOn(syntax.jvm)
.dependsOn(graph)
.dependsOn(`polyglot-api`)
.dependsOn(`text-buffer`)
/* Note [Unmanaged Classpath]
* ~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -1,7 +1,7 @@
package org.enso.languageserver.text
import org.enso.languageserver.data.ContentBasedVersioning
import org.enso.languageserver.data.buffer.Rope
import org.enso.text.buffer.Rope
/**
* A buffer state representation.

View File

@ -22,7 +22,6 @@ import org.enso.languageserver.text.TextProtocol.{
OpenFile,
SaveFile
}
import org.enso.languageserver.text.editing.model.FileEdit
/**
* An actor that routes request regarding text editing to the right buffer.

View File

@ -28,8 +28,14 @@ import org.enso.languageserver.util.UnhandledLogging
import org.enso.languageserver.text.Buffer.Version
import org.enso.languageserver.text.CollaborativeBuffer.IOTimeout
import org.enso.languageserver.text.TextProtocol._
import org.enso.languageserver.text.editing._
import org.enso.languageserver.text.editing.model.{FileEdit, TextEdit}
import org.enso.text.editing.{
EditorOps,
EndPositionBeforeStartPosition,
InvalidPosition,
NegativeCoordinateInPosition,
TextEditValidationFailure
}
import org.enso.text.editing.model.TextEdit
import scala.concurrent.duration._
import scala.language.postfixOps

View File

@ -0,0 +1,19 @@
package org.enso.languageserver.text
import org.enso.languageserver.filemanager.Path
import org.enso.text.editing.model.TextEdit
/**
* A representation of a batch of edits to a file, versioned.
*
* @param path a path of a file
* @param edits a series of edits to a file
* @param oldVersion the current version of a buffer
* @param newVersion the version of a buffer after applying all edits
*/
case class FileEdit(
path: Path,
edits: List[TextEdit],
oldVersion: Buffer.Version,
newVersion: Buffer.Version
)

View File

@ -3,7 +3,6 @@ package org.enso.languageserver.text
import org.enso.languageserver.data.CapabilityRegistration
import org.enso.languageserver.filemanager.Path
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused}
import org.enso.languageserver.text.editing.model.FileEdit
/**
* The text editing JSON RPC API provided by the language server.

View File

@ -2,7 +2,6 @@ package org.enso.languageserver.text
import org.enso.languageserver.data.{CapabilityRegistration, Client}
import org.enso.languageserver.filemanager.{FileSystemFailure, Path}
import org.enso.languageserver.text.editing.model.FileEdit
object TextProtocol {

View File

@ -11,6 +11,7 @@ import com.fasterxml.jackson.module.scala.{
DefaultScalaModule,
ScalaObjectMapper
}
import org.enso.text.editing.model.TextEdit
import scala.util.Try
@ -54,6 +55,22 @@ object Runtime {
value = classOf[Api.PopContextResponse],
name = "popContextResponse"
),
new JsonSubTypes.Type(
value = classOf[Api.OpenFileNotification],
name = "openFileNotification"
),
new JsonSubTypes.Type(
value = classOf[Api.EditFileNotification],
name = "editFileNotification"
),
new JsonSubTypes.Type(
value = classOf[Api.CloseFileNotification],
name = "closeFileNotification"
),
new JsonSubTypes.Type(
value = classOf[Api.CreateFileNotification],
name = "createFileNotification"
),
new JsonSubTypes.Type(
value = classOf[Api.ExpressionValuesComputed],
name = "expressionValuesComputed"
@ -280,6 +297,41 @@ object Runtime {
*/
case class InvalidStackItemError(contextId: ContextId) extends Error
/**
* A notification sent to the server about switching a file to literal
* contents.
*
* @param path the file being moved to memory.
* @param contents the current file contents.
*/
case class OpenFileNotification(path: File, contents: String)
extends ApiRequest
/**
* A notification sent to the server about in-memory file contents being
* edited.
*
* @param path the file being edited.
* @param edits the diffs to apply to the contents.
*/
case class EditFileNotification(path: File, edits: Seq[TextEdit])
extends ApiRequest
/**
* A notification sent to the server about dropping the file from memory
* back to on-disk version.
*
* @param path the file being closed.
*/
case class CloseFileNotification(path: File) extends ApiRequest
/**
* A notification sent to the server about a file being created.
*
* @param path the newly created file.
*/
case class CreateFileNotification(path: File) extends ApiRequest
/**
* Notification sent from the server to the client upon successful
* initialization. Any messages sent to the server before receiving this

View File

@ -115,6 +115,6 @@ public final class Language extends TruffleLanguage<Context> {
*/
@Override
protected Iterable<Scope> findTopScopes(Context context) {
return Collections.singleton(context.compiler().topScope().getScope());
return Collections.singleton(context.getCompiler().topScope().getScope());
}
}

View File

@ -48,7 +48,10 @@ public class ProgramRootNode extends RootNode {
public Object execute(VirtualFrame frame) {
if (module == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
module = new Module(QualifiedName.simpleName(sourceCode.getName()), sourceCode);
module =
new Module(
QualifiedName.simpleName(sourceCode.getName()),
sourceCode.getCharacters().toString());
}
// Note [Static Passes]
return module;

View File

@ -17,7 +17,6 @@ import org.enso.interpreter.runtime.callable.argument.Thunk;
import org.enso.interpreter.runtime.scope.LocalScope;
import org.enso.interpreter.runtime.scope.ModuleScope;
import org.enso.interpreter.runtime.state.Stateful;
import scala.Some;
/** Node running Enso expressions passed to it as strings. */
@NodeInfo(shortName = "Eval", description = "Evaluates code passed to it as string")
@ -63,7 +62,7 @@ public abstract class EvalNode extends BaseNode {
ExpressionNode expr =
lookupContextReference(Language.class)
.get()
.compiler()
.getCompiler()
.runInline(expression, inlineContext)
.getOrElse(null);
if (expr == null) {

View File

@ -83,7 +83,7 @@ public class Context {
*
* @return a handle to the compiler
*/
public final Compiler compiler() {
public final Compiler getCompiler() {
return compiler;
}
@ -126,18 +126,22 @@ public class Context {
return moduleScope;
}
/**
* Removes all contents from a given scope.
*
* @param scope the scope to reset.
*/
public void resetScope(ModuleScope scope) {
scope.reset();
initializeScope(scope);
}
/**
* Guess module name from the file path by comparing it with the source pathes
* of imported packages.
* Fetches the module name associated with a given file, using the environment packages
* information.
*
* @param path file path.
* @return qualified module name if the function can find imported package
* with matching path.
* @param path the path to decode.
* @return a qualified name of the module corresponding to the file, if exists.
*/
public Optional<QualifiedName> getModuleNameForFile(File path) {
return packages.stream()
@ -147,15 +151,25 @@ public class Context {
}
/**
* Get module from the file path. Function tries to recover module name from
* the provided file path.
* Fetches a module associated with a given file.
*
* @param path file path.
* @return module if module name can be guessed from the provided file path.
* @param path the module path to lookup.
* @return the relevant module, if exists.
*/
public Optional<Module> getModuleForFile(File path) {
return getModuleNameForFile(path)
.flatMap(n -> compiler().topScope().getModule(n.toString()));
.flatMap(n -> getCompiler().topScope().getModule(n.toString()));
}
/**
* Registers a new module corresponding to a given file.
*
* @param path the file to register.
* @return the newly created module, if the file is a source file.
*/
public Optional<Module> createModuleForFile(File path) {
return getModuleNameForFile(path)
.map(name -> getCompiler().topScope().createModule(name, getTruffleFile(path)));
}
private void initializeScope(ModuleScope scope) {

View File

@ -25,13 +25,14 @@ import org.enso.interpreter.runtime.type.Types;
import org.enso.pkg.QualifiedName;
import org.enso.polyglot.LanguageInfo;
import org.enso.polyglot.MethodNames;
import org.enso.text.buffer.Rope;
/** Represents a source module with a known location. */
@ExportLibrary(InteropLibrary.class)
public class Module implements TruffleObject {
private ModuleScope scope;
private TruffleFile sourceFile;
private Source literalSource;
private Rope literalSource;
private boolean isParsed = false;
private final QualifiedName name;
@ -46,7 +47,24 @@ public class Module implements TruffleObject {
this.name = name;
}
public Module(QualifiedName name, Source literalSource) {
/**
* Creates a new module.
*
* @param name the qualified name of this module.
* @param literalSource the module's source.
*/
public Module(QualifiedName name, String literalSource) {
this.literalSource = Rope.apply(literalSource);
this.name = name;
}
/**
* Creates a new module.
*
* @param name the qualified name of this module.
* @param literalSource the module's source.
*/
public Module(QualifiedName name, Rope literalSource) {
this.literalSource = literalSource;
this.name = name;
}
@ -62,12 +80,41 @@ public class Module implements TruffleObject {
this.isParsed = true;
}
public void setLiteralSource(Source source) {
this.literalSource = source;
this.sourceFile = null;
/** Clears any literal source set for this module. */
public void unsetLiteralSource() {
this.literalSource = null;
this.isParsed = false;
}
/** @return the literal source of this module. */
public Rope getLiteralSource() {
return literalSource;
}
/**
* Sets new literal sources for the module.
*
* @param source the module source.
*/
public void setLiteralSource(String source) {
setLiteralSource(Rope.apply(source));
}
/**
* Sets new literal sources for the module.
*
* @param source the module source.
*/
public void setLiteralSource(Rope source) {
this.literalSource = source;
this.isParsed = false;
}
/**
* Sets a source file for the module.
*
* @param file the module source file.
*/
public void setSourceFile(TruffleFile file) {
this.literalSource = null;
this.sourceFile = file;
@ -95,17 +142,24 @@ public class Module implements TruffleObject {
}
}
public void parse(Context context) {
private void parse(Context context) {
ensureScopeExists(context);
context.resetScope(scope);
isParsed = true;
if (sourceFile != null) {
context.compiler().run(sourceFile, scope);
} else if (literalSource != null) {
context.compiler().run(literalSource, scope);
if (literalSource != null) {
Source source =
Source.newBuilder(LanguageInfo.ID, literalSource.characters(), name.toString()).build();
context.getCompiler().run(source, scope);
} else if (sourceFile != null) {
context.getCompiler().run(sourceFile, scope);
}
}
/** @return the qualified name of this module. */
public QualifiedName getName() {
return name;
}
/**
* Handles member invocations through the polyglot API.
*
@ -140,7 +194,7 @@ public class Module implements TruffleObject {
Source source =
Source.newBuilder(LanguageInfo.ID, sourceString, scope.getAssociatedType().getName())
.build();
context.compiler().run(source, scope);
context.getCompiler().run(source, scope);
return module;
}
@ -154,8 +208,7 @@ public class Module implements TruffleObject {
private static Module setSource(Module module, Object[] args, Context context)
throws ArityException, UnsupportedTypeException {
String source = Types.extractArguments(args, String.class);
module.setLiteralSource(
Source.newBuilder(LanguageInfo.ID, source, module.name.module()).build());
module.setLiteralSource(source);
return module;
}

View File

@ -1,6 +1,7 @@
package org.enso.interpreter.runtime.scope;
import com.oracle.truffle.api.Scope;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.dsl.CachedContext;
import com.oracle.truffle.api.dsl.Specialization;
@ -57,9 +58,25 @@ public class TopLevelScope implements TruffleObject {
* @return empty result if the module does not exist or the requested module.
*/
public Optional<Module> getModule(String name) {
if (name.equals(Builtins.MODULE_NAME)) {
return Optional.of(builtins.getModule());
}
return Optional.ofNullable(modules.get(name));
}
/**
* Creates and registers a new module with given name and source file.
*
* @param name the module name.
* @param sourceFile the module source file.
* @return the newly created module.
*/
public Module createModule(QualifiedName name, TruffleFile sourceFile) {
Module module = new Module(name, sourceFile);
modules.put(name.toString(), module);
return module;
}
/**
* Returns the builtins module.
*

View File

@ -14,9 +14,13 @@ import org.enso.interpreter.runtime.Module;
import org.enso.interpreter.runtime.callable.atom.AtomConstructor;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.scope.ModuleScope;
import org.enso.pkg.QualifiedName;
import java.io.File;
import org.enso.text.buffer.Rope;
import org.enso.text.editing.JavaEditorAdapter;
import org.enso.text.editing.model;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
@ -43,7 +47,7 @@ public class ExecutionService {
private Optional<FunctionCallInstrumentationNode.FunctionCall> prepareFunctionCall(
String moduleName, String consName, String methodName) {
Optional<Module> moduleMay = context.compiler().topScope().getModule(moduleName);
Optional<Module> moduleMay = context.getCompiler().topScope().getModule(moduleName);
if (!moduleMay.isPresent()) {
return Optional.empty();
}
@ -108,13 +112,63 @@ public class ExecutionService {
Consumer<IdExecutionInstrument.ExpressionValue> valueCallback,
Consumer<IdExecutionInstrument.ExpressionCall> funCallCallback)
throws UnsupportedMessageException, ArityException, UnsupportedTypeException {
Optional<FunctionCallInstrumentationNode.FunctionCall> callMay = context
.getModuleNameForFile(modulePath)
.flatMap(moduleName -> prepareFunctionCall(moduleName.toString(), consName, methodName));
Optional<FunctionCallInstrumentationNode.FunctionCall> callMay =
context
.getModuleNameForFile(modulePath)
.flatMap(
moduleName -> prepareFunctionCall(moduleName.toString(), consName, methodName));
if (!callMay.isPresent()) {
return;
}
execute(callMay.get(), valueCallback, funCallCallback);
}
/**
* Sets a module at a given path to use a literal source.
*
* @param path the module path.
* @param contents the sources to use for it.
*/
public void setModuleSources(File path, String contents) {
Optional<Module> module = context.getModuleForFile(path);
module.ifPresent(mod -> mod.setLiteralSource(contents));
}
/**
* Resets a module to use on-disk sources.
*
* @param path the module path.
*/
public void resetModuleSources(File path) {
Optional<Module> module = context.getModuleForFile(path);
module.ifPresent(Module::unsetLiteralSource);
}
/**
* Registers a new file as a source module.
*
* @param path the file to register.
*/
public void createModule(File path) {
context.createModuleForFile(path);
}
/**
* Applies modifications to literal module sources.
*
* @param path the module to edit.
* @param edits the edits to apply.
*/
public void modifyModuleSources(File path, List<model.TextEdit> edits) {
Optional<Module> moduleMay = context.getModuleForFile(path);
if (!moduleMay.isPresent()) {
return;
}
Module module = moduleMay.get();
if (module.getLiteralSource() == null) {
return;
}
Optional<Rope> editedSource = JavaEditorAdapter.applyEdits(module.getLiteralSource(), edits);
editedSource.ifPresent(module::setLiteralSource);
}
}

View File

@ -131,7 +131,7 @@ class IRToTruffle(
// Register the imports in scope
imports.foreach(i =>
this.moduleScope.addImport(context.compiler.processImport(i.name))
this.moduleScope.addImport(context.getCompiler.processImport(i.name))
)
// Register the atoms and their constructors in scope

View File

@ -16,6 +16,7 @@ import org.enso.polyglot.runtime.Runtime.Api
import org.graalvm.polyglot.io.MessageEndpoint
import scala.jdk.javaapi.OptionConverters
import scala.jdk.CollectionConverters._
/**
* A message endpoint implementation used by the
@ -193,66 +194,104 @@ final class Handler {
*
* @param msg the message to handle.
*/
def onMessage(msg: Api.Request): Unit = msg match {
case Api.Request(requestId, Api.CreateContextRequest(contextId)) =>
contextManager.create(contextId)
endpoint.sendToClient(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
def onMessage(msg: Api.Request): Unit = {
val requestId = msg.requestId
msg.payload match {
case Api.CreateContextRequest(contextId) =>
contextManager.create(contextId)
endpoint.sendToClient(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)
case Api.Request(requestId, Api.DestroyContextRequest(contextId)) =>
if (contextManager.get(contextId).isDefined) {
contextManager.destroy(contextId)
endpoint.sendToClient(
Api.Response(requestId, Api.DestroyContextResponse(contextId))
)
} else {
endpoint.sendToClient(
Api.Response(requestId, Api.ContextNotExistError(contextId))
)
case Api.PushContextRequest(contextId, item) => {
if (contextManager.get(contextId).isDefined) {
val stack = contextManager.getStack(contextId)
val payload = item match {
case call: Api.StackItem.ExplicitCall if stack.isEmpty =>
contextManager.push(contextId, item)
withContext(execute(contextId, List(call)))
Api.PushContextResponse(contextId)
case _: Api.StackItem.LocalCall if stack.nonEmpty =>
contextManager.push(contextId, item)
withContext(execute(contextId, stack.toList))
Api.PushContextResponse(contextId)
case _ =>
Api.InvalidStackItemError(contextId)
}
endpoint.sendToClient(Api.Response(requestId, payload))
} else {
endpoint.sendToClient(
Api.Response(requestId, Api.ContextNotExistError(contextId))
)
}
}
case Api.Request(requestId, Api.PushContextRequest(contextId, item)) => {
if (contextManager.get(contextId).isDefined) {
val stack = contextManager.getStack(contextId)
val payload = item match {
case call: Api.StackItem.ExplicitCall if stack.isEmpty =>
contextManager.push(contextId, item)
withContext(execute(contextId, List(call)))
Api.PushContextResponse(contextId)
case _: Api.StackItem.LocalCall if stack.nonEmpty =>
contextManager.push(contextId, item)
withContext(execute(contextId, stack.toList))
Api.PushContextResponse(contextId)
case _ =>
Api.InvalidStackItemError(contextId)
case Api.PopContextRequest(contextId) =>
if (contextManager.get(contextId).isDefined) {
val payload = contextManager.pop(contextId) match {
case Some(_: Api.StackItem.ExplicitCall) =>
Api.PopContextResponse(contextId)
case Some(_: Api.StackItem.LocalCall) =>
withContext(
execute(contextId, contextManager.getStack(contextId).toList)
)
Api.PopContextResponse(contextId)
case None =>
Api.EmptyStackError(contextId)
}
endpoint.sendToClient(Api.Response(requestId, payload))
} else {
endpoint.sendToClient(
Api.Response(requestId, Api.ContextNotExistError(contextId))
)
}
case Api.DestroyContextRequest(contextId) =>
if (contextManager.get(contextId).isDefined) {
contextManager.destroy(contextId)
endpoint.sendToClient(
Api.Response(requestId, Api.DestroyContextResponse(contextId))
)
} else {
endpoint.sendToClient(
Api.Response(requestId, Api.ContextNotExistError(contextId))
)
}
case Api.PushContextRequest(contextId, item) =>
val payload = contextManager.push(contextId, item) match {
case Some(()) => Api.PushContextResponse(contextId)
case None => Api.ContextNotExistError(contextId)
}
endpoint.sendToClient(Api.Response(requestId, payload))
} else {
endpoint.sendToClient(
Api.Response(requestId, Api.ContextNotExistError(contextId))
)
}
case Api.PopContextRequest(contextId) =>
if (contextManager.get(contextId).isDefined) {
val payload = contextManager.pop(contextId) match {
case Some(_) => Api.PopContextResponse(contextId)
case None => Api.EmptyStackError(contextId)
}
endpoint.sendToClient(Api.Response(requestId, payload))
} else {
endpoint.sendToClient(
Api.Response(requestId, Api.ContextNotExistError(contextId))
)
}
case Api.OpenFileNotification(path, contents) =>
executionService.setModuleSources(path, contents)
case Api.CreateFileNotification(path) =>
executionService.createModule(path)
case Api.OpenFileNotification(path, contents) =>
executionService.setModuleSources(path, contents)
case Api.CloseFileNotification(path) =>
executionService.resetModuleSources(path)
case Api.EditFileNotification(path, edits) =>
executionService.modifyModuleSources(path, edits.asJava)
}
case Api.Request(requestId, Api.PopContextRequest(contextId)) =>
if (contextManager.get(contextId).isDefined) {
val payload = contextManager.pop(contextId) match {
case Some(_: Api.StackItem.ExplicitCall) =>
Api.PopContextResponse(contextId)
case Some(_: Api.StackItem.LocalCall) =>
withContext(
execute(contextId, contextManager.getStack(contextId).toList)
)
Api.PopContextResponse(contextId)
case None =>
Api.EmptyStackError(contextId)
}
endpoint.sendToClient(Api.Response(requestId, payload))
} else {
endpoint.sendToClient(
Api.Response(requestId, Api.ContextNotExistError(contextId))
)
}
}
}

View File

@ -1,19 +1,21 @@
package org.enso.interpreter.test.instrument
import java.io.File
import java.io.{ByteArrayOutputStream, File, OutputStream}
import java.nio.ByteBuffer
import java.nio.file.Files
import java.util.UUID
import org.enso.interpreter.test.Metadata
import org.enso.pkg.Package
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.polyglot.runtime.Runtime.{Api, ApiRequest}
import org.enso.polyglot.{
LanguageInfo,
PolyglotContext,
RuntimeOptions,
RuntimeServerInfo
}
import org.enso.text.editing.model
import org.enso.text.editing.model.TextEdit
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.io.MessageEndpoint
import org.scalatest.BeforeAndAfterEach
@ -30,8 +32,11 @@ class RuntimeServerTest
class TestContext(packageName: String) {
var endPoint: MessageEndpoint = _
var messageQueue: List[Api.Response] = List()
val tmpDir: File = Files.createTempDirectory("enso-test-packages").toFile
val pkg: Package = Package.create(tmpDir, packageName)
val tmpDir: File = Files.createTempDirectory("enso-test-packages").toFile
val pkg: Package = Package.create(tmpDir, packageName)
val out: ByteArrayOutputStream = new ByteArrayOutputStream()
val executionContext = new PolyglotContext(
Context
.newBuilder(LanguageInfo.ID)
@ -39,6 +44,7 @@ class RuntimeServerTest
.allowAllAccess(true)
.option(RuntimeOptions.getPackagesPathOption, pkg.root.getAbsolutePath)
.option(RuntimeServerInfo.ENABLE_OPTION, "true")
.out(out)
.serverTransport { (uri, peer) =>
if (uri.toString == RuntimeServerInfo.URI) {
endPoint = peer
@ -60,8 +66,11 @@ class RuntimeServerTest
)
executionContext.context.initialize(LanguageInfo.ID)
def writeMain(contents: String): File = {
def writeMain(contents: String): File =
Files.write(pkg.mainFile.toPath, contents.getBytes).toFile
def writeFile(file: File, contents: String): Unit = {
Files.write(file.toPath, contents.getBytes): Unit
}
def send(msg: Api.Request): Unit = endPoint.sendBinary(Api.serialize(msg))
@ -71,6 +80,12 @@ class RuntimeServerTest
messageQueue = messageQueue.drop(1)
msg
}
def consumeOut: List[String] = {
val result = out.toString
out.reset()
result.linesIterator.toList
}
}
object Program {
@ -276,4 +291,68 @@ class RuntimeServerTest
)
}
"Runtime server" should "support file modification operations" in {
def send(msg: ApiRequest): Unit =
context.send(Api.Request(UUID.randomUUID(), msg))
val fooFile = new File(context.pkg.sourceDir, "Foo.enso")
val contextId = UUID.randomUUID()
send(Api.CreateContextRequest(contextId))
context.receive
def push: Unit =
send(
Api.PushContextRequest(
contextId,
Api.StackItem
.ExplicitCall(
Api.MethodPointer(fooFile, "Foo", "main"),
None,
Vector()
)
)
)
def pop: Unit = send(Api.PopContextRequest(contextId))
// Create a new file
context.writeFile(fooFile, "main = IO.println \"I'm a file!\"")
send(Api.CreateFileNotification(fooFile))
push
context.consumeOut shouldEqual List("I'm a file!")
// Open the new file and set literal source
send(
Api.OpenFileNotification(
fooFile,
"main = IO.println \"I'm an open file!\""
)
)
pop
push
context.consumeOut shouldEqual List("I'm an open file!")
// Modify the file
send(
Api.EditFileNotification(
fooFile,
Seq(
TextEdit(
model.Range(model.Position(0, 24), model.Position(0, 30)),
" modified"
)
)
)
)
pop
push
context.consumeOut shouldEqual List("I'm a modified file!")
// Close the file
send(Api.CloseFileNotification(fooFile))
pop
push
context.consumeOut shouldEqual List("I'm a file!")
}
}

View File

@ -1,4 +1,4 @@
package org.enso.languageserver.data.buffer
package org.enso.text.buffer
/**
* Exposes a character-based API for rope operations.
@ -72,6 +72,8 @@ case class CharView(rope: Rope) extends CharSequence {
start: Int,
end: Int
): CharSequence = CharView(substring(start, end))
override def toString: String = rope.toString
}
object CharView {

View File

@ -1,7 +1,8 @@
package org.enso.languageserver.data.buffer
package org.enso.text.buffer
/**
* Exposes a code points based view over rope indexing operations.
*
* @param rope the underlying rope.
*/
case class CodePointView(rope: Rope) {

View File

@ -1,7 +1,8 @@
package org.enso.languageserver.data.buffer
package org.enso.text.buffer
/**
* Exposes a line-based API for the rope.
*
* @param rope the underlying rope.
*/
case class LineView(rope: Rope) {

View File

@ -1,8 +1,10 @@
package org.enso.languageserver.data.buffer
import cats.kernel.Monoid
package org.enso.text.buffer
import cats.Monoid
/**
* The measure used for storing strings in the b-tree.
*
* @param utf16Size number of characters.
* @param utf32Size number of code points.
* @param fullLines number of lines terminated with a new line character.
@ -66,7 +68,7 @@ case class Rope(root: Node[String, StringMeasure]) {
*/
override def toString: String = {
val sb = new StringBuilder(root.measure.utf16Size)
root.value.foreach { str => val _ = sb.append(str) }
root.value.foreach { str => sb.append(str) }
sb.toString()
}

View File

@ -1,4 +1,5 @@
package org.enso.languageserver.data.buffer
package org.enso.text.buffer
import scala.collection.mutable.ArrayBuffer
object StringUtils {

View File

@ -1,6 +1,6 @@
package org.enso.languageserver.data.buffer
package org.enso.text.buffer
import cats.kernel.Monoid
import cats.Monoid
/**
* A super class of nodes stored in the tree.

View File

@ -1,4 +1,4 @@
package org.enso.languageserver.data.buffer
package org.enso.text.buffer
/**
* Encodes element-level operation on a tree.

View File

@ -1,8 +1,7 @@
package org.enso.languageserver.text.editing
package org.enso.text.editing
import cats.implicits._
import org.enso.languageserver.text.editing.TextEditValidator.validate
import org.enso.languageserver.text.editing.model.TextEdit
import org.enso.text.editing.model.TextEdit
/**
* Auxiliary operations that edit a buffer validating diffs before applying it.
@ -25,7 +24,9 @@ object EditorOps {
* @return either validation failure or a modified buffer
*/
def edit[A: TextEditor](buffer: A, diff: TextEdit): EditorOp[A] =
validate(buffer, diff).map(_ => TextEditor[A].edit(buffer, diff))
TextEditValidator
.validate(buffer, diff)
.map(_ => TextEditor[A].edit(buffer, diff))
/**
* Applies a series of edits to the buffer and validates each diff against

View File

@ -0,0 +1,31 @@
package org.enso.text.editing
import java.util.Optional
import org.enso.text.buffer.Rope
import org.enso.text.editing.model.TextEdit
import scala.jdk.CollectionConverters._
/**
* A convenience class for using the text editor logic from Java code.
*/
object JavaEditorAdapter {
implicit private val editor: TextEditor[Rope] = RopeTextEditor
/**
* Applies a series of edits to a given text.
*
* @param rope the initial text.
* @param edits the edits to apply.
* @return the result of applying edits, if they pass the validations.
*/
def applyEdits(
rope: Rope,
edits: java.util.List[TextEdit]
): java.util.Optional[Rope] =
EditorOps
.applyEdits(rope, edits.asScala.toList)
.map(Optional.of[Rope])
.getOrElse(Optional.empty[Rope]())
}

View File

@ -1,7 +1,7 @@
package org.enso.languageserver.text.editing
package org.enso.text.editing
import org.enso.languageserver.data.buffer.Rope
import org.enso.languageserver.text.editing.model.TextEdit
import org.enso.text.buffer.Rope
import org.enso.text.editing.model.TextEdit
/**
* Instance of the [[TextEditor]] type class for the [[Rope]] type.

View File

@ -1,6 +1,6 @@
package org.enso.languageserver.text.editing
package org.enso.text.editing
import org.enso.languageserver.text.editing.model.Position
import org.enso.text.editing.model.Position
/**
* Base trait for text edit validation failures.

View File

@ -1,7 +1,7 @@
package org.enso.languageserver.text.editing
package org.enso.text.editing
import org.enso.languageserver.text.editing.model.TextEdit
import cats.implicits._
import org.enso.text.editing.model.TextEdit
/**
* A validator of [[TextEdit]] object.

View File

@ -1,7 +1,7 @@
package org.enso.languageserver.text.editing
package org.enso.text.editing
import org.enso.languageserver.data.buffer.Rope
import org.enso.languageserver.text.editing.model.TextEdit
import org.enso.text.buffer.Rope
import org.enso.text.editing.model.TextEdit
/**
* TextEditor is a type class that specifies a set of function for text

View File

@ -1,7 +1,4 @@
package org.enso.languageserver.text.editing
import org.enso.languageserver.filemanager.Path
import org.enso.languageserver.text.Buffer
package org.enso.text.editing
object model {
@ -44,19 +41,4 @@ object model {
*/
case class TextEdit(range: Range, text: String)
/**
* A representation of a batch of edits to a file, versioned.
*
* @param path a path of a file
* @param edits a series of edits to a file
* @param oldVersion the current version of a buffer
* @param newVersion the version of a buffer after applying all edits
*/
case class FileEdit(
path: Path,
edits: List[TextEdit],
oldVersion: Buffer.Version,
newVersion: Buffer.Version
)
}

View File

@ -1,4 +1,5 @@
package org.enso.languageserver.text
package org.enso.text
import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.Gen

View File

@ -1,5 +1,6 @@
package org.enso.languageserver.text
import org.enso.languageserver.data.buffer.StringUtils
package org.enso.text
import org.enso.text.buffer.StringUtils
case class MockBuffer(lines: List[String]) {
override def toString: String = lines.mkString("")

View File

@ -1,5 +1,6 @@
package org.enso.languageserver.text
import org.enso.languageserver.data.buffer.Rope
package org.enso.text
import org.enso.text.buffer.Rope
import org.scalacheck.Prop.forAll
import org.scalacheck.Arbitrary._
import org.scalacheck.Properties

View File

@ -1,5 +1,6 @@
package org.enso.languageserver.text
import org.enso.languageserver.data.buffer.StringUtils
package org.enso.text
import org.enso.text.buffer.StringUtils
import org.scalacheck.Properties
import org.scalacheck.Prop.forAll

View File

@ -1,7 +1,7 @@
package org.enso.languageserver.text.editing
package org.enso.text.editing
import org.enso.languageserver.text.editing.TestData.testSnippet
import org.enso.languageserver.text.editing.model.{Position, Range, TextEdit}
import TestData.testSnippet
import org.enso.text.editing.model.{Position, Range, TextEdit}
import org.scalatest.EitherValues
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.must.Matchers

View File

@ -1,8 +1,8 @@
package org.enso.languageserver.text.editing
package org.enso.text.editing
import org.enso.languageserver.data.buffer.Rope
import org.enso.languageserver.text.editing.TestData._
import org.enso.languageserver.text.editing.model.{Position, Range, TextEdit}
import TestData._
import org.enso.text.buffer.Rope
import org.enso.text.editing.model.{Position, Range, TextEdit}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.must.Matchers

View File

@ -1,6 +1,6 @@
package org.enso.languageserver.text.editing
package org.enso.text.editing
import org.enso.languageserver.data.buffer.Rope
import org.enso.text.buffer.Rope
object TestData {

View File

@ -1,10 +1,10 @@
package org.enso.languageserver.text.editing
package org.enso.text.editing
import org.enso.languageserver.data.buffer.Rope
import org.enso.languageserver.text.editing.TextEditValidator.validate
import org.enso.languageserver.text.editing.model._
import org.enso.text.buffer.Rope
import org.enso.text.editing.model.{Position, Range, TextEdit}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.must.Matchers
import TextEditValidator.validate
class TextEditValidatorSpec extends AnyFlatSpec with Matchers {