/* * Copyright 2000-2015 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; import com.intellij.diagnostic.Dumpable; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.colors.impl.FontPreferencesImpl; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.ex.*; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.editor.impl.softwrap.*; import com.intellij.openapi.editor.impl.softwrap.mapping.CachingSoftWrapDataMapper; import com.intellij.openapi.editor.impl.softwrap.mapping.SoftWrapApplianceManager; import com.intellij.openapi.editor.impl.softwrap.mapping.SoftWrapAwareDocumentParsingListenerAdapter; import com.intellij.openapi.util.TextRange; import com.intellij.util.DocumentUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.awt.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Default {@link SoftWrapModelEx} implementation. * <p/> * Works as a mix of <code>GoF Facade and Bridge</code>, i.e. delegates the processing to the target sub-components and provides * utility methods built on top of sub-components API. * <p/> * Not thread-safe. * * @author Denis Zhdanov * @since Jun 8, 2010 12:47:32 PM */ public class SoftWrapModelImpl extends InlayModel.SimpleAdapter implements SoftWrapModelEx, PrioritizedInternalDocumentListener, FoldingListener, PropertyChangeListener, Dumpable, Disposable { private static final Logger LOG = Logger.getInstance(SoftWrapModelImpl.class); private final List<SoftWrapChangeListener> mySoftWrapListeners = new ArrayList<>(); /** * There is a possible case that particular activity performs batch fold regions operations (addition, removal etc). * We don't want to process them at the same time we get notifications about that because there is a big chance that * we see inconsistent state (e.g. there was a problem with {@link FoldingModel#getCollapsedRegionAtOffset(int)} because that * method uses caching internally and cached data becomes inconsistent if, for example, the top region is removed). * <p/> * So, our strategy is to collect information about changed fold regions and process it only when batch folding processing ends. */ private final List<TextRange> myDeferredFoldRegions = new ArrayList<>(); private final CachingSoftWrapDataMapper myDataMapper; private final SoftWrapsStorage myStorage; private SoftWrapPainter myPainter; private final SoftWrapApplianceManager myApplianceManager; private EditorTextRepresentationHelper myEditorTextRepresentationHelper; @NotNull private final EditorImpl myEditor; private boolean myUseSoftWraps; private int myTabWidth = -1; private final FontPreferencesImpl myFontPreferences = new FontPreferencesImpl(); /** * Soft wraps need to be kept up-to-date on all editor modification (changing text, adding/removing/expanding/collapsing fold * regions etc). Hence, we need to react to all types of target changes. However, soft wraps processing uses various information * provided by editor and there is a possible case that that information is inconsistent during update time (e.g. fold model * advances fold region offsets when end-user types before it, hence, fold regions data is inconsistent between the moment * when text changes are applied to the document and fold data is actually updated). * <p/> * Current field serves as a flag that indicates if all preliminary actions necessary for successful soft wraps processing is done. */ private boolean myUpdateInProgress; private boolean myBulkUpdateInProgress; /** * There is a possible case that target document is changed while its editor is inactive (e.g. user opens two editors for classes * <code>'Part'</code> and <code>'Whole'</code>; activates editor for the class <code>'Whole'</code> and performs 'rename class' * for <code>'Part'</code> from it). Soft wraps cache is not recalculated during that because corresponding editor is not shown * and we lack information about visible area width. Hence, we will need to recalculate the whole soft wraps cache as soon * as target editor becomes visible. * <p/> * Current field serves as a flag for that <code>'dirty document, need complete soft wraps cache recalculation'</code> state. */ private boolean myDirty; private boolean myForceAdditionalColumns; public SoftWrapModelImpl(@NotNull EditorImpl editor) { myEditor = editor; myStorage = new SoftWrapsStorage(); myPainter = new CompositeSoftWrapPainter(editor); myEditorTextRepresentationHelper = new DefaultEditorTextRepresentationHelper(editor); myDataMapper = new CachingSoftWrapDataMapper(editor, myStorage); myApplianceManager = new SoftWrapApplianceManager(myStorage, editor, myPainter, myDataMapper); myApplianceManager.addListener(new SoftWrapAwareDocumentParsingListenerAdapter() { @Override public void recalculationEnds() { for (SoftWrapChangeListener listener : mySoftWrapListeners) { listener.recalculationEnds(); } } }); myUseSoftWraps = areSoftWrapsEnabledInEditor(); myEditor.getColorsScheme().getFontPreferences().copyTo(myFontPreferences); editor.addPropertyChangeListener(this, this); myApplianceManager.addListener(myDataMapper); myEditor.getInlayModel().addListener(this, this); } private boolean areSoftWrapsEnabledInEditor() { return myEditor.getSettings().isUseSoftWraps() && !myEditor.isOneLineMode() && (!(myEditor.getDocument() instanceof DocumentImpl) || !((DocumentImpl)myEditor.getDocument()).acceptsSlashR()); } /** * Called on editor settings change. Current model is expected to drop all cached information about the settings if any. */ public void reinitSettings() { boolean softWrapsUsedBefore = myUseSoftWraps; myUseSoftWraps = areSoftWrapsEnabledInEditor(); int tabWidthBefore = myTabWidth; myTabWidth = EditorUtil.getTabSize(myEditor); boolean fontsChanged = false; if (!myFontPreferences.equals(myEditor.getColorsScheme().getFontPreferences()) && myEditorTextRepresentationHelper instanceof DefaultEditorTextRepresentationHelper) { fontsChanged = true; myEditor.getColorsScheme().getFontPreferences().copyTo(myFontPreferences); ((DefaultEditorTextRepresentationHelper)myEditorTextRepresentationHelper).clearSymbolWidthCache(); myPainter.reinit(); } if ((myUseSoftWraps ^ softWrapsUsedBefore) || (tabWidthBefore >= 0 && myTabWidth != tabWidthBefore) || fontsChanged) { myApplianceManager.reset(); myDeferredFoldRegions.clear(); myStorage.removeAll(); myEditor.myView.reinitSettings(); myEditor.getScrollingModel().scrollToCaret(ScrollType.CENTER); } } @Override public boolean isRespectAdditionalColumns() { return myForceAdditionalColumns; } @Override public void forceAdditionalColumnsUsage() { myForceAdditionalColumns = true; } @Override public boolean isSoftWrappingEnabled() { ApplicationManager.getApplication().assertIsDispatchThread(); return myUseSoftWraps && !myEditor.isPurePaintingMode() && !myApplianceManager.getAvailableArea().isEmpty(); } @Override @Nullable public SoftWrap getSoftWrap(int offset) { if (!isSoftWrappingEnabled()) { return null; } return myStorage.getSoftWrap(offset); } @Override public int getSoftWrapIndex(int offset) { if (!isSoftWrappingEnabled()) { return -1; } return myStorage.getSoftWrapIndex(offset); } @NotNull @Override public List<? extends SoftWrap> getSoftWrapsForRange(int start, int end) { if (!isSoftWrappingEnabled() || end < start) { return Collections.emptyList(); } List<? extends SoftWrap> softWraps = myStorage.getSoftWraps(); int startIndex = myStorage.getSoftWrapIndex(start); if (startIndex < 0) { startIndex = -startIndex - 1; if (startIndex >= softWraps.size() || softWraps.get(startIndex).getStart() > end) { return Collections.emptyList(); } } int endIndex = myStorage.getSoftWrapIndex(end); if (endIndex >= 0) { return softWraps.subList(startIndex, endIndex + 1); } else { endIndex = -endIndex - 1; return softWraps.subList(startIndex, endIndex); } } @Override @NotNull public List<? extends SoftWrap> getSoftWrapsForLine(int documentLine) { if (!isSoftWrappingEnabled() || documentLine < 0) { return Collections.emptyList(); } Document document = myEditor.getDocument(); if (documentLine >= document.getLineCount()) { return Collections.emptyList(); } int start = document.getLineStartOffset(documentLine); int end = document.getLineEndOffset(documentLine); return getSoftWrapsForRange(start, end + 1/* it's theoretically possible that soft wrap is registered just before the line feed, * hence, we add '1' here assuming that end line offset points to line feed symbol */ ); } /** * @return total number of soft wrap-introduced new visual lines */ public int getSoftWrapsIntroducedLinesNumber() { return myStorage.getSoftWraps().size(); // Assuming that soft wrap has single line feed all the time } @Override public List<? extends SoftWrap> getRegisteredSoftWraps() { if (!isSoftWrappingEnabled()) { return Collections.emptyList(); } List<SoftWrapImpl> softWraps = myStorage.getSoftWraps(); if (!softWraps.isEmpty() && softWraps.get(softWraps.size() - 1).getStart() >= myEditor.getDocument().getTextLength()) { LOG.error("Unexpected soft wrap location", new Attachment("editorState.txt", myEditor.dumpState())); } return softWraps; } @Override public boolean isVisible(SoftWrap softWrap) { FoldingModel foldingModel = myEditor.getFoldingModel(); int start = softWrap.getStart(); if (foldingModel.isOffsetCollapsed(start)) { return false; } // There is a possible case that soft wrap and collapsed folding region share the same offset, i.e. soft wrap is represented // before the folding. We need to return 'true' in such situation. Hence, we check if offset just before the soft wrap // is collapsed as well. return start <= 0 || !foldingModel.isOffsetCollapsed(start - 1); } @Override public int paint(@NotNull Graphics g, @NotNull SoftWrapDrawingType drawingType, int x, int y, int lineHeight) { if (!isSoftWrappingEnabled()) { return 0; } if (!myEditor.getSettings().isAllSoftWrapsShown()) { int visualLine = y / lineHeight; LogicalPosition position = myEditor.visualToLogicalPosition(new VisualPosition(visualLine, 0)); if (position.line != myEditor.getCaretModel().getLogicalPosition().line) { return myPainter.getDrawingHorizontalOffset(g, drawingType, x, y, lineHeight); } } return doPaint(g, drawingType, x, y, lineHeight); } public int doPaint(@NotNull Graphics g, @NotNull SoftWrapDrawingType drawingType, int x, int y, int lineHeight) { return myPainter.paint(g, drawingType, x, y, lineHeight); } @Override public int getMinDrawingWidthInPixels(@NotNull SoftWrapDrawingType drawingType) { return myPainter.getMinDrawingWidth(drawingType); } /** * Encapsulates preparations for performing document dimension mapping (e.g. visual to logical position) and answers * if soft wraps-aware processing should be used (e.g. there is no need to consider soft wraps if user configured them * not to be used). * * @return <code>true</code> if soft wraps-aware processing should be used; <code>false</code> otherwise */ public boolean prepareToMapping() { if (myUpdateInProgress || myBulkUpdateInProgress || !isSoftWrappingEnabled()) { return false; } if (myDirty) { myStorage.removeAll(); myApplianceManager.reset(); myDeferredFoldRegions.clear(); myDirty = false; } return myApplianceManager.recalculateIfNecessary(); } /** * Allows to answer if given visual position points to soft wrap-introduced virtual space. * * @param visual target visual position to check * @return <code>true</code> if given visual position points to soft wrap-introduced virtual space; * <code>false</code> otherwise */ @Override public boolean isInsideSoftWrap(@NotNull VisualPosition visual) { return isInsideSoftWrap(visual, false); } /** * Allows to answer if given visual position points to soft wrap-introduced virtual space or points just before soft wrap. * * @param visual target visual position to check * @return <code>true</code> if given visual position points to soft wrap-introduced virtual space; * <code>false</code> otherwise */ @Override public boolean isInsideOrBeforeSoftWrap(@NotNull VisualPosition visual) { return isInsideSoftWrap(visual, true); } private boolean isInsideSoftWrap(@NotNull VisualPosition visual, boolean countBeforeSoftWrap) { if (!isSoftWrappingEnabled()) { return false; } SoftWrapModel model = myEditor.getSoftWrapModel(); if (!model.isSoftWrappingEnabled()) { return false; } LogicalPosition logical = myEditor.visualToLogicalPosition(visual); int offset = myEditor.logicalPositionToOffset(logical); if (offset <= 0) { // Never expect to be here, just a defensive programming. return false; } SoftWrap softWrap = model.getSoftWrap(offset); if (softWrap == null) { return false; } // We consider visual positions that point after the last symbol before soft wrap and the first symbol after soft wrap to not // belong to soft wrap-introduced virtual space. VisualPosition visualAfterSoftWrap = myEditor.offsetToVisualPosition(offset); if (visualAfterSoftWrap.line == visual.line && visualAfterSoftWrap.column <= visual.column) { return false; } VisualPosition beforeSoftWrap = myEditor.offsetToVisualPosition(offset, true, true); return visual.line > beforeSoftWrap.line || visual.column > beforeSoftWrap.column || visual.column == beforeSoftWrap.column && countBeforeSoftWrap; } @Override public void beforeDocumentChangeAtCaret() { CaretModel caretModel = myEditor.getCaretModel(); VisualPosition visualCaretPosition = caretModel.getVisualPosition(); if (!isInsideSoftWrap(visualCaretPosition)) { return; } SoftWrap softWrap = myStorage.getSoftWrap(caretModel.getOffset()); if (softWrap == null) { return; } myEditor.getDocument().replaceString(softWrap.getStart(), softWrap.getEnd(), softWrap.getText()); caretModel.moveToVisualPosition(visualCaretPosition); } @Override public boolean addSoftWrapChangeListener(@NotNull SoftWrapChangeListener listener) { mySoftWrapListeners.add(listener); return myStorage.addSoftWrapChangeListener(listener); } @Override public int getPriority() { return EditorDocumentPriorities.SOFT_WRAP_MODEL; } @Override public void beforeDocumentChange(DocumentEvent event) { if (myBulkUpdateInProgress) { return; } myUpdateInProgress = true; if (!isSoftWrappingEnabled()) { myDirty = true; return; } myApplianceManager.beforeDocumentChange(event); } @Override public void documentChanged(DocumentEvent event) { if (myBulkUpdateInProgress) { return; } myUpdateInProgress = false; if (!isSoftWrappingEnabled()) { return; } myApplianceManager.documentChanged(event); } @Override public void moveTextHappened(int start, int end, int base) { if (myBulkUpdateInProgress) { return; } if (!isSoftWrappingEnabled()) { myDirty = true; return; } myApplianceManager.recalculate(Arrays.asList(new TextRange(start, end), new TextRange(base, base + end - start))); } void onBulkDocumentUpdateStarted() { myBulkUpdateInProgress = true; } void onBulkDocumentUpdateFinished() { myBulkUpdateInProgress = false; if (!isSoftWrappingEnabled()) { myDirty = true; return; } recalculate(); } @Override public void onFoldRegionStateChange(@NotNull FoldRegion region) { myUpdateInProgress = true; if (!isSoftWrappingEnabled() || !region.isValid()) { myDirty = true; return; } // We delay processing of changed fold regions till the invocation of onFoldProcessingEnd(), as // FoldingModel can return inconsistent data before that moment. myDeferredFoldRegions.add(new TextRange(region.getStartOffset(), region.getEndOffset())); } @Override public void onFoldProcessingEnd() { myUpdateInProgress = false; if (!isSoftWrappingEnabled()) { return; } try { if (!myDirty) { // no need to recalculate specific areas if the whole document will be reprocessed myApplianceManager.recalculate(myDeferredFoldRegions); } } finally { myDeferredFoldRegions.clear(); } } @Override public void onUpdated(@NotNull Inlay inlay) { if (myEditor.getDocument().isInEventsHandling() || myEditor.getDocument().isInBulkUpdate()) return; if (!isSoftWrappingEnabled()) { myDirty = true; return; } if (!myDirty) { int offset = inlay.getOffset(); myApplianceManager.recalculate(Collections.singletonList(new TextRange(offset, offset))); } } @Override public void propertyChange(PropertyChangeEvent evt) { if (EditorEx.PROP_FONT_SIZE.equals(evt.getPropertyName())) { myDirty = true; } } @Override public void dispose() { release(); } @Override public void release() { myApplianceManager.release(); myStorage.removeAll(); myDeferredFoldRegions.clear(); } public void recalculate() { myApplianceManager.reset(); myStorage.removeAll(); myDeferredFoldRegions.clear(); myApplianceManager.recalculateIfNecessary(); } public SoftWrapApplianceManager getApplianceManager() { return myApplianceManager; } @TestOnly public void setSoftWrapPainter(SoftWrapPainter painter) { myPainter = painter; myApplianceManager.setSoftWrapPainter(painter); } public static EditorTextRepresentationHelper getEditorTextRepresentationHelper(@NotNull Editor editor) { return ((SoftWrapModelEx)editor.getSoftWrapModel()).getEditorTextRepresentationHelper(); } public EditorTextRepresentationHelper getEditorTextRepresentationHelper() { return myEditorTextRepresentationHelper; } @TestOnly public void setEditorTextRepresentationHelper(EditorTextRepresentationHelper editorTextRepresentationHelper) { myEditorTextRepresentationHelper = editorTextRepresentationHelper; myApplianceManager.reset(); } @NotNull @Override public String dumpState() { return String.format("\nuse soft wraps: %b, tab width: %d, additional columns: %b, " + "update in progress: %b, bulk update in progress: %b, dirty: %b, deferred regions: %s" + "\nappliance manager state: %s\nsoft wraps mapping info: %s\nsoft wraps: %s", myUseSoftWraps, myTabWidth, myForceAdditionalColumns, myUpdateInProgress, myBulkUpdateInProgress, myDirty, myDeferredFoldRegions.toString(), myApplianceManager.dumpState(), myDataMapper.dumpState(), myStorage.dumpState()); } @Override public String toString() { return dumpState(); } public boolean isDirty() { return myUseSoftWraps && myDirty; } @TestOnly public void validateState() { Document document = myEditor.getDocument(); if (myEditor.getDocument().isInBulkUpdate()) return; FoldingModel foldingModel = myEditor.getFoldingModel(); List<? extends SoftWrap> softWraps = getRegisteredSoftWraps(); int lastSoftWrapOffset = -1; for (SoftWrap wrap : softWraps) { int softWrapOffset = wrap.getStart(); LOG.assertTrue(softWrapOffset > lastSoftWrapOffset, "Soft wraps are not ordered"); LOG.assertTrue(softWrapOffset < document.getTextLength(), "Soft wrap is after document's end"); FoldRegion foldRegion = foldingModel.getCollapsedRegionAtOffset(softWrapOffset); LOG.assertTrue(foldRegion == null || foldRegion.getStartOffset() == softWrapOffset, "Soft wrap is inside fold region"); LOG.assertTrue(softWrapOffset != DocumentUtil.getLineEndOffset(softWrapOffset, document) || foldRegion != null, "Soft wrap before line break"); LOG.assertTrue(softWrapOffset != DocumentUtil.getLineStartOffset(softWrapOffset, document) || foldingModel.isOffsetCollapsed(softWrapOffset - 1), "Soft wrap after line break"); LOG.assertTrue(!DocumentUtil.isInsideSurrogatePair(document, softWrapOffset), "Soft wrap inside a surrogate pair"); lastSoftWrapOffset = softWrapOffset; } } }