diff --git a/python/ipnb/README.md b/python/ipnb/README.md new file mode 100644 index 000000000000..79f04e780b7d --- /dev/null +++ b/python/ipnb/README.md @@ -0,0 +1,4 @@ +intellij-ipnb +============= + +IPython notebook support in IntelliJ diff --git a/python/ipnb/ipnb.iml b/python/ipnb/ipnb.iml new file mode 100644 index 000000000000..dfebdfc0e6df --- /dev/null +++ b/python/ipnb/ipnb.iml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/ipnb/ipynb_json_structure b/python/ipnb/ipynb_json_structure new file mode 100644 index 000000000000..7991a873484c --- /dev/null +++ b/python/ipnb/ipynb_json_structure @@ -0,0 +1,35 @@ +There are 4 possible cell types: +- Markdown +- Heading +- Raw +- Code + +Markdown cell type consists of: +- metadata +- Source <- List of Strings + +Heading cell type consists of: +- metadata +- Source <- List of Strings +- level <- int + +Raw cell type consists of: +- metadata + +Code cell type consists of: +- metadata +- collapsed <- Boolean +- input <- List of Strings +- language <- String +- prompt_number <- int +- outputs <- List of Outputs + +Output of Code cell consists of: +- metadata +- output_type <- String +- text <- String +- one of [png, latex, stream, ..] <- String +- prompt_number <- int *present only with latex field + + +Cells are packed into worksheets. diff --git a/python/ipnb/lib/java_websocket.jar b/python/ipnb/lib/java_websocket.jar new file mode 100644 index 000000000000..c9cc0cfca55e Binary files /dev/null and b/python/ipnb/lib/java_websocket.jar differ diff --git a/python/ipnb/python/requirements.txt b/python/ipnb/python/requirements.txt new file mode 100644 index 000000000000..02ec117e150a --- /dev/null +++ b/python/ipnb/python/requirements.txt @@ -0,0 +1 @@ +pyzmq diff --git a/python/ipnb/python/test_ipnb_zmq.py b/python/ipnb/python/test_ipnb_zmq.py new file mode 100644 index 000000000000..c60be52e6e15 --- /dev/null +++ b/python/ipnb/python/test_ipnb_zmq.py @@ -0,0 +1,11 @@ +"""Tests of interactions with IPython kernel via 0MQ.""" + + +from unittest import TestCase + +import zmq + + +class ZMQVersionTest(TestCase): + def test_version_available(self): + self.assertIsNotNone(zmq.pyzmq_version()) diff --git a/python/ipnb/resources/META-INF/plugin.xml b/python/ipnb/resources/META-INF/plugin.xml new file mode 100644 index 000000000000..286aeb7e63dd --- /dev/null +++ b/python/ipnb/resources/META-INF/plugin.xml @@ -0,0 +1,43 @@ + + IPython Notebook + org.jetbrains.plugins.ipnb + 0.1 + JetBrains + IPython Notebook support + com.intellij.modules.platform + Pythonid + com.intellij.modules.python + + + + org.jetbrains.plugins.ipnb.configuration.IpnbConnectionManager + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbFileType.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbFileType.java new file mode 100644 index 000000000000..1bd66bc41d99 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbFileType.java @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.ipnb; + +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.vfs.CharsetToolkit; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.PlatformIcons; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +public class IpnbFileType implements FileType { + + public static final IpnbFileType INSTANCE = new IpnbFileType(); + + @NonNls + public static final String DEFAULT_EXTENSION = "ipynb"; + + @NotNull + public String getName() { + return "IPNB"; + } + + @NotNull + public String getDescription() { + return "IPython Notebook"; + } + + @NotNull + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + public Icon getIcon() { + return PlatformIcons.UI_FORM_ICON; + } //TODO + + public boolean isBinary() { + return false; + } + + public boolean isReadOnly() { + return false; + } + + public String getCharset(@NotNull VirtualFile file, @NotNull final byte[] content) { + return CharsetToolkit.UTF8; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbFileTypeFactory.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbFileTypeFactory.java new file mode 100644 index 000000000000..9c7e9c36a111 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbFileTypeFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.ipnb; + +import com.intellij.openapi.fileTypes.FileTypeConsumer; +import com.intellij.openapi.fileTypes.FileTypeFactory; +import org.jetbrains.annotations.NotNull; + +/** + * @author traff + */ +public class IpnbFileTypeFactory extends FileTypeFactory { + + public void createFileTypes(@NotNull FileTypeConsumer consumer) { + consumer.consume(IpnbFileType.INSTANCE); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbUtils.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbUtils.java new file mode 100644 index 000000000000..db7bc6190915 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/IpnbUtils.java @@ -0,0 +1,231 @@ +package org.jetbrains.plugins.ipnb; + +import com.intellij.ide.BrowserUtil; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.ArrayUtil; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.text.MarkdownUtil; +import com.petebevin.markdown.MarkdownProcessor; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.concurrent.Worker; +import javafx.embed.swing.JFXPanel; +import javafx.scene.Scene; +import javafx.scene.layout.BorderPane; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebView; +import netscape.javascript.JSException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +public class IpnbUtils { + private static final Logger LOG = Logger.getInstance(IpnbUtils.class); + private static MarkdownProcessor ourMarkdownProcessor = new MarkdownProcessor(); + + private static final String ourPrefix = "
"; + + public static String markdown2Html(@NotNull final String description) { + // TODO: add links to the dependant notebook pages (see: index.ipynb) + //TODO: relative picture links (see: index.ipynb in IPython.kernel) We should use absolute file:/// path + final List lines = ContainerUtil.newArrayList(description.split("\n|\r|\r\n")); + final List processedLines = new ArrayList(); + boolean isInCode = false; + for (String line : lines) { + String processedLine = line; + if (line.startsWith(" ")) { + processedLine = line.substring(1); + } + + if (processedLine.contains("```")) isInCode = !isInCode; + if (isInCode) { + processedLine = processedLine + .replace("&", "&"); + } + else { + processedLine = processedLine + .replaceAll("([\\w])_([\\w])", "$1&underline;$2"); + } + processedLines.add(processedLine); + } + MarkdownUtil.replaceHeaders(processedLines); + MarkdownUtil.removeImages(processedLines); + MarkdownUtil.generateLists(processedLines); + MarkdownUtil.replaceCodeBlock(processedLines); + final String[] lineArray = ArrayUtil.toStringArray(processedLines); + final String normalizedMarkdown = StringUtil.join(lineArray, "\n"); + String html = ourMarkdownProcessor.markdown(normalizedMarkdown); + html = html + .replace("
", "
").replace("
", "
") + .replace("", "").replace("", "") + .replace("", "").replace("", "") + .replace("&underline;", "_") + .trim(); + return html; + } + + public static void addLatexToPanel(@NotNull final String source, @NotNull final JPanel panel) { + final StringBuilder result = convertToHtml(source); + addToPanel(result.toString(), panel); + + } + + private static StringBuilder convertToHtml(@NotNull final String source) { + final StringBuilder result = new StringBuilder(); + StringBuilder markdown = new StringBuilder(); + boolean inCode = false; + int inMultiStringCode = 0; + boolean inEnd = false; + boolean escaped = false; + boolean backQuoted = false; + + for (int i = 0; i != source.length(); ++i) { + final char charAt = source.charAt(i); + + if (charAt == '`') { + backQuoted = !backQuoted; + if (source.length() > i + 2 && source.charAt(i + 1) == '`' && source.charAt(i + 2) == '`') { + markdown.append(escaped ? "" : "
");
+          escaped = !escaped;
+          i = i + 2;
+          continue;
+        }
+      }
+
+      if (!escaped && !backQuoted) {
+        if (charAt == '$' && source.length() > i + 1 && source.charAt(i+1) != '$') {
+          inCode = !inCode;
+        }
+
+        if (charAt == '\\' && source.substring(i).startsWith("\\begin")) {
+          inMultiStringCode += 1;
+        }
+        if (charAt == '\\' && source.substring(i).startsWith("\\end")) {
+          inEnd = true;
+        }
+      }
+
+      final boolean doubleDollar = charAt == '$' && ((source.length() > i + 1 && source.charAt(i + 1) == '$')
+                                                     || (i >= 1  && source.charAt(i - 1) == '$'));
+      if (inCode || inMultiStringCode != 0 || (!backQuoted && doubleDollar)) {
+        if (markdown.length() != 0) {
+          result.append(markdown2Html(markdown.toString()));
+          markdown = new StringBuilder();
+        }
+
+        result.append(charAt);
+      }
+      else {
+        markdown.append(charAt);
+      }
+
+      if (inEnd && charAt == '}') {
+        inMultiStringCode -= 1;
+      }
+
+    }
+    if (markdown.length() != 0) {
+      result.append(markdown2Html(markdown.toString()));
+    }
+    return result;
+  }
+
+  public static void addToPanel(@NotNull final String source, @NotNull final JPanel panel) {
+    Platform.setImplicitExit(false);
+
+    final String text = ourPrefix + source + "
"; + + final JFXPanel javafxPanel = new JFXPanel(); + javafxPanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + final MouseEvent parentEvent = SwingUtilities.convertMouseEvent(javafxPanel, e, panel); + panel.dispatchEvent(parentEvent); + } + }); + Platform.runLater(new Runnable() { + @Override + public void run() { + final BorderPane borderPane = new BorderPane(); + final WebView webComponent = new WebView(); + webComponent.setPrefWidth(IpnbEditorUtil.PANEL_WIDTH + 100); + webComponent.setPrefHeight(5); + final WebEngine engine = webComponent.getEngine(); + engine.locationProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue value, String newValue, String t1) { + try { + final URI address = new URI(value.getValue()); + BrowserUtil.browse(address); + } + catch (URISyntaxException e) { + LOG.warn(e); + } + } + }); + engine.getLoadWorker().stateProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue arg0, Worker.State oldState, Worker.State newState) { + if (newState == Worker.State.SUCCEEDED) { + adjustHeight(webComponent, javafxPanel, panel); + } + } + }); + engine.loadContent(text); + borderPane.setCenter(webComponent); + final Scene scene = new Scene(borderPane); + javafxPanel.setScene(scene); + } + }); + panel.add(javafxPanel); + } + + private static void adjustHeight(final WebView webComponent, final JFXPanel javafxPanel, final JPanel panel) { + Platform.runLater(new Runnable() { + @Override + public void run() { + try { + Object result = webComponent.getEngine().executeScript("document.getElementById(\"mydiv\").offsetHeight"); + if (result instanceof Integer) { + final double value = ((Integer)result + 150); + + javafxPanel.setPreferredSize(new Dimension((int)webComponent.getPrefWidth(), (int)value)); + panel.revalidate(); + panel.repaint(); + } + } + catch (JSException e) { + LOG.warn(e.getMessage()); + } + } + }); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbConfigurable.form b/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbConfigurable.form new file mode 100644 index 000000000000..5f6842a4a839 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbConfigurable.form @@ -0,0 +1,37 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbConfigurable.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbConfigurable.java new file mode 100644 index 000000000000..13b15180d83a --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbConfigurable.java @@ -0,0 +1,69 @@ +package org.jetbrains.plugins.ipnb.configuration; + +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.options.SearchableConfigurable; +import com.intellij.openapi.project.Project; +import com.intellij.ui.components.JBTextField; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +public class IpnbConfigurable implements SearchableConfigurable { + private JPanel myMainPanel; + private JBTextField myFieldUrl; + @NotNull private final Project myProject; + + public IpnbConfigurable(@NotNull Project project) { + myProject = project; + myFieldUrl.setText(IpnbSettings.getInstance(myProject).getURL()); + } + + @Nls + @Override + public String getDisplayName() { + return "IPython Notebook"; + } + + @Override + public String getHelpTopic() { + return "reference-ipnb"; + } + + @Override + public JComponent createComponent() { + return myMainPanel; + } + + @Override + public boolean isModified() { + final String oldUrl = IpnbSettings.getInstance(myProject).getURL(); + final String url = myFieldUrl.getText(); + return !url.equals(oldUrl); + } + + @Override + public void apply() throws ConfigurationException { + IpnbSettings.getInstance(myProject).setURL(myFieldUrl.getText()); + } + + @Override + public void reset() { + } + + @Override + public void disposeUIResources() { + } + + @NotNull + @Override + public String getId() { + return "IpnbConfigurable"; + } + + @Override + public Runnable enableSearch(String option) { + return null; + } +} + diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbConnectionManager.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbConnectionManager.java new file mode 100644 index 000000000000..030d4b6a54b6 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbConnectionManager.java @@ -0,0 +1,129 @@ +package org.jetbrains.plugins.ipnb.configuration; + +import com.intellij.openapi.components.ProjectComponent; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.MessageType; +import com.intellij.openapi.ui.popup.Balloon; +import com.intellij.openapi.ui.popup.BalloonBuilder; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor; +import org.jetbrains.plugins.ipnb.editor.panels.code.IpnbCodePanel; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbOutputCell; +import org.jetbrains.plugins.ipnb.protocol.IpnbConnection; +import org.jetbrains.plugins.ipnb.protocol.IpnbConnectionListenerBase; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class IpnbConnectionManager implements ProjectComponent { + private static final Logger LOG = Logger.getInstance(IpnbConnectionManager.class); + private final Project myProject; + private Map myKernels = new HashMap(); + private Map myUpdateMap = new HashMap(); + + public IpnbConnectionManager(final Project project) { + myProject = project; + } + + public static IpnbConnectionManager getInstance(Project project) { + return project.getComponent(IpnbConnectionManager.class); + } + + public void executeCell(@NotNull final IpnbCodePanel codePanel) { + final IpnbFileEditor fileEditor = codePanel.getFileEditor(); + final String path = fileEditor.getVirtualFile().getPath(); + if (!myKernels.containsKey(path)) { + try { + final String url = IpnbSettings.getInstance(myProject).getURL(); + if (StringUtil.isEmptyOrSpaces(url)) { + BalloonBuilder balloonBuilder = JBPopupFactory.getInstance().createHtmlTextBalloonBuilder( + "Please, specify IPython Notebook URL in Settings->IPython Notebook", null, MessageType.WARNING.getPopupBackground(), null); + final Balloon balloon = balloonBuilder.createBalloon(); + balloon.showInCenterOf(fileEditor.getRunCellButton()); + return; + } + final IpnbConnection connection = new IpnbConnection(new URI(url), new IpnbConnectionListenerBase() { + @Override + public void onOpen(@NotNull IpnbConnection connection) { + final String messageId = connection.execute(codePanel.getCell().getSourceAsString()); + myUpdateMap.put(messageId, codePanel); + } + + @Override + public void onOutput(@NotNull IpnbConnection connection, + @NotNull String parentMessageId, + @NotNull List outputs, + @Nullable Integer execCount) { + if (!myUpdateMap.containsKey(parentMessageId)) return; + final IpnbCodePanel cell = myUpdateMap.remove(parentMessageId); + cell.getCell().setPromptNumber(execCount); + cell.updatePanel(outputs); + } + }); + myKernels.put(path, connection); + } + catch (IOException e) { + BalloonBuilder balloonBuilder = JBPopupFactory.getInstance().createHtmlTextBalloonBuilder( + "Please, check that IPython Notebook is running", null, MessageType.WARNING.getPopupBackground(), null); + final Balloon balloon = balloonBuilder.createBalloon(); + balloon.showInCenterOf(fileEditor.getRunCellButton()); + } + catch (URISyntaxException e) { + BalloonBuilder balloonBuilder = JBPopupFactory.getInstance().createHtmlTextBalloonBuilder( + "Please, check IPython Notebook URL in Settings->IPython Notebook", null, MessageType.WARNING.getPopupBackground(), null); + final Balloon balloon = balloonBuilder.createBalloon(); + balloon.showInCenterOf(fileEditor.getRunCellButton()); + } + } + else { + final IpnbConnection connection = myKernels.get(path); + if (connection != null) { + final String messageId = connection.execute(codePanel.getCell().getSourceAsString()); + myUpdateMap.put(messageId, codePanel); + } + } + } + + public void projectOpened() {} + + + public void projectClosed() { + shutdownKernels(); + } + + private void shutdownKernels() { + for (IpnbConnection connection : myKernels.values()) { + connection.shutdown(); + try { + connection.close(); + } + catch (IOException e) { + LOG.error(e); + } + catch (InterruptedException e) { + LOG.error(e); + } + } + myKernels.clear(); + } + + @NotNull + public String getComponentName() { + return "IpnbConnectionManager"; + } + + public void initComponent() { + } + + public void disposeComponent() { + shutdownKernels(); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbSettings.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbSettings.java new file mode 100644 index 000000000000..c8e182bc2b2a --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/configuration/IpnbSettings.java @@ -0,0 +1,38 @@ +package org.jetbrains.plugins.ipnb.configuration; + +import com.intellij.openapi.components.*; +import com.intellij.openapi.project.Project; +import com.intellij.util.xmlb.XmlSerializerUtil; +import com.intellij.util.xmlb.annotations.Transient; +import org.jetbrains.annotations.NotNull; + +@State(name = "IpnbSettings", + storages = {@Storage(file = StoragePathMacros.PROJECT_FILE)} +) +public class IpnbSettings implements PersistentStateComponent { + public String URL = ""; + + public static IpnbSettings getInstance(@NotNull Project project) { + return ServiceManager.getService(project, IpnbSettings.class); + } + + public void setURL(@NotNull final String url) { + URL = url; + } + + @Transient + public String getURL() { + return URL; + } + + @Override + public IpnbSettings getState() { + return this; + } + + @Override + public void loadState(IpnbSettings state) { + XmlSerializerUtil.copyBean(state, this); + } + +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbEditorProvider.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbEditorProvider.java new file mode 100644 index 000000000000..6b17ce0c1bc7 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbEditorProvider.java @@ -0,0 +1,72 @@ +package org.jetbrains.plugins.ipnb.editor; + +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorPolicy; +import com.intellij.openapi.fileEditor.FileEditorProvider; +import com.intellij.openapi.fileEditor.FileEditorState; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.vfs.VirtualFile; +import org.jdom.Element; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.IpnbFileType; + +/** + * @author traff + */ +public class IpnbEditorProvider implements FileEditorProvider, DumbAware { + @NonNls private static final String SELECTED_CELL = "selected"; + @NonNls private static final String ID = "id"; + @NonNls private static final String TOP = "top"; + + @Override + public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { + return file.getFileType() == IpnbFileType.INSTANCE; + } + + @NotNull + @Override + public FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { + return new IpnbFileEditor(project, file); + } + + @Override + public void disposeEditor(@NotNull FileEditor editor) { + Disposer.dispose(editor); + } + + @NotNull + @Override + public FileEditorState readState(@NotNull Element sourceElement, @NotNull Project project, @NotNull VirtualFile file) { + final IpnbEditorState state = new IpnbEditorState(-1, 0, 0); + final Element child = sourceElement.getChild(SELECTED_CELL); + state.setSelectedIndex(child == null ? 0 : Integer.parseInt(child.getAttributeValue(ID))); + state.setSelectedTop(child == null ? 0 : Integer.parseInt(child.getAttributeValue(TOP))); + return state; + } + + @Override + public void writeState(@NotNull FileEditorState state, @NotNull Project project, @NotNull Element targetElement) { + IpnbEditorState editorState = (IpnbEditorState)state; + final int id = editorState.getSelectedIndex(); + final int location = editorState.getSelectedTop(); + final Element element = new Element(SELECTED_CELL); + element.setAttribute(ID, String.valueOf(id)); + element.setAttribute(TOP, String.valueOf(location)); + targetElement.addContent(element); + } + + @NotNull + @Override + public String getEditorTypeId() { + return "ipnb-editor"; + } + + @NotNull + @Override + public FileEditorPolicy getPolicy() { + return FileEditorPolicy.PLACE_BEFORE_DEFAULT_EDITOR; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbEditorState.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbEditorState.java new file mode 100644 index 000000000000..fd771c618d80 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbEditorState.java @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.ipnb.editor; + +import com.intellij.openapi.fileEditor.FileEditorState; +import com.intellij.openapi.fileEditor.FileEditorStateLevel; + +final class IpnbEditorState implements FileEditorState{ + private final transient long myDocumentModificationStamp; // should not be serialized + private int mySelectedIndex; + private int mySelectedTop; + + public IpnbEditorState(final long modificationStamp, int selectedComponentIndex, int top) { + myDocumentModificationStamp = modificationStamp; + mySelectedIndex = selectedComponentIndex; + mySelectedTop = top; + } + + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof IpnbEditorState)) return false; + + final IpnbEditorState state = (IpnbEditorState)o; + + return myDocumentModificationStamp == state.myDocumentModificationStamp && mySelectedIndex == state.mySelectedIndex && + mySelectedTop == state.mySelectedTop; + } + + public int getSelectedTop() { + return mySelectedTop; + } + + public void setSelectedTop(int selectedTop) { + mySelectedTop = selectedTop; + } + + public void setSelectedIndex(int selectedIndex) { + mySelectedIndex = selectedIndex; + } + + public int getSelectedIndex() { + return mySelectedIndex; + } + + public int hashCode(){ + return (int)(myDocumentModificationStamp ^ (myDocumentModificationStamp >>> 32)); + } + + public boolean canBeMergedWith(FileEditorState otherState, FileEditorStateLevel level) { + return otherState instanceof IpnbEditorState; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbEditorUtil.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbEditorUtil.java new file mode 100644 index 000000000000..28797477f363 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbEditorUtil.java @@ -0,0 +1,121 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.ipnb.editor; + +import com.google.common.collect.Lists; +import com.intellij.execution.impl.ConsoleViewUtil; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.ui.Gray; +import com.intellij.ui.JBColor; +import com.intellij.util.ui.UIUtil; +import com.jetbrains.python.PythonFileType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.panels.code.IpnbCodeSourcePanel; +import org.jetbrains.plugins.ipnb.psi.IpnbPyFragment; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseWheelListener; +import java.util.List; + +/** + * @author traff + */ +public class IpnbEditorUtil { + public enum PromptType { In, Out, None } + + public static Dimension PROMPT_SIZE = new Dimension(80, 30); + public static int PANEL_WIDTH = 900; + + public static Editor createPythonCodeEditor(@NotNull final Project project, @NotNull final IpnbCodeSourcePanel codeSourcePanel) { + final EditorFactory editorFactory = EditorFactory.getInstance(); + assert editorFactory != null; + final Document document = createPythonCodeDocument(project, codeSourcePanel); + assert document != null; + EditorEx editor = (EditorEx)editorFactory.createEditor(document, project, PythonFileType.INSTANCE, false); + + setupEditor(editor); + return editor; + } + + private static void setupEditor(@NotNull final EditorEx editor) { + if (!UIUtil.isUnderDarcula()) + editor.setBackgroundColor(Gray._247); + noScrolling(editor); + editor.getScrollPane().setBorder(null); + ConsoleViewUtil.setupConsoleEditor(editor, false, false); + } + + public static Editor createPlainCodeEditor(@NotNull final Project project, @NotNull final String text) { + final EditorFactory editorFactory = EditorFactory.getInstance(); + assert editorFactory != null; + final Document document = editorFactory.createDocument(text); + EditorEx editor = (EditorEx)editorFactory.createEditor(document, project); + setupEditor(editor); + return editor; + } + + private static void noScrolling(EditorEx editor) { + editor.getScrollPane().setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); + editor.getScrollPane().setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + editor.getScrollPane().setWheelScrollingEnabled(false); + + List listeners = Lists.newArrayList(editor.getScrollPane().getMouseWheelListeners()); + for (MouseWheelListener l : listeners) { + editor.getScrollPane().removeMouseWheelListener(l); + } + } + + public static Document createPythonCodeDocument(@NotNull final Project project, @NotNull IpnbCodeSourcePanel codeSourcePanel) { + final String text = codeSourcePanel.getCell().getSourceAsString().trim(); + final IpnbPyFragment fragment = new IpnbPyFragment(project, text, true, codeSourcePanel); + + return PsiDocumentManager.getInstance(project).getDocument(fragment); + } + + public static JComponent createPromptComponent(Integer promptNumber, @NotNull final PromptType type) { + final String promptText = prompt(promptNumber, type); + JLabel promptLabel = new JLabel(promptText); + promptLabel.setHorizontalAlignment(SwingConstants.RIGHT); + promptLabel.setPreferredSize(PROMPT_SIZE); + final Font font = promptLabel.getFont(); + assert font != null; + promptLabel.setFont(font.deriveFont(Font.BOLD)); + final JBColor darkRed = new JBColor(new Color(210, 30, 50), new Color(210, 30, 50)); + promptLabel.setForeground(type == PromptType.In ? JBColor.BLUE : darkRed); + promptLabel.setBackground(getBackground()); + return promptLabel; + } + + protected static String prompt(Integer promptNumber, @NotNull final PromptType type) { + if (type == PromptType.In) + return promptNumber == null ? type + " [ ]:" : promptNumber > 0 ? String.format(type + " [%d]:", promptNumber) : type + " [*]:"; + else if (type == PromptType.Out) + return promptNumber == null ? type + "[ ]:" : promptNumber > 0 ? String.format(type + "[%d]:", promptNumber) : type + "[*]:"; + return ""; + } + + + public static Color getBackground() { + return EditorColorsManager.getInstance().getGlobalScheme().getDefaultBackground(); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbFileEditor.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbFileEditor.java new file mode 100644 index 000000000000..946d6c665a70 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/IpnbFileEditor.java @@ -0,0 +1,447 @@ +package org.jetbrains.plugins.ipnb.editor; + +import com.google.common.collect.Lists; +import com.intellij.AppTopics; +import com.intellij.codeHighlighting.BackgroundEditorHighlighter; +import com.intellij.icons.AllIcons; +import com.intellij.ide.structureView.StructureViewBuilder; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.*; +import com.intellij.openapi.fileEditor.impl.FileEditorProviderManagerImpl; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComboBox; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.UserDataHolderBase; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.pom.Navigatable; +import com.intellij.ui.JBColor; +import com.intellij.ui.ScrollPaneFactory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.ipnb.editor.actions.*; +import org.jetbrains.plugins.ipnb.editor.panels.*; +import org.jetbrains.plugins.ipnb.editor.panels.code.IpnbCodePanel; +import org.jetbrains.plugins.ipnb.format.IpnbParser; +import org.jetbrains.plugins.ipnb.format.cells.IpnbCell; +import org.jetbrains.plugins.ipnb.format.cells.IpnbCodeCell; +import org.jetbrains.plugins.ipnb.format.cells.IpnbHeadingCell; +import org.jetbrains.plugins.ipnb.format.cells.IpnbMarkdownCell; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbOutputCell; + +import javax.swing.*; +import javax.swing.border.MatteBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeListener; +import java.util.List; + +/** + * @author traff + */ +public class IpnbFileEditor extends UserDataHolderBase implements FileEditor, TextEditor { + private final Project myProject; + private final VirtualFile myFile; + + private final String myName; + + private final JComponent myEditorPanel; + + private final TextEditor myEditor; + private final IpnbFilePanel myIpnbFilePanel; + private ComboBox myCellTypeCombo; + private static final String codeCellType = "Code"; + private static final String markdownCellType = "Markdown"; + private static final String headingCellType = "Heading "; + private static final String rawNBCellType = "Raw NBConvert"; + private final static String[] ourCellTypes = new String[]{codeCellType, markdownCellType, /*rawNBCellType, */headingCellType + "1", + headingCellType + "2", headingCellType + "3", headingCellType + "4", headingCellType + "5", headingCellType + "6"}; + private JButton myRunCellButton; + private final JScrollPane myScrollPane; + + + public IpnbFileEditor(Project project, final VirtualFile vFile) { + myProject = project; + myProject.getMessageBus().connect(this).subscribe(AppTopics.FILE_DOCUMENT_SYNC, new FileDocumentManagerAdapter() { + @Override + public void beforeAllDocumentsSaving() { + final IpnbFilePanel filePanel = myIpnbFilePanel; + if (filePanel != null) { + IpnbParser.saveIpnbFile(filePanel); + vFile.refresh(false, false); + } + + } + }); + + myProject.getMessageBus().connect(this).subscribe(FileEditorManagerListener.Before.FILE_EDITOR_MANAGER, new FileEditorManagerListener.Before.Adapter() { + @Override + public void beforeFileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) { + IpnbParser.saveIpnbFile(myIpnbFilePanel); + file.refresh(false, false); + } + }); + myFile = vFile; + + myName = vFile.getName(); + + myEditor = createEditor(project, vFile); + + myEditorPanel = new JPanel(new BorderLayout()); + myEditorPanel.setBackground(IpnbEditorUtil.getBackground()); + + myIpnbFilePanel = createIpnbEditorPanel(myProject, vFile); + + final JPanel controlPanel = createControlPanel(); + myEditorPanel.add(controlPanel, BorderLayout.NORTH); + myScrollPane = ScrollPaneFactory.createScrollPane(myIpnbFilePanel); + myScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + + myEditorPanel.add(myScrollPane, BorderLayout.CENTER); + } + + public JScrollPane getScrollPane() { + return myScrollPane; + } + + private JPanel createControlPanel() { + final JPanel controlPanel = new JPanel(); + controlPanel.setBackground(IpnbEditorUtil.getBackground()); + addSaveButton(controlPanel); + addAddButton(controlPanel); + addCutButton(controlPanel); + addCopyButton(controlPanel); + addPasteButton(controlPanel); + addRunButton(controlPanel); + + myCellTypeCombo = new ComboBox(ourCellTypes); + + myCellTypeCombo.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final Object selectedItem = myCellTypeCombo.getSelectedItem(); + final IpnbEditablePanel selectedCell = myIpnbFilePanel.getSelectedCell(); + if (selectedCell != null && selectedItem instanceof String) { + updateCellType((String)selectedItem, selectedCell); + } + } + }); + final IpnbPanel selectedCell = myIpnbFilePanel.getSelectedCell(); + updateCellTypeCombo(selectedCell); + controlPanel.add(myCellTypeCombo); + final MatteBorder border = BorderFactory.createMatteBorder(0, 0, 1, 0, JBColor.GRAY); + controlPanel.setBorder(border); + return controlPanel; + } + + private void addRunButton(@NotNull final JPanel controlPanel) { + myRunCellButton = new JButton(); + myRunCellButton.setBackground(IpnbEditorUtil.getBackground()); + myRunCellButton.setPreferredSize(new Dimension(30, 30)); + myRunCellButton.setIcon(AllIcons.General.Run); + myRunCellButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final IpnbRunCellAction action = (IpnbRunCellAction)ActionManager.getInstance().getAction("IpnbRunCellAction"); + action.runCell(myIpnbFilePanel); + } + }); + controlPanel.add(myRunCellButton); + } + + private void addSaveButton(@NotNull final JPanel controlPanel) { + addButton(controlPanel, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final IpnbSaveAction action = (IpnbSaveAction)ActionManager.getInstance().getAction("IpnbSaveAction"); + action.saveAndCheckpoint(IpnbFileEditor.this); + } + }, AllIcons.Actions.Menu_saveall); + } + + private void addCutButton(@NotNull final JPanel controlPanel) { + addButton(controlPanel, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final IpnbCutCellAction action = (IpnbCutCellAction)ActionManager.getInstance().getAction("IpnbCutCellAction"); + action.cutCell(myIpnbFilePanel); + } + }, AllIcons.Actions.Menu_cut); + } + + private void addCopyButton(@NotNull final JPanel controlPanel) { + addButton(controlPanel, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final IpnbCopyCellAction action = (IpnbCopyCellAction)ActionManager.getInstance().getAction("IpnbCopyCellAction"); + action.copyCell(myIpnbFilePanel); + } + }, AllIcons.Actions.Copy); + } + + private void addPasteButton(@NotNull final JPanel controlPanel) { + addButton(controlPanel, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final IpnbPasteCellAction action = (IpnbPasteCellAction)ActionManager.getInstance().getAction("IpnbPasteCellAction"); + action.pasteCell(myIpnbFilePanel); + } + }, AllIcons.Actions.Menu_paste); + } + + private void addButton(@NotNull final JPanel controlPanel, @NotNull final ActionListener listener, @NotNull final Icon icon) { + final JButton button = new JButton(); + button.setBackground(IpnbEditorUtil.getBackground()); + button.setPreferredSize(new Dimension(30, 30)); + button.setIcon(icon); + button.addActionListener(listener); + controlPanel.add(button); + } + + private void addAddButton(@NotNull final JPanel controlPanel) { + addButton(controlPanel, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final IpnbAddCellAction action = (IpnbAddCellAction)ActionManager.getInstance().getAction("IpnbAddCellAction"); + action.addCell(myIpnbFilePanel); + } + }, AllIcons.General.Add); + } + + public JButton getRunCellButton() { + return myRunCellButton; + } + + private void updateCellType(@NotNull final String selectedItem, @NotNull final IpnbEditablePanel selectedCell) { + if (selectedCell instanceof IpnbHeadingPanel) { + final IpnbHeadingCell cell = ((IpnbHeadingPanel)selectedCell).getCell(); + if (selectedItem.startsWith(headingCellType)) { + final char c = selectedItem.charAt(selectedItem.length() - 1); + final int level = Character.getNumericValue(c); + if (level != cell.getLevel()) { + cell.setLevel(level); + selectedCell.updateCellView(); + } + } + else if (selectedItem.equals(markdownCellType)) { + final List cells = myIpnbFilePanel.getIpnbFile().getCells(); + final int index = cells.indexOf(((IpnbHeadingPanel)selectedCell).getCell()); + final IpnbMarkdownCell markdownCell = new IpnbMarkdownCell(cell.getSource()); + if (index >= 0) + cells.set(index, markdownCell); + myIpnbFilePanel.replaceComponent(selectedCell, markdownCell); + } + else if (selectedItem.equals(codeCellType)) { + final List cells = myIpnbFilePanel.getIpnbFile().getCells(); + final int index = cells.indexOf(((IpnbHeadingPanel)selectedCell).getCell()); + final IpnbCodeCell codeCell = new IpnbCodeCell("python", cell.getSource(), null, Lists.newArrayList()); + if (index >= 0) + cells.set(index, codeCell); + + myIpnbFilePanel.replaceComponent(selectedCell, codeCell); + } + } + else if (selectedCell instanceof IpnbMarkdownPanel) { + final IpnbMarkdownCell cell = ((IpnbMarkdownPanel)selectedCell).getCell(); + if (selectedItem.startsWith(headingCellType)) { + final char c = selectedItem.charAt(selectedItem.length() - 1); + final int level = Character.getNumericValue(c); + final List cells = myIpnbFilePanel.getIpnbFile().getCells(); + final int index = cells.indexOf(((IpnbMarkdownPanel)selectedCell).getCell()); + final IpnbHeadingCell headingCell = new IpnbHeadingCell(cell.getSource(), level); + if (index >= 0) + cells.set(index, headingCell); + + myIpnbFilePanel.replaceComponent(selectedCell, headingCell); + } + else if (selectedItem.equals(codeCellType)) { + final List cells = myIpnbFilePanel.getIpnbFile().getCells(); + final int index = cells.indexOf(((IpnbMarkdownPanel)selectedCell).getCell()); + final IpnbCodeCell codeCell = new IpnbCodeCell("python", cell.getSource(), null, Lists.newArrayList()); + if (index >= 0) + cells.set(index, codeCell); + + myIpnbFilePanel.replaceComponent(selectedCell, codeCell); + } + } + else if (selectedCell instanceof IpnbCodePanel) { + final IpnbCodeCell cell = ((IpnbCodePanel)selectedCell).getCell(); + if (selectedItem.startsWith(headingCellType)) { + final char c = selectedItem.charAt(selectedItem.length() - 1); + final int level = Character.getNumericValue(c); + final List cells = myIpnbFilePanel.getIpnbFile().getCells(); + final int index = cells.indexOf(((IpnbCodePanel)selectedCell).getCell()); + final IpnbHeadingCell headingCell = new IpnbHeadingCell(cell.getSource(), level); + if (index >= 0) + cells.set(index, headingCell); + myIpnbFilePanel.replaceComponent(selectedCell, headingCell); + } + else if(selectedItem.equals(markdownCellType)) { + final List cells = myIpnbFilePanel.getIpnbFile().getCells(); + final int index = cells.indexOf(((IpnbCodePanel)selectedCell).getCell()); + final IpnbMarkdownCell markdownCell = new IpnbMarkdownCell(cell.getSource()); + if (index >= 0) + cells.set(index, markdownCell); + + myIpnbFilePanel.replaceComponent(selectedCell, markdownCell); + } + } + } + + @NotNull + private IpnbFilePanel createIpnbEditorPanel(Project project, VirtualFile vFile) { + return new IpnbFilePanel(project, this, vFile, + new CellSelectionListener() { + @Override + public void selectionChanged(@NotNull IpnbPanel ipnbPanel) { + if (myCellTypeCombo == null) return; + updateCellTypeCombo(ipnbPanel); + } + }); + } + + private void updateCellTypeCombo(IpnbPanel ipnbPanel) { + if (ipnbPanel instanceof IpnbHeadingPanel) { + final IpnbHeadingCell cell = ((IpnbHeadingPanel)ipnbPanel).getCell(); + final int level = cell.getLevel(); + myCellTypeCombo.setSelectedItem(headingCellType + level); + } + else if (ipnbPanel instanceof IpnbMarkdownPanel) { + myCellTypeCombo.setSelectedItem(markdownCellType); + } + else if (ipnbPanel instanceof IpnbCodePanel) { + myCellTypeCombo.setSelectedItem(codeCellType); + } + } + + public IpnbFilePanel getIpnbFilePanel() { + return myIpnbFilePanel; + } + + @NotNull + @Override + public JComponent getComponent() { + return myEditorPanel; + } + + @Override + public JComponent getPreferredFocusedComponent() { + return myEditorPanel; + } + + @NotNull + @Override + public String getName() { + return myName; + } + + @NotNull + @Override + public FileEditorState getState(@NotNull FileEditorStateLevel level) { + final int index = getIpnbFilePanel().getSelectedIndex(); + final IpnbEditablePanel cell = getIpnbFilePanel().getSelectedCell(); + final int top = cell != null ? cell.getTop() : 0; + return new IpnbEditorState(-1, index, top); + } + + @Override + public void setState(@NotNull FileEditorState state) { + final int index = ((IpnbEditorState)state).getSelectedIndex(); + final int position = ((IpnbEditorState)state).getSelectedTop(); + myIpnbFilePanel.setInitialPosition(index, position); + } + + @Override + public boolean isModified() { + return false; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public void selectNotify() { + } + + @Override + public void deselectNotify() { + } + + @Override + public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { + } + + @Override + public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { + } + + @Override + public BackgroundEditorHighlighter getBackgroundHighlighter() { + return null; + } + + @Override + public FileEditorLocation getCurrentLocation() { + return null; + } + + @Override + public StructureViewBuilder getStructureViewBuilder() { + return null; + } + + @Override + public void dispose() { + Disposer.dispose(myEditor); + } + + @NotNull + @Override + public Editor getEditor() { + return myEditor.getEditor(); + } + + @Override + public boolean canNavigateTo(@NotNull Navigatable navigatable) { + return true; + } + + @Override + public void navigateTo(@NotNull Navigatable navigatable) { + } + + @Nullable + private static TextEditor createEditor(@NotNull Project project, @NotNull VirtualFile vFile) { + FileEditorProvider provider = getProvider(project, vFile); + + if (provider != null) { + FileEditor editor = provider.createEditor(project, vFile); + if (editor instanceof TextEditor) { + return (TextEditor)editor; + } + } + return null; + } + + @Nullable + private static FileEditorProvider getProvider(Project project, VirtualFile vFile) { + FileEditorProvider[] providers = FileEditorProviderManagerImpl.getInstance().getProviders(project, vFile); + for (FileEditorProvider provider : providers) { + if (!(provider instanceof IpnbEditorProvider)) { + return provider; + } + } + return null; + } + + public abstract class CellSelectionListener { + public abstract void selectionChanged(@NotNull final IpnbPanel ipnbPanel); + } + + public VirtualFile getVirtualFile() { + return myFile; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbAddCellAction.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbAddCellAction.java new file mode 100644 index 000000000000..1078f633a513 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbAddCellAction.java @@ -0,0 +1,31 @@ +package org.jetbrains.plugins.ipnb.editor.actions; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.fileEditor.FileEditor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel; + +public class IpnbAddCellAction extends AnAction { + public IpnbAddCellAction() { + super(AllIcons.General.Run); + } + + @Override + public void actionPerformed(AnActionEvent event) { + final DataContext context = event.getDataContext(); + final FileEditor editor = PlatformDataKeys.FILE_EDITOR.getData(context); + if (editor instanceof IpnbFileEditor) { + final IpnbFilePanel component = ((IpnbFileEditor)editor).getIpnbFilePanel(); + addCell(component); + } + } + + public void addCell(@NotNull final IpnbFilePanel ipnbFilePanel) { + ipnbFilePanel.createAndAddCell(); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbCopyCellAction.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbCopyCellAction.java new file mode 100644 index 000000000000..b12569f62447 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbCopyCellAction.java @@ -0,0 +1,31 @@ +package org.jetbrains.plugins.ipnb.editor.actions; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.fileEditor.FileEditor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel; + +public class IpnbCopyCellAction extends AnAction { + public IpnbCopyCellAction() { + super(AllIcons.General.Run); + } + + @Override + public void actionPerformed(AnActionEvent event) { + final DataContext context = event.getDataContext(); + final FileEditor editor = PlatformDataKeys.FILE_EDITOR.getData(context); + if (editor instanceof IpnbFileEditor) { + final IpnbFilePanel component = ((IpnbFileEditor)editor).getIpnbFilePanel(); + copyCell(component); + } + } + + public void copyCell(@NotNull final IpnbFilePanel ipnbFilePanel) { + ipnbFilePanel.copyCell(); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbCutCellAction.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbCutCellAction.java new file mode 100644 index 000000000000..c79ce99a969b --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbCutCellAction.java @@ -0,0 +1,33 @@ +package org.jetbrains.plugins.ipnb.editor.actions; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.fileEditor.FileEditor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel; + +public class IpnbCutCellAction extends AnAction { + public IpnbCutCellAction() { + super(AllIcons.General.Run); + } + + @Override + public void actionPerformed(AnActionEvent event) { + final DataContext context = event.getDataContext(); + final FileEditor editor = PlatformDataKeys.FILE_EDITOR.getData(context); + if (editor instanceof IpnbFileEditor) { + final IpnbFilePanel component = ((IpnbFileEditor)editor).getIpnbFilePanel(); + cutCell(component); + } + } + + public void cutCell(@NotNull final IpnbFilePanel ipnbFilePanel) { + ipnbFilePanel.cutCell(); + ipnbFilePanel.revalidate(); + ipnbFilePanel.repaint(); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbPasteCellAction.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbPasteCellAction.java new file mode 100644 index 000000000000..de10dca7f866 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbPasteCellAction.java @@ -0,0 +1,31 @@ +package org.jetbrains.plugins.ipnb.editor.actions; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.fileEditor.FileEditor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel; + +public class IpnbPasteCellAction extends AnAction { + public IpnbPasteCellAction() { + super(AllIcons.General.Run); + } + + @Override + public void actionPerformed(AnActionEvent event) { + final DataContext context = event.getDataContext(); + final FileEditor editor = PlatformDataKeys.FILE_EDITOR.getData(context); + if (editor instanceof IpnbFileEditor) { + final IpnbFilePanel component = ((IpnbFileEditor)editor).getIpnbFilePanel(); + pasteCell(component); + } + } + + public void pasteCell(@NotNull final IpnbFilePanel ipnbFilePanel) { + ipnbFilePanel.pasteCell(); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbRunCellAction.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbRunCellAction.java new file mode 100644 index 000000000000..35e562d67efa --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbRunCellAction.java @@ -0,0 +1,37 @@ +package org.jetbrains.plugins.ipnb.editor.actions; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.fileEditor.FileEditor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbEditablePanel; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel; + +public class IpnbRunCellAction extends AnAction { + public IpnbRunCellAction() { + super(AllIcons.General.Run); + } + + @Override + public void actionPerformed(AnActionEvent event) { + final DataContext context = event.getDataContext(); + final FileEditor editor = PlatformDataKeys.FILE_EDITOR.getData(context); + if (editor instanceof IpnbFileEditor) { + final IpnbFilePanel component = ((IpnbFileEditor)editor).getIpnbFilePanel(); + runCell(component); + } + } + + public void runCell(@NotNull final IpnbFilePanel ipnbFilePanel) { + final IpnbEditablePanel cell = ipnbFilePanel.getSelectedCell(); + cell.runCell(); + ipnbFilePanel.selectNext(cell); + ipnbFilePanel.revalidate(); + ipnbFilePanel.repaint(); + ipnbFilePanel.requestFocus(); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbSaveAction.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbSaveAction.java new file mode 100644 index 000000000000..ae880d7fd88e --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/actions/IpnbSaveAction.java @@ -0,0 +1,35 @@ +package org.jetbrains.plugins.ipnb.editor.actions; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel; +import org.jetbrains.plugins.ipnb.format.IpnbParser; + +public class IpnbSaveAction extends AnAction { + public IpnbSaveAction() { + super(AllIcons.General.Run); + } + + @Override + public void actionPerformed(AnActionEvent event) { + final DataContext context = event.getDataContext(); + final FileEditor editor = PlatformDataKeys.FILE_EDITOR.getData(context); + if (editor instanceof IpnbFileEditor) { + saveAndCheckpoint((IpnbFileEditor)editor); + } + } + + public void saveAndCheckpoint(@NotNull final IpnbFileEditor editor) { + final IpnbFilePanel filePanel = editor.getIpnbFilePanel(); + IpnbParser.saveIpnbFile(filePanel); + final VirtualFile file = editor.getVirtualFile(); + file.refresh(false, false); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbEditablePanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbEditablePanel.java new file mode 100644 index 000000000000..486595322b97 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbEditablePanel.java @@ -0,0 +1,161 @@ +package org.jetbrains.plugins.ipnb.editor.panels; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.ui.Gray; +import com.intellij.ui.JBColor; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.format.cells.IpnbEditableCell; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +public abstract class IpnbEditablePanel extends IpnbPanel { + private static final Logger LOG = Logger.getInstance(IpnbEditablePanel.class); + private boolean myEditing; + protected JTextArea myEditablePanel; + public final static String EDITABLE_PANEL = "Editable panel"; + public final static String VIEW_PANEL = "View panel"; + + public IpnbEditablePanel(@NotNull K cell) { + super(cell); + } + + + protected void initPanel() { + myViewPanel = createViewPanel(); + myViewPanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + final Container parent = getParent(); + final MouseEvent parentEvent = SwingUtilities.convertMouseEvent(myViewPanel, e, parent); + parent.dispatchEvent(parentEvent); + if (e.getClickCount() == 2) { + switchToEditing(); + } + } + }); + myViewPanel.setName(VIEW_PANEL); + add(myViewPanel, VIEW_PANEL); + myEditablePanel = createEditablePanel(); + myEditablePanel.setName(EDITABLE_PANEL); + add(myEditablePanel, EDITABLE_PANEL); + } + + public void switchToEditing() { + setEditing(true); + + final LayoutManager layout = getLayout(); + if (layout instanceof CardLayout) { + ((CardLayout)layout).show(this, EDITABLE_PANEL); + UIUtil.requestFocus(myEditablePanel); + } + } + + public boolean isModified() { + final Component[] components = getComponents(); + for (Component component : components) { + final String name = component.getName(); + if (component.isVisible() && EDITABLE_PANEL.equals(name)) return true; + } + return false; + } + + protected String getRawCellText() { return ""; } + + public void runCell() { + final LayoutManager layout = getLayout(); + if (layout instanceof CardLayout) { + updateCellSource(); + updateCellView(); + ((CardLayout)layout).show(this, VIEW_PANEL); + setEditing(false); + } + } + + private JTextArea createEditablePanel() { + final JTextArea textArea = new JTextArea(getRawCellText()); + textArea.setLineWrap(true); + textArea.setEditable(true); + textArea.setBorder(BorderFactory.createLineBorder(JBColor.lightGray)); + textArea.setBackground(Gray._247); + textArea.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 1) { + setEditing(true); + final Container parent = getParent(); + parent.repaint(); + if (parent instanceof IpnbFilePanel) { + ((IpnbFilePanel)parent).setSelectedCell(IpnbEditablePanel.this); + textArea.requestFocus(); + } + } + } + }); + textArea.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + setEditing(false); + final Container parent = getParent(); + if (parent instanceof IpnbFilePanel) { + parent.repaint(); + UIUtil.requestFocus((IpnbFilePanel)parent); + } + } + } + }); + return textArea; + } + + public boolean contains(int y) { + return y>= getTop() && y<=getBottom(); + } + + public int getTop() { + return getY(); + } + + public int getBottom() { + return getTop() + getHeight(); + } + + public boolean isEditing() { + return myEditing; + } + + public void setEditing(boolean editing) { + myEditing = editing; + } + + public void updateCellView() { // TODO: make abstract + } + + public void updateCellSource() { + final String text = myEditablePanel.getText(); + myCell.setSource(StringUtil.splitByLinesKeepSeparators(text != null ? text : "")); + } + + @SuppressWarnings("CloneDoesntDeclareCloneNotSupportedException") + @Override + protected Object clone() { + try { + return super.clone(); + } + catch (CloneNotSupportedException e) { + LOG.error(e); + } + return null; + } + + public K getCell() { + return myCell; + } + +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbEditorPanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbEditorPanel.java new file mode 100644 index 000000000000..f3a3592c43b5 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbEditorPanel.java @@ -0,0 +1,25 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.ipnb.editor.panels; + +import com.intellij.openapi.editor.Editor; + +/** + * @author traff + */ +public interface IpnbEditorPanel { + Editor getEditor(); +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbFilePanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbFilePanel.java new file mode 100644 index 000000000000..cd7c006e843e --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbFilePanel.java @@ -0,0 +1,424 @@ +package org.jetbrains.plugins.ipnb.editor.panels; + +import com.google.common.collect.Lists; +import com.intellij.openapi.actionSystem.DataProvider; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.JBColor; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil; +import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor; +import org.jetbrains.plugins.ipnb.editor.panels.code.IpnbCodePanel; +import org.jetbrains.plugins.ipnb.format.IpnbFile; +import org.jetbrains.plugins.ipnb.format.IpnbParser; +import org.jetbrains.plugins.ipnb.format.cells.*; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbOutputCell; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class IpnbFilePanel extends JPanel implements Scrollable, DataProvider { + + public static final int INSET_Y = 10; + public static final int INSET_X = 5; + private IpnbFile myIpnbFile; + + private Project myProject; + @NotNull private IpnbFileEditor myParent; + @NotNull private final IpnbFileEditor.CellSelectionListener myListener; + + private final List myIpnbPanels = Lists.newArrayList(); + + private IpnbEditablePanel mySelectedCell; + private IpnbEditablePanel myBufferPanel; + private int myIncrement = 10; + private int myInitialSelection = 0; + private int myInitialPosition = 0; + + public IpnbFilePanel(@NotNull final Project project, @NotNull final IpnbFileEditor parent, @NotNull final VirtualFile vFile, + @NotNull final IpnbFileEditor.CellSelectionListener listener) { + super(new GridBagLayout()); + myProject = project; + myParent = parent; + myListener = listener; + setBackground(IpnbEditorUtil.getBackground()); + + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + try { + myIpnbFile = IpnbParser.parseIpnbFile(vFile); + layoutFile(); + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + updateCellSelection(e); + } + }); + setFocusable(true); + + } + catch (IOException e) { + Messages.showErrorDialog(project, e.getMessage(), "Can't open " + vFile.getPath()); + } + } + }); + + UIUtil.requestFocus(this); + } + + public List getIpnbPanels() { + return myIpnbPanels; + } + + private void layoutFile() { + GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + c.gridx = 0; + c.gridy = 0; + c.gridwidth = 2; + c.insets = new Insets(INSET_Y, INSET_X, 0, 0); + + final int width = IpnbEditorUtil.PANEL_WIDTH + IpnbEditorUtil.PROMPT_SIZE.width; + final JLabel label = new JLabel(""); + add(label, c); + + c.gridy = 1; + c.gridwidth = 1; + final JPanel panel = new JPanel(); + panel.setPreferredSize(IpnbEditorUtil.PROMPT_SIZE); + panel.setBackground(getBackground()); + panel.setOpaque(false); + add(panel, c); + + final List cells = myIpnbFile.getCells(); + for (IpnbCell cell : cells) { + c.gridy = addCellToPanel(cell, c); + } + + if (myInitialSelection >= 0 && myIpnbPanels.size() > myInitialSelection) { + final IpnbEditablePanel toSelect = myIpnbPanels.get(myInitialSelection); + setSelectedCell(toSelect); + myParent.getScrollPane().getViewport().setViewPosition(new Point(0, myInitialPosition)); + } + c.weighty = 1; + add(createEmptyPanel(), c); + } + + private int addCellToPanel(IpnbCell cell, GridBagConstraints c) { + IpnbEditablePanel panel; + if (cell instanceof IpnbCodeCell) { + panel = new IpnbCodePanel(myProject, myParent, (IpnbCodeCell)cell); + c.gridwidth = 2; + c.gridx = 0; + add(panel, c); + myIpnbPanels.add(panel); + } + else if (cell instanceof IpnbMarkdownCell) { + panel = new IpnbMarkdownPanel((IpnbMarkdownCell)cell); + addComponent(c, panel); + } + else if (cell instanceof IpnbHeadingCell) { + panel = new IpnbHeadingPanel((IpnbHeadingCell)cell); + addComponent(c, panel); + } + else { + throw new UnsupportedOperationException(cell.getClass().toString()); + } + return c.gridy + 1; + } + + public void createAndAddCell() { + removeAll(); + final IpnbCodeCell cell = new IpnbCodeCell("python", new String[]{""}, null, new ArrayList()); + final IpnbCodePanel codePanel = new IpnbCodePanel(myProject, myParent, cell); + + addCell(cell, codePanel); + } + + private void addCell(IpnbEditableCell cell, IpnbEditablePanel panel) { + final IpnbEditablePanel selectedCell = getSelectedCell(); + final int index = myIpnbPanels.indexOf(selectedCell); + myIpnbFile.addCell(cell, index+1); + myIpnbPanels.add(index + 1, panel); + + final GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + c.gridx = 0; + c.gridy = 0; + c.gridwidth = 1; + c.insets = new Insets(INSET_Y, INSET_X, 0, 0); + + final JPanel promptPanel = new JPanel(); + promptPanel.setPreferredSize(new Dimension(IpnbEditorUtil.PROMPT_SIZE.width, 1)); + promptPanel.setBackground(getBackground()); + promptPanel.setOpaque(false); + add(promptPanel, c); + + c.gridy += 1; + for (IpnbPanel comp : myIpnbPanels) { + c.gridwidth = 1; + c.gridx = 1; + if (comp instanceof IpnbCodePanel) { + c.gridwidth = 2; + c.gridx = 0; + add(comp, c); + } + else { + add(comp, c); + } + c.gridy += 1; + } + c.weighty = 1; + add(createEmptyPanel(), c); + + setSelectedCell(panel); + requestFocus(); + revalidate(); + repaint(); + } + + public void cutCell() { + myBufferPanel = getSelectedCell(); + selectNextOrPrev(myBufferPanel); + final int index = myIpnbPanels.indexOf(myBufferPanel); + if (index < 0) return; + myIpnbPanels.remove(index); + myIpnbFile.removeCell(index); + + remove(myBufferPanel); + } + + public void copyCell() { + myBufferPanel = getSelectedCell(); + } + + public void pasteCell() { + if (myBufferPanel == null) return; + removeAll(); + final IpnbEditablePanel editablePanel = (IpnbEditablePanel)myBufferPanel.clone(); + addCell(editablePanel.getCell(), editablePanel); + } + + public void replaceComponent(@NotNull final IpnbEditablePanel from, @NotNull final IpnbCell cell) { + final GridBagConstraints c = ((GridBagLayout)getLayout()).getConstraints(from); + final int index = myIpnbPanels.indexOf(from); + IpnbEditablePanel panel; + if (cell instanceof IpnbCodeCell) { + panel = new IpnbCodePanel(myProject, myParent, (IpnbCodeCell)cell); + c.gridwidth = 2; + c.gridx = 0; + add(panel, c); + } + else if (cell instanceof IpnbMarkdownCell) { + panel = new IpnbMarkdownPanel((IpnbMarkdownCell)cell); + c.gridwidth = 1; + c.gridx = 1; + add(panel, c); + } + else if (cell instanceof IpnbHeadingCell) { + panel = new IpnbHeadingPanel((IpnbHeadingCell)cell); + c.gridwidth = 1; + c.gridx = 1; + add(panel, c); + } + else { + throw new UnsupportedOperationException(cell.getClass().toString()); + } + if (index >= 0) { + myIpnbPanels.remove(index); + myIpnbPanels.add(index, panel); + } + setSelectedCell(panel); + remove(from); + revalidate(); + repaint(); + } + + private void addComponent(@NotNull final GridBagConstraints c, @NotNull final IpnbEditablePanel comp) { + c.gridwidth = 1; + c.gridx = 1; + add(comp, c); + + myIpnbPanels.add(comp); + } + + private JPanel createEmptyPanel() { + JPanel panel = new JPanel(); + panel.setBackground(IpnbEditorUtil.getBackground()); + return panel; + } + + @Override + protected void processKeyEvent(KeyEvent e) { + if (mySelectedCell != null && e.getID() == KeyEvent.KEY_PRESSED) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + mySelectedCell.switchToEditing(); + } + int index = myIpnbPanels.indexOf(mySelectedCell); + final Rectangle rect = getVisibleRect(); + + + if (e.getKeyCode() == KeyEvent.VK_UP) { + selectPrev(mySelectedCell); + if (index > 0) { + final Rectangle cellBounds = mySelectedCell.getBounds(); + if (cellBounds.getY() + cellBounds.getHeight() <= rect.getY()) { + myIncrement = rect.y - cellBounds.y; + getParent().dispatchEvent(e); + } + } + } + else if (e.getKeyCode() == KeyEvent.VK_DOWN) { + selectNext(mySelectedCell); + if (index < myIpnbPanels.size() - 1) { + final Rectangle cellBounds = mySelectedCell.getBounds(); + if (cellBounds.getY() > rect.getY() + rect.getHeight()) { + myIncrement = cellBounds.y + cellBounds.height - rect.y - rect.height; + getParent().dispatchEvent(e); + } + } + } + else { + getParent().dispatchEvent(e); + } + } + } + + public void selectPrev(@NotNull IpnbEditablePanel cell) { + int index = myIpnbPanels.indexOf(cell); + if (index > 0) { + setSelectedCell(myIpnbPanels.get(index - 1)); + } + } + + public void selectNext(@NotNull IpnbEditablePanel cell) { + int index = myIpnbPanels.indexOf(cell); + if (index < myIpnbPanels.size() - 1) { + setSelectedCell(myIpnbPanels.get(index + 1)); + } + } + + public void selectNextOrPrev(@NotNull IpnbEditablePanel cell) { + int index = myIpnbPanels.indexOf(cell); + if (index < myIpnbPanels.size() - 1) { + setSelectedCell(myIpnbPanels.get(index + 1)); + } + else if (index > 0) { + setSelectedCell(myIpnbPanels.get(index - 1)); + } + else { + mySelectedCell = null; + repaint(); + } + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + if (mySelectedCell != null) { + g.setColor(mySelectedCell.isEditing() ? JBColor.GREEN : JBColor.GRAY); + g.drawRoundRect(100, mySelectedCell.getTop() - 1, getWidth() - 200, mySelectedCell.getHeight() + 2, 5, 5); + } + } + + private void updateCellSelection(MouseEvent e) { + if (e.getClickCount() > 0) { + IpnbEditablePanel ipnbPanel = getIpnbPanelByClick(e.getPoint()); + if (ipnbPanel != null) { + ipnbPanel.setEditing(false); + ipnbPanel.requestFocus(); + repaint(); + setSelectedCell(ipnbPanel); + } + } + } + + public void setInitialPosition(int index, int position) { + myInitialSelection = index; + myInitialPosition = position; + } + + public void setSelectedCell(@NotNull final IpnbEditablePanel ipnbPanel) { + if (ipnbPanel.equals(mySelectedCell)) return; + if (mySelectedCell != null) + mySelectedCell.setEditing(false); + mySelectedCell = ipnbPanel; + revalidate(); + UIUtil.requestFocus(this); + repaint(); + myListener.selectionChanged(ipnbPanel); + } + + public IpnbEditablePanel getSelectedCell() { + return mySelectedCell; + } + + public int getSelectedIndex() { + final IpnbEditablePanel selectedCell = getSelectedCell(); + return myIpnbPanels.indexOf(selectedCell); + } + + @Nullable + private IpnbEditablePanel getIpnbPanelByClick(@NotNull final Point point) { + for (IpnbEditablePanel c: myIpnbPanels) { + if (c.contains(point.y)) { + return c; + } + } + return null; + } + + public IpnbFile getIpnbFile() { + return myIpnbFile; + } + + @Override + public Dimension getPreferredScrollableViewportSize() { + return null; + } + + @Override + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { + return myIncrement; + + } + + @Override + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { + return 100; + } + + @Override + public boolean getScrollableTracksViewportWidth() { + return false; + } + + @Override + public boolean getScrollableTracksViewportHeight() { + return false; + } + + @Nullable + @Override + public Object getData(String dataId) { + final IpnbEditablePanel cell = getSelectedCell(); + if (OpenFileDescriptor.NAVIGATE_IN_EDITOR.is(dataId)) { + if (cell instanceof IpnbCodePanel) { + return ((IpnbCodePanel)cell).getEditor(); + } + } + return null; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbHeadingPanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbHeadingPanel.java new file mode 100644 index 000000000000..448ea28d74d8 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbHeadingPanel.java @@ -0,0 +1,44 @@ +package org.jetbrains.plugins.ipnb.editor.panels; + +import com.intellij.ui.components.JBLabel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil; +import org.jetbrains.plugins.ipnb.format.cells.IpnbHeadingCell; + +public class IpnbHeadingPanel extends IpnbEditablePanel { + + public IpnbHeadingPanel(@NotNull final IpnbHeadingCell cell) { + super(cell); + initPanel(); + } + + @Override + protected String getRawCellText() { + return myCell.getSourceAsString(); + } + + private String renderCellText() { + return "" + myCell.getSourceAsString() + ""; + } + + @Override + public void updateCellView() { + myViewPanel.setText(renderCellText()); + } + + @Override + protected JBLabel createViewPanel() { + final JBLabel label = new JBLabel(renderCellText()); + label.setBackground(IpnbEditorUtil.getBackground()); + label.setOpaque(true); + + return label; + } + + @SuppressWarnings("CloneDoesntCallSuperClone") + @Override + protected Object clone() { + return new IpnbHeadingPanel((IpnbHeadingCell)myCell.clone()); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbMarkdownPanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbMarkdownPanel.java new file mode 100644 index 000000000000..4fb1a5add120 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbMarkdownPanel.java @@ -0,0 +1,49 @@ +package org.jetbrains.plugins.ipnb.editor.panels; + +import com.intellij.openapi.ui.VerticalFlowLayout; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.IpnbUtils; +import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil; +import org.jetbrains.plugins.ipnb.format.cells.IpnbMarkdownCell; + +import javax.swing.*; +import java.awt.*; + +public class IpnbMarkdownPanel extends IpnbEditablePanel { + + public IpnbMarkdownPanel(@NotNull final IpnbMarkdownCell cell) { + super(cell); + initPanel(); + } + + @Override + protected String getRawCellText() { + return myCell.getSourceAsString(); + } + + @Override + protected JPanel createViewPanel() { + final JPanel panel = new JPanel(new VerticalFlowLayout(FlowLayout.LEFT, false, true)); + updatePanel(panel); + panel.setBackground(IpnbEditorUtil.getBackground()); + panel.setOpaque(true); + return panel; + } + + private void updatePanel(@NotNull final JPanel panel) { + panel.removeAll(); + IpnbUtils.addLatexToPanel(myCell.getSourceAsString(), panel); + } + + + @Override + public void updateCellView() { + updatePanel(myViewPanel); + } + + @SuppressWarnings("CloneDoesntCallSuperClone") + @Override + protected Object clone() { + return new IpnbMarkdownPanel((IpnbMarkdownCell)myCell.clone()); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbPanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbPanel.java new file mode 100644 index 000000000000..034fa1077a5e --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/IpnbPanel.java @@ -0,0 +1,23 @@ +package org.jetbrains.plugins.ipnb.editor.panels; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.format.cells.IpnbCell; + +import javax.swing.*; +import java.awt.*; + +public abstract class IpnbPanel extends JPanel { + protected T myViewPanel; + protected K myCell; + + public IpnbPanel(@NotNull final K cell) { + super(new CardLayout()); + myCell = cell; + } + + public K getCell() { + return myCell; + } + + protected abstract T createViewPanel(); +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbCodeOutputPanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbCodeOutputPanel.java new file mode 100644 index 000000000000..bf9b8ad9d744 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbCodeOutputPanel.java @@ -0,0 +1,22 @@ +package org.jetbrains.plugins.ipnb.editor.panels.code; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbPanel; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbOutputCell; + +import javax.swing.*; + +public class IpnbCodeOutputPanel extends IpnbPanel { + + public IpnbCodeOutputPanel(@NotNull final K cell) { + super(cell); + myViewPanel = createViewPanel(); + add(myViewPanel); + } + + protected JComponent createViewPanel() { + JTextArea textArea = new JTextArea(myCell.getSourceAsString()); + textArea.setEditable(false); + return textArea; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbCodePanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbCodePanel.java new file mode 100644 index 000000000000..f73b7de1c76e --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbCodePanel.java @@ -0,0 +1,191 @@ +package org.jetbrains.plugins.ipnb.editor.panels.code; + +import com.google.common.collect.Lists; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.ipnb.configuration.IpnbConnectionManager; +import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil; +import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbEditablePanel; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbPanel; +import org.jetbrains.plugins.ipnb.format.cells.IpnbCodeCell; +import org.jetbrains.plugins.ipnb.format.cells.output.*; + +import javax.swing.*; +import java.awt.*; +import java.util.List; + +public class IpnbCodePanel extends IpnbEditablePanel { + private final Project myProject; + private final Disposable myParent; + private IpnbCodeSourcePanel myCodeSourcePanel; + private final List myOutputPanels = Lists.newArrayList(); + + public IpnbCodePanel(@NotNull final Project project, @Nullable final Disposable parent, @NotNull final IpnbCodeCell cell) { + super(cell); + myProject = project; + myParent = parent; + + myViewPanel = createViewPanel(); + add(myViewPanel); + } + + public IpnbFileEditor getFileEditor() { + assert myParent instanceof IpnbFileEditor; + return (IpnbFileEditor)myParent; + } + + public Editor getEditor() { + return myCodeSourcePanel.getEditor(); + } + + public void addPromptPanel(@NotNull final JComponent parent, Integer promptNumber, + @NotNull final IpnbEditorUtil.PromptType promptType, + @NotNull final IpnbPanel component, @NotNull final GridBagConstraints c) { + c.gridx = 0; + c.weightx = 0; + c.anchor = GridBagConstraints.NORTHWEST; + final JComponent promptComponent = IpnbEditorUtil.createPromptComponent(promptNumber, promptType); + c.insets = new Insets(2,2,2,5); + parent.add(promptComponent, c); + + c.gridx = 1; + c.weightx = 1; + c.insets = new Insets(2,2,2,2); + c.anchor = GridBagConstraints.CENTER; + parent.add(component, c); + myOutputPanels.add(component); + } + + @Override + protected JComponent createViewPanel() { + final JPanel panel = new JPanel(new GridBagLayout()); + panel.setBackground(IpnbEditorUtil.getBackground()); + + final GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + c.gridx = 0; + c.gridy = 0; + c.gridwidth = 1; + + myCodeSourcePanel = new IpnbCodeSourcePanel(myProject, this, myCell); + if (myParent != null) + Disposer.register(myParent, new Disposable() { + @Override + public void dispose() { + EditorFactory.getInstance().releaseEditor(myCodeSourcePanel.getEditor()); + } + }); + addPromptPanel(panel, myCell.getPromptNumber(), IpnbEditorUtil.PromptType.In, myCodeSourcePanel, c); + + c.gridx = 1; + + c.gridy = 0; + for (IpnbOutputCell outputCell : myCell.getCellOutputs()) { + c.gridy++; + addOutputPanel(panel, c, outputCell, true); + } + return panel; + } + + private void addOutputPanel(@NotNull final JComponent panel, @NotNull final GridBagConstraints c, + @NotNull final IpnbOutputCell outputCell, boolean addPrompt) { + final IpnbEditorUtil.PromptType promptType = addPrompt ? IpnbEditorUtil.PromptType.Out : IpnbEditorUtil.PromptType.None; + if (outputCell instanceof IpnbImageOutputCell) { + addPromptPanel(panel, myCell.getPromptNumber(), promptType, + new IpnbImagePanel((IpnbImageOutputCell)outputCell), c); + } + else if (outputCell instanceof IpnbHtmlOutputCell) { + addPromptPanel(panel, myCell.getPromptNumber(), promptType, + new IpnbHtmlPanel((IpnbHtmlOutputCell)outputCell), c); + } + else if (outputCell instanceof IpnbLatexOutputCell) { + addPromptPanel(panel, myCell.getPromptNumber(), promptType, + new IpnbLatexPanel((IpnbLatexOutputCell)outputCell), c); + } + else if (outputCell instanceof IpnbErrorOutputCell) { + addPromptPanel(panel, myCell.getPromptNumber(), promptType, + new IpnbErrorPanel((IpnbErrorOutputCell)outputCell), c); + } + else if (outputCell instanceof IpnbStreamOutputCell) { + addPromptPanel(panel, myCell.getPromptNumber(), IpnbEditorUtil.PromptType.None, + new IpnbStreamPanel((IpnbStreamOutputCell)outputCell), c); + } + else if (outputCell.getSourceAsString() != null) { + addPromptPanel(panel, myCell.getPromptNumber(), promptType, + new IpnbCodeOutputPanel(outputCell), c); + } + } + + @Override + public void switchToEditing() { + setEditing(true); + getParent().repaint(); + UIUtil.requestFocus(myCodeSourcePanel.getEditor().getContentComponent()); + } + + @Override + public void runCell() { + super.runCell(); + updateCellSource(); + myCell.setPromptNumber(-1); + updatePanel(myCell.getCellOutputs()); + final IpnbConnectionManager connectionManager = IpnbConnectionManager.getInstance(myProject); + connectionManager.executeCell(this); + } + + @Override + public boolean isModified() { + return true; + } + + @Override + public void updateCellSource() { + final Document document = myCodeSourcePanel.getEditor().getDocument(); + final String text = document.getText(); + myCell.setSource(StringUtil.splitByLinesKeepSeparators(text)); + } + + public void updatePanel(@NotNull final List outputContent) { + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + myCell.removeCellOutputs(); + + myViewPanel.removeAll(); + final GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + c.gridx = 0; + c.gridy = 0; + c.gridwidth = 1; + + addPromptPanel(myViewPanel, myCell.getPromptNumber(), IpnbEditorUtil.PromptType.In, myCodeSourcePanel, c); + + for (IpnbOutputCell output : outputContent) { + myCell.addCellOutput(output); + c.gridx = 0; + c.gridy += 1; + + addOutputPanel(myViewPanel, c, output, output instanceof IpnbOutOutputCell); + } + revalidate(); + repaint(); + } + }); + } + + @SuppressWarnings({"CloneDoesntCallSuperClone", "CloneDoesntDeclareCloneNotSupportedException"}) + @Override + protected Object clone() { + return new IpnbCodePanel(myProject, myParent, (IpnbCodeCell)myCell.clone()); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbCodeSourcePanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbCodeSourcePanel.java new file mode 100644 index 000000000000..8e05dd0f9570 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbCodeSourcePanel.java @@ -0,0 +1,123 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.plugins.ipnb.editor.panels.code; + +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.VerticalFlowLayout; +import com.intellij.ui.Gray; +import com.intellij.ui.JBColor; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil; +import org.jetbrains.plugins.ipnb.editor.actions.IpnbRunCellAction; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbEditorPanel; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbPanel; +import org.jetbrains.plugins.ipnb.format.cells.IpnbCodeCell; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; + +/** + * @author traff + */ +public class IpnbCodeSourcePanel extends IpnbPanel implements IpnbEditorPanel { + private Editor myEditor; + @NotNull private final Project myProject; + @NotNull private final IpnbCodePanel myParent; + @NotNull private final String mySource; + + public IpnbCodeSourcePanel(@NotNull final Project project, @NotNull final IpnbCodePanel parent, @NotNull final IpnbCodeCell cell) { + super(cell); + myProject = project; + myParent = parent; + mySource = cell.getSourceAsString(); + final JComponent panel = createViewPanel(); + add(panel); + } + + @NotNull + public IpnbCodePanel getIpnbCodePanel() { + return myParent; + } + + @Override + @NotNull + public Editor getEditor() { + return myEditor; + } + + @Override + protected JComponent createViewPanel() { + final JPanel panel = new JPanel(new VerticalFlowLayout(FlowLayout.LEFT, true, true)); + panel.setBackground(UIUtil.isUnderDarcula() ? IpnbEditorUtil.getBackground() : Gray._247); + + if (mySource.startsWith("%")) { + myEditor = IpnbEditorUtil.createPlainCodeEditor(myProject, mySource); + } + else { + myEditor = IpnbEditorUtil.createPythonCodeEditor(myProject, this); + } + + final JComponent component = myEditor.getComponent(); + final JComponent contentComponent = myEditor.getContentComponent(); + contentComponent.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent e) { + final int keyCode = e.getKeyCode(); + final int height = myEditor.getLineHeight() * Math.max(myEditor.getDocument().getLineCount(), 1); + component.setPreferredSize(new Dimension(IpnbEditorUtil.PANEL_WIDTH, height)); + final Container parent = myParent.getParent(); + if (parent instanceof IpnbFilePanel) { + IpnbFilePanel ipnbFilePanel = (IpnbFilePanel)parent; + ipnbFilePanel.revalidate(); + ipnbFilePanel.repaint(); + if (keyCode == KeyEvent.VK_ESCAPE) { + getIpnbCodePanel().setEditing(false); + UIUtil.requestFocus(getIpnbCodePanel().getFileEditor().getIpnbFilePanel()); + } + else if (keyCode == KeyEvent.VK_ENTER && InputEvent.CTRL_DOWN_MASK == e.getModifiersEx()) { + final IpnbRunCellAction action = (IpnbRunCellAction)ActionManager.getInstance().getAction("IpnbRunCellAction"); + action.runCell(ipnbFilePanel); + } + } + + } + }); + + contentComponent.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (InputEvent.CTRL_DOWN_MASK == e.getModifiersEx()) return; + final Container ipnbFilePanel = myParent.getParent(); + if (ipnbFilePanel instanceof IpnbFilePanel) { + ((IpnbFilePanel)ipnbFilePanel).setSelectedCell(myParent); + myParent.switchToEditing(); + } + UIUtil.requestFocus(contentComponent); + } + }); + + panel.add(component); + + component.setPreferredSize(new Dimension(IpnbEditorUtil.PANEL_WIDTH, component.getPreferredSize().height)); + setBorder(BorderFactory.createLineBorder(JBColor.lightGray, 1, true)); + return panel; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbErrorPanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbErrorPanel.java new file mode 100644 index 000000000000..78d7e7a901e7 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbErrorPanel.java @@ -0,0 +1,138 @@ +package org.jetbrains.plugins.ipnb.editor.panels.code; + +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.ui.JBColor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbErrorOutputCell; + +import javax.swing.*; +import javax.swing.text.AttributeSet; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyleContext; +import java.awt.*; + +public class IpnbErrorPanel extends IpnbCodeOutputPanel { + public IpnbErrorPanel(@NotNull final IpnbErrorOutputCell cell) { + super(cell); + } + + @Override + protected JComponent createViewPanel() { + final String[] text = myCell.getText(); + if (text == null) return new JLabel(); + ColorPane ansiColoredPane = new ColorPane(); + ansiColoredPane.appendANSI(StringUtil.join(text)); + ansiColoredPane.setBackground(IpnbEditorUtil.getBackground()); + ansiColoredPane.setEditable(false); + return ansiColoredPane; + } + + + public static class ColorPane extends JTextPane { + static final Color D_Red = Color.decode("#8B0000"); + static final Color D_Magenta = JBColor.MAGENTA; + static final Color D_Green = Color.decode("#006400"); + static final Color D_Yellow = Color.decode("#A52A2A"); + static final Color D_Cyan = Color.decode("#5AB4EB"); + + static final Color cReset = JBColor.BLACK; + + static Color currentColor = cReset; + String remaining = ""; + + public void append(Color color, String s) { + StyleContext styleContext = StyleContext.getDefaultStyleContext(); + AttributeSet attributeSet = styleContext.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, color); + int len = getDocument().getLength(); + setCaretPosition(len); + setCharacterAttributes(attributeSet, false); + replaceSelection(s); + } + + public void appendANSI(String string) { + int position = 0; + int index; + int mIndex; + String substring; + boolean continueSearch; + String addString = remaining + string; + remaining = ""; + + if (addString.length() > 0) { + index = addString.indexOf("\u001B"); + if (index == -1) { + append(currentColor, addString); + return; + } + + if (index > 0) { + substring = addString.substring(0, index); + append(currentColor, substring); + position = index; + } + continueSearch = true; + while (continueSearch) { + mIndex = addString.indexOf("m", position); + if (mIndex < 0) { + remaining = addString.substring(position, addString.length()); + continueSearch = false; + continue; + } + else { + substring = addString.substring(position, mIndex + 1); + currentColor = getANSIColor(substring); + } + position = mIndex + 1; + + index = addString.indexOf("\u001B", position); + + if (index == -1) { + substring = addString.substring(position, addString.length()); + append(currentColor, substring); + continueSearch = false; + continue; + } + + substring = addString.substring(position, index); + position = index; + append(currentColor, substring); + } + } + } + + public static Color getANSIColor(String ANSIColor) { + if (ANSIColor.equals("\u001B[30m") || ANSIColor.equals("\u001B[0;30m") || ANSIColor.equals("\u001B[1;30m")) { + return JBColor.BLACK; + } + else if (ANSIColor.equals("\u001B[31m") || ANSIColor.equals("\u001B[0;31m") || ANSIColor.equals("\u001B[1;31m")) { + return D_Red; + } + else if (ANSIColor.equals("\u001B[32m") || ANSIColor.equals("\u001B[0;32m") || ANSIColor.equals("\u001B[1;32m")) { + return D_Green; + } + else if (ANSIColor.equals("\u001B[33m") || ANSIColor.equals("\u001B[0;33m") || ANSIColor.equals("\u001B[1;33m")) { + return D_Yellow; + } + else if (ANSIColor.equals("\u001B[34m") || ANSIColor.equals("\u001B[0;34m") || ANSIColor.equals("\u001B[1;34m")) { + return JBColor.BLUE; + } + else if (ANSIColor.equals("\u001B[35m") || ANSIColor.equals("\u001B[0;35m") || ANSIColor.equals("\u001B[1;35m")) { + return D_Magenta; + } + else if (ANSIColor.equals("\u001B[36m") || ANSIColor.equals("\u001B[0;36m") || ANSIColor.equals("\u001B[1;36m")) { + return D_Cyan; + } + else if (ANSIColor.equals("\u001B[37m") || ANSIColor.equals("\u001B[0;37m") || ANSIColor.equals("\u001B[1;37m")) { + return JBColor.WHITE; + } + else if (ANSIColor.equals("\u001B[0m")) { + return cReset; + } + else { + return JBColor.WHITE; + } + } + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbHtmlPanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbHtmlPanel.java new file mode 100644 index 000000000000..cbc13e6e45fb --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbHtmlPanel.java @@ -0,0 +1,45 @@ +package org.jetbrains.plugins.ipnb.editor.panels.code; + +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; +import javafx.scene.Scene; +import javafx.scene.layout.BorderPane; +import javafx.scene.web.WebView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbHtmlOutputCell; + +import javax.swing.*; + +public class IpnbHtmlPanel extends IpnbCodeOutputPanel { + public IpnbHtmlPanel(@NotNull final IpnbHtmlOutputCell cell) { + super(cell); + } + + @Override + protected JComponent createViewPanel() { + Platform.setImplicitExit(false); + final JFXPanel javafxPanel = new JFXPanel(); + final StringBuilder text = new StringBuilder(""); + for (String html : myCell.getHtmls()) { + html = html.replace("\"", "'"); + text.append(html); + } + text.append(""); + Platform.runLater(new Runnable() { + @Override + public void run() { + + BorderPane borderPane = new BorderPane(); + WebView webComponent = new WebView(); + + webComponent.getEngine().loadContent(text.toString()); + + borderPane.setCenter(webComponent); + Scene scene = new Scene(borderPane, 450, 450); + javafxPanel.setScene(scene); + } + }); + + return javafxPanel; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbImagePanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbImagePanel.java new file mode 100644 index 000000000000..afd731dad805 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbImagePanel.java @@ -0,0 +1,42 @@ +package org.jetbrains.plugins.ipnb.editor.panels.code; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.ui.components.JBLabel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbImageOutputCell; +import sun.misc.BASE64Decoder; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class IpnbImagePanel extends IpnbCodeOutputPanel { + private static final Logger LOG = Logger.getInstance(IpnbImagePanel.class); + + public IpnbImagePanel(@NotNull final IpnbImageOutputCell cell) { + super(cell); + } + + @Override + protected JComponent createViewPanel() { + final String png = myCell.getBase64String(); + + final JBLabel label = new JBLabel(); + try { + byte[] btDataFile = new BASE64Decoder().decodeBuffer(png); + BufferedImage image = ImageIO.read(new ByteArrayInputStream(btDataFile)); + label.setIcon(new ImageIcon(image)); + } + catch (IOException e) { + LOG.error("Couldn't parse image. " + e.getMessage()); + } + + label.setBackground(IpnbEditorUtil.getBackground()); + label.setOpaque(true); + + return label; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbLatexPanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbLatexPanel.java new file mode 100644 index 000000000000..2dc6924c4290 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbLatexPanel.java @@ -0,0 +1,29 @@ +package org.jetbrains.plugins.ipnb.editor.panels.code; + +import com.intellij.openapi.ui.VerticalFlowLayout; +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.IpnbUtils; +import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbLatexOutputCell; + +import javax.swing.*; +import java.awt.*; + +public class IpnbLatexPanel extends IpnbCodeOutputPanel { + public IpnbLatexPanel(@NotNull final IpnbLatexOutputCell cell) { + super(cell); + setLayout(new VerticalFlowLayout(FlowLayout.LEFT)); + } + + @Override + protected JComponent createViewPanel() { + final JPanel panel = new JPanel(); + IpnbUtils.addLatexToPanel(StringUtil.join(myCell.getLatex()), panel); + setBackground(IpnbEditorUtil.getBackground()); + panel.setBackground(IpnbEditorUtil.getBackground()); + panel.setOpaque(true); + setOpaque(true); + return panel; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbStreamPanel.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbStreamPanel.java new file mode 100644 index 000000000000..d721f5b9c902 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/editor/panels/code/IpnbStreamPanel.java @@ -0,0 +1,10 @@ +package org.jetbrains.plugins.ipnb.editor.panels.code; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbStreamOutputCell; + +public class IpnbStreamPanel extends IpnbCodeOutputPanel { + public IpnbStreamPanel(@NotNull final IpnbStreamOutputCell cell) { + super(cell); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/IpnbFile.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/IpnbFile.java new file mode 100644 index 000000000000..50a0f6af1786 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/IpnbFile.java @@ -0,0 +1,38 @@ +package org.jetbrains.plugins.ipnb.format; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.format.cells.IpnbCell; + +import java.util.List; + +public class IpnbFile { + private final IpnbParser.IpnbFileRaw myRawFile; + private final List myCells; + private final String myPath; + + IpnbFile(IpnbParser.IpnbFileRaw rawFile, List cells, String path) { + myRawFile = rawFile; + myCells = cells; + myPath = path; + } + + public List getCells() { + return myCells; + } + + public void addCell(@NotNull final IpnbCell cell, int index) { + myCells.add(index, cell); + } + + public void removeCell(int index) { + myCells.remove(index); + } + + public String getPath() { + return myPath; + } + + public IpnbParser.IpnbFileRaw getRawFile() { + return myRawFile; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/IpnbParser.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/IpnbParser.java new file mode 100644 index 000000000000..f7d545bb19ae --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/IpnbParser.java @@ -0,0 +1,253 @@ +package org.jetbrains.plugins.ipnb.format; + +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.vfs.CharsetToolkit; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbEditablePanel; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel; +import org.jetbrains.plugins.ipnb.format.cells.*; +import org.jetbrains.plugins.ipnb.format.cells.output.*; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class IpnbParser { + private static final Logger LOG = Logger.getInstance(IpnbParser.class); + private static final Gson gson = initGson(); + + @NotNull + private static Gson initGson() { + final GsonBuilder builder = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping(); + return builder.create(); + } + + @NotNull + public static IpnbFile parseIpnbFile(@NotNull String fileText, String path) throws IOException { + IpnbFileRaw rawFile = gson.fromJson(fileText, IpnbFileRaw.class); + if (rawFile == null) return new IpnbFile(new IpnbFileRaw(), Lists.newArrayList(), path); + List cells = new ArrayList(); + final IpnbWorksheet[] worksheets = rawFile.worksheets; + for (IpnbWorksheet worksheet : worksheets) { + final IpnbCellRaw[] rawCells = worksheet.cells; + for (IpnbCellRaw rawCell : rawCells) { + cells.add(rawCell.createCell()); + } + } + return new IpnbFile(rawFile, cells, path); + } + + @NotNull + public static IpnbFile parseIpnbFile(@NotNull VirtualFile virtualFile) throws IOException { + final String fileText = new String(virtualFile.contentsToByteArray(), CharsetToolkit.UTF8); + return parseIpnbFile(fileText, virtualFile.getPath()); + } + + public static void saveIpnbFile(@NotNull final IpnbFilePanel ipnbPanel) { + final IpnbFile ipnbFile = ipnbPanel.getIpnbFile(); + if (ipnbFile == null) return; + for (IpnbEditablePanel panel : ipnbPanel.getIpnbPanels()) { + if (panel.isModified()) { + panel.updateCellSource(); + } + } + + final IpnbFileRaw fileRaw = ipnbFile.getRawFile(); + final IpnbWorksheet worksheet = new IpnbWorksheet(); + final ArrayList cellRaws = new ArrayList(); + for (IpnbCell cell: ipnbFile.getCells()) { + cellRaws.add(IpnbCellRaw.fromCell(cell)); + } + worksheet.cells = cellRaws.toArray(new IpnbCellRaw[cellRaws.size()]); + fileRaw.worksheets = new IpnbWorksheet[]{worksheet}; + final String json = gson.toJson(fileRaw); + final String path = ipnbFile.getPath(); + final File file = new File(path); + FileWriter writer = null; + try { + writer = new FileWriter(file); + writer.write(json); + } catch (IOException e) { + LOG.error(e); + } + finally { + try { + if (writer != null) + writer.close(); + } catch (IOException e) { + LOG.error(e); + } + } + } + + public static class IpnbFileRaw { + Map metadata; + int nbformat; + int nbformat_minor; + IpnbWorksheet[] worksheets; + } + + private static class IpnbWorksheet { + IpnbCellRaw[] cells; + } + private static class IpnbCellRaw { + String cell_type; + Integer level; + String[] source; + String[] input; + String language; + CellOutputRaw[] outputs; + Integer prompt_number; + + public static IpnbCellRaw fromCell(@NotNull final IpnbCell cell) { + final IpnbCellRaw raw = new IpnbCellRaw(); + if (cell instanceof IpnbMarkdownCell) { + raw.cell_type = "markdown"; + raw.source = ((IpnbMarkdownCell)cell).getSource(); + } + else if (cell instanceof IpnbCodeCell) { + raw.cell_type = "code"; + final ArrayList outputRaws = new ArrayList(); + for (IpnbOutputCell outputCell : ((IpnbCodeCell)cell).getCellOutputs()) { + outputRaws.add(CellOutputRaw.fromOutput(outputCell)); + } + raw.outputs = outputRaws.toArray(new CellOutputRaw[outputRaws.size()]); + raw.language = ((IpnbCodeCell)cell).getLanguage(); + raw.input = ((IpnbCodeCell)cell).getSource(); + raw.prompt_number = ((IpnbCodeCell)cell).getPromptNumber(); + } + else if (cell instanceof IpnbRawCell) { + raw.cell_type = "raw"; + } + else if (cell instanceof IpnbHeadingCell) { + raw.cell_type = "heading"; + raw.source = ((IpnbHeadingCell)cell).getSource(); + raw.level = ((IpnbHeadingCell)cell).getLevel(); + } + return raw; + } + + public IpnbCell createCell() { + final IpnbCell cell; + if (cell_type.equals("markdown")) { + cell = new IpnbMarkdownCell(source); + } + else if (cell_type.equals("code")) { + final List outputCells = new ArrayList(); + for (CellOutputRaw outputRaw : outputs) { + outputCells.add(outputRaw.createOutput()); + } + cell = new IpnbCodeCell(language, input, prompt_number, outputCells); + } + else if (cell_type.equals("raw")) { + cell = new IpnbRawCell(); + } + else if (cell_type.equals("heading")) { + cell = new IpnbHeadingCell(source, level); + } + else { + cell = null; + } + return cell; + } + } + + private static class CellOutputRaw { + String ename; + String evalue; + String output_type; + String png; + String stream; + String jpeg; + String[] html; + String[] latex; + String[] svg; + Integer prompt_number; + String[] text; + String[] traceback; + + + public static CellOutputRaw fromOutput(@NotNull final IpnbOutputCell outputCell) { + final CellOutputRaw raw = new CellOutputRaw(); + + if (outputCell instanceof IpnbPngOutputCell) { + raw.png = ((IpnbPngOutputCell)outputCell).getBase64String(); + raw.text = outputCell.getText(); + //raw.output_type = "display_data"; + } + else if (outputCell instanceof IpnbSvgOutputCell) { + raw.svg = ((IpnbSvgOutputCell)outputCell).getSvg(); + raw.text = outputCell.getText(); + } + else if (outputCell instanceof IpnbJpegOutputCell) { + raw.jpeg = ((IpnbJpegOutputCell)outputCell).getBase64String(); + raw.text = outputCell.getText(); + } + else if (outputCell instanceof IpnbLatexOutputCell) { + raw.latex = ((IpnbLatexOutputCell)outputCell).getLatex(); + raw.prompt_number = outputCell.getPromptNumber(); + raw.text = outputCell.getText(); + } + else if (outputCell instanceof IpnbStreamOutputCell) { + raw.stream = ((IpnbStreamOutputCell)outputCell).getStream(); + raw.output_type = "stream"; + raw.text = outputCell.getText(); + } + else if (outputCell instanceof IpnbHtmlOutputCell) { + raw.html = ((IpnbHtmlOutputCell)outputCell).getHtmls(); + raw.text = outputCell.getText(); + } + else if (outputCell instanceof IpnbErrorOutputCell) { + raw.output_type = "pyerr"; + raw.evalue = ((IpnbErrorOutputCell)outputCell).getEvalue(); + raw.ename = ((IpnbErrorOutputCell)outputCell).getEname(); + raw.traceback = outputCell.getText(); + } + else if (outputCell instanceof IpnbOutOutputCell) { + raw.output_type = "pyout"; + raw.text = outputCell.getText(); + raw.prompt_number = outputCell.getPromptNumber(); + } + return raw; + } + + public IpnbOutputCell createOutput() { + final IpnbOutputCell outputCell; + if (png != null) { + outputCell = new IpnbPngOutputCell(png, text, prompt_number); + } + else if (jpeg != null) { + outputCell = new IpnbJpegOutputCell(jpeg, text, prompt_number); + } + else if (svg != null) { + outputCell = new IpnbSvgOutputCell(svg, text, prompt_number); + } + else if (latex != null) { + outputCell = new IpnbLatexOutputCell(latex, prompt_number, text); + } + else if (stream != null) { + outputCell = new IpnbStreamOutputCell(stream, text, prompt_number); + } + else if (html != null) { + outputCell = new IpnbHtmlOutputCell(html, text, prompt_number); + } + else if ("pyerr".equals(output_type)) { + outputCell = new IpnbErrorOutputCell(evalue, ename, traceback, prompt_number); + } + else if ("pyout".equals(output_type)) { + outputCell = new IpnbOutOutputCell(text, prompt_number); + } + else { + outputCell = new IpnbOutputCell(text, prompt_number); + } + return outputCell; + } + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbCell.java new file mode 100644 index 000000000000..c5fc0c6c5654 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbCell.java @@ -0,0 +1,4 @@ +package org.jetbrains.plugins.ipnb.format.cells; + +public interface IpnbCell { +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbCodeCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbCodeCell.java new file mode 100644 index 000000000000..766b6e3d3c12 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbCodeCell.java @@ -0,0 +1,58 @@ +package org.jetbrains.plugins.ipnb.format.cells; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbOutputCell; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class IpnbCodeCell extends IpnbEditableCell { + @NotNull private final String myLanguage; + @Nullable private Integer myPromptNumber; + @NotNull private final List myCellOutputs; + + public IpnbCodeCell(@NotNull final String language, + @NotNull final String[] input, + @Nullable final Integer number, + @NotNull final List cellOutputs) { + super(input); + myLanguage = language; + myPromptNumber = number; + myCellOutputs = cellOutputs; + } + + @NotNull + public String getLanguage() { + return myLanguage; + } + + @Nullable + public Integer getPromptNumber() { + return myPromptNumber; + } + + public void setPromptNumber(@Nullable Integer promptNumber) { + myPromptNumber = promptNumber; + } + + @NotNull + public List getCellOutputs() { + return myCellOutputs; + } + + public void removeCellOutputs() { + myCellOutputs.clear(); + } + + public void addCellOutput(@NotNull final IpnbOutputCell cellOutput) { + myCellOutputs.add(cellOutput); + } + + @SuppressWarnings({"CloneDoesntCallSuperClone", "CloneDoesntDeclareCloneNotSupportedException"}) + @Override + public Object clone() { + return new IpnbCodeCell(myLanguage, Arrays.copyOf(getSource(), getSource().length), myPromptNumber, new ArrayList(myCellOutputs)); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbEditableCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbEditableCell.java new file mode 100644 index 000000000000..ea60c10d78c7 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbEditableCell.java @@ -0,0 +1,26 @@ +package org.jetbrains.plugins.ipnb.format.cells; + +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; + +public abstract class IpnbEditableCell implements IpnbCell { + @NotNull private String[] mySource; + + IpnbEditableCell(@NotNull final String[] source) { + mySource = source; + } + + @NotNull + public String[] getSource() { + return mySource; + } + + public void setSource(@NotNull final String[] source) { + mySource = source; + } + + @NotNull + public String getSourceAsString() { + return StringUtil.join(mySource); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbHeadingCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbHeadingCell.java new file mode 100644 index 000000000000..41c1d8998356 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbHeadingCell.java @@ -0,0 +1,26 @@ +package org.jetbrains.plugins.ipnb.format.cells; + +import org.jetbrains.annotations.NotNull; + +public class IpnbHeadingCell extends IpnbEditableCell { + private int myLevel; + + public IpnbHeadingCell(@NotNull final String[] source, int level) { + super(source); + myLevel = level; + } + + public int getLevel() { + return myLevel; + } + + public void setLevel(int level) { + myLevel = level; + } + + @SuppressWarnings({"CloneDoesntCallSuperClone", "CloneDoesntDeclareCloneNotSupportedException"}) + @Override + public Object clone() { + return new IpnbHeadingCell(getSource(), myLevel); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbMarkdownCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbMarkdownCell.java new file mode 100644 index 000000000000..772f739f63a1 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbMarkdownCell.java @@ -0,0 +1,15 @@ +package org.jetbrains.plugins.ipnb.format.cells; + +import org.jetbrains.annotations.NotNull; + +public class IpnbMarkdownCell extends IpnbEditableCell { + public IpnbMarkdownCell(@NotNull final String[] source) { + super(source); + } + + @SuppressWarnings({"CloneDoesntCallSuperClone", "CloneDoesntDeclareCloneNotSupportedException"}) + @Override + public Object clone() { + return new IpnbMarkdownCell(getSource()); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbRawCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbRawCell.java new file mode 100644 index 000000000000..f9ca8b70e420 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/IpnbRawCell.java @@ -0,0 +1,4 @@ +package org.jetbrains.plugins.ipnb.format.cells; + +public class IpnbRawCell implements IpnbCell { +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbErrorOutputCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbErrorOutputCell.java new file mode 100644 index 000000000000..fc1ebcc517bd --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbErrorOutputCell.java @@ -0,0 +1,27 @@ +package org.jetbrains.plugins.ipnb.format.cells.output; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class IpnbErrorOutputCell extends IpnbOutputCell { + + @NotNull private final String myEvalue; + @NotNull private final String myEname; + + public IpnbErrorOutputCell(@NotNull final String evalue, @NotNull final String ename, @NotNull final String[] traceback, + @Nullable final Integer prompt) { + super(traceback, prompt); + myEvalue = evalue; + myEname = ename; + } + + @NotNull + public String getEvalue() { + return myEvalue; + } + + @NotNull + public String getEname() { + return myEname; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbHtmlOutputCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbHtmlOutputCell.java new file mode 100644 index 000000000000..cd2f53be1f67 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbHtmlOutputCell.java @@ -0,0 +1,18 @@ +package org.jetbrains.plugins.ipnb.format.cells.output; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class IpnbHtmlOutputCell extends IpnbOutputCell { + @NotNull private final String[] myHtml; + + public IpnbHtmlOutputCell(@NotNull final String[] html, String[] text, @Nullable final Integer prompt) { + super(text, prompt); + myHtml = html; + } + + @NotNull + public String[] getHtmls() { + return myHtml; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbImageOutputCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbImageOutputCell.java new file mode 100644 index 000000000000..8224203085da --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbImageOutputCell.java @@ -0,0 +1,18 @@ +package org.jetbrains.plugins.ipnb.format.cells.output; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class IpnbImageOutputCell extends IpnbOutputCell { + @NotNull private final String myBase64String; + + public IpnbImageOutputCell(@NotNull final String base64String, @Nullable final String[] text, @Nullable final Integer prompt) { + super(text, prompt); + myBase64String = base64String; + } + + @NotNull + public String getBase64String() { + return myBase64String; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbJpegOutputCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbJpegOutputCell.java new file mode 100644 index 000000000000..29faafa60f31 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbJpegOutputCell.java @@ -0,0 +1,12 @@ +package org.jetbrains.plugins.ipnb.format.cells.output; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class IpnbJpegOutputCell extends IpnbImageOutputCell { + + public IpnbJpegOutputCell(@NotNull final String jpeg, @Nullable final String[] text, @Nullable final Integer prompt) { + super(jpeg, text, prompt); + } + +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbLatexOutputCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbLatexOutputCell.java new file mode 100644 index 000000000000..39b3078ed910 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbLatexOutputCell.java @@ -0,0 +1,17 @@ +package org.jetbrains.plugins.ipnb.format.cells.output; + +import org.jetbrains.annotations.NotNull; + +public class IpnbLatexOutputCell extends IpnbOutputCell { + @NotNull private final String[] myLatex; + + public IpnbLatexOutputCell(@NotNull final String[] latex, Integer promptNumber, @NotNull final String[] text) { + super(text, promptNumber); + myLatex = latex; + } + + @NotNull + public String[] getLatex() { + return myLatex; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbOutOutputCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbOutOutputCell.java new file mode 100644 index 000000000000..4d5e1c9cd226 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbOutOutputCell.java @@ -0,0 +1,9 @@ +package org.jetbrains.plugins.ipnb.format.cells.output; + +import org.jetbrains.annotations.Nullable; + +public class IpnbOutOutputCell extends IpnbOutputCell { + public IpnbOutOutputCell(@Nullable final String[] text, Integer promptNumber) { + super(text, promptNumber); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbOutputCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbOutputCell.java new file mode 100644 index 000000000000..27be1a5a0d14 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbOutputCell.java @@ -0,0 +1,30 @@ +package org.jetbrains.plugins.ipnb.format.cells.output; + +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.ipnb.format.cells.IpnbCell; + +public class IpnbOutputCell implements IpnbCell { + @Nullable protected final Integer myPromptNumber; + @Nullable private final String[] myText; + + public IpnbOutputCell(@Nullable final String[] text, @Nullable final Integer promptNumber) { + myText = text; + myPromptNumber = promptNumber; + } + + @Nullable + public String[] getText() { + return myText; + } + + @Nullable + public String getSourceAsString() { + return myText == null ? null : StringUtil.join(myText, "\n"); + } + + @Nullable + public Integer getPromptNumber() { + return myPromptNumber; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbPngOutputCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbPngOutputCell.java new file mode 100644 index 000000000000..b100a3c312f0 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbPngOutputCell.java @@ -0,0 +1,12 @@ +package org.jetbrains.plugins.ipnb.format.cells.output; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class IpnbPngOutputCell extends IpnbImageOutputCell { + + public IpnbPngOutputCell(@NotNull final String png, @Nullable final String[] text, @Nullable final Integer prompt) { + super(png, text, prompt); + } + +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbStreamOutputCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbStreamOutputCell.java new file mode 100644 index 000000000000..b76d97e1ed28 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbStreamOutputCell.java @@ -0,0 +1,18 @@ +package org.jetbrains.plugins.ipnb.format.cells.output; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class IpnbStreamOutputCell extends IpnbOutputCell { + @NotNull private final String myStream; + + public IpnbStreamOutputCell(@NotNull final String stream, String[] text, @Nullable final Integer prompt) { + super(text, prompt); + myStream = stream; + } + + @NotNull + public String getStream() { + return myStream; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbSvgOutputCell.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbSvgOutputCell.java new file mode 100644 index 000000000000..5b476a8534d6 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/format/cells/output/IpnbSvgOutputCell.java @@ -0,0 +1,18 @@ +package org.jetbrains.plugins.ipnb.format.cells.output; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class IpnbSvgOutputCell extends IpnbOutputCell { + @NotNull private final String[] mySvg; + + public IpnbSvgOutputCell(@NotNull final String[] svg, @Nullable final String[] text, @Nullable final Integer prompt) { + super(text, prompt); + mySvg = svg; + } + + @NotNull + public String[] getSvg() { + return mySvg; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/protocol/IpnbConnection.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/protocol/IpnbConnection.java new file mode 100644 index 000000000000..f10e88856ef1 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/protocol/IpnbConnection.java @@ -0,0 +1,434 @@ +package org.jetbrains.plugins.ipnb.protocol; + +import com.google.gson.*; +import com.intellij.openapi.util.text.StringUtil; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.drafts.Draft; +import org.java_websocket.drafts.Draft_17; +import org.java_websocket.handshake.ClientHandshakeBuilder; +import org.java_websocket.handshake.ServerHandshake; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.format.cells.output.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.*; +import java.util.*; + +/** +* @author vlan +*/ +public class IpnbConnection { + private static final String API_URL = "/api"; + private static final String KERNELS_URL = API_URL + "/kernels"; + private static final String HTTP_POST = "POST"; + public static final String HTTP_DELETE = "DELETE"; + + @NotNull private final URI myURI; + @NotNull private final String myKernelId; + @NotNull private final String mySessionId; + @NotNull private final IpnbConnectionListener myListener; + @NotNull private final WebSocketClient myShellClient; + @NotNull private final WebSocketClient myIOPubClient; + @NotNull private final Thread myShellThread; + @NotNull private final Thread myIOPubThread; + + private volatile boolean myIsShellOpen = false; + private volatile boolean myIsIOPubOpen = false; + private volatile boolean myIsOpened = false; + + public IpnbConnection(@NotNull URI uri, @NotNull IpnbConnectionListener listener) throws IOException, URISyntaxException { + myURI = uri; + myListener = listener; + mySessionId = UUID.randomUUID().toString(); + myKernelId = startKernel(); + + final Draft draft = new Draft17WithOrigin(); + // TODO: Serialize cookies for the authentication message + final String authMessage = "identity:foo"; + + myShellClient = new WebSocketClient(getShellURI(), draft) { + @Override + public void onOpen(@NotNull ServerHandshake handshakeData) { + send(authMessage); + myIsShellOpen = true; + notifyOpen(); + } + + @Override + public void onMessage(@NotNull String message) { + } + + @Override + public void onClose(int code, @NotNull String reason, boolean remote) { + } + + @Override + public void onError(@NotNull Exception e) { + } + }; + myShellThread = new Thread(myShellClient); + myShellThread.start(); + + myIOPubClient = new WebSocketClient(getIOPubURI(), draft) { + private ArrayList myOutput = new ArrayList(); + private Integer myExecCount = null; + @Override + public void onOpen(ServerHandshake handshakeData) { + send(authMessage); + myIsIOPubOpen = true; + notifyOpen(); + } + + @Override + public void onMessage(String message) { + final Gson gson = new Gson(); + final Message msg = gson.fromJson(message, Message.class); + final Header header = msg.getHeader(); + final Header parentHeader = gson.fromJson(msg.getParentHeader(), Header.class); + final String messageType = header.getMessageType(); + if ("pyout".equals(messageType) || "display_data".equals(messageType)) { + final PyOutContent content = gson.fromJson(msg.getContent(), PyOutContent.class); + addCellOutput(content, myOutput); + } + else if ("pyerr".equals(messageType)) { + final PyErrContent content = gson.fromJson(msg.getContent(), PyErrContent.class); + addCellOutput(content, myOutput); + } + else if ("stream".equals(messageType)) { + final PyStreamContent content = gson.fromJson(msg.getContent(), PyStreamContent.class); + addCellOutput(content, myOutput); + } + else if ("pyin".equals(messageType)) { + final JsonElement executionCount = msg.getContent().get("execution_count"); + if (executionCount != null) { + myExecCount = executionCount.getAsInt(); + } + } + else if ("status".equals(messageType)) { + final PyStatusContent content = gson.fromJson(msg.getContent(), PyStatusContent.class); + if (content.getExecutionState().equals("idle")) { + //noinspection unchecked + myListener.onOutput(IpnbConnection.this, parentHeader.getMessageId(), (List)myOutput.clone(), myExecCount); + myOutput.clear(); + } + } + } + + @Override + public void onClose(int code, String reason, boolean remote) { + + } + + @Override + public void onError(Exception ex) { + + } + }; + myIOPubThread = new Thread(myIOPubClient); + myIOPubThread.start(); + } + + private void notifyOpen() { + if (!myIsOpened && myIsShellOpen && myIsIOPubOpen) { + myIsOpened = true; + myListener.onOpen(this); + } + } + + @NotNull + public String execute(@NotNull String code) { + final String messageId = UUID.randomUUID().toString(); + myShellClient.send(new Gson().toJson(createExecuteRequest(code, messageId))); + return messageId; + } + + public void shutdown() { + myIOPubClient.close(); + myShellClient.close(); + } + + public void close() throws IOException, InterruptedException { + myIOPubThread.join(); + myShellThread.join(); + shutdownKernel(); + } + + @NotNull + public String getKernelId() { + return myKernelId; + } + + @NotNull + private String startKernel() throws IOException { + final String s = httpRequest(myURI + KERNELS_URL, HTTP_POST); + final Gson gson = new Gson(); + final Kernel kernel = gson.fromJson(s, Kernel.class); + return kernel.getId(); + } + + private void shutdownKernel() throws IOException { + httpRequest(myURI + KERNELS_URL + "/" + myKernelId, HTTP_DELETE); + } + + @NotNull + public URI getShellURI() throws URISyntaxException { + return new URI(getWebSocketURIBase() + "/shell"); + } + + @NotNull + public URI getIOPubURI() throws URISyntaxException { + return new URI(getWebSocketURIBase() + "/iopub"); + } + + @NotNull + private String getWebSocketURIBase() { + return "ws://" + myURI.getAuthority() + KERNELS_URL + "/" + myKernelId; + } + + @NotNull + private static String httpRequest(@NotNull String url, @NotNull String method) throws IOException { + final URLConnection urlConnection = new URL(url).openConnection(); + if (urlConnection instanceof HttpURLConnection) { + final HttpURLConnection connection = (HttpURLConnection)urlConnection; + connection.setRequestMethod(method); + final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8")); + try { + final StringBuilder builder = new StringBuilder(); + char[] buffer = new char[4096]; + int n; + while ((n = reader.read(buffer)) != -1) { + builder.append(buffer, 0, n); + } + return builder.toString(); + } + finally { + reader.close(); + } + } + else { + throw new UnsupportedOperationException("Only HTTP URLs are supported"); + } + } + + @NotNull + public Message createExecuteRequest(String code, String messageId) { + final JsonObject content = new JsonObject(); + content.addProperty("code", code); + content.addProperty("silent", false); + content.add("user_variables", new JsonArray()); + content.add("output_type", new JsonPrimitive("")); + content.add("user_expressions", new JsonObject()); + content.addProperty("allow_stdin", false); + + return createMessage("execute_request", content, messageId); + } + + private Message createMessage(String messageType, JsonObject content, String messageId) { + final Header header = Header.create(messageId, "username", mySessionId, messageType); + final JsonObject parentHeader = new JsonObject(); + + final JsonObject metadata = new JsonObject(); + + return Message.create(header, parentHeader, metadata, content); + } + + @SuppressWarnings("UnusedDeclaration") + private static class Kernel { + @NotNull private String id; + + @NotNull + public String getId() { + return id; + } + } + + @SuppressWarnings("UnusedDeclaration") + private static class Header { + private String msg_id; + private String username; + private String session; + private String msg_type; + + @NotNull + public static Header create(String messageId, String username, String sessionId, String messageType) { + final Header header = new Header(); + header.msg_id = messageId; + header.username = username; + header.session = sessionId; + header.msg_type = messageType; + return header; + } + + public String getMessageId() { + return msg_id; + } + + public String getUsername() { + return username; + } + + public String getSessionId() { + return session; + } + + public String getMessageType() { + return msg_type; + } + } + + @SuppressWarnings("UnusedDeclaration") + private static class Message { + private Header header; + private JsonObject parent_header; + private JsonObject metadata; + private JsonObject content; + + public static Message create(Header header, JsonObject parentHeader, JsonObject metadata, JsonObject content) { + final Message message = new Message(); + message.header = header; + message.parent_header = parentHeader; + message.metadata = metadata; + message.content = content; + return message; + } + + public Header getHeader() { + return header; + } + + public JsonObject getParentHeader() { + return parent_header; + } + + public JsonObject getMetadata() { + return metadata; + } + + public JsonObject getContent() { + return content; + } + } + + private void addCellOutput(@NotNull final PyContent content, ArrayList output) { + if (content instanceof PyErrContent) { + output.add(new IpnbErrorOutputCell(((PyErrContent)content).getEvalue(), + ((PyErrContent)content).getEname(), ((PyErrContent)content).getTraceback(), null)); + } + else if (content instanceof PyStreamContent) { + final String data = ((PyStreamContent)content).getData(); + output.add(new IpnbStreamOutputCell(data, new String[]{data}, null)); + } + else if (content instanceof PyOutContent) { + final Map data = ((PyOutContent)content).getData(); + if (data.containsKey("text/latex")) { + final String text = data.get("text/latex"); + final String plainText = data.get("text/plain"); + output.add(new IpnbLatexOutputCell(new String[]{text}, null, new String[]{plainText})); + } + else if (data.containsKey("text/html")) { + final String html = data.get("text/html"); + output.add(new IpnbHtmlOutputCell(StringUtil.splitByLinesKeepSeparators(html), StringUtil.splitByLinesKeepSeparators(html), null)); + } + else if (data.containsKey("image/png")) { + final String png = data.get("image/png"); + final String plainText = data.get("text/plain"); + output.add(new IpnbPngOutputCell(png, StringUtil.splitByLinesKeepSeparators(plainText), null)); + } + else if (data.containsKey("image/jpeg")) { + final String jpeg = data.get("image/jpeg"); + final String plainText = data.get("text/plain"); + output.add(new IpnbJpegOutputCell(jpeg, StringUtil.splitByLinesKeepSeparators(plainText), null)); + } + else if (data.containsKey("image/svg")) { + final String svg = data.get("image/svg"); + final String plainText = data.get("text/plain"); + output.add(new IpnbSvgOutputCell(StringUtil.splitByLinesKeepSeparators(svg), StringUtil.splitByLinesKeepSeparators(plainText), null)); + } + else { + for (Map.Entry entry : data.entrySet()) { + output.add(new IpnbOutOutputCell(new String[]{entry.getValue()}, null)); + } + } + } + } + + private interface PyContent {} + + @SuppressWarnings("UnusedDeclaration") + private static class PyOutContent implements PyContent { + private int execution_count; + private HashMap data; + private JsonObject metadata; + + public int getExecutionCount() { + return execution_count; + } + + public Map getData() { + return data; + } + + public JsonObject getMetadata() { + return metadata; + } + } + + @SuppressWarnings("UnusedDeclaration") + private static class PyErrContent implements PyContent { + private String ename; + private String evalue; + private String[] traceback; + + public String getEname() { + return ename; + } + + public String getEvalue() { + return evalue; + } + + public String[] getTraceback() { + return traceback; + } + } + + @SuppressWarnings("UnusedDeclaration") + private static class PyStreamContent implements PyContent { + private String data; + private String name; + + public String getData() { + return data; + } + + public String getName() { + return name; + } + } + + @SuppressWarnings("UnusedDeclaration") + private static class PyStatusContent { + private String execution_state; + + public String getExecutionState() { + return execution_state; + } + } + + private class Draft17WithOrigin extends Draft_17 { + @Override + public Draft copyInstance() { + return new Draft17WithOrigin(); + } + + @NotNull + @Override + public ClientHandshakeBuilder postProcessHandshakeRequestAsClient(@NotNull ClientHandshakeBuilder request) { + super.postProcessHandshakeRequestAsClient(request); + request.put("Origin", myURI.toString()); + return request; + } + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/protocol/IpnbConnectionListener.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/protocol/IpnbConnectionListener.java new file mode 100644 index 000000000000..8def54cb44a0 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/protocol/IpnbConnectionListener.java @@ -0,0 +1,20 @@ +package org.jetbrains.plugins.ipnb.protocol; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbOutputCell; + +import java.util.List; + +/** + * TODO: Expose execution counter via API + * + * @author vlan + */ +public interface IpnbConnectionListener { + void onOpen(@NotNull IpnbConnection connection); + void onOutput(@NotNull IpnbConnection connection, + @NotNull String parentMessageId, + @NotNull List outputs, + @Nullable Integer execCount); +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/protocol/IpnbConnectionListenerBase.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/protocol/IpnbConnectionListenerBase.java new file mode 100644 index 000000000000..2fb6fa5881ec --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/protocol/IpnbConnectionListenerBase.java @@ -0,0 +1,23 @@ +package org.jetbrains.plugins.ipnb.protocol; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbOutputCell; + +import java.util.List; + +/** + * @author vlan + */ +public class IpnbConnectionListenerBase implements IpnbConnectionListener { + @Override + public void onOpen(@NotNull IpnbConnection connection) { + } + + @Override + public void onOutput(@NotNull IpnbConnection connection, + @NotNull String parentMessageId, + @NotNull List outputs, + @Nullable Integer execCount) { + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyFileElementType.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyFileElementType.java new file mode 100644 index 000000000000..5ab8112e10c4 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyFileElementType.java @@ -0,0 +1,17 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.intellij.lang.Language; +import com.jetbrains.python.psi.PyFileElementType; +import org.jetbrains.annotations.NotNull; + +public class IpnbPyFileElementType extends PyFileElementType { + public IpnbPyFileElementType(Language language) { + super(language); + } + + @NotNull + @Override + public String getExternalId() { + return "IpnbFile.Python"; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyFileType.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyFileType.java new file mode 100644 index 000000000000..35a8386b038b --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyFileType.java @@ -0,0 +1,30 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.jetbrains.python.PythonFileType; +import org.jetbrains.annotations.NotNull; + +public class IpnbPyFileType extends PythonFileType { + public static PythonFileType INSTANCE = new IpnbPyFileType(); + + protected IpnbPyFileType() { + super(new IpnbPyLanguageDialect()); + } + + @NotNull + @Override + public String getName() { + return "Ipnb Python"; + } + + @NotNull + @Override + public String getDescription() { + return "Ipnb Python"; + } + + @NotNull + @Override + public String getDefaultExtension() { + return "ipnb_py"; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyFragment.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyFragment.java new file mode 100644 index 000000000000..dc1b279732f3 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyFragment.java @@ -0,0 +1,76 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.intellij.openapi.project.Project; +import com.intellij.psi.FileViewProvider; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiManager; +import com.intellij.psi.SingleRootFileViewProvider; +import com.intellij.psi.impl.PsiManagerEx; +import com.intellij.psi.impl.file.impl.FileManager; +import com.intellij.psi.impl.source.tree.FileElement; +import com.intellij.testFramework.LightVirtualFile; +import com.jetbrains.python.psi.impl.PyFileImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel; +import org.jetbrains.plugins.ipnb.editor.panels.code.IpnbCodeSourcePanel; + +public class IpnbPyFragment extends PyFileImpl { + private PsiElement myContext; + private boolean myPhysical; + private final IpnbFilePanel myFilePanel; + private final IpnbCodeSourcePanel myCodeSourcePanel; + private FileViewProvider myViewProvider; + + public IpnbPyFragment(Project project, CharSequence text, boolean isPhysical, IpnbCodeSourcePanel codeSourcePanel) { + super(((PsiManagerEx)PsiManager.getInstance(project)).getFileManager().createFileViewProvider( + new LightVirtualFile("code.py", IpnbPyLanguageDialect.getInstance(), text), isPhysical) + ); + myPhysical = isPhysical; + myCodeSourcePanel = codeSourcePanel; + myFilePanel = codeSourcePanel.getIpnbCodePanel().getFileEditor().getIpnbFilePanel(); + ((SingleRootFileViewProvider)getViewProvider()).forceCachedPsi(this); + } + + public IpnbCodeSourcePanel getCodeSourcePanel() { + return myCodeSourcePanel; + } + + protected IpnbPyFragment clone() { + final IpnbPyFragment clone = (IpnbPyFragment)cloneImpl((FileElement)calcTreeElement().clone()); + clone.myPhysical = false; + clone.myOriginalFile = this; + FileManager fileManager = ((PsiManagerEx)getManager()).getFileManager(); + SingleRootFileViewProvider cloneViewProvider = (SingleRootFileViewProvider)fileManager.createFileViewProvider(new LightVirtualFile(getName(), getLanguage(), getText()), false); + cloneViewProvider.forceCachedPsi(clone); + clone.myViewProvider = cloneViewProvider; + return clone; + } + + public PsiElement getContext() { + return myContext; + } + + @NotNull + public FileViewProvider getViewProvider() { + if(myViewProvider != null) return myViewProvider; + return super.getViewProvider(); + } + + public boolean isValid() { + if (!super.isValid()) return false; + if (myContext != null && !myContext.isValid()) return false; + return true; + } + + public boolean isPhysical() { + return myPhysical; + } + + public void setContext(PsiElement context) { + myContext = context; + } + + public IpnbFilePanel getFilePanel() { + return myFilePanel; + } +} \ No newline at end of file diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyLanguageDialect.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyLanguageDialect.java new file mode 100644 index 000000000000..554e4e41a349 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyLanguageDialect.java @@ -0,0 +1,15 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.intellij.lang.InjectableLanguage; +import com.intellij.lang.Language; +import com.jetbrains.python.PythonLanguage; + +public class IpnbPyLanguageDialect extends Language implements InjectableLanguage { + public static IpnbPyLanguageDialect getInstance() { + return (IpnbPyLanguageDialect)IpnbPyFileType.INSTANCE.getLanguage(); + } + + protected IpnbPyLanguageDialect() { + super(PythonLanguage.getInstance(), "IpnbPython"); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyParser.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyParser.java new file mode 100644 index 000000000000..925539c18328 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyParser.java @@ -0,0 +1,14 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.intellij.lang.PsiBuilder; +import com.jetbrains.python.parsing.ParsingContext; +import com.jetbrains.python.parsing.PyParser; +import com.jetbrains.python.parsing.StatementParsing; +import com.jetbrains.python.psi.LanguageLevel; + +public class IpnbPyParser extends PyParser { + @Override + protected ParsingContext createParsingContext(PsiBuilder builder, LanguageLevel languageLevel, StatementParsing.FUTURE futureFlag) { + return new IpnbPyParsingContext(builder, languageLevel, futureFlag); + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyParserDefinition.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyParserDefinition.java new file mode 100644 index 000000000000..d55e933d55ae --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyParserDefinition.java @@ -0,0 +1,30 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.intellij.lang.PsiParser; +import com.intellij.lexer.Lexer; +import com.intellij.openapi.project.Project; +import com.intellij.psi.tree.IFileElementType; +import com.jetbrains.python.PythonParserDefinition; +import com.jetbrains.python.lexer.PythonIndentingLexer; +import org.jetbrains.annotations.NotNull; + +public class IpnbPyParserDefinition extends PythonParserDefinition { + public static final IFileElementType IPNB_PYTHON_FILE = new IpnbPyFileElementType(IpnbPyLanguageDialect.getInstance()); + + @NotNull + public Lexer createLexer(Project project) { + return new PythonIndentingLexer(); + } + + @NotNull + @Override + public PsiParser createParser(Project project) { + return new IpnbPyParser(); + } + + @Override + public IFileElementType getFileNodeType() { + return IPNB_PYTHON_FILE; + } + +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyParsingContext.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyParsingContext.java new file mode 100644 index 000000000000..13d1196b510d --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyParsingContext.java @@ -0,0 +1,71 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.intellij.lang.PsiBuilder; +import com.intellij.psi.tree.IElementType; +import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.parsing.ExpressionParsing; +import com.jetbrains.python.parsing.ParsingContext; +import com.jetbrains.python.parsing.StatementParsing; +import com.jetbrains.python.psi.LanguageLevel; +import org.jetbrains.annotations.Nullable; + +public class IpnbPyParsingContext extends ParsingContext { + private final StatementParsing myStatementParser; + private final ExpressionParsing myExpressionParser; + + public IpnbPyParsingContext(final PsiBuilder builder, + LanguageLevel languageLevel, + StatementParsing.FUTURE futureFlag) { + super(builder, languageLevel, futureFlag); + myStatementParser = new IpnbPyStatementParsing(this, futureFlag); + myExpressionParser = new IpnbPyExpressionParsing(this); + } + + @Override + public ExpressionParsing getExpressionParser() { + return myExpressionParser; + } + + @Override + public StatementParsing getStatementParser() { + return myStatementParser; + } + + private static class IpnbPyExpressionParsing extends ExpressionParsing { + public IpnbPyExpressionParsing(ParsingContext context) { + super(context); + } + + @Override + protected IElementType getReferenceType() { + return IpnbPyTokenTypes.IPNB_REFERENCE; + } + + public boolean parsePrimaryExpression(boolean isTargetExpression) { + final IElementType firstToken = myBuilder.getTokenType(); + if (firstToken == PyTokenTypes.IDENTIFIER) { + if (isTargetExpression) { + buildTokenElement(IpnbPyTokenTypes.IPNB_TARGET, myBuilder); + } + else { + buildTokenElement(getReferenceType(), myBuilder); + } + return true; + } + return super.parsePrimaryExpression(isTargetExpression); + } + } + + private static class IpnbPyStatementParsing extends StatementParsing { + + protected IpnbPyStatementParsing(ParsingContext context, @Nullable FUTURE futureFlag) { + super(context, futureFlag); + } + + @Override + protected IElementType getReferenceType() { + return IpnbPyTokenTypes.IPNB_REFERENCE; + } + + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyReference.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyReference.java new file mode 100644 index 000000000000..038ce69f0333 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyReference.java @@ -0,0 +1,64 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.intellij.lang.annotation.HighlightSeverity; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.intellij.psi.ResolveResult; +import com.jetbrains.python.psi.PyQualifiedExpression; +import com.jetbrains.python.psi.impl.references.PyReferenceImpl; +import com.jetbrains.python.psi.resolve.PyResolveContext; +import com.jetbrains.python.psi.resolve.PyResolveUtil; +import com.jetbrains.python.psi.resolve.RatedResolveResult; +import com.jetbrains.python.psi.resolve.ResolveProcessor; +import com.jetbrains.python.psi.types.TypeEvalContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbEditablePanel; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel; +import org.jetbrains.plugins.ipnb.editor.panels.code.IpnbCodePanel; + +import java.util.List; + +public class IpnbPyReference extends PyReferenceImpl { + public IpnbPyReference(PyQualifiedExpression element, @NotNull PyResolveContext context) { + super(element, context); + } + + @Override + public HighlightSeverity getUnresolvedHighlightSeverity(TypeEvalContext context) { + return HighlightSeverity.WARNING; + } + + @NotNull + @Override + public ResolveResult[] multiResolve(boolean incompleteCode) { + ResolveResult[] results = super.multiResolve(incompleteCode); + if (results.length == 0) { + PsiFile file = myElement.getContainingFile(); + if (file instanceof IpnbPyFragment) { + final IpnbFilePanel panel = ((IpnbPyFragment)file).getFilePanel(); + final List panels = panel.getIpnbPanels(); + + final String referencedName = myElement.getReferencedName(); + if (referencedName == null) return ResolveResult.EMPTY_ARRAY; + + for (IpnbEditablePanel editablePanel : panels) { + if (!(editablePanel instanceof IpnbCodePanel)) continue; + final Editor editor = ((IpnbCodePanel)editablePanel).getEditor(); + final IpnbPyFragment psiFile = (IpnbPyFragment)PsiDocumentManager.getInstance(myElement.getProject()).getPsiFile(editor.getDocument()); + if (psiFile == null) continue; + ResolveProcessor processor = new ResolveProcessor(referencedName); + + PyResolveUtil.scopeCrawlUp(processor, psiFile, referencedName, psiFile); + final List resultList = getResultsFromProcessor(referencedName, processor, psiFile, psiFile); + if (resultList.size() > 0) { + List ret = RatedResolveResult.sorted(resultList); + return ret.toArray(new RatedResolveResult[ret.size()]); + } + } + } + } + return results; + } + +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyReferenceExpression.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyReferenceExpression.java new file mode 100644 index 000000000000..b7776f3fa90c --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyReferenceExpression.java @@ -0,0 +1,34 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiPolyVariantReference; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.python.psi.PyFromImportStatement; +import com.jetbrains.python.psi.PyImportElement; +import com.jetbrains.python.psi.impl.PyReferenceExpressionImpl; +import com.jetbrains.python.psi.impl.references.PyImportReference; +import com.jetbrains.python.psi.impl.references.PyQualifiedReference; +import com.jetbrains.python.psi.resolve.PyResolveContext; +import org.jetbrains.annotations.NotNull; + +public class IpnbPyReferenceExpression extends PyReferenceExpressionImpl { + + public IpnbPyReferenceExpression(ASTNode astNode) { + super(astNode); + } + + @NotNull + @Override + public PsiPolyVariantReference getReference(PyResolveContext context) { + if (isQualified()) { + return new PyQualifiedReference(this, context); + } + final PsiElement importParent = PsiTreeUtil.getParentOfType(this, PyImportElement.class, PyFromImportStatement.class); + if (importParent != null) { + return PyImportReference.forElement(this, importParent, context); + } + return new IpnbPyReference(this, context); + } +} + diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyTargetExpression.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyTargetExpression.java new file mode 100644 index 000000000000..62f39a96cbb5 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyTargetExpression.java @@ -0,0 +1,34 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.intellij.lang.ASTNode; +import com.intellij.openapi.editor.Editor; +import com.intellij.util.ui.UIUtil; +import com.jetbrains.python.psi.impl.PyTargetExpressionImpl; +import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor; +import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel; +import org.jetbrains.plugins.ipnb.editor.panels.code.IpnbCodePanel; +import org.jetbrains.plugins.ipnb.editor.panels.code.IpnbCodeSourcePanel; + +public class IpnbPyTargetExpression extends PyTargetExpressionImpl { + + public IpnbPyTargetExpression(ASTNode astNode) { + super(astNode); + } + + @Override + public void navigate(boolean requestFocus) { + final IpnbCodeSourcePanel sourcePanel = ((IpnbPyFragment)getContainingFile()).getCodeSourcePanel(); + final Editor editor = sourcePanel.getEditor(); + + final IpnbCodePanel codePanel = sourcePanel.getIpnbCodePanel(); + final IpnbFileEditor fileEditor = codePanel.getFileEditor(); + final IpnbFilePanel filePanel = fileEditor.getIpnbFilePanel(); + codePanel.setEditing(true); + filePanel.setSelectedCell(codePanel); + super.navigate(false); + UIUtil.requestFocus(editor.getContentComponent()); + + } + +} + diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyTokenSetContributor.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyTokenSetContributor.java new file mode 100644 index 000000000000..8eeef95d8927 --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyTokenSetContributor.java @@ -0,0 +1,21 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.intellij.psi.tree.TokenSet; +import com.jetbrains.python.PythonDialectsTokenSetContributorBase; +import org.jetbrains.annotations.NotNull; + +public class IpnbPyTokenSetContributor extends PythonDialectsTokenSetContributorBase { + public static final TokenSet IPNB_REFERENCE_EXPRESSIONS = TokenSet.create(IpnbPyTokenTypes.IPNB_REFERENCE); + + @NotNull + @Override + public TokenSet getExpressionTokens() { + return IPNB_REFERENCE_EXPRESSIONS; + } + + @NotNull + @Override + public TokenSet getReferenceExpressionTokens() { + return IPNB_REFERENCE_EXPRESSIONS; + } +} diff --git a/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyTokenTypes.java b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyTokenTypes.java new file mode 100644 index 000000000000..a271089b784d --- /dev/null +++ b/python/ipnb/src/org/jetbrains/plugins/ipnb/psi/IpnbPyTokenTypes.java @@ -0,0 +1,11 @@ +package org.jetbrains.plugins.ipnb.psi; + +import com.jetbrains.python.psi.PyElementType; + +public class IpnbPyTokenTypes { + public static final PyElementType IPNB_REFERENCE = new PyElementType("IPNB_REFERENCE", IpnbPyReferenceExpression.class); + public static final PyElementType IPNB_TARGET = new PyElementType("IPNB_TARGET", IpnbPyTargetExpression.class); + + private IpnbPyTokenTypes() { + } +} diff --git a/python/ipnb/testData/SymPy.ipynb b/python/ipnb/testData/SymPy.ipynb new file mode 100644 index 000000000000..ab6c590587df --- /dev/null +++ b/python/ipnb/testData/SymPy.ipynb @@ -0,0 +1,670 @@ +{ + "metadata": { + "name": "", + "signature": "sha256:a0f4cda82587c5b8e7a4502192d1210abedac9084077604eb60aea15f262a344" + }, + "nbformat": 3, + "nbformat_minor": 0, + "worksheets": [ + { + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# SymPy: Open Source Symbolic Mathematics\n", + "\n", + "This notebook uses the [SymPy](http://sympy.org) package to perform symbolic manipulations,\n", + "and combined with numpy and matplotlib, also displays numerical visualizations of symbolically\n", + "constructed expressions.\n", + "\n", + "We first load sympy printing extensions, as well as all of sympy:" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "from IPython.display import display\n", + "\n", + "from sympy.interactive import printing\n", + "printing.init_printing(use_latex='mathjax')\n", + "\n", + "from __future__ import division\n", + "import sympy as sym\n", + "from sympy import *\n", + "x, y, z = symbols(\"x y z\")\n", + "k, m, n = symbols(\"k m n\", integer=True)\n", + "f, g, h = map(Function, 'fgh')" + ], + "language": "python", + "metadata": {}, + "outputs": [], + "prompt_number": 1 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "

Elementary operations

" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "Rational(3,2)*pi + exp(I*x) / (x**2 + y)" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$\\frac{3 \\pi}{2} + \\frac{e^{i x}}{x^{2} + y}$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 2, + "text": [ + " \u2148\u22c5x \n", + "3\u22c5\u03c0 \u212f \n", + "\u2500\u2500\u2500 + \u2500\u2500\u2500\u2500\u2500\u2500\n", + " 2 2 \n", + " x + y" + ] + } + ], + "prompt_number": 2 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "exp(I*x).subs(x,pi).evalf()" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$-1.0$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 3, + "text": [ + "-1.00000000000000" + ] + } + ], + "prompt_number": 3 + }, + { + "cell_type": "code", + "collapsed": true, + "input": [ + "e = x + 2*y" + ], + "language": "python", + "metadata": {}, + "outputs": [], + "prompt_number": 4 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "srepr(e)" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "pyout", + "prompt_number": 5, + "text": [ + "\"Add(Symbol('x'), Mul(Integer(2), Symbol('y')))\"" + ] + } + ], + "prompt_number": 5 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "exp(pi * sqrt(163)).evalf(50)" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$262537412640768743.99999999999925007259719818568888$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 6, + "text": [ + "262537412640768743.99999999999925007259719818568888" + ] + } + ], + "prompt_number": 6 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "

Algebra

" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "eq = ((x+y)**2 * (x+1))\n", + "eq" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$\\left(x + 1\\right) \\left(x + y\\right)^{2}$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 7, + "text": [ + " 2\n", + "(x + 1)\u22c5(x + y) " + ] + } + ], + "prompt_number": 7 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "expand(eq)" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$x^{3} + 2 x^{2} y + x^{2} + x y^{2} + 2 x y + y^{2}$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 8, + "text": [ + " 3 2 2 2 2\n", + "x + 2\u22c5x \u22c5y + x + x\u22c5y + 2\u22c5x\u22c5y + y " + ] + } + ], + "prompt_number": 8 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "a = 1/x + (x*sin(x) - 1)/x\n", + "a" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$\\frac{1}{x} \\left(x \\sin{\\left (x \\right )} - 1\\right) + \\frac{1}{x}$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 9, + "text": [ + "x\u22c5sin(x) - 1 1\n", + "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 + \u2500\n", + " x x" + ] + } + ], + "prompt_number": 9 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "simplify(a)" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$\\sin{\\left (x \\right )}$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 10, + "text": [ + "sin(x)" + ] + } + ], + "prompt_number": 10 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "eq = Eq(x**3 + 2*x**2 + 4*x + 8, 0)\n", + "eq" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$x^{3} + 2 x^{2} + 4 x + 8 = 0$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 11, + "text": [ + " 3 2 \n", + "x + 2\u22c5x + 4\u22c5x + 8 = 0" + ] + } + ], + "prompt_number": 11 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "solve(eq, x)" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$\\begin{bmatrix}-2, & - 2 i, & 2 i\\end{bmatrix}$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 12, + "text": [ + "[-2, -2\u22c5\u2148, 2\u22c5\u2148]" + ] + } + ], + "prompt_number": 12 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "a, b = symbols('a b')\n", + "Sum(6*n**2 + 2**n, (n, a, b))" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$\\sum_{n=a}^{b} \\left(2^{n} + 6 n^{2}\\right)$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 13, + "text": [ + " b \n", + " ___ \n", + " \u2572 \n", + " \u2572 \u239b n 2\u239e\n", + " \u2571 \u239d2 + 6\u22c5n \u23a0\n", + " \u2571 \n", + " \u203e\u203e\u203e \n", + "n = a " + ] + } + ], + "prompt_number": 13 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "

Calculus

" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "limit((sin(x)-x)/x**3, x, 0)" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$- \\frac{1}{6}$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 14, + "text": [ + "-1/6" + ] + } + ], + "prompt_number": 14 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "(1/cos(x)).series(x, 0, 6)" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$1 + \\frac{x^{2}}{2} + \\frac{5 x^{4}}{24} + \\mathcal{O}\\left(x^{6}\\right)$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 15, + "text": [ + " 2 4 \n", + " x 5\u22c5x \u239b 6\u239e\n", + "1 + \u2500\u2500 + \u2500\u2500\u2500\u2500 + O\u239dx \u23a0\n", + " 2 24 " + ] + } + ], + "prompt_number": 15 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "diff(cos(x**2)**2 / (1+x), x)" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$- \\frac{4 x \\cos{\\left (x^{2} \\right )}}{x + 1} \\sin{\\left (x^{2} \\right )} - \\frac{\\cos^{2}{\\left (x^{2} \\right )}}{\\left(x + 1\\right)^{2}}$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 16, + "text": [ + " \u239b 2\u239e \u239b 2\u239e 2\u239b 2\u239e\n", + " 4\u22c5x\u22c5sin\u239dx \u23a0\u22c5cos\u239dx \u23a0 cos \u239dx \u23a0\n", + "- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 - \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n", + " x + 1 2\n", + " (x + 1) " + ] + } + ], + "prompt_number": 16 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "integrate(x**2 * cos(x), (x, 0, pi/2))" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$-2 + \\frac{\\pi^{2}}{4}$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 17, + "text": [ + " 2\n", + " \u03c0 \n", + "-2 + \u2500\u2500\n", + " 4 " + ] + } + ], + "prompt_number": 17 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "eqn = Eq(Derivative(f(x),x,x) + 9*f(x), 1)\n", + "display(eqn)\n", + "dsolve(eqn, f(x))" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "latex": [ + "$$9 f{\\left (x \\right )} + \\frac{d^{2}}{d x^{2}} f{\\left (x \\right )} = 1$$" + ], + "metadata": {}, + "output_type": "display_data", + "text": [ + " 2 \n", + " d \n", + "9\u22c5f(x) + \u2500\u2500\u2500(f(x)) = 1\n", + " 2 \n", + " dx " + ] + }, + { + "latex": [ + "$$f{\\left (x \\right )} = C_{1} \\sin{\\left (3 x \\right )} + C_{2} \\cos{\\left (3 x \\right )} + \\frac{1}{9}$$" + ], + "metadata": {}, + "output_type": "pyout", + "prompt_number": 18, + "text": [ + "f(x) = C\u2081\u22c5sin(3\u22c5x) + C\u2082\u22c5cos(3\u22c5x) + 1/9" + ] + } + ], + "prompt_number": 18 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Illustrating Taylor series\n", + "\n", + "We will define a function to compute the Taylor series expansions of a symbolically defined expression at\n", + "various orders and visualize all the approximations together with the original function" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "%matplotlib inline\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ], + "language": "python", + "metadata": {}, + "outputs": [], + "prompt_number": 19 + }, + { + "cell_type": "code", + "collapsed": true, + "input": [ + "# You can change the default figure size to be a bit larger if you want,\n", + "# uncomment the next line for that:\n", + "#plt.rc('figure', figsize=(10, 6))" + ], + "language": "python", + "metadata": {}, + "outputs": [], + "prompt_number": 20 + }, + { + "cell_type": "code", + "collapsed": true, + "input": [ + "def plot_taylor_approximations(func, x0=None, orders=(2, 4), xrange=(0,1), yrange=None, npts=200):\n", + " \"\"\"Plot the Taylor series approximations to a function at various orders.\n", + "\n", + " Parameters\n", + " ----------\n", + " func : a sympy function\n", + " x0 : float\n", + " Origin of the Taylor series expansion. If not given, x0=xrange[0].\n", + " orders : list\n", + " List of integers with the orders of Taylor series to show. Default is (2, 4).\n", + " xrange : 2-tuple or array.\n", + " Either an (xmin, xmax) tuple indicating the x range for the plot (default is (0, 1)),\n", + " or the actual array of values to use.\n", + " yrange : 2-tuple\n", + " (ymin, ymax) tuple indicating the y range for the plot. If not given,\n", + " the full range of values will be automatically used. \n", + " npts : int\n", + " Number of points to sample the x range with. Default is 200.\n", + " \"\"\"\n", + " if not callable(func):\n", + " raise ValueError('func must be callable')\n", + " if isinstance(xrange, (list, tuple)):\n", + " x = np.linspace(float(xrange[0]), float(xrange[1]), npts)\n", + " else:\n", + " x = xrange\n", + " if x0 is None: x0 = x[0]\n", + " xs = sym.Symbol('x')\n", + " # Make a numpy-callable form of the original function for plotting\n", + " fx = func(xs)\n", + " f = sym.lambdify(xs, fx, modules=['numpy'])\n", + " # We could use latex(fx) instead of str(), but matploblib gets confused\n", + " # with some of the (valid) latex constructs sympy emits. So we play it safe.\n", + " plt.plot(x, f(x), label=str(fx), lw=2)\n", + " # Build the Taylor approximations, plotting as we go\n", + " apps = {}\n", + " for order in orders:\n", + " app = fx.series(xs, x0, n=order).removeO()\n", + " apps[order] = app\n", + " # Must be careful here: if the approximation is a constant, we can't\n", + " # blindly use lambdify as it won't do the right thing. In that case, \n", + " # evaluate the number as a float and fill the y array with that value.\n", + " if isinstance(app, sym.numbers.Number):\n", + " y = np.zeros_like(x)\n", + " y.fill(app.evalf())\n", + " else:\n", + " fa = sym.lambdify(xs, app, modules=['numpy'])\n", + " y = fa(x)\n", + " tex = sym.latex(app).replace('$', '')\n", + " plt.plot(x, y, label=r'$n=%s:\\, %s$' % (order, tex) )\n", + " \n", + " # Plot refinements\n", + " if yrange is not None:\n", + " plt.ylim(*yrange)\n", + " plt.grid()\n", + " plt.legend(loc='best').get_frame().set_alpha(0.8)" + ], + "language": "python", + "metadata": {}, + "outputs": [], + "prompt_number": 21 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With this function defined, we can now use it for any sympy function or expression" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plot_taylor_approximations(sin, 0, [2, 4, 6], (0, 2*pi), (-2,2))" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "display_data", + "png": "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\ngg==\n", + "text": [ + "" + ] + } + ], + "prompt_number": 22 + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "plot_taylor_approximations(cos, 0, [2, 4, 6], (0, 2*pi), (-2,2))" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "display_data", + "png": "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\ntYbY1r+2UgAAAABJRU5ErkJggg==\n", + "text": [ + "" + ] + } + ], + "prompt_number": 23 + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This shows easily how a Taylor series is useless beyond its convergence radius, illustrated by \n", + "a simple function that has singularities on the real axis:" + ] + }, + { + "cell_type": "code", + "collapsed": false, + "input": [ + "# For an expression made from elementary functions, we must first make it into\n", + "# a callable function, the simplest way is to use the Python lambda construct.\n", + "plot_taylor_approximations(lambda x: 1/cos(x), 0, [2,4,6], (0, 2*pi), (-5,5))" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "display_data", + "png": "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\n", + "text": [ + "" + ] + } + ], + "prompt_number": 24 + } + ], + "metadata": {} + } + ] +} \ No newline at end of file diff --git a/python/ipnb/testData/code.ipynb b/python/ipnb/testData/code.ipynb new file mode 100644 index 000000000000..e10469be05be --- /dev/null +++ b/python/ipnb/testData/code.ipynb @@ -0,0 +1,25 @@ +{ + "metadata": { + "name": "", + "signature": "sha256:0b4b631419772e40e0f4893f5a0f0fe089a39e46c8862af0256164628394302d" + }, + "nbformat": 3, + "nbformat_minor": 0, + "worksheets": [ + { + "cells": [ + { + "cell_type": "code", + "collapsed": true, + "input": [ + "e = x + 2*y" + ], + "language": "python", + "metadata": {}, + "outputs": [], + "prompt_number": 4 + } + ] + } + ] +} \ No newline at end of file diff --git a/python/ipnb/testData/markdown.ipynb b/python/ipnb/testData/markdown.ipynb new file mode 100644 index 000000000000..e8d831e16676 --- /dev/null +++ b/python/ipnb/testData/markdown.ipynb @@ -0,0 +1,21 @@ +{ + "metadata": { + "name": "", + "signature": "sha256:0b4b631419772e40e0f4893f5a0f0fe089a39e46c8862af0256164628394302d" + }, + "nbformat": 3, + "nbformat_minor": 0, + "worksheets": [ + { + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/python/ipnb/testData/outputs.ipynb b/python/ipnb/testData/outputs.ipynb new file mode 100644 index 000000000000..42b65eaadf70 --- /dev/null +++ b/python/ipnb/testData/outputs.ipynb @@ -0,0 +1,34 @@ +{ + "metadata": { + "name": "", + "signature": "sha256:0b4b631419772e40e0f4893f5a0f0fe089a39e46c8862af0256164628394302d" + }, + "nbformat": 3, + "nbformat_minor": 0, + "worksheets": [ + { + "cells": [ + { + "cell_type": "code", + "collapsed": false, + "input": [ + "srepr(e)" + ], + "language": "python", + "metadata": {}, + "outputs": [ + { + "metadata": {}, + "output_type": "pyout", + "prompt_number": 5, + "text": [ + "\"Add(Symbol('x'), Mul(Integer(2), Symbol('y')))\"" + ] + } + ], + "prompt_number": 5 + } + ] + } + ] +} \ No newline at end of file diff --git a/python/ipnb/testSrc/JsonParserTest.java b/python/ipnb/testSrc/JsonParserTest.java new file mode 100644 index 000000000000..afcd0789b295 --- /dev/null +++ b/python/ipnb/testSrc/JsonParserTest.java @@ -0,0 +1,110 @@ +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.intellij.openapi.util.text.StringUtil; +import junit.framework.TestCase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.format.IpnbFile; +import org.jetbrains.plugins.ipnb.format.IpnbParser; +import org.jetbrains.plugins.ipnb.format.cells.IpnbCodeCell; +import org.jetbrains.plugins.ipnb.format.cells.IpnbCell; +import org.jetbrains.plugins.ipnb.format.cells.IpnbMarkdownCell; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbOutputCell; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.List; + +public class JsonParserTest extends TestCase { + public void testFile() throws IOException { + final String fileName = "testData/SymPy.ipynb"; + final String fileText = getFileText(fileName); + final IpnbFile ipnbFile = IpnbParser.parseIpnbFile(fileText, fileName); + assertNotNull(ipnbFile); + assertEquals(31, ipnbFile.getCells().size()); + } + + public void testMarkdownCells() throws IOException { + final String fileName = "testData/SymPy.ipynb"; + final String fileText = getFileText(fileName); + final IpnbFile ipnbFile = IpnbParser.parseIpnbFile(fileText,fileName); + assertNotNull(ipnbFile); + final List cells = ipnbFile.getCells(); + Iterables.removeIf(cells, new Predicate() { + @Override + public boolean apply(IpnbCell cell) { + return !(cell instanceof IpnbMarkdownCell); + } + }); + assertEquals(7, cells.size()); + } + + public void testMarkdownCell() throws IOException { + final String fileName = "testData/markdown.ipynb"; + final String fileText = getFileText(fileName); + final IpnbFile ipnbFile = IpnbParser.parseIpnbFile(fileText, fileName); + assertNotNull(ipnbFile); + final List cells = ipnbFile.getCells(); + assertEquals(1, cells.size()); + final IpnbCell cell = cells.get(0); + assertTrue(cell instanceof IpnbMarkdownCell); + final String[] source = ((IpnbMarkdownCell)cell).getSource(); + final String joined = StringUtil.join(source); + assertEquals("", joined); + } + + public void testCodeCell() throws IOException { + final String fileName = "testData/code.ipynb"; + final String fileText = getFileText(fileName); + final IpnbFile ipnbFile = IpnbParser.parseIpnbFile(fileText, fileName); + assertNotNull(ipnbFile); + final List cells = ipnbFile.getCells(); + assertEquals(1, cells.size()); + final IpnbCell cell = cells.get(0); + assertTrue(cell instanceof IpnbCodeCell); + final List outputs = ((IpnbCodeCell)cell).getCellOutputs(); + assertEquals(0, outputs.size()); + final String[] source = ((IpnbCodeCell)cell).getSource(); + final String joined = StringUtil.join(source); + assertEquals("e = x + 2*y", joined); + final String language = ((IpnbCodeCell)cell).getLanguage(); + assertEquals("python", language); + final Integer number = ((IpnbCodeCell)cell).getPromptNumber(); + assertEquals(new Integer(4), number); + } + + public void testOutputs() throws IOException { + final String fileName = "testData/outputs.ipynb"; + final String fileText = getFileText(fileName); + final IpnbFile ipnbFile = IpnbParser.parseIpnbFile(fileText, fileName); + assertNotNull(ipnbFile); + final List cells = ipnbFile.getCells(); + assertEquals(1, cells.size()); + final IpnbCell cell = cells.get(0); + assertTrue(cell instanceof IpnbCodeCell); + final List outputs = ((IpnbCodeCell)cell).getCellOutputs(); + assertEquals(1, outputs.size()); + final IpnbOutputCell output = outputs.get(0); + final String[] text = output.getText(); + final String joined = StringUtil.join(text); + assertEquals("\"Add(Symbol('x'), Mul(Integer(2), Symbol('y')))\"", joined); + } + + private String getFileText(@NotNull final String fileName) throws IOException { + final BufferedReader br = new BufferedReader(new FileReader(fileName)); + try { + final StringBuilder sb = new StringBuilder(); + String line = br.readLine(); + + while (line != null) { + sb.append(line); + sb.append("\n"); + line = br.readLine(); + } + return sb.toString(); + } + finally { + br.close(); + } + } +} diff --git a/python/ipnb/testSrc/WebSocketConnectionTest.java b/python/ipnb/testSrc/WebSocketConnectionTest.java new file mode 100644 index 000000000000..37fb37dc539b --- /dev/null +++ b/python/ipnb/testSrc/WebSocketConnectionTest.java @@ -0,0 +1,116 @@ +import com.intellij.openapi.util.Ref; +import junit.framework.TestCase; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbOutOutputCell; +import org.jetbrains.plugins.ipnb.format.cells.output.IpnbOutputCell; +import org.jetbrains.plugins.ipnb.protocol.IpnbConnection; +import org.jetbrains.plugins.ipnb.protocol.IpnbConnectionListenerBase; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +/** + * + * * Message Spec + * http://ipython.org/ipython-doc/dev/development/messaging.html + * + * * Notebook REST API + * https://github.com/ipython/ipython/wiki/IPEP-16%3A-Notebook-multi-directory-dashboard-and-URL-mapping + * + * @author vlan + */ +public class WebSocketConnectionTest extends TestCase { + @Override + protected void setUp() throws Exception { + //WebSocketImpl.DEBUG = true; + } + + public void testStartAndShutdownKernel() throws URISyntaxException, IOException, InterruptedException { + final IpnbConnection connection = new IpnbConnection(getTestServerURI(), new IpnbConnectionListenerBase() { + @Override + public void onOpen(@NotNull IpnbConnection connection) { + assertTrue(connection.getKernelId().length() > 0); + connection.shutdown(); + } + }); + connection.close(); + } + + public void testBasicWebSocket() throws IOException, URISyntaxException, InterruptedException { + final Ref evaluated = Ref.create(false); + final IpnbConnection connection = new IpnbConnection(getTestServerURI(), new IpnbConnectionListenerBase() { + private String myMessageId; + + @Override + public void onOpen(@NotNull IpnbConnection connection) { + myMessageId = connection.execute("2 + 2"); + } + + @Override + public void onOutput(@NotNull IpnbConnection connection, + @NotNull String parentMessageId, + @NotNull List outputs, + Integer execCount) { + if (myMessageId.equals(parentMessageId)) { + assertEquals(outputs.size(), 1); + assertEquals(outputs.get(0).getClass(), IpnbOutOutputCell.class); + final String[] text = outputs.get(0).getText(); + assertNotNull(text); + assertEquals("4", text[0]); + evaluated.set(true); + connection.shutdown(); + } + } + }); + connection.close(); + assertTrue(evaluated.get()); + } + + public void testCompositeInput() throws IOException, URISyntaxException, InterruptedException { + final Ref evaluated = Ref.create(false); + final IpnbConnection connection = new IpnbConnection(getTestServerURI(), new IpnbConnectionListenerBase() { + private String myMessageId; + + @Override + public void onOpen(@NotNull IpnbConnection connection) { + myMessageId = connection.execute("def simple_crit_func(feat_sub):\n" + + "\n" + + " \"\"\" Returns sum of numerical values of an input list. \"\"\" \n" + + "\n" + + " return sum(feat_sub)\n" + + "\n" + + "simple_crit_func([1,2,4])"); + } + + @Override + public void onOutput(@NotNull IpnbConnection connection, + @NotNull String parentMessageId, + @NotNull List outputs, + Integer execCount) { + if (myMessageId.equals(parentMessageId)) { + assertEquals(outputs.size(), 1); + assertEquals(outputs.get(0).getClass(), IpnbOutOutputCell.class); + final String[] text = outputs.get(0).getText(); + assertNotNull(text); + assertEquals("7", text[0]); + evaluated.set(true); + connection.shutdown(); + } + } + }); + connection.close(); + assertTrue(evaluated.get()); + } + + @NotNull + public static URI getTestServerURI() { + try { + return new URI("http://127.0.0.1:8888"); + } + catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } +}