/*******************************************************************************
* Copyright (c) 2006, 2008 Wind River Systems, Inc. and others.
* 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:
* Anton Leherbauer (Wind River Systems) - initial API and implementation
* Markus Schorn (Wind River Systems)
*******************************************************************************/
package org.eclipse.cdt.internal.ui.editor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EmptyStackException;
import java.util.List;
import java.util.Stack;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.TypedPosition;
import org.eclipse.swt.widgets.Display;
import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorElifStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorElseStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorEndifStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIfStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIfdefStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorIfndefStatement;
import org.eclipse.cdt.core.dom.ast.IASTPreprocessorStatement;
import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ILanguage;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.internal.core.model.ASTCache;
import org.eclipse.cdt.internal.ui.LineBackgroundPainter;
import org.eclipse.cdt.internal.ui.text.ICReconcilingListener;
/**
* Paints code lines disabled by preprocessor directives (#ifdef etc.)
* with a configurable background color (default light gray).
*
* @see LineBackgroundPainter
* @since 4.0
*/
public class InactiveCodeHighlighting implements ICReconcilingListener, ITextInputListener {
/**
* Implementation of <code>IRegion</code> that can be reused
* by setting the offset and the length.
*/
private static class HighlightPosition extends TypedPosition implements IRegion {
public HighlightPosition(int offset, int length, String type) {
super(offset, length, type);
}
public HighlightPosition(IRegion region, String type) {
super(region.getOffset(), region.getLength(), type);
}
}
/** The line background painter */
private LineBackgroundPainter fLineBackgroundPainter;
/** The key for inactive code positions in the background painter */
private String fHighlightKey;
/** The current translation unit */
private ITranslationUnit fTranslationUnit;
/** The background job doing the AST parsing */
private Job fUpdateJob;
/** The lock for job manipulation */
private Object fJobLock = new Object();
/** The editor this is installed on */
private CEditor fEditor;
/** The list of currently highlighted positions */
private List<Position> fInactiveCodePositions= Collections.emptyList();
private IDocument fDocument;
/**
* Create a highlighter for the given key.
* @param highlightKey
*/
public InactiveCodeHighlighting(String highlightKey) {
fHighlightKey= highlightKey;
}
/**
* Schedule update of the inactive code positions in the background.
*/
private void scheduleJob() {
synchronized (fJobLock) {
if (fUpdateJob == null) {
fUpdateJob = new Job(CEditorMessages.InactiveCodeHighlighting_job) {
@Override
protected IStatus run(final IProgressMonitor monitor) {
IStatus result = Status.OK_STATUS;
if (fTranslationUnit != null) {
final ASTProvider astProvider= CUIPlugin.getDefault().getASTProvider();
result= astProvider.runOnAST(fTranslationUnit, ASTProvider.WAIT_IF_OPEN, monitor, new ASTCache.ASTRunnable() {
public IStatus runOnAST(ILanguage lang, IASTTranslationUnit ast) {
reconciled(ast, true, monitor);
return Status.OK_STATUS;
}
});
}
if (monitor.isCanceled()) {
result = Status.CANCEL_STATUS;
}
return result;
}
};
fUpdateJob.setPriority(Job.DECORATE);
}
if (fUpdateJob.getState() == Job.NONE) {
fUpdateJob.schedule();
}
}
}
/**
* Install this highlighting on the given editor and line background painter.
*
* @param editor
* @param lineBackgroundPainter
*/
public void install(CEditor editor, LineBackgroundPainter lineBackgroundPainter) {
assert fEditor == null;
assert editor != null && lineBackgroundPainter != null;
fEditor= editor;
fLineBackgroundPainter= lineBackgroundPainter;
ICElement cElement= fEditor.getInputCElement();
if (cElement instanceof ITranslationUnit) {
fTranslationUnit = (ITranslationUnit)cElement;
} else {
fTranslationUnit = null;
}
fDocument= fEditor.getDocumentProvider().getDocument(fEditor.getEditorInput());
fEditor.getViewer().addTextInputListener(this);
fEditor.addReconcileListener(this);
}
/**
* Uninstall this highlighting from the editor. Does nothing if already uninstalled.
*/
public void uninstall() {
synchronized (fJobLock) {
if (fUpdateJob != null && fUpdateJob.getState() == Job.RUNNING) {
fUpdateJob.cancel();
}
}
if (fLineBackgroundPainter != null && !fLineBackgroundPainter.isDisposed()) {
fLineBackgroundPainter.removeHighlightPositions(fInactiveCodePositions);
fInactiveCodePositions= Collections.emptyList();
fLineBackgroundPainter= null;
}
if (fEditor != null) {
fEditor.removeReconcileListener(this);
if (fEditor.getViewer() != null) {
fEditor.getViewer().removeTextInputListener(this);
}
fEditor= null;
fTranslationUnit= null;
fDocument= null;
}
}
/**
* Force refresh.
*/
public void refresh() {
scheduleJob();
}
/*
* @see org.eclipse.cdt.internal.ui.text.ICReconcilingListener#aboutToBeReconciled()
*/
public void aboutToBeReconciled() {
}
/*
* @see org.eclipse.cdt.internal.ui.text.ICReconcilingListener#reconciled(IASTTranslationUnit, boolean, IProgressMonitor)
*/
public void reconciled(IASTTranslationUnit ast, final boolean force, IProgressMonitor progressMonitor) {
if (progressMonitor != null && progressMonitor.isCanceled()) {
return;
}
final List<Position> newInactiveCodePositions= collectInactiveCodePositions(ast);
Runnable updater = new Runnable() {
public void run() {
if (fEditor != null && fLineBackgroundPainter != null && !fLineBackgroundPainter.isDisposed()) {
fLineBackgroundPainter.replaceHighlightPositions(fInactiveCodePositions, newInactiveCodePositions);
fInactiveCodePositions= newInactiveCodePositions;
}
}
};
if (fEditor != null) {
Display.getDefault().asyncExec(updater);
}
}
/**
* Collect source positions of preprocessor-hidden branches
* in the given translation unit.
*
* @param translationUnit the {@link IASTTranslationUnit}, may be <code>null</code>
* @return a {@link List} of {@link IRegion}s
*/
private List<Position> collectInactiveCodePositions(IASTTranslationUnit translationUnit) {
if (translationUnit == null) {
return Collections.emptyList();
}
String fileName = translationUnit.getFilePath();
if (fileName == null) {
return Collections.emptyList();
}
List<Position> positions = new ArrayList<Position>();
int inactiveCodeStart = -1;
boolean inInactiveCode = false;
Stack<Boolean> inactiveCodeStack = new Stack<Boolean>();
IASTPreprocessorStatement[] preprocStmts = translationUnit.getAllPreprocessorStatements();
for (IASTPreprocessorStatement statement : preprocStmts) {
IASTFileLocation floc= statement.getFileLocation();
if (floc == null || !fileName.equals(floc.getFileName())) {
// preprocessor directive is from a different file
continue;
}
if (statement instanceof IASTPreprocessorIfStatement) {
IASTPreprocessorIfStatement ifStmt = (IASTPreprocessorIfStatement)statement;
inactiveCodeStack.push(Boolean.valueOf(inInactiveCode));
if (!ifStmt.taken()) {
if (!inInactiveCode) {
inactiveCodeStart = floc.getNodeOffset();
inInactiveCode = true;
}
}
} else if (statement instanceof IASTPreprocessorIfdefStatement) {
IASTPreprocessorIfdefStatement ifdefStmt = (IASTPreprocessorIfdefStatement)statement;
inactiveCodeStack.push(Boolean.valueOf(inInactiveCode));
if (!ifdefStmt.taken()) {
if (!inInactiveCode) {
inactiveCodeStart = floc.getNodeOffset();
inInactiveCode = true;
}
}
} else if (statement instanceof IASTPreprocessorIfndefStatement) {
IASTPreprocessorIfndefStatement ifndefStmt = (IASTPreprocessorIfndefStatement)statement;
inactiveCodeStack.push(Boolean.valueOf(inInactiveCode));
if (!ifndefStmt.taken()) {
if (!inInactiveCode) {
inactiveCodeStart = floc.getNodeOffset();
inInactiveCode = true;
}
}
} else if (statement instanceof IASTPreprocessorElseStatement) {
IASTPreprocessorElseStatement elseStmt = (IASTPreprocessorElseStatement)statement;
if (!elseStmt.taken() && !inInactiveCode) {
inactiveCodeStart = floc.getNodeOffset();
inInactiveCode = true;
} else if (elseStmt.taken() && inInactiveCode) {
int inactiveCodeEnd = floc.getNodeOffset();
positions.add(createHighlightPosition(inactiveCodeStart, inactiveCodeEnd, false, fHighlightKey));
inInactiveCode = false;
}
} else if (statement instanceof IASTPreprocessorElifStatement) {
IASTPreprocessorElifStatement elifStmt = (IASTPreprocessorElifStatement)statement;
if (!elifStmt.taken() && !inInactiveCode) {
inactiveCodeStart = floc.getNodeOffset();
inInactiveCode = true;
} else if (elifStmt.taken() && inInactiveCode) {
int inactiveCodeEnd = floc.getNodeOffset();
positions.add(createHighlightPosition(inactiveCodeStart, inactiveCodeEnd, false, fHighlightKey));
inInactiveCode = false;
}
} else if (statement instanceof IASTPreprocessorEndifStatement) {
try {
boolean wasInInactiveCode = inactiveCodeStack.pop().booleanValue();
if (inInactiveCode && !wasInInactiveCode) {
int inactiveCodeEnd = floc.getNodeOffset() + floc.getNodeLength();
positions.add(createHighlightPosition(inactiveCodeStart, inactiveCodeEnd, true, fHighlightKey));
}
inInactiveCode = wasInInactiveCode;
}
catch( EmptyStackException e) {}
}
}
if (inInactiveCode) {
// handle unterminated #if - http://bugs.eclipse.org/255018
int inactiveCodeEnd = fDocument.getLength();
positions.add(createHighlightPosition(inactiveCodeStart, inactiveCodeEnd, true, fHighlightKey));
}
return positions;
}
/**
* Create a highlight position aligned to start at a line offset. The region's start is
* decreased to the line offset, and the end offset decreased to the line start if
* <code>inclusive</code> is <code>false</code>.
*
* @param startOffset the start offset of the region to align
* @param endOffset the (exclusive) end offset of the region to align
* @param inclusive whether the last line should be included or not
* @param key the highlight key
* @return a position aligned for background highlighting
*/
private HighlightPosition createHighlightPosition(int startOffset, int endOffset, boolean inclusive, String key) {
final IDocument document= fDocument;
try {
if (document != null) {
int start= document.getLineOfOffset(startOffset);
int end= document.getLineOfOffset(endOffset);
startOffset= document.getLineOffset(start);
if (!inclusive) {
endOffset= document.getLineOffset(end);
}
}
} catch (BadLocationException x) {
// concurrent modification?
}
return new HighlightPosition(startOffset, endOffset - startOffset, key);
}
/*
* @see org.eclipse.jface.text.ITextInputListener#inputDocumentAboutToBeChanged(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.IDocument)
*/
public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
if (fEditor != null && fLineBackgroundPainter != null && !fLineBackgroundPainter.isDisposed()) {
fLineBackgroundPainter.removeHighlightPositions(fInactiveCodePositions);
fInactiveCodePositions= Collections.emptyList();
}
}
/*
* @see org.eclipse.jface.text.ITextInputListener#inputDocumentChanged(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.IDocument)
*/
public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
fDocument= newInput;
}
}