Mercurial > projects > dwt-addons
annotate dwtx/jface/text/reconciler/AbstractReconciler.d @ 158:25f1f92fa3df
...
author | Frank Benoit <benoit@tionex.de> |
---|---|
date | Tue, 26 Aug 2008 02:46:34 +0200 |
parents | 7d818bd32d63 |
children | 1a5b8f8129df |
rev | line source |
---|---|
129 | 1 /******************************************************************************* |
2 * Copyright (c) 2000, 2008 IBM Corporation and others. | |
3 * All rights reserved. This program and the accompanying materials | |
4 * are made available under the terms of the Eclipse Public License v1.0 | |
5 * which accompanies this distribution, and is available at | |
6 * http://www.eclipse.org/legal/epl-v10.html | |
7 * | |
8 * Contributors: | |
9 * IBM Corporation - initial API and implementation | |
10 * Port to the D programming language: | |
11 * Frank Benoit <benoit@tionex.de> | |
12 *******************************************************************************/ | |
13 module dwtx.jface.text.reconciler.AbstractReconciler; | |
14 | |
131 | 15 import dwtx.jface.text.reconciler.IReconciler; // packageimport |
16 import dwtx.jface.text.reconciler.DirtyRegionQueue; // packageimport | |
17 import dwtx.jface.text.reconciler.IReconcilingStrategy; // packageimport | |
18 import dwtx.jface.text.reconciler.AbstractReconcileStep; // packageimport | |
19 import dwtx.jface.text.reconciler.IReconcilingStrategyExtension; // packageimport | |
20 import dwtx.jface.text.reconciler.MonoReconciler; // packageimport | |
21 import dwtx.jface.text.reconciler.IReconcileStep; // packageimport | |
22 import dwtx.jface.text.reconciler.Reconciler; // packageimport | |
23 import dwtx.jface.text.reconciler.IReconcilableModel; // packageimport | |
24 import dwtx.jface.text.reconciler.DirtyRegion; // packageimport | |
25 import dwtx.jface.text.reconciler.IReconcileResult; // packageimport | |
26 import dwtx.jface.text.reconciler.IReconcilerExtension; // packageimport | |
27 | |
28 | |
129 | 29 import dwt.dwthelper.utils; |
158 | 30 import tango.core.Thread; |
129 | 31 |
32 import dwtx.core.runtime.Assert; | |
33 import dwtx.core.runtime.IProgressMonitor; | |
34 import dwtx.core.runtime.NullProgressMonitor; | |
35 import dwtx.jface.text.DocumentEvent; | |
36 import dwtx.jface.text.IDocument; | |
37 import dwtx.jface.text.IDocumentListener; | |
38 import dwtx.jface.text.ITextInputListener; | |
39 import dwtx.jface.text.ITextViewer; | |
40 | |
41 | |
42 /** | |
43 * Abstract implementation of {@link IReconciler}. The reconciler | |
44 * listens to input document changes as well as changes of | |
45 * the input document of the text viewer it is installed on. Depending on | |
46 * its configuration it manages the received change notifications in a | |
47 * queue folding neighboring or overlapping changes together. The reconciler | |
48 * processes the dirty regions as a background activity after having waited for further | |
49 * changes for the configured duration of time. A reconciler is started using the | |
50 * {@link #install(ITextViewer)} method. As a first step {@link #initialProcess()} is | |
51 * executed in the background. Then, the reconciling thread waits for changes that | |
52 * need to be reconciled. A reconciler can be resumed by calling {@link #forceReconciling()} | |
53 * independent from the existence of actual changes. This mechanism is for subclasses only. | |
54 * It is the clients responsibility to stop a reconciler using its {@link #uninstall()} | |
55 * method. Unstopped reconcilers do not free their resources. | |
56 * <p> | |
57 * It is subclass responsibility to specify how dirty regions are processed. | |
58 * </p> | |
59 * | |
60 * @see dwtx.jface.text.IDocumentListener | |
61 * @see dwtx.jface.text.ITextInputListener | |
62 * @see dwtx.jface.text.reconciler.DirtyRegion | |
63 * @since 2.0 | |
64 */ | |
65 abstract public class AbstractReconciler : IReconciler { | |
66 | |
67 | |
68 /** | |
69 * Background thread for the reconciling activity. | |
70 */ | |
158 | 71 class BackgroundThread { |
72 Thread thread; | |
129 | 73 |
74 /** Has the reconciler been canceled. */ | |
75 private bool fCanceled= false; | |
76 /** Has the reconciler been reset. */ | |
77 private bool fReset= false; | |
78 /** Some changes need to be processed. */ | |
79 private bool fIsDirty= false; | |
80 /** Is a reconciling strategy active. */ | |
81 private bool fIsActive= false; | |
82 | |
83 /** | |
84 * Creates a new background thread. The thread | |
85 * runs with minimal priority. | |
86 * | |
87 * @param name the thread's name | |
88 */ | |
133
7d818bd32d63
Fix ctors to this with gvim regexp
Frank Benoit <benoit@tionex.de>
parents:
131
diff
changeset
|
89 public this(String name) { |
158 | 90 thread = new Thread( &run ); |
91 thread.name = name; | |
92 thread.priority = Thread.MIN_PRIORITY; | |
93 thread.isDaemon(true); | |
94 } | |
95 | |
96 public void start(){ | |
97 thread.start(); | |
129 | 98 } |
99 | |
100 /** | |
101 * Returns whether a reconciling strategy is active right now. | |
102 * | |
103 * @return <code>true</code> if a activity is active | |
104 */ | |
105 public bool isActive() { | |
106 return fIsActive; | |
107 } | |
108 | |
109 /** | |
110 * Returns whether some changes need to be processed. | |
111 * | |
112 * @return <code>true</code> if changes wait to be processed | |
113 * @since 3.0 | |
114 */ | |
115 public synchronized bool isDirty() { | |
116 return fIsDirty; | |
117 } | |
118 | |
119 /** | |
120 * Cancels the background thread. | |
121 */ | |
122 public void cancel() { | |
123 fCanceled= true; | |
124 IProgressMonitor pm= fProgressMonitor; | |
125 if (pm !is null) | |
126 pm.setCanceled(true); | |
127 synchronized (fDirtyRegionQueue) { | |
128 fDirtyRegionQueue.notifyAll(); | |
129 } | |
130 } | |
131 | |
132 /** | |
133 * Suspends the caller of this method until this background thread has | |
134 * emptied the dirty region queue. | |
135 */ | |
136 public void suspendCallerWhileDirty() { | |
137 bool isDirty; | |
138 do { | |
139 synchronized (fDirtyRegionQueue) { | |
140 isDirty= fDirtyRegionQueue.getSize() > 0; | |
141 if (isDirty) { | |
142 try { | |
143 fDirtyRegionQueue.wait(); | |
144 } catch (InterruptedException x) { | |
145 } | |
146 } | |
147 } | |
148 } while (isDirty); | |
149 } | |
150 | |
151 /** | |
152 * Reset the background thread as the text viewer has been changed, | |
153 */ | |
154 public void reset() { | |
155 | |
156 if (fDelay > 0) { | |
157 | |
158 synchronized (this) { | |
159 fIsDirty= true; | |
160 fReset= true; | |
161 } | |
162 | |
163 } else { | |
164 | |
165 synchronized (this) { | |
166 fIsDirty= true; | |
167 } | |
168 | |
169 synchronized (fDirtyRegionQueue) { | |
170 fDirtyRegionQueue.notifyAll(); | |
171 } | |
172 } | |
173 | |
174 reconcilerReset(); | |
175 } | |
176 | |
177 /** | |
178 * The background activity. Waits until there is something in the | |
179 * queue managing the changes that have been applied to the text viewer. | |
180 * Removes the first change from the queue and process it. | |
181 * <p> | |
182 * Calls {@link AbstractReconciler#initialProcess()} on entrance. | |
183 * </p> | |
184 */ | |
185 public void run() { | |
186 | |
187 synchronized (fDirtyRegionQueue) { | |
188 try { | |
189 fDirtyRegionQueue.wait(fDelay); | |
190 } catch (InterruptedException x) { | |
191 } | |
192 } | |
193 | |
194 if (fCanceled) | |
195 return; | |
196 | |
197 initialProcess(); | |
198 | |
199 while (!fCanceled) { | |
200 | |
201 synchronized (fDirtyRegionQueue) { | |
202 try { | |
203 fDirtyRegionQueue.wait(fDelay); | |
204 } catch (InterruptedException x) { | |
205 } | |
206 } | |
207 | |
208 if (fCanceled) | |
209 break; | |
210 | |
211 if (!isDirty()) | |
212 continue; | |
213 | |
214 synchronized (this) { | |
215 if (fReset) { | |
216 fReset= false; | |
217 continue; | |
218 } | |
219 } | |
220 | |
221 DirtyRegion r= null; | |
222 synchronized (fDirtyRegionQueue) { | |
223 r= fDirtyRegionQueue.removeNextDirtyRegion(); | |
224 } | |
225 | |
226 fIsActive= true; | |
227 | |
228 fProgressMonitor.setCanceled(false); | |
229 | |
230 process(r); | |
231 | |
232 synchronized (fDirtyRegionQueue) { | |
233 if (0 is fDirtyRegionQueue.getSize()) { | |
234 synchronized (this) { | |
235 fIsDirty= fProgressMonitor.isCanceled(); | |
236 } | |
237 fDirtyRegionQueue.notifyAll(); | |
238 } | |
239 } | |
240 | |
241 fIsActive= false; | |
242 } | |
243 } | |
244 } | |
245 | |
246 /** | |
247 * Internal document listener and text input listener. | |
248 */ | |
249 class Listener : IDocumentListener, ITextInputListener { | |
250 | |
251 /* | |
252 * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent) | |
253 */ | |
254 public void documentAboutToBeChanged(DocumentEvent e) { | |
255 } | |
256 | |
257 /* | |
258 * @see IDocumentListener#documentChanged(DocumentEvent) | |
259 */ | |
260 public void documentChanged(DocumentEvent e) { | |
261 | |
262 if (!fThread.isDirty() && fThread.isAlive()) { | |
263 if (!fIsAllowedToModifyDocument && Thread.currentThread() is fThread) | |
264 throw new UnsupportedOperationException("The reconciler thread is not allowed to modify the document"); //$NON-NLS-1$ | |
265 aboutToBeReconciled(); | |
266 } | |
267 | |
268 /* | |
269 * The second OR condition handles the case when the document | |
270 * gets changed while still inside initialProcess(). | |
271 */ | |
272 if (fThread.isActive() || fThread.isDirty() && fThread.isAlive()) | |
273 fProgressMonitor.setCanceled(true); | |
274 | |
275 if (fIsIncrementalReconciler) | |
276 createDirtyRegion(e); | |
277 | |
278 fThread.reset(); | |
279 | |
280 } | |
281 | |
282 /* | |
283 * @see ITextInputListener#inputDocumentAboutToBeChanged(IDocument, IDocument) | |
284 */ | |
285 public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) { | |
286 | |
287 if (oldInput is fDocument) { | |
288 | |
289 if (fDocument !is null) | |
290 fDocument.removeDocumentListener(this); | |
291 | |
292 if (fIsIncrementalReconciler) { | |
293 synchronized (fDirtyRegionQueue) { | |
294 fDirtyRegionQueue.purgeQueue(); | |
295 } | |
296 if (fDocument !is null && fDocument.getLength() > 0 && fThread.isDirty() && fThread.isAlive()) { | |
297 DocumentEvent e= new DocumentEvent(fDocument, 0, fDocument.getLength(), ""); //$NON-NLS-1$ | |
298 createDirtyRegion(e); | |
299 fThread.reset(); | |
300 fThread.suspendCallerWhileDirty(); | |
301 } | |
302 } | |
303 | |
304 fDocument= null; | |
305 } | |
306 } | |
307 | |
308 /* | |
309 * @see ITextInputListener#inputDocumentChanged(IDocument, IDocument) | |
310 */ | |
311 public void inputDocumentChanged(IDocument oldInput, IDocument newInput) { | |
312 | |
313 fDocument= newInput; | |
314 if (fDocument is null) | |
315 return; | |
316 | |
317 | |
318 reconcilerDocumentChanged(fDocument); | |
319 | |
320 fDocument.addDocumentListener(this); | |
321 | |
322 if (!fThread.isDirty()) | |
323 aboutToBeReconciled(); | |
324 | |
325 startReconciling(); | |
326 } | |
327 } | |
328 | |
329 /** Queue to manage the changes applied to the text viewer. */ | |
330 private DirtyRegionQueue fDirtyRegionQueue; | |
331 /** The background thread. */ | |
332 private BackgroundThread fThread; | |
333 /** Internal document and text input listener. */ | |
334 private Listener fListener; | |
335 /** The background thread delay. */ | |
336 private int fDelay= 500; | |
337 /** Are there incremental reconciling strategies? */ | |
338 private bool fIsIncrementalReconciler= true; | |
339 /** The progress monitor used by this reconciler. */ | |
340 private IProgressMonitor fProgressMonitor; | |
341 /** | |
342 * Tells whether this reconciler is allowed to modify the document. | |
343 * @since 3.2 | |
344 */ | |
345 private bool fIsAllowedToModifyDocument= true; | |
346 | |
347 | |
348 /** The text viewer's document. */ | |
349 private IDocument fDocument; | |
350 /** The text viewer */ | |
351 private ITextViewer fViewer; | |
352 | |
353 | |
354 /** | |
355 * Processes a dirty region. If the dirty region is <code>null</code> the whole | |
356 * document is consider being dirty. The dirty region is partitioned by the | |
357 * document and each partition is handed over to a reconciling strategy registered | |
358 * for the partition's content type. | |
359 * | |
360 * @param dirtyRegion the dirty region to be processed | |
361 */ | |
362 abstract protected void process(DirtyRegion dirtyRegion); | |
363 | |
364 /** | |
365 * Hook called when the document whose contents should be reconciled | |
366 * has been changed, i.e., the input document of the text viewer this | |
367 * reconciler is installed on. Usually, subclasses use this hook to | |
368 * inform all their reconciling strategies about the change. | |
369 * | |
370 * @param newDocument the new reconciler document | |
371 */ | |
372 abstract protected void reconcilerDocumentChanged(IDocument newDocument); | |
373 | |
374 | |
375 /** | |
376 * Creates a new reconciler without configuring it. | |
377 */ | |
133
7d818bd32d63
Fix ctors to this with gvim regexp
Frank Benoit <benoit@tionex.de>
parents:
131
diff
changeset
|
378 protected this() { |
129 | 379 fProgressMonitor= new NullProgressMonitor(); |
380 } | |
381 | |
382 /** | |
383 * Tells the reconciler how long it should wait for further text changes before | |
384 * activating the appropriate reconciling strategies. | |
385 * | |
386 * @param delay the duration in milliseconds of a change collection period. | |
387 */ | |
388 public void setDelay(int delay) { | |
389 fDelay= delay; | |
390 } | |
391 | |
392 /** | |
393 * Tells the reconciler whether any of the available reconciling strategies | |
394 * is interested in getting detailed dirty region information or just in the | |
395 * fact that the document has been changed. In the second case, the reconciling | |
396 * can not incrementally be pursued. | |
397 * | |
398 * @param isIncremental indicates whether this reconciler will be configured with | |
399 * incremental reconciling strategies | |
400 * | |
401 * @see DirtyRegion | |
402 * @see IReconcilingStrategy | |
403 */ | |
404 public void setIsIncrementalReconciler(bool isIncremental) { | |
405 fIsIncrementalReconciler= isIncremental; | |
406 } | |
158 | 407 |
129 | 408 /** |
409 * Tells the reconciler whether it is allowed to change the document | |
410 * inside its reconciler thread. | |
411 * <p> | |
412 * If this is set to <code>false</code> an {@link UnsupportedOperationException} | |
413 * will be thrown when this restriction will be violated. | |
414 * </p> | |
415 * | |
416 * @param isAllowedToModify indicates whether this reconciler is allowed to modify the document | |
417 * @since 3.2 | |
418 */ | |
419 public void setIsAllowedToModifyDocument(bool isAllowedToModify) { | |
420 fIsAllowedToModifyDocument= isAllowedToModify; | |
421 } | |
422 | |
423 /** | |
424 * Sets the progress monitor of this reconciler. | |
425 * | |
426 * @param monitor the monitor to be used | |
427 */ | |
428 public void setProgressMonitor(IProgressMonitor monitor) { | |
429 Assert.isLegal(monitor !is null); | |
430 fProgressMonitor= monitor; | |
431 } | |
432 | |
433 /** | |
434 * Returns whether any of the reconciling strategies is interested in | |
435 * detailed dirty region information. | |
436 * | |
437 * @return whether this reconciler is incremental | |
438 * | |
439 * @see IReconcilingStrategy | |
440 */ | |
441 protected bool isIncrementalReconciler() { | |
442 return fIsIncrementalReconciler; | |
443 } | |
444 | |
445 /** | |
446 * Returns the input document of the text viewer this reconciler is installed on. | |
447 * | |
448 * @return the reconciler document | |
449 */ | |
450 protected IDocument getDocument() { | |
451 return fDocument; | |
452 } | |
453 | |
454 /** | |
455 * Returns the text viewer this reconciler is installed on. | |
456 * | |
457 * @return the text viewer this reconciler is installed on | |
458 */ | |
459 protected ITextViewer getTextViewer() { | |
460 return fViewer; | |
461 } | |
462 | |
463 /** | |
464 * Returns the progress monitor of this reconciler. | |
465 * | |
466 * @return the progress monitor of this reconciler | |
467 */ | |
468 protected IProgressMonitor getProgressMonitor() { | |
469 return fProgressMonitor; | |
470 } | |
471 | |
472 /* | |
473 * @see IReconciler#install(ITextViewer) | |
474 */ | |
475 public void install(ITextViewer textViewer) { | |
476 | |
477 Assert.isNotNull(textViewer); | |
478 fViewer= textViewer; | |
479 | |
480 synchronized (this) { | |
481 if (fThread !is null) | |
482 return; | |
483 fThread= new BackgroundThread(getClass().getName()); | |
484 } | |
485 | |
486 fDirtyRegionQueue= new DirtyRegionQueue(); | |
487 | |
488 fListener= new Listener(); | |
489 fViewer.addTextInputListener(fListener); | |
490 | |
491 // see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=67046 | |
492 // if the reconciler gets installed on a viewer that already has a document | |
493 // (e.g. when reusing editors), we force the listener to register | |
494 // itself as document listener, because there will be no input change | |
495 // on the viewer. | |
496 // In order to do that, we simulate an input change. | |
497 IDocument document= textViewer.getDocument(); | |
498 if (document !is null) { | |
499 fListener.inputDocumentAboutToBeChanged(fDocument, document); | |
500 fListener.inputDocumentChanged(fDocument, document); | |
501 } | |
502 } | |
503 | |
504 /* | |
505 * @see IReconciler#uninstall() | |
506 */ | |
507 public void uninstall() { | |
508 if (fListener !is null) { | |
509 | |
510 fViewer.removeTextInputListener(fListener); | |
511 if (fDocument !is null) { | |
512 fListener.inputDocumentAboutToBeChanged(fDocument, null); | |
513 fListener.inputDocumentChanged(fDocument, null); | |
514 } | |
515 fListener= null; | |
516 | |
517 synchronized (this) { | |
518 // http://dev.eclipse.org/bugs/show_bug.cgi?id=19135 | |
519 BackgroundThread bt= fThread; | |
520 fThread= null; | |
521 bt.cancel(); | |
522 } | |
523 } | |
524 } | |
525 | |
526 /** | |
527 * Creates a dirty region for a document event and adds it to the queue. | |
528 * | |
529 * @param e the document event for which to create a dirty region | |
530 */ | |
531 private void createDirtyRegion(DocumentEvent e) { | |
532 synchronized (fDirtyRegionQueue) { | |
533 if (e.getLength() is 0 && e.getText() !is null) { | |
534 // Insert | |
535 fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getText().length(), DirtyRegion.INSERT, e.getText())); | |
158 | 536 |
129 | 537 } else if (e.getText() is null || e.getText().length() is 0) { |
538 // Remove | |
539 fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getLength(), DirtyRegion.REMOVE, null)); | |
158 | 540 |
129 | 541 } else { |
542 // Replace (Remove + Insert) | |
543 fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getLength(), DirtyRegion.REMOVE, null)); | |
544 fDirtyRegionQueue.addDirtyRegion(new DirtyRegion(e.getOffset(), e.getText().length(), DirtyRegion.INSERT, e.getText())); | |
545 } | |
546 } | |
547 } | |
548 | |
549 /** | |
550 * Hook for subclasses which want to perform some | |
551 * action as soon as reconciliation is needed. | |
552 * <p> | |
553 * Default implementation is to do nothing. | |
554 * </p> | |
555 * | |
556 * @since 3.0 | |
557 */ | |
558 protected void aboutToBeReconciled() { | |
559 } | |
560 | |
561 /** | |
562 * This method is called on startup of the background activity. It is called only | |
563 * once during the life time of the reconciler. Clients may reimplement this method. | |
564 */ | |
565 protected void initialProcess() { | |
566 } | |
567 | |
568 /** | |
569 * Forces the reconciler to reconcile the structure of the whole document. | |
570 * Clients may extend this method. | |
571 */ | |
572 protected void forceReconciling() { | |
573 | |
574 if (fDocument !is null) { | |
575 | |
576 if (!fThread.isDirty()&& fThread.isAlive()) | |
577 aboutToBeReconciled(); | |
578 | |
579 if (fThread.isActive()) | |
580 fProgressMonitor.setCanceled(true); | |
158 | 581 |
129 | 582 if (fIsIncrementalReconciler) { |
583 DocumentEvent e= new DocumentEvent(fDocument, 0, fDocument.getLength(), fDocument.get()); | |
584 createDirtyRegion(e); | |
585 } | |
586 | |
587 startReconciling(); | |
588 } | |
589 } | |
590 | |
591 /** | |
592 * Starts the reconciler to reconcile the queued dirty-regions. | |
593 * Clients may extend this method. | |
594 */ | |
595 protected synchronized void startReconciling() { | |
596 if (fThread is null) | |
597 return; | |
598 | |
599 if (!fThread.isAlive()) { | |
600 try { | |
601 fThread.start(); | |
602 } catch (IllegalThreadStateException e) { | |
603 // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=40549 | |
604 // This is the only instance where the thread is started; since | |
605 // we checked that it is not alive, it must be dead already due | |
606 // to a run-time exception or error. Exit. | |
607 } | |
608 } else { | |
609 fThread.reset(); | |
610 } | |
611 } | |
612 | |
613 /** | |
614 * Hook that is called after the reconciler thread has been reset. | |
615 */ | |
616 protected void reconcilerReset() { | |
617 } | |
158 | 618 |
129 | 619 /** |
620 * Tells whether the code is running in this reconciler's | |
621 * background thread. | |
158 | 622 * |
129 | 623 * @return <code>true</code> if running in this reconciler's background thread |
624 * @since 3.4 | |
625 */ | |
626 protected bool isRunningInReconcilerThread() { | |
627 return Thread.currentThread() is fThread; | |
628 } | |
629 } |