blob: 9e80850f7ce3bf026fdcffe04239a567ce70d7d0 [file] [log] [blame]
package org.eclipse.wst.sse.ui.internal.reconcile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.wst.sse.ui.internal.IReleasable;
import org.eclipse.wst.sse.ui.internal.reconcile.validator.ValidatorBuilder;
import org.eclipse.wst.sse.ui.internal.reconcile.validator.ValidatorMetaData;
import org.eclipse.wst.sse.ui.internal.reconcile.validator.ValidatorStrategy;
/**
* Adds to DirtyRegionProcessor Job:
*
* - IDocumentListener
* - ValidatorStrategy
* - Text viewer(dispose, input changed) listeners.
* - default and validator strategies
* - DirtyRegion processing logic.
*/
public class DocumentRegionProcessor extends DirtyRegionProcessor implements IDocumentListener{
/**
* Reconclies the entire document when the document in the viewer is
* changed. This happens when the document is initially opened, as well as
* after a save-as.
*
* Also see processPostModelEvent(...) for similar behavior when document
* for the model is changed.
*/
private class SourceTextInputListener implements ITextInputListener {
public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
// do nothing
}
public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
handleInputDocumentChanged(oldInput, newInput);
}
}
/** for initital reconcile when document is opened */
private SourceTextInputListener fTextInputListener = null;
/**
* The strategy that runs validators contributed via
* <code>org.eclipse.wst.sse.ui.extensions.sourcevalidation</code>
* extension point
*/
private ValidatorStrategy fValidatorStrategy;
private final String SSE_EDITOR_ID = "org.eclipse.wst.sse.ui"; //$NON-NLS-1$
/**
* so we can tell if a partition changed after the last edit
*/
private String[] fLastPartitions;
/**
* @see org.eclipse.wst.sse.ui.internal.reconcile.DirtyRegionProcessor#install(org.eclipse.jface.text.ITextViewer)
*/
public void install(ITextViewer textViewer) {
super.install(textViewer);
fTextInputListener = new SourceTextInputListener();
textViewer.addTextInputListener(fTextInputListener);
}
/**
* @see org.eclipse.wst.sse.ui.internal.reconcile.DirtyRegionProcessor#uninstall()
*/
public void uninstall() {
if (isInstalled()) {
cancel();
// removes document listeners
reconcilerDocumentChanged(null);
// removes widget listener
getTextViewer().removeTextInputListener(fTextInputListener);
IReconcilingStrategy validatorStrategy = getValidatorStrategy();
if(validatorStrategy != null) {
if(validatorStrategy instanceof IReleasable)
((IReleasable)validatorStrategy).release();
}
}
super.uninstall();
}
/**
*
* @param oldInput
* @param newInput
*/
public void handleInputDocumentChanged(IDocument oldInput, IDocument newInput) {
// don't bother if reconciler not installed
if (isInstalled()) {
reconcilerDocumentChanged(newInput);
setDocument(newInput);
setEntireDocumentDirty(newInput);
}
}
public void setDocument(IDocument doc) {
super.setDocument(doc);
IReconcilingStrategy validatorStrategy = getValidatorStrategy();
if(validatorStrategy != null) {
validatorStrategy.setDocument(doc);
}
}
/**
* Reinitializes listeners and sets new document onall strategies.
*
* @see org.eclipse.jface.text.reconciler.AbstractReconciler#reconcilerDocumentChanged(IDocument)
*/
protected void reconcilerDocumentChanged(IDocument newDocument) {
IDocument currentDoc = getDocument();
// unhook old document listener
if (currentDoc != null)
currentDoc.removeDocumentListener(this);
// hook up new document listener
if (newDocument != null)
newDocument.addDocumentListener(this);
// sets document on all strategies
super.reconcilerDocumentChanged(newDocument);
}
/**
* @param dirtyRegion
*/
protected void process(DirtyRegion dirtyRegion) {
if (!isInstalled())
return;
ITypedRegion[] unfiltered = computePartitioning(dirtyRegion);
// remove duplicate typed regions
// that are handled by the same "total scope" strategy
ITypedRegion[] filtered = filterTotalScopeRegions(unfiltered);
DirtyRegion dirty = null;
for (int i = 0; i < filtered.length; i++) {
dirty = createDirtyRegion(filtered[i], DirtyRegion.INSERT);
// validator (extension) for this partition
if (getValidatorStrategy() != null)
getValidatorStrategy().reconcile(filtered[i], dirty);
}
}
/**
* Removes multiple "total-scope" regions (and leaves one) for a each
* partitionType. This improves performance by preventing unnecessary full
* document validations.
*
* @param unfiltered
* @return
*/
protected ITypedRegion[] filterTotalScopeRegions(ITypedRegion[] unfiltered) {
IReconcilingStrategy s = null;
// ensure there is only one typed region in the list
// for regions handled by "total scope" strategies
HashMap totalScopeRegions = new HashMap();
HashMap partialScopeRegions = new HashMap();
List allRegions = new ArrayList();
for (int i = 0; i < unfiltered.length; i++) {
String partitionType = unfiltered[i].getType();
// short circuit loop
if (totalScopeRegions.containsKey(partitionType) || partialScopeRegions.containsKey(partitionType))
continue;
s = getReconcilingStrategy(partitionType);
// might be the validator strategy
if (s == null) {
ValidatorStrategy validatorStrategy = getValidatorStrategy();
if (validatorStrategy != null) {
if (validatorStrategy.canValidatePartition(partitionType))
s = validatorStrategy;
}
}
if (s instanceof AbstractStructuredTextReconcilingStrategy) {
// only allow one dirty region for a strategy
// that has "total scope"
if (((AbstractStructuredTextReconcilingStrategy) s).isTotalScope())
totalScopeRegions.put(partitionType, unfiltered[i]);
else
partialScopeRegions.put(partitionType, unfiltered[i]);
}
else
partialScopeRegions.put(partitionType, unfiltered[i]);
}
allRegions.addAll(totalScopeRegions.values());
allRegions.addAll(partialScopeRegions.values());
ITypedRegion[] filtered = (ITypedRegion[]) allRegions.toArray(new ITypedRegion[allRegions.size()]);
if (DEBUG)
System.out.println("filtered out this many 'total-scope' regions: " + (unfiltered.length - filtered.length)); //$NON-NLS-1$
return filtered;
}
/**
* @return Returns the fValidatorStrategy.
*/
public ValidatorStrategy getValidatorStrategy() {
if (fValidatorStrategy == null) {
ValidatorStrategy validatorStrategy = null;
if (getTextViewer() instanceof ISourceViewer) {
ISourceViewer viewer = (ISourceViewer)getTextViewer();
String contentTypeId = null;
IDocument doc = viewer.getDocument();
contentTypeId = getContentType(doc);
if (contentTypeId != null) {
validatorStrategy = new ValidatorStrategy(viewer, contentTypeId);
ValidatorBuilder vBuilder = new ValidatorBuilder();
ValidatorMetaData[] vmds = vBuilder.getValidatorMetaData(SSE_EDITOR_ID);
for (int i = 0; i < vmds.length; i++) {
if (vmds[i].canHandleContentType(contentTypeId))
validatorStrategy.addValidatorMetaData(vmds[i]);
}
}
}
fValidatorStrategy = validatorStrategy;
}
return fValidatorStrategy;
}
protected String getContentType(IDocument doc) {
if(doc == null)
return null;
String contentTypeId = null;
// pa_TODO it would be nice to be able to get filename from IDocument...
// because it seems that getting content type from input stream
// isn't all that accurate (eg. w/ a javascript file)
//IContentType ct = Platform.getContentTypeManager().findContentTypeFor("test.js");
IContentType ct = null;
try {
IContentDescription desc = Platform.getContentTypeManager().getDescriptionFor(new ByteArrayInputStream(doc.get().getBytes()), null, IContentDescription.ALL);
if(desc != null) {
ct = desc.getContentType();
if(ct != null)
contentTypeId = ct.getId();
}
}
catch (IOException e) {
// just bail
}
return contentTypeId;
}
public void documentAboutToBeChanged(DocumentEvent event) {
// save partition type (to see if it changes in documentChanged())
fLastPartitions = getPartitions(event.getOffset(), event.getLength());
}
public void documentChanged(DocumentEvent event) {
if(partitionsChanged(event)) {
// pa_TODO
// this is a simple way to ensure old
// annotations are removed when partition changes
// it might be a performance hit though
setEntireDocumentDirty(getDocument());
}
else {
DirtyRegion dr = null;
if(event.getLength() == 0) {
// it's an insert
// we use text length though
// so that the new region gets validated...
dr = createDirtyRegion(event.getOffset(), 0, DirtyRegion.INSERT);
}
else {
if("".equals(event.getText())) { //$NON-NLS-1$
// it's a delete
dr =createDirtyRegion(event.getOffset(), event.getLength(), DirtyRegion.REMOVE);
}
else {
// it's a replace
dr = createDirtyRegion(event.getOffset(), event.getLength(), DirtyRegion.INSERT);
}
}
processDirtyRegion(dr);
}
}
/**
* Checks previous partitions from the span of the event
* w/ the new partitions from the span of the event.
* If partitions changed, return true, else return false
*
* @param event
* @return
*/
private boolean partitionsChanged(DocumentEvent event) {
boolean changed = false;
String[] newPartitions = getPartitions(event.getOffset(), event.getLength());
if(fLastPartitions != null) {
if(fLastPartitions.length != newPartitions.length) {
changed = true;
}
else {
for(int i=0; i<fLastPartitions.length; i++) {
if(!fLastPartitions[i].equals(newPartitions[i])) {
changed = true;
break;
}
}
}
}
return changed;
}
/**
* Basically means process the entire document.
* @param document
*/
protected void setEntireDocumentDirty(IDocument document) {
// make the entire document dirty
// this also happens on a "save as"
if (document != null && isInstalled()) {
// since we're marking the entire doc dirty
getDirtyRegionQueue().clear();
DirtyRegion entireDocument = createDirtyRegion(0, document.getLength(), DirtyRegion.INSERT);
processDirtyRegion(entireDocument);
}
}
}