/****************************************************************************** * Copyright (C) 2013 Fabio Zadrozny * * 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: * Fabio Zadrozny <fabiofz@gmail.com> - initial API and implementation ******************************************************************************/ package org.python.pydev.shared_ui.editor; import java.io.File; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IStorage; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.text.source.IOverviewRuler; import org.eclipse.jface.text.source.ISharedTextColors; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IStorageEditorInput; import org.eclipse.ui.IURIEditorInput; import org.eclipse.ui.editors.text.TextEditor; import org.eclipse.ui.texteditor.AbstractTextEditor; import org.eclipse.ui.texteditor.AnnotationPreference; import org.eclipse.ui.texteditor.IDocumentProvider; import org.python.pydev.overview_ruler.MinimapOverviewRuler; import org.python.pydev.overview_ruler.MinimapOverviewRulerPreferencesPage; import org.python.pydev.shared_core.editor.IBaseEditor; import org.python.pydev.shared_core.log.Log; import org.python.pydev.shared_core.model.ErrorDescription; import org.python.pydev.shared_core.model.IModelListener; import org.python.pydev.shared_core.model.ISimpleNode; import org.python.pydev.shared_core.parsing.BaseParserManager; import org.python.pydev.shared_core.parsing.IScopesParser; import org.python.pydev.shared_core.string.ICharacterPairMatcher2; import org.python.pydev.shared_core.string.TextSelectionUtils; import org.python.pydev.shared_core.structure.OrderedSet; import org.python.pydev.shared_core.utils.Reflection; import org.python.pydev.shared_ui.outline.IOutlineModel; public abstract class BaseEditor extends TextEditor implements IBaseEditor { /** * Those are the ones that register at runtime (not through extensions points). */ protected final Collection<IPyEditListener> registeredEditListeners = new OrderedSet<IPyEditListener>(); /** * Lock for initialization sync */ private final Object initFinishedLock = new Object(); /** * Indicates whether the init was already finished */ protected boolean initFinished = false; protected final PyEditNotifier notifier; public BaseEditor() { super(); notifier = new PyEditNotifier(this); try { //Applying the fix from https://bugs.eclipse.org/bugs/show_bug.cgi?id=368354#c18 in PyDev Field field = AbstractTextEditor.class.getDeclaredField("fSelectionChangedListener"); field.setAccessible(true); field.set(this, new ISelectionChangedListener() { private Runnable fRunnable = new Runnable() { @Override public void run() { ISourceViewer sourceViewer = BaseEditor.this.getSourceViewer(); // check whether editor has not been disposed yet if (sourceViewer != null && sourceViewer.getDocument() != null) { updateSelectionDependentActions(); } } }; private Display fDisplay; @Override public void selectionChanged(SelectionChangedEvent event) { Display current = Display.getCurrent(); if (current != null) { // Don't execute asynchronously if we're in a thread that has a display. // Fix for: https://bugs.eclipse.org/bugs/show_bug.cgi?id=368354 (the rationale // is that the actions were not being enabled because they were previously // updated in an async call). // but just patching getSelectionChangedListener() properly. fRunnable.run(); } else { if (fDisplay == null) { fDisplay = getSite().getShell().getDisplay(); } fDisplay.asyncExec(fRunnable); } handleCursorPositionChanged(); } }); } catch (Exception e) { Log.log(e); } } public void addPyeditListener(IPyEditListener listener) { synchronized (registeredEditListeners) { registeredEditListeners.add(listener); } } public void removePyeditListener(IPyEditListener listener) { synchronized (registeredEditListeners) { registeredEditListeners.remove(listener); } } public List<IPyEditListener> getAllListeners() { return getAllListeners(true); } public List<IPyEditListener> getAllListeners(boolean waitInit) { if (waitInit) { while (initFinished == false) { synchronized (getInitFinishedLock()) { try { if (initFinished == false) { getInitFinishedLock().wait(); } } catch (Exception e) { //ignore Log.log(e); } } } } ArrayList<IPyEditListener> listeners = new ArrayList<IPyEditListener>(); List<IPyEditListener> editListeners = getAdditionalEditorListeners(); if (editListeners != null) { listeners.addAll(editListeners); //no need to sync because editListeners is read-only } synchronized (registeredEditListeners) { listeners.addAll(registeredEditListeners); } return listeners; } protected List<IPyEditListener> getAdditionalEditorListeners() { return null; } protected Object getInitFinishedLock() { return initFinishedLock; } /** * Subclasses MUST call this method when the #init finishes. */ protected void markInitFinished() { initFinished = true; synchronized (getInitFinishedLock()) { getInitFinishedLock().notifyAll(); } } /** * implementation copied from org.eclipse.ui.externaltools.internal.ant.editor.PlantyEditor#setSelection */ public void setSelection(int offset, int length) { ISourceViewer sourceViewer = getSourceViewer(); sourceViewer.setSelectedRange(offset, length); sourceViewer.revealRange(offset, length); } public ISourceViewer getEditorSourceViewer() { return getSourceViewer(); } public IAnnotationModel getAnnotationModel() { final IDocumentProvider documentProvider = getDocumentProvider(); if (documentProvider == null) { return null; } return documentProvider.getAnnotationModel(getEditorInput()); } /** * This map may be used by clients to store info regarding this editor. * * Clients should be careful so that this key is unique and does not conflict with other * plugins. * * This is not enforced. * * The suggestion is that the cache key is always preceded by the class name that will use it. */ public Map<String, Object> cache = new HashMap<String, Object>(); @Override public Map<String, Object> getCache() { return cache; } public abstract void revealModelNodes(ISimpleNode[] node); /** * @return true if the editor passed as a parameter has the same input as this editor. */ @Override public boolean hasSameInput(IBaseEditor edit) { IEditorInput thisInput = this.getEditorInput(); IEditorInput otherInput = (IEditorInput) edit.getEditorInput(); if (thisInput == null || otherInput == null) { return false; } if (thisInput == otherInput || thisInput.equals(otherInput)) { return true; } IResource r1 = (IResource) thisInput.getAdapter(IResource.class); IResource r2 = (IResource) otherInput.getAdapter(IResource.class); if (r1 == null || r2 == null) { return false; } if (r1.equals(r2)) { return true; } return false; } @Override protected void doSetInput(IEditorInput input) throws CoreException { super.doSetInput(input); } @Override protected void performSave(boolean overwrite, IProgressMonitor progressMonitor) { super.performSave(overwrite, progressMonitor); try { getParserManager().notifySaved(this); notifier.notifyOnSave(); } catch (Throwable e) { //can never fail Log.log(e); } } @Override protected void createNavigationActions() { super.createNavigationActions(); BaseEditorCursorListener cursorListener = new BaseEditorCursorListener(this); //add a cursor listener StyledText textWidget = getSourceViewer().getTextWidget(); textWidget.addMouseListener(cursorListener); textWidget.addKeyListener(cursorListener); } /** * @return the document that is binded to this editor (may be null) */ @Override public IDocument getDocument() { IDocumentProvider documentProvider = getDocumentProvider(); if (documentProvider != null) { return documentProvider.getDocument(getEditorInput()); } return null; } /** * @return the project for the file that's being edited (or null if not available) */ public IProject getProject() { IEditorInput editorInput = this.getEditorInput(); if (editorInput instanceof IAdaptable) { IAdaptable adaptable = editorInput; IFile file = (IFile) adaptable.getAdapter(IFile.class); if (file != null) { return file.getProject(); } IResource resource = (IResource) adaptable.getAdapter(IResource.class); if (resource != null) { return resource.getProject(); } if (editorInput instanceof IStorageEditorInput) { IStorageEditorInput iStorageEditorInput = (IStorageEditorInput) editorInput; try { IStorage storage = iStorageEditorInput.getStorage(); IPath fullPath = storage.getFullPath(); if (fullPath != null) { IWorkspace ws = ResourcesPlugin.getWorkspace(); for (String s : fullPath.segments()) { IProject p = ws.getRoot().getProject(s); if (p.exists()) { return p; } } } } catch (Exception e) { Log.log(e); } } } return null; } /** * @return the IFile being edited in this input (or null if not available) */ public IFile getIFile() { try { IEditorInput editorInput = this.getEditorInput(); return (IFile) editorInput.getAdapter(IFile.class); } catch (Exception e) { Log.log(e); //Shouldn't really happen, but if it does, let's not fail! return null; } } /** * @return the File being edited */ public File getEditorFile() { File f = null; IEditorInput editorInput = this.getEditorInput(); IFile file = (IFile) editorInput.getAdapter(IFile.class); if (file != null) { IPath location = file.getLocation(); if (location != null) { IPath path = location.makeAbsolute(); f = path.toFile(); } } else { try { if (editorInput instanceof IURIEditorInput) { IURIEditorInput iuriEditorInput = (IURIEditorInput) editorInput; return new File(iuriEditorInput.getURI()); } } catch (Throwable e) { //OK, IURIEditorInput was only added on eclipse 3.3 } try { IPath path = (IPath) Reflection.invoke(editorInput, "getPath", new Object[0]); f = path.toFile(); } catch (Throwable e) { //ok, it has no getPath } } return f; } protected abstract BaseParserManager getParserManager(); /** listeners that get notified of model changes */ protected final List<IModelListener> modelListeners = new ArrayList<IModelListener>(); /** stock listener implementation */ @Override public void addModelListener(IModelListener listener) { Assert.isNotNull(listener); if (!modelListeners.contains(listener)) { modelListeners.add(listener); } } /** stock listener implementation */ @Override public void removeModelListener(IModelListener listener) { Assert.isNotNull(listener); modelListeners.remove(listener); } /** * stock listener implementation event is fired whenever we get a new root */ protected void fireModelChanged(ISimpleNode root) { //create a copy, to avoid concurrent modifications for (IModelListener listener : new ArrayList<IModelListener>(modelListeners)) { try { listener.modelChanged(root); } catch (Exception e) { Log.log(e); } } } /** * stock listener implementation event is fired whenever the errors change in the editor */ protected void fireParseErrorChanged(ErrorDescription errorDesc) { for (IModelListener listener : new ArrayList<IModelListener>(modelListeners)) { listener.errorChanged(errorDesc); } } public abstract TextSelectionUtils createTextSelectionUtils(); /** * Notifies clients about a change in the cursor position. */ public void notifyCursorPositionChanged() { if (!this.initFinished) { return; } TextSelectionUtils ps = createTextSelectionUtils(); for (IPyEditListener listener : this.getAllListeners()) { try { if (listener instanceof IPyEditListener2) { ((IPyEditListener2) listener).handleCursorPositionChanged(this, ps); } } catch (Throwable e) { //must not fail Log.log(e); } } } public abstract ICharacterPairMatcher2 getPairMatcher(); public abstract IScopesParser createScopesParser(); @Override protected IOverviewRuler createOverviewRuler(ISharedTextColors sharedColors) { // Note: create the minimap overview ruler regardless of whether it should be shown or not // (the setting to show it will control what's drawn). if (MinimapOverviewRulerPreferencesPage.useMinimap()) { IOutlineModel outlineModel = (IOutlineModel) this.getAdapter(IOutlineModel.class); IOverviewRuler ruler = new MinimapOverviewRuler(getAnnotationAccess(), sharedColors, outlineModel); Iterator e = getAnnotationPreferences().getAnnotationPreferences().iterator(); while (e.hasNext()) { AnnotationPreference preference = (AnnotationPreference) e.next(); if (preference.contributesToHeader()) { ruler.addHeaderAnnotationType(preference.getAnnotationType()); } } return ruler; } else { return super.createOverviewRuler(sharedColors); } } IOutlineModel outlineModel; @Override public Object getAdapter(Class adapter) { if (IOutlineModel.class.equals(adapter)) { if (outlineModel == null) { outlineModel = createOutlineModel(); } return outlineModel; } return super.getAdapter(adapter); } public abstract IOutlineModel createOutlineModel(); @Override public void dispose() { if (outlineModel != null) { outlineModel.dispose(); outlineModel = null; } super.dispose(); } }