| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.openapi.editor.impl.softwrap.mapping; |
| |
| import com.intellij.diagnostic.Dumpable; |
| import com.intellij.diagnostic.LogMessageEx; |
| import com.intellij.openapi.application.ex.ApplicationManagerEx; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.*; |
| import com.intellij.openapi.editor.event.DocumentEvent; |
| import com.intellij.openapi.editor.event.DocumentListener; |
| import com.intellij.openapi.editor.event.VisibleAreaEvent; |
| import com.intellij.openapi.editor.event.VisibleAreaListener; |
| import com.intellij.openapi.editor.ex.DocumentEx; |
| import com.intellij.openapi.editor.ex.EditorEx; |
| import com.intellij.openapi.editor.ex.ScrollingModelEx; |
| import com.intellij.openapi.editor.ex.util.EditorUtil; |
| import com.intellij.openapi.editor.impl.*; |
| import com.intellij.openapi.editor.impl.softwrap.*; |
| import com.intellij.openapi.editor.markup.TextAttributes; |
| import com.intellij.openapi.util.text.StringUtil; |
| import org.intellij.lang.annotations.JdkConstants; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.annotations.TestOnly; |
| |
| import javax.swing.*; |
| import java.awt.*; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * The general idea of soft wraps processing is to build a cache to use for quick document dimensions mapping |
| * ({@code 'logical position -> visual position'}, {@code 'offset -> logical position'} etc) and update it incrementally |
| * on events like document modification fold region(s) expanding/collapsing etc. |
| * <p/> |
| * This class encapsulates document parsing logic. It notifies {@link SoftWrapAwareDocumentParsingListener registered listeners} |
| * about parsing and they are free to store necessary information for further usage. |
| * <p/> |
| * Not thread-safe. |
| * |
| * @author Denis Zhdanov |
| * @since Jul 5, 2010 10:01:27 AM |
| */ |
| public class SoftWrapApplianceManager implements DocumentListener, Dumpable { |
| |
| private static final Logger LOG = Logger.getInstance("#" + SoftWrapApplianceManager.class.getName()); |
| |
| /** Enumerates possible type of soft wrap indents to use. */ |
| enum IndentType { |
| /** Don't apply special indent to soft-wrapped line at all. */ |
| NONE, |
| |
| /** |
| * Indent soft wraps for the {@link EditorSettings#getCustomSoftWrapIndent() user-defined number of columns} |
| * to the start of the previous visual line. |
| */ |
| CUSTOM |
| } |
| |
| private final List<SoftWrapAwareDocumentParsingListener> myListeners = new ArrayList<SoftWrapAwareDocumentParsingListener>(); |
| private final List<IncrementalCacheUpdateEvent> myActiveEvents = new ArrayList<IncrementalCacheUpdateEvent>(); |
| private final CacheUpdateEventsStorage myEventsStorage = new CacheUpdateEventsStorage(); |
| private final ProcessingContext myContext = new ProcessingContext(); |
| private final FontTypesStorage myOffset2fontType = new FontTypesStorage(); |
| private final WidthsStorage myOffset2widthInPixels = new WidthsStorage(); |
| |
| private final SoftWrapsStorage myStorage; |
| private final EditorEx myEditor; |
| private SoftWrapPainter myPainter; |
| private final SoftWrapDataMapper myDataMapper; |
| |
| /** |
| * Visual area width change causes soft wraps addition/removal, so, we want to update <code>'y'</code> coordinate |
| * of the editor viewport then. For example, we observe particular text region at the 'vcs diff' control and change |
| * its width. We would like to see the same text range at the viewport then. |
| * <p/> |
| * This field holds offset of the text range that is shown at the top-left viewport position. It's used as an anchor |
| * during viewport's <code>'y'</code> coordinate adjustment on visual area width change. |
| */ |
| private int myLastTopLeftCornerOffset = -1; |
| private int myVerticalScrollBarWidth = -1; |
| |
| private VisibleAreaWidthProvider myWidthProvider; |
| private LineWrapPositionStrategy myLineWrapPositionStrategy; |
| private IncrementalCacheUpdateEvent myEventBeingProcessed; |
| private boolean myVisualAreaListenerAttached; |
| private boolean myCustomIndentUsedLastTime; |
| private int myCustomIndentValueUsedLastTime; |
| private int myVisibleAreaWidth; |
| private boolean myInProgress; |
| private boolean myHasLinesWithFailedWrap; |
| |
| public SoftWrapApplianceManager(@NotNull SoftWrapsStorage storage, |
| @NotNull EditorEx editor, |
| @NotNull SoftWrapPainter painter, |
| SoftWrapDataMapper dataMapper) |
| { |
| myStorage = storage; |
| myEditor = editor; |
| myPainter = painter; |
| myDataMapper = dataMapper; |
| myWidthProvider = new DefaultVisibleAreaWidthProvider(editor); |
| } |
| |
| /** |
| * @return <code>true</code> if soft wraps processing detected line(s) that exceeds viewport's size but can't be soft-wrapped; |
| * i.e. part of it lays outside of the screen; |
| * <code>false</code> otherwise |
| */ |
| public boolean hasLinesWithFailedWrap() { |
| return myHasLinesWithFailedWrap; |
| } |
| |
| public void registerSoftWrapIfNecessary() { |
| recalculateIfNecessary(); |
| } |
| |
| public void reset() { |
| myEventsStorage.release(); |
| myEventsStorage.add(myEditor.getDocument(), new IncrementalCacheUpdateEvent(myEditor.getDocument())); |
| for (SoftWrapAwareDocumentParsingListener listener : myListeners) { |
| listener.reset(); |
| } |
| } |
| |
| public void release() { |
| myEventsStorage.release(); |
| myLineWrapPositionStrategy = null; |
| } |
| |
| private void initListenerIfNecessary() { |
| // We can't attach the listener during this object initialization because there is a big chance that the editor is in incomplete |
| // state there (e.g. it's scrolling model is not initialized yet). |
| if (myVisualAreaListenerAttached) { |
| return; |
| } |
| myVisualAreaListenerAttached = true; |
| myEditor.getScrollingModel().addVisibleAreaListener(new VisibleAreaListener() { |
| @Override |
| public void visibleAreaChanged(VisibleAreaEvent e) { |
| updateLastTopLeftCornerOffset(); |
| } |
| }); |
| updateLastTopLeftCornerOffset(); |
| } |
| |
| /** |
| * @return <code>true</code> if soft wraps were really re-calculated; |
| * <code>false</code> if it's not possible to do at the moment (e.g. current editor is not shown and we don't |
| * have information about viewport width) |
| */ |
| private boolean recalculateSoftWraps() { |
| initListenerIfNecessary(); |
| if (myEventsStorage.getEvents().isEmpty()) { |
| return true; |
| } |
| if (myVisibleAreaWidth <= 0) { |
| return false; |
| } |
| |
| // There is a possible case that new dirty regions are encountered during processing, hence, we iterate on regions snapshot here. |
| List<IncrementalCacheUpdateEvent> events = new ArrayList<IncrementalCacheUpdateEvent>(myEventsStorage.getEvents()); |
| myActiveEvents.addAll(events); |
| myEventsStorage.release(); |
| if (myInProgress && !events.isEmpty()) { |
| String state = ""; |
| if (myEditor instanceof EditorImpl) { |
| state = ((EditorImpl)myEditor).dumpState(); |
| } |
| LogMessageEx.error(LOG, "Detected race condition at soft wraps recalculation", String.format( |
| "Current events: %s. Concurrent events: %s, event being processed: %s%n%s", |
| events, myActiveEvents, myEventBeingProcessed, state |
| )); |
| } |
| myInProgress = true; |
| myHasLinesWithFailedWrap = false; |
| try { |
| for (IncrementalCacheUpdateEvent event : events) { |
| myEventBeingProcessed = event; |
| recalculateSoftWraps(event); |
| } |
| } |
| finally { |
| myInProgress = false; |
| myActiveEvents.clear(); |
| myEventBeingProcessed = null; |
| } |
| updateLastTopLeftCornerOffset(); |
| for (SoftWrapAwareDocumentParsingListener listener : myListeners) { |
| listener.recalculationEnds(); |
| } |
| return true; |
| } |
| |
| private void recalculateSoftWraps(IncrementalCacheUpdateEvent event) { |
| event.updateNewOffsetsIfNecessary(myEditor); |
| |
| //CachingSoftWrapDataMapper.log("xxxxxxxxxxxxxx Processing soft wraps for " + event + ". Document length: " + myEditor.getDocument().getTextLength() |
| // + ", document: " + System.identityHashCode(myEditor.getDocument())); |
| //long start; |
| //start = System.currentTimeMillis(); |
| notifyListenersOnCacheUpdateStart(event); |
| //CachingSoftWrapDataMapper.log("xxxxxxxxxxxxxxx Listeners notification on start is complete in " + (System.currentTimeMillis() - start) + " ms"); |
| |
| boolean normalCompletion = true; |
| try { |
| //start = System.currentTimeMillis(); |
| normalCompletion = doRecalculateSoftWraps(event); |
| //CachingSoftWrapDataMapper.log("xxxxxxxxxxxxxxxxx Processing is complete in " + (System.currentTimeMillis() - start) + " ms"); |
| } |
| finally { |
| //start = System.currentTimeMillis(); |
| notifyListenersOnCacheUpdateEnd(event, normalCompletion); |
| //CachingSoftWrapDataMapper.log( |
| // "xxxxxxxxxxxxxxxxxxx Listeners notification on end is complete in " + (System.currentTimeMillis() - start) |
| // + " ms. Processing finished " + (normalCompletion ? "normally" : "non-normally") |
| //); |
| } |
| } |
| |
| private boolean doRecalculateSoftWraps(IncrementalCacheUpdateEvent event) { |
| // Preparation. |
| myContext.reset(); |
| myOffset2fontType.clear(); |
| myOffset2widthInPixels.clear(); |
| |
| // Define start of the visual line that holds target range start. |
| int start = event.getNewStartOffset(); |
| |
| final LogicalPosition logical; |
| final Point point; |
| |
| if (start == 0 && myEditor.getPrefixTextWidthInPixels() <= 0) { |
| logical = new LogicalPosition(0, 0, 0, 0, 0, 0, 0); |
| point = new Point(0, 0); |
| } |
| else { |
| logical = myDataMapper.offsetToLogicalPosition(start); |
| VisualPosition visual = new VisualPosition( |
| myDataMapper.logicalToVisualPosition(logical, myEditor.logicalToVisualPosition(logical, false)).line, |
| 0 |
| ); |
| point = myEditor.visualPositionToXY(visual); |
| start = myEditor.logicalPositionToOffset(logical); |
| } |
| |
| Document document = myEditor.getDocument(); |
| myContext.text = document.getCharsSequence(); |
| myContext.tokenStartOffset = start; |
| IterationState iterationState = new IterationState(myEditor, start, document.getTextLength(), false); |
| TextAttributes attributes = iterationState.getMergedAttributes(); |
| myContext.fontType = attributes.getFontType(); |
| myContext.rangeEndOffset = event.getNewEndOffset(); |
| |
| EditorPosition position = new EditorPosition(logical, start, myEditor); |
| position.x = point.x; |
| int spaceWidth = EditorUtil.getSpaceWidth(myContext.fontType, myEditor); |
| int plainSpaceWidth = EditorUtil.getSpaceWidth(Font.PLAIN, myEditor); |
| |
| myContext.logicalLineData.update(logical.line, spaceWidth, plainSpaceWidth); |
| |
| myContext.currentPosition = position; |
| myContext.lineStartPosition = position.clone(); |
| myContext.fontType2spaceWidth.put(myContext.fontType, spaceWidth); |
| myContext.softWrapStartOffset = position.offset; |
| |
| myContext.contentComponent = myEditor.getContentComponent(); |
| myContext.reservedWidthInPixels = myPainter.getMinDrawingWidth(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED); |
| |
| // Perform soft wraps calculation. |
| while (!iterationState.atEnd() && myContext.currentPosition.offset <= event.getNewEndOffset()) { |
| FoldRegion currentFold = iterationState.getCurrentFold(); |
| if (currentFold == null) { |
| myContext.tokenEndOffset = iterationState.getEndOffset(); |
| processNonFoldToken(); |
| } |
| else { |
| boolean continueProcessing = processCollapsedFoldRegion(currentFold); |
| if (!continueProcessing) { |
| return false; |
| } |
| |
| // 'myOffset2widthInPixels' contains information necessary to processing soft wraps that lay before the current offset. |
| // We do know that soft wraps are not allowed to go backward after processed collapsed fold region, hence, we drop |
| // information about processed symbols width. |
| myOffset2widthInPixels.clear(); |
| } |
| |
| iterationState.advance(); |
| attributes = iterationState.getMergedAttributes(); |
| myContext.fontType = attributes.getFontType(); |
| myContext.tokenStartOffset = iterationState.getStartOffset(); |
| myOffset2fontType.fill(myContext.tokenStartOffset, iterationState.getEndOffset(), myContext.fontType); |
| } |
| notifyListenersOnVisualLineEnd(); |
| return true; |
| } |
| |
| /** |
| * Encapsulates logic of processing given collapsed fold region. |
| * |
| * @param foldRegion target collapsed fold region to process |
| * @return <code>true</code> if processing should be continued; <code>false</code> otherwise |
| */ |
| @SuppressWarnings("MagicConstant") |
| private boolean processCollapsedFoldRegion(FoldRegion foldRegion) { |
| if (processOutOfDateFoldRegion(foldRegion)) { |
| return false; |
| } |
| |
| String placeholder = foldRegion.getPlaceholderText(); |
| if (placeholder.isEmpty()) { |
| return true; |
| } |
| int placeholderWidthInPixels = 0; |
| for (int i = 0; i < placeholder.length(); i++) { |
| placeholderWidthInPixels += SoftWrapModelImpl.getEditorTextRepresentationHelper(myEditor) |
| .charWidth(placeholder.charAt(i), myContext.fontType); |
| } |
| int newX = myContext.currentPosition.x + placeholderWidthInPixels; |
| |
| notifyListenersOnVisualLineStart(myContext.lineStartPosition); |
| |
| if (!myContext.exceedsVisualEdge(newX) || myContext.currentPosition.offset == myContext.lineStartPosition.offset) { |
| myContext.advance(foldRegion, placeholderWidthInPixels); |
| return true; |
| } |
| |
| myContext.logicalLineData.update(foldRegion.getStartOffset()); |
| |
| SoftWrap softWrap; |
| if (myContext.exceedsVisualEdge(myContext.currentPosition.x + myContext.reservedWidthInPixels)) { |
| softWrap = registerSoftWrap( |
| myContext.softWrapStartOffset, myContext.tokenStartOffset, myContext.tokenStartOffset, myContext.getSpaceWidth(), |
| myContext.logicalLineData |
| ); |
| } |
| else { |
| softWrap = registerSoftWrap(foldRegion.getStartOffset(), myContext.getSpaceWidth(), myContext.logicalLineData); |
| } |
| |
| if (softWrap == null) { |
| // If we're here that means that we can't find appropriate soft wrap offset before the fold region. |
| // However, we expect that it's always possible to wrap collapsed fold region placeholder text |
| softWrap = registerSoftWrap(myContext.tokenStartOffset, myContext.getSpaceWidth(), myContext.logicalLineData); |
| } |
| myContext.softWrapStartOffset = softWrap.getStart(); |
| if (softWrap.getStart() < myContext.tokenStartOffset) { |
| revertListeners(softWrap.getStart(), myContext.currentPosition.visualLine); |
| for (int j = foldRegion.getStartOffset() - 1; j >= softWrap.getStart(); j--) { |
| int pixelsDiff = myOffset2widthInPixels.data[j - myOffset2widthInPixels.anchor]; |
| int columnsDiff = calculateWidthInColumns(myContext.text.charAt(j), pixelsDiff, myContext.getPlainSpaceWidth()); |
| myContext.currentPosition.offset--; |
| myContext.currentPosition.logicalColumn -= columnsDiff; |
| myContext.currentPosition.visualColumn -= columnsDiff; |
| } |
| } |
| notifyListenersOnSoftWrapLineFeed(true); |
| |
| myContext.currentPosition.visualColumn = 0; |
| myContext.currentPosition.softWrapColumnDiff = myContext.currentPosition.visualColumn - myContext.currentPosition.foldingColumnDiff |
| - myContext.currentPosition.logicalColumn; |
| myContext.currentPosition.softWrapLinesCurrent++; |
| myContext.currentPosition.visualLine++; |
| notifyListenersOnSoftWrapLineFeed(false); |
| |
| myContext.currentPosition.x = softWrap.getIndentInPixels(); |
| myContext.currentPosition.visualColumn = softWrap.getIndentInColumns(); |
| myContext.currentPosition.softWrapColumnDiff += softWrap.getIndentInColumns(); |
| |
| for (int j = softWrap.getStart(); j < myContext.tokenStartOffset; j++) { |
| char c = myContext.text.charAt(j); |
| newX = calculateNewX(c); |
| myContext.onNonLineFeedSymbol(c, newX); |
| } |
| myOffset2fontType.clear(); |
| myContext.advance(foldRegion, placeholderWidthInPixels); |
| return true; |
| } |
| |
| /** |
| * There is a possible case that user just removed text that contained fold region and fold model is not updated yet. |
| * <p/> |
| * This method encapsulates logic for checking and reacting on such a situation. |
| * |
| * @param foldRegion fold region that may be out-of-date |
| * @return <code>true</code> if given fold region is really out-of-date and processing should be stopped; |
| * <code>false</code> otherwise; |
| */ |
| private boolean processOutOfDateFoldRegion(FoldRegion foldRegion) { |
| |
| Document document = myEditor.getDocument(); |
| |
| // Update to the bottom of the document because it looks that fold model is in inconsistent state now and there is a possible |
| // case that offsets of the trailing fold regions should be updated as well. |
| IncrementalCacheUpdateEvent newEvent = new IncrementalCacheUpdateEvent(document); |
| |
| if (!foldRegion.isValid() || myContext.tokenStartOffset != foldRegion.getStartOffset()) { |
| myEventsStorage.add(document, newEvent); |
| return true; |
| } |
| |
| if (foldRegion.getEndOffset() <= document.getTextLength()) { |
| return false; |
| } |
| // There is a possible case that user just removed text that contained fold region and fold model is not updated yet |
| myEventsStorage.add(document, newEvent); |
| return true; |
| } |
| |
| //private static int normalizedOffset(int offset, Document document) { |
| // int textLength = document.getTextLength(); |
| // if (offset > document.getTextLength()) { |
| // offset = textLength - 1; |
| // } |
| // if (offset < 0) { |
| // return 0; |
| // } |
| // return offset; |
| //} |
| |
| /** |
| * Encapsulates logic of processing target non-fold region token defined by the {@link #myContext current processing context} |
| * (target token start offset is identified by {@link ProcessingContext#tokenStartOffset}; end offset is stored |
| * at {@link ProcessingContext#tokenEndOffset}). |
| * <p/> |
| * <code>'Token'</code> here stands for the number of subsequent symbols that are represented using the same font by IJ editor. |
| */ |
| private void processNonFoldToken() { |
| int limit = 3 * (myContext.tokenEndOffset - myContext.lineStartPosition.offset); |
| int counter = 0; |
| int startOffset = myContext.currentPosition.offset; |
| while (myContext.currentPosition.offset < myContext.tokenEndOffset) { |
| if (counter++ > limit) { |
| String editorInfo = myEditor instanceof EditorImpl ? ((EditorImpl)myEditor).dumpState() : myEditor.getClass().toString(); |
| LogMessageEx.error(LOG, "Cycled soft wraps recalculation detected", String.format( |
| "Start recalculation offset: %d, visible area width: %d, calculation context: %s, editor info: %s", |
| startOffset, myVisibleAreaWidth, myContext, editorInfo)); |
| for (int i = myContext.currentPosition.offset; i < myContext.tokenEndOffset; i++) { |
| char c = myContext.text.charAt(i); |
| if (c == '\n') { |
| myContext.onNewLine(); |
| } |
| else { |
| myContext.onNonLineFeedSymbol(c); |
| } |
| } |
| return; |
| } |
| int offset = myContext.currentPosition.offset; |
| if (offset > myContext.rangeEndOffset) { |
| return; |
| } |
| |
| if (myContext.delayedSoftWrap != null && myContext.delayedSoftWrap.getStart() == offset) { |
| processSoftWrap(myContext.delayedSoftWrap); |
| myContext.delayedSoftWrap = null; |
| } |
| |
| char c = myContext.text.charAt(offset); |
| if (c == '\n') { |
| myContext.onNewLine(); |
| continue; |
| } |
| |
| if (myContext.skipToLineEnd) { |
| myContext.skipToLineEnd = false; // Assuming that this flag is set if no soft wrap is registered during processing the call below |
| createSoftWrapIfPossible(); |
| continue; |
| } |
| |
| int newX = offsetToX(offset, c); |
| if (myContext.exceedsVisualEdge(newX) && myContext.delayedSoftWrap == null) { |
| createSoftWrapIfPossible(); |
| } |
| else { |
| myContext.onNonLineFeedSymbol(c, newX); |
| } |
| } |
| } |
| |
| /** |
| * Allows to retrieve 'x' coordinate of the right edge of document symbol referenced by the given offset. |
| * |
| * @param offset target symbol offset |
| * @param c target symbol referenced by the given offset |
| * @return 'x' coordinate of the right edge of document symbol referenced by the given offset |
| */ |
| private int offsetToX(int offset, char c) { |
| if (myOffset2widthInPixels.end > offset |
| && (myOffset2widthInPixels.anchor + myOffset2widthInPixels.end > offset) |
| && myContext.currentPosition.symbol != '\t'/*we need to recalculate tabulation width after soft wrap*/) |
| { |
| return myContext.currentPosition.x + myOffset2widthInPixels.data[offset - myOffset2widthInPixels.anchor]; |
| } |
| else { |
| return calculateNewX(c); |
| } |
| } |
| |
| @SuppressWarnings("MagicConstant") |
| private void createSoftWrapIfPossible() { |
| final int offset = myContext.currentPosition.offset; |
| myContext.logicalLineData.update(offset); |
| int softWrapStartOffset = myContext.softWrapStartOffset; |
| int preferredOffset = Math.max(softWrapStartOffset, offset - 1 /* reserve a column for the soft wrap sign */); |
| SoftWrap softWrap = registerSoftWrap( |
| softWrapStartOffset, |
| preferredOffset, |
| calculateSoftWrapEndOffset(softWrapStartOffset, myContext.logicalLineData.endLineOffset), |
| myContext.getSpaceWidth(), |
| myContext.logicalLineData |
| ); |
| boolean revertedToFoldRegion = false; |
| if (softWrap == null) { |
| EditorPosition wrapPosition = null; |
| |
| // Try to insert soft wrap after the last collapsed fold region that is located on the current visual line. |
| if (myContext.lastFoldEndPosition != null && myStorage.getSoftWrap(myContext.lastFoldEndPosition.offset) == null) { |
| wrapPosition = myContext.lastFoldEndPosition; |
| } |
| |
| if (wrapPosition == null && myContext.lastFoldStartPosition != null |
| && myStorage.getSoftWrap(myContext.lastFoldStartPosition.offset) == null |
| && myContext.lastFoldStartPosition.offset < myContext.currentPosition.offset) |
| { |
| wrapPosition = myContext.lastFoldStartPosition; |
| } |
| |
| if (wrapPosition != null){ |
| revertListeners(wrapPosition.offset, wrapPosition.visualLine); |
| myContext.currentPosition = wrapPosition; |
| softWrap = registerSoftWrap(wrapPosition.offset, myContext.getSpaceWidth(), myContext.logicalLineData); |
| myContext.tokenStartOffset = wrapPosition.offset; |
| revertedToFoldRegion = true; |
| } |
| else { |
| myContext.tryToShiftToNextLine(); |
| myHasLinesWithFailedWrap = true; |
| return; |
| } |
| } |
| |
| myContext.skipToLineEnd = false; |
| |
| notifyListenersOnVisualLineStart(myContext.lineStartPosition); |
| int actualSoftWrapOffset = softWrap.getStart(); |
| |
| // There are three possible options: |
| // 1. Soft wrap offset is located before the current offset; |
| // 2. Soft wrap offset is located after the current offset but doesn't exceed current token end offset |
| // (it may occur if there are no convenient wrap positions before the current offset); |
| // 3. Soft wrap offset is located after the current offset and exceeds current token end offset; |
| // We should process that accordingly. |
| if (actualSoftWrapOffset > myContext.tokenEndOffset) { |
| //CachingSoftWrapDataMapper.log(String.format( |
| // "Avoiding creating soft wrap on detected overflow on offset %d. Reason: soft wrap position (%d) lays beyond of the " + |
| // "recalculation offset (%d). Marked soft wrap as delayed (%s)", myContext.currentPosition.offset, actualSoftWrapOffset, |
| // myContext.endOffset, softWrap) |
| //); |
| myContext.delayedSoftWrap = softWrap; |
| myContext.onNonLineFeedSymbol(myContext.text.charAt(offset)); |
| return; |
| } |
| else if (actualSoftWrapOffset < offset) { |
| if (!revertedToFoldRegion) { |
| revertListeners(actualSoftWrapOffset, myContext.currentPosition.visualLine); |
| for (int j = offset - 1; j >= actualSoftWrapOffset; j--) { |
| int pixelsDiff = myOffset2widthInPixels.data[j - myOffset2widthInPixels.anchor]; |
| int columnsDiff = calculateWidthInColumns(myContext.text.charAt(j), pixelsDiff, myContext.getPlainSpaceWidth()); |
| myContext.currentPosition.offset--; |
| myContext.currentPosition.logicalColumn -= columnsDiff; |
| myContext.currentPosition.visualColumn -= columnsDiff; |
| myContext.currentPosition.x -= pixelsDiff; |
| } |
| } |
| } |
| else if (actualSoftWrapOffset > offset) { |
| myContext.onNonLineFeedSymbol(myContext.text.charAt(offset)); |
| for (int j = offset + 1; j < actualSoftWrapOffset; j++) { |
| myContext.onNonLineFeedSymbol(myContext.text.charAt(offset)); |
| } |
| } |
| |
| processSoftWrap(softWrap); |
| myContext.currentPosition.offset = actualSoftWrapOffset; |
| myOffset2fontType.clear(); |
| myOffset2widthInPixels.clear(); |
| |
| if (revertedToFoldRegion && myContext.currentPosition.offset == myContext.lastFold.getStartOffset()) { |
| processCollapsedFoldRegion(myContext.lastFold); |
| } |
| } |
| |
| private int calculateNewX(char c) { |
| if (c == '\t') { |
| return EditorUtil.nextTabStop(myContext.currentPosition.x, myEditor); |
| } |
| else { |
| return myContext.currentPosition.x + SoftWrapModelImpl.getEditorTextRepresentationHelper(myEditor).charWidth(c, myContext.fontType); |
| //FontInfo fontInfo = EditorUtil.fontForChar(c, myContext.fontType, myEditor); |
| //return myContext.currentPosition.x + fontInfo.charWidth(c, myContext.contentComponent); |
| } |
| } |
| |
| private int calculateSoftWrapEndOffset(int start, int end) { |
| CharSequence text = myEditor.getDocument().getCharsSequence(); |
| for (int i = start; i < end; i++) { |
| char c = text.charAt(i); |
| if (c == '\n') { |
| return i; |
| } |
| } |
| return Math.max(start, end); |
| } |
| |
| private static int calculateWidthInColumns(char c, int widthInPixels, int plainSpaceWithInPixels) { |
| if (c != '\t') { |
| return 1; |
| } |
| int result = widthInPixels / plainSpaceWithInPixels; |
| if (widthInPixels % plainSpaceWithInPixels > 0) { |
| result++; |
| } |
| return result; |
| } |
| |
| /** |
| * This method is assumed to be called in a situation when visible area width is exceeded. It tries to create and register |
| * new soft wrap which data is defined in accordance with the given parameters. |
| * <p/> |
| * There is a possible case that no soft wrap is created and registered. That is true, for example, for a situation when |
| * we have a long line of text that doesn't contain white spaces, operators or any other symbols that may be used |
| * as a <code>'wrap points'</code>. We just left such lines as-is. |
| * |
| * @param minOffset min line <code>'wrap point'</code> offset |
| * @param preferredOffset preferred <code>'wrap point'</code> offset, i.e. max offset which symbol doesn't exceed right margin |
| * @param maxOffset max line <code>'wrap point'</code> offset |
| * @param spaceSize current space width in pixels |
| * @param lineData object that encapsulates information about currently processed logical line |
| * @return newly created and registered soft wrap if any; <code>null</code> otherwise |
| */ |
| @Nullable |
| private SoftWrap registerSoftWrap(int minOffset, int preferredOffset, int maxOffset, int spaceSize, LogicalLineData lineData) { |
| int softWrapOffset = calculateBackwardSpaceOffsetIfPossible(minOffset, preferredOffset); |
| if (softWrapOffset < 0) { |
| softWrapOffset = calculateBackwardOffsetForEasternLanguageIfPossible(minOffset, preferredOffset); |
| } |
| if (softWrapOffset < 0) { |
| Document document = myEditor.getDocument(); |
| |
| // Performance optimization implied by profiling results analysis. |
| if (myLineWrapPositionStrategy == null) { |
| myLineWrapPositionStrategy = LanguageLineWrapPositionStrategy.INSTANCE.forEditor(myEditor); |
| } |
| |
| softWrapOffset = myLineWrapPositionStrategy.calculateWrapPosition( |
| document, myEditor.getProject(), minOffset, maxOffset, preferredOffset, true, true |
| ); |
| } |
| |
| if (softWrapOffset >= lineData.endLineOffset || softWrapOffset < 0 |
| || (myCustomIndentUsedLastTime && softWrapOffset == lineData.nonWhiteSpaceSymbolOffset) |
| || (softWrapOffset > preferredOffset && myContext.lastFoldStartPosition != null // Prefer to wrap on fold region backwards |
| && myContext.lastFoldStartPosition.offset <= preferredOffset)) // to wrapping forwards. |
| { |
| return null; |
| } |
| |
| return registerSoftWrap(softWrapOffset, spaceSize, lineData); |
| } |
| |
| @NotNull |
| private SoftWrap registerSoftWrap(int offset, int spaceSize, LogicalLineData lineData) { |
| int indentInColumns = 0; |
| int indentInPixels = myPainter.getMinDrawingWidth(SoftWrapDrawingType.AFTER_SOFT_WRAP); |
| if (myCustomIndentUsedLastTime) { |
| indentInColumns = myCustomIndentValueUsedLastTime + lineData.indentInColumns; |
| indentInPixels += lineData.indentInPixels + (myCustomIndentValueUsedLastTime * spaceSize); |
| } |
| SoftWrapImpl result = new SoftWrapImpl( |
| new TextChangeImpl("\n" + StringUtil.repeatSymbol(' ', indentInColumns), offset, offset), |
| indentInColumns + 1/* for 'after soft wrap' drawing */, |
| indentInPixels |
| ); |
| myStorage.storeOrReplace(result, true); |
| return result; |
| } |
| |
| /** |
| * It was found out that frequent soft wrap position calculation may become performance bottleneck (e.g. consider application |
| * that is run under IJ and writes long strings to stdout non-stop. If those strings are long enough to be soft-wrapped, |
| * we have the mentioned situation). |
| * <p/> |
| * Hence, we introduce an optimization here - try to find offset of white space symbol that belongs to the target interval and |
| * use its offset as soft wrap position. |
| * |
| * @param minOffset min offset to use (inclusive) |
| * @param preferredOffset max offset to use (inclusive) |
| * @return offset of the space symbol that belongs to <code>[minOffset; preferredOffset]</code> interval if any; |
| * <code>'-1'</code> otherwise |
| */ |
| private int calculateBackwardSpaceOffsetIfPossible(int minOffset, int preferredOffset) { |
| // There is a possible case that we have a long line that contains many non-white space symbols eligible for performing |
| // soft wrap that are preceded by white space symbol. We don't want to create soft wrap that is located so far from the |
| // preferred position then, hence, we check white space symbol existence not more than specific number of symbols back. |
| int maxTrackBackSymbolsNumber = 10; |
| int minOffsetToUse = minOffset; |
| if (preferredOffset - minOffset > maxTrackBackSymbolsNumber) { |
| minOffsetToUse = preferredOffset - maxTrackBackSymbolsNumber; |
| } |
| for (int i = preferredOffset - 1; i >= minOffsetToUse; i--) { |
| char c = myContext.text.charAt(i); |
| if (c == ' ') { |
| return i + 1; |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * There is a possible case that current line holds eastern language symbols (e.g. japanese text). We want to allow soft |
| * wrap just after such symbols and this method encapsulates the logic that tries to calculate soft wraps offset on that basis. |
| * |
| * @param minOffset min offset to use (inclusive) |
| * @param preferredOffset max offset to use (inclusive) |
| * @return soft wrap offset that belongs to <code>[minOffset; preferredOffset]</code> interval if any; |
| * <code>'-1'</code> otherwise |
| */ |
| public int calculateBackwardOffsetForEasternLanguageIfPossible(int minOffset, int preferredOffset) { |
| // There is a possible case that we have a long line that contains many non-white space symbols eligible for performing |
| // soft wrap that are preceded by white space symbol. We don't want to create soft wrap that is located so far from the |
| // preferred position then, hence, we check white space symbol existence not more than specific number of symbols back. |
| int maxTrackBackSymbolsNumber = 10; |
| int minOffsetToUse = minOffset; |
| if (preferredOffset - minOffset > maxTrackBackSymbolsNumber) { |
| minOffsetToUse = preferredOffset - maxTrackBackSymbolsNumber; |
| } |
| for (int i = preferredOffset - 1; i >= minOffsetToUse; i--) { |
| char c = myContext.text.charAt(i); |
| if (c >= 0x2f00) { // Check this document for eastern languages unicode ranges - http://www.unicode.org/charts |
| return i + 1; |
| } |
| } |
| return -1; |
| } |
| |
| private void processSoftWrap(SoftWrap softWrap) { |
| notifyListenersOnSoftWrapLineFeed(true); |
| |
| EditorPosition position = myContext.currentPosition; |
| position.visualColumn = 0; |
| position.softWrapColumnDiff = position.visualColumn - position.foldingColumnDiff - position.logicalColumn; |
| position.softWrapLinesCurrent++; |
| position.visualLine++; |
| notifyListenersOnSoftWrapLineFeed(false); |
| myContext.lineStartPosition.from(myContext.currentPosition); |
| |
| position.x = softWrap.getIndentInPixels(); |
| position.visualColumn = softWrap.getIndentInColumns(); |
| position.softWrapColumnDiff += softWrap.getIndentInColumns(); |
| |
| myContext.softWrapStartOffset = softWrap.getStart() + 1; |
| } |
| |
| /** |
| * There is a possible case that we need to reparse the whole document (e.g. visible area width is changed or user-defined |
| * soft wrap indent is changed etc). This method encapsulates that logic, i.e. it checks if necessary conditions are satisfied |
| * and updates internal state as necessary. |
| * |
| * @return <code>true</code> if re-calculation logic was performed; |
| * <code>false</code> otherwise (e.g. we need to perform re-calculation but current editor is now shown, i.e. we don't |
| * have information about viewport width |
| */ |
| public boolean recalculateIfNecessary() { |
| if (myInProgress) { |
| return false; |
| } |
| |
| // Check if we need to recalculate soft wraps due to indent settings change. |
| boolean indentChanged = false; |
| IndentType currentIndentType = getIndentToUse(); |
| boolean useCustomIndent = currentIndentType == IndentType.CUSTOM; |
| int currentCustomIndent = myEditor.getSettings().getCustomSoftWrapIndent(); |
| if (useCustomIndent ^ myCustomIndentUsedLastTime || (useCustomIndent && myCustomIndentValueUsedLastTime != currentCustomIndent)) { |
| indentChanged = true; |
| } |
| myCustomIndentUsedLastTime = useCustomIndent; |
| myCustomIndentValueUsedLastTime = currentCustomIndent; |
| |
| // Check if we need to recalculate soft wraps due to visible area width change. |
| int currentVisibleAreaWidth = myWidthProvider.getVisibleAreaWidth(); |
| if (!indentChanged && myVisibleAreaWidth == currentVisibleAreaWidth) { |
| return recalculateSoftWraps(); // Recalculate existing dirty regions if any. |
| } |
| |
| final JScrollBar scrollBar = myEditor.getScrollPane().getVerticalScrollBar(); |
| if (myVerticalScrollBarWidth < 0) { |
| myVerticalScrollBarWidth = scrollBar.getWidth(); |
| if (myVerticalScrollBarWidth <= 0) { |
| myVerticalScrollBarWidth = scrollBar.getPreferredSize().width; |
| } |
| } |
| |
| // We experienced the following situation: |
| // 1. Editor is configured to show scroll bars only when necessary; |
| // 2. Editor with active soft wraps is changed in order for the vertical scroll bar to appear; |
| // 3. Vertical scrollbar consumes vertical space, hence, soft wraps are recalculated because of the visual area width change; |
| // 4. Newly recalculated soft wraps trigger editor size update; |
| // 5. Editor size update starts scroll pane update which, in turn, disables vertical scroll bar at first (the reason for that |
| // lays somewhere at the swing depth); |
| // 6. Soft wraps are recalculated because of visible area width change caused by the disabled vertical scroll bar; |
| // 7. Go to the step 4; |
| // I.e. we have an endless EDT activity that stops only when editor is re-sized in a way to avoid vertical scroll bar. |
| // That's why we don't recalculate soft wraps when visual area width is changed to the vertical scroll bar width value assuming |
| // that such a situation is triggered by the scroll bar (dis)appearance. |
| if (Math.abs(currentVisibleAreaWidth - myVisibleAreaWidth) == myVerticalScrollBarWidth) { |
| myVisibleAreaWidth = currentVisibleAreaWidth; |
| return recalculateSoftWraps(); |
| } |
| |
| // We want to adjust viewport's 'y' coordinate on complete recalculation, so, we remember number of soft-wrapped lines |
| // before the target offset on recalculation start and compare it with the number of soft-wrapped lines before the same offset |
| // after the recalculation. |
| int softWrapsBefore = -1; |
| final ScrollingModelEx scrollingModel = myEditor.getScrollingModel(); |
| int yScrollOffset = scrollingModel.getVerticalScrollOffset(); |
| int anchorOffset = myLastTopLeftCornerOffset; |
| if (anchorOffset >= 0) { |
| softWrapsBefore = getNumberOfSoftWrapsBefore(anchorOffset); |
| } |
| |
| // Drop information about processed lines. |
| reset(); |
| myStorage.removeAll(); |
| myVisibleAreaWidth = currentVisibleAreaWidth; |
| final boolean result = recalculateSoftWraps(); |
| if (!result) { |
| return false; |
| } |
| |
| // Adjust viewport's 'y' coordinate if necessary. |
| if (softWrapsBefore >= 0) { |
| int softWrapsNow = getNumberOfSoftWrapsBefore(anchorOffset); |
| if (softWrapsNow != softWrapsBefore) { |
| scrollingModel.disableAnimation(); |
| try { |
| scrollingModel.scrollVertically(yScrollOffset + (softWrapsNow - softWrapsBefore) * myEditor.getLineHeight()); |
| } |
| finally { |
| scrollingModel.enableAnimation(); |
| } |
| } |
| } |
| updateLastTopLeftCornerOffset(); |
| return true; |
| } |
| |
| private void updateLastTopLeftCornerOffset() { |
| final LogicalPosition logicalPosition = myEditor.visualToLogicalPosition( |
| new VisualPosition(1 + myEditor.getScrollingModel().getVisibleArea().y / myEditor.getLineHeight(), 0) |
| ); |
| myLastTopLeftCornerOffset = myEditor.logicalPositionToOffset(logicalPosition); |
| } |
| |
| private int getNumberOfSoftWrapsBefore(int offset) { |
| final int i = myStorage.getSoftWrapIndex(offset); |
| return i >= 0 ? i : -i - 1; |
| } |
| |
| private IndentType getIndentToUse() { |
| return myEditor.getSettings().isUseCustomSoftWrapIndent() ? IndentType.CUSTOM : IndentType.NONE; |
| } |
| |
| /** |
| * Registers given listener within the current manager. |
| * |
| * @param listener listener to register |
| * @return <code>true</code> if this collection changed as a result of the call; <code>false</code> otherwise |
| */ |
| public boolean addListener(@NotNull SoftWrapAwareDocumentParsingListener listener) { |
| return myListeners.add(listener); |
| } |
| |
| public boolean removeListener(@NotNull SoftWrapAwareDocumentParsingListener listener) { |
| return myListeners.remove(listener); |
| } |
| |
| @SuppressWarnings({"ForLoopReplaceableByForEach"}) |
| private void revertListeners(int offset, int visualLine) { |
| for (int i = 0; i < myListeners.size(); i++) { |
| // Avoid unnecessary Iterator object construction as this method is expected to be called frequently. |
| SoftWrapAwareDocumentParsingListener listener = myListeners.get(i); |
| listener.revertToOffset(offset, visualLine); |
| } |
| } |
| |
| @SuppressWarnings({"ForLoopReplaceableByForEach"}) |
| private void notifyListenersOnFoldRegion(@NotNull FoldRegion foldRegion, int collapsedFoldingWidthInColumns, int visualLine) { |
| for (int i = 0; i < myListeners.size(); i++) { |
| // Avoid unnecessary Iterator object construction as this method is expected to be called frequently. |
| SoftWrapAwareDocumentParsingListener listener = myListeners.get(i); |
| listener.onCollapsedFoldRegion(foldRegion, collapsedFoldingWidthInColumns, visualLine); |
| } |
| } |
| |
| @SuppressWarnings({"ForLoopReplaceableByForEach"}) |
| private void notifyListenersOnVisualLineStart(@NotNull EditorPosition position) { |
| for (int i = 0; i < myListeners.size(); i++) { |
| // Avoid unnecessary Iterator object construction as this method is expected to be called frequently. |
| SoftWrapAwareDocumentParsingListener listener = myListeners.get(i); |
| listener.onVisualLineStart(position); |
| } |
| } |
| |
| @SuppressWarnings({"ForLoopReplaceableByForEach"}) |
| private void notifyListenersOnVisualLineEnd() { |
| for (int i = 0; i < myListeners.size(); i++) { |
| // Avoid unnecessary Iterator object construction as this method is expected to be called frequently. |
| SoftWrapAwareDocumentParsingListener listener = myListeners.get(i); |
| listener.onVisualLineEnd(myContext.currentPosition); |
| } |
| } |
| |
| @SuppressWarnings({"ForLoopReplaceableByForEach"}) |
| private void notifyListenersOnTabulation(int widthInColumns) { |
| for (int i = 0; i < myListeners.size(); i++) { |
| // Avoid unnecessary Iterator object construction as this method is expected to be called frequently. |
| SoftWrapAwareDocumentParsingListener listener = myListeners.get(i); |
| listener.onTabulation(myContext.currentPosition, widthInColumns); |
| } |
| } |
| |
| @SuppressWarnings({"ForLoopReplaceableByForEach"}) |
| private void notifyListenersOnSoftWrapLineFeed(boolean before) { |
| for (int i = 0; i < myListeners.size(); i++) { |
| // Avoid unnecessary Iterator object construction as this method is expected to be called frequently. |
| SoftWrapAwareDocumentParsingListener listener = myListeners.get(i); |
| if (before) { |
| listener.beforeSoftWrapLineFeed(myContext.currentPosition); |
| } |
| else { |
| listener.afterSoftWrapLineFeed(myContext.currentPosition); |
| } |
| } |
| } |
| |
| @SuppressWarnings({"ForLoopReplaceableByForEach"}) |
| private void notifyListenersOnCacheUpdateStart(IncrementalCacheUpdateEvent event) { |
| for (int i = 0; i < myListeners.size(); i++) { |
| // Avoid unnecessary Iterator object construction as this method is expected to be called frequently. |
| SoftWrapAwareDocumentParsingListener listener = myListeners.get(i); |
| listener.onCacheUpdateStart(event); |
| } |
| } |
| |
| @SuppressWarnings({"ForLoopReplaceableByForEach"}) |
| private void notifyListenersOnCacheUpdateEnd(IncrementalCacheUpdateEvent event, boolean normal) { |
| for (int i = 0; i < myListeners.size(); i++) { |
| // Avoid unnecessary Iterator object construction as this method is expected to be called frequently. |
| SoftWrapAwareDocumentParsingListener listener = myListeners.get(i); |
| listener.onRecalculationEnd(event, normal); |
| } |
| } |
| |
| public void onFoldRegionStateChange(int startOffset, int endOffset) { |
| assert ApplicationManagerEx.getApplicationEx().isDispatchThread(); |
| |
| myEventsStorage.add(myEditor.getDocument(), new IncrementalCacheUpdateEvent(myEditor, startOffset, endOffset)); |
| } |
| |
| public void onFoldProcessingEnd() { |
| //CachingSoftWrapDataMapper.log("xxxxxxxxxxx On fold region processing end"); |
| recalculateSoftWraps(); |
| } |
| |
| @Override |
| public void beforeDocumentChange(DocumentEvent event) { |
| myEventsStorage.add(event.getDocument(), new IncrementalCacheUpdateEvent(event, myEditor)); |
| } |
| |
| @Override |
| public void documentChanged(DocumentEvent event) { |
| recalculateIfNecessary(); |
| } |
| |
| public void setWidthProvider(@NotNull VisibleAreaWidthProvider widthProvider) { |
| myWidthProvider = widthProvider; |
| reset(); |
| } |
| |
| @NotNull |
| @Override |
| public String dumpState() { |
| return String.format( |
| "recalculation in progress: %b; stored update events: %s; active update events: %s, event being processed: %s", |
| myInProgress, myEventsStorage, myActiveEvents, myEventBeingProcessed |
| ); |
| } |
| |
| @Override |
| public String toString() { |
| return dumpState(); |
| } |
| |
| @TestOnly |
| public void setSoftWrapPainter(SoftWrapPainter painter) { |
| myPainter = painter; |
| } |
| |
| /** |
| * We need to use correct indent for soft-wrapped lines, i.e. they should be indented to the start of the logical line. |
| * This class stores information about logical line start indent. |
| */ |
| private class LogicalLineData { |
| |
| public int indentInColumns; |
| public int indentInPixels; |
| public int endLineOffset; |
| public int nonWhiteSpaceSymbolOffset; |
| |
| public void update(int logicalLine, int spaceWidth, int plainSpaceWidth) { |
| Document document = myEditor.getDocument(); |
| int startLineOffset; |
| if (logicalLine >= document.getLineCount()) { |
| startLineOffset = endLineOffset = document.getTextLength(); |
| } |
| else { |
| startLineOffset = document.getLineStartOffset(logicalLine); |
| endLineOffset = document.getLineEndOffset(logicalLine); |
| } |
| CharSequence text = document.getCharsSequence(); |
| indentInColumns = 0; |
| indentInPixels = 0; |
| nonWhiteSpaceSymbolOffset = -1; |
| |
| for (int i = startLineOffset; i < endLineOffset; i++) { |
| char c = text.charAt(i); |
| switch (c) { |
| case ' ': indentInColumns += 1; indentInPixels += spaceWidth; break; |
| case '\t': |
| int x = EditorUtil.nextTabStop(indentInPixels, myEditor); |
| indentInColumns += calculateWidthInColumns(c, x - indentInPixels, plainSpaceWidth); |
| indentInPixels = x; |
| break; |
| default: nonWhiteSpaceSymbolOffset = i; return; |
| } |
| } |
| } |
| |
| /** |
| * There is a possible case that all document line symbols before the first soft wrap are white spaces. We don't want to use |
| * such a big indent then. |
| * <p/> |
| * This method encapsulates logic that 'resets' indent to use if such a situation is detected. |
| * |
| * @param softWrapOffset offset of the soft wrap that occurred on document line which data is stored at the current object |
| */ |
| public void update(int softWrapOffset) { |
| if (nonWhiteSpaceSymbolOffset >= 0 && softWrapOffset > nonWhiteSpaceSymbolOffset) { |
| return; |
| } |
| indentInColumns = 0; |
| indentInPixels = 0; |
| } |
| |
| public void reset() { |
| indentInColumns = 0; |
| indentInPixels = 0; |
| endLineOffset = 0; |
| } |
| } |
| |
| /** |
| * This interface is introduced mostly for encapsulating GUI-specific values retrieval and make it possible to write |
| * tests for soft wraps processing. |
| */ |
| public interface VisibleAreaWidthProvider { |
| int getVisibleAreaWidth(); |
| } |
| |
| private static class DefaultVisibleAreaWidthProvider implements VisibleAreaWidthProvider { |
| |
| private final Editor myEditor; |
| |
| DefaultVisibleAreaWidthProvider(Editor editor) { |
| myEditor = editor; |
| } |
| |
| @Override |
| public int getVisibleAreaWidth() { |
| return myEditor.getScrollingModel().getVisibleArea().width; |
| } |
| } |
| |
| /** |
| * Primitive array-based data structure that contain mappings like {@code int -> int}. |
| * <p/> |
| * The key is array index plus anchor; the value is array value. |
| */ |
| private static class WidthsStorage { |
| public int[] data = new int[256]; |
| public int anchor; |
| public int end; |
| |
| public void clear() { |
| anchor = 0; |
| end = 0; |
| } |
| } |
| |
| /** |
| * |
| * We need to be able to track back font types to offsets mappings because text processing may be shifted back because of soft wrap. |
| * <p/> |
| * <b>Example</b> |
| * Suppose with have this line of text that should be soft-wrapped |
| * <pre> |
| * | <- right margin |
| * token1 token2-toke|n3 |
| * | <- right margin |
| * </pre> |
| * It's possible that <code>'token1'</code>, white spaces and <code>'token2'</code> use different font types and |
| * soft wrapping should be performed between <code>'token1'</code> and <code>'token2'</code>. We need to be able to |
| * match offsets of <code>'token2'</code> to font types then. |
| * <p/> |
| * There is an additional trick here - there is a possible case that a bunch number of adjacent symbols use the same font |
| * type (are marked by {@link IterationState} as a single token. That is often the case for plain text). We don't want to |
| * store those huge mappings then (it may take over million records) because it's indicated by profiling as extremely expensive |
| * and causing unnecessary garbage collections that dramatically reduce overall application throughput. |
| * <p/> |
| * Hence, we want to restrict ourselves by storing information about particular sub-sequence of overall token offsets. |
| * <p/> |
| * This is primitive array-based data structure that contains {@code offset -> font type} mappings. |
| */ |
| private static class FontTypesStorage { |
| |
| private int[] myStarts = new int[256]; |
| private int[] myEnds = new int[256]; |
| private int[] myData = new int[256]; |
| private int myLastIndex = -1; |
| |
| public void fill(int start, int end, int value) { |
| if (myLastIndex >= 0 && myData[myLastIndex] == value && myEnds[myLastIndex] == start) { |
| myEnds[myLastIndex] = end; |
| return; |
| } |
| if (++myLastIndex >= myData.length) { |
| expand(); |
| } |
| myStarts[myLastIndex] = start; |
| myEnds[myLastIndex] = end; |
| myData[myLastIndex] = value; |
| } |
| |
| /** |
| * Tries to retrieve stored value for the given offset if any; |
| * |
| * @param offset target offset |
| * @return target value if any is stored; <code>-1</code> otherwise |
| */ |
| public int get(int offset) { |
| // The key is array index plus anchor; the value is array value. |
| if (myLastIndex < 0) { |
| return -1; |
| } |
| for (int i = myLastIndex; i >= 0 && myEnds[i] >= offset; i--) { |
| if (myStarts[i] <= offset) { |
| return myData[i]; |
| } |
| } |
| return -1; |
| } |
| |
| public void clear() { |
| myLastIndex = -1; |
| } |
| |
| private void expand() { |
| int[] tmp = new int[myStarts.length * 2]; |
| System.arraycopy(myStarts, 0, tmp, 0, myStarts.length); |
| myStarts = tmp; |
| |
| tmp = new int[myEnds.length * 2]; |
| System.arraycopy(myEnds, 0, tmp, 0, myEnds.length); |
| myEnds = tmp; |
| |
| tmp = new int[myData.length * 2]; |
| System.arraycopy(myData, 0, tmp, 0, myData.length); |
| myData = tmp; |
| } |
| } |
| |
| private class ProcessingContext { |
| |
| public final PrimitiveIntMap fontType2spaceWidth = new PrimitiveIntMap(); |
| public final LogicalLineData logicalLineData = new LogicalLineData(); |
| |
| public CharSequence text; |
| public EditorPosition lineStartPosition; |
| public EditorPosition currentPosition; |
| /** |
| * Start position of the last collapsed fold region that is located at the current visual line and can be used as a fall back |
| * position for soft wrapping. |
| */ |
| public EditorPosition lastFoldStartPosition; |
| public EditorPosition lastFoldEndPosition; |
| /** A fold region referenced by the {@link #lastFoldStartPosition}. */ |
| public FoldRegion lastFold; |
| public SoftWrap delayedSoftWrap; |
| public JComponent contentComponent; |
| public int reservedWidthInPixels; |
| /** |
| * Min offset to use when new soft wrap should be introduced. I.e. every time we detect that text exceeds visual width, |
| */ |
| public int softWrapStartOffset; |
| public int rangeEndOffset; |
| public int tokenStartOffset; |
| public int tokenEndOffset; |
| @JdkConstants.FontStyle |
| public int fontType; |
| public boolean notifyListenersOnLineStartPosition; |
| public boolean skipToLineEnd; |
| |
| @Override |
| public String toString() { |
| return "reserved width: " + reservedWidthInPixels + ", soft wrap start offset: " + softWrapStartOffset + ", range end offset: " |
| + rangeEndOffset + ", token offsets: [" + tokenStartOffset + "; " + tokenEndOffset + "], font type: " + fontType |
| + ", skip to line end: " + skipToLineEnd + ", delayed soft wrap: " + delayedSoftWrap + ", current position: "+ currentPosition |
| + "line start position: " + lineStartPosition; |
| } |
| |
| public void reset() { |
| text = null; |
| lineStartPosition = null; |
| currentPosition = null; |
| lastFoldStartPosition = null; |
| lastFoldEndPosition = null; |
| lastFold = null; |
| delayedSoftWrap = null; |
| contentComponent = null; |
| reservedWidthInPixels = 0; |
| softWrapStartOffset = 0; |
| rangeEndOffset = 0; |
| tokenStartOffset = 0; |
| tokenEndOffset = 0; |
| fontType = 0; |
| notifyListenersOnLineStartPosition = false; |
| skipToLineEnd = false; |
| fontType2spaceWidth.reset(); |
| logicalLineData.reset(); |
| } |
| |
| public int getSpaceWidth() { |
| return getSpaceWidth(fontType); |
| } |
| |
| public int getPlainSpaceWidth() { |
| return getSpaceWidth(Font.PLAIN); |
| } |
| |
| private int getSpaceWidth(@JdkConstants.FontStyle int fontType) { |
| int result = fontType2spaceWidth.get(fontType); |
| if (result <= 0) { |
| result = EditorUtil.getSpaceWidth(fontType, myEditor); |
| fontType2spaceWidth.put(fontType, result); |
| } |
| assert result > 0; |
| return result; |
| } |
| |
| /** |
| * Asks current context to update its state assuming that it begins to point to the line next to its current position. |
| */ |
| @SuppressWarnings("MagicConstant") |
| public void onNewLine() { |
| notifyListenersOnVisualLineEnd(); |
| currentPosition.onNewLine(); |
| softWrapStartOffset = currentPosition.offset; |
| lastFoldStartPosition = null; |
| lastFoldEndPosition = null; |
| lastFold = null; |
| lineStartPosition.from(currentPosition); |
| logicalLineData.update(currentPosition.logicalLine, getSpaceWidth(), getPlainSpaceWidth()); |
| fontType = myOffset2fontType.get(currentPosition.offset); |
| |
| myOffset2fontType.clear(); |
| myOffset2widthInPixels.clear(); |
| } |
| |
| public void onNonLineFeedSymbol(char c) { |
| int newX; |
| if (myOffset2widthInPixels.end > myContext.currentPosition.offset |
| && (myOffset2widthInPixels.anchor + myOffset2widthInPixels.end > myContext.currentPosition.offset) |
| && myContext.currentPosition.symbol != '\t'/*we need to recalculate tabulation width after soft wrap*/) |
| { |
| newX = myContext.currentPosition.x + myOffset2widthInPixels.data[myContext.currentPosition.offset - myOffset2widthInPixels.anchor]; |
| } |
| else { |
| newX = calculateNewX(c); |
| } |
| onNonLineFeedSymbol(c, newX); |
| } |
| |
| @SuppressWarnings("MagicConstant") |
| public void onNonLineFeedSymbol(char c, int newX) { |
| int widthInPixels = newX - myContext.currentPosition.x; |
| |
| if (myOffset2widthInPixels.anchor <= 0) { |
| myOffset2widthInPixels.anchor = currentPosition.offset; |
| } |
| if (currentPosition.offset - myOffset2widthInPixels.anchor >= myOffset2widthInPixels.data.length) { |
| int newLength = Math.max(myOffset2widthInPixels.data.length * 2, currentPosition.offset - myOffset2widthInPixels.anchor + 1); |
| int[] newData = new int[newLength]; |
| System.arraycopy(myOffset2widthInPixels.data, 0, newData, 0, myOffset2widthInPixels.data.length); |
| myOffset2widthInPixels.data = newData; |
| } |
| myOffset2widthInPixels.data[currentPosition.offset - myOffset2widthInPixels.anchor] = widthInPixels; |
| myOffset2widthInPixels.end++; |
| |
| int widthInColumns = calculateWidthInColumns(c, widthInPixels, myContext.getPlainSpaceWidth()); |
| if (c == '\t') { |
| notifyListenersOnVisualLineStart(myContext.lineStartPosition); |
| notifyListenersOnTabulation(widthInColumns); |
| } |
| |
| currentPosition.logicalColumn += widthInColumns; |
| currentPosition.visualColumn += widthInColumns; |
| currentPosition.x = newX; |
| currentPosition.offset++; |
| fontType = myOffset2fontType.get(currentPosition.offset); |
| } |
| |
| /** |
| * Updates state of the current context object in order to point to the end of the given collapsed fold region. |
| * |
| * @param foldRegion collapsed fold region to process |
| */ |
| private void advance(FoldRegion foldRegion, int placeHolderWidthInPixels) { |
| lastFoldStartPosition = currentPosition.clone(); |
| lastFold = foldRegion; |
| int visualLineBefore = currentPosition.visualLine; |
| int logicalLineBefore = currentPosition.logicalLine; |
| int logicalColumnBefore = currentPosition.logicalColumn; |
| currentPosition.advance(foldRegion, -1); |
| currentPosition.x += placeHolderWidthInPixels; |
| int collapsedFoldingWidthInColumns = currentPosition.logicalColumn; |
| if (currentPosition.logicalLine <= logicalLineBefore) { |
| // Single-line fold region. |
| collapsedFoldingWidthInColumns = currentPosition.logicalColumn - logicalColumnBefore; |
| } |
| else { |
| final DocumentEx document = myEditor.getDocument(); |
| int endFoldLine = document.getLineNumber(foldRegion.getEndOffset()); |
| logicalLineData.endLineOffset = document.getLineEndOffset(endFoldLine); |
| } |
| notifyListenersOnFoldRegion(foldRegion, collapsedFoldingWidthInColumns, visualLineBefore); |
| tokenStartOffset = myContext.currentPosition.offset; |
| softWrapStartOffset = foldRegion.getEndOffset(); |
| lastFoldEndPosition = currentPosition.clone(); |
| } |
| |
| /** |
| * Asks current context to update its state in order to show to the first symbol of the next visual line if it belongs to |
| * [{@link #tokenStartOffset}; {@link #skipToLineEnd} is set to <code>'true'</code> otherwise |
| */ |
| public void tryToShiftToNextLine() { |
| for (int i = currentPosition.offset; i < tokenEndOffset; i++) { |
| char c = text.charAt(i); |
| currentPosition.offset = i; |
| if (c == '\n') { |
| onNewLine(); // Assuming that offset is incremented during this method call |
| skipToLineEnd = false; |
| return; |
| } |
| else { |
| onNonLineFeedSymbol(c, offsetToX(i, c)); |
| } |
| } |
| skipToLineEnd = true; |
| } |
| |
| /** |
| * Allows to answer if point with the given <code>'x'</code> coordinate exceeds visual area's right edge. |
| * |
| * @param x target <code>'x'</code> coordinate to check |
| * @return <code>true</code> if given <code>'x'</code> coordinate exceeds visual area's right edge; <code>false</code> otherwise |
| */ |
| public boolean exceedsVisualEdge(int x) { |
| return x >= myVisibleAreaWidth; |
| } |
| } |
| |
| /** |
| * Primitive data structure to hold {@code int -> int} mappings assuming that the following is true: |
| * <pre> |
| * <ul> |
| * <li>number of entries is small;</li> |
| * <li>the keys are roughly adjacent;</li> |
| * </ul> |
| * </pre> |
| */ |
| private static class PrimitiveIntMap { |
| |
| private int[] myData = new int[16]; |
| private int myShift; |
| |
| public int get(int key) { |
| int index = key + myShift; |
| if (index < 0 || index >= myData.length) { |
| return -1; |
| } |
| return myData[index]; |
| } |
| |
| public void put(int key, int value) { |
| int index = key + myShift; |
| if (index < 0) { |
| int[] tmp = new int[myData.length - index]; |
| System.arraycopy(myData, 0, tmp, -index, myData.length); |
| myData = tmp; |
| myShift -= index; |
| index = 0; |
| } |
| myData[index] = value; |
| } |
| |
| public void reset() { |
| myShift = 0; |
| Arrays.fill(myData, 0); |
| } |
| } |
| } |