configurable context for jshell: module and alternative JRE;

support jshell's addToClasspath() feature in communication protocol.
This commit is contained in:
Eugene Zhuravlev
2017-07-10 14:47:47 +02:00
parent 55deb53fdd
commit c457b017cd
10 changed files with 288 additions and 50 deletions
@@ -19,12 +19,13 @@ import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
@@ -65,7 +66,10 @@ class ExecuteJShellAction extends AnAction{
try {
JShellHandler handler = JShellHandler.getAssociatedHandler(vFile);
if (handler == null) {
handler = JShellHandler.create(project, vFile, e.getData(LangDataKeys.MODULE));
final SnippetEditorDecorator.ConfigurationPane config = SnippetEditorDecorator.getJShellConfiguration(e.getDataContext());
final Module module = config != null ? config.getContextModule() : null;
final Sdk sdk = config != null ? config.getRuntimeSdk() : null;
handler = JShellHandler.create(project, vFile, module, sdk);
}
if (handler != null) {
handler.toFront();
@@ -57,12 +57,11 @@ import java.awt.*;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashSet;
import java.util.*;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author Eugene Zhuravlev
@@ -84,6 +83,7 @@ public class JShellHandler {
private final MessageReader<Response> myMessageReader;
private final MessageWriter<Request> myMessageWriter;
private final ExecutorService myTaskQueue = new SequentialTaskExecutor("JShell Command Queue", PooledThreadExecutor.INSTANCE);
private final AtomicReference<Collection<String>> myEvalClasspathRef = new AtomicReference<>(null);
private JShellHandler(@NotNull Project project,
RunContentDescriptor descriptor,
@@ -145,14 +145,29 @@ public class JShellHandler {
return contentFile != null? contentFile.getUserData(MARKER_KEY) : null;
}
public static JShellHandler create(@NotNull final Project project, @NotNull final VirtualFile contentFile, @Nullable Module module) throws Exception{
final OSProcessHandler processHandler = launchProcess(project, module);
public static JShellHandler create(@NotNull final Project project,
@NotNull final VirtualFile contentFile,
@Nullable Module module,
@Nullable Sdk alternateSdk) throws Exception{
final OSProcessHandler processHandler = launchProcess(project, module, alternateSdk);
final String title = "JShell " + contentFile.getNameWithoutExtension();
final ConsoleViewImpl consoleView = new MyConsoleView(project);
final RunContentDescriptor descriptor = new RunContentDescriptor(consoleView, processHandler, new JPanel(new BorderLayout()), title);
final JShellHandler jshellHandler = new JShellHandler(project, descriptor, consoleView, contentFile, processHandler);
// init classpath for evaluation
final Set<String> cp = new LinkedHashSet<>();
final Computable<OrderEnumerator> orderEnumerator = module != null ? () -> ModuleRootManager.getInstance(module).orderEntries()
: () -> ProjectRootManager.getInstance(project).orderEntries();
ApplicationManager.getApplication().runReadAction(() -> {
cp.addAll(orderEnumerator.compute().librariesOnly().recursively().withoutSdk().getPathsList().getPathList());
});
if (!cp.isEmpty()) {
jshellHandler.myEvalClasspathRef.set(cp);
}
// must call getComponent before createConsoleActions()
final JComponent consoleViewComponent = consoleView.getComponent();
@@ -186,8 +201,12 @@ public class JShellHandler {
// todo: do we need to include project's compiled classes into the classpath or libraries only?
// todo: if we include project classes, make sure they are compiled
private static OSProcessHandler launchProcess(@NotNull Project project, @Nullable Module module) throws Exception{
final Sdk sdk = module != null? ModuleRootManager.getInstance(module).getSdk() : ProjectRootManager.getInstance(project).getProjectSdk();
private static OSProcessHandler launchProcess(@NotNull Project project,
@Nullable Module module,
@Nullable Sdk alternateSdk) throws Exception{
final Sdk sdk = alternateSdk != null? alternateSdk :
module != null? ModuleRootManager.getInstance(module).getSdk() :
ProjectRootManager.getInstance(project).getProjectSdk();
if (sdk == null || !(sdk.getSdkType() instanceof JavaSdkType)) {
throw new ExecException(
(sdk != null ? "Expected Java SDK" : " SDK is not configured") +
@@ -232,17 +251,41 @@ public class JShellHandler {
cmdLine.addParameter(launchCp.toString());
}
cmdLine.addParameter("com.intellij.execution.jshell.frontend.Main");
final Set<File> cp = new LinkedHashSet<>();
final Computable<OrderEnumerator> orderEnumerator = module != null ? () -> ModuleRootManager.getInstance(module).orderEntries()
: () -> ProjectRootManager.getInstance(project).orderEntries();
ApplicationManager.getApplication().runReadAction(() -> {
for (String s : orderEnumerator.compute().librariesOnly().recursively().withoutSdk().getPathsList().getPathList()) {
cp.add(new File(s));
}
});
cmdLine.addParameter("--class-path");
cmdLine.addParameter(StringUtil.join(cp, File.pathSeparator));
return new OSProcessHandler(cmdLine);
// init classpath for evaluation
//final Set<String> cp = new LinkedHashSet<>();
//final Computable<OrderEnumerator> orderEnumerator = module != null ? () -> ModuleRootManager.getInstance(module).orderEntries()
// : () -> ProjectRootManager.getInstance(project).orderEntries();
//ApplicationManager.getApplication().runReadAction(() -> {
// cp.addAll(orderEnumerator.compute().librariesOnly().recursively().withoutSdk().getPathsList().getPathList());
//});
//final File cpFile;
//if (!cp.isEmpty()) {
// cpFile = FileUtilRt.createTempFile("_jshell_classpath_", "", true);
// try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(cpFile), StandardCharsets.UTF_8))) {
// for (String path : cp) {
// writer.write(path);
// writer.newLine();
// }
// }
// cmdLine.addParameter("--@class-path");
// cmdLine.addParameter(cpFile.getAbsolutePath());
//}
//else {
// cpFile = null;
//}
final OSProcessHandler processHandler = new OSProcessHandler(cmdLine);
//if (cpFile != null) {
// processHandler.addProcessListener(new ProcessAdapter() {
// @Override
// public void processTerminated(ProcessEvent event) {
// FileUtil.delete(cpFile);
// }
// });
//}
return processHandler;
}
private static String findFrontEndLibrary() {
@@ -280,6 +323,13 @@ public class JShellHandler {
private Response sendInput(final Request request) {
final boolean alive = !myProcess.isProcessTerminating() && !myProcess.isProcessTerminated();
if (alive) {
// consume evaluation classpath, if any
final Collection<String> cp = myEvalClasspathRef.getAndSet(null);
if (cp != null) {
for (String path : cp) {
request.addClasspathItem(path);
}
}
myConsoleView.performWhenNoDeferredOutput(() -> {
try {
myMessageWriter.send(request);
@@ -19,9 +19,11 @@ import com.intellij.execution.console.ConsoleHistoryController;
import com.intellij.ide.scratch.ScratchFileService;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.vfs.VirtualFile;
/**
@@ -46,8 +48,18 @@ public class LaunchJShellConsoleAction extends AnAction{
);
assert contentFile != null;
try {
FileEditorManager.getInstance(project).openFile(contentFile, true);
JShellHandler.create(project, contentFile, e.getData(LangDataKeys.MODULE));
final FileEditor[] editors = FileEditorManager.getInstance(project).openFile(contentFile, true);
Sdk alternateSdk = null;
Module module = null;
for (FileEditor editor : editors) {
final SnippetEditorDecorator.ConfigurationPane config = SnippetEditorDecorator.getJShellConfiguration(editor);
if (config != null) {
alternateSdk = config.getRuntimeSdk();
module = config.getContextModule();
break;
}
}
JShellHandler.create(project, contentFile, module, alternateSdk);
}
catch (Exception ex) {
JShellDiagnostic.notifyError(ex, project);
@@ -15,48 +15,166 @@
*/
package com.intellij.execution.jshell;
import com.intellij.ProjectTopics;
import com.intellij.application.options.ModulesComboBox;
import com.intellij.execution.ui.ConfigurationModuleSelector;
import com.intellij.execution.ui.DefaultJreSelector;
import com.intellij.execution.ui.JrePathEditor;
import com.intellij.ide.scratch.RootType;
import com.intellij.ide.scratch.ScratchFileService;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.editor.impl.EditorHeaderComponent;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.ModuleListener;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.LabeledComponent;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.EditorNotifications;
import com.intellij.util.Alarm;
import com.intellij.util.Function;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.JBUI;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.List;
/**
* @author Eugene Zhuravlev
* Date: 06-Jun-17
*/
public class SnippetEditorDecorator extends EditorNotifications.Provider<JComponent>{
public static final Key<JComponent> CONTEXT_KEY = Key.create("jshell.editor.toolbar");
public class SnippetEditorDecorator extends EditorNotifications.Provider<SnippetEditorDecorator.ConfigurationPane>{
public static final Key<ConfigurationPane> CONTEXT_KEY = Key.create("jshell.editor.toolbar");
private final Project myProject;
public SnippetEditorDecorator(Project project) {
myProject = project;
}
public static class ConfigurationPane extends EditorHeaderComponent {
private final Alarm myUpdateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
private final JrePathEditor myJreEditor;
private final ConfigurationModuleSelector myModuleSelector;
private MessageBusConnection myBusConnection;
ConfigurationPane(Project project) {
final DefaultActionGroup actions = new DefaultActionGroup(ExecuteJShellAction.getSharedInstance(), DropJShellStateAction.getSharedInstance());
final ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar("JShellSnippetEditor", actions, true);
myJreEditor = new JrePathEditor(DefaultJreSelector.projectSdk(project));
myJreEditor.setToolTipText("Alternative JRE to run JShell");
myJreEditor.setPathOrName(null, true);
LabeledComponent<ModulesComboBox> modulePane = new LabeledComponent<>();
ModulesComboBox modulesCombo = new ModulesComboBox();
modulePane.setComponent(modulesCombo);
modulePane.setLabelLocation(BorderLayout.WEST);
modulePane.setText("Use classpath of:");
myModuleSelector = new ConfigurationModuleSelector(project, modulesCombo, "<whole project>");
JPanel mainPane = new JPanel(new GridBagLayout());
mainPane.add(toolbar.getComponent(), new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, JBUI.insets(2, 3, 0, 0), 0, 0));
mainPane.add(modulePane, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, JBUI.insets(2, 3, 0, 0), 0, 0));
mainPane.add(myJreEditor, new GridBagConstraints(2, 0, 1, 1, 1.0, 0.0, GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, JBUI.insets(2, 15, 0, 0), 0, 0));
add(mainPane, BorderLayout.CENTER);
}
@Override
public void addNotify() {
super.addNotify();
myBusConnection = myModuleSelector.getProject().getMessageBus().connect();
myBusConnection.subscribe(ProjectTopics.MODULES, new ModuleListener() {
@Override
public void moduleAdded(@NotNull Project project, @NotNull Module module) {
reloadModules();
}
@Override
public void moduleRemoved(@NotNull Project project, @NotNull Module module) {
reloadModules();
}
@Override
public void modulesRenamed(@NotNull Project project, @NotNull List<Module> modules, @NotNull Function<Module, String> oldNameProvider) {
reloadModules();
}
});
reloadModules();
}
@Override
public void removeNotify() {
super.removeNotify();
final MessageBusConnection conn = myBusConnection;
if (conn != null) {
myBusConnection = null;
conn.disconnect();
myUpdateAlarm.cancelAllRequests();
}
}
private void reloadModules() {
myUpdateAlarm.cancelAllRequests();
myUpdateAlarm.addRequest(()->myModuleSelector.reset(), 300L);
}
@Nullable
public Module getContextModule() {
return myModuleSelector.getModule();
}
@Nullable
public Sdk getRuntimeSdk() {
final String pathOrName = myJreEditor.getJrePathOrName();
if (pathOrName != null) {
final JavaSdk javaSdkType = JavaSdk.getInstance();
final ProjectJdkTable jdkTable = ProjectJdkTable.getInstance();
final Sdk sdkByName = jdkTable.findJdk(pathOrName, javaSdkType.getName());
if (sdkByName != null) {
return sdkByName;
}
// assuming we have sdk home path
for (Sdk sdk : jdkTable.getSdksOfType(javaSdkType)) {
if (FileUtil.pathsEqual(pathOrName, sdk.getHomePath())) {
return sdk;
}
}
}
return null;
}
}
@NotNull
@Override
public Key<JComponent> getKey() {
public Key<ConfigurationPane> getKey() {
return CONTEXT_KEY;
}
@Nullable
@Override
public JComponent createNotificationPanel(@NotNull VirtualFile file, @NotNull FileEditor fileEditor) {
public ConfigurationPane createNotificationPanel(@NotNull VirtualFile file, @NotNull FileEditor fileEditor) {
final RootType root = ScratchFileService.getInstance().getRootType(file);
if ((root instanceof JShellRootType)) {
final DefaultActionGroup actions = new DefaultActionGroup(ExecuteJShellAction.getSharedInstance(), DropJShellStateAction.getSharedInstance());
final ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar("JShellSnippetEditor", actions, true);
final EditorHeaderComponent header = new EditorHeaderComponent();
header.add(toolbar.getComponent());
return header;
if (!(root instanceof JShellRootType)) {
return null;
}
return new ConfigurationPane(myProject);
}
return null;
@Nullable
public static ConfigurationPane getJShellConfiguration(DataContext context) {
return getJShellConfiguration(PlatformDataKeys.FILE_EDITOR.getData(context));
}
@Nullable
public static ConfigurationPane getJShellConfiguration(FileEditor fileEditor) {
return CONTEXT_KEY.get(fileEditor);
}
}
@@ -104,12 +104,7 @@ public class ConfigurationModuleSelector {
}
public void reset(final ModuleBasedConfiguration configuration) {
final Module[] modules = ModuleManager.getInstance(getProject()).getModules();
final List<Module> list = new ArrayList<>();
for (final Module module : modules) {
if (isModuleAccepted(module)) list.add(module);
}
setModules(list);
reset();
if (myModulesList != null) {
myModulesList.setSelectedItem(configuration.getConfigurationModule().getModule());
}
@@ -118,6 +113,17 @@ public class ConfigurationModuleSelector {
}
}
public void reset() {
final Module[] modules = ModuleManager.getInstance(getProject()).getModules();
final List<Module> list = new ArrayList<>();
for (final Module module : modules) {
if (isModuleAccepted(module)) {
list.add(module);
}
}
setModules(list);
}
public boolean isModuleAccepted(final Module module) {
return ModuleTypeManager.getInstance().isClasspathProvider(ModuleType.get(module));
}
@@ -3,9 +3,8 @@ package com.intellij.execution.jshell.frontend;
import com.intellij.execution.jshell.protocol.*;
import jdk.jshell.*;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -20,6 +19,7 @@ import java.util.function.Consumer;
*/
public class Main {
private static final String ARG_CLASSPATH = "--class-path";
private static final String ARG_CLASSPATH_FILE = "--@class-path";
private static final Consumer<String> NULL_CONSUMER = s -> {};
//private static Request createTestRequest() {
@@ -47,6 +47,14 @@ public class Main {
response.setUid(request.getUid());
try {
// first, handle eval classpath if any
final List<String> cp = request.getClassPath();
if (cp != null && !cp.isEmpty()) {
for (String path : cp) {
shell.addToClasspath(path);
}
}
if (command == Request.Command.DROP_STATE) {
shell.snippets().forEach(snippet -> exportEvents(shell, shell.drop(snippet), response));
}
@@ -124,13 +132,17 @@ public class Main {
}
}
private static void configureJShell(String[] args, JShell shell) {
private static void configureJShell(String[] args, JShell shell) throws IOException {
// todo: add more parameters if needed
boolean cpFound = false;
boolean cpFileFound = false;
for (String arg : args) {
if (ARG_CLASSPATH.equals(arg)) {
cpFound = true;
}
else if (ARG_CLASSPATH_FILE.equals(arg)) {
cpFileFound = true;
}
else {
if (cpFound) {
cpFound = false;
@@ -138,6 +150,15 @@ public class Main {
shell.addToClasspath(path);
}
}
else if (cpFileFound) {
cpFileFound = false;
final File cpFile = new File(arg);
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(cpFile), StandardCharsets.UTF_8))) {
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
shell.addToClasspath(line);
}
}
}
}
}
}
@@ -3,6 +3,8 @@ package com.intellij.execution.jshell.protocol;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlEnum;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.ArrayList;
import java.util.List;
/**
* @author Eugene Zhuravlev
@@ -12,6 +14,7 @@ import javax.xml.bind.annotation.XmlRootElement;
public class Request extends Message{
private Command myCommand;
private String myCodeText;
private List<String> myClassPath;
@XmlEnum
public enum Command{
@@ -44,4 +47,22 @@ public class Request extends Message{
public void setCodeText(String codeText) {
myCodeText = codeText;
}
public List<String> getClassPath() {
return myClassPath;
}
@XmlElement(name = "cp")
public void setClassPath(List<String> classPath) {
myClassPath = classPath;
}
public void addClasspathItem(String path) {
List<String> cp = myClassPath;
if (cp == null) {
cp = new ArrayList<>();
myClassPath = cp;
}
cp.add(path);
}
}
@@ -23,7 +23,6 @@ public class Response extends Message{
Collections.addAll(myEvents = new ArrayList<>(), events);
}
@XmlElement
public List<Event> getEvents() {
return myEvents;
}
@@ -4,6 +4,7 @@ import junit.framework.TestCase;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.List;
import java.util.UUID;
/**
@@ -28,10 +29,16 @@ public class JShellMessageMarshallingTest extends TestCase {
final Request request = new Request(UUID.randomUUID().toString(), Request.Command.EVAL,
"System.out.println(\"Hello, World!\");\n int var = 7 + 7;");
request.addClasspathItem("C:/work/path1");
request.addClasspathItem("C:/work/path2");
final List<String> requestClasspath = request.getClassPath();
clientWriter.send(request);
final Request receivedRequest = serverReader.receive(s -> {});
assertEquals(request.getUid(), receivedRequest.getUid());
assertEquals(request.getCodeText(), receivedRequest.getCodeText());
final List<String> receivedClasspath = receivedRequest.getClassPath();
assertEquals(requestClasspath, receivedClasspath);
final CodeSnippet snippet = new CodeSnippet("code-snippet-id", CodeSnippet.Kind.EXPRESSION, CodeSnippet.SubKind.OTHER_EXPRESSION_SUBKIND, "a+b", "expression:a+b");
final Event event1 = new Event(null, null, CodeSnippet.Status.UNKNOWN, CodeSnippet.Status.NONEXISTENT, null);
Binary file not shown.