org.eclipse.mylyn.internal.wikitext.ui.editor.MarkupEditor.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.mylyn.internal.wikitext.ui.editor.MarkupEditor.java

Source

/*******************************************************************************
 * Copyright (c) 2007, 2016 David Green and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     David Green - initial API and implementation
 *******************************************************************************/
package org.eclipse.mylyn.internal.wikitext.ui.editor;

import static java.text.MessageFormat.format;

import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jface.action.AbstractAction;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.commands.ActionHandler;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.IDocumentPartitioningListener;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewerExtension6;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.URLHyperlink;
import org.eclipse.jface.text.reconciler.IReconciler;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.IVerticalRuler;
import org.eclipse.jface.text.source.projection.IProjectionListener;
import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
import org.eclipse.jface.text.source.projection.ProjectionSupport;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.mylyn.internal.wikitext.ui.WikiTextUiPlugin;
import org.eclipse.mylyn.internal.wikitext.ui.editor.actions.PreviewOutlineItemAction;
import org.eclipse.mylyn.internal.wikitext.ui.editor.actions.SetMarkupLanguageAction;
import org.eclipse.mylyn.internal.wikitext.ui.editor.operations.AbstractDocumentCommand;
import org.eclipse.mylyn.internal.wikitext.ui.editor.operations.CommandManager;
import org.eclipse.mylyn.internal.wikitext.ui.editor.preferences.Preferences;
import org.eclipse.mylyn.internal.wikitext.ui.editor.reconciler.MarkupMonoReconciler;
import org.eclipse.mylyn.internal.wikitext.ui.editor.syntax.FastMarkupPartitioner;
import org.eclipse.mylyn.internal.wikitext.ui.editor.syntax.MarkupDocumentProvider;
import org.eclipse.mylyn.internal.wikitext.ui.editor.syntax.MarkupTokenScanner;
import org.eclipse.mylyn.internal.wikitext.ui.util.NlsResourceBundle;
import org.eclipse.mylyn.wikitext.core.parser.Attributes;
import org.eclipse.mylyn.wikitext.core.parser.DocumentBuilder.BlockType;
import org.eclipse.mylyn.wikitext.core.parser.MarkupParser;
import org.eclipse.mylyn.wikitext.core.parser.builder.HtmlDocumentBuilder;
import org.eclipse.mylyn.wikitext.core.parser.markup.AbstractMarkupLanguage;
import org.eclipse.mylyn.wikitext.core.parser.markup.MarkupLanguage;
import org.eclipse.mylyn.wikitext.core.parser.outline.OutlineItem;
import org.eclipse.mylyn.wikitext.core.parser.outline.OutlineParser;
import org.eclipse.mylyn.wikitext.ui.WikiText;
import org.eclipse.mylyn.wikitext.ui.editor.MarkupSourceViewerConfiguration;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.browser.ProgressAdapter;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPageLayout;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.editors.text.TextEditor;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.part.IShowInSource;
import org.eclipse.ui.part.IShowInTarget;
import org.eclipse.ui.part.IShowInTargetList;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.progress.UIJob;
import org.eclipse.ui.swt.IFocusService;
import org.eclipse.ui.texteditor.ContentAssistAction;
import org.eclipse.ui.texteditor.ITextEditorActionConstants;
import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;

/**
 * A text editor for editing lightweight markup. Can be configured to accept any {@link MarkupLanguage}, with pluggable
 * content assist, validation, and cheat-sheet help content.
 *
 * @author David Green
 * @author Nicolas Bros
 */
public class MarkupEditor extends TextEditor implements IShowInTarget, IShowInSource, CommandManager {
    private static final String CSS_CLASS_EDITOR_PREVIEW = "editorPreview"; //$NON-NLS-1$

    private static final String RULER_CONTEXT_MENU_ID = "org.eclipse.mylyn.internal.wikitext.ui.editor.MarkupEditor.ruler"; //$NON-NLS-1$

    /**
     * the name of the property that stores the markup language name for per-file preference
     *
     * @see IFile#setPersistentProperty(QualifiedName, String) property
     */
    private static final String MARKUP_LANGUAGE = "markupLanguage"; //$NON-NLS-1$

    /**
     * the source editing context
     */
    public static final String CONTEXT = "org.eclipse.mylyn.wikitext.ui.editor.markupSourceContext"; //$NON-NLS-1$

    /**
     * the ID of the editor
     */
    public static final String ID = "org.eclipse.mylyn.wikitext.ui.editor.markupEditor"; //$NON-NLS-1$

    private static final String[] SHOW_IN_TARGETS = { //
            "org.eclipse.ui.views.ResourceNavigator", //$NON-NLS-1$
            "org.eclipse.jdt.ui.PackageExplorer", //$NON-NLS-1$
            "org.eclipse.ui.navigator.ProjectExplorer", // 3.5 //$NON-NLS-1$
            IPageLayout.ID_OUTLINE };

    private static IShowInTargetList SHOW_IN_TARGET_LIST = new IShowInTargetList() {
        public String[] getShowInTargetIds() {
            return SHOW_IN_TARGETS;
        }
    };

    private IDocument document;

    private IDocumentListener documentListener;

    private boolean previewDirty = true;

    private boolean outlineDirty = true;

    private Browser browser;

    private MarkupEditorOutline outlinePage;

    private OutlineItem outlineModel;

    private final OutlineParser outlineParser = new OutlineParser();

    {
        outlineParser.setLabelMaxLength(48);
        outlineModel = outlineParser.createRootItem();
    }

    private boolean disableReveal = false;

    private ISourceViewer viewer;

    private IPropertyChangeListener preferencesListener;

    private IDocumentPartitioningListener documentPartitioningListener;

