inlined ImmediatePainterImpl

This commit is contained in:
Alexey Kudravtsev
2015-09-17 14:35:42 +03:00
parent 1d27803183
commit 9217c98af8
3 changed files with 422 additions and 461 deletions
@@ -349,7 +349,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
myCommandProcessor = CommandProcessor.getInstance();
myImmediatePainter = new ImmediatePainterImpl(this);
myImmediatePainter = new ImmediatePainter(this);
if (project != null) {
myConnection = project.getMessageBus().connect();
@@ -15,20 +15,436 @@
*/
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.Set;
/**
* @author Pavel Fatin
*/
interface ImmediatePainter {
void paintCharacter(Graphics g, char c);
class ImmediatePainter {
// Characters that excluded from zero-latency painting after key typing
private static final Set<Character> KEY_CHARS_TO_SKIP =
new HashSet<Character>(Arrays.asList('\n', '\t', '(', ')', '[', ']', '{', '}', '"', '\''));
void beforeUpdate(@NotNull DocumentEvent e);
// Characters that excluded from zero-latency painting after document update
private static final Set<Character> DOCUMENT_CHARS_TO_SKIP =
new HashSet<Character>(Arrays.asList(')', ']', '}', '"', '\''));
void paintUpdate(Graphics g, @NotNull DocumentEvent e);
// 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<Class> IMMEDIATE_EDITING_ACTIONS = new HashSet<Class>(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";
void afterPainting();
public static final String ZERO_LATENCY_TYPING_DEBUG_KEY = "editor.zero.latency.typing.debug";
public static final int DEBUG_PAUSE_DURATION = 1000;
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();
ImmediatePainter(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();
}
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 = "Zero-latency: "+isZeroLatencyTypingEnabled()+"; "+myTypingLatencyMeter.stat();
printToEventLog(stats);
resetTypingLatencyStats();
}
}
}
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();
}
public void afterPainting() {
if (isTypingLatencyStatsEnabled()) {
myTypingLatencyMeter.registerFinish();
}
}
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
}
}
}
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 isZeroLatencyTypingDebugEnabled() {
return Registry.is(ZERO_LATENCY_TYPING_DEBUG_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(java.util.List<Caret> 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(java.util.List<Caret> 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();
if (isZeroLatencyTypingDebugEnabled()) {
pause();
}
}
private static void pause() {
try {
Thread.sleep(DEBUG_PAUSE_DURATION);
}
catch (InterruptedException e) {
// ...
}
}
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();
if (isZeroLatencyTypingDebugEnabled()) {
pause();
}
}
@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);
}
}
@@ -1,455 +0,0 @@
/*
* 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<Character> KEY_CHARS_TO_SKIP =
new HashSet<Character>(Arrays.asList('\n', '\t', '(', ')', '[', ']', '{', '}', '"', '\''));
// Characters that excluded from zero-latency painting after document update
private static final Set<Character> DOCUMENT_CHARS_TO_SKIP =
new HashSet<Character>(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<Class> IMMEDIATE_EDITING_ACTIONS = new HashSet<Class>(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 ZERO_LATENCY_TYPING_DEBUG_KEY = "editor.zero.latency.typing.debug";
public static final int DEBUG_PAUSE_DURATION = 1000;
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 = "Zero-latency: "+isZeroLatencyTypingEnabled()+"; "+myTypingLatencyMeter.stat();
printToEventLog(stats);
resetTypingLatencyStats();
}
}
}
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 isZeroLatencyTypingDebugEnabled() {
return Registry.is(ZERO_LATENCY_TYPING_DEBUG_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<Caret> 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<Caret> 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();
if (isZeroLatencyTypingDebugEnabled()) {
pause();
}
}
private static void pause() {
try {
Thread.sleep(DEBUG_PAUSE_DURATION);
}
catch (InterruptedException e) {
// ...
}
}
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();
if (isZeroLatencyTypingDebugEnabled()) {
pause();
}
}
@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);
}
}