diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java index d1505e61a566..b69e92258937 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java @@ -270,6 +270,8 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi private boolean myStickySelection; private int myStickySelectionStart; private boolean myScrollToCaret = true; + + private final EditorSizeAdjustmentStrategy mySizeAdjustmentStrategy = new EditorSizeAdjustmentStrategy(); static { ourCaretBlinkingCommand = new RepaintCursorCommand(); @@ -1497,6 +1499,10 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi Dimension dim = getPreferredSize(); if (!dim.equals(myPreferredSize) && !myDocument.isInBulkUpdate()) { + dim = mySizeAdjustmentStrategy.adjust(dim, myPreferredSize, this); + if (dim == null) { + return; + } myPreferredSize = dim; stopOptimizedScrolling(); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorSizeAdjustmentStrategy.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorSizeAdjustmentStrategy.java new file mode 100644 index 000000000000..dd22bc3ac766 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorSizeAdjustmentStrategy.java @@ -0,0 +1,141 @@ +/* + * Copyright 2000-2011 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.openapi.editor.ex.util.EditorUtil; +import com.intellij.util.Alarm; +import gnu.trove.TLongArrayList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; + +/** + * This class is aimed to help {@link EditorImpl the editor} when user extensively modifies the longest line + * at the document (e.g. is typing at its end). + *

+ * The problem is that the longest line's width is a {@link JComponent#getPreferredSize() preferred size's width} as well. + * So, every time width of the longest line is changed, editor's preferred size is changed too and that triggers the whole + * component repaint. + *

+ * This component comes into the play here - it's assumed that editor notifies it every time preferred size is changed and + * receives instructions for the further actions. For example, we can reserve additional space if we see that preferred size + * is permanently increasing (the user is typing at the end of the longest line) etc. See method javadocs for more details. + *

+ * Not thread-safe. + * + * @author Denis Zhdanov + * @since 6/17/11 2:45 PM + */ +public class EditorSizeAdjustmentStrategy { + + /** Amount of time (in milliseconds) to keep information about preferred size change. */ + private static final long TIMING_TTL_MILLIS = 10000L; + + /** Constant that indicates minimum number of preferred size changes per target amount of time that is considered to be frequent. */ + private static final int FREQUENT_SIZE_CHANGES_NUMBER = 10; + + /** Default number of columns to reserve during frequent typing at the end of the longest document line. */ + private static final int DEFAULT_RESERVE_COLUMNS_NUMBER = 4; + + private final Alarm myAlarm = new Alarm(); + private final TLongArrayList myTimings = new TLongArrayList(); + + private int myReserveColumns = DEFAULT_RESERVE_COLUMNS_NUMBER; + private boolean mySkip; + + /** + * Asks to adjust new preferred size appliance if necessary. + * + * @param newPreferredSize newly calculated preferred size that differs from the old preferred size + * @param oldPreferredSize old preferred size (if any) + * @param editor target editor + * @return preferred size to use (given 'new preferred size' may be adjusted); null if given + * new preferred size should not be applied + */ + @Nullable + public Dimension adjust(@NotNull Dimension newPreferredSize, @Nullable Dimension oldPreferredSize, @NotNull EditorImpl editor) { + if (oldPreferredSize == null || mySkip) { + return newPreferredSize; + } + // Process only width change. + if (newPreferredSize.height != oldPreferredSize.height) { + return newPreferredSize; + } + + stripTimings(); + myTimings.add(System.currentTimeMillis()); + if (myTimings.size() < FREQUENT_SIZE_CHANGES_NUMBER) { + return newPreferredSize; + } + + boolean increaseWidth = newPreferredSize.width > oldPreferredSize.width; + if (increaseWidth) { + final int spaceWidth = EditorUtil.getSpaceWidth(Font.PLAIN, editor); + newPreferredSize.width += myReserveColumns * spaceWidth; + myReserveColumns += 3; + + } + // Don't reduce preferred size on frequent reduce of the longest document line. + + scheduleSizeUpdate(editor); + return newPreferredSize; + } + + /** + * Removes old timings. + */ + private void stripTimings() { + long limit = System.currentTimeMillis() - TIMING_TTL_MILLIS; + int endIndex = 0; + for (; endIndex < myTimings.size(); endIndex++) { + if (myTimings.get(endIndex) > limit) { + break; + } + } + if (endIndex > 0) { + myTimings.remove(0, endIndex); + } + } + + private void scheduleSizeUpdate(@NotNull EditorImpl editor) { + myAlarm.cancelAllRequests(); + myAlarm.addRequest(new UpdateSizeTask(editor), 1000); + } + + private class UpdateSizeTask implements Runnable { + + private final EditorImpl myEditor; + + UpdateSizeTask(@NotNull EditorImpl editor) { + myEditor = editor; + } + + @Override + public void run() { + mySkip = true; + myReserveColumns = DEFAULT_RESERVE_COLUMNS_NUMBER; + myTimings.clear(); + try { + myEditor.validateSize(); + } + finally { + mySkip = false; + } + } + } +}