    private final MarkupSourceViewerConfiguration sourceViewerConfiguration;

    private CTabItem sourceTab;

    private ProjectionSupport projectionSupport;

    private Map<String, HeadingProjectionAnnotation> projectionAnnotationById;

    private boolean updateJobScheduled = false;

    protected int documentGeneration = 0;

    public static final String EDITOR_SOURCE_VIEWER = "org.eclipse.mylyn.wikitext.ui.editor.sourceViewer"; //$NON-NLS-1$

    private UIJob updateOutlineJob;

    private IFoldingStructure foldingStructure;

    private CTabFolder tabFolder;

    private CTabItem previewTab;

    public MarkupEditor() {
        setDocumentProvider(new MarkupDocumentProvider());
        sourceViewerConfiguration = new MarkupSourceViewerConfiguration(getPreferenceStore());
        sourceViewerConfiguration.setOutline(outlineModel);
        sourceViewerConfiguration.setShowInTarget(this);
        setSourceViewerConfiguration(sourceViewerConfiguration);
    }

    @Override
    protected ISourceViewer createSourceViewer(Composite parent, IVerticalRuler ruler, int styles) {
        sourceViewerConfiguration.initializeDefaultFonts();
        tabFolder = new CTabFolder(parent, SWT.BOTTOM);

        {
            sourceTab = new CTabItem(tabFolder, SWT.NONE);
            updateSourceTabLabel();

            viewer = new MarkupProjectionViewer(tabFolder, ruler, getOverviewRuler(), isOverviewRulerVisible(),
                    styles | SWT.WRAP);

            sourceTab.setControl(((Viewer) viewer).getControl());
            tabFolder.setSelection(sourceTab);
        }

        try {
            previewTab = new CTabItem(tabFolder, SWT.NONE);
            previewTab.setText(Messages.MarkupEditor_preview);
            previewTab.setToolTipText(Messages.MarkupEditor_preview_tooltip);

            browser = new Browser(tabFolder, SWT.NONE);
            // bug 260479: open hyperlinks in a browser
            browser.addLocationListener(new LocationListener() {
                public void changed(LocationEvent event) {
                    event.doit = false;
                }

                public void changing(LocationEvent event) {
                    // if it looks like an absolute URL
                    if (event.location.matches("([a-zA-Z]{3,8})://?.*")) { //$NON-NLS-1$

                        // workaround for browser problem (bug 262043)
                        int idxOfSlashHash = event.location.indexOf("/#"); //$NON-NLS-1$
                        if (idxOfSlashHash != -1) {
                            // allow javascript-based scrolling to work
                            if (!event.location.startsWith("file:///#")) { //$NON-NLS-1$
                                event.doit = false;
                            }
                            return;
                        }
                        // workaround end

                        event.doit = false;
                        try {
                            PlatformUI.getWorkbench().getBrowserSupport().createBrowser("org.eclipse.ui.browser") //$NON-NLS-1$
                                    .openURL(new URL(event.location));
                        } catch (Exception e) {
                            new URLHyperlink(new Region(0, 1), event.location).open();
                        }
                    }
                }
            });
            previewTab.setControl(browser);
        } catch (SWTError e) {
            // disable preview, the exception is probably due to the internal browser not being available
            if (previewTab != null) {
                previewTab.dispose();
                previewTab = null;
            }
            logPreviewTabUnavailable(e);
        }

        tabFolder.addSelectionListener(new SelectionListener() {

            public void widgetDefaultSelected(SelectionEvent selectionevent) {
                widgetSelected(selectionevent);
            }

            public void widgetSelected(SelectionEvent selectionevent) {
                if (isShowingPreview()) {
                    updatePreview();
                }
            }
        });
        viewer.getTextWidget().addSelectionListener(new SelectionListener() {
            public void widgetDefaultSelected(SelectionEvent e) {
            }

            public void widgetSelected(SelectionEvent e) {
                updateOutlineSelection();
            }

        });
        viewer.getTextWidget().addKeyListener(new KeyAdapter() {
            @Override
            public void keyReleased(KeyEvent e) {
                if (isRelevantKeyCode(e.keyCode)) {
                    updateOutlineSelection();
                }
            }

            private boolean isRelevantKeyCode(int keyCode) {
                // for some reason not all key presses result in a selection change
                switch (keyCode) {
                case SWT.ARROW_DOWN:
                case SWT.ARROW_LEFT:
                case SWT.ARROW_RIGHT:
                case SWT.ARROW_UP:
                case SWT.PAGE_DOWN:
                case SWT.PAGE_UP:
                    return true;
                }
                return false;
            }
        });
        viewer.getTextWidget().addMouseListener(new MouseAdapter() {
            @Override
            public void mouseUp(MouseEvent e) {
                updateOutlineSelection();
            }
        });

        IFocusService focusService = (IFocusService) PlatformUI.getWorkbench().getService(IFocusService.class);
        if (focusService != null) {
            focusService.addFocusTracker(viewer.getTextWidget(), MarkupEditor.EDITOR_SOURCE_VIEWER);
        }

        viewer.getTextWidget().setData(MarkupLanguage.class.getName(), getMarkupLanguage());
        viewer.getTextWidget().setData(ISourceViewer.class.getName(), viewer);

        getSourceViewerDecorationSupport(viewer);

        updateDocument();

        if (preferencesListener == null) {
            preferencesListener = new IPropertyChangeListener() {
                public void propertyChange(PropertyChangeEvent event) {
                    if (viewer.getTextWidget() == null || viewer.getTextWidget().isDisposed()) {
                        return;
                    }
                    if (isFontPreferenceChange(event)) {
                        viewer.getTextWidget().getDisplay().asyncExec(new Runnable() {
                            public void run() {
                                reloadPreferences();
                            }
                        });
                    }
                }
            };
            WikiTextUiPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(preferencesListener);
        }

        return viewer;
    }

