blob: 2380e39c65ea8415731f862ab47f1d6a6a88c423 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011 Obeo.
* 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:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.docs.intent.client.ui.editor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.projection.ProjectionAnnotation;
import org.eclipse.mylyn.docs.intent.client.ui.IntentEditorActivator;
import org.eclipse.mylyn.docs.intent.client.ui.editor.scanner.IntentPartitionScanner;
import org.eclipse.mylyn.docs.intent.client.ui.preferences.IntentPreferenceConstants;
import org.eclipse.swt.widgets.Display;
/**
* This reconciling strategy will allow us to enable folding support in the Intent editor.
*
* @author <a href="mailto:william.piers@obeo.fr">William Piers</a>
*/
public final class IntentReconcilingStrategy implements IReconcilingStrategy, IReconcilingStrategyExtension {
/**
* This will hold the list of all annotations that have been added since the last reconciling.
*/
private final Map<Annotation, Position> addedAnnotations = new HashMap<Annotation, Position>();
/** Current known positions of foldable block. */
private final Map<Annotation, Position> currentAnnotations = new HashMap<Annotation, Position>();
/**
* This will hold the list of all annotations that have been removed since the last reconciling.
*/
private final List<Annotation> deletedAnnotations = new ArrayList<Annotation>();
/** Editor this provides folding support to. */
private final IntentEditor editor;
/**
* This will hold the list of all annotations that have been modified since the last reconciling.
*/
private final Map<Annotation, Position> modifiedAnnotations = new HashMap<Annotation, Position>();
/** The document we'll seek foldable blocks in. */
private IDocument document;
/** Current offset. */
private int offset;
/**
* Pair matcher used to reconcile document.
*/
private IntentPairMatcher pairMatcher = new IntentPairMatcher();
/**
* Instantiates the reconciling strategy for a given editor.
*
* @param editor
* Editor which we seek to provide folding support for.
*/
public IntentReconcilingStrategy(IntentEditor editor) {
this.editor = editor;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#initialReconcile()
*/
public void initialReconcile() {
offset = 0;
computePositions();
updateFoldingStructure();
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#reconcile(org.eclipse.jface.text.reconciler.DirtyRegion,
* org.eclipse.jface.text.IRegion)
*/
public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) {
reconcile(subRegion);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#reconcile(org.eclipse.jface.text.IRegion)
*/
public void reconcile(IRegion partition) {
offset = partition.getOffset();
computePositions();
updateFoldingStructure();
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#setDocument(org.eclipse.jface.text.IDocument)
*/
public void setDocument(IDocument document) {
this.document = document;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#setProgressMonitor(org.eclipse.core.runtime.IProgressMonitor)
*/
public void setProgressMonitor(IProgressMonitor monitor) {
// none
}
/**
* This will compute the current block positions. The offset at which computations start is determined by
* {@link #offset}.
*/
private void computePositions() {
deletedAnnotations.clear();
modifiedAnnotations.clear();
addedAnnotations.clear();
deletedAnnotations.addAll(currentAnnotations.keySet());
for (Map.Entry<Annotation, Position> entry : currentAnnotations.entrySet()) {
final Position position = entry.getValue();
if (position.getOffset() + position.getLength() < offset) {
deletedAnnotations.remove(entry.getKey());
}
}
try {
boolean eof = seekBlockStart();
int startOffset = offset;
while (!eof) {
offset++;
// Case 1: Structural Content folding
if (document.getLineOfOffset(startOffset) > 0
&& document.getContentType(startOffset).equals(
IntentPartitionScanner.INTENT_STRUCTURAL_CONTENT)) {
IRegion match = pairMatcher.match(document, offset);
if (match != null) {
int endOffset = match.getOffset() + match.getLength() + 1;
if (document.getNumberOfLines(startOffset, endOffset - startOffset) > 2) {
createOrUpdateAnnotation(startOffset, endOffset - startOffset, false);
}
}
} else if (document.getLineOfOffset(startOffset) > 0
&& document.getContentType(startOffset).equals(
IntentPartitionScanner.INTENT_MODELINGUNIT)) {
// Case 2: Modeling Unit folding
// Search for modeling unit end
String documentZone = document.get().substring(startOffset);
int endOffset = documentZone.indexOf("M@") + 2;
if (endOffset > -1) {
createOrUpdateAnnotation(startOffset - 1, endOffset,
shouldCollapseModelingUnitByDefault());
}
}
eof = seekBlockStart();
startOffset = offset;
}
} catch (BadLocationException e) {
// Nothing to do
}
for (Annotation deleted : deletedAnnotations) {
currentAnnotations.remove(deleted);
}
}
/**
* Eats chars away till we find a start char.
*
* @return <code>true</code> if the end of file has been reached. <code>false</code> otherwise.
* @throws BadLocationException
* Thrown if we try and get an out of range character. Should not happen.
*/
private boolean seekBlockStart() throws BadLocationException {
char next = document.getChar(offset);
char previous = ' ';
boolean eof = offset + 1 >= document.getLength();
boolean foundModelingUnit = false;
while (!eof && next != '{' && !foundModelingUnit) {
offset++;
previous = next;
next = document.getChar(offset);
eof = offset + 1 == document.getLength();
foundModelingUnit = next == 'M' && previous == '@';
}
return eof;
}
/**
* This will update lists {@link #deletedAnnotations}, {@link #addedAnnotations} and
* {@link #modifiedAnnotations} according to the given values.
*
* @param newOffset
* Offset of the text we want the annotation updated of.
* @param newLength
* Length of the text we want the annotation updated of.
* @param initiallyCollapsed
* Indicates that the created annotation should be folded from start.
* @throws BadLocationException
* Thrown if we try and get an out of range character. Should not happen.
*/
private void createOrUpdateAnnotation(final int newOffset, final int newLength, boolean initiallyCollapsed)
throws BadLocationException {
boolean createAnnotation = true;
final Map<Annotation, Position> copy = new HashMap<Annotation, Position>(currentAnnotations);
final String text = document.get(newOffset, newLength);
for (Iterator<Entry<Annotation, Position>> iterator = copy.entrySet().iterator(); iterator.hasNext();) {
Entry<Annotation, Position> entry = iterator.next();
// added checking to avoid same text elements to bo ignored
if (entry.getKey().getText().equals(text) && entry.getValue().getOffset() == newOffset) {
createAnnotation = false;
final Position oldPosition = entry.getValue();
if (oldPosition.getOffset() != newOffset || oldPosition.getLength() != newLength) {
final Position newPosition = new Position(newOffset, newLength);
modifiedAnnotations.put(entry.getKey(), newPosition);
currentAnnotations.put(entry.getKey(), newPosition);
}
deletedAnnotations.remove(entry.getKey());
break;
}
}
if (createAnnotation) {
Annotation annotation = null;
annotation = new ProjectionAnnotation(initiallyCollapsed);
annotation.setText(text);
final Position position = new Position(newOffset, newLength);
currentAnnotations.put(annotation, position);
addedAnnotations.put(annotation, position);
}
}
/**
* Updates the editor's folding structure.
*/
private void updateFoldingStructure() {
Display.getDefault().asyncExec(new Runnable() {
public void run() {
editor.updateFoldingStructure(addedAnnotations, deletedAnnotations, modifiedAnnotations);
}
});
}
/**
* Indicates whether Modeling units should be initially collapsed in the Intent editor.
*
* @return true if Modeling units should be initially collapsed in the Intent editor, false otherwise
*/
private boolean shouldCollapseModelingUnitByDefault() {
// We should collapse modeling units if
return
// - preference say so
IntentEditorActivator.getDefault().getPreferenceStore()
.getBoolean(IntentPreferenceConstants.COLLAPSE_MODELING_UNITS)
&&
// - and this is editor opening
!editor.isInitialFoldingStructureComplete();
}
}