Execute and debug .enso files with bin/enso in VSCode (#8923)

Let's _untie_ the [VSCode Enso extension](https://marketplace.visualstudio.com/items?itemName=Enso.enso4vscode) from `sbt` commands. Let's **open any Enso file in the editor** and then use _F5_ or _Ctrl-F5_ to execute it. Let the user choose which `bin/enso` script to use for execution completely skipping the need for `sbt`.
This commit is contained in:
Jaroslav Tulach 2024-02-12 07:38:11 +01:00 committed by GitHub
parent b00dc9e9c0
commit ca9125f8e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 300 additions and 8 deletions

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
java: ["8"]
java: ["11"]
steps:
- uses: actions/checkout@v2
@ -30,7 +30,7 @@ jobs:
# Why do we subtract a number? Read versioning policy!
# https://github.com/enso-org/enso/pull/7861#discussion_r1333133490
echo "POM_VERSION=`mvn -q -DforceStdout help:evaluate -Dexpression=project.version | cut -f1 -d -`" >> "$GITHUB_ENV"
echo "MICRO_VERSION=`expr $GITHUB_RUN_NUMBER - 2100`" >> "$GITHUB_ENV"
echo "MICRO_VERSION=`expr $GITHUB_RUN_NUMBER - 2250`" >> "$GITHUB_ENV"
- name: Update project version
working-directory: tools/enso4igv

5
.vscode/launch.json vendored
View File

@ -1,6 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "java+",
"request": "launch",
"name": "Launch Enso File"
},
{
"name": "Listen to 5005",
"type": "java+",

View File

@ -1040,6 +1040,7 @@
- [DataflowError.withoutTrace doesn't store stacktrace][8608]
- [Derive --in-project from --run source location][8775]
- [Binary operator resolution based on that value][8779]
- [Execute and debug individual Enso files in VSCode extension][8923]
[3227]: https://github.com/enso-org/enso/pull/3227
[3248]: https://github.com/enso-org/enso/pull/3248
@ -1195,6 +1196,7 @@
[8608]: https://github.com/enso-org/enso/pull/8608
[8775]: https://github.com/enso-org/enso/pull/8775
[8779]: https://github.com/enso-org/enso/pull/8779
[8923]: https://github.com/enso-org/enso/pull/8923
# Enso 2.0.0-alpha.18 (2021-10-12)

View File

@ -22,9 +22,26 @@ following two extensions in the system:
![Installed VSCode extensions](https://user-images.githubusercontent.com/26887752/274904239-ae1ad4cc-e2ec-4c5b-bca0-c7d7189c6885.png)
## Using & Debugging
## Debugging a Single Enso File
Once installation is over let's continue with choosing _File/Open Folder..._ and
Open any `.enso` files. Click left editor gutter to place breakpoints. Then
choose _Run/Start Debugging_. If asked, choose _debug with_ **Java+** (Enso is
Java virtual machine friendly). A prompt appears asking for path to `bin/enso`
binary:
![Select enso executable](https://github.com/enso-org/enso/assets/26887752/4e1d0666-634d-4fb8-bf61-6dbf765311e8)
Locate `bin/enso` executable in the Enso engine download. If binding from source
code, the executable is located at root of
[Enso repository](https://github.com/enso-org/enso/) in
`./built-distribution/enso-engine-*/enso-*/bin/enso`. The `.enso` file gets
executed and output is printed in the area below editor:
![Executed](https://github.com/enso-org/enso/assets/26887752/2165a04f-bc0a-4b62-9ad7-e74e354e6937)
## Workspace Debugging
To work with all Enso code base continue with choosing _File/Open Folder..._ and
opening root of [Enso Git Repository](http://github.com/enso-org/enso)
(presumably already built with
[sbt buildEngineDistribution](https://github.com/enso-org/enso/blob/develop/docs/CONTRIBUTING.md#running-enso)).
@ -43,6 +60,17 @@ place one on line 120:
![Breakpoint](https://github.com/enso-org/enso/assets/26887752/b6ae4725-49ef-439f-b900-3e08724e3748)
To debug the `test/Base_Tests/src/Data/Vector_Spec.enso` file with the root of
Enso repository opened in the VSCode workspace, choose preconfigured _Launch
Enso File_ debug configuration before _Run/Start Debugging_.:
![Launch Enso File in a Project](https://github.com/enso-org/enso/assets/26887752/3680aab2-bf99-41d2-ada7-491d6040f8d2)
The rest of the workflow remains the same as in case of individual (without any
project )`.enso` file case.
## Attach Debugger to a Process
Let's do a bit of debugging. Select _"Listen to 5005"_ debug configuration:
![Listen to 5005](https://github.com/enso-org/enso/assets/26887752/1874bcb1-cf8b-4df4-92d8-e7fb57e1b17a)

View File

@ -23,6 +23,7 @@
},
"categories": [
"Programming Languages",
"Debuggers",
"Other"
],
"keywords": [
@ -31,7 +32,10 @@
"truffle"
],
"activationEvents": [
"onLanguage:enso"
"workspaceContains:**/*.java",
"onLanguage:enso",
"onDebug",
"onDebugDynamicConfigurations"
],
"main": "./dist/extension",
"contributes": {
@ -66,7 +70,33 @@
"language": "enso"
}
],
"debuggers": []
"debuggers": [
{
"type": "java+",
"runtime": "node",
"languages": [
"enso"
],
"initialConfigurations": [
{
"type": "java+",
"request": "launch",
"name": "Launch Enso File"
}
],
"configurationSnippets": [
{
"label": "Enso Launch",
"description": "Debugging Enso Programs.",
"body": {
"type": "java+",
"request": "launch",
"name": "Launch Enso File"
}
}
]
}
]
},
"scripts": {
"vsix": "vsce package",

View File

@ -5,7 +5,7 @@
<artifactId>enso4igv</artifactId>
<packaging>nbm</packaging>
<name>Enso Language Support for NetBeans &amp; Ideal Graph Visualizer</name>
<version>1.21-SNAPSHOT</version>
<version>1.33-SNAPSHOT</version>
<build>
<plugins>
<plugin>
@ -72,6 +72,7 @@
<target>1.8</target>
<compilerArgs>
<arg>-Xlint:deprecation</arg>
<arg>-XDignore.symbol.file</arg>
</compilerArgs>
</configuration>
</plugin>
@ -128,6 +129,11 @@
<artifactId>org-openide-util</artifactId>
<version>${netbeans.version}</version>
</dependency>
<dependency>
<groupId>org.netbeans.api</groupId>
<artifactId>org-openide-modules</artifactId>
<version>${netbeans.version}</version>
</dependency>
<dependency>
<groupId>org.netbeans.api</groupId>
<artifactId>org-openide-util-lookup</artifactId>
@ -225,10 +231,28 @@
<scope>test</scope>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.netbeans.api</groupId>
<artifactId>org-netbeans-modules-extexecution-base</artifactId>
<version>${netbeans.version}</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.netbeans.api</groupId>
<artifactId>org-netbeans-modules-extexecution</artifactId>
<version>${netbeans.version}</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.netbeans.api</groupId>
<artifactId>org-openide-io</artifactId>
<version>${netbeans.version}</version>
<type>jar</type>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<netbeans.version>RELEASE113</netbeans.version>
<netbeans.version>RELEASE140</netbeans.version>
<netbeans.compile.on.save>none</netbeans.compile.on.save>
</properties>
<profiles>

View File

@ -0,0 +1,203 @@
package org.enso.tools.enso4igv;
import com.sun.jdi.connect.Connector;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Future;
import org.netbeans.api.debugger.jpda.JPDADebugger;
import org.netbeans.api.debugger.jpda.ListeningDICookie;
import org.netbeans.api.extexecution.ExecutionDescriptor;
import org.netbeans.api.extexecution.ExecutionService;
import org.netbeans.api.extexecution.base.ExplicitProcessParameters;
import org.netbeans.spi.project.ActionProvider;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.filesystems.FileObject;
import org.openide.util.Lookup;
import org.openide.util.lookup.ServiceProvider;
import org.netbeans.api.extexecution.base.ProcessBuilder;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.platform.JavaPlatform;
import org.netbeans.spi.project.ActionProgress;
import org.openide.filesystems.FileUtil;
import org.openide.modules.Modules;
import org.openide.util.NbBundle;
import org.openide.util.NbPreferences;
import org.openide.util.RequestProcessor;
import org.openide.windows.IOProvider;
@NbBundle.Messages({
"CTL_EnsoWhere=Enso Executable Location",
"CTL_EnsoExecutable=enso/enso.bat",
"# {0} - executable file",
"MSG_CannotExecute=Cannot execute {0}",
"# {0} - executable file",
"# {1} - exit code",
"MSG_ExecutionError=Process {0} finished with exit code {1}"
})
@ServiceProvider(service = ActionProvider.class)
public final class EnsoActionProvider implements ActionProvider {
@Override
public String[] getSupportedActions() {
return new String[]{ActionProvider.COMMAND_RUN_SINGLE, ActionProvider.COMMAND_DEBUG_SINGLE};
}
@Override
public void invokeAction(String action, Lookup lkp) throws IllegalArgumentException {
var process = ActionProgress.start(lkp);
var params = ExplicitProcessParameters.buildExplicitParameters(lkp);
var fo = lkp.lookup(FileObject.class);
var script = FileUtil.toFile(fo);
var io = IOProvider.getDefault().getIO(script.getName(), false);
var dd = DialogDisplayer.getDefault();
var prefs = NbPreferences.forModule(EnsoActionProvider.class);
var exeKey = "enso.executable";
var exe = prefs.get(exeKey, "");
var nd = new NotifyDescriptor.InputLine(Bundle.CTL_EnsoExecutable(), Bundle.CTL_EnsoWhere());
nd.setInputText(exe);
var builderFuture = dd.notifyFuture(nd).thenApply(exec -> {
var file = new File(exec.getInputText());
if (file.canExecute()) {
prefs.put(exeKey, file.getPath());
var platform = JavaPlatform.getDefault();
var isGraalVM = platform.findTool("native-image") != null;
var java = platform.findTool("java");
var b = ProcessBuilder.getLocal();
b.setExecutable(file.getPath());
b.setArguments(prepareArguments(script, isGraalVM));
b.setWorkingDirectory(script.getParent());
b.setRedirectErrorStream(true);
var env = b.getEnvironment();
var path = env.getVariable("PATH");
if (path != null && java != null) {
var javaBinDir = FileUtil.toFile(java.getParent());
if (javaBinDir != null) {
var newPath = path + File.pathSeparator + javaBinDir;
env.setVariable("PATH", newPath);
}
}
return b;
}
var msg = Bundle.MSG_CannotExecute(file.getPath());
throw new IllegalArgumentException(msg);
});
var waitForProcessFuture = builderFuture.thenCompose((builder) -> {
var cf = new CompletableFuture<Integer>();
var descriptor = new ExecutionDescriptor()
.frontWindow(true).controllable(true)
.inputOutput(io)
.postExecution((exitCode) -> {
cf.complete(exitCode);
});
var launch = ActionProvider.COMMAND_DEBUG_SINGLE.equals(action) ?
new DebugAndLaunch(fo, builder, params) : builder;
var service = ExecutionService.newService(launch, descriptor, script.getName());
service.run();
return cf;
});
waitForProcessFuture.thenAcceptBoth(builderFuture, (exitCode, builder) -> {
if (exitCode != 0) {
var msg = Bundle.MSG_ExecutionError(builder.getDescription(), exitCode);
var md = new NotifyDescriptor.Message(msg, NotifyDescriptor.ERROR_MESSAGE);
dd.notifyLater(md);
}
process.finished(exitCode == 0);
}).exceptionally((ex) -> {
process.finished(false);
if (ex instanceof CompletionException && ex.getCause() instanceof CancellationException) {
return null;
}
dd.notifyLater(new NotifyDescriptor.Message(ex.getMessage(), NotifyDescriptor.ERROR_MESSAGE));
return null;
});
}
private static List<String> prepareArguments(File script, boolean isGraalVM) {
var list = new ArrayList<String>();
list.add("--run");
list.add(script.getPath());
var isIGV = Modules.getDefault().findCodeNameBase("org.graalvm.visualizer.connection") != null;
if (isGraalVM && isIGV) {
list.add("--dump-graphs");
}
return list;
}
@Override
public boolean isActionEnabled(String string, Lookup lkp) throws IllegalArgumentException {
if (lkp.lookup(EnsoDataObject.class) != null) {
return true;
} else {
var fo = lkp.lookup(FileObject.class);
return fo != null && fo.getLookup().lookup(EnsoDataObject.class) != null;
}
}
static final class DebugAndLaunch implements Callable<Process> {
private static final RequestProcessor RP = new RequestProcessor(DebugAndLaunch.class);
private final FileObject script;
private final ProcessBuilder builder;
private final ExplicitProcessParameters params;
private final Future<String> computeAddress;
DebugAndLaunch(FileObject script, ProcessBuilder builder, ExplicitProcessParameters params) {
this.script = script;
this.builder = builder;
this.computeAddress = RP.submit(this::initAddress);
this.params = params;
}
@Override
public Process call() throws Exception {
var port = computeAddress.get();
builder.getEnvironment().setVariable("JAVA_OPTS", "-agentlib:jdwp=transport=dt_socket,address=" + port);
return builder.call();
}
private String initAddress() throws Exception {
var lc = ListeningDICookie.create(-1);
var connector = lc.getListeningConnector();
var args = lc.getArgs();
var address = connector.startListening(args);
var properties = new HashMap<>();
{
var sourcePath = ClassPath.getClassPath(script, ClassPath.SOURCE);
properties.put("sourcepath", sourcePath);
properties.put("baseDir", FileUtil.toFile(script.getParent()));
properties.put("name", script.getName());
}
var services = new Object[] { properties };
int port = Integer.parseInt(address.substring(address.indexOf(':') + 1));
Connector.IntegerArgument portArg = (Connector.IntegerArgument) args.get("port");
portArg.setValue(port);
RP.submit(() -> {
JPDADebugger.startListening(connector, args, services);
return null;
});
return Integer.toString(port);
}
}
}