    private void logPreviewTabUnavailable(SWTError e) {
        WikiTextUiPlugin.getDefault().getLog().log(WikiTextUiPlugin.getDefault()
                .createStatus(format(Messages.MarkupEditor_previewUnavailable, e.getMessage()), IStatus.ERROR, e));
    }

    @Override
    public void createPartControl(Composite parent) {
        super.createPartControl(parent);
        ProjectionViewer viewer = (ProjectionViewer) getSourceViewer();
        // fix bug 267553: font problems can occur if the default font of the text widget doesn't match the
        //                 default font returned by the token scanner
        if (sourceViewerConfiguration.getDefaultFont() != null) {
            viewer.getTextWidget().setFont(sourceViewerConfiguration.getDefaultFont());
        }

        projectionSupport = new ProjectionSupport(viewer, getAnnotationAccess(), getSharedColors());
        projectionSupport.install();

        syncProjectionModeWithPreferences();

        viewer.addProjectionListener(new IProjectionListener() {
            public void projectionDisabled() {
                projectionAnnotationById = null;
                saveProjectionPreferences();
            }

            public void projectionEnabled() {
                saveProjectionPreferences();
                updateProjectionAnnotations();
            }
        });

        if (!outlineDirty && isFoldingEnabled()) {
            updateProjectionAnnotations();
        }
        JFaceResources.getFontRegistry().addListener(preferencesListener);
    }

    private void reloadPreferences() {
        previewDirty = true;
        syncProjectionModeWithPreferences();
        ((MarkupTokenScanner) sourceViewerConfiguration.getMarkupScanner()).reloadPreferences();
        sourceViewerConfiguration.initializeDefaultFonts();
        viewer.invalidateTextPresentation();
    }

    private boolean isFontPreferenceChange(PropertyChangeEvent event) {
        if (event.getProperty().equals(sourceViewerConfiguration.getFontPreference())
                || event.getProperty().equals(sourceViewerConfiguration.getMonospaceFontPreference())) {
            return true;
        }
        return false;
    }

    @Override
    protected void handlePreferenceStoreChanged(PropertyChangeEvent event) {
        super.handlePreferenceStoreChanged(event);
        reloadPreferences();
    }

    private void syncProjectionModeWithPreferences() {
        ProjectionViewer viewer = (ProjectionViewer) getSourceViewer();
        if (viewer.isProjectionMode() != WikiTextUiPlugin.getDefault().getPreferences().isEditorFolding()) {
            viewer.doOperation(ProjectionViewer.TOGGLE);
        }
    }

    @Override
    public void updatePartControl(IEditorInput input) {
        super.updatePartControl(input);
        updateDocument();
    }

    public void saveProjectionPreferences() {
        if (isFoldingEnabled() != WikiTextUiPlugin.getDefault().getPreferences().isEditorFolding()) {
            Preferences preferences = WikiTextUiPlugin.getDefault().getPreferences().clone();
            preferences.setEditorFolding(isFoldingEnabled());
            preferences.save(WikiTextUiPlugin.getDefault().getPreferenceStore(), false);
        }
    }

    @Override
    public void dispose() {
        if (document != null) {
            if (documentListener != null) {
                document.removeDocumentListener(documentListener);
            }
            if (documentPartitioningListener != null) {
                document.removeDocumentPartitioningListener(documentPartitioningListener);
            }
            document = null;
        }
        if (preferencesListener != null) {
            WikiTextUiPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(preferencesListener);
            JFaceResources.getFontRegistry().addListener(preferencesListener);
            preferencesListener = null;
        }
        super.dispose();
    }

    @Override
    protected void initializeEditor() {
        super.initializeEditor(); // ORDER DEPENDENCY
        setHelpContextId(CONTEXT); // ORDER DEPENDENCY
        setRulerContextMenuId(RULER_CONTEXT_MENU_ID);

    }

    @Override
    protected void doSetInput(IEditorInput input) throws CoreException {
        super.doSetInput(input);
        updateDocument();
        IFile file = getFile();
        if (sourceViewerConfiguration != null) {
            sourceViewerConfiguration.setFile(file);
        }
        initializeMarkupLanguage(input);
        outlineModel.setResourcePath(file == null ? null : file.getFullPath().toString());
    }

    private void updateDocument() {
        if (getSourceViewer() != null) {
            IDocument previousDocument = document;
            document = getSourceViewer().getDocument();
            if (previousDocument == document) {
                return;
            }
            if (previousDocument != null && documentListener != null) {
                previousDocument.removeDocumentListener(documentListener);
            }
            if (previousDocument != null && documentPartitioningListener != null) {
                previousDocument.removeDocumentPartitioningListener(documentPartitioningListener);
            }
            if (document != null) {
                if (documentListener == null) {
                    documentListener = new IDocumentListener() {
                        public void documentAboutToBeChanged(DocumentEvent event) {
                        }

                        public void documentChanged(DocumentEvent event) {
                            previewDirty = true;
                            outlineDirty = true;
                            synchronized (MarkupEditor.this) {
                                ++documentGeneration;
                            }
                            scheduleOutlineUpdate();
                            if (isShowingPreview()) {
                                updatePreview();
                            }
                        }

                    };
                }
                document.addDocumentListener(documentListener);
                if (documentPartitioningListener == null) {
                    documentPartitioningListener = new IDocumentPartitioningListener() {

                        public void documentPartitioningChanged(IDocument document) {
                            // async update
                            scheduleOutlineUpdate();
                        }
                    };
                }
                document.addDocumentPartitioningListener(documentPartitioningListener);
            }

            previewDirty = true;
            outlineDirty = true;
            updateOutline();
        }
    }

