Merge commit 'origin/master'
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / editor / impl / softwrap / mapping / SoftWrapApplianceManager.java
1 /*
2  * Copyright 2000-2010 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.editor.impl.softwrap.mapping;
17
18 import com.intellij.openapi.application.ex.ApplicationManagerEx;
19 import com.intellij.openapi.editor.*;
20 import com.intellij.openapi.editor.event.DocumentEvent;
21 import com.intellij.openapi.editor.event.DocumentListener;
22 import com.intellij.openapi.editor.ex.EditorEx;
23 import com.intellij.openapi.editor.ex.FoldingListener;
24 import com.intellij.openapi.editor.ex.FoldingModelEx;
25 import com.intellij.openapi.editor.ex.util.EditorUtil;
26 import com.intellij.openapi.editor.impl.EditorTextRepresentationHelper;
27 import com.intellij.openapi.editor.impl.FontInfo;
28 import com.intellij.openapi.editor.impl.IterationState;
29 import com.intellij.openapi.editor.impl.softwrap.*;
30 import com.intellij.openapi.editor.markup.TextAttributes;
31 import com.intellij.openapi.util.TextRange;
32 import com.intellij.openapi.util.text.StringUtil;
33 import gnu.trove.TIntIntHashMap;
34 import org.jetbrains.annotations.NotNull;
35
36 import javax.swing.*;
37 import java.awt.*;
38 import java.util.ArrayList;
39 import java.util.List;
40
41 /**
42  * //TODO den add doc
43  *
44  * Default {@link SoftWrapApplianceManager} implementation that is built with the following design guide lines:
45  * <pre>
46  * <ul>
47  *   <li>
48  *      perform soft wrap processing per-logical line, i.e. every time current manager is asked to process
49  *      particular text range, it calculates logical lines that contain all target symbols, checks if they should
50  *      be soft-wrapped and registers corresponding soft wraps if necessary;
51  *   </li>
52  *   <li>
53  *      objects of this class remember processed logical lines and perform new processing for them only if visible
54  *      area width is changed;
55  *   </li>
56  *   <li>
57  *      {@link SoftWrapsStorage#removeAll() drops all registered soft wraps} if visible area width is changed;
58  *   </li>
59  * </ul>
60  * </pre>
61  * <p/>
62  * Not thread-safe.
63  *
64  * @author Denis Zhdanov
65  * @since Jul 5, 2010 10:01:27 AM
66  */
67 public class SoftWrapApplianceManager implements FoldingListener, DocumentListener {
68
69   /** Enumerates possible type of soft wrap indents to use. */
70   enum IndentType {
71     /** Don't apply special indent to soft-wrapped line at all. */
72     NONE,
73
74     /**
75      * Indent soft wraps for the {@link EditorSettings#getCustomSoftWrapIndent() user-defined number of columns}
76      * to the start of the previous visual line.
77      */
78     CUSTOM
79   }
80
81   private final List<SoftWrapAwareDocumentParsingListener> myListeners = new ArrayList<SoftWrapAwareDocumentParsingListener>();
82   private final List<DirtyRegion> myDirtyRegions = new ArrayList<DirtyRegion>();
83
84   private final SoftWrapsStorage               myStorage;
85   private final EditorEx                       myEditor;
86   private final SoftWrapPainter myPainter;
87   private final EditorTextRepresentationHelper myRepresentationHelper;
88
89   private LineWrapPositionStrategy myLineWrapPositionStrategy;
90   private boolean myCustomIndentUsedLastTime;
91   private int myCustomIndentValueUsedLastTime;
92   private int myVisibleAreaWidth;
93
94   public SoftWrapApplianceManager(@NotNull SoftWrapsStorage storage,
95                                   @NotNull EditorEx editor,
96                                   @NotNull SoftWrapPainter painter,
97                                   @NotNull EditorTextRepresentationHelper representationHelper)
98   {
99     myStorage = storage;
100     myEditor = editor;
101     myPainter = painter;
102     myRepresentationHelper = representationHelper;
103   }
104
105   public void registerSoftWrapIfNecessary(@NotNull Rectangle clip, int startOffset) {
106     //TODO den    perform full soft wraps recalculation at background thread, calculate soft wraps only for the target
107     //TODO den    visible clip at EDT
108     dropDataIfNecessary();
109     recalculateSoftWraps();
110   }
111
112   public void release() {
113     myDirtyRegions.clear();
114     myDirtyRegions.add(new DirtyRegion(0, myEditor.getDocument().getTextLength()));
115     myLineWrapPositionStrategy = null;
116   }
117
118   private void recalculateSoftWraps() {
119     if (myVisibleAreaWidth <= 0 || myDirtyRegions.isEmpty()) {
120       return;
121     }
122
123     //TODO den think about sorting and merging dirty ranges here.
124     for (DirtyRegion dirtyRegion : myDirtyRegions) {
125       recalculateSoftWraps(dirtyRegion);
126     }
127     myDirtyRegions.clear();
128   }
129
130   private void recalculateSoftWraps(DirtyRegion region) {
131     if (region.notifyAboutRecalculationStart) {
132       notifyListenersOnRangeRecalculation(region, true);
133     }
134     myStorage.removeInRange(region.startRange.getStartOffset(), region.startRange.getEndOffset());
135     try {
136       region.beforeRecalculation();
137       doRecalculateSoftWraps(region.endRange);
138     }
139     finally {
140       notifyListenersOnRangeRecalculation(region, false);
141     }
142   }
143
144   @SuppressWarnings({"AssignmentToForLoopParameter"})
145   private void doRecalculateSoftWraps(TextRange range) {
146     // Define start of the visual line that holds target range start.
147     int start = range.getStartOffset();
148     int end;
149     VisualPosition visual = new VisualPosition(myEditor.offsetToVisualPosition(start).line, 0);
150     LogicalPosition logical = myEditor.visualToLogicalPosition(visual);
151     start = myEditor.logicalPositionToOffset(logical);
152     Document document = myEditor.getDocument();
153     CharSequence text = document.getCharsSequence();
154     IterationState iterationState = new IterationState(myEditor, start, false);
155     TextAttributes attributes = iterationState.getMergedAttributes();
156     int fontType = attributes.getFontType();
157
158     ProcessingContext context = new ProcessingContext(logical, start, myEditor, myRepresentationHelper);
159     Point point = myEditor.visualPositionToXY(visual);
160     context.x = point.x;
161     int newX;
162     int spaceWidth = EditorUtil.getSpaceWidth(fontType, myEditor);
163
164     LogicalLineData logicalLineData = new LogicalLineData();
165     logicalLineData.update(logical.line, spaceWidth, myEditor);
166
167     ProcessingContext startLineContext = context.clone();
168     JComponent contentComponent = myEditor.getContentComponent();
169     TIntIntHashMap offset2fontType = new TIntIntHashMap();
170     TIntIntHashMap offset2widthInPixels = new TIntIntHashMap();
171     TIntIntHashMap fontType2spaceWidth = new TIntIntHashMap();
172     fontType2spaceWidth.put(fontType, spaceWidth);
173     int softWrapStartOffset = startLineContext.offset;
174
175     int reservedWidth = myPainter.getMinDrawingWidth(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED);
176
177     // Perform soft wraps calculation.
178     outer:
179     while (!iterationState.atEnd() && start <= range.getEndOffset()) {
180       FoldRegion currentFold = iterationState.getCurrentFold();
181       if (currentFold != null) {
182         String placeholder = currentFold.getPlaceholderText();
183         FontInfo fontInfo = EditorUtil.fontForChar(placeholder.charAt(0), fontType, myEditor);
184         newX = context.x;
185         for (int i = 0; i < placeholder.length(); i++) {
186           newX += fontInfo.charWidth(placeholder.charAt(i), contentComponent);
187         }
188         if (newX + reservedWidth >= myVisibleAreaWidth) {
189           logicalLineData.update(currentFold.getStartOffset(), spaceWidth);
190           SoftWrap softWrap = registerSoftWrap(
191             softWrapStartOffset, start, start, logicalLineData.indentInColumns,
192             logicalLineData.indentInPixels, spaceWidth
193           );
194           softWrapStartOffset = softWrap.getStart();
195           if (softWrap.getStart() < start) {
196             revertListeners(softWrap.getStart(), context.visualLine);
197             for (int j = currentFold.getStartOffset() - 1; j >= softWrap.getStart(); j--) {
198               int pixelsDiff = offset2widthInPixels.get(j);
199               int columnsDiff = calculateWidthInColumns(pixelsDiff, fontType2spaceWidth.get(offset2fontType.get(j)));
200               context.offset--;
201               context.logicalColumn -= columnsDiff;
202               context.visualColumn -= columnsDiff;
203             }
204             notifyListenersOnBeforeSoftWrap(context);
205           }
206
207           context.visualColumn = 0;
208           context.softWrapColumnDiff = context.visualColumn - context.foldingColumnDiff - context.logicalColumn;
209           context.softWrapLinesCurrent++;
210           context.visualLine++;
211           notifyListenersOnAfterSoftWrapLineFeed(context);
212
213           context.x = softWrap.getIndentInPixels();
214           context.visualColumn = softWrap.getIndentInColumns();
215           context.softWrapColumnDiff += softWrap.getIndentInColumns();
216           startLineContext.from(context);
217
218           for (int j = softWrap.getStart(); j < start; j++) {
219             fontType = offset2fontType.get(j);
220             newX = calculateNewX(context, fontType, contentComponent);
221             processSymbol(context, startLineContext, logicalLineData, fontType, newX, fontType2spaceWidth, offset2widthInPixels,
222                           offset2fontType);
223           }
224           continue;
225         }
226         else {
227           int visualLineBefore = context.visualLine;
228           int logicalColumnBefore = context.logicalColumn;
229           context.advance(currentFold);
230           context.x = newX;
231           int collapsedFoldingWidthInColumns = context.logicalColumn;
232           if (context.visualLine <= visualLineBefore) {
233             // Single-line fold region.
234             collapsedFoldingWidthInColumns = context.logicalColumn - logicalColumnBefore;
235           }
236           notifyListenersOnFoldRegion(currentFold, collapsedFoldingWidthInColumns, visualLineBefore);
237           start = context.offset;
238           softWrapStartOffset = currentFold.getEndOffset();
239         }
240       }
241
242       end = iterationState.getEndOffset();
243       for (int i = start; i < end; i++) {
244         if (!offset2fontType.contains(i)) {
245           offset2fontType.put(i, fontType);
246         }
247       }
248       for (int i = start; i < end; i++) {
249         if (i > range.getEndOffset()) {
250           break outer;
251         }
252         char c = text.charAt(i);
253         if (offset2fontType.contains(i)) {
254           fontType = offset2fontType.get(i);
255         }
256         context.symbol = c;
257         if (c == '\n') {
258           processSymbol(context, startLineContext, logicalLineData, fontType, 0, fontType2spaceWidth, offset2widthInPixels, offset2fontType);
259           softWrapStartOffset = startLineContext.offset;
260           continue;
261         }
262
263         if (offset2widthInPixels.contains(context.offset) && context.symbol != '\t'/*we need to recalculate tabulation width after soft wrap*/) {
264           newX = context.x + offset2widthInPixels.get(context.offset);
265         }
266         else {
267           newX = calculateNewX(context, fontType, contentComponent);
268         }
269
270         if (newX + reservedWidth >= myVisibleAreaWidth) {
271           logicalLineData.update(i, spaceWidth);
272           SoftWrap softWrap = registerSoftWrap(
273             softWrapStartOffset, Math.max(softWrapStartOffset, i - 1), calculateSoftWrapEndOffset(softWrapStartOffset, end),
274             logicalLineData.indentInColumns, logicalLineData.indentInPixels, spaceWidth
275           );
276           int newI = softWrap.getStart();
277
278           // There are two possible options: soft wrap offset is located before/after the current offset (it may be
279           // located after offset in situation when it's not possible to wrap in [softWrapStartOffset; currentOffset)
280           // interval). We should process that accordingly.
281           if (newI < i) {
282             revertListeners(newI, context.visualLine);
283             for (int j = i - 1; j >= newI; j--) {
284               int pixelsDiff = offset2widthInPixels.get(j);
285               int columnsDiff = calculateWidthInColumns(pixelsDiff, fontType2spaceWidth.get(offset2fontType.get(j)));
286               context.offset--;
287               context.logicalColumn -= columnsDiff;
288               context.visualColumn -= columnsDiff;
289             }
290           }
291           else if (newI > i) {
292             processSymbol(context, startLineContext, logicalLineData, fontType, newX, fontType2spaceWidth, offset2widthInPixels,
293                           offset2fontType);
294             for (int j = i + 1; j < newI; j++) {
295               context.symbol = text.charAt(j);
296               newX = calculateNewX(context, fontType, contentComponent);
297               processSymbol(context, startLineContext, logicalLineData, fontType, newX, fontType2spaceWidth, offset2widthInPixels,
298                             offset2fontType);
299             }
300           }
301
302           notifyListenersOnBeforeSoftWrap(context);
303           softWrapStartOffset = newI;
304
305           context.visualColumn = 0;
306           context.softWrapColumnDiff = context.visualColumn - context.foldingColumnDiff - context.logicalColumn;
307           context.softWrapLinesCurrent++;
308           context.visualLine++;
309           notifyListenersOnAfterSoftWrapLineFeed(context);
310
311           context.x = softWrap.getIndentInPixels();
312           context.visualColumn = softWrap.getIndentInColumns();
313           context.softWrapColumnDiff += softWrap.getIndentInColumns();
314           i = newI - 1/* because of loop increment */;
315           startLineContext.from(context);
316         }
317         else {
318           processSymbol(context, startLineContext, logicalLineData, fontType, newX, fontType2spaceWidth, offset2widthInPixels,
319                         offset2fontType);
320         }
321       }
322
323       iterationState.advance();
324       attributes = iterationState.getMergedAttributes();
325       fontType = attributes.getFontType();
326       start = iterationState.getStartOffset();
327     }
328   }
329
330   private int calculateNewX(ProcessingContext context, int fontType, JComponent component) {
331     if (context.symbol == '\t') {
332       return EditorUtil.nextTabStop(context.x, myEditor);
333     }
334     else {
335       FontInfo fontInfo = EditorUtil.fontForChar(context.symbol, fontType, myEditor);
336       return context.x + fontInfo.charWidth(context.symbol, component);
337     }
338   }
339
340   private int calculateSoftWrapEndOffset(int start, int end) {
341     CharSequence text = myEditor.getDocument().getCharsSequence();
342     for (int i = start; i < end; i++) {
343       char c = text.charAt(i);
344       if (c == '\n') {
345         return i;
346       }
347     }
348     return end;
349   }
350
351   private void processSymbol(ProcessingContext context, ProcessingContext startLineContext, LogicalLineData logicalLineData,
352                              int fontType, int newX, TIntIntHashMap fontType2spaceWidth, TIntIntHashMap offset2widthInPixels,
353                              TIntIntHashMap offset2fontType)
354   {
355     int spaceWidth;
356     if (fontType2spaceWidth.contains(fontType)) {
357       spaceWidth = fontType2spaceWidth.get(fontType);
358     }
359     else {
360       spaceWidth = EditorUtil.getSpaceWidth(fontType, myEditor);
361       fontType2spaceWidth.put(fontType, spaceWidth);
362     }
363
364     if (context.symbol == '\n') {
365       context.symbolWidthInColumns = 0;
366       context.symbolWidthInPixels = 0;
367       notifyListenersOnProcessedSymbol(context);
368       context.offset++;
369       context.onNewLine();
370       offset2fontType.clear();
371       startLineContext.from(context);
372       logicalLineData.update(context.logicalLine, spaceWidth, myEditor);
373       context.x = 0;
374       return;
375     }
376
377     context.symbolWidthInPixels = newX - context.x;
378     context.symbolWidthInColumns = calculateWidthInColumns(context.symbolWidthInPixels, spaceWidth);
379     notifyListenersOnProcessedSymbol(context);
380     context.visualColumn += context.symbolWidthInColumns;
381     context.logicalColumn += context.symbolWidthInColumns;
382     context.x = newX;
383     offset2widthInPixels.put(context.offset, context.symbolWidthInPixels);
384     context.offset++;
385   }
386
387   private static int calculateWidthInColumns(int widthInPixels, int spaceWithInPixels) {
388     int result = widthInPixels / spaceWithInPixels;
389     if (widthInPixels % spaceWithInPixels > 0) {
390       result++;
391     }
392     return result;
393   }
394
395   private SoftWrap registerSoftWrap(int minOffset, int preferredOffset, int maxOffset, int indentInColumns, int indentInPixels,
396                                     int spaceSize)
397   {
398     Document document = myEditor.getDocument();
399
400     // Performance optimization implied by profiling results analysis.
401     if (myLineWrapPositionStrategy == null) {
402       myLineWrapPositionStrategy = LanguageLineWrapPositionStrategy.INSTANCE.forEditor(myEditor);
403     }
404     int softWrapOffset = myLineWrapPositionStrategy.calculateWrapPosition(
405       document.getCharsSequence(), minOffset, maxOffset, preferredOffset, minOffset != preferredOffset
406     );
407     int indent = 0;
408     if (myCustomIndentUsedLastTime) {
409       indent = myCustomIndentValueUsedLastTime;
410     }
411     SoftWrapImpl softWrap = new SoftWrapImpl(
412       new TextChangeImpl("\n" + StringUtil.repeatSymbol(' ', indentInColumns + indent), softWrapOffset, softWrapOffset),
413       indentInColumns + indent + 1/* for 'after soft wrap' drawing */,
414       indentInPixels + (indent * spaceSize) + myPainter.getMinDrawingWidth(SoftWrapDrawingType.AFTER_SOFT_WRAP)
415     );
416     myStorage.storeOrReplace(softWrap, true);
417     return softWrap;
418   }
419
420   //TODO den add doc
421   public void dropDataIfNecessary() {
422     // Check if we need to recalculate soft wraps due to indent settings change.
423     boolean indentChanged = false;
424     IndentType currentIndentType = getIndentToUse();
425     boolean useCustomIndent = currentIndentType == IndentType.CUSTOM;
426     int currentCustomIndent = myEditor.getSettings().getCustomSoftWrapIndent();
427     if (useCustomIndent ^ myCustomIndentUsedLastTime || (useCustomIndent && myCustomIndentValueUsedLastTime != currentCustomIndent)) {
428       indentChanged = true;
429     }
430     myCustomIndentUsedLastTime = useCustomIndent;
431     myCustomIndentValueUsedLastTime = currentCustomIndent;
432
433     // Check if we need to recalculate soft wraps due to visible area width change.
434     int currentVisibleAreaWidth = myEditor.getScrollingModel().getVisibleArea().width;
435     if (!indentChanged && myVisibleAreaWidth == currentVisibleAreaWidth) {
436       return;
437     }
438
439     // Drop information about processed lines then.
440     myDirtyRegions.clear();
441     myDirtyRegions.add(new DirtyRegion(0, myEditor.getDocument().getTextLength() - 1));
442     myStorage.removeAll();
443     myVisibleAreaWidth = currentVisibleAreaWidth;
444   }
445
446   private IndentType getIndentToUse() {
447     return myEditor.getSettings().isUseCustomSoftWrapIndent() ? IndentType.CUSTOM : IndentType.NONE;
448   }
449
450   /**
451    * Registers given listener within the current manager.
452    *
453    * @param listener    listener to register
454    * @return            <code>true</code> if this collection changed as a result of the call; <code>false</code> otherwise
455    */
456   public boolean addListener(@NotNull SoftWrapAwareDocumentParsingListener listener) {
457     return myListeners.add(listener);
458   }
459
460   @SuppressWarnings({"ForLoopReplaceableByForEach"})
461   private void revertListeners(int offset, int visualLine) {
462     for (int i = 0; i < myListeners.size(); i++) {
463       // Avoid unnecessary Iterator object construction as this method is expected to be called frequently.
464       SoftWrapAwareDocumentParsingListener listener = myListeners.get(i);
465       listener.revertToOffset(offset, visualLine);
466     }
467   }
468
469   @SuppressWarnings({"ForLoopReplaceableByForEach"})
470   private void notifyListenersOnFoldRegion(@NotNull FoldRegion foldRegion, int collapsedFoldingWidthInColumns, int visualLine) {
471     for (int i = 0; i < myListeners.size(); i++) {
472       // Avoid unnecessary Iterator object construction as this method is expected to be called frequently.
473       SoftWrapAwareDocumentParsingListener listener = myListeners.get(i);
474       listener.onCollapsedFoldRegion(foldRegion, collapsedFoldingWidthInColumns, visualLine);
475     }
476   }
477
478   @SuppressWarnings({"ForLoopReplaceableByForEach"})
479   private void notifyListenersOnProcessedSymbol(@NotNull ProcessingContext context) {
480     for (int i = 0; i < myListeners.size(); i++) {
481       // Avoid unnecessary Iterator object construction as this method is expected to be called frequently.
482       SoftWrapAwareDocumentParsingListener listener = myListeners.get(i);
483       listener.onProcessedSymbol(context);
484     }
485   }
486
487   @SuppressWarnings({"ForLoopReplaceableByForEach"})
488   private void notifyListenersOnBeforeSoftWrap(@NotNull ProcessingContext context) {
489     for (int i = 0; i < myListeners.size(); i++) {
490       // Avoid unnecessary Iterator object construction as this method is expected to be called frequently.
491       SoftWrapAwareDocumentParsingListener listener = myListeners.get(i);
492       listener.beforeSoftWrap(context);
493     }
494   }
495
496   @SuppressWarnings({"ForLoopReplaceableByForEach"})
497   private void notifyListenersOnAfterSoftWrapLineFeed(@NotNull ProcessingContext context) {
498     for (int i = 0; i < myListeners.size(); i++) {
499       // Avoid unnecessary Iterator object construction as this method is expected to be called frequently.
500       SoftWrapAwareDocumentParsingListener listener = myListeners.get(i);
501       listener.afterSoftWrapLineFeed(context);
502     }
503   }
504
505   @SuppressWarnings({"ForLoopReplaceableByForEach"})
506   private void notifyListenersOnRangeRecalculation(DirtyRegion region, boolean start) {
507     for (int i = 0; i < myListeners.size(); i++) {
508       // Avoid unnecessary Iterator object construction as this method is expected to be called frequently.
509       SoftWrapAwareDocumentParsingListener listener = myListeners.get(i);
510       if (start) {
511         listener.onRecalculationStart(region.startRange.getStartOffset(), region.startRange.getEndOffset());
512       }
513       else {
514         listener.onRecalculationEnd(region.endRange.getStartOffset(), region.endRange.getEndOffset());
515       }
516     }
517   }
518
519   @Override
520   public void onFoldRegionStateChange(@NotNull FoldRegion region) {
521     /*
522     assert ApplicationManagerEx.getApplicationEx().isDispatchThread();
523
524     Document document = myEditor.getDocument();
525     int startLine = document.getLineNumber(region.getStartOffset());
526     int endLine = document.getLineNumber(region.getEndOffset());
527
528     int startOffset = document.getLineStartOffset(startLine);
529     int endOffset = document.getLineEndOffset(endLine);
530
531     myDirtyRegions.add(new DirtyRegion(startOffset, endOffset));
532     */
533   }
534
535   @Override
536   public void onFoldProcessingEnd() {
537     recalculateSoftWraps();
538   }
539
540   @Override
541   public void beforeDocumentChange(DocumentEvent event) {
542     DirtyRegion region = new DirtyRegion(event);
543     myDirtyRegions.add(region);
544     notifyListenersOnRangeRecalculation(region, true);
545   }
546
547   @Override
548   public void documentChanged(DocumentEvent event) {
549     recalculateSoftWraps();
550   }
551
552   //TODO den add doc
553   private class DirtyRegion {
554
555     public TextRange startRange;
556     public TextRange endRange;
557     public boolean notifyAboutRecalculationStart;
558     private boolean myRecalculateEnd;
559
560     DirtyRegion(int startOffset, int endOffset) {
561       startRange = new TextRange(startOffset, endOffset);
562       endRange = new TextRange(startOffset, endOffset);
563       notifyAboutRecalculationStart = true;
564     }
565
566     DirtyRegion(DocumentEvent event) {
567       Document document = event.getDocument();
568       int startLine = document.getLineNumber(event.getOffset());
569       int oldEndLine = document.getLineNumber(event.getOffset() + event.getOldLength());
570       startRange = new TextRange(document.getLineStartOffset(startLine), document.getLineEndOffset(oldEndLine));
571       endRange = new TextRange(event.getOffset(), event.getOffset() + event.getNewLength());
572       myRecalculateEnd = true;
573     }
574
575     public void beforeRecalculation() {
576       if (!myRecalculateEnd) {
577         return;
578       }
579       Document document = myEditor.getDocument();
580       int startLine = document.getLineNumber(endRange.getStartOffset());
581       int endLine = document.getLineNumber(endRange.getEndOffset());
582       endRange = new TextRange(document.getLineStartOffset(startLine), document.getLineEndOffset(endLine));
583     }
584   }
585
586   private class LogicalLineData {
587     public int indentInColumns;
588     public int indentInPixels;
589     public int endLineOffset;
590
591     private int myNonWhiteSpaceSymbolOffset;
592
593     public void update(int logicalLine, int spaceWidth, Editor editor) {
594       Document document = myEditor.getDocument();
595       int startLineOffset = document.getLineStartOffset(logicalLine);
596       endLineOffset = document.getLineEndOffset(logicalLine);
597       CharSequence text = document.getCharsSequence();
598       indentInColumns = 0;
599       indentInPixels = 0;
600
601       for (int i = startLineOffset; i < endLineOffset; i++) {
602         char c = text.charAt(i);
603         switch (c) {
604           case ' ': indentInColumns += 1; indentInPixels += spaceWidth; break;
605           case '\t':
606             int x = EditorUtil.nextTabStop(indentInPixels, editor);
607             indentInColumns += calculateWidthInColumns(x - indentInPixels, spaceWidth);
608             indentInPixels = x;
609             break;
610           default: myNonWhiteSpaceSymbolOffset = i; return;
611         }
612       }
613     }
614
615     /**
616      * There is a possible case that all document line symbols before the first soft wrap are white spaces. We don't want to use
617      * such a big indent then.
618      * <p/>
619      * This method encapsulates that 'reset' logical
620      *
621      * @param softWrapOffset    offset of the soft wrap that occurred on document line which data is stored at the current object
622      * @param spaceWidth        space width to use
623      */
624     public void update(int softWrapOffset, int spaceWidth) {
625       if (softWrapOffset > myNonWhiteSpaceSymbolOffset) {
626         return;
627       }
628       if (myCustomIndentUsedLastTime) {
629         indentInColumns = myCustomIndentValueUsedLastTime;
630         indentInPixels = indentInColumns * spaceWidth;
631       }
632       else {
633         indentInColumns = 0;
634         indentInPixels = 0;
635       }
636     }
637   }
638 }