/******************************************************************************* * Copyright (c) 2009-2011 CWI * 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: * * Jurgen J. Vinju - Jurgen.Vinju@cwi.nl - CWI * * Paul Klint - Paul.Klint@cwi.nl - CWI * * Arnold Lankamp - Arnold.Lankamp@cwi.nl * * Davy Landman - Davy.Landman@cwi.nl - CWI *******************************************************************************/ package org.rascalmpl.eclipse.library.util; import java.io.IOException; import java.net.URI; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.preference.PreferenceConverter; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ISynchronizable; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IEditorDescriptor; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IPartListener; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.editors.text.EditorsUI; import org.eclipse.ui.ide.IDE; import org.eclipse.ui.ide.ResourceUtil; import org.eclipse.ui.part.FileEditorInput; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; import org.rascalmpl.eclipse.Activator; import org.rascalmpl.eclipse.util.RascalInvoker; import org.rascalmpl.interpreter.IEvaluator; import org.rascalmpl.interpreter.result.ICallableValue; import org.rascalmpl.interpreter.result.Result; import org.rascalmpl.eclipse.library.vis.util.FigureColorUtils; import org.rascalmpl.uri.URIEditorInput; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIStorage; import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IInteger; import io.usethesource.vallang.IList; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; import io.usethesource.vallang.type.Type; import io.usethesource.vallang.type.TypeFactory; import org.rascalmpl.values.ValueFactoryFactory; public class Editors { /* * Code is cloned from SourceLocationHyperlink (but heavily adapted) */ private abstract class DecoratorRunnerBase implements Runnable { private final ISourceLocation loc; private final IWorkbenchPage page; protected abstract IList getLineInfo(); private DecoratorRunnerBase(ISourceLocation loc, IWorkbenchPage page) { this.loc = loc; this.page = page; } private IEditorPart cachedEditorPart = null; protected IEditorPart getEditorPart() throws PartInitException { // we cache the editor part because each openEditorOnFileStore / // openEditor will reactive that editor part // to avoid looping due to events fired by a editor part activation // we want to avoid that except for the first time. if (cachedEditorPart == null) { IEditorDescriptor desc = PlatformUI.getWorkbench().getEditorRegistry().getDefaultEditor(loc.getPath()); if (desc != null) { URIResolverRegistry reg = URIResolverRegistry.getInstance(); ISourceLocation theLoc; try { theLoc = reg.logicalToPhysical(loc); } catch (IOException e) { theLoc = loc; } cachedEditorPart = page.openEditor(getEditorInput(theLoc.getURI()), desc.getId()); } else { IFileStore fileStore = EFS.getLocalFileSystem().getStore(loc.getURI()); IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); cachedEditorPart = IDE.openEditorOnFileStore(page, fileStore); } } return cachedEditorPart; } private IList previousList = null; private boolean firstTime = true; public void run() { try { IEditorPart editorPart = getEditorPart(); if (editorPart != null && editorPart instanceof ITextEditor) { ITextEditor editor = (ITextEditor) editorPart; IDocumentProvider documentProvider = editor.getDocumentProvider(); IDocument document = documentProvider.getDocument(editor.getEditorInput()); if (document != null) { IList lines = getLineInfo(); if (previousList != null && previousList.equals(lines)) { return; // nothing has changed, so we can avoid // removing and re-adding everything } else { // previousList = lines; } // First delete old markers IEditorInput input = editor.getEditorInput(); IResource inputResource = ResourceUtil.getResource(input); if (inputResource == null) { Activator.log("can not show markers because file is not in the Eclipse workspace: " + input, new NullPointerException()); return; } for (IMarker marker : inputResource.findMarkers(RASCAL_MARKER, true, IResource.DEPTH_INFINITE)) { if (marker.exists()) { marker.delete(); } } // ... and old annotations IAnnotationModel annotationModel = documentProvider.getAnnotationModel(editor.getEditorInput()); // Lock on the annotation model Object lockObject = ((ISynchronizable) annotationModel).getLockObject(); synchronized (lockObject) { @SuppressWarnings("unchecked") Iterator<Annotation> iter = annotationModel.getAnnotationIterator(); while (iter.hasNext()) { Annotation anno = iter.next(); if (anno.getType().startsWith("rascal.highlight")) { annotationModel.removeAnnotation(anno); } } } for (IValue v : lines) { IConstructor lineDecor = (IConstructor) v; int lineNumber = ((IInteger) lineDecor.get(0)).intValue(); String msg = ((IString) lineDecor.get(1)).getValue(); int severity = 0; boolean useMarker = true; String name = lineDecor.getName(); if (name.equals("info")) { severity = IMarker.SEVERITY_INFO; } else if (name.equals("warning")) { severity = IMarker.SEVERITY_WARNING; } else if (name.equals("error")) { severity = IMarker.SEVERITY_ERROR; } else { useMarker = false; } if (useMarker) { // Add a marker IMarker marker = inputResource.createMarker(RASCAL_MARKER); marker.setAttribute(IMarker.LINE_NUMBER, lineNumber); marker.setAttribute(IMarker.MESSAGE, msg); marker.setAttribute(IMarker.LOCATION, "line " + lineNumber); marker.setAttribute(IMarker.SEVERITY, severity); } else { // Add an annotation int highlightKind = 0; if (lineDecor.arity() > 2) { highlightKind = ((IInteger) lineDecor.get(2)).intValue(); if (highlightKind < 0) { highlightKind = 0; } if (highlightKind >= LINE_HIGHLIGHT_LENGTH) { highlightKind = LINE_HIGHLIGHT_LENGTH - 1; } } String highlightName = RASCAL_LINE_HIGHLIGHT[highlightKind]; try { // line count internally starts with 0, and // not with 1 like in GUI IRegion lineInfo = document.getLineInformation(lineNumber - 1); synchronized (lockObject) { Annotation currentLine = new Annotation(highlightName, true, msg); Position currentPosition = new Position(lineInfo.getOffset(), lineInfo.getLength()); annotationModel.addAnnotation(currentLine, currentPosition); } } catch (org.eclipse.jface.text.BadLocationException e) { // Ignore, lineNumber may just not exist in file } } } if (firstTime) { firstTime = false; if (loc.hasOffsetLength() && editorPart instanceof ITextEditor) { ((ITextEditor)editorPart).selectAndReveal(loc.getOffset(), loc.getLength()); } } } } } catch (PartInitException e) { Activator.getInstance().logException("failed to open editor for source loc:" + loc, e); } catch (CoreException e) { e.printStackTrace(); } } private IEditorInput getEditorInput(URI uri) { String scheme = uri.getScheme(); if (scheme.equals("project")) { IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(uri.getAuthority()); if (project != null) { return new FileEditorInput(project.getFile(uri.getPath())); } Activator.getInstance().logException("project " + uri.getAuthority() + " does not exist", new RuntimeException()); } else if (scheme.equals("file")) { IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IFile[] cs = root.findFilesForLocationURI(uri); if (cs != null && cs.length > 0) { return new FileEditorInput(cs[0]); } } URIStorage storage = new URIStorage(VF.sourceLocation(uri)); return new URIEditorInput(storage); } } private class OneTimeDecoratorRunner extends DecoratorRunnerBase { private final IList lineInfo; public OneTimeDecoratorRunner(ISourceLocation loc, IList lineInfo, IWorkbenchPage page) { super(loc, page); this.lineInfo = lineInfo; } protected IList getLineInfo() { return lineInfo; } } private class RepeatableDecoratorRunner extends DecoratorRunnerBase { private final ICallableValue fun; public RepeatableDecoratorRunner(ISourceLocation loc, ICallableValue fun, IWorkbenchPage page) { super(loc, page); if (!Thread.currentThread().equals(Display.getDefault().getThread())) { throw new RuntimeException("Initialization should be run from UI thread!"); } this.fun = fun; IEditorPart editorPart = null; try { editorPart = getEditorPart(); } catch (PartInitException e) { throw new RuntimeException("Init failure with part?", e); } addRunnableToAnnotationListner(editorPart); } protected IList getLineInfo() { if (Thread.currentThread().equals(Display.getDefault().getThread())) { throw new RuntimeException("The repeatable runner should not be run from inside the UI thread"); } fun.getEval().__setInterrupt(false); return (IList) fun.call(new Type[0], new IValue[0], null).getValue(); } private void addRunnableToAnnotationListner(IEditorPart editorPart) { annotationRunners.put(editorPart, this); annotationRunnerEvaluators.put(editorPart, fun.getEval()); String fileName = editorPart.getEditorInput().getName(); int dotPosition = fileName.lastIndexOf('.'); if (dotPosition != -1) { String extension = fileName.substring(dotPosition).toLowerCase(); Set<IWorkbenchPart> extensionList = partsProvided.get(extension); if (extensionList != null) { extensionList.add(editorPart); } } } } IValueFactory values; public Editors(IValueFactory values) { this.values = values; } /* * Local declarations for annotations and markers */ private final int LINE_HIGHLIGHT_LENGTH = 5; // This following list of annotations has to be in sync with the annotations // declared in plugin.xml private final static String[] RASCAL_LINE_HIGHLIGHT = { "rascal.highlight0", "rascal.highlight1", "rascal.highlight2", "rascal.highlight3", "rascal.highlight4" }; private final static String RASCAL_MARKER = "rascal.markerType.queryResult"; /** * Define a list of custom colors for line highlights in the editor * * @param colors * The list of colors */ public void setHighlightColors(final IList colors) { FigureColorUtils.setHighlightColors(colors); IPreferenceStore prefStore = EditorsUI.getPreferenceStore(); for (int i = 0; i < colors.length() && i < LINE_HIGHLIGHT_LENGTH; i++) { int color = ((IInteger) colors.get(i)).intValue(); String prefKey = "Highlight" + i + "ColorPreferenceKey"; PreferenceConverter.setValue(prefStore, prefKey, FigureColorUtils.toRGB(color)); } } /** * Define a list of custom colors for line highlights in the editor * * @param colors * The list of colors */ public void setErrorColors(final IList colors) { FigureColorUtils.setErrorColors(colors); IPreferenceStore prefStore = EditorsUI.getPreferenceStore(); for (int i = 0; i < colors.length() && i < LINE_HIGHLIGHT_LENGTH; i++) { int color = ((IInteger) colors.get(i)).intValue(); // TODO: is this right? String prefKey = "Error" + i + "ColorPreferenceKey"; PreferenceConverter.setValue(prefStore, prefKey, FigureColorUtils.toRGB(color)); } } /** * Start an editor for a given source location * * @param loc * The source location to be edited * @param lineInfo * A list of LineDecorations (see Render.rsc) * * Code is cloned from SourceLocationHyperlink (but heavily * adapted) */ public void edit(ISourceLocation loc, final IList lineInfo) { IWorkbenchWindow win = getWorkbenchWindow(); if (win != null) { IWorkbenchPage page = win.getActivePage(); if (page != null) { Display.getDefault().asyncExec(new OneTimeDecoratorRunner(loc, lineInfo, page)); } } } /** * Start an editor for a given source location * * @param loc * The source location to be edited * @param lineInfo * A list of LineDecorations (see Render.rsc) or a Computed list * of LineDecorations * * Code is cloned from SourceLocationHyperlink (but heavily * adapted) */ public void edit(final ISourceLocation loc, final IValue lineInfo) { if (lineInfo instanceof ICallableValue) { final ICallableValue lineInfoFunc = (ICallableValue) lineInfo; IWorkbenchWindow win = getWorkbenchWindow(); if (win != null) { final IWorkbenchPage page = win.getActivePage(); if (page != null) { page.addPartListener(annotationListener); // The Decorator was to be constructed on the UI thread due to initialisation // afterwards we can run it outside the UI thread (and we should) Display.getDefault().asyncExec(new Runnable() { @Override public void run() { // and only then schedule it on a non UI thread for the rascal callbacks RascalInvoker.invokeAsync(new RepeatableDecoratorRunner(loc, lineInfoFunc, page), lineInfoFunc.getEval()); } }); } } } } private IWorkbenchWindow getWorkbenchWindow() { IWorkbench wb = PlatformUI.getWorkbench(); IWorkbenchWindow win = wb.getActiveWorkbenchWindow(); if (win == null && wb.getWorkbenchWindowCount() != 0) { win = wb.getWorkbenchWindows()[0]; } return win; } public void provideDefaultLineDecorations(IString extension, IValue handleNewFile) { if (handleNewFile instanceof ICallableValue) { IWorkbenchWindow win = getWorkbenchWindow(); if (win != null) { IWorkbenchPage page = win.getActivePage(); page.addPartListener(annotationListener); if (partsProvided.containsKey(extension.getValue())) { // okay, we have to remove the old provided LineDecorators for (IWorkbenchPart part : partsProvided.get(extension.getValue())) { annotationRunners.remove(part); annotationRunnerEvaluators.remove(part); } } partsProvided.put(extension.getValue(), new HashSet<IWorkbenchPart>()); defaultProviders.put(extension.getValue(), (ICallableValue) handleNewFile); } } } /* * This is the storage of WorkbenchPart and their associated annotation * runners. */ private final static Map<IWorkbenchPart, Runnable> annotationRunners = new ConcurrentHashMap<IWorkbenchPart, Runnable>(); private final static Map<IWorkbenchPart, IEvaluator<Result<IValue>>> annotationRunnerEvaluators = new ConcurrentHashMap<IWorkbenchPart, IEvaluator<Result<IValue>>>(); private final static Map<String, Set<IWorkbenchPart>> partsProvided = new ConcurrentHashMap<String, Set<IWorkbenchPart>>(); private final static Map<String, ICallableValue> defaultProviders = new ConcurrentHashMap<String, ICallableValue>(); private final static TypeFactory TF = TypeFactory.getInstance(); private final static IValueFactory VF = ValueFactoryFactory .getValueFactory(); private final static IPartListener annotationListener = new IPartListener() { public void partActivated(IWorkbenchPart part) { Runnable runAgain = annotationRunners.get(part); if (runAgain != null) { IEvaluator<Result<IValue>> eval = annotationRunnerEvaluators.get(part); RascalInvoker.invokeAsync(runAgain, eval); } else if (part instanceof ITextEditor) { // only try to run the callback if there wasn't another runner associated IEditorInput editorInput = part.getSite().getPage().getActiveEditor().getEditorInput(); String fileName = editorInput.getName(); int dotPosition = fileName.lastIndexOf('.'); if (dotPosition != -1) { String extension = fileName.substring(dotPosition).toLowerCase(); final ICallableValue defaultProvider = defaultProviders.get(extension); if (defaultProvider != null) { partsProvided.get(extension).add(part); final ISourceLocation fileLoc = new Resources(VF).makeFile(editorInput); Thread callBackThread = new Thread(new Runnable() { public void run() { Result<IValue> result; synchronized (defaultProvider.getEval()) { defaultProvider.getEval().__setInterrupt(false); result = defaultProvider.call(new Type[] { TF.sourceLocationType() }, new IValue[] { fileLoc }, null); } new Editors(VF).edit(fileLoc, result.getValue()); } }); // execute the callback on a separate thread to avoid slowing down the page switches callBackThread.start(); } } } } public void partClosed(IWorkbenchPart part) { annotationRunners.remove(part); annotationRunnerEvaluators.remove(part); for (String ext : partsProvided.keySet()) { partsProvided.get(ext).remove(part); } } public void partBroughtToTop(IWorkbenchPart part) { } public void partDeactivated(IWorkbenchPart part) { } public void partOpened(IWorkbenchPart part) { } }; }