diff --git a/python/python-rest/resources/META-INF/python-rest-plugin.xml b/python/python-rest/resources/META-INF/python-rest-plugin.xml
index 9bd933de2a0d..1cf133ec8869 100644
--- a/python/python-rest/resources/META-INF/python-rest-plugin.xml
+++ b/python/python-rest/resources/META-INF/python-rest-plugin.xml
@@ -26,4 +26,8 @@
+
+
+
+
diff --git a/python/python-rest/src/com/jetbrains/rest/RestPythonPreviewProvider.java b/python/python-rest/src/com/jetbrains/rest/RestPythonPreviewProvider.java
new file mode 100644
index 000000000000..c7c695146975
--- /dev/null
+++ b/python/python-rest/src/com/jetbrains/rest/RestPythonPreviewProvider.java
@@ -0,0 +1,43 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.rest;
+
+import com.google.common.collect.Lists;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.process.ProcessOutput;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.roots.ProjectFileIndex;
+import com.intellij.openapi.vfs.CharsetToolkit;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.python.sdk.PySdkUtil;
+import com.jetbrains.python.sdk.PythonSdkType;
+import com.jetbrains.rest.editor.RestPreviewProvider;
+import org.jetbrains.annotations.Nullable;
+
+import static com.jetbrains.python.PythonHelper.REST_RUNNER;
+
+public class RestPythonPreviewProvider extends RestPreviewProvider {
+
+ @Nullable
+ public String toHtml(String text, VirtualFile virtualFile, Project project) {
+ Module module = ProjectFileIndex.SERVICE.getInstance(project).getModuleForFile(virtualFile);
+ if (module == null) return null;
+
+ final Sdk sdk = PythonSdkType.findPythonSdk(module);
+ if (sdk == null) {
+ return null;
+ }
+ final String sdkHomePath = sdk.getHomePath();
+ if (sdkHomePath == null) {
+ return null;
+ }
+
+ final GeneralCommandLine commandLine = REST_RUNNER.newCommandLine(sdkHomePath, Lists.newArrayList("rst2html"));
+ final ProcessOutput output = PySdkUtil.getProcessOutput(commandLine, virtualFile.getParent().getPath(), null, 5000,
+ text.getBytes(CharsetToolkit.UTF8_CHARSET), true);
+ if (output.isCancelled() || output.isTimeout()) return null;
+
+ return output.getStdout();
+ }
+}
diff --git a/python/rest/intellij.reStructuredText.iml b/python/rest/intellij.reStructuredText.iml
index 0cf073dab5ba..44ba7bcca1e5 100644
--- a/python/rest/intellij.reStructuredText.iml
+++ b/python/rest/intellij.reStructuredText.iml
@@ -15,5 +15,6 @@
+
\ No newline at end of file
diff --git a/python/rest/resources/META-INF/rest.xml b/python/rest/resources/META-INF/rest.xml
index 5b5cb91d30da..8af190fbfbc8 100644
--- a/python/rest/resources/META-INF/rest.xml
+++ b/python/rest/resources/META-INF/rest.xml
@@ -1,4 +1,8 @@
+
+
+
+
@@ -25,6 +29,8 @@
+
+
diff --git a/python/rest/src/com/jetbrains/rest/editor/RestPreviewFileEditor.java b/python/rest/src/com/jetbrains/rest/editor/RestPreviewFileEditor.java
new file mode 100644
index 000000000000..d6fcff57a588
--- /dev/null
+++ b/python/rest/src/com/jetbrains/rest/editor/RestPreviewFileEditor.java
@@ -0,0 +1,201 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.rest.editor;
+
+import com.intellij.codeHighlighting.BackgroundEditorHighlighter;
+import com.intellij.openapi.application.ModalityState;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.event.DocumentEvent;
+import com.intellij.openapi.editor.event.DocumentListener;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.fileEditor.FileEditorLocation;
+import com.intellij.openapi.fileEditor.FileEditorState;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.NotNullLazyValue;
+import com.intellij.openapi.util.UserDataHolderBase;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.javafx.JavaFxHtmlPanel;
+import com.intellij.util.Alarm;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.owasp.html.HtmlPolicyBuilder;
+import org.owasp.html.PolicyFactory;
+import org.owasp.html.Sanitizers;
+
+import javax.swing.*;
+import java.beans.PropertyChangeListener;
+
+public class RestPreviewFileEditor extends UserDataHolderBase implements FileEditor {
+ private final static long PARSING_CALL_TIMEOUT_MS = 50L;
+
+ private final static long RENDERING_DELAY_MS = 20L;
+
+ final static NotNullLazyValue SANITIZER_VALUE = new NotNullLazyValue() {
+ @NotNull
+ @Override
+ protected PolicyFactory compute() {
+ return Sanitizers.BLOCKS
+ .and(Sanitizers.FORMATTING)
+ .and(new HtmlPolicyBuilder()
+ .allowUrlProtocols("file", "http", "https").allowElements("img")
+ .allowAttributes("alt", "src", "title").onElements("img")
+ .allowAttributes("border", "height", "width").onElements("img")
+ .toFactory())
+ .and(new HtmlPolicyBuilder()
+ .allowUrlProtocols("file", "http", "https", "mailto").allowElements("a")
+ .allowAttributes("href", "title").onElements("a")
+ .toFactory())
+ .and(Sanitizers.TABLES)
+ .and(new HtmlPolicyBuilder()
+ .allowElements("code", "tr")
+ .allowAttributes("class").onElements("code", "tr")
+ .toFactory());
+ }
+ };
+ @NotNull
+ private final JavaFxHtmlPanel myPanel;
+ @NotNull
+ private final VirtualFile myFile;
+ private final Project myProject;
+ @Nullable
+ private final Document myDocument;
+ @NotNull
+ private final Alarm myPooledAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
+ @NotNull
+ private final Alarm mySwingAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, this);
+
+ private final Object REQUESTS_LOCK = new Object();
+ @Nullable
+ private Runnable myLastRequest = null;
+
+ @NotNull
+ private String myLastRenderedHtml = "";
+
+ public RestPreviewFileEditor(@NotNull VirtualFile file, Project project) {
+ myFile = file;
+ myProject = project;
+ myDocument = FileDocumentManager.getInstance().getDocument(myFile);
+ myPanel = new JavaFxHtmlPanel();
+
+ if (myDocument != null) {
+ myDocument.addDocumentListener(new DocumentListener() {
+
+ @Override
+ public void beforeDocumentChange(DocumentEvent e) {
+ myPooledAlarm.cancelAllRequests();
+ }
+
+ @Override
+ public void documentChanged(final DocumentEvent e) {
+ myPooledAlarm.addRequest(() -> updateHtml(), PARSING_CALL_TIMEOUT_MS);
+ }
+ }, this);
+ }
+ }
+
+ @NotNull
+ @Override
+ public JComponent getComponent() {
+ return myPanel.getComponent();
+ }
+
+ @Nullable
+ @Override
+ public JComponent getPreferredFocusedComponent() {
+ return myPanel.getComponent();
+ }
+
+ @NotNull
+ @Override
+ public String getName() {
+ return "ReStructuredText HTML Preview";
+ }
+
+ @Override
+ public void setState(@NotNull FileEditorState state) {
+ }
+
+ @Override
+ public boolean isModified() {
+ return false;
+ }
+
+ @Override
+ public boolean isValid() {
+ return true;
+ }
+
+ @Override
+ public void selectNotify() {
+ myPooledAlarm.cancelAllRequests();
+ myPooledAlarm.addRequest(() -> updateHtml(), 0);
+ }
+
+ /**
+ * Is always run from pooled thread
+ */
+ private void updateHtml() {
+ if (!myFile.isValid() || myDocument == null || Disposer.isDisposed(this)) {
+ return;
+ }
+
+ final String html = RestPreviewProvider.getProviders()[0].toHtml(myDocument.getText(), myFile, myProject);
+ if (html == null) {
+ return;
+ }
+
+ // EA-75860: The lines to the top may be processed slowly; Since we're in pooled thread, we can be disposed already.
+ if (!myFile.isValid() || Disposer.isDisposed(this)) {
+ return;
+ }
+
+ synchronized (REQUESTS_LOCK) {
+ if (myLastRequest != null) {
+ mySwingAlarm.cancelRequest(myLastRequest);
+ }
+ myLastRequest = () -> {
+ final String currentHtml = "" + SANITIZER_VALUE.getValue().sanitize(html) + "";
+ if (!currentHtml.equals(myLastRenderedHtml)) {
+ myLastRenderedHtml = currentHtml;
+ myPanel.setHtml(myLastRenderedHtml);
+ }
+
+ myPanel.render();
+ synchronized (REQUESTS_LOCK) {
+ myLastRequest = null;
+ }
+ };
+ mySwingAlarm.addRequest(myLastRequest, RENDERING_DELAY_MS, ModalityState.stateForComponent(getComponent()));
+ }
+ }
+
+ @Override
+ public void deselectNotify() {
+ }
+
+ @Override
+ public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) {
+ }
+
+ @Override
+ public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) {
+ }
+
+ @Nullable
+ @Override
+ public BackgroundEditorHighlighter getBackgroundHighlighter() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public FileEditorLocation getCurrentLocation() {
+ return null;
+ }
+
+ @Override
+ public void dispose() {
+ Disposer.dispose(myPanel);
+ }
+}
\ No newline at end of file
diff --git a/python/rest/src/com/jetbrains/rest/editor/RestPreviewProvider.java b/python/rest/src/com/jetbrains/rest/editor/RestPreviewProvider.java
new file mode 100644
index 000000000000..fa27896bcc98
--- /dev/null
+++ b/python/rest/src/com/jetbrains/rest/editor/RestPreviewProvider.java
@@ -0,0 +1,22 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.rest.editor;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public abstract class RestPreviewProvider {
+ public static final ExtensionPointName EP_NAME =
+ ExtensionPointName.create("restructured.text.html.preview.provider");
+
+ @Nullable
+ public abstract String toHtml(String text, VirtualFile virtualFile, Project project);
+
+ @NotNull
+ public static RestPreviewProvider[] getProviders() {
+ return EP_NAME.getExtensions();
+ }
+
+}
diff --git a/python/rest/src/com/jetbrains/rest/editor/RestSplitEditorProvider.java b/python/rest/src/com/jetbrains/rest/editor/RestSplitEditorProvider.java
new file mode 100644
index 000000000000..a902828b7c77
--- /dev/null
+++ b/python/rest/src/com/jetbrains/rest/editor/RestSplitEditorProvider.java
@@ -0,0 +1,39 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.rest.editor;
+
+import com.intellij.openapi.fileEditor.*;
+import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.rest.RestFileType;
+import org.jetbrains.annotations.NotNull;
+
+public class RestSplitEditorProvider implements FileEditorProvider, DumbAware {
+ public RestSplitEditorProvider() {
+ }
+
+ @Override
+ public boolean accept(@NotNull Project project, @NotNull VirtualFile file) {
+ return file.getFileType() == RestFileType.INSTANCE;
+ }
+
+ @NotNull
+ @Override
+ public FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) {
+ TextEditor editor = (TextEditor)TextEditorProvider.getInstance().createEditor(project, file);
+ return new TextEditorWithPreview(editor, new RestPreviewFileEditor(file, project));
+ }
+
+ @NotNull
+ @Override
+ public String getEditorTypeId() {
+ return "restructured-text-editor";
+ }
+
+ @NotNull
+ @Override
+ public FileEditorPolicy getPolicy() {
+ return FileEditorPolicy.HIDE_DEFAULT_EDITOR;
+ }
+}