mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 18:38:11 +03:00
Module file operations through execution server (#660)
This commit is contained in:
parent
75f25b66db
commit
e2d901fb68
17
build.sbt
17
build.sbt
@ -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]
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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!")
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
@ -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) {
|
@ -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) {
|
@ -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()
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
package org.enso.languageserver.data.buffer
|
||||
package org.enso.text.buffer
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
|
||||
object StringUtils {
|
@ -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.
|
@ -1,4 +1,4 @@
|
||||
package org.enso.languageserver.data.buffer
|
||||
package org.enso.text.buffer
|
||||
|
||||
/**
|
||||
* Encodes element-level operation on a tree.
|
@ -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
|
@ -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]())
|
||||
}
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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
|
@ -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
|
||||
)
|
||||
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
package org.enso.languageserver.text
|
||||
package org.enso.text
|
||||
|
||||
import org.scalacheck.Arbitrary.arbitrary
|
||||
import org.scalacheck.Gen
|
||||
|
@ -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("")
|
@ -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
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user