diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/DelayMeter.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/DelayMeter.java index 066ab9ee4803..a09d6eddf54e 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/DelayMeter.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/DelayMeter.java @@ -61,50 +61,5 @@ class DelayMeter { double getStandardDeviation() { return myStats.getStandardDeviation() / 1000000.0D; } - - - private static class SummaryStatistics { - private int myCount = 0; - - private double myMin = Double.MAX_VALUE; - - private double myMax = 0.0D; - - private double myMean = 0.0D; - - private double myS = 0.0D; - - void accept(double value) { - myCount++; - - myMin = Math.min(myMin, value); - - myMax = Math.max(myMax, value); - - if (myCount == 1) { - myMean = value; - } else { - double previousMean = myMean; - myMean += (value - myMean) / myCount; - myS += (value - previousMean) * (value - myMean); - } - } - - double getMin() { - return myMin == Double.MAX_VALUE ? 0.0D : myMin; - } - - double getMax() { - return myMax; - } - - double getMean() { - return myMean; - } - - double getStandardDeviation() { - return Math.sqrt(myS / (myCount - 1)); - } - } } 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 4481b15409ce..a8e151c71710 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 @@ -25,15 +25,11 @@ import com.intellij.diagnostic.Dumpable; import com.intellij.diagnostic.LogMessageEx; import com.intellij.ide.*; import com.intellij.ide.dnd.DnDManager; -import com.intellij.ide.plugins.IdeaPluginDescriptor; -import com.intellij.ide.plugins.PluginManager; import com.intellij.ide.ui.UISettings; import com.intellij.ide.ui.customization.CustomActionsSchema; -import com.intellij.notification.*; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.ActionManagerEx; -import com.intellij.openapi.actionSystem.ex.AnActionListener; import com.intellij.openapi.actionSystem.impl.MouseGestureManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; @@ -42,15 +38,12 @@ import com.intellij.openapi.command.UndoConfirmationPolicy; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.actionSystem.*; -import com.intellij.openapi.editor.actions.*; import com.intellij.openapi.editor.colors.*; import com.intellij.openapi.editor.colors.impl.DelegateColorScheme; import com.intellij.openapi.editor.event.*; import com.intellij.openapi.editor.ex.*; -import com.intellij.openapi.editor.ex.util.EditorUIUtil; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.editor.ex.util.EmptyEditorHighlighter; -import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter; import com.intellij.openapi.editor.highlighter.EditorHighlighter; import com.intellij.openapi.editor.highlighter.HighlighterClient; import com.intellij.openapi.editor.impl.event.MarkupModelListener; @@ -58,7 +51,6 @@ import com.intellij.openapi.editor.impl.softwrap.SoftWrapAppliancePlaces; import com.intellij.openapi.editor.impl.softwrap.SoftWrapDrawingType; import com.intellij.openapi.editor.impl.view.EditorView; import com.intellij.openapi.editor.markup.*; -import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory; import com.intellij.openapi.fileEditor.impl.EditorsSplitters; @@ -324,41 +316,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi private String myContextMenuGroupId = IdeActions.GROUP_BASIC_EDITOR_POPUP; - // Characters that excluded from zero-latency painting after key typing - private static final Set KEY_CHARS_TO_SKIP = new HashSet(Arrays.asList('\n', '\t', '(', ')', '[', ']', '{', '}', '"', '\'')); - - // Characters that excluded from zero-latency painting after document update - private static final Set DOCUMENT_CHARS_TO_SKIP = new HashSet(Arrays.asList(')', ']', '}', '"', '\'')); - - // Although it's possible to paint arbitrary line changes immediately, - // our primary interest is direct user editing actions, where visual delay is crucial. - // Moreover, as many subsystems (like PsiToDocumentSynchronizer, UndoManager, etc.) don't enforce bulk document updates, - // and can trigger multiple write actions / document changes sequentially, we need to avoid possible flickering during such an activity. - // There seems to be no other way to determine whether particular document change is triggered by direct user editing - // (raw character typing is handled separately, even before write action). - private static final Set IMMEDIATE_EDITING_ACTIONS = new HashSet(Arrays.asList(BackspaceAction.class, - DeleteAction.class, - DeleteToWordStartAction.class, - DeleteToWordEndAction.class, - DeleteToWordStartInDifferentHumpsModeAction.class, - DeleteToWordEndInDifferentHumpsModeAction.class, - DeleteToLineStartAction.class, - DeleteToLineEndAction.class, - CutAction.class, - PasteAction.class)); - - private Rectangle myOldArea = new Rectangle(0, 0, 0, 0); - private Rectangle myOldTailArea = new Rectangle(0, 0, 0, 0); - private boolean myImmediateEditingInProgress; - - // TODO Should be removed when IDEA adopts typing without starting write actions. - private static final boolean VIM_PLUGIN_LOADED = isPluginLoaded("IdeaVIM"); - - private static final int TYPING_STATS_SAMPLE_SIZE = 50; - - private int myCharsTyped; - private final DelayMeter myTypingLatencyMeter = new DelayMeter(); - private boolean myZeroLatencyTypingWasEnabled = isZeroLatencyTypingEnabled(); + private final ImmediatePainter myImmediatePainter; static { ourCaretBlinkingCommand.start(); @@ -391,6 +349,8 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi myCommandProcessor = CommandProcessor.getInstance(); + myImmediatePainter = new ImmediatePainterImpl(this); + if (project != null) { myConnection = project.getMessageBus().connect(); myConnection.subscribe(DocumentBulkUpdateListener.TOPIC, new EditorDocumentBulkUpdateAdapter()); @@ -996,25 +956,6 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi myPanel.add(myScrollPane); } - AnActionListener.Adapter actionListener = new AnActionListener.Adapter() { - @Override - public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { - if (isZeroLatencyTypingEnabled() && IMMEDIATE_EDITING_ACTIONS.contains(action.getClass()) && - !(action.getClass() == PasteAction.class && mySelectionModel.hasSelection())) { - myImmediateEditingInProgress = true; - } - } - - @Override - public void afterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { - if (isZeroLatencyTypingEnabled()) { - myImmediateEditingInProgress = false; - } - } - }; - - ActionManager.getInstance().addAnActionListener(actionListener, myDisposable); - myEditorComponent.addKeyListener(new KeyListener() { @Override public void keyPressed(@NotNull KeyEvent e) { @@ -1198,62 +1139,15 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); DataContext dataContext = getDataContext(); - if (isTypingLatencyStatsEnabled()) { - if (myZeroLatencyTypingWasEnabled != isZeroLatencyTypingEnabled()) { - resetTypingLatencyStats(); - myZeroLatencyTypingWasEnabled = isZeroLatencyTypingEnabled(); - } - myTypingLatencyMeter.registerStart(); - } - - if (isZeroLatencyTypingEnabled() && myDocument.isWritable() && !isViewer() && canPaintImmediately(c)) { - for (Caret caret : myCaretModel.getAllCarets()) { - paintImmediately(caret.getOffset(), c, myIsInsertMode); - } - if (isTypingLatencyStatsEnabled()) { - // not all chars are painted immediately, so type usual letters / digits to compute precise stats - myTypingLatencyMeter.registerFinish(); - } - } + myImmediatePainter.paintCharacter(myEditorComponent.getGraphics(), c); actionManager.fireBeforeEditorTyping(c, dataContext); MacUIUtil.hideCursor(); EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); - if (isTypingLatencyStatsEnabled()) { - myCharsTyped++; - - if (myCharsTyped == TYPING_STATS_SAMPLE_SIZE) { - String stats = formatTypingLatencyStats(myTypingLatencyMeter); - printToEventLog(stats); - LOG.info(stats); - - resetTypingLatencyStats(); - } - } - return true; } - private static String formatTypingLatencyStats(DelayMeter meter) { - return String.format("Zero-latency: %3s; typing delay, ms: min: %5.1f | max: %5.1f | avg: %5.1f | sigma: %4.1f", - isZeroLatencyTypingEnabled() ? "on" : "off", - meter.getMin(), meter.getMax(), meter.getMean(), meter.getStandardDeviation()); - } - - private void printToEventLog(String message) { - NotificationGroup group = NotificationGroup.logOnlyGroup("typing-delay-stats"); - Notification notification = group.createNotification(message, NotificationType.INFORMATION); - notification.setImportant(true); - notification.notify(myProject); - notification.hideBalloon(); - } - - private void resetTypingLatencyStats() { - myCharsTyped = 0; - myTypingLatencyMeter.reset(); - } - private void fireFocusLost() { for (FocusChangeListener listener : myFocusListeners) { listener.focusLost(this); @@ -1994,26 +1888,14 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi mySizeContainer.beforeChange(e); } - if (isZeroLatencyTypingEnabled() && myImmediateEditingInProgress && canPaintImmediately(e)) { - int offset = e.getOffset(); - int length = e.getOldLength(); - - myOldArea = lineRectangleBetween(offset, offset + length); - - myOldTailArea = lineRectangleBetween(offset + length, myDocument.getLineEndOffset(myDocument.getLineNumber(offset))); - if (myOldTailArea.isEmpty()) { - myOldTailArea.width += EditorUtil.getSpaceWidth(Font.PLAIN, this); // include possible caret - } - } + myImmediatePainter.beforeUpdate(e); } private void changedUpdate(DocumentEvent e) { myDocumentChangeInProgress = false; if (myDocument.isInBulkUpdate()) return; - if (isZeroLatencyTypingEnabled() && myImmediateEditingInProgress && canPaintImmediately(e)) { - paintImmediately(e); - } + myImmediatePainter.paintUpdate(myEditorComponent.getGraphics(), e); if (myErrorStripeNeedsRepaint) { myMarkupModel.repaint(e.getOffset(), e.getOffset() + e.getNewLength()); @@ -2228,219 +2110,6 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi putUserData(BUFFER, image); } - private static boolean isZeroLatencyTypingEnabled() { - // Zero-latency typing is suppressed when Idea VIM plugin is loaded, because of VIM-1007. - // That issue will be resolved automatically when IDEA adopts typing without starting write actions. - return !VIM_PLUGIN_LOADED && Registry.is("editor.zero.latency.typing"); - } - - private static boolean isTypingLatencyStatsEnabled() { - return Registry.is("editor.typing.latency.stats"); - } - - private static boolean isPluginLoaded(@NotNull String id) { - PluginId pluginId = PluginId.findId(id); - if (pluginId == null) return false; - IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId); - if (plugin == null) return false; - return plugin.isEnabled(); - } - - private boolean canPaintImmediately(char c) { - return myDocument instanceof DocumentImpl && - myHighlighter instanceof LexerEditorHighlighter && - !mySelectionModel.hasSelection() && - arePositionsWithinDocument(myCaretModel.getAllCarets()) && - areVisualLinesUnique(myCaretModel.getAllCarets()) && - !isInplaceRenamerActive() && - !KEY_CHARS_TO_SKIP.contains(c); - } - - private static boolean areVisualLinesUnique(List carets) { - if (carets.size() > 1) { - TIntHashSet lines = new TIntHashSet(carets.size()); - for (Caret caret : carets) { - if (!lines.add(caret.getVisualLineStart())) { - return false; - } - } - } - return true; - } - - // Checks whether all the carets are within document or some of them are in a so called "virtual space". - private boolean arePositionsWithinDocument(List carets) { - for (Caret caret : carets) { - if (caret.getLogicalPosition().compareTo(offsetToLogicalPosition(caret.getOffset())) != 0) { - return false; - } - } - return true; - } - - // TODO Improve the approach - handle such cases in a more general way. - private boolean isInplaceRenamerActive() { - Key key = Key.findKeyByName("EditorInplaceRenamer"); - return key != null && key.isIn(this); - } - - // Called to display a single character insertion before starting a write action and the general painting routine. - // Bypasses RepaintManager (c.repaint, c.paintComponent) and double buffering (g.paintImmediately) to minimize visual lag. - // TODO Should be replaced with the generic paintImmediately(event) call when we implement typing without starting write actions. - private void paintImmediately(int offset, char c, boolean insert) { - Graphics g = myEditorComponent.getGraphics(); - - if (g == null) return; // editor component is currently not displayable - - TextAttributes attributes = ((LexerEditorHighlighter)myHighlighter).getAttributes((DocumentImpl)myDocument, offset, c); - - int fontType = attributes.getFontType(); - FontInfo fontInfo = EditorUtil.fontForChar(c, attributes.getFontType(), this); - Font font = fontInfo.getFont(); - - // it's more reliable to query actual font metrics - FontMetrics fontMetrics = getFontMetrics(fontType); - - int charWidth = fontMetrics.charWidth(c); - - int delta = charWidth; - - if (!insert && offset < myDocument.getTextLength()) { - delta -= fontMetrics.charWidth(myDocument.getCharsSequence().charAt(offset)); - } - - Rectangle tailArea = lineRectangleBetween(offset, myDocument.getLineEndOffset(offsetToLogicalLine(offset))); - if (tailArea.isEmpty()) { - tailArea.width += EditorUtil.getSpaceWidth(fontType, this); // include caret - } - - Color background = attributes.getBackgroundColor() == null ? getCaretRowBackground() : attributes.getBackgroundColor(); - - Rectangle newArea = lineRectangleBetween(offset, offset); - newArea.width += charWidth; - - String newText = Character.toString(c); - Point point = newArea.getLocation(); - int ascent = getAscent(); - Color foreground = attributes.getForegroundColor() == null ? getForegroundColor() : attributes.getForegroundColor(); - - EditorUIUtil.setupAntialiasing(g); - - // pre-compute all the arguments beforehand to minimize delays between the calls (as there's no double-buffering) - if (delta != 0) { - shift(g, tailArea, delta); - } - fill(g, newArea, background); - print(g, newText, point, ascent, font, foreground); - - // flush changes (there can be batching / buffering in video driver) - Toolkit.getDefaultToolkit().sync(); - } - - private boolean canPaintImmediately(@NotNull DocumentEvent e) { - return myDocument instanceof DocumentImpl && - !isInplaceRenamerActive() && - StringUtil.indexOf(e.getOldFragment(), '\n') == -1 && - StringUtil.indexOf(e.getNewFragment(), '\n') == -1 && - !(e.getNewLength() == 1 && DOCUMENT_CHARS_TO_SKIP.contains(e.getNewFragment().charAt(0))); - } - - // Called to display insertion / deletion / replacement within a single line before the general painting routine. - // Bypasses RepaintManager (c.repaint, c.paintComponent) and double buffering (g.paintImmediately) to minimize visual lag. - private void paintImmediately(@NotNull DocumentEvent e) { - Graphics g = myEditorComponent.getGraphics(); - - if (g == null) return; // editor component is currently not displayable - - int offset = e.getOffset(); - String newText = e.getNewFragment().toString(); - Rectangle newArea = lineRectangleBetween(offset, offset + newText.length()); - int delta = newArea.width - myOldArea.width; - Color background = getCaretRowBackground(); - - if (delta != 0) { - if (delta < 0) { - // Pre-paint carets at new positions, if needed, before shifting the tail area (to avoid flickering), - // because painting takes some time while copyArea is almost instantaneous. - CaretRectangle[] caretRectangles = myCaretCursor.getCaretLocations(true); - if (caretRectangles != null) { - for (CaretRectangle it : caretRectangles) { - Rectangle r = toRectangle(it); - if (myOldArea.contains(r) && !newArea.contains(r)) { - myCaretCursor.paintAt(g, it.myPoint.x - delta, it.myPoint.y, it.myWidth, it.myCaret); - } - } - } - } - - shift(g, myOldTailArea, delta); - - if (delta < 0) { - Rectangle remainingArea = new Rectangle(myOldTailArea.x + myOldTailArea.width + delta, - myOldTailArea.y, -delta, myOldTailArea.height); - fill(g, remainingArea, background); - } - } - - if (!newArea.isEmpty()) { - TextAttributes attributes = myHighlighter.createIterator(offset).getTextAttributes(); - - Point point = newArea.getLocation(); - int ascent = getAscent(); - // simplified font selection (based on the first character) - FontInfo fontInfo = EditorUtil.fontForChar(newText.charAt(0), attributes.getFontType(), this); - Font font = fontInfo.getFont(); - - Color foreground = attributes.getForegroundColor() == null ? getForegroundColor() : attributes.getForegroundColor(); - - EditorUIUtil.setupAntialiasing(g); - - // pre-compute all the arguments beforehand to minimize delay between the calls (as there's no double-buffering) - fill(g, newArea, background); - print(g, newText, point, ascent, font, foreground); - } - - // flush changes (there can be batching / buffering in video driver) - Toolkit.getDefaultToolkit().sync(); - } - - @NotNull - private Rectangle lineRectangleBetween(int begin, int end) { - Point p1 = offsetToXY(begin, false); - Point p2 = offsetToXY(end, false); - // When soft wrap is present, handle only the first visual line (for simplicity, yet it works reasonably well) - int x2 = p1.y == p2.y ? p2.x : Math.max(p1.x, myEditorComponent.getWidth() - getVerticalScrollBar().getWidth()); - return new Rectangle(p1.x, p1.y, x2 - p1.x, getLineHeight()); - } - - @NotNull - private Rectangle toRectangle(@NotNull CaretRectangle caretRectangle) { - Point p = caretRectangle.myPoint; - return new Rectangle(p.x, p.y, caretRectangle.myWidth, getLineHeight()); - } - - @NotNull - private Color getCaretRowBackground() { - Color color = myScheme.getColor(EditorColors.CARET_ROW_COLOR); - return color == null ? getBackgroundColor() : color; - } - - private static void shift(@NotNull Graphics g, @NotNull Rectangle r, int delta) { - g.copyArea(r.x, r.y, r.width, r.height, delta, 0); - } - - private static void fill(@NotNull Graphics g, @NotNull Rectangle r, @NotNull Color color) { - g.setColor(color); - g.fillRect(r.x, r.y, r.width, r.height); - } - - private static void print(@NotNull Graphics g, @NotNull String text, @NotNull Point point, - int ascent, @NotNull Font font, @NotNull Color color) { - g.setFont(font); - g.setColor(color); - g.drawString(text, point.x, point.y + ascent); - } - void paint(@NotNull Graphics2D g) { Rectangle clip = g.getClipBounds(); @@ -2501,9 +2170,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi paintComposedTextDecoration(g); } - if (isTypingLatencyStatsEnabled()) { - myTypingLatencyMeter.registerFinish(); - } + myImmediatePainter.afterPainting(); } private static final char IDEOGRAPHIC_SPACE = '\u3000'; // http://www.marathon-studios.com/unicode/U3000/Ideographic_Space @@ -2579,7 +2246,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } @NotNull - private Color getForegroundColor() { + Color getForegroundColor() { return myScheme.getDefaultForeground(); } @@ -3581,6 +3248,11 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi myCaretCursor.paint(g); } + + @NotNull + CaretCursor getCaretCursor() { + return myCaretCursor; + } @Nullable public CaretRectangle[] getCaretLocations(boolean onlyIfShown) { @@ -5092,7 +4764,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } } - private class CaretCursor { + class CaretCursor { private CaretRectangle[] myLocations; private boolean myEnabled; diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/ImmediatePainter.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ImmediatePainter.java new file mode 100644 index 000000000000..1f3680539612 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ImmediatePainter.java @@ -0,0 +1,34 @@ +/* + * 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.openapi.editor.event.DocumentEvent; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; + +/** + * @author Pavel Fatin + */ +interface ImmediatePainter { + void paintCharacter(Graphics g, char c); + + void beforeUpdate(@NotNull DocumentEvent e); + + void paintUpdate(Graphics g, @NotNull DocumentEvent e); + + void afterPainting(); +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/ImmediatePainterImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ImmediatePainterImpl.java new file mode 100644 index 000000000000..59fe313f29a3 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/ImmediatePainterImpl.java @@ -0,0 +1,436 @@ +/* + * 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.ide.plugins.IdeaPluginDescriptor; +import com.intellij.ide.plugins.PluginManager; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationGroup; +import com.intellij.notification.NotificationType; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.ex.AnActionListener; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.SelectionModel; +import com.intellij.openapi.editor.actions.*; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.editor.ex.util.EditorUIUtil; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter; +import com.intellij.openapi.editor.highlighter.EditorHighlighter; +import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.extensions.PluginId; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.util.text.StringUtil; +import gnu.trove.TIntHashSet; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Pavel Fatin + */ +class ImmediatePainterImpl implements ImmediatePainter { + // Characters that excluded from zero-latency painting after key typing + private static final Set KEY_CHARS_TO_SKIP = + new HashSet(Arrays.asList('\n', '\t', '(', ')', '[', ']', '{', '}', '"', '\'')); + + // Characters that excluded from zero-latency painting after document update + private static final Set DOCUMENT_CHARS_TO_SKIP = + new HashSet(Arrays.asList(')', ']', '}', '"', '\'')); + + // Although it's possible to paint arbitrary line changes immediately, + // our primary interest is direct user editing actions, where visual delay is crucial. + // Moreover, as many subsystems (like PsiToDocumentSynchronizer, UndoManager, etc.) don't enforce bulk document updates, + // and can trigger multiple write actions / document changes sequentially, we need to avoid possible flickering during such an activity. + // There seems to be no other way to determine whether particular document change is triggered by direct user editing + // (raw character typing is handled separately, even before write action). + private static final Set IMMEDIATE_EDITING_ACTIONS = new HashSet(Arrays.asList(BackspaceAction.class, + DeleteAction.class, + DeleteToWordStartAction.class, + DeleteToWordEndAction.class, + DeleteToWordStartInDifferentHumpsModeAction.class, + DeleteToWordEndInDifferentHumpsModeAction.class, + DeleteToLineStartAction.class, + DeleteToLineEndAction.class, + CutAction.class, + PasteAction.class)); + public static final String ZERO_LATENCY_TYPING_KEY = "editor.zero.latency.typing"; + + public static final String TYPING_LATENCY_STATS_KEY = "editor.typing.latency.stats"; + + private static final int TYPING_STATS_SAMPLE_SIZE = 50; + + // TODO Should be removed when IDEA adopts typing without starting write actions. + private static final boolean VIM_PLUGIN_LOADED = isPluginLoaded("IdeaVIM"); + + private Rectangle myOldArea = new Rectangle(0, 0, 0, 0); + private Rectangle myOldTailArea = new Rectangle(0, 0, 0, 0); + private boolean myImmediateEditingInProgress; + + private final EditorImpl myEditor; + + private int myCharsTyped; + private final DelayMeter myTypingLatencyMeter = new DelayMeter(); + private boolean myZeroLatencyTypingWasEnabled = isZeroLatencyTypingEnabled(); + + + ImmediatePainterImpl(EditorImpl editor) { + myEditor = editor; + AnActionListener.Adapter actionListener = new AnActionListener.Adapter() { + @Override + public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { + if (isZeroLatencyTypingEnabled() && IMMEDIATE_EDITING_ACTIONS.contains(action.getClass()) && + !(action.getClass() == PasteAction.class && getSelectionModel().hasSelection())) { + myImmediateEditingInProgress = true; + } + } + + @Override + public void afterActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { + if (isZeroLatencyTypingEnabled()) { + myImmediateEditingInProgress = false; + } + } + }; + + ActionManager.getInstance().addAnActionListener(actionListener, editor.getDisposable()); + } + + protected Document getDocument() { + return myEditor.getDocument(); + } + + protected SelectionModel getSelectionModel() { + return myEditor.getSelectionModel(); + } + + protected EditorHighlighter getHighlighter() { + return myEditor.getHighlighter(); + } + + protected CaretModel getCaretModel() { + return myEditor.getCaretModel(); + } + + protected EditorColorsScheme getColorsScheme() { + return myEditor.getColorsScheme(); + } + + protected EditorComponentImpl getContentComponent() { + return myEditor.getContentComponent(); + } + + @Override + public void paintCharacter(Graphics g, char c) { + if (isTypingLatencyStatsEnabled()) { + if (myZeroLatencyTypingWasEnabled != isZeroLatencyTypingEnabled()) { + resetTypingLatencyStats(); + myZeroLatencyTypingWasEnabled = isZeroLatencyTypingEnabled(); + } + myTypingLatencyMeter.registerStart(); + } + + if (isZeroLatencyTypingEnabled() && getDocument().isWritable() && !myEditor.isViewer() && canPaintImmediately(c)) { + for (Caret caret : getCaretModel().getAllCarets()) { + paintImmediately(g, caret.getOffset(), c, myEditor.isInsertMode()); + } + if (isTypingLatencyStatsEnabled()) { + // not all chars are painted immediately, so type usual letters / digits to compute precise stats + myTypingLatencyMeter.registerFinish(); + } + } + + if (isTypingLatencyStatsEnabled()) { + myCharsTyped++; + + if (myCharsTyped == TYPING_STATS_SAMPLE_SIZE) { + String stats = formatTypingLatencyStats(myTypingLatencyMeter); + printToEventLog(stats); + + resetTypingLatencyStats(); + } + } + } + + private static String formatTypingLatencyStats(DelayMeter meter) { + return String.format("Zero-latency: %3s; typing delay, ms: min: %5.1f | max: %5.1f | avg: %5.1f | sigma: %4.1f", + isZeroLatencyTypingEnabled() ? "on" : "off", + meter.getMin(), meter.getMax(), meter.getMean(), meter.getStandardDeviation()); + } + + private void printToEventLog(String message) { + NotificationGroup group = NotificationGroup.logOnlyGroup("typing-delay-stats"); + Notification notification = group.createNotification(message, NotificationType.INFORMATION); + notification.setImportant(true); + notification.notify(myEditor.getProject()); + notification.hideBalloon(); + } + + private void resetTypingLatencyStats() { + myCharsTyped = 0; + myTypingLatencyMeter.reset(); + } + + @Override + public void afterPainting() { + if (isTypingLatencyStatsEnabled()) { + myTypingLatencyMeter.registerFinish(); + } + } + + @Override + public void beforeUpdate(@NotNull DocumentEvent e) { + if (isZeroLatencyTypingEnabled() && myImmediateEditingInProgress && canPaintImmediately(e)) { + int offset = e.getOffset(); + int length = e.getOldLength(); + + myOldArea = lineRectangleBetween(offset, offset + length); + + myOldTailArea = lineRectangleBetween(offset + length, getDocument().getLineEndOffset(getDocument().getLineNumber(offset))); + if (myOldTailArea.isEmpty()) { + myOldTailArea.width += EditorUtil.getSpaceWidth(Font.PLAIN, myEditor); // include possible caret + } + } + } + + @Override + public void paintUpdate(Graphics g, @NotNull DocumentEvent e) { + if (isZeroLatencyTypingEnabled() && myImmediateEditingInProgress && canPaintImmediately(e)) { + paintImmediately(g, e); + } + } + + private static boolean isZeroLatencyTypingEnabled() { + // Zero-latency typing is suppressed when Idea VIM plugin is loaded, because of VIM-1007. + // That issue will be resolved automatically when IDEA adopts typing without starting write actions. + return !VIM_PLUGIN_LOADED && Registry.is(ZERO_LATENCY_TYPING_KEY); + } + + private static boolean isTypingLatencyStatsEnabled() { + return Registry.is(TYPING_LATENCY_STATS_KEY); + } + + private static boolean isPluginLoaded(@NotNull String id) { + PluginId pluginId = PluginId.findId(id); + if (pluginId == null) return false; + IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId); + if (plugin == null) return false; + return plugin.isEnabled(); + } + + private boolean canPaintImmediately(char c) { + return getDocument() instanceof DocumentImpl && + getHighlighter() instanceof LexerEditorHighlighter && + !getSelectionModel().hasSelection() && + arePositionsWithinDocument(getCaretModel().getAllCarets()) && + areVisualLinesUnique(getCaretModel().getAllCarets()) && + !isInplaceRenamerActive() && + !KEY_CHARS_TO_SKIP.contains(c); + } + + private static boolean areVisualLinesUnique(List carets) { + if (carets.size() > 1) { + TIntHashSet lines = new TIntHashSet(carets.size()); + for (Caret caret : carets) { + if (!lines.add(caret.getVisualLineStart())) { + return false; + } + } + } + return true; + } + + // Checks whether all the carets are within document or some of them are in a so called "virtual space". + private boolean arePositionsWithinDocument(List carets) { + for (Caret caret : carets) { + if (caret.getLogicalPosition().compareTo(myEditor.offsetToLogicalPosition(caret.getOffset())) != 0) { + return false; + } + } + return true; + } + + // TODO Improve the approach - handle such cases in a more general way. + private boolean isInplaceRenamerActive() { + Key key = Key.findKeyByName("EditorInplaceRenamer"); + return key != null && key.isIn(myEditor); + } + + // Called to display a single character insertion before starting a write action and the general painting routine. + // Bypasses RepaintManager (c.repaint, c.paintComponent) and double buffering (g.paintImmediately) to minimize visual lag. + // TODO Should be replaced with the generic paintImmediately(event) call when we implement typing without starting write actions. + private void paintImmediately(Graphics g, int offset, char c, boolean insert) { + if (g == null) return; // editor component is currently not displayable + + TextAttributes attributes = ((LexerEditorHighlighter)getHighlighter()).getAttributes((DocumentImpl)getDocument(), offset, c); + + int fontType = attributes.getFontType(); + FontInfo fontInfo = EditorUtil.fontForChar(c, attributes.getFontType(), myEditor); + Font font = fontInfo.getFont(); + + // it's more reliable to query actual font metrics + FontMetrics fontMetrics = myEditor.getFontMetrics(fontType); + + int charWidth = fontMetrics.charWidth(c); + + int delta = charWidth; + + if (!insert && offset < getDocument().getTextLength()) { + delta -= fontMetrics.charWidth(getDocument().getCharsSequence().charAt(offset)); + } + + Rectangle tailArea = lineRectangleBetween(offset, getDocument().getLineEndOffset(myEditor.offsetToLogicalLine(offset))); + if (tailArea.isEmpty()) { + tailArea.width += EditorUtil.getSpaceWidth(fontType, myEditor); // include caret + } + + Color background = attributes.getBackgroundColor() == null ? getCaretRowBackground() : attributes.getBackgroundColor(); + + Rectangle newArea = lineRectangleBetween(offset, offset); + newArea.width += charWidth; + + String newText = Character.toString(c); + Point point = newArea.getLocation(); + int ascent = myEditor.getAscent(); + Color foreground = attributes.getForegroundColor() == null ? myEditor.getForegroundColor() : attributes.getForegroundColor(); + + EditorUIUtil.setupAntialiasing(g); + + // pre-compute all the arguments beforehand to minimize delays between the calls (as there's no double-buffering) + if (delta != 0) { + shift(g, tailArea, delta); + } + fill(g, newArea, background); + print(g, newText, point, ascent, font, foreground); + + // flush changes (there can be batching / buffering in video driver) + Toolkit.getDefaultToolkit().sync(); + } + + private boolean canPaintImmediately(@NotNull DocumentEvent e) { + return getDocument() instanceof DocumentImpl && + !isInplaceRenamerActive() && + StringUtil.indexOf(e.getOldFragment(), '\n') == -1 && + StringUtil.indexOf(e.getNewFragment(), '\n') == -1 && + !(e.getNewLength() == 1 && DOCUMENT_CHARS_TO_SKIP.contains(e.getNewFragment().charAt(0))); + } + + // Called to display insertion / deletion / replacement within a single line before the general painting routine. + // Bypasses RepaintManager (c.repaint, c.paintComponent) and double buffering (g.paintImmediately) to minimize visual lag. + private void paintImmediately(Graphics g, @NotNull DocumentEvent e) { + if (g == null) return; // editor component is currently not displayable + + int offset = e.getOffset(); + String newText = e.getNewFragment().toString(); + Rectangle newArea = lineRectangleBetween(offset, offset + newText.length()); + int delta = newArea.width - myOldArea.width; + Color background = getCaretRowBackground(); + + if (delta != 0) { + if (delta < 0) { + // Pre-paint carets at new positions, if needed, before shifting the tail area (to avoid flickering), + // because painting takes some time while copyArea is almost instantaneous. + EditorImpl.CaretRectangle[] caretRectangles = myEditor.getCaretCursor().getCaretLocations(true); + if (caretRectangles != null) { + for (EditorImpl.CaretRectangle it : caretRectangles) { + Rectangle r = toRectangle(it); + if (myOldArea.contains(r) && !newArea.contains(r)) { + myEditor.getCaretCursor().paintAt(g, it.myPoint.x - delta, it.myPoint.y, it.myWidth, it.myCaret); + } + } + } + } + + shift(g, myOldTailArea, delta); + + if (delta < 0) { + Rectangle remainingArea = new Rectangle(myOldTailArea.x + myOldTailArea.width + delta, + myOldTailArea.y, -delta, myOldTailArea.height); + fill(g, remainingArea, background); + } + } + + if (!newArea.isEmpty()) { + TextAttributes attributes = getHighlighter().createIterator(offset).getTextAttributes(); + + Point point = newArea.getLocation(); + int ascent = myEditor.getAscent(); + // simplified font selection (based on the first character) + FontInfo fontInfo = EditorUtil.fontForChar(newText.charAt(0), attributes.getFontType(), myEditor); + Font font = fontInfo.getFont(); + + Color foreground = attributes.getForegroundColor() == null ? myEditor.getForegroundColor() : attributes.getForegroundColor(); + + EditorUIUtil.setupAntialiasing(g); + + // pre-compute all the arguments beforehand to minimize delay between the calls (as there's no double-buffering) + fill(g, newArea, background); + print(g, newText, point, ascent, font, foreground); + } + + // flush changes (there can be batching / buffering in video driver) + Toolkit.getDefaultToolkit().sync(); + } + + @NotNull + private Rectangle lineRectangleBetween(int begin, int end) { + Point p1 = myEditor.offsetToXY(begin, false); + Point p2 = myEditor.offsetToXY(end, false); + // When soft wrap is present, handle only the first visual line (for simplicity, yet it works reasonably well) + int x2 = p1.y == p2.y ? p2.x : Math.max(p1.x, getContentComponent().getWidth() - myEditor.getVerticalScrollBar().getWidth()); + return new Rectangle(p1.x, p1.y, x2 - p1.x, myEditor.getLineHeight()); + } + + @NotNull + private Rectangle toRectangle(@NotNull EditorImpl.CaretRectangle caretRectangle) { + Point p = caretRectangle.myPoint; + return new Rectangle(p.x, p.y, caretRectangle.myWidth, myEditor.getLineHeight()); + } + + @NotNull + private Color getCaretRowBackground() { + Color color = getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR); + return color == null ? myEditor.getBackgroundColor() : color; + } + + private static void shift(@NotNull Graphics g, @NotNull Rectangle r, int delta) { + g.copyArea(r.x, r.y, r.width, r.height, delta, 0); + } + + private static void fill(@NotNull Graphics g, @NotNull Rectangle r, @NotNull Color color) { + g.setColor(color); + g.fillRect(r.x, r.y, r.width, r.height); + } + + private static void print(@NotNull Graphics g, @NotNull String text, @NotNull Point point, + int ascent, @NotNull Font font, @NotNull Color color) { + g.setFont(font); + g.setColor(color); + g.drawString(text, point.x, point.y + ascent); + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/SummaryStatistics.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SummaryStatistics.java new file mode 100644 index 000000000000..06d1d08ed16b --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SummaryStatistics.java @@ -0,0 +1,64 @@ +/* + * 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; + +/** + * @author Pavel Fatin + */ +class SummaryStatistics { + private int myCount = 0; + + private double myMin = Double.MAX_VALUE; + + private double myMax = 0.0D; + + private double myMean = 0.0D; + + private double myS = 0.0D; + + void accept(double value) { + myCount++; + + myMin = Math.min(myMin, value); + + myMax = Math.max(myMax, value); + + if (myCount == 1) { + myMean = value; + } + else { + double previousMean = myMean; + myMean += (value - myMean) / myCount; + myS += (value - previousMean) * (value - myMean); + } + } + + double getMin() { + return myMin == Double.MAX_VALUE ? 0.0D : myMin; + } + + double getMax() { + return myMax; + } + + double getMean() { + return myMean; + } + + double getStandardDeviation() { + return Math.sqrt(myS / (myCount - 1)); + } +}