/*
* 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.FontPreferences;
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.getName());
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 FontPreferences 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;
}
}
}