| /******************************************************************************* |
| * 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()); |
| } |
| } |