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; + } +}