    /**
     * JavaScript that returns the current top scroll position of the browser widget
     */
    private static final String JAVASCRIPT_GETSCROLLTOP = "function getScrollTop() { " //$NON-NLS-1$
            + "  if(typeof pageYOffset!='undefined') return pageYOffset;" //$NON-NLS-1$
            + "  else{" + //$NON-NLS-1$
            "var B=document.body;" + //$NON-NLS-1$
            "var D=document.documentElement;" + //$NON-NLS-1$
            "D=(D.clientHeight)?D:B;return D.scrollTop;}" //$NON-NLS-1$
            + "}; return getScrollTop();"; //$NON-NLS-1$

    /**
     * updates the preview
     */
    private void updatePreview() {
        updatePreview(null);
    }

    /**
     * updates the preview and optionally reveal the section that corresponds to the given outline item.
     *
     * @param outlineItem
     *            the outline item, or null
     */
    private void updatePreview(final OutlineItem outlineItem) {
        if (previewDirty && browser != null) {
            Object result = browser.evaluate(JAVASCRIPT_GETSCROLLTOP);
            final int verticalScrollbarPos = result != null ? ((Number) result).intValue() : 0;
            String xhtml = null;
            if (document == null) {
                xhtml = "<?xml version=\"1.0\" ?><html xmlns=\"http://www.w3.org/1999/xhtml\"><body></body></html>"; //$NON-NLS-1$
            } else {
                try {
                    IFile file = getFile();
                    String title = file == null ? "" : file.getName(); //$NON-NLS-1$
                    if (title.lastIndexOf('.') != -1) {
                        title = title.substring(0, title.lastIndexOf('.'));
                    }
                    StringWriter writer = new StringWriter();
                    HtmlDocumentBuilder builder = new HtmlDocumentBuilder(writer) {
                        @Override
                        protected void emitAnchorHref(String href) {
                            if (href != null && href.startsWith("#")) { //$NON-NLS-1$
                                writer.writeAttribute("onclick", //$NON-NLS-1$
                                        String.format("javascript: window.location.hash = '%s'; return false;", //$NON-NLS-1$
                                                href));
                                writer.writeAttribute("href", "#"); //$NON-NLS-1$//$NON-NLS-2$
                            } else {
                                super.emitAnchorHref(href);
                            }
                        }

                        @Override
                        public void beginHeading(int level, Attributes attributes) {
                            attributes.appendCssClass(CSS_CLASS_EDITOR_PREVIEW);
                            super.beginHeading(level, attributes);
                        }

                        @Override
                        public void beginBlock(BlockType type, Attributes attributes) {
                            attributes.appendCssClass(CSS_CLASS_EDITOR_PREVIEW);
                            super.beginBlock(type, attributes);
                        }
                    };
                    builder.setTitle(title);

                    IPath location = file == null ? null : file.getLocation();
                    if (location != null) {
                        builder.setBaseInHead(true);
                        builder.setBase(location.removeLastSegments(1).toFile().toURI());
                    }

                    String css = WikiTextUiPlugin.getDefault().getPreferences().getMarkupViewerCss();
                    if (css != null && css.length() > 0) {
                        builder.addCssStylesheet(new HtmlDocumentBuilder.Stylesheet(new StringReader(css)));
                    }

                    MarkupLanguage markupLanguage = getMarkupLanguage();
                    if (markupLanguage != null) {
                        markupLanguage = markupLanguage.clone();
                        if (markupLanguage instanceof AbstractMarkupLanguage) {
                            ((AbstractMarkupLanguage) markupLanguage).setEnableMacros(true);
                        }

                        if (markupLanguage instanceof AbstractMarkupLanguage) {
                            AbstractMarkupLanguage language = (AbstractMarkupLanguage) markupLanguage;
                            language.setFilterGenerativeContents(false);
                            language.setBlocksOnly(false);
                        }

                        MarkupParser markupParser = new MarkupParser();
                        markupParser.setBuilder(builder);
                        markupParser.setMarkupLanguage(markupLanguage);

                        markupParser.parse(document.get());
                    } else {
                        builder.beginDocument();
                        builder.beginBlock(BlockType.PREFORMATTED, new Attributes());
                        builder.characters(document.get());
                        builder.endBlock();
                        builder.endDocument();
                    }
                    xhtml = writer.toString();
                } catch (Exception e) {
                    StringWriter stackTrace = new StringWriter();
                    PrintWriter writer = new PrintWriter(stackTrace);
                    e.printStackTrace(writer);
                    writer.close();

                    StringWriter documentWriter = new StringWriter();
                    HtmlDocumentBuilder builder = new HtmlDocumentBuilder(documentWriter);
                    builder.beginDocument();
                    builder.beginBlock(BlockType.PREFORMATTED, new Attributes());
                    builder.characters(stackTrace.toString());
                    builder.endBlock();
                    builder.endDocument();

                    xhtml = documentWriter.toString();
                }
            }
            browser.addProgressListener(new ProgressAdapter() {

                @Override
                public void completed(ProgressEvent event) {
                    browser.removeProgressListener(this);
                    if (outlineItem != null) {
                        revealInBrowser(outlineItem);
                    } else {
                        browser.execute(String.format("window.scrollTo(0,%d);", verticalScrollbarPos)); //$NON-NLS-1$
                    }
                }

            });
            browser.setText(xhtml);
            previewDirty = false;
        } else if (outlineItem != null && browser != null) {
            revealInBrowser(outlineItem);
        }
    }

