/*
* $Id$
*
* Copyright (c) 2007-2008 by the TeXlipse team.
* 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
*/
package net.sourceforge.texlipse.editor;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.texlipse.TexlipsePlugin;
import net.sourceforge.texlipse.properties.TexlipseProperties;
import net.sourceforge.texlipse.texparser.LatexParserUtils;
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.ITextSelection;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.ui.texteditor.AbstractTextEditor;
/**
* This class implements a PostSelectionChangeListener which creates annotations
* to highlight
* <ul>
* <li>associated begin or end</li>
* <li>all references of a label</li>
* </ul>
* in the current document.
*
* @author Boris von Loesch
*
*/
public class TexlipseAnnotationUpdater implements ISelectionChangedListener {
private final List<Annotation> fOldAnnotations= new LinkedList<Annotation>();
private AbstractTextEditor fEditor;
private Job fUpdateJob;
private final static String ANNOTATION_TYPE = "net.sourceforge.texlipse.defAnnotation";
private boolean fEnabled;
/**
* Creates a new TexlipseAnnotationUpdater and adds itself to the TexEditor via
* <code>addPostSelectionChangedListener</code>
* @param editor The TexEditor
*/
public TexlipseAnnotationUpdater (AbstractTextEditor editor) {
//Add this listener to the current editors IPostSelectionListener (lazy update)
((IPostSelectionProvider) editor.getSelectionProvider()).addPostSelectionChangedListener(this);
fEditor = editor;
fEnabled = TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(
TexlipseProperties.TEX_EDITOR_ANNOTATATIONS);
//Add a PropertyChangeListener
TexlipsePlugin.getDefault().getPreferenceStore().addPropertyChangeListener(new
IPropertyChangeListener() {
public void propertyChange(PropertyChangeEvent event) {
String property = event.getProperty();
if (TexlipseProperties.TEX_EDITOR_ANNOTATATIONS.equals(property)) {
boolean enabled = TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(
TexlipseProperties.TEX_EDITOR_ANNOTATATIONS);
fEnabled = enabled;
}
}
});
}
public void selectionChanged(SelectionChangedEvent event) {
update((ISourceViewer) event.getSource());
}
/**
* Updates the annotations. It first checks if the current selection is
* already annotated, if not it clears all annotations and tries to detect
* if the current selection is part of a \[a-zA-Z]*ref, \label, \begin{...}
* or \end{...} string. If the last is true, it searches with regular expressions
* to find the associated part(s) and highlights them (The last uses a non UI-Job
* which do not influence the responsiveness of the editor).
*
* @param viewer
*/
private void update(ISourceViewer viewer) {
final IDocument document = viewer.getDocument();
final IAnnotationModel model = viewer.getAnnotationModel();
ISelection selection = fEditor.getSelectionProvider().getSelection();
if (testSelection(selection, model)) return;
if (fUpdateJob != null) {
fUpdateJob.cancel();
}
removeOldAnnotations(model);
if (!fEnabled) {
//Feature is turned off, but we have to delete the old annotations
return;
}
if (selection instanceof ITextSelection) {
try {
//TODO Split this and create new classes for the different annotations
final ITextSelection textSelection = (ITextSelection) selection;
final int offset = textSelection.getOffset();
final int lineNr = document.getLineOfOffset(offset);
final int lineOff = document.getLineOffset(lineNr);
final String line = document.get(lineOff, document.getLineLength(lineNr));
IRegion r = LatexParserUtils.getCommand(line, offset - lineOff);
if (r == null) return;
final String command = line.substring(r.getOffset(), r.getOffset() + r.getLength()).trim();
if ("\\begin".equals(command) || "\\end".equals(command)) {
//TODO Its maybe better/faster to use the AST here
IRegion r2 = LatexParserUtils.getCommandArgument(line, r.getOffset());
if (r2 == null) return;
final IRegion startRegion = new Region(lineOff + r.getOffset(), r2.getOffset() + r2.getLength() - r.getOffset() + 1);
final String refName = line.substring(r2.getOffset(), r2.getOffset() + r2.getLength());
//Create a job to update the annotations in the background
fUpdateJob = createMatchEnvironmentJob(document, model, offset, command, startRegion, refName);
fUpdateJob.setPriority(Job.DECORATE);
fUpdateJob.setSystem(true);
fUpdateJob.schedule();
}
else if (command.endsWith("ref") || "\\label".equals(command)) {
//TODO Its maybe better/faster to use the AST here
IRegion r2 = LatexParserUtils.getCommandArgument(line, r.getOffset());
if (r2 == null) return;
final String refName = line.substring(r2.getOffset(), r2.getOffset() + r2.getLength());
//Create a job to update the annotations in the background
fUpdateJob = createMatchReferenceJob(document, model, refName);
fUpdateJob.setPriority(Job.DECORATE);
fUpdateJob.setSystem(true);
fUpdateJob.schedule();
}
} catch (BadLocationException ex) {
//Do not inform the user cause this is only a decorator
}
}
}
/**
* Creates and returns a background job which searches and highlights all \label and \*ref.
* @param document
* @param model
* @param refName The name of the reference
* @return The job
*/
private Job createMatchReferenceJob(final IDocument document, final IAnnotationModel model, final String refName) {
return
new Job("Update Annotations") {
public IStatus run(IProgressMonitor monitor) {
String text = document.get();
String refNameRegExp = refName.replaceAll("\\*", "\\\\*");
final String simpleRefRegExp = "\\\\([a-zA-Z]*ref|label)\\s*\\{" + refNameRegExp + "\\}";
Matcher m = (Pattern.compile(simpleRefRegExp)).matcher(text);
while (m.find()) {
if (monitor.isCanceled()) return Status.CANCEL_STATUS;
IRegion match = LatexParserUtils.getCommand(text, m.start());
//Test if it is a real LaTeX command
if (match != null) {
IRegion fi = new Region(m.start(), m.end()-m.start());
createNewAnnotation(fi, "References", model);
}
}
return Status.OK_STATUS;
}
};
}
/**
* Creates and returns a new background job which searches and highlights the matching \end or \begin environment.
* @param document
* @param model
* @param offset The offset of the selection (cursor)
* @param command \begin or \end
* @param startRegion A region which contains the command and the argument (e.g \begin{environment})
* @param envName The name of the environment
* @return The Job
*/
private Job createMatchEnvironmentJob(final IDocument document, final IAnnotationModel model, final int offset,
final String command, final IRegion startRegion, final String envName) {
return new Job("Update Annotations") {
public IStatus run(IProgressMonitor monitor) {
String text = document.get();
boolean forward = false;
if ("\\begin".equals(command)) forward = true;
if (forward) {
IRegion endRegion = LatexParserUtils.findMatchingEndEnvironment(text, envName, startRegion.getOffset());
if (endRegion != null) {
createNewAnnotation(endRegion, "Environment", model);
createNewAnnotation(startRegion, "Environment", model);
}
} else {
IRegion endRegion = LatexParserUtils.findMatchingBeginEnvironment(text, envName, startRegion.getOffset());
if (endRegion != null) {
createNewAnnotation(endRegion, "Environment", model);
createNewAnnotation(startRegion, "Environment", model);
}
}
return Status.OK_STATUS;
}
};
}
/**
* Tests if the selection is already annotated
* @param selection current selection
* @param model The AnnotationModel
* @return true, if selection is already annotated
*/
private boolean testSelection (ISelection selection, IAnnotationModel model) {
if (selection instanceof ITextSelection) {
final ITextSelection textSelection = (ITextSelection) selection;
//Iterate over all existing annotations
for (Iterator<Annotation> iter = fOldAnnotations.iterator(); iter.hasNext();) {
Annotation anno = iter.next();
Position p = model.getPosition(anno);
if (p != null && p.offset <= textSelection.getOffset() && p.offset+p.length >= textSelection.getOffset()) {
return true;
}
}
}
return false;
}
/**
* Removes all existing annotations
* @param model AnnotationModel
*/
private void removeOldAnnotations(IAnnotationModel model) {
for (Iterator<Annotation> it= fOldAnnotations.iterator(); it.hasNext();) {
Annotation annotation= (Annotation) it.next();
model.removeAnnotation(annotation);
}
fOldAnnotations.clear();
}
/**
* Creates a new annotation
* @param r The IRegion which should be highlighted
* @param annString The name of the annotation (not important)
* @param model The AnnotationModel
*/
private void createNewAnnotation(IRegion r, String annString, IAnnotationModel model) {
Annotation annotation= new Annotation(ANNOTATION_TYPE, false, annString);
Position position= new Position(r.getOffset(), r.getLength());
model.addAnnotation(annotation, position);
fOldAnnotations.add(annotation);
}
}