pavery | dc8b306 | 2005-10-24 20:43:42 +0000 | [diff] [blame^] | 1 | package org.eclipse.wst.sse.ui.internal.reconcile; |
| 2 | |
| 3 | import java.io.ByteArrayInputStream; |
| 4 | import java.io.IOException; |
| 5 | import java.util.ArrayList; |
| 6 | import java.util.HashMap; |
| 7 | import java.util.Iterator; |
| 8 | import java.util.List; |
| 9 | |
| 10 | import org.eclipse.core.runtime.Platform; |
| 11 | import org.eclipse.core.runtime.content.IContentDescription; |
| 12 | import org.eclipse.core.runtime.content.IContentType; |
| 13 | import org.eclipse.jface.text.DocumentEvent; |
| 14 | import org.eclipse.jface.text.IDocument; |
| 15 | import org.eclipse.jface.text.IDocumentListener; |
| 16 | import org.eclipse.jface.text.ITextInputListener; |
| 17 | import org.eclipse.jface.text.ITextViewer; |
| 18 | import org.eclipse.jface.text.ITypedRegion; |
| 19 | import org.eclipse.jface.text.reconciler.DirtyRegion; |
| 20 | import org.eclipse.jface.text.reconciler.IReconcilingStrategy; |
| 21 | import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension; |
| 22 | import org.eclipse.jface.text.source.ISourceViewer; |
| 23 | import org.eclipse.wst.sse.ui.internal.IReleasable; |
| 24 | import org.eclipse.wst.sse.ui.internal.reconcile.validator.ValidatorBuilder; |
| 25 | import org.eclipse.wst.sse.ui.internal.reconcile.validator.ValidatorMetaData; |
| 26 | import org.eclipse.wst.sse.ui.internal.reconcile.validator.ValidatorStrategy; |
| 27 | |
| 28 | /** |
| 29 | * Adds to DirtyRegionProcessor Job: |
| 30 | * |
| 31 | * - IDocumentListener |
| 32 | * - ValidatorStrategy |
| 33 | * - DefaultStrategy |
| 34 | * - Text viewer(dispose, input changed) listeners. |
| 35 | * - default and validator strategies |
| 36 | * - DirtyRegion processing logic. |
| 37 | */ |
| 38 | public class DocumentRegionProcessor extends DirtyRegionProcessor implements IDocumentListener{ |
| 39 | |
| 40 | /** |
| 41 | * Reconclies the entire document when the document in the viewer is |
| 42 | * changed. This happens when the document is initially opened, as well as |
| 43 | * after a save-as. |
| 44 | * |
| 45 | * Also see processPostModelEvent(...) for similar behavior when document |
| 46 | * for the model is changed. |
| 47 | */ |
| 48 | private class SourceTextInputListener implements ITextInputListener { |
| 49 | |
| 50 | public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) { |
| 51 | // do nothing |
| 52 | } |
| 53 | |
| 54 | public void inputDocumentChanged(IDocument oldInput, IDocument newInput) { |
| 55 | handleInputDocumentChanged(oldInput, newInput); |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | /** for initital reconcile when document is opened */ |
| 60 | private SourceTextInputListener fTextInputListener = null; |
| 61 | |
| 62 | |
| 63 | /** strategy called for unmapped partitions */ |
| 64 | private IReconcilingStrategy fDefaultStrategy; |
| 65 | |
| 66 | /** |
| 67 | * The strategy that runs validators contributed via |
| 68 | * <code>org.eclipse.wst.sse.ui.extensions.sourcevalidation</code> |
| 69 | * extension point |
| 70 | */ |
| 71 | private ValidatorStrategy fValidatorStrategy; |
| 72 | |
| 73 | private final String SSE_EDITOR_ID = "org.eclipse.wst.sse.ui"; //$NON-NLS-1$ |
| 74 | |
| 75 | private String[] fLastPartitions; |
| 76 | |
| 77 | /** |
| 78 | * @see org.eclipse.wst.sse.ui.internal.reconcile.DirtyRegionProcessor#install(org.eclipse.jface.text.ITextViewer) |
| 79 | */ |
| 80 | public void install(ITextViewer textViewer) { |
| 81 | |
| 82 | super.install(textViewer); |
| 83 | fTextInputListener = new SourceTextInputListener(); |
| 84 | textViewer.addTextInputListener(fTextInputListener); |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * @see org.eclipse.wst.sse.ui.internal.reconcile.DirtyRegionProcessor#uninstall() |
| 89 | */ |
| 90 | public void uninstall() { |
| 91 | if (isInstalled()) { |
| 92 | |
| 93 | getLocalProgressMonitor().setCanceled(true); |
| 94 | cancel(); |
| 95 | |
| 96 | // removes document listeners |
| 97 | reconcilerDocumentChanged(null); |
| 98 | |
| 99 | // removes widget listener |
| 100 | getTextViewer().removeTextInputListener(fTextInputListener); |
| 101 | // getTextViewer().getTextWidget().removeDisposeListener(fDisposeListener); |
| 102 | |
| 103 | // release all strategies |
| 104 | List strategyTypes = getStrategyTypes(); |
| 105 | if (!strategyTypes.isEmpty()) { |
| 106 | Iterator it = strategyTypes.iterator(); |
| 107 | IReconcilingStrategy strategy = null; |
| 108 | while (it.hasNext()) { |
| 109 | strategy = getReconcilingStrategy((String) it.next()); |
| 110 | if (strategy instanceof IReleasable) { |
| 111 | ((IReleasable) strategy).release(); |
| 112 | strategy = null; |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | IReconcilingStrategy defaultStrategy = getDefaultStrategy(); |
| 118 | IReconcilingStrategy validatorStrategy = getValidatorStrategy(); |
| 119 | |
| 120 | if(defaultStrategy != null) { |
| 121 | if(defaultStrategy instanceof IReleasable) |
| 122 | ((IReleasable)defaultStrategy).release(); |
| 123 | } |
| 124 | |
| 125 | if(validatorStrategy != null) { |
| 126 | if(validatorStrategy instanceof IReleasable) |
| 127 | ((IReleasable)validatorStrategy).release(); |
| 128 | } |
| 129 | } |
| 130 | super.uninstall(); |
| 131 | } |
| 132 | |
| 133 | /** |
| 134 | * |
| 135 | * @param oldInput |
| 136 | * @param newInput |
| 137 | */ |
| 138 | public void handleInputDocumentChanged(IDocument oldInput, IDocument newInput) { |
| 139 | // don't bother if reconciler not installed |
| 140 | if (isInstalled()) { |
| 141 | |
| 142 | reconcilerDocumentChanged(newInput); |
| 143 | |
| 144 | setDocument(newInput); |
| 145 | setDocumentOnAllStrategies(newInput); |
| 146 | setEntireDocumentDirty(newInput); |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * Reinitializes listeners and sets new document onall strategies. |
| 152 | * |
| 153 | * @see org.eclipse.jface.text.reconciler.AbstractReconciler#reconcilerDocumentChanged(IDocument) |
| 154 | */ |
| 155 | protected void reconcilerDocumentChanged(IDocument newDocument) { |
| 156 | |
| 157 | IDocument currentDoc = getDocument(); |
| 158 | |
| 159 | // unhook old document listener |
| 160 | if (currentDoc != null) |
| 161 | currentDoc.removeDocumentListener(this); |
| 162 | // hook up new document listener |
| 163 | if (newDocument != null) |
| 164 | newDocument.addDocumentListener(this); |
| 165 | |
| 166 | // sets document on all strategies |
| 167 | super.reconcilerDocumentChanged(newDocument); |
| 168 | } |
| 169 | |
| 170 | /** |
| 171 | * @param dirtyRegion |
| 172 | */ |
| 173 | protected void process(DirtyRegion dirtyRegion) { |
| 174 | if (!isInstalled()) |
| 175 | return; |
| 176 | |
| 177 | ITypedRegion[] unfiltered = computePartitioning(dirtyRegion); |
| 178 | |
| 179 | // remove duplicate typed regions |
| 180 | // that are handled by the same "total scope" strategy |
| 181 | ITypedRegion[] filtered = filterTotalScopeRegions(unfiltered); |
| 182 | |
| 183 | IReconcilingStrategy s; |
| 184 | DirtyRegion dirty = null; |
| 185 | for (int i = 0; i < filtered.length; i++) { |
| 186 | |
| 187 | dirty = createDirtyRegion(filtered[i], DirtyRegion.INSERT); |
| 188 | s = getReconcilingStrategy(filtered[i].getType()); |
| 189 | if (s != null && dirty != null) { |
| 190 | s.reconcile(dirty, dirty); |
| 191 | } |
| 192 | |
| 193 | // validator for this partition |
| 194 | if (fValidatorStrategy != null) |
| 195 | fValidatorStrategy.reconcile(filtered[i], dirty); |
| 196 | } |
| 197 | } |
| 198 | /** |
| 199 | * Removes multiple "total-scope" regions (and leaves one) for a each |
| 200 | * partitionType. This improves performance by preventing unnecessary full |
| 201 | * document validations. |
| 202 | * |
| 203 | * @param unfiltered |
| 204 | * @return |
| 205 | */ |
| 206 | private ITypedRegion[] filterTotalScopeRegions(ITypedRegion[] unfiltered) { |
| 207 | IReconcilingStrategy s = null; |
| 208 | // ensure there is only one typed region in the list |
| 209 | // for regions handled by "total scope" strategies |
| 210 | HashMap totalScopeRegions = new HashMap(); |
| 211 | HashMap partialScopeRegions = new HashMap(); |
| 212 | List allRegions = new ArrayList(); |
| 213 | for (int i = 0; i < unfiltered.length; i++) { |
| 214 | |
| 215 | String partitionType = unfiltered[i].getType(); |
| 216 | |
| 217 | // short circuit loop |
| 218 | if (totalScopeRegions.containsKey(partitionType) || partialScopeRegions.containsKey(partitionType)) |
| 219 | continue; |
| 220 | |
| 221 | s = getReconcilingStrategy(partitionType); |
| 222 | |
| 223 | // might be the validator strategy |
| 224 | if (s == null) { |
| 225 | ValidatorStrategy validatorStrategy = getValidatorStrategy(); |
| 226 | if (validatorStrategy != null) { |
| 227 | if (validatorStrategy.canValidatePartition(partitionType)) |
| 228 | s = validatorStrategy; |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | if (s instanceof AbstractStructuredTextReconcilingStrategy) { |
| 233 | // only allow one dirty region for a strategy |
| 234 | // that has "total scope" |
| 235 | if (((AbstractStructuredTextReconcilingStrategy) s).isTotalScope()) |
| 236 | totalScopeRegions.put(partitionType, unfiltered[i]); |
| 237 | else |
| 238 | partialScopeRegions.put(partitionType, unfiltered[i]); |
| 239 | } |
| 240 | else |
| 241 | partialScopeRegions.put(partitionType, unfiltered[i]); |
| 242 | } |
| 243 | allRegions.addAll(totalScopeRegions.values()); |
| 244 | allRegions.addAll(partialScopeRegions.values()); |
| 245 | ITypedRegion[] filtered = (ITypedRegion[]) allRegions.toArray(new ITypedRegion[allRegions.size()]); |
| 246 | |
| 247 | if (DEBUG) |
| 248 | System.out.println("filtered out this many 'total-scope' regions: " + (unfiltered.length - filtered.length)); //$NON-NLS-1$ |
| 249 | |
| 250 | return filtered; |
| 251 | } |
| 252 | |
| 253 | /** |
| 254 | * @param defaultStrategy |
| 255 | * The fDefaultStrategy to set. |
| 256 | */ |
| 257 | public void setDefaultStrategy(IReconcilingStrategy defaultStrategy) { |
| 258 | fDefaultStrategy = defaultStrategy; |
| 259 | if (fDefaultStrategy != null) { |
| 260 | fDefaultStrategy.setDocument(getDocument()); |
| 261 | if (fDefaultStrategy instanceof IReconcilingStrategyExtension) |
| 262 | ((IReconcilingStrategyExtension) fDefaultStrategy).setProgressMonitor(getLocalProgressMonitor()); |
| 263 | } |
| 264 | } |
| 265 | /** |
| 266 | * @return Returns the fDefaultStrategy. |
| 267 | */ |
| 268 | public IReconcilingStrategy getDefaultStrategy() { |
| 269 | return fDefaultStrategy; |
| 270 | } |
| 271 | |
| 272 | /** |
| 273 | * @see org.eclipse.wst.sse.ui.internal.reconcile.DirtyRegionProcessor#getAppropriateStrategy(org.eclipse.jface.text.reconciler.DirtyRegion) |
| 274 | */ |
| 275 | protected IReconcilingStrategy getStrategy(DirtyRegion dirtyRegion) { |
| 276 | IReconcilingStrategy strategy = super.getStrategy(dirtyRegion); |
| 277 | if (strategy == null) |
| 278 | strategy = getDefaultStrategy(); |
| 279 | return strategy; |
| 280 | } |
| 281 | |
| 282 | /** |
| 283 | * @return Returns the fValidatorStrategy. |
| 284 | */ |
| 285 | public ValidatorStrategy getValidatorStrategy() { |
| 286 | if (fValidatorStrategy == null) { |
| 287 | ValidatorStrategy validatorStrategy = null; |
| 288 | |
| 289 | if (getTextViewer() instanceof ISourceViewer) { |
| 290 | ISourceViewer viewer = (ISourceViewer)getTextViewer(); |
| 291 | String contentTypeId = null; |
| 292 | |
| 293 | IDocument doc = viewer.getDocument(); |
| 294 | contentTypeId = getContentType(doc); |
| 295 | |
| 296 | if (contentTypeId != null) { |
| 297 | |
| 298 | validatorStrategy = new ValidatorStrategy(viewer, contentTypeId); |
| 299 | ValidatorBuilder vBuilder = new ValidatorBuilder(); |
| 300 | ValidatorMetaData[] vmds = vBuilder.getValidatorMetaData(SSE_EDITOR_ID); |
| 301 | for (int i = 0; i < vmds.length; i++) { |
| 302 | if (vmds[i].canHandleContentType(contentTypeId)) |
| 303 | validatorStrategy.addValidatorMetaData(vmds[i]); |
| 304 | } |
| 305 | } |
| 306 | } |
| 307 | fValidatorStrategy = validatorStrategy; |
| 308 | } |
| 309 | return fValidatorStrategy; |
| 310 | } |
| 311 | |
| 312 | protected String getContentType(IDocument doc) { |
| 313 | |
| 314 | if(doc == null) |
| 315 | return null; |
| 316 | |
| 317 | String contentTypeId = null; |
| 318 | |
| 319 | // pa_TODO it would be nice to be able to get filename from IDocument... |
| 320 | // because it seems that getting content type from input stream |
| 321 | // isn't all that accurate (eg. w/ a javascript file) |
| 322 | |
| 323 | //IContentType ct = Platform.getContentTypeManager().findContentTypeFor("test.js"); |
| 324 | IContentType ct = null; |
| 325 | try { |
| 326 | IContentDescription desc = Platform.getContentTypeManager().getDescriptionFor(new ByteArrayInputStream(doc.get().getBytes()), null, IContentDescription.ALL); |
| 327 | if(desc != null) { |
| 328 | ct = desc.getContentType(); |
| 329 | if(ct != null) |
| 330 | contentTypeId = ct.getId(); |
| 331 | } |
| 332 | } |
| 333 | catch (IOException e) { |
| 334 | // just bail |
| 335 | } |
| 336 | return contentTypeId; |
| 337 | } |
| 338 | /** |
| 339 | * @see org.eclipse.wst.sse.ui.internal.reconcile.DirtyRegionProcessor#setDocumentOnAllStrategies(org.eclipse.jface.text.IDocument) |
| 340 | */ |
| 341 | protected void setDocumentOnAllStrategies(IDocument document) { |
| 342 | |
| 343 | super.setDocumentOnAllStrategies(document); |
| 344 | |
| 345 | IReconcilingStrategy defaultStrategy = getDefaultStrategy(); |
| 346 | IReconcilingStrategy validatorStrategy = getValidatorStrategy(); |
| 347 | |
| 348 | // default strategies |
| 349 | if (defaultStrategy != null) |
| 350 | defaultStrategy.setDocument(document); |
| 351 | |
| 352 | // external validator strategy |
| 353 | if (validatorStrategy != null) |
| 354 | validatorStrategy.setDocument(document); |
| 355 | } |
| 356 | |
| 357 | public void documentAboutToBeChanged(DocumentEvent event) { |
| 358 | // save partition type (to see if it changes in documentChanged()) |
| 359 | fLastPartitions = getPartitions(event.getOffset(), event.getLength()); |
| 360 | } |
| 361 | |
| 362 | public void documentChanged(DocumentEvent event) { |
| 363 | if(partitionsChanged(event)) { |
| 364 | // pa_TODO |
| 365 | // this is a simple way to ensure old |
| 366 | // annotations are removed when partition changes |
| 367 | |
| 368 | // it might be a performance hit though |
| 369 | setEntireDocumentDirty(getDocument()); |
| 370 | } |
| 371 | else { |
| 372 | DirtyRegion dr = null; |
| 373 | if(event.getLength() == 0) { |
| 374 | // it's an insert |
| 375 | // we use text length though |
| 376 | // so that the new region gets validated... |
| 377 | dr = createDirtyRegion(event.getOffset(), 0, DirtyRegion.INSERT); |
| 378 | } |
| 379 | else { |
| 380 | if(event.getText().equals("")) { |
| 381 | // it's a delete |
| 382 | dr =createDirtyRegion(event.getOffset(), event.getLength(), DirtyRegion.REMOVE); |
| 383 | } |
| 384 | else { |
| 385 | // it's a replace |
| 386 | dr = createDirtyRegion(event.getOffset(), event.getLength(), DirtyRegion.INSERT); |
| 387 | } |
| 388 | } |
| 389 | processDirtyRegion(dr); |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | /** |
| 394 | * Checks previous partitions from the span of the event |
| 395 | * w/ the new partitions from the span of the event. |
| 396 | * If partitions changed, return true, else return false |
| 397 | * |
| 398 | * @param event |
| 399 | * @return |
| 400 | */ |
| 401 | private boolean partitionsChanged(DocumentEvent event) { |
| 402 | boolean changed = false; |
| 403 | String[] newPartitions = getPartitions(event.getOffset(), event.getLength()); |
| 404 | if(fLastPartitions != null) { |
| 405 | if(fLastPartitions.length != newPartitions.length) { |
| 406 | changed = true; |
| 407 | } |
| 408 | else { |
| 409 | for(int i=0; i<fLastPartitions.length; i++) { |
| 410 | if(!fLastPartitions[i].equals(newPartitions[i])) { |
| 411 | changed = true; |
| 412 | break; |
| 413 | } |
| 414 | } |
| 415 | } |
| 416 | } |
| 417 | return changed; |
| 418 | } |
| 419 | |
| 420 | /** |
| 421 | * Basically means process the entire document. |
| 422 | * @param document |
| 423 | */ |
| 424 | protected void setEntireDocumentDirty(IDocument document) { |
| 425 | |
| 426 | // make the entire document dirty |
| 427 | // this also happens on a "save as" |
| 428 | if (document != null && isInstalled()) { |
| 429 | |
| 430 | // since we're marking the entire doc dirty |
| 431 | getDirtyRegionQueue().clear(); |
| 432 | DirtyRegion entireDocument = createDirtyRegion(0, document.getLength(), DirtyRegion.INSERT); |
| 433 | processDirtyRegion(entireDocument); |
| 434 | } |
| 435 | } |
| 436 | } |