    public IFile getFile() {
        IEditorInput editorInput = getEditorInput();
        if (editorInput instanceof IFileEditorInput) {
            IFileEditorInput fileEditorInput = (IFileEditorInput) editorInput;
            return fileEditorInput.getFile();
        }
        return null;
    }

    @SuppressWarnings("rawtypes")
    @Override
    public Object getAdapter(Class adapter) {
        if (IContentOutlinePage.class == adapter) {
            if (!isOutlinePageValid()) {
                outlinePage = new MarkupEditorOutline(this);
            }
            return outlinePage;
        }
        if (adapter == OutlineItem.class) {
            return getOutlineModel();
        }
        if (adapter == IFoldingStructure.class) {
            if (!isFoldingEnabled()) {
                return null;
            }
            if (foldingStructure == null) {
                foldingStructure = new FoldingStructure(this);
            }
            return foldingStructure;
        }
        if (adapter == IShowInTargetList.class) {
            return SHOW_IN_TARGET_LIST;
        }
        return super.getAdapter(adapter);
    }

    public ISourceViewer getViewer() {
        return viewer;
    }

    public OutlineItem getOutlineModel() {
        // ensure that outline model is caught up with current version of document
        if (outlineDirty) {
            updateOutlineNow();
        }
        return outlineModel;
    }

    private void scheduleOutlineUpdate() {
        synchronized (MarkupEditor.this) {
            if (updateJobScheduled) {
                return;
            }
        }
        updateOutlineJob = new UIJob(Messages.MarkupEditor_updateOutline) {
            @Override
            public IStatus runInUIThread(IProgressMonitor monitor) {
                synchronized (MarkupEditor.this) {
                    updateJobScheduled = false;
                }
                if (!outlineDirty) {
                    return Status.CANCEL_STATUS;
                }
                updateOutline();
                return Status.OK_STATUS;
            }
        };
        updateOutlineJob.addJobChangeListener(new JobChangeAdapter() {
            @Override
            public void scheduled(IJobChangeEvent event) {
                synchronized (MarkupEditor.this) {
                    updateJobScheduled = true;
                }
            }

            @Override
            public void done(IJobChangeEvent event) {
                synchronized (MarkupEditor.this) {
                    updateJobScheduled = false;
                    updateOutlineJob = null;
                }
            }
        });
        updateOutlineJob.setUser(false);
        updateOutlineJob.setSystem(true);
        updateOutlineJob.setPriority(Job.INTERACTIVE);
        updateOutlineJob.schedule(600);
    }

    private void updateOutlineNow() {
        if (!outlineDirty) {
            return;
        }
        if (!isSourceViewerValid()) {
            return;
        }
        // we maintain the outline even if the outline page is not in use, which allows us to use the outline for
        // content assist and other things

        MarkupLanguage markupLanguage = getMarkupLanguage();
        if (markupLanguage == null) {
            return;
        }
        final MarkupLanguage language = markupLanguage.clone();
        final String content = document.get();
        final int contentGeneration;
        synchronized (MarkupEditor.this) {
            contentGeneration = documentGeneration;
        }
        outlineParser.setMarkupLanguage(language);
        OutlineItem rootItem = outlineParser.parse(content);
        updateOutline(contentGeneration, rootItem);
    }

    private void updateOutline() {
        if (!outlineDirty) {
            return;
        }
        if (!isSourceViewerValid()) {
            return;
        }
        // we maintain the outline even if the outline page is not in use, which allows us to use the outline for
        // content assist and other things

        MarkupLanguage markupLanguage = getMarkupLanguage();
        if (markupLanguage == null) {
            return;
        }
        final MarkupLanguage language = markupLanguage.clone();

        final Display display = getSourceViewer().getTextWidget().getDisplay();
        final String content = document.get();
        final int contentGeneration;
        synchronized (MarkupEditor.this) {
            contentGeneration = documentGeneration;
        }
        // we parse the outline in another thread so that the UI remains responsive
        Job parseOutlineJob = new Job(MarkupEditor.class.getSimpleName() + "#updateOutline") { //$NON-NLS-1$
            @Override
            protected IStatus run(IProgressMonitor monitor) {
                outlineParser.setMarkupLanguage(language);
                if (shouldCancel()) {
                    return Status.CANCEL_STATUS;
                }
                final OutlineItem rootItem = outlineParser.parse(content);
                if (shouldCancel()) {
                    return Status.CANCEL_STATUS;
                }

                display.asyncExec(new Runnable() {
                    public void run() {
                        updateOutline(contentGeneration, rootItem);
                    }
                });
                return Status.OK_STATUS;
            }

            private boolean shouldCancel() {
                synchronized (MarkupEditor.this) {
                    if (contentGeneration != documentGeneration) {
                        return true;
                    }
                }
                return false;
            }
        };
        parseOutlineJob.setPriority(Job.INTERACTIVE);
        parseOutlineJob.setSystem(true);
        parseOutlineJob.schedule();
    }

    private void updateOutline(int contentGeneration, OutlineItem rootItem) {
        if (!isSourceViewerValid()) {
            return;
        }
        synchronized (this) {
            if (contentGeneration != documentGeneration) {
                return;
            }
        }
        outlineDirty = false;

        outlineModel.clear();
        outlineModel.moveChildren(rootItem);

        IFile file = getFile();
        outlineModel.setResourcePath(file == null ? null : file.getFullPath().toString());

        if (isOutlinePageValid()) {
            outlinePage.refresh();

            outlinePage.getControl().getDisplay().asyncExec(new Runnable() {
                public void run() {
                    if (isOutlinePageValid()) {
                        updateOutlineSelection();
                    }
                }
            });
        }
        updateProjectionAnnotations();
    }

    private boolean isOutlinePageValid() {
        return outlinePage != null && outlinePage.getControl() != null && !outlinePage.getControl().isDisposed();
    }

    private boolean isSourceViewerValid() {
        return getSourceViewer() != null && getSourceViewer().getTextWidget() != null
                && !getSourceViewer().getTextWidget().isDisposed();
    }

    @SuppressWarnings("unchecked")
    private void updateProjectionAnnotations() {
        ProjectionViewer viewer = (ProjectionViewer) getSourceViewer();
        ProjectionAnnotationModel projectionAnnotationModel = viewer.getProjectionAnnotationModel();
        if (projectionAnnotationModel != null) {
            List<Annotation> newProjectionAnnotations = new ArrayList<>(
                    projectionAnnotationById == null ? 10 : projectionAnnotationById.size() + 2);
            Map<HeadingProjectionAnnotation, Position> annotationToPosition = new HashMap<>();

            List<OutlineItem> children = outlineModel.getChildren();
            if (!children.isEmpty()) {
                createProjectionAnnotations(newProjectionAnnotations, annotationToPosition, children,
                        document.getLength());
            }
            if (newProjectionAnnotations.isEmpty()
                    && (projectionAnnotationById == null || projectionAnnotationById.isEmpty())) {
                return;
            }

            Map<String, HeadingProjectionAnnotation> newProjectionAnnotationById = new HashMap<>();

            if (projectionAnnotationById != null) {
                Set<HeadingProjectionAnnotation> toDelete = new HashSet<>(projectionAnnotationById.size());
                Iterator<Entry<HeadingProjectionAnnotation, Position>> newPositionIt = annotationToPosition
                        .entrySet().iterator();
                while (newPositionIt.hasNext()) {
                    Entry<HeadingProjectionAnnotation, Position> newAnnotationEnt = newPositionIt.next();

                    HeadingProjectionAnnotation newAnnotation = newAnnotationEnt.getKey();
                    Position newPosition = newAnnotationEnt.getValue();
                    HeadingProjectionAnnotation annotation = projectionAnnotationById
                            .get(newAnnotation.getHeadingId());
                    if (annotation != null) {
                        Position position = projectionAnnotationModel.getPosition(annotation);
                        if (newPosition.equals(position)) {
                            newPositionIt.remove();
                            newProjectionAnnotationById.put(annotation.getHeadingId(), annotation);
                        } else {
                            toDelete.add(annotation);
                            if (annotation.isCollapsed()) {
                                newAnnotation.markCollapsed();
                            } else {
                                newAnnotation.markExpanded();
                            }
                            newProjectionAnnotationById.put(annotation.getHeadingId(), newAnnotation);
                        }
                    } else {
                        newProjectionAnnotationById.put(newAnnotation.getHeadingId(), newAnnotation);
                    }
                }
                Iterator<Annotation> annotationIt = projectionAnnotationModel.getAnnotationIterator();
                while (annotationIt.hasNext()) {
                    Annotation annotation = annotationIt.next();
                    if (annotation instanceof HeadingProjectionAnnotation) {
                        HeadingProjectionAnnotation projectionAnnotation = (HeadingProjectionAnnotation) annotation;
                        if (!projectionAnnotationById.containsKey(projectionAnnotation.getHeadingId())
                                && !toDelete.contains(projectionAnnotation)) {
                            toDelete.add(projectionAnnotation);
                        }
                    }
                }
                projectionAnnotationModel.modifyAnnotations(
                        toDelete.isEmpty() ? null : toDelete.toArray(new Annotation[toDelete.size()]),
                        annotationToPosition, null);
            } else {
                projectionAnnotationModel.modifyAnnotations(null, annotationToPosition, null);
                for (HeadingProjectionAnnotation annotation : annotationToPosition.keySet()) {
                    newProjectionAnnotationById.put(annotation.getHeadingId(), annotation);
                }
            }
            projectionAnnotationById = newProjectionAnnotationById;
        } else {
            projectionAnnotationById = null;
        }
    }

    private void createProjectionAnnotations(List<Annotation> newProjectionAnnotations,
            Map<HeadingProjectionAnnotation, Position> annotationToPosition, List<OutlineItem> children,
            int endOffset) {
        final int size = children.size();
        final int lastIndex = size - 1;
        for (int x = 0; x < size; ++x) {
            OutlineItem child = children.get(x);
            if (child.getId() == null || child.getId().length() == 0) {
                continue;
            }
            int offset = child.getOffset();
            int end;
            if (x == lastIndex) {
                end = endOffset;
            } else {
                end = children.get(x + 1).getOffset();
            }
            int length = end - offset;

            if (length > 0) {
                HeadingProjectionAnnotation annotation = new HeadingProjectionAnnotation(child.getId());
                Position position = new Position(offset, length);

                newProjectionAnnotations.add(annotation);
                annotationToPosition.put(annotation, position);
            }

            if (!child.getChildren().isEmpty()) {
                createProjectionAnnotations(newProjectionAnnotations, annotationToPosition, child.getChildren(),
                        end);
            }
        }
    }

    private void updateOutlineSelection() {
        if (disableReveal) {
            return;
        }
        if (outlineModel != null && outlinePage != null) {

            disableReveal = true;
            try {
                OutlineItem item = getNearestMatchingOutlineItem();
                if (item != null) {
                    outlinePage.setSelection(new StructuredSelection(item));
                }
            } finally {
                disableReveal = false;
            }
        }
    }

    /**
     * get the outline item nearest matching the selection in the source viewer
     */
    private OutlineItem getNearestMatchingOutlineItem() {
        Point selectedRange = getSourceViewer().getSelectedRange();
        if (selectedRange != null) {
            return outlineModel.findNearestMatchingOffset(selectedRange.x);
        }
        return null;
    }

    @Override
    protected void initializeKeyBindingScopes() {
        setKeyBindingScopes(new String[] { CONTEXT });
    }

    @Override
    public void init(IEditorSite site, IEditorInput input) throws PartInitException {
        super.init(site, input);

        IContextService contextService = (IContextService) site.getService(IContextService.class);
        contextService.activateContext(CONTEXT);

    }

    private void initializeMarkupLanguage(IEditorInput input) {
        MarkupLanguage markupLanguage = loadMarkupLanguagePreference();
        if (markupLanguage == null) {
            String name = input.getName();
            if (input instanceof IFileEditorInput) {
                name = ((IFileEditorInput) input).getFile().getName();
            } else if (input instanceof IPathEditorInput) {
                name = ((IPathEditorInput) input).getPath().lastSegment();
            }
            markupLanguage = WikiText.getMarkupLanguageForFilename(name);
            if (markupLanguage == null) {
                markupLanguage = WikiText.getMarkupLanguage("Textile"); //$NON-NLS-1$
            }
        }
        setMarkupLanguage(markupLanguage, false);
    }

    public void setMarkupLanguage(MarkupLanguage markupLanguage, boolean persistSetting) {
        if (markupLanguage instanceof AbstractMarkupLanguage) {
            ((AbstractMarkupLanguage) markupLanguage).setEnableMacros(false);
        }
        ((MarkupDocumentProvider) getDocumentProvider()).setMarkupLanguage(markupLanguage);

        IDocument document = getDocumentProvider().getDocument(getEditorInput());
        IDocumentPartitioner partitioner = document.getDocumentPartitioner();
        if (partitioner instanceof FastMarkupPartitioner) {
            final FastMarkupPartitioner fastMarkupPartitioner = (FastMarkupPartitioner) partitioner;
            fastMarkupPartitioner.setMarkupLanguage(markupLanguage);
        }
        sourceViewerConfiguration.setMarkupLanguage(markupLanguage);
        if (getSourceViewer() != null) {
            getSourceViewer().invalidateTextPresentation();
        }
        outlineDirty = true;
        scheduleOutlineUpdate();
        updateSourceTabLabel();

        if (viewer != null) {
            viewer.getTextWidget().setData(MarkupLanguage.class.getName(), getMarkupLanguage());
        }

        if (persistSetting && markupLanguage != null) {
            storeMarkupLanguagePreference(markupLanguage);
        }
        if (persistSetting) {
            ISourceViewer sourceViewer = getSourceViewer();
            if (sourceViewer instanceof MarkupProjectionViewer) {
                IReconciler reconciler = ((MarkupProjectionViewer) sourceViewer).getReconciler();
                if (reconciler instanceof MarkupMonoReconciler) {
                    ((MarkupMonoReconciler) reconciler).forceReconciling();
                }
            }
        }
    }

    private void updateSourceTabLabel() {
        if (sourceTab != null) {
            // bug 270215 carbon shows tooltip in source editing area.
            boolean isCarbon = Platform.WS_CARBON.equals(Platform.getWS());

            MarkupLanguage markupLanguage = getMarkupLanguage();
            if (markupLanguage == null) {
                sourceTab.setText(Messages.MarkupEditor_markupSource);
                if (!isCarbon) {
                    sourceTab.setToolTipText(Messages.MarkupEditor_markupSource_tooltip);
                }
            } else {
                sourceTab.setText(NLS.bind(Messages.MarkupEditor_markupSource_named,
                        new Object[] { markupLanguage.getName() }));
                if (!isCarbon) {
                    sourceTab.setToolTipText(NLS.bind(Messages.MarkupEditor_markupSource_tooltip_named,
                            new Object[] { markupLanguage.getName() }));
                }
            }
        }
    }

    private MarkupLanguage loadMarkupLanguagePreference() {
        IFile file = getFile();
        if (file != null) {
            return loadMarkupLanguagePreference(file);
        }
        return null;
    }

    /**
     * lookup the markup language preference of a file based on the persisted preference.
     *
     * @param file
     *            the file for which the preference should be looked up
     * @return the markup language preference, or null if it was not set or could not be loaded.
     */
    public static MarkupLanguage loadMarkupLanguagePreference(IFile file) {
        String languageName = getMarkupLanguagePreference(file);
        if (languageName != null) {
            return WikiText.getMarkupLanguage(languageName);
        }
        return null;
    }

    /**
     * lookup the markup language preference of a file based on the persisted preference.
     *
     * @param file
     *            the file for which the preference should be looked up
     * @return the markup language name, or null if no preference exists
     */
    public static String getMarkupLanguagePreference(IFile file) {
        if (file.exists()) {
            try {
                return file.getPersistentProperty(new QualifiedName(WikiTextUiPlugin.getDefault().getPluginId(),
                        MarkupEditor.MARKUP_LANGUAGE));
            } catch (CoreException e) {
                WikiTextUiPlugin.getDefault().log(IStatus.ERROR, Messages.MarkupEditor_markupPreferenceError, e);
            }
        }
        return null;
    }

    private void storeMarkupLanguagePreference(MarkupLanguage markupLanguage) {
        if (markupLanguage == null) {
            throw new IllegalArgumentException();
        }
        IFile file = getFile();
        if (file != null) {
            MarkupLanguage defaultMarkupLanguage = WikiText.getMarkupLanguageForFilename(file.getName());
            String preference = markupLanguage.getName();
            if (defaultMarkupLanguage != null && defaultMarkupLanguage.getName().equals(preference)) {
                preference = null;
            }
            try {
                file.setPersistentProperty(
                        new QualifiedName(WikiTextUiPlugin.getDefault().getPluginId(), MARKUP_LANGUAGE),
                        preference);
            } catch (CoreException e) {
                WikiTextUiPlugin.getDefault().log(IStatus.ERROR,
                        NLS.bind(Messages.MarkupEditor_markupPreferenceError2, new Object[] { preference }), e);
            }
        }
    }

    public MarkupLanguage getMarkupLanguage() {
        IDocument document = getDocumentProvider().getDocument(getEditorInput());
        IDocumentPartitioner partitioner = document.getDocumentPartitioner();
        MarkupLanguage markupLanguage = null;
        if (partitioner instanceof FastMarkupPartitioner) {
            markupLanguage = ((FastMarkupPartitioner) partitioner).getMarkupLanguage();
        }
        return markupLanguage;
    }

    @Override
    protected void createActions() {
        super.createActions();

        IAction action;

        //      action = new ShowCheatSheetAction(this);
        //      setAction(action.getId(),action);

        action = new ContentAssistAction(new NlsResourceBundle(Messages.class), "ContentAssistProposal_", this); //$NON-NLS-1$
        action.setActionDefinitionId(ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS);
        setAction("ContentAssistProposal", action); //$NON-NLS-1$
        markAsStateDependentAction("ContentAssistProposal", true); //$NON-NLS-1$
    }

    @Override
    public void setAction(String actionID, IAction action) {
        if (action != null && action.getActionDefinitionId() != null && !isCommandAction(action)) {
            // bug 336679: don't activate handlers for CommandAction.
            // We do this by class name so that we don't rely on internals
            IHandlerService handlerService = (IHandlerService) getSite().getService(IHandlerService.class);
            handlerService.activateHandler(action.getActionDefinitionId(), new ActionHandler(action));
        }
        super.setAction(actionID, action);
    }

    private boolean isCommandAction(IAction action) {
        for (Class<?> clazz = action.getClass(); clazz != Object.class
                && clazz != AbstractAction.class; clazz = clazz.getSuperclass()) {
            if (clazz.getName().equals("org.eclipse.ui.internal.actions.CommandAction")) { //$NON-NLS-1$
                return true;
            }
        }
        return false;
    }

    @Override
    protected void editorContextMenuAboutToShow(IMenuManager menu) {
        super.editorContextMenuAboutToShow(menu);

        final MarkupLanguage markupLanguage = getMarkupLanguage();
        MenuManager markupLanguageMenu = new MenuManager(Messages.MarkupEditor_markupLanguage);
        for (String markupLanguageName : new TreeSet<>(WikiText.getMarkupLanguageNames())) {
            markupLanguageMenu.add(new SetMarkupLanguageAction(this, markupLanguageName,
                    markupLanguage != null && markupLanguageName.equals(markupLanguage.getName())));
        }

        menu.prependToGroup(ITextEditorActionConstants.GROUP_SETTINGS, markupLanguageMenu);

        OutlineItem nearestOutlineItem = getNearestMatchingOutlineItem();
        if (nearestOutlineItem != null && !nearestOutlineItem.isRootItem()) {
            menu.appendToGroup(ITextEditorActionConstants.GROUP_OPEN,
                    new PreviewOutlineItemAction(this, nearestOutlineItem));
        }
    }

    public boolean isFoldingEnabled() {
        ProjectionViewer viewer = (ProjectionViewer) getSourceViewer();
        return viewer.getProjectionAnnotationModel() != null;
    }

    public boolean show(ShowInContext context) {
        ISelection selection = context.getSelection();
        if (selection instanceof IStructuredSelection) {
            for (Object element : ((IStructuredSelection) selection).toArray()) {
                if (element instanceof OutlineItem) {
                    OutlineItem item = (OutlineItem) element;
                    selectAndReveal(item);
                    if (isOutlinePageValid()) {
                        outlinePage.setSelection(selection);
                    }
                    return true;
                }
            }
        } else if (selection instanceof ITextSelection) {
            ITextSelection textSel = (ITextSelection) selection;
            selectAndReveal(textSel.getOffset(), textSel.getLength());
            return true;
        }
        return false;
    }

    public void selectAndReveal(OutlineItem item) {
        selectAndReveal(item.getOffset(), item.getLength());
        if (isShowingPreview()) {
            // scroll preview to the selected item.
            revealInBrowser(item);
        }
    }

    private void revealInBrowser(OutlineItem item) {
        browser.execute(
                String.format("document.getElementById('%s').scrollIntoView(true);window.location.hash = '%s';", //$NON-NLS-1$
                        item.getId(), item.getId()));
    }

    public ShowInContext getShowInContext() {
        OutlineItem item = getNearestMatchingOutlineItem();
        return new ShowInContext(getEditorInput(),
                item == null ? new StructuredSelection() : new StructuredSelection(item));
    }

    /**
     * Causes the editor to display the preview at the specified outline item.
     */
    public void showPreview(OutlineItem outlineItem) {
        if (!isShowingPreview()) {
            tabFolder.setSelection(previewTab);
        }
        updatePreview(outlineItem);
    }

    public void perform(AbstractDocumentCommand command) throws CoreException {
        disableReveal = true;
        try {
            command.execute(((ITextViewerExtension6) getViewer()).getUndoManager(), getViewer().getDocument());
        } finally {
            disableReveal = false;
        }
        updateOutlineSelection();
    }

    private boolean isShowingPreview() {
        return previewTab != null && tabFolder.getSelection() == previewTab;
    }

    protected boolean getInitialWordWrapStatus() {
        return true;
    }
}