/* * Copyright (c) 2011-2014 Julien Nicoulaud <julien.nicoulaud@gmail.com> * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 net.nicoulaj.idea.markdown.editor; import com.intellij.codeHighlighting.BackgroundEditorHighlighter; import com.intellij.ide.structureView.StructureViewBuilder; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.event.DocumentAdapter; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorLocation; import com.intellij.openapi.fileEditor.FileEditorState; import com.intellij.openapi.fileEditor.FileEditorStateLevel; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.UserDataHolderBase; import com.intellij.ui.components.JBScrollPane; import net.nicoulaj.idea.markdown.MarkdownBundle; import net.nicoulaj.idea.markdown.settings.MarkdownGlobalSettings; import net.nicoulaj.idea.markdown.settings.MarkdownGlobalSettingsListener; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.pegdown.PegDownProcessor; import javax.swing.*; import javax.swing.text.DefaultCaret; import javax.swing.text.html.HTMLEditorKit; import javax.swing.text.html.StyleSheet; import java.awt.*; import java.beans.PropertyChangeListener; /** * {@link FileEditor} implementation that provides rendering preview for Markdown documents. * <p/> * The preview is generated by <a href="https://github.com/sirthias/pegdown">pegdown</a>. * * @author Julien Nicoulaud <julien.nicoulaud@gmail.com> * @author Roger Grantham (https://github.com/grantham) * @see <a href="https://github.com/sirthias/pegdown">pegdown library</a> * @see MarkdownPreviewEditorProvider * @since 0.1 */ public class MarkdownPreviewEditor extends UserDataHolderBase implements FileEditor { /** Logger. */ private static final Logger LOGGER = Logger.getInstance(MarkdownPreviewEditor.class); /** The editor name, displayed as the tab name of the editor. */ public static final String EDITOR_NAME = MarkdownBundle.message("markdown.editor.preview.tab-name"); /** The path to the stylesheet used for displaying the HTML preview of the document. */ @NonNls public static final String PREVIEW_STYLESHEET_PATH = "/net/nicoulaj/idea/markdown/preview.css"; /** The {@link java.awt.Component} used to render the HTML preview. */ protected final JEditorPane jEditorPane = new JEditorPane(); /** The {@link JBScrollPane} allowing to browse {@link #jEditorPane}. */ protected final JBScrollPane scrollPane = new JBScrollPane(jEditorPane); /** The {@link Document} previewed in this editor. */ protected final Document document; /** The {@link PegDownProcessor} used for building the document AST. */ private ThreadLocal<PegDownProcessor> processor = initProcessor(); /** Init/reinit thread local {@link PegDownProcessor}. */ private static ThreadLocal<PegDownProcessor> initProcessor() { return new ThreadLocal<PegDownProcessor>() { @Override protected PegDownProcessor initialValue() { return new PegDownProcessor(MarkdownGlobalSettings.getInstance().getExtensionsValue(), MarkdownGlobalSettings.getInstance().getParsingTimeout()); } }; } /** Indicates whether the HTML preview is obsolete and should regenerated from the Markdown {@link #document}. */ protected boolean previewIsObsolete = true; /** * Build a new instance of {@link MarkdownPreviewEditor}. * * @param project the {@link Project} containing the document * @param document the {@link com.intellij.openapi.editor.Document} previewed in this editor. */ public MarkdownPreviewEditor(@NotNull Project project, @NotNull Document document) { this.document = document; // Listen to the document modifications. this.document.addDocumentListener(new DocumentAdapter() { @Override public void documentChanged(DocumentEvent e) { previewIsObsolete = true; } }); // Listen to settings changes MarkdownGlobalSettings.getInstance().addListener(new MarkdownGlobalSettingsListener() { public void handleSettingsChanged(@NotNull final MarkdownGlobalSettings newSettings) { initProcessor(); previewIsObsolete = true; } }); // Setup the editor pane for rendering HTML. final HTMLEditorKit kit = new MarkdownEditorKit(document); final StyleSheet style = new StyleSheet(); style.importStyleSheet(MarkdownPreviewEditor.class.getResource(PREVIEW_STYLESHEET_PATH)); kit.setStyleSheet(style); jEditorPane.setEditorKit(kit); jEditorPane.setEditable(false); // Set the editor pane position to top left, and do not let it reset it jEditorPane.getCaret().setMagicCaretPosition(new Point(0, 0)); ((DefaultCaret) jEditorPane.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE); // Add a custom link listener which can resolve local link references. jEditorPane.addHyperlinkListener(new MarkdownLinkListener(jEditorPane, project, document)); } /** * Get the {@link java.awt.Component} to display as this editor's UI. * * @return a scrollable {@link JEditorPane}. */ @NotNull public JComponent getComponent() { return scrollPane; } /** * Get the component to be focused when the editor is opened. * * @return {@link #scrollPane} */ @Nullable public JComponent getPreferredFocusedComponent() { return scrollPane; } /** * Get the editor displayable name. * * @return {@link #EDITOR_NAME} */ @NotNull @NonNls public String getName() { return EDITOR_NAME; } /** * Get the state of the editor. * <p/> * Just returns {@link FileEditorState#INSTANCE} as {@link MarkdownPreviewEditor} is stateless. * * @param level the level. * @return {@link FileEditorState#INSTANCE} * @see #setState(com.intellij.openapi.fileEditor.FileEditorState) */ @NotNull public FileEditorState getState(@NotNull FileEditorStateLevel level) { return FileEditorState.INSTANCE; } /** * Set the state of the editor. * <p/> * Does not do anything as {@link MarkdownPreviewEditor} is stateless. * * @param state the new state. * @see #getState(com.intellij.openapi.fileEditor.FileEditorStateLevel) */ public void setState(@NotNull FileEditorState state) { } /** * Indicates whether the document content is modified compared to its file. * * @return {@code false} as {@link MarkdownPreviewEditor} is read-only. */ public boolean isModified() { return false; } /** * Indicates whether the editor is valid. * * @return {@code true} if {@link #document} content is readable. */ public boolean isValid() { return document.getText() != null; } /** * Invoked when the editor is selected. * <p/> * Update the HTML content if obsolete. */ public void selectNotify() { if (previewIsObsolete) { try { jEditorPane.setText("<div id=\"markdown-preview\">" + processor.get().markdownToHtml(document.getText()) + "</div>"); previewIsObsolete = false; } catch (Exception e) { LOGGER.error("Failed processing Markdown document", e); } } } /** * Invoked when the editor is deselected. * <p/> * Does nothing. */ public void deselectNotify() { } /** * Add specified listener. * <p/> * Does nothing. * * @param listener the listener. */ public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { } /** * Remove specified listener. * <p/> * Does nothing. * * @param listener the listener. */ public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { } /** * Get the background editor highlighter. * * @return {@code null} as {@link MarkdownPreviewEditor} does not require highlighting. */ @Nullable public BackgroundEditorHighlighter getBackgroundHighlighter() { return null; } /** * Get the current location. * * @return {@code null} as {@link MarkdownPreviewEditor} is not navigable. */ @Nullable public FileEditorLocation getCurrentLocation() { return null; } /** * Get the structure view builder. * * @return TODO {@code null} as parsing/PSI is not implemented. */ @Nullable public StructureViewBuilder getStructureViewBuilder() { return null; } /** Dispose the editor. */ public void dispose() { Disposer.dispose(this); } }