/******************************************************************************* * 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.parser.Attributes; import org.eclipse.mylyn.wikitext.parser.DocumentBuilder.BlockType; import org.eclipse.mylyn.wikitext.parser.MarkupParser; import org.eclipse.mylyn.wikitext.parser.builder.HtmlDocumentBuilder; import org.eclipse.mylyn.wikitext.parser.markup.AbstractMarkupLanguage; import org.eclipse.mylyn.wikitext.parser.markup.MarkupLanguage; import org.eclipse.mylyn.wikitext.parser.outline.OutlineItem; import org.eclipse.mylyn.wikitext.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 = 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;", href)); //$NON-NLS-1$ 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(); } 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); } } @SuppressWarnings("unchecked") 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 = 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 = 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; } }