/******************************************************************************* * Copyright (c) 2007 IBM Corporation. * 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: * Robert Fuhrer (rfuhrer@watson.ibm.com) - initial API and implementation *******************************************************************************/ package org.eclipse.imp.editor.internal; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Stack; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.imp.core.ErrorHandler; import org.eclipse.imp.editor.LanguageServiceManager; import org.eclipse.imp.parser.IModelListener; import org.eclipse.imp.parser.IParseController; import org.eclipse.imp.parser.ISourcePositionLocator; import org.eclipse.imp.preferences.PreferenceCache; import org.eclipse.imp.runtime.RuntimePlugin; import org.eclipse.imp.services.ITokenColorer; import org.eclipse.imp.utils.ConsoleUtil; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TextAttribute; import org.eclipse.jface.text.TextPresentation; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.widgets.Display; /** * A class that does the real work of repairing the text presentation for an associated ISourceViewer. * Calls to damage(IRegion) simply accumulate damaged regions into a work queue, which is processed * at the subsequent call to update(IParseController, IProgressMonitor). * @author Claffra * @author rfuhrer@watson.ibm.com */ public class PresentationController implements IModelListener { public static final String CONSOLE_NAME= "Source Tokens"; private final ISourceViewer fSourceViewer; private final ITokenColorer fColorer; private final IParseController fParseCtlr; private final Stack<IRegion> fWorkItems= new Stack<IRegion>(); public PresentationController(ISourceViewer sourceViewer, LanguageServiceManager langServiceMgr) { fSourceViewer= sourceViewer; this.fParseCtlr= langServiceMgr.getParseController(); fColorer= langServiceMgr.getTokenColorer(); } public AnalysisRequired getAnalysisRequired() { return AnalysisRequired.LEXICAL_ANALYSIS; } private void dumpToken(Object token, ISourcePositionLocator locator, PrintStream ps) { if (locator != null) { try { final IDocument document= fSourceViewer.getDocument(); final int startOffset= locator.getStartOffset(token); // ps.print( " (" + prs.getKind(i) + ")"); ps.print(" \t" + startOffset); ps.print(" \t" + locator.getLength(token)); int line = document.getLineOfOffset(startOffset); ps.print(" \t" + line); ps.print(" \t" + (startOffset - document.getLineOffset(line))); } catch (BadLocationException e) { RuntimePlugin.getInstance().logException("Error computing position of token", e); } } ps.print(" \t" + token); ps.println(); } private void dumpTokens(Iterator<Object> tokenIter, PrintStream ps) { ISourcePositionLocator locator = fParseCtlr.getSourcePositionLocator(); if (locator != null) { ps.println(" Offset \tLen \tLine \tCol \tText"); } else { ps.println(" Text"); } for(; tokenIter.hasNext(); ) { dumpToken(tokenIter.next(), locator, ps); } } /** * Push the damaged area onto the work queue for repair when we get scheduled to process the queue. * @param region the damaged area */ public void damage(IRegion region) { if (fColorer == null) return; IRegion bigRegion= fColorer.calculateDamageExtent(region, fParseCtlr); if (bigRegion != null) { synchronized (fWorkItems) { boolean redundant= false; for(IRegion dr : fWorkItems) { if (contains(bigRegion, dr)) { redundant= true; } } if (!redundant) { // System.out.println("damage(): got irredundant damage region: " + bigRegion.getOffset() + ":" + bigRegion.getLength()); fWorkItems.push(bigRegion); } } } } private boolean contains(IRegion r1, IRegion r2) { return r2.getOffset() <= r1.getOffset() && r2.getOffset() + r2.getLength() >= r1.getOffset() + r1.getLength(); } public void update(IParseController controller, IProgressMonitor monitor) { // try { // throw new Exception(); // } catch (Exception e) { // System.out.println("Entered PresentationController.update()"); // e.printStackTrace(System.out); // } if (!monitor.isCanceled()) { // if (fWorkItems.size() == 0) { // ConsoleUtil.findConsoleStream(PresentationController.CONSOLE_NAME).println("PresentationController.update() called, but no damage in the work queue?"); // } synchronized (fWorkItems) { if (fWorkItems.size() == 0) { // TODO Shouldn't need to re-color the entire source file here. // This is intended to handle the case that the parser finishes *after* // the PresentationRepairer asks for an update(). // We could do a more focused update, if we knew what part of the file had changed. // System.out.println("PresentationController.update() called, but no work items; reprocessing entire document"); fWorkItems.add(new Region(0, fSourceViewer.getDocument().getLength())); } // TODO Optimization: when there are multiple work items, control redrawing explicitly. // See JavaDoc regarding ITextViewer.changeTextPresentation()'s 2nd argument. // Probably not very common (only refactoring or search/replace?), but perhaps worthwhile. for(int n= fWorkItems.size() - 1; !monitor.isCanceled() && n >= 0; n--) { Region damage= (Region) fWorkItems.get(n); // System.out.println(">>> Processing damage region: " + damage.getOffset() + ":" + damage.getLength()); changeTextPresentationForRegion(controller, monitor, damage); } // TODO Remove the work items we actually processed, whether the monitor was canceled or not if (!monitor.isCanceled()) { fWorkItems.removeAllElements(); } } } } private void changeTextPresentationForRegion(IParseController parseController, IProgressMonitor monitor, IRegion damage) { if (parseController == null) { return; } if (PreferenceCache.dumpTokens /*RuntimePlugin.getInstance().getPreferencesService().getBooleanPreference(PreferenceConstants.P_DUMP_TOKENS)*/) { PrintStream ps= ConsoleUtil.findConsoleStream(PresentationController.CONSOLE_NAME); dumpTokens(parseController.getTokenIterator(damage), ps); } final TextPresentation presentation= new TextPresentation(); ISourcePositionLocator locator= parseController.getSourcePositionLocator(); aggregateTextPresentation(parseController, monitor, damage, presentation, locator); if (monitor.isCanceled()) { System.err.println("Ignored cancelled presentation update"); } else if (presentation.isEmpty()) { System.err.println("Ignored empty presentation update"); } else { submitTextPresentation(presentation); } } private void aggregateTextPresentation(IParseController parseController, IProgressMonitor monitor, IRegion damage, TextPresentation presentation, ISourcePositionLocator locator) { int prevOffset= -1; int prevEnd= -1; Iterator tokenIterator= parseController.getTokenIterator(damage); if (tokenIterator == null) { return; } for(Iterator<Object> iter= tokenIterator; iter.hasNext() && !monitor.isCanceled(); ) { Object token= iter.next(); int offset= locator.getStartOffset(token); int end= locator.getEndOffset(token); if (offset <= prevEnd && end >= prevOffset) { continue; } changeTokenPresentation(parseController, presentation, token, locator); prevOffset= offset; prevEnd= end; } } private void changeTokenPresentation(IParseController controller, TextPresentation presentation, Object token, ISourcePositionLocator locator) { TextAttribute attribute= fColorer.getColoring(controller, token); StyleRange styleRange= new StyleRange(locator.getStartOffset(token), locator.getEndOffset(token) - locator.getStartOffset(token) + 1, attribute == null ? null : attribute.getForeground(), attribute == null ? null : attribute.getBackground(), attribute == null ? SWT.NORMAL : attribute.getStyle()); // Negative (possibly 0) length style ranges will cause an // IllegalArgumentException in changeTextPresentation(..) if (styleRange.length <= 0 || styleRange.start + styleRange.length > fSourceViewer.getDocument().getLength()) { // System.err.println("Omitting token '" + token + "' w/ empty style range: " + styleRange.start + ":" + styleRange.length); } else { presentation.addStyleRange(styleRange); } } private void submitTextPresentation(final TextPresentation presentation) { if (fSourceViewer == null) { return; } final int docLength= (fSourceViewer.getDocument() != null) ? fSourceViewer.getDocument().getLength() : 0; final TextPresentation newPresentation= fixPresentation(presentation, docLength, false /*sort?*/); Display.getDefault().asyncExec(new Runnable() { public void run() { try { if (fSourceViewer != null) { // The document might have changed since the presentation was computed, so // trim the presentation's "result window" to the current document's extent. // This avoids upsetting SWT, but there's still a question as to whether // this is really the right thing to do. I.e., this assumes that the // presentation will get recomputed later on, when the new document change // gets noticed. But will it? int newDocLength= (fSourceViewer.getDocument() != null) ? fSourceViewer.getDocument().getLength() : 0; IRegion presExtent= newPresentation.getExtent(); if (presExtent.getOffset() + presExtent.getLength() > newDocLength) { // System.out.println("Trimming result window..."); newPresentation.setResultWindow(new Region(presExtent.getOffset(), newDocLength - presExtent.getOffset())); } fSourceViewer.changeTextPresentation(newPresentation, true); } } catch (IllegalArgumentException e) { int curDocLength= (fSourceViewer.getDocument() != null) ? fSourceViewer.getDocument().getLength() : 0; diagnoseStyleRangeError(presentation, curDocLength, e); } } }); } /** * Adjusts the StyleRanges in the given presentation as necessary to ones that * should be acceptable to ITextViewer.changeTextPresentation(). * In particular, no range will extend beyond the end of the source text, * and their lengths will all be positive. * Optionally, will also sort the ranges, and ensure that they don't overlap. */ private TextPresentation fixPresentation(final TextPresentation presentation, int docLen, boolean sort) { if (checkPresentation(presentation, docLen)) { return presentation; } int lastStart = presentation.getLastStyleRange().start; int lastLength = presentation.getLastStyleRange().length; int end = lastStart + lastLength; List<StyleRange> newRanges= new ArrayList<StyleRange>(presentation.getDenumerableRanges()); // Phase 1: Collect all ranges in a sortable data structure and trim each one // to ensure it lies within the document bounds. Iterator presIt = presentation.getAllStyleRangeIterator(); while (presIt.hasNext()) { StyleRange nextRange = (StyleRange) presIt.next(); if (nextRange.start < docLen) { if (nextRange.start + nextRange.length > docLen) { nextRange.length= docLen - nextRange.start; } newRanges.add(nextRange); } else { // discard range that lies completely outside the document } } // Phase 2: sort the ranges by their start offset Collections.sort(newRanges, new Comparator<StyleRange>() { public int compare(StyleRange o1, StyleRange o2) { return o1.start - o2.start; } }); // Phase 3: check for overlap of adjacent ranges and trim as needed StyleRange prevRange= newRanges.get(0); for(int i=1; i < newRanges.size(); i++) { StyleRange currRange= newRanges.get(i); if (currRange.start < prevRange.start + prevRange.length) { prevRange.length= currRange.start - prevRange.start; } prevRange= currRange; } // Phase 4: remove any ranges that are now empty (as a result of trimming in Phase 3) for(Iterator<StyleRange> ri= newRanges.iterator(); ri.hasNext(); ) { StyleRange r= ri.next(); if (r.length <= 0) { ri.remove(); } } // Final phase: construct new TextPresentation TextPresentation newPresentation = new TextPresentation(); for(StyleRange r: newRanges) { newPresentation.addStyleRange(r); } return newPresentation; } /** * A fail-fast checker that returns false to indicate whether any problems * necessitate a fixing pass over the StyleRanges. */ private boolean checkPresentation(TextPresentation presentation, int docLen) { Iterator<StyleRange> presIt = presentation.getAllStyleRangeIterator(); int end= -1; while (presIt.hasNext()) { StyleRange r= presIt.next(); int rangeStart= r.start; int rangeLen= r.length; if (rangeStart < end) { return false; } if (rangeLen < 1) { return false; } if (rangeStart + rangeLen > docLen) { return false; } end= Math.max(end, rangeStart+rangeLen); } return true; // ok } /** * Called when an IllegalArgumentException has occurred, presumably due to the * TextPresentation containing an inappropriate style range, or perhaps an invalid * combination of ranges (e.g., overlapping). * Try to determine the real cause of the problem, and add an appropriate message * for the exception in the plugin log. */ private void diagnoseStyleRangeError(final TextPresentation presentation, int charCount, IllegalArgumentException e) { // Possible causes (not necessarily complete): // - negative length in a style range // - overlapping ranges // - range extends beyond last character in file Iterator<StyleRange> ranges = presentation.getAllStyleRangeIterator(); List<StyleRange> rangesList = new ArrayList<StyleRange>(); while (ranges.hasNext()) { rangesList.add((StyleRange) ranges.next()); } StringBuilder explanation = new StringBuilder(); if (rangesList.size() > 0) { StyleRange firstRange = rangesList.get(0); if (firstRange.length < 0) { explanation.append("Style range with start = " + firstRange.start + " has negative length = " + firstRange.length); } StyleRange prevRange= firstRange; for (int i = 1; i < rangesList.size(); i++) { StyleRange currRange= rangesList.get(i); int currStart = currRange.start; int currLength = currRange.length; if (currLength < 0) { explanation.append("Style range with start = " + currStart + " has negative length = " + currLength); break; } int prevStart = prevRange.start; int prevLength = prevRange.length; if (prevStart + prevLength - 1 >= currStart) { explanation.append("Style range with start = " + prevStart + " and length = " + prevLength + " overlaps style range with start = " + currStart); break; } prevRange= currRange; } int finalStart = presentation.getLastStyleRange().start; int finalLength = presentation.getLastStyleRange().length; int finalEnd = finalStart + finalLength; if (finalEnd >= charCount) { explanation.append("Final style range with start = " + finalStart + " and length = " + finalLength + " extends beyond last character (character count = " + charCount + ")"); } if (explanation.length() == 0) { explanation.append("Cause not identified"); } } ErrorHandler.logError("Malformed TextPresentation:" + explanation, e); } }