Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 13 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/enso4igv.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
java: ["8"]
java: ["11"]

steps:
- uses: actions/checkout@v2
Expand Down
23 changes: 21 additions & 2 deletions tools/enso4igv/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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.31-SNAPSHOT</version>
<build>
<plugins>
<plugin>
Expand Down Expand Up @@ -72,6 +72,7 @@
<target>1.8</target>
<compilerArgs>
<arg>-Xlint:deprecation</arg>
<arg>-XDignore.symbol.file</arg>
</compilerArgs>
</configuration>
</plugin>
Expand Down Expand Up @@ -225,10 +226,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>
Copy link
Member Author

@JaroslavTulach JaroslavTulach Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While working on this code base, I am trying to make sure the code works as:

The biggest challenge is support for IGV as it is not being updated to run on latest NetBeans version too frequently.

However for this PR I need DialogDisplayer.notifyFuture (thank you, @sdedic) which is only available since NetBeans 14 - e.g. we have to upgrade.

Copy link
Member Author

@JaroslavTulach JaroslavTulach Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With oracle/graal#8290 the Enso IGV Plugin version 1.31.129 can be installed into IGV:

Installation

Copy link
Member Author

@JaroslavTulach JaroslavTulach Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the .nbm file is installed into IGV, one can press Shift-F6 inside of a .enso file and get the same behavior as in the VSCode extension:

Exec in IGV

<netbeans.compile.on.save>none</netbeans.compile.on.save>
</properties>
<profiles>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package org.enso.tools.enso4igv;

import com.sun.jdi.connect.Connector;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
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.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 b = ProcessBuilder.getLocal();
b.setExecutable(file.getPath());
b.setArguments(Arrays.asList("--run", script.getPath()));
b.setWorkingDirectory(script.getParent());
b.setRedirectErrorStream(true);

var env = b.getEnvironment();
var path = env.getVariable("PATH");
var java = JavaPlatform.getDefault().findTool("java");
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;
});
}

@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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be simpler to do just lookup(EnsoDataObject.class) != null, but that doesn't work currently. I needs apache/netbeans#7011 (comment) fix first which is going to be part of NetBeans 21 only. E.g. we need to keep this lookup(FileObject.class).getLookup().lookup(EnsoDataObject.class)) workaround.

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) {
Copy link
Member Author

@JaroslavTulach JaroslavTulach Feb 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got debugging working. Execute VSCode with empty directories:

$ code  --extensions-dir /tmp/ext --user-data-dir /tmp/usr

install the .vsix 1.31.138 extension and open some .enso file

Select java+ debugging

Stop on breakpoint

Right now the debugging seems to work when there is no workspace opened - opening root of Enso repository as workspace prevents the debugging to work. Still investigating how to fix that.

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);
}
}
}
Loading