blob: 90f0bb8b9319b6786162c3edbc2435e6f1340ffa [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2013 David Green and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* David Green - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.wikitext.ui.editor;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
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.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.source.ISourceViewer;
import org.eclipse.jface.text.source.IVerticalRuler;
import org.eclipse.jface.text.source.SourceViewerConfiguration;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.mylyn.internal.wikitext.ui.editor.syntax.FastMarkupPartitioner;
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.util.ServiceLocator;
import org.eclipse.swt.SWT;
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.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
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.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.editors.text.TextEditor;
import org.eclipse.ui.part.IShowInSource;
import org.eclipse.ui.part.IShowInTarget;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.progress.UIJob;
import org.eclipse.ui.swt.IFocusService;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
/**
* A WikiText source editor. Users must {@link #setDocumentProvider(IDocumentProvider) set the document provider}.
*
* @author David Green
* @see AbstractWikiTextDocumentProvider
* @since 1.3
*/
public class WikiTextSourceEditor extends TextEditor implements IShowInSource, IShowInTarget {
private static final String EDITOR_SOURCE_VIEWER = "org.eclipse.mylyn.wikitext.ui.editor.sourceViewer"; //$NON-NLS-1$
/**
* The source editing context. This context id is activated via the {@link IContextService} when the editor is
* {@link #init(IEditorSite, IEditorInput) initialized}. This context is also used as the editor help context id.
*/
public static final String CONTEXT = "org.eclipse.mylyn.wikitext.ui.editor.markupSourceContext"; //$NON-NLS-1$
/**
* The property id for editor outline change events. Clients wishing to react to changes in the outline should
* listen for this event.
*/
public static final int PROP_OUTLINE = 0x10000001;
/**
* The property id for outline location change events. Outline location events are fired when navigation within the
* source document causes the nearest computed outline item to change. Clients wishing to react to changes in the
* location of the caret with respect to the current outline should listen for this event.
*/
public static final int PROP_OUTLINE_LOCATION = 0x10000002;
private MarkupLanguage markupLanguage;
private MarkupSourceViewer viewer;
private MarkupSourceViewerConfiguration sourceViewerConfiguration;
private boolean outlineDirty = true;
private int documentGeneration = 0;
private IDocument document;
private IDocumentListener documentListener;
private IDocumentPartitioningListener documentPartitioningListener;
private boolean updateJobScheduled = false;
private UIJob updateOutlineJob;
private OutlineItem outlineModel;
private OutlineItem outlineLocation;
private OutlineParser outlineParser;
private AbstractWikiTextSourceEditorOutline outlinePage;
public WikiTextSourceEditor() {
}
/**
* Users must set the document provider.
*
* @see AbstractWikiTextDocumentProvider
* @see WikiTextDocumentProvider
*/
@Override
public void setDocumentProvider(IDocumentProvider provider) {
if (provider instanceof WikiTextDocumentProvider) {
((WikiTextDocumentProvider) provider).setMarkupLanguage(getMarkupLanguage());
}
super.setDocumentProvider(provider);
}
/**
* Set the source viewer configuration. This method should be called in the constructor of subclasses.
*
* @throws ClassCastException
* if the configuration does not subclass {@link MarkupSourceViewerConfiguration}
*/
@Override
protected void setSourceViewerConfiguration(SourceViewerConfiguration configuration) {
sourceViewerConfiguration = (MarkupSourceViewerConfiguration) configuration;
super.setSourceViewerConfiguration(sourceViewerConfiguration);
}
@Override
protected ISourceViewer createSourceViewer(Composite parent, IVerticalRuler ruler, int styles) {
if (getMarkupLanguage() == null) {
setMarkupLanguage(ServiceLocator.getInstance().getMarkupLanguage("Textile")); //$NON-NLS-1$
}
viewer = createMarkupSourceViewer(parent, ruler, styles);
IFocusService focusService = PlatformUI.getWorkbench().getService(IFocusService.class);
if (focusService != null) {
focusService.addFocusTracker(viewer.getTextWidget(), EDITOR_SOURCE_VIEWER);
}
viewer.getTextWidget().setData(MarkupLanguage.class.getName(), getMarkupLanguage());
viewer.getTextWidget().setData(ISourceViewer.class.getName(), viewer);
viewer.getTextWidget().addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
detectOutlineLocationChanged();
}
});
viewer.getTextWidget().addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
if (isRelevantKeyCode(e.keyCode)) {
detectOutlineLocationChanged();
}
}
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) {
detectOutlineLocationChanged();
}
});
getSourceViewerDecorationSupport(viewer);
updateDocument();
return viewer;
}
/**
* Create a markup source viewer. Subclasses may override.
*
* @param parent
* the parent of the source viewer
* @param ruler
* the vertical ruler
* @param styles
* the styles to pass to the viewer
* @return a new markup source viewer
* @see #createSourceViewer(Composite, IVerticalRuler, int)
*/
protected MarkupSourceViewer createMarkupSourceViewer(Composite parent, IVerticalRuler ruler, int styles) {
return new MarkupSourceViewer(parent, ruler, styles | SWT.WRAP, getMarkupLanguage());
}
/**
* The markup language. If unspecified, it's assumed to be Textile.
*
* @return the current markup language, or null if it's unspecified.
* @since 3.0
*/
public MarkupLanguage getMarkupLanguage() {
return markupLanguage;
}
/**
* set the markup language. If unspecified, it's assumed to be Textile.
*
* @since 3.0
*/
public void setMarkupLanguage(MarkupLanguage markupLanguage) {
this.markupLanguage = markupLanguage;
if (this.markupLanguage instanceof AbstractMarkupLanguage) {
((AbstractMarkupLanguage) this.markupLanguage).setEnableMacros(false);
}
sourceViewerConfiguration.setMarkupLanguage(markupLanguage);
IDocumentProvider documentProvider = getDocumentProvider();
if (documentProvider instanceof WikiTextDocumentProvider) {
((WikiTextDocumentProvider) documentProvider).setMarkupLanguage(markupLanguage);
}
if (getEditorInput() != null) {
IDocument document = documentProvider.getDocument(getEditorInput());
IDocumentPartitioner partitioner = document.getDocumentPartitioner();
if (partitioner instanceof FastMarkupPartitioner) {
final FastMarkupPartitioner fastMarkupPartitioner = (FastMarkupPartitioner) partitioner;
fastMarkupPartitioner.setMarkupLanguage(markupLanguage);
}
}
if (viewer != null) {
viewer.getTextWidget().setData(MarkupLanguage.class.getName(), getMarkupLanguage());
}
if (getSourceViewer() != null) {
getSourceViewer().invalidateTextPresentation();
}
}
@Override
protected void initializeEditor() {
super.initializeEditor();
setHelpContextId(CONTEXT); // ORDER DEPENDENCY
setSourceViewerConfiguration(new MarkupSourceViewerConfiguration(getPreferenceStore()));
}
@Override
public void init(IEditorSite site, IEditorInput input) throws PartInitException {
super.init(site, input);
IContextService contextService = site.getService(IContextService.class);
contextService.activateContext(CONTEXT);
}
@SuppressWarnings({ "rawtypes" })
@Override
public Object getAdapter(Class adapter) {
if (adapter == IContentOutlinePage.class) {
if (outlinePage == null || outlinePage.getControl() == null || outlinePage.getControl().isDisposed()) {
outlinePage = createContentOutline();
outlinePage.setEditor(this);
}
return outlinePage;
}
if (adapter == OutlineItem.class) {
return getOutlineModel();
} else if (adapter == IShowInSource.class) {
return this;
} else if (adapter == IShowInTarget.class) {
return this;
}
return super.getAdapter(adapter);
}
/**
* subclasses may override to provide a non-default content outline.
*/
protected AbstractWikiTextSourceEditorOutline createContentOutline() {
return new DefaultWikiTextSourceEditorOutline();
}
@Override
public void updatePartControl(IEditorInput input) {
super.updatePartControl(input);
updateDocument();
}
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) {
outlineDirty = true;
synchronized (WikiTextSourceEditor.this) {
++documentGeneration;
}
scheduleOutlineUpdate();
}
};
}
document.addDocumentListener(documentListener);
if (documentPartitioningListener == null) {
documentPartitioningListener = new IDocumentPartitioningListener() {
public void documentPartitioningChanged(IDocument document) {
// async update
scheduleOutlineUpdate();
}
};
}
document.addDocumentPartitioningListener(documentPartitioningListener);
}
synchronized (WikiTextSourceEditor.this) {
outlineDirty = true;
}
updateOutline();
}
}
/**
* Get the outline model for the document being edited. The returned outline model is guaranteed to be up to date
* with respect to the current document. Note that the model will change if the document changes, however all
* changes occur on the UI thread.
*
* @since 3.0
*/
public final OutlineItem getOutlineModel() {
synchronized (WikiTextSourceEditor.this) {
// ensure that outline model is caught up with current version of document
if (outlineDirty || outlineModel == null) {
updateOutlineNow();
}
return outlineModel;
}
}
private void scheduleOutlineUpdate() {
synchronized (WikiTextSourceEditor.this) {
if (updateJobScheduled || outlineModel == null) {
return;
}
}
updateOutlineJob = new UIJob("WikiText - Outline Job") { //$NON-NLS-1$
@Override
public IStatus runInUIThread(IProgressMonitor monitor) {
synchronized (WikiTextSourceEditor.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 (WikiTextSourceEditor.this) {
updateJobScheduled = true;
}
}
@Override
public void done(IJobChangeEvent event) {
synchronized (WikiTextSourceEditor.this) {
updateJobScheduled = false;
updateOutlineJob = null;
}
}
});
updateOutlineJob.setUser(false);
updateOutlineJob.setSystem(true);
updateOutlineJob.setPriority(Job.INTERACTIVE);
updateOutlineJob.schedule(600); // NAGLE algorithm: capture more changes with a delay
}
private void updateOutline() {
if (!outlineDirty) {
return;
}
if (getSourceViewer().getTextWidget().isDisposed()) {
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 (WikiTextSourceEditor.this) {
contentGeneration = documentGeneration;
initializeOutlineParser();
}
// we parse the outline in another thread so that the UI remains responsive
Job parseOutlineJob = new Job(WikiTextSourceEditor.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 (WikiTextSourceEditor.this) {
if (contentGeneration != documentGeneration) {
return true;
}
}
return false;
}
};
parseOutlineJob.setPriority(Job.INTERACTIVE);
parseOutlineJob.setSystem(true);
parseOutlineJob.schedule();
}
private void updateOutlineNow() {
if (!outlineDirty) {
return;
}
if (getSourceViewer().getTextWidget().isDisposed()) {
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 (WikiTextSourceEditor.this) {
contentGeneration = documentGeneration;
initializeOutlineParser();
}
outlineParser.setMarkupLanguage(language);
OutlineItem rootItem = outlineParser.parse(content);
updateOutline(contentGeneration, rootItem);
}
private void initializeOutlineParser() {
synchronized (WikiTextSourceEditor.this) {
if (outlineParser == null) {
outlineParser = new OutlineParser();
outlineParser.setLabelMaxLength(48);
outlineModel = outlineParser.createRootItem();
}
}
}
private void updateOutline(int contentGeneration, OutlineItem rootItem) {
if (getSourceViewer().getTextWidget().isDisposed()) {
return;
}
synchronized (this) {
if (contentGeneration != documentGeneration) {
return;
}
}
outlineDirty = false;
outlineModel.clear();
outlineModel.moveChildren(rootItem);
IFile file = (IFile) getAdapter(IFile.class);
outlineModel.setResourcePath(file == null ? null : file.getFullPath().toString());
firePropertyChange(PROP_OUTLINE);
}
private void detectOutlineLocationChanged() {
OutlineItem nearestItem = getNearestMatchingOutlineItem();
if (nearestItem != outlineLocation && (nearestItem == null || !nearestItem.equals(outlineLocation))) {
outlineLocation = nearestItem;
firePropertyChange(PROP_OUTLINE_LOCATION);
}
}
public ShowInContext getShowInContext() {
OutlineItem item = getNearestMatchingOutlineItem();
return new ShowInContext(getEditorInput(),
item == null ? new StructuredSelection() : new StructuredSelection(item));
}
/**
* get the outline item nearest matching the selection in the source viewer
*/
private OutlineItem getNearestMatchingOutlineItem() {
Point selectedRange = getSourceViewer().getSelectedRange();
if (selectedRange != null) {
return getOutlineModel().findNearestMatchingOffset(selectedRange.x);
}
return null;
}
/* prevent line number ruler from appearing since it doesn't work with line wrapping
*/
@Override
protected boolean isLineNumberRulerVisible() {
return false;
}
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 (outlinePage != null && outlinePage.getControl() != null
&& !outlinePage.getControl().isDisposed()) {
outlinePage.setSelection(selection);
}
return true;
}
}
} else if (selection instanceof ITextSelection) {
ITextSelection textSel = (ITextSelection) selection;
selectAndReveal(textSel.getOffset(), textSel.getLength());
return true;
}
return false;
}
/**
* Select and reveal the given outline item, based on its offset and length.
*
* @param item
* the item, must not be null
* @since 3.0
*/
public void selectAndReveal(OutlineItem item) {
selectAndReveal(item.getOffset(), item.getLength());
}
}