/* * Copyright 2000-2014 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.Patches; import com.intellij.application.options.OptionsConstants; import com.intellij.codeInsight.hint.DocumentFragmentTooltipRenderer; import com.intellij.codeInsight.hint.EditorFragmentComponent; import com.intellij.codeInsight.hint.TooltipController; import com.intellij.codeInsight.hint.TooltipGroup; import com.intellij.concurrency.JobScheduler; import com.intellij.diagnostic.Dumpable; import com.intellij.diagnostic.LogMessageEx; import com.intellij.ide.*; import com.intellij.ide.dnd.DnDManager; import com.intellij.ide.ui.UISettings; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.ex.ActionManagerEx; import com.intellij.openapi.actionSystem.impl.MouseGestureManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.application.impl.LaterInvocator; import com.intellij.openapi.command.CommandProcessor; 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.EditorActionUtil; 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.EditorUtil; import com.intellij.openapi.editor.ex.util.EmptyEditorHighlighter; import com.intellij.openapi.editor.highlighter.EditorHighlighter; import com.intellij.openapi.editor.highlighter.HighlighterClient; import com.intellij.openapi.editor.impl.event.MarkupModelListener; import com.intellij.openapi.editor.impl.softwrap.SoftWrapAppliancePlaces; import com.intellij.openapi.editor.impl.softwrap.SoftWrapDrawingType; import com.intellij.openapi.editor.markup.*; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory; import com.intellij.openapi.fileEditor.impl.EditorsSplitters; import com.intellij.openapi.keymap.Keymap; import com.intellij.openapi.keymap.KeymapManager; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Queryable; import com.intellij.openapi.util.*; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.openapi.wm.IdeGlassPane; import com.intellij.openapi.wm.ToolWindowAnchor; import com.intellij.openapi.wm.ex.ToolWindowManagerEx; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.ui.*; import com.intellij.ui.components.JBLayeredPane; import com.intellij.ui.components.JBScrollBar; import com.intellij.ui.components.JBScrollPane; import com.intellij.ui.components.OrphanGuardian; import com.intellij.util.*; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.ContainerUtilRt; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.text.CharArrayCharSequence; import com.intellij.util.text.CharArrayUtil; import com.intellij.util.ui.ButtonlessScrollBarUI; import com.intellij.util.ui.GraphicsUtil; import com.intellij.util.ui.MacUIUtil; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.update.Activatable; import com.intellij.util.ui.update.UiNotifyConnector; import gnu.trove.TIntArrayList; import gnu.trove.TIntFunction; import gnu.trove.TIntHashSet; import gnu.trove.TIntIntHashMap; import org.intellij.lang.annotations.JdkConstants; import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import javax.swing.*; import javax.swing.Timer; import javax.swing.border.Border; import javax.swing.border.EmptyBorder; import javax.swing.plaf.ScrollBarUI; import javax.swing.plaf.basic.BasicScrollBarUI; import java.awt.*; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetAdapter; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.event.*; import java.awt.font.TextHitInfo; import java.awt.im.InputMethodRequests; import java.awt.image.BufferedImage; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.text.AttributedCharacterIterator; import java.text.AttributedString; import java.text.CharacterIterator; import java.util.*; import java.util.List; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; public final class EditorImpl extends UserDataHolderBase implements EditorEx, HighlighterClient, Queryable, Dumpable { private static final boolean isOracleRetina = UIUtil.isRetina() && SystemInfo.isOracleJvm; private static final int MIN_FONT_SIZE = 8; private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl"); private static final Key DND_COMMAND_KEY = Key.create("DndCommand"); @NonNls public static final Object IGNORE_MOUSE_TRACKING = "ignore_mouse_tracking"; public static final Key PERMANENT_HEADER = Key.create("PERMANENT_HEADER"); public static final Key DO_DOCUMENT_UPDATE_TEST = Key.create("DoDocumentUpdateTest"); private static final boolean HONOR_CAMEL_HUMPS_ON_TRIPLE_CLICK = Boolean.parseBoolean(System.getProperty("idea.honor.camel.humps.on.triple.click")); private static final Key BUFFER = Key.create("buffer"); public static final JBColor CURSOR_FOREGROUND = new JBColor(Gray._255, Gray._0); @NotNull private final DocumentEx myDocument; private final JPanel myPanel; @NotNull private final JScrollPane myScrollPane; @NotNull private final EditorComponentImpl myEditorComponent; @NotNull private final EditorGutterComponentImpl myGutterComponent; private final TraceableDisposable myTraceableDisposable = new TraceableDisposable(new Throwable()); static { ComplementaryFontsRegistry.getFontAbleToDisplay(' ', 0, 0, UIManager.getFont("Label.font").getFamily()); // load costly font info } private final CommandProcessor myCommandProcessor; @NotNull private final MyScrollBar myVerticalScrollBar; private final List myMouseListeners = ContainerUtil.createLockFreeCopyOnWriteList(); @NotNull private final List myMouseMotionListeners = ContainerUtil.createLockFreeCopyOnWriteList(); private int myCharHeight = -1; private int myLineHeight = -1; private int myDescent = -1; private boolean myIsInsertMode = true; @NotNull private final CaretCursor myCaretCursor; private final ScrollingTimer myScrollingTimer = new ScrollingTimer(); @SuppressWarnings("RedundantStringConstructorCall") private final Object MOUSE_DRAGGED_GROUP = new String("MouseDraggedGroup"); @NotNull private final SettingsImpl mySettings; private boolean isReleased = false; @Nullable private MouseEvent myMousePressedEvent = null; @Nullable private MouseEvent myMouseMovedEvent = null; /** * Holds information about area where mouse was pressed. */ @Nullable private EditorMouseEventArea myMousePressArea; private int mySavedSelectionStart = -1; private int mySavedSelectionEnd = -1; private int myLastColumnNumber = 0; private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this); private MyEditable myEditable; @NotNull private EditorColorsScheme myScheme; private ArrowPainter myTabPainter; private final boolean myIsViewer; @NotNull private final SelectionModelImpl mySelectionModel; @NotNull private final EditorMarkupModelImpl myMarkupModel; @NotNull private final FoldingModelImpl myFoldingModel; @NotNull private final ScrollingModelImpl myScrollingModel; @NotNull private final CaretModelImpl myCaretModel; @NotNull private final SoftWrapModelImpl mySoftWrapModel; @NotNull private static final RepaintCursorCommand ourCaretBlinkingCommand; private MessageBusConnection myConnection; private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE; @Nullable private FoldRegion myMouseSelectedRegion = null; private static final int MOUSE_SELECTION_STATE_NONE = 0; private static final int MOUSE_SELECTION_STATE_WORD_SELECTED = 1; private static final int MOUSE_SELECTION_STATE_LINE_SELECTED = 2; private EditorHighlighter myHighlighter; private Disposable myHighlighterDisposable = Disposer.newDisposable(); private final TextDrawingCallback myTextDrawingCallback = new MyTextDrawingCallback(); @MagicConstant(intValues = {VERTICAL_SCROLLBAR_LEFT, VERTICAL_SCROLLBAR_RIGHT}) private int myScrollBarOrientation; private boolean myMousePressedInsideSelection; private FontMetrics myPlainFontMetrics; private FontMetrics myBoldFontMetrics; private FontMetrics myItalicFontMetrics; private FontMetrics myBoldItalicFontMetrics; private static final int CACHED_CHARS_BUFFER_SIZE = 300; private final ArrayList myFontCache = new ArrayList(); @Nullable private FontInfo myCurrentFontType = null; private final EditorSizeContainer mySizeContainer = new EditorSizeContainer(); private boolean myUpdateCursor; private int myCaretUpdateVShift; @Nullable private final Project myProject; private long myMouseSelectionChangeTimestamp; private int mySavedCaretOffsetForDNDUndoHack; private final List myFocusListeners = ContainerUtil.createLockFreeCopyOnWriteList(); private MyInputMethodHandler myInputMethodRequestsHandler; private InputMethodRequests myInputMethodRequestsSwingWrapper; private boolean myIsOneLineMode; private boolean myIsRendererMode; private VirtualFile myVirtualFile; private boolean myIsColumnMode = false; @Nullable private Color myForcedBackground = null; @Nullable private Dimension myPreferredSize; private int myVirtualPageHeight; private Alarm myAppleRepaintAlarm; private final Alarm myMouseSelectionStateAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); private Runnable myMouseSelectionStateResetRunnable; private boolean myEmbeddedIntoDialogWrapper; @Nullable private CachedFontContent myLastCache; private int myDragOnGutterSelectionStartLine = -1; /** * Positive value is assumed to indicate that space width for all interested font styles (bold, italic etc) is equal. * That value is stored at the current field. */ private int myCommonSpaceWidth; /** * There is a possible case that specific font is used for particular text drawing operation (e.g. for 'before' and 'after' * soft wraps drawings). Hence, even if mySpacesHaveSameWidth is true, space size for that specific * font may be different. So, we define additional flag that should indicate that {@link #myLastCache} should be reset. */ private boolean myForceRefreshFont; private boolean mySoftWrapsChanged; private Color myLastBackgroundColor = null; private Point myLastBackgroundPosition = null; private int myLastBackgroundWidth; private static final boolean ourIsUnitTestMode = ApplicationManager.getApplication().isUnitTestMode(); @NotNull private final JPanel myHeaderPanel; @Nullable private MouseEvent myInitialMouseEvent; private boolean myIgnoreMouseEventsConsecutiveToInitial; private EditorDropHandler myDropHandler; private char[] myPrefixText; private TextAttributes myPrefixAttributes; private int myPrefixWidthInPixels; @NotNull private final IndentsModel myIndentsModel; @Nullable private CharSequence myPlaceholderText; private int myLastPaintedPlaceholderWidth; private boolean myStickySelection; private int myStickySelectionStart; private boolean myScrollToCaret = true; private boolean myPurePaintingMode; private boolean myPaintSelection; private final EditorSizeAdjustmentStrategy mySizeAdjustmentStrategy = new EditorSizeAdjustmentStrategy(); private final Disposable myDisposable = Disposer.newDisposable(); private LogicalPosition myLastMousePressedLocation; private VisualPosition myTargetMultiSelectionPosition; private boolean myMultiSelectionInProgress; private boolean myLastPressCreatedCaret; // Set when the selection (normal or block one) initiated by mouse drag becomes noticeable (at least one character is selected). // Reset on mouse press event. private boolean myCurrentDragIsSubstantial; private CaretImpl myPrimaryCaret; static { ourCaretBlinkingCommand = new RepaintCursorCommand(); ourCaretBlinkingCommand.start(); } EditorImpl(@NotNull Document document, boolean viewer, @Nullable Project project) { assertIsDispatchThread(); myProject = project; myDocument = (DocumentEx)document; if (myDocument instanceof DocumentImpl) { ((DocumentImpl)myDocument).requestTabTracking(); } myScheme = createBoundColorSchemeDelegate(null); initTabPainter(); myIsViewer = viewer; mySettings = new SettingsImpl(this, project); mySelectionModel = new SelectionModelImpl(this); myMarkupModel = new EditorMarkupModelImpl(this); myFoldingModel = new FoldingModelImpl(this); myCaretModel = new CaretModelImpl(this); mySoftWrapModel = new SoftWrapModelImpl(this); mySizeContainer.reset(); myCommandProcessor = CommandProcessor.getInstance(); if (project != null) { myConnection = project.getMessageBus().connect(); myConnection.subscribe(DocumentBulkUpdateListener.TOPIC, new EditorDocumentBulkUpdateAdapter()); } MarkupModelListener markupModelListener = new MarkupModelListener() { private boolean areRenderersInvolved(@NotNull RangeHighlighterEx highlighter) { return highlighter.getCustomRenderer() != null || highlighter.getGutterIconRenderer() != null || highlighter.getLineMarkerRenderer() != null || highlighter.getLineSeparatorRenderer() != null; } @Override public void afterAdded(@NotNull RangeHighlighterEx highlighter) { attributesChanged(highlighter, areRenderersInvolved(highlighter)); } @Override public void beforeRemoved(@NotNull RangeHighlighterEx highlighter) { attributesChanged(highlighter, areRenderersInvolved(highlighter)); } @Override public void attributesChanged(@NotNull RangeHighlighterEx highlighter, boolean renderersChanged) { if (myDocument.isInBulkUpdate()) return; // bulkUpdateFinished() will repaint anything int textLength = myDocument.getTextLength(); clearTextWidthCache(); int start = Math.min(Math.max(highlighter.getAffectedAreaStartOffset(), 0), textLength); int end = Math.min(Math.max(highlighter.getAffectedAreaEndOffset(), 0), textLength); int startLine = start == -1 ? 0 : myDocument.getLineNumber(start); int endLine = end == -1 ? myDocument.getLineCount() : myDocument.getLineNumber(end); repaintLines(Math.max(0, startLine - 1), Math.min(endLine + 1, getDocument().getLineCount())); // optimization: there is no need to repaint error stripe if the highlighter is invisible on it if (renderersChanged || highlighter.getErrorStripeMarkColor() != null) { ((EditorMarkupModelImpl)getMarkupModel()).repaint(start, end); } if (renderersChanged) { updateGutterSize(); } updateCaretCursor(); } }; ((MarkupModelEx)DocumentMarkupModel.forDocument(myDocument, myProject, true)).addMarkupModelListener(myCaretModel, markupModelListener); getMarkupModel().addMarkupModelListener(myCaretModel, markupModelListener); myDocument.addDocumentListener(myFoldingModel, myCaretModel); myDocument.addDocumentListener(myCaretModel, myCaretModel); myDocument.addDocumentListener(mySelectionModel, myCaretModel); myDocument.addDocumentListener(new EditorDocumentAdapter(), myCaretModel); myDocument.addDocumentListener(mySoftWrapModel, myCaretModel); myFoldingModel.addListener(mySoftWrapModel, myCaretModel); myIndentsModel = new IndentsModelImpl(this); myCaretModel.addCaretListener(new CaretListener() { @Nullable private LightweightHint myCurrentHint = null; @Nullable private IndentGuideDescriptor myCurrentCaretGuide = null; @Override public void caretPositionChanged(CaretEvent e) { if (myStickySelection) { int selectionStart = Math.min(myStickySelectionStart, getDocument().getTextLength() - 1); mySelectionModel.setSelection(selectionStart, myCaretModel.getVisualPosition(), myCaretModel.getOffset()); } final IndentGuideDescriptor newGuide = myIndentsModel.getCaretIndentGuide(); if (!Comparing.equal(myCurrentCaretGuide, newGuide)) { repaintGuide(newGuide); repaintGuide(myCurrentCaretGuide); myCurrentCaretGuide = newGuide; if (myCurrentHint != null) { myCurrentHint.hide(); myCurrentHint = null; } if (newGuide != null) { final Rectangle visibleArea = getScrollingModel().getVisibleArea(); final int line = newGuide.startLine; if (logicalLineToY(line) < visibleArea.y) { TextRange textRange = new TextRange(myDocument.getLineStartOffset(line), myDocument.getLineEndOffset(line)); myCurrentHint = EditorFragmentComponent.showEditorFragmentHint(EditorImpl.this, textRange, false, false); } } } } @Override public void caretAdded(CaretEvent e) { if (myPrimaryCaret != null) { myPrimaryCaret.updateVisualPosition(); // repainting old primary caret's row background } updateCaretVisualPosition(e); myPrimaryCaret = myCaretModel.getPrimaryCaret(); } @Override public void caretRemoved(CaretEvent e) { updateCaretVisualPosition(e); myPrimaryCaret = myCaretModel.getPrimaryCaret(); // repainting new primary caret's row background myPrimaryCaret.updateVisualPosition(); } }); myCaretCursor = new CaretCursor(); myFoldingModel.flushCaretShift(); myScrollBarOrientation = VERTICAL_SCROLLBAR_RIGHT; mySoftWrapModel.addSoftWrapChangeListener(new SoftWrapChangeListenerAdapter() { @Override public void recalculationEnds() { if (myCaretModel.isUpToDate()) { myCaretModel.updateVisualPosition(); } } @Override public void softWrapAdded(@NotNull SoftWrap softWrap) { mySoftWrapsChanged = true; } @Override public void softWrapsRemoved() { mySoftWrapsChanged = true; } }); mySoftWrapModel.addVisualSizeChangeListener(new VisualSizeChangeListener() { @Override public void onLineWidthsChange(int startLine, int oldEndLine, int newEndLine, @NotNull TIntIntHashMap lineWidths) { mySizeContainer.update(startLine, newEndLine, oldEndLine); for (int i = startLine; i <= newEndLine; i++) { if (lineWidths.contains(i)) { int width = lineWidths.get(i); if (width >= 0) { mySizeContainer.updateLineWidthIfNecessary(i, width); } } } } }); EditorHighlighter highlighter = new EmptyEditorHighlighter(myScheme.getAttributes(HighlighterColors.TEXT)); setHighlighter(highlighter); myEditorComponent = new EditorComponentImpl(this); myScrollPane = new MyScrollPane(); myVerticalScrollBar = (MyScrollBar)myScrollPane.getVerticalScrollBar(); myPanel = new JPanel(); myPanel.putClientProperty(OrphanGuardian.CLIENT_PROPERTY_KEY, new OrphanGuardian() { @Override public void iterateOrphans(Consumer consumer) { JComponent component = getPermanentHeaderComponent(); if (component != null && !component.isValid()) { consumer.consume(component); } } }); myHeaderPanel = new MyHeaderPanel(); myGutterComponent = new EditorGutterComponentImpl(this); initComponent(); myScrollingModel = new ScrollingModelImpl(this); if (UISettings.getInstance().PRESENTATION_MODE) { setFontSize(UISettings.getInstance().PRESENTATION_MODE_FONT_SIZE); } myGutterComponent.updateSize(); Dimension preferredSize = getPreferredSize(); myEditorComponent.setSize(preferredSize); if (Patches.APPLE_BUG_ID_3716835) { myScrollingModel.addVisibleAreaListener(new VisibleAreaListener() { @Override public void visibleAreaChanged(VisibleAreaEvent e) { if (myAppleRepaintAlarm == null) { myAppleRepaintAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); } myAppleRepaintAlarm.cancelAllRequests(); myAppleRepaintAlarm.addRequest(new Runnable() { @Override public void run() { repaint(0, myDocument.getTextLength()); } }, 50, ModalityState.stateForComponent(myEditorComponent)); } }); } updateCaretCursor(); // This hacks context layout problem where editor appears scrolled to the right just after it is created. if (!ourIsUnitTestMode) { UiNotifyConnector.doWhenFirstShown(myEditorComponent, new Runnable() { @Override public void run() { if (!isDisposed() && !myScrollingModel.isScrollingNow()) { myScrollingModel.disableAnimation(); myScrollingModel.scrollHorizontally(0); myScrollingModel.enableAnimation(); } } }); } } private static void updateCaretVisualPosition(CaretEvent e) { CaretImpl caretImpl = ((CaretImpl)e.getCaret()); if (caretImpl != null) { caretImpl.updateVisualPosition(); // repainting caret region } } @NotNull @Override public EditorColorsScheme createBoundColorSchemeDelegate(@Nullable final EditorColorsScheme customGlobalScheme) { return new MyColorSchemeDelegate(customGlobalScheme); } private void repaintGuide(@Nullable IndentGuideDescriptor guide) { if (guide != null) { repaintLines(guide.startLine, guide.endLine); } } @Override public int getPrefixTextWidthInPixels() { return myPrefixWidthInPixels; } @Override public void setPrefixTextAndAttributes(@Nullable String prefixText, @Nullable TextAttributes attributes) { myPrefixText = prefixText == null ? null : prefixText.toCharArray(); myPrefixAttributes = attributes; myPrefixWidthInPixels = 0; if (myPrefixText != null) { for (char c : myPrefixText) { LOG.assertTrue(myPrefixAttributes != null); if (myPrefixAttributes != null) { myPrefixWidthInPixels += EditorUtil.charWidth(c, myPrefixAttributes.getFontType(), this); } } } mySoftWrapModel.recalculate(); } @Override public boolean isPurePaintingMode() { return myPurePaintingMode; } @Override public void setPurePaintingMode(boolean enabled) { myPurePaintingMode = enabled; } @Override public void registerScrollBarRepaintCallback(@Nullable RepaintCallback callback) { myVerticalScrollBar.registerRepaintCallback(callback); } @Override public boolean isViewer() { return myIsViewer || myIsRendererMode; } @Override public boolean isRendererMode() { return myIsRendererMode; } @Override public void setRendererMode(boolean isRendererMode) { myIsRendererMode = isRendererMode; } @Override public void setFile(VirtualFile vFile) { myVirtualFile = vFile; reinitSettings(); } @Override public VirtualFile getVirtualFile() { return myVirtualFile; } @Override public void setSoftWrapAppliancePlace(@NotNull SoftWrapAppliancePlaces place) { getSoftWrapModel().setPlace(place); mySettings.setSoftWrapAppliancePlace(place); } @Override @NotNull public SelectionModelImpl getSelectionModel() { return mySelectionModel; } @Override @NotNull public MarkupModelEx getMarkupModel() { return myMarkupModel; } @Override @NotNull public FoldingModelImpl getFoldingModel() { return myFoldingModel; } @Override @NotNull public CaretModelImpl getCaretModel() { return myCaretModel; } @Override @NotNull public ScrollingModelEx getScrollingModel() { return myScrollingModel; } @Override @NotNull public SoftWrapModelImpl getSoftWrapModel() { return mySoftWrapModel; } @Override @NotNull public EditorSettings getSettings() { assertReadAccess(); return mySettings; } public void resetSizes() { mySizeContainer.reset(); } @Override public void reinitSettings() { assertIsDispatchThread(); myCharHeight = -1; myLineHeight = -1; myDescent = -1; myPlainFontMetrics = null; clearTextWidthCache(); boolean softWrapsUsedBefore = mySoftWrapModel.isSoftWrappingEnabled(); mySettings.reinitSettings(); mySoftWrapModel.reinitSettings(); myCaretModel.reinitSettings(); mySelectionModel.reinitSettings(); ourCaretBlinkingCommand.setBlinkCaret(mySettings.isBlinkCaret()); ourCaretBlinkingCommand.setBlinkPeriod(mySettings.getCaretBlinkPeriod()); mySizeContainer.reset(); myFoldingModel.rebuild(); if (softWrapsUsedBefore ^ mySoftWrapModel.isSoftWrappingEnabled()) { mySizeContainer.reset(); validateSize(); } for (EditorColorsScheme scheme = myScheme; scheme instanceof DelegateColorScheme; scheme = ((DelegateColorScheme)scheme).getDelegate()) { if (scheme instanceof MyColorSchemeDelegate) { ((MyColorSchemeDelegate)scheme).updateGlobalScheme(); break; } } myHighlighter.setColorScheme(myScheme); myFoldingModel.refreshSettings(); myGutterComponent.reinitSettings(); myGutterComponent.revalidate(); myEditorComponent.repaint(); initTabPainter(); updateCaretCursor(); if (myInitialMouseEvent != null) { myIgnoreMouseEventsConsecutiveToInitial = true; } if (myCaretModel.supportsMultipleCarets()) { myCaretModel.updateVisualPosition(); } else { // There is a possible case that 'use soft wrap' setting value is changed and we need to repaint all affected lines then. repaintToScreenBottom(getCaretModel().getLogicalPosition().line); int y = getCaretModel().getVisualLineStart() * getLineHeight(); myGutterComponent.repaint(0, y, myGutterComponent.getWidth(), myGutterComponent.getHeight() - y); } // make sure carets won't appear at invalid positions (e.g. on Tab width change) getCaretModel().runForEachCaret(new CaretAction() { @Override public void perform(Caret caret) { caret.moveToOffset(caret.getOffset()); } }); } private void initTabPainter() { myTabPainter = new ArrowPainter( ColorProvider.byColorsScheme(myScheme, EditorColors.WHITESPACES_COLOR), new Computable.PredefinedValueComputable(EditorUtil.getSpaceWidth(Font.PLAIN, this)), new Computable() { @Override public Integer compute() { return getCharHeight(); } } ); } public void throwDisposalError(@NonNls @NotNull String msg) { myTraceableDisposable.throwDisposalError(msg); } public void release() { assertIsDispatchThread(); if (isReleased) { throwDisposalError("Double release of editor:"); } myTraceableDisposable.kill(null); isReleased = true; myFoldingModel.dispose(); mySoftWrapModel.release(); myMarkupModel.dispose(); myLineHeight = -1; myCharHeight = -1; myDescent = -1; myPlainFontMetrics = null; myScrollingModel.dispose(); myGutterComponent.dispose(); myMousePressedEvent = null; myMouseMovedEvent = null; Disposer.dispose(myCaretModel); Disposer.dispose(mySoftWrapModel); clearCaretThread(); myFocusListeners.clear(); myMouseListeners.clear(); myMouseMotionListeners.clear(); if (myConnection != null) { myConnection.disconnect(); } if (myDocument instanceof DocumentImpl) { ((DocumentImpl)myDocument).giveUpTabTracking(); } Disposer.dispose(myDisposable); } private void clearCaretThread() { synchronized (ourCaretBlinkingCommand) { if (ourCaretBlinkingCommand.myEditor == this) { ourCaretBlinkingCommand.myEditor = null; } } } private void initComponent() { myPanel.setLayout(new BorderLayout()); myPanel.add(myHeaderPanel, BorderLayout.NORTH); myGutterComponent.setOpaque(true); myScrollPane.setViewportView(myEditorComponent); //myScrollPane.setBorder(null); myScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); myScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); myScrollPane.setRowHeaderView(myGutterComponent); myEditorComponent.setTransferHandler(new MyTransferHandler()); myEditorComponent.setAutoscrolls(true); /* Default mode till 1.4.0 * myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE); */ if (mayShowToolbar()) { JLayeredPane layeredPane = new JBLayeredPane() { @Override public void doLayout() { final Component[] components = getComponents(); final Rectangle r = getBounds(); for (Component c : components) { if (c instanceof JScrollPane) { c.setBounds(0, 0, r.width, r.height); } else { final Dimension d = c.getPreferredSize(); final MyScrollBar scrollBar = getVerticalScrollBar(); c.setBounds(r.width - d.width - scrollBar.getWidth() - 30, 20, d.width, d.height); } } } }; layeredPane.add(myScrollPane, JLayeredPane.DEFAULT_LAYER); myPanel.add(layeredPane); new ContextMenuImpl(layeredPane, myScrollPane, this); } else { myPanel.add(myScrollPane); } myEditorComponent.addKeyListener(new KeyAdapter() { @Override public void keyTyped(@NotNull KeyEvent event) { if (Patches.APPLE_BUG_ID_3337563) return; // Everything is going through InputMethods under MacOS X in JDK releases earlier than 1.4.2_03-117.1 if (event.isConsumed()) { return; } if (processKeyTyped(event)) { event.consume(); } } }); MyMouseAdapter mouseAdapter = new MyMouseAdapter(); myEditorComponent.addMouseListener(mouseAdapter); myGutterComponent.addMouseListener(mouseAdapter); MyMouseMotionListener mouseMotionListener = new MyMouseMotionListener(); myEditorComponent.addMouseMotionListener(mouseMotionListener); myGutterComponent.addMouseMotionListener(mouseMotionListener); myEditorComponent.addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { myCaretCursor.activate(); for (Caret caret : myCaretModel.getAllCarets()) { int caretLine = caret.getLogicalPosition().line; repaintLines(caretLine, caretLine); } fireFocusGained(); } @Override public void focusLost(FocusEvent e) { clearCaretThread(); for (Caret caret : myCaretModel.getAllCarets()) { int caretLine = caret.getLogicalPosition().line; repaintLines(caretLine, caretLine); } fireFocusLost(); } }); new UiNotifyConnector(myEditorComponent, new Activatable.Adapter(){ @Override public void showNotify() { myGutterComponent.updateSize(); } }); try { final DropTarget dropTarget = myEditorComponent.getDropTarget(); if (dropTarget != null) { // might be null in headless environment dropTarget.addDropTargetListener(new DropTargetAdapter() { @Override public void drop(DropTargetDropEvent e) { } @Override public void dragOver(@NotNull DropTargetDragEvent e) { Point location = e.getLocation(); getCaretModel().moveToLogicalPosition(getLogicalPositionForScreenPos(location.x, location.y, true)); getScrollingModel().scrollToCaret(ScrollType.RELATIVE); } }); } } catch (TooManyListenersException e) { LOG.error(e); } myPanel.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { myMarkupModel.recalcEditorDimensions(); myMarkupModel.repaint(-1, -1); } }); } private boolean mayShowToolbar() { return !isEmbeddedIntoDialogWrapper() && !isOneLineMode() && ContextMenuImpl.mayShowToolbar(myDocument); } @Override public void setFontSize(final int fontSize) { setFontSize(fontSize, null); } /** * Changes editor font size, attempting to keep a given point unmoved. If point is not given, top left screen corner is assumed. * * @param fontSize new font size * @param zoomCenter zoom point, relative to viewport */ private void setFontSize(final int fontSize, @Nullable Point zoomCenter) { int oldFontSize = myScheme.getEditorFontSize(); Rectangle visibleArea = myScrollingModel.getVisibleArea(); Point zoomCenterRelative = zoomCenter == null ? new Point() : zoomCenter; Point zoomCenterAbsolute = new Point(visibleArea.x + zoomCenterRelative.x, visibleArea.y + zoomCenterRelative.y); LogicalPosition zoomCenterLogical = xyToLogicalPosition(zoomCenterAbsolute).withoutVisualPositionInfo(); int oldLineHeight = getLineHeight(); int intraLineOffset = zoomCenterAbsolute.y % oldLineHeight; myScheme.setEditorFontSize(fontSize); myPropertyChangeSupport.firePropertyChange(PROP_FONT_SIZE, oldFontSize, fontSize); // Update vertical scroll bar bounds if necessary (we had a problem that use increased editor font size and it was not possible // to scroll to the bottom of the document). myScrollPane.getViewport().invalidate(); Point shiftedZoomCenterAbsolute = logicalPositionToXY(zoomCenterLogical); myScrollingModel.disableAnimation(); try { myScrollingModel.scrollToOffsets(visibleArea.x == 0 ? 0 : shiftedZoomCenterAbsolute.x - zoomCenterRelative.x, // stick to left border if it's visible shiftedZoomCenterAbsolute.y - zoomCenterRelative.y + (intraLineOffset * getLineHeight() + oldLineHeight / 2) / oldLineHeight); } finally { myScrollingModel.enableAnimation(); } } public int getFontSize() { return myScheme.getEditorFontSize(); } @NotNull public ActionCallback type(@NotNull final String text) { final ActionCallback result = new ActionCallback(); ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { for (int i = 0; i < text.length(); i++) { if (!processKeyTyped(text.charAt(i))) { result.setRejected(); return; } } result.setDone(); } }); return result; } private boolean processKeyTyped(char c) { // [vova] This is patch for Mac OS X. Under Mac "input methods" // is handled before our EventQueue consume upcoming KeyEvents. IdeEventQueue queue = IdeEventQueue.getInstance(); if (queue.shouldNotTypeInEditor() || ProgressManager.getInstance().hasModalProgressIndicator()) { return false; } FileDocumentManager manager = FileDocumentManager.getInstance(); if (manager != null) { final VirtualFile file = manager.getFile(myDocument); if (file != null && !file.isValid()) { return false; } } ActionManagerEx actionManager = ActionManagerEx.getInstanceEx(); DataContext dataContext = getDataContext(); actionManager.fireBeforeEditorTyping(c, dataContext); MacUIUtil.hideCursor(); EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext); return true; } private void fireFocusLost() { for (FocusChangeListener listener : myFocusListeners) { listener.focusLost(this); } } private void fireFocusGained() { for (FocusChangeListener listener : myFocusListeners) { listener.focusGained(this); } } @Override public void setHighlighter(@NotNull final EditorHighlighter highlighter) { assertIsDispatchThread(); final Document document = getDocument(); Disposer.dispose(myHighlighterDisposable); document.addDocumentListener(highlighter); myHighlighter = highlighter; myHighlighterDisposable = new Disposable() { @Override public void dispose() { document.removeDocumentListener(highlighter); } }; Disposer.register(myDisposable, myHighlighterDisposable); highlighter.setEditor(this); highlighter.setText(document.getImmutableCharSequence()); EditorHighlighterCache.rememberEditorHighlighterForCachesOptimization(document, highlighter); if (myPanel != null) { reinitSettings(); } } @NotNull @Override public EditorHighlighter getHighlighter() { assertReadAccess(); return myHighlighter; } @Override @NotNull public EditorComponentImpl getContentComponent() { return myEditorComponent; } @NotNull @Override public EditorGutterComponentEx getGutterComponentEx() { return myGutterComponent; } @Override public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { myPropertyChangeSupport.addPropertyChangeListener(listener); } @Override public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { myPropertyChangeSupport.removePropertyChangeListener(listener); } @Override public void setInsertMode(boolean mode) { assertIsDispatchThread(); boolean oldValue = myIsInsertMode; myIsInsertMode = mode; myPropertyChangeSupport.firePropertyChange(PROP_INSERT_MODE, oldValue, mode); if (myCaretModel.supportsMultipleCarets()) { myCaretCursor.repaint(); } else { //Repaint the caret line by moving caret to the same place LogicalPosition caretPosition = getCaretModel().getLogicalPosition(); getCaretModel().moveToLogicalPosition(caretPosition); } } @Override public boolean isInsertMode() { return myIsInsertMode; } @Override public void setColumnMode(boolean mode) { assertIsDispatchThread(); boolean oldValue = myIsColumnMode; myIsColumnMode = mode; myPropertyChangeSupport.firePropertyChange(PROP_COLUMN_MODE, oldValue, mode); } @Override public boolean isColumnMode() { return myIsColumnMode; } private int yPositionToVisibleLine(int y) { assert y >= 0 : y; return y / getLineHeight(); } @Override @NotNull public VisualPosition xyToVisualPosition(@NotNull Point p) { int line = yPositionToVisibleLine(p.y); int px = p.x; if (line == 0 && myPrefixText != null) { px -= myPrefixWidthInPixels; } int textLength = myDocument.getTextLength(); LogicalPosition logicalPosition = visualToLogicalPosition(new VisualPosition(line, 0)); int offset = logicalPositionToOffset(logicalPosition); if (offset >= textLength) return new VisualPosition(line, EditorUtil.columnsNumber(p.x, EditorUtil.getSpaceWidth(Font.PLAIN, this))); // There is a possible case that starting logical line is split by soft-wraps and it's part after the split should be drawn. // We mark that we're under such circumstances then. boolean activeSoftWrapProcessed = logicalPosition.softWrapLinesOnCurrentLogicalLine <= 0; CharSequence text = myDocument.getImmutableCharSequence(); LogicalPosition endLogicalPosition = visualToLogicalPosition(new VisualPosition(line + 1, 0)); int endOffset = logicalPositionToOffset(endLogicalPosition); if (offset > endOffset) { LogMessageEx.error(LOG, "Detected invalid (x; y)->VisualPosition processing", String.format( "Given point: %s, mapped to visual line %d. Visual(%d; %d) is mapped to " + "logical position '%s' which is mapped to offset %d (start offset). Visual(%d; %d) is mapped to logical '%s' which is mapped " + "to offset %d (end offset). State: %s", p, line, line, 0, logicalPosition, offset, line + 1, 0, endLogicalPosition, endOffset, dumpState() )); return new VisualPosition(line, EditorUtil.columnsNumber(p.x, EditorUtil.getSpaceWidth(Font.PLAIN, this))); } IterationState state = new IterationState(this, offset, endOffset, false); int fontType = state.getMergedAttributes().getFontType(); int spaceSize = EditorUtil.getSpaceWidth(fontType, this); int x = 0; int charWidth; boolean onSoftWrapDrawing = false; char c = ' '; int prevX = 0; int column = 0; outer: while (true) { charWidth = -1; if (offset >= textLength) { break; } if (offset >= state.getEndOffset()) { state.advance(); fontType = state.getMergedAttributes().getFontType(); } SoftWrap softWrap = mySoftWrapModel.getSoftWrap(offset); if (softWrap != null) { if (activeSoftWrapProcessed) { prevX = x; charWidth = getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED); x += charWidth; if (x >= px) { onSoftWrapDrawing = true; } else { column++; } break outer; } else { CharSequence softWrapText = softWrap.getText(); for (int i = 1/*Assuming line feed is located at the first position*/; i < softWrapText.length(); i++) { c = softWrapText.charAt(i); prevX = x; charWidth = charToVisibleWidth(c, fontType, x); x += charWidth; if (x >= px) { break outer; } column += EditorUtil.columnsNumber(c, x, prevX, spaceSize); } // Process 'after soft wrap' sign. prevX = x; charWidth = mySoftWrapModel.getMinDrawingWidthInPixels(SoftWrapDrawingType.AFTER_SOFT_WRAP); x += charWidth; if (x >= px) { onSoftWrapDrawing = true; break outer; } column++; activeSoftWrapProcessed = true; } } FoldRegion region = state.getCurrentFold(); if (region != null) { char[] placeholder = region.getPlaceholderText().toCharArray(); for (char aPlaceholder : placeholder) { c = aPlaceholder; x += EditorUtil.charWidth(c, fontType, this); if (x >= px) { break outer; } column++; } offset = region.getEndOffset(); } else { prevX = x; c = text.charAt(offset); if (c == '\n') { break; } charWidth = charToVisibleWidth(c, fontType, x); x += charWidth; if (x >= px) { break; } column += EditorUtil.columnsNumber(c, x, prevX, spaceSize); offset++; } } if (charWidth < 0) { charWidth = EditorUtil.charWidth(c, fontType, this); } if (charWidth < 0) { charWidth = spaceSize; } if (x >= px && c == '\t' && !onSoftWrapDrawing) { if (mySettings.isCaretInsideTabs()) { column += (px - prevX) / spaceSize; if ((px - prevX) % spaceSize > spaceSize / 2) column++; } else if ((x - px) * 2 < x - prevX) { column += EditorUtil.columnsNumber(c, x, prevX, spaceSize); } } else { if (x >= px) { if (c != '\n' && (x - px) * 2 < charWidth) column++; } else { int diff = px - x; column += diff / spaceSize; if (diff % spaceSize * 2 >= spaceSize) { column++; } } } return new VisualPosition(line, column); } /** * Allows to answer how much width requires given char to be represented on a screen. * * @param c target character * @param fontType font type to use for representation of the given character * @param currentX current 'x' position on a line where given character should be displayed * @return width required to represent given char with the given settings on a screen; * '0' if given char is a line break */ private int charToVisibleWidth(char c, @JdkConstants.FontStyle int fontType, int currentX) { if (c == '\n') { return 0; } if (c == '\t') { return EditorUtil.nextTabStop(currentX, this) - currentX; } return EditorUtil.charWidth(c, fontType, this); } @Override @NotNull public VisualPosition offsetToVisualPosition(int offset) { return logicalToVisualPosition(offsetToLogicalPosition(offset)); } @Override @NotNull public LogicalPosition offsetToLogicalPosition(int offset) { return offsetToLogicalPosition(offset, true); } @NotNull @Override public LogicalPosition offsetToLogicalPosition(int offset, boolean softWrapAware) { if (softWrapAware) { return mySoftWrapModel.offsetToLogicalPosition(offset); } int line = offsetToLogicalLine(offset, false); int column = calcColumnNumber(offset, line, false, myDocument.getImmutableCharSequence()); return new LogicalPosition(line, column); } @TestOnly public void setCaretActive() { synchronized (ourCaretBlinkingCommand) { ourCaretBlinkingCommand.myEditor = this; } } // optimization: do not do column calculations here since we are interested in line number only public int offsetToVisualLine(int offset) { int textLength = getDocument().getTextLength(); if (offset >= textLength) { return Math.max(0, getVisibleLineCount() - 1); // lines are 0 based } int line = offsetToLogicalLine(offset); int lineStartOffset = line >= myDocument.getLineCount() ? myDocument.getTextLength() : myDocument.getLineStartOffset(line); int result = logicalToVisualLine(line); // There is a possible case that logical line that contains target offset is soft-wrapped (represented in more than one visual // line). Hence, we need to perform necessary adjustments to the visual line that is used to show logical line start if necessary. int i = getSoftWrapModel().getSoftWrapIndex(lineStartOffset); if (i < 0) { i = -i - 1; } List softWraps = getSoftWrapModel().getRegisteredSoftWraps(); for (; i < softWraps.size(); i++) { SoftWrap softWrap = softWraps.get(i); if (softWrap.getStart() > offset) { break; } result++; // Assuming that every soft wrap contains only one virtual line feed symbol } return result; } private int logicalToVisualLine(int line) { assertReadAccess(); return logicalToVisualPosition(new LogicalPosition(line, 0)).line; } @Override @NotNull public LogicalPosition xyToLogicalPosition(@NotNull Point p) { Point pp = p.x >= 0 && p.y >= 0 ? p : new Point(Math.max(p.x, 0), Math.max(p.y, 0)); return visualToLogicalPosition(xyToVisualPosition(pp)); } private int logicalLineToY(int line) { VisualPosition visible = logicalToVisualPosition(new LogicalPosition(line, 0)); return visibleLineToY(visible.line); } @Override @NotNull public Point logicalPositionToXY(@NotNull LogicalPosition pos) { VisualPosition visible = logicalToVisualPosition(pos); return visualPositionToXY(visible); } @Override @NotNull public Point visualPositionToXY(@NotNull VisualPosition visible) { int y = visibleLineToY(visible.line); LogicalPosition logical = visualToLogicalPosition(new VisualPosition(visible.line, 0)); int logLine = logical.line; int lineStartOffset = -1; int reserved = 0; int column = visible.column; if (logical.softWrapLinesOnCurrentLogicalLine > 0) { int linesToSkip = logical.softWrapLinesOnCurrentLogicalLine; List softWraps = getSoftWrapModel().getSoftWrapsForLine(logLine); for (SoftWrap softWrap : softWraps) { if (myFoldingModel.isOffsetCollapsed(softWrap.getStart()) && myFoldingModel.isOffsetCollapsed(softWrap.getStart() - 1)) { continue; } linesToSkip--; // Assuming here that every soft wrap has exactly one line feed if (linesToSkip > 0) { continue; } lineStartOffset = softWrap.getStart(); int widthInColumns = softWrap.getIndentInColumns(); int widthInPixels = softWrap.getIndentInPixels(); if (widthInColumns <= column) { column -= widthInColumns; reserved = widthInPixels; } else { char[] softWrapChars = softWrap.getChars(); int i = CharArrayUtil.lastIndexOf(softWrapChars, '\n', 0, softWrapChars.length); int start = 0; if (i >= 0) { start = i + 1; } return new Point(EditorUtil.textWidth(this, softWrap.getText(), start, column + 1, Font.PLAIN, 0), y); } break; } } if (logLine < 0) { lineStartOffset = 0; } else if (lineStartOffset < 0) { if (logLine >= myDocument.getLineCount()) { lineStartOffset = myDocument.getTextLength(); } else { lineStartOffset = myDocument.getLineStartOffset(logLine); } } int x = getTabbedTextWidth(lineStartOffset, column, reserved); return new Point(x, y); } private int calcEndOffset(int startOffset, int visualColumn) { FoldRegion[] regions = myFoldingModel.fetchTopLevel(); if (regions == null) { return startOffset + visualColumn; } int low = 0; int high = regions.length - 1; int i = -1; while (low <= high) { int mid = (low + high) >>> 1; FoldRegion midVal = regions[mid]; if (midVal.getStartOffset() <= startOffset && midVal.getEndOffset() > startOffset) { i = mid; break; } if (midVal.getStartOffset() < startOffset) low = mid + 1; else if (midVal.getStartOffset() > startOffset) high = mid - 1; } if (i < 0) { i = low; } int result = startOffset; int columnsToProcess = visualColumn; for (; i < regions.length; i++) { FoldRegion region = regions[i]; // Process text between the last fold region end and current fold region start. int nonFoldTextColumnsNumber = region.getStartOffset() - result; if (nonFoldTextColumnsNumber >= columnsToProcess) { return result + columnsToProcess; } columnsToProcess -= nonFoldTextColumnsNumber; // Process fold region. int placeHolderLength = region.getPlaceholderText().length(); if (placeHolderLength >= columnsToProcess) { return region.getEndOffset(); } result = region.getEndOffset(); columnsToProcess -= placeHolderLength; } return result + columnsToProcess; } // TODO: tabbed text width is additive, it should be possible to have buckets, containing arguments / values to start with private final int[] myLastStartOffsets = new int[2]; private final int[] myLastTargetColumns = new int[myLastStartOffsets.length]; private final int[] myLastXOffsets = new int[myLastStartOffsets.length]; private final int[] myLastXs = new int[myLastStartOffsets.length]; private int myCurrentCachePosition; private int myLastCacheHits; private int myTotalRequests; // todo remove private int getTabbedTextWidth(int startOffset, int targetColumn, int xOffset) { int x = xOffset; if (startOffset == 0 && myPrefixText != null) { x += myPrefixWidthInPixels; } if (targetColumn <= 0) return x; ++myTotalRequests; for(int i = 0; i < myLastStartOffsets.length; ++i) { if (startOffset == myLastStartOffsets[i] && targetColumn == myLastTargetColumns[i] && xOffset == myLastXOffsets[i]) { ++myLastCacheHits; if ((myLastCacheHits & 0xFFF) == 0) { // todo remove PsiFile file = myProject != null ? PsiDocumentManager.getInstance(myProject).getCachedPsiFile(myDocument):null; LOG.info("Cache hits:" + myLastCacheHits + ", total requests:" + myTotalRequests + "," + (file != null ? file.getViewProvider().getVirtualFile():null)); } return myLastXs[i]; } } int offset = startOffset; CharSequence text = myDocument.getImmutableCharSequence(); int textLength = myDocument.getTextLength(); // We need to calculate max offset to provide to the IterationState here based on the given start offset and target // visual column. The problem is there is a possible case that there is a collapsed fold region at the target interval, // so, we can't just use 'startOffset + targetColumn' as a max end offset. IterationState state = new IterationState(this, startOffset, calcEndOffset(startOffset, targetColumn), false); int fontType = state.getMergedAttributes().getFontType(); int spaceSize = EditorUtil.getSpaceWidth(fontType, this); int column = 0; outer: while (column < targetColumn) { if (offset >= textLength) break; if (offset >= state.getEndOffset()) { state.advance(); fontType = state.getMergedAttributes().getFontType(); } // We need to consider 'before soft wrap drawing'. SoftWrap softWrap = getSoftWrapModel().getSoftWrap(offset); if (softWrap != null && offset > startOffset) { column++; x += getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED); // Assuming that first soft wrap symbol is line feed or all soft wrap symbols before the first line feed are spaces. break; } FoldRegion region = state.getCurrentFold(); if (region != null) { char[] placeholder = region.getPlaceholderText().toCharArray(); for (char aPlaceholder : placeholder) { x += EditorUtil.charWidth(aPlaceholder, fontType, this); column++; if (column >= targetColumn) break outer; } offset = region.getEndOffset(); } else { char c = text.charAt(offset); if (c == '\n') { break; } if (c == '\t') { int prevX = x; x = EditorUtil.nextTabStop(x, this); int columnDiff = (x - prevX) / spaceSize; if ((x - prevX) % spaceSize > 0) { // There is a possible case that tabulation symbol takes more than one visual column to represent and it's shown at // soft-wrapped line. Soft wrap sign width may be not divisible by space size, hence, part of tabulation symbol represented // as a separate visual column may take less space than space width. columnDiff++; } column += columnDiff; } else { x += EditorUtil.charWidth(c, fontType, this); column++; } offset++; } } if (column != targetColumn) { x += EditorUtil.getSpaceWidth(fontType, this) * (targetColumn - column); } myLastTargetColumns[myCurrentCachePosition] = targetColumn; myLastStartOffsets[myCurrentCachePosition] = startOffset; myLastXs[myCurrentCachePosition] = x; myLastXOffsets[myCurrentCachePosition] = xOffset; myCurrentCachePosition = (myCurrentCachePosition + 1) % myLastStartOffsets.length; return x; } private void clearTextWidthCache() { for(int i = 0; i < myLastStartOffsets.length; ++i) { myLastTargetColumns[i] = -1; myLastStartOffsets[i] = - 1; myLastXs[i] = -1; myLastXOffsets[i] = -1; } } public int visibleLineToY(int line) { if (line < 0) throw new IndexOutOfBoundsException("Wrong line: " + line); return line * getLineHeight(); } @Override public void repaint(final int startOffset, int endOffset) { if (!isShowing() || myDocument.isInBulkUpdate()) { return; } endOffset = Math.min(endOffset, myDocument.getTextLength()); assertIsDispatchThread(); // We do repaint in case of equal offsets because there is a possible case that there is a soft wrap at the same offset and // it does occupy particular amount of visual space that may be necessary to repaint. if (startOffset <= endOffset) { int startLine = myDocument.getLineNumber(startOffset); int endLine = myDocument.getLineNumber(endOffset); repaintLines(startLine, endLine); } } private boolean isShowing() { return myGutterComponent.isShowing(); } private void repaintToScreenBottom(int startLine) { Rectangle visibleArea = getScrollingModel().getVisibleArea(); int yStartLine = logicalLineToY(startLine); int yEndLine = visibleArea.y + visibleArea.height; myEditorComponent.repaintEditorComponent(visibleArea.x, yStartLine, visibleArea.x + visibleArea.width, yEndLine - yStartLine); myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine); ((EditorMarkupModelImpl)getMarkupModel()).repaint(-1, -1); } /** * Asks to repaint all logical lines from the given [start; end] range. * * @param startLine start logical line to repaint (inclusive) * @param endLine end logical line to repaint (inclusive) */ public void repaintLines(int startLine, int endLine) { if (!isShowing()) return; Rectangle visibleArea = getScrollingModel().getVisibleArea(); int yStartLine = logicalLineToY(startLine); int endVisLine; if (myDocument.getTextLength() <= 0) { endVisLine = 0; } else { endVisLine = offsetToVisualLine(myDocument.getLineEndOffset(Math.min(myDocument.getLineCount() - 1, endLine))); } int height = endVisLine * getLineHeight() - yStartLine + getLineHeight() + WAVE_HEIGHT; myEditorComponent.repaintEditorComponent(visibleArea.x, yStartLine, visibleArea.x + visibleArea.width, height); myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), height); } private void bulkUpdateStarted() { } private void bulkUpdateFinished() { clearTextWidthCache(); mySelectionModel.removeBlockSelection(); setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); mySizeContainer.reset(); validateSize(); updateGutterSize(); repaintToScreenBottom(0); updateCaretCursor(); } private void beforeChangedUpdate(@NotNull DocumentEvent e) { if (isStickySelection()) { setStickySelection(false); } if (myDocument.isInBulkUpdate()) { // Assuming that the job is done at bulk listener callback methods. return; } Rectangle visibleArea = getScrollingModel().getVisibleArea(); Point pos = visualPositionToXY(getCaretModel().getVisualPosition()); myCaretUpdateVShift = pos.y - visibleArea.y; // We assume that size container is already notified with the visual line widths during soft wraps processing if (!mySoftWrapModel.isSoftWrappingEnabled()) { mySizeContainer.beforeChange(e); } } private void changedUpdate(DocumentEvent e) { if (myDocument.isInBulkUpdate()) return; clearTextWidthCache(); mySelectionModel.removeBlockSelection(); setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); // We assume that size container is already notified with the visual line widths during soft wraps processing if (!mySoftWrapModel.isSoftWrappingEnabled()) { mySizeContainer.changedUpdate(e); } validateSize(); int startLine = offsetToLogicalLine(e.getOffset()); int endLine = offsetToLogicalLine(e.getOffset() + e.getNewLength()); boolean painted = false; if (myDocument.getTextLength() > 0) { int startDocLine = myDocument.getLineNumber(e.getOffset()); int endDocLine = myDocument.getLineNumber(e.getOffset() + e.getNewLength()); if (e.getOldLength() > e.getNewLength() || startDocLine != endDocLine || StringUtil.indexOf(e.getOldFragment(), '\n') != -1) { updateGutterSize(); } if (countLineFeeds(e.getOldFragment()) != countLineFeeds(e.getNewFragment())) { // Lines removed. Need to repaint till the end of the screen repaintToScreenBottom(startLine); painted = true; } } updateCaretCursor(); if (!painted) { repaintLines(startLine, endLine); } Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition()); int scrollOffset = caretLocation.y - myCaretUpdateVShift; getScrollingModel().scrollVertically(scrollOffset); } public boolean hasTabs() { return !(myDocument instanceof DocumentImpl) || ((DocumentImpl)myDocument).mightContainTabs(); } public boolean isScrollToCaret() { return myScrollToCaret; } public void setScrollToCaret(boolean scrollToCaret) { myScrollToCaret = scrollToCaret; } @NotNull public Disposable getDisposable() { return myDisposable; } private static int countLineFeeds(@NotNull CharSequence c) { return StringUtil.countNewLines(c); } private void updateGutterSize() { LaterInvocator.invokeLater(new Runnable() { @Override public void run() { myGutterComponent.updateSize(); } }); } void validateSize() { Dimension dim = getPreferredSize(); if (!dim.equals(myPreferredSize) && !myDocument.isInBulkUpdate()) { dim = mySizeAdjustmentStrategy.adjust(dim, myPreferredSize, this); if (dim == null) { return; } myPreferredSize = dim; myGutterComponent.setLineNumberAreaWidth(new TIntFunction() { @Override public int execute(int lineNumber) { return getFontMetrics(Font.PLAIN).stringWidth(Integer.toString(lineNumber + 2)) + 6; } }); myGutterComponent.updateSize(); myEditorComponent.setSize(dim); myEditorComponent.fireResized(); myMarkupModel.recalcEditorDimensions(); myMarkupModel.repaint(-1, -1); } } void recalculateSizeAndRepaint() { mySizeContainer.reset(); validateSize(); myEditorComponent.repaintEditorComponent(); } @Override @NotNull public DocumentEx getDocument() { return myDocument; } @Override @NotNull public JComponent getComponent() { return myPanel; } @Override public void addEditorMouseListener(@NotNull EditorMouseListener listener) { myMouseListeners.add(listener); } @Override public void removeEditorMouseListener(@NotNull EditorMouseListener listener) { boolean success = myMouseListeners.remove(listener); LOG.assertTrue(success || isReleased); } @Override public void addEditorMouseMotionListener(@NotNull EditorMouseMotionListener listener) { myMouseMotionListeners.add(listener); } @Override public void removeEditorMouseMotionListener(@NotNull EditorMouseMotionListener listener) { boolean success = myMouseMotionListeners.remove(listener); LOG.assertTrue(success || isReleased); } @Override public boolean isStickySelection() { return myStickySelection; } @Override public void setStickySelection(boolean enable) { myStickySelection = enable; if (enable) { myStickySelectionStart = getCaretModel().getOffset(); } else { mySelectionModel.removeSelection(); } } @Override public boolean isDisposed() { return isReleased; } public void stopDumbLater() { if (ApplicationManager.getApplication().isUnitTestMode()) return; final Runnable stopDumbRunnable = new Runnable() { @Override public void run() { stopDumb(); } }; ApplicationManager.getApplication().invokeLater(stopDumbRunnable, ModalityState.current()); } public void stopDumb() { putUserData(BUFFER, null); } /** * {@link #stopDumbLater} or {@link #stopDumb} must be performed in finally */ public void startDumb() { if (ApplicationManager.getApplication().isUnitTestMode()) return; final JComponent component = getContentComponent(); final Rectangle rect = ((JViewport)component.getParent()).getViewRect(); BufferedImage image = UIUtil.createImage(rect.width, rect.height, BufferedImage.TYPE_INT_RGB); final Graphics2D graphics = image.createGraphics(); UISettings.setupAntialiasing(graphics); graphics.translate(-rect.x, -rect.y); graphics.setClip(rect.x, rect.y, rect.width, rect.height); paint(graphics); graphics.dispose(); putUserData(BUFFER, image); } void paint(@NotNull Graphics2D g) { Rectangle clip = g.getClipBounds(); if (clip == null) { return; } if (Registry.is("editor.dumb.mode.available")) { final BufferedImage buffer = getUserData(BUFFER); if (buffer != null) { final Rectangle rect = getContentComponent().getVisibleRect(); UIUtil.drawImage(g, buffer, null, rect.x, rect.y); return; } } if (myUpdateCursor) { setCursorPosition(); myUpdateCursor = false; } if (isReleased) { g.setColor(new JBColor(new Color(128, 255, 128), new Color(128, 255, 128))); g.fillRect(clip.x, clip.y, clip.width, clip.height); return; } if (myProject != null && myProject.isDisposed()) return; VisualPosition clipStartVisualPos = xyToVisualPosition(new Point(0, clip.y)); LogicalPosition clipStartPosition = visualToLogicalPosition(clipStartVisualPos); int clipStartOffset = logicalPositionToOffset(clipStartPosition); LogicalPosition clipEndPosition = xyToLogicalPosition(new Point(0, clip.y + clip.height + getLineHeight())); int clipEndOffset = logicalPositionToOffset(clipEndPosition); paintBackgrounds(g, clip, clipStartPosition, clipStartVisualPos, clipStartOffset, clipEndOffset); if (paintPlaceholderText(g, clip)) return; paintRectangularSelection(g); paintRightMargin(g, clip); paintCustomRenderers(g, clipStartOffset, clipEndOffset); MarkupModelEx docMarkup = (MarkupModelEx)DocumentMarkupModel.forDocument(myDocument, myProject, true); paintLineMarkersSeparators(g, clip, docMarkup, clipStartOffset, clipEndOffset); paintLineMarkersSeparators(g, clip, myMarkupModel, clipStartOffset, clipEndOffset); paintText(g, clip, clipStartPosition, clipStartOffset, clipEndOffset); paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip, clipStartOffset, clipEndOffset, docMarkup); BorderEffect borderEffect = new BorderEffect(this, g, clipStartOffset, clipEndOffset); borderEffect.paintHighlighters(getHighlighter()); borderEffect.paintHighlighters(docMarkup); borderEffect.paintHighlighters(myMarkupModel); paintCaretCursor(g); paintComposedTextDecoration(g); } private void paintCustomRenderers(@NotNull final Graphics2D g, final int clipStartOffset, final int clipEndOffset) { myMarkupModel.processRangeHighlightersOverlappingWith(clipStartOffset, clipEndOffset, new Processor() { @Override public boolean process(@NotNull RangeHighlighterEx highlighter) { if (!highlighter.getEditorFilter().avaliableIn(EditorImpl.this)) return true; final CustomHighlighterRenderer customRenderer = highlighter.getCustomRenderer(); if (customRenderer != null && clipStartOffset < highlighter.getEndOffset() && highlighter.getStartOffset() < clipEndOffset) { customRenderer.paint(EditorImpl.this, highlighter, g); } return true; } }); } @NotNull @Override public IndentsModel getIndentsModel() { return myIndentsModel; } @Override public void setHeaderComponent(JComponent header) { myHeaderPanel.removeAll(); header = header == null ? getPermanentHeaderComponent() : header; if (header != null) { myHeaderPanel.add(header); } myHeaderPanel.revalidate(); } @Override public boolean hasHeaderComponent() { JComponent header = getHeaderComponent(); return header != null && header != getPermanentHeaderComponent(); } @Override @Nullable public JComponent getPermanentHeaderComponent() { return getUserData(PERMANENT_HEADER); } @Override public void setPermanentHeaderComponent(@Nullable JComponent component) { putUserData(PERMANENT_HEADER, component); } @Override @Nullable public JComponent getHeaderComponent() { if (myHeaderPanel.getComponentCount() > 0) { return (JComponent)myHeaderPanel.getComponent(0); } return null; } @Override public void setBackgroundColor(Color color) { myScrollPane.setBackground(color); if (getBackgroundIgnoreForced().equals(color)) { myForcedBackground = null; return; } myForcedBackground = color; } @NotNull private Color getForegroundColor() { return myScheme.getDefaultForeground(); } @NotNull @Override public Color getBackgroundColor() { if (myForcedBackground != null) return myForcedBackground; return getBackgroundIgnoreForced(); } @NotNull @Override public TextDrawingCallback getTextDrawingCallback() { return myTextDrawingCallback; } @Override public void setPlaceholder(@Nullable CharSequence text) { myPlaceholderText = text; } private Color getBackgroundColor(@NotNull final TextAttributes attributes) { final Color attrColor = attributes.getBackgroundColor(); return Comparing.equal(attrColor, myScheme.getDefaultBackground()) ? getBackgroundColor() : attrColor; } @NotNull private Color getBackgroundIgnoreForced() { Color color = myScheme.getDefaultBackground(); if (myDocument.isWritable()) { return color; } Color readOnlyColor = myScheme.getColor(EditorColors.READONLY_BACKGROUND_COLOR); return readOnlyColor != null ? readOnlyColor : color; } private void paintComposedTextDecoration(@NotNull Graphics2D g) { if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null && myInputMethodRequestsHandler.composedTextRange != null) { VisualPosition visStart = offsetToVisualPosition(Math.min(myInputMethodRequestsHandler.composedTextRange.getStartOffset(), myDocument.getTextLength())); int y = visibleLineToY(visStart.line) + getAscent() + 1; Point p1 = visualPositionToXY(visStart); Point p2 = logicalPositionToXY( offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextRange.getEndOffset(), myDocument.getTextLength()))); Stroke saved = g.getStroke(); BasicStroke dotted = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[]{0, 2, 0, 2}, 0); g.setStroke(dotted); UIUtil.drawLine(g, p1.x, y, p2.x, y); g.setStroke(saved); } } private void paintRightMargin(@NotNull Graphics g, @NotNull Rectangle clip) { Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR); if (!mySettings.isRightMarginShown() || rightMargin == null) { return; } int x = mySettings.getRightMargin(myProject) * EditorUtil.getSpaceWidth(Font.PLAIN, this); if (x >= clip.x && x < clip.x + clip.width) { g.setColor(rightMargin); UIUtil.drawLine(g, x, clip.y, x, clip.y + clip.height); } } private void paintSegmentHighlightersBorderAndAfterEndOfLine(@NotNull final Graphics g, @NotNull Rectangle clip, int clipStartOffset, int clipEndOffset, @NotNull MarkupModelEx docMarkup) { if (myDocument.getLineCount() == 0) return; final int startLine = yPositionToVisibleLine(clip.y); final int endLine = yPositionToVisibleLine(clip.y + clip.height) + 1; Processor paintProcessor = new Processor() { @Override public boolean process(@NotNull RangeHighlighterEx highlighter) { if (!highlighter.getEditorFilter().avaliableIn(EditorImpl.this)) return true; paintSegmentHighlighterAfterEndOfLine(g, highlighter, startLine, endLine); return true; } }; docMarkup.processRangeHighlightersOverlappingWith(clipStartOffset, clipEndOffset, paintProcessor); myMarkupModel.processRangeHighlightersOverlappingWith(clipStartOffset, clipEndOffset, paintProcessor); } private void paintSegmentHighlighterAfterEndOfLine(@NotNull Graphics g, @NotNull RangeHighlighterEx segmentHighlighter, int startLine, int endLine) { if (!segmentHighlighter.isAfterEndOfLine()) { return; } int startOffset = segmentHighlighter.getStartOffset(); int visibleStartLine = offsetToVisualLine(startOffset); if (getFoldingModel().isOffsetCollapsed(startOffset)) { return; } if (visibleStartLine >= startLine && visibleStartLine <= endLine) { int logStartLine = offsetToLogicalLine(startOffset); if (logStartLine >= myDocument.getLineCount()) { return; } LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine)); Point end = logicalPositionToXY(logPosition); int charWidth = EditorUtil.getSpaceWidth(Font.PLAIN, this); int lineHeight = getLineHeight(); TextAttributes attributes = segmentHighlighter.getTextAttributes(); if (attributes != null && getBackgroundColor(attributes) != null) { g.setColor(getBackgroundColor(attributes)); g.fillRect(end.x, end.y, charWidth, lineHeight); } if (attributes != null && attributes.getEffectColor() != null) { int y = visibleLineToY(visibleStartLine) + getAscent() + 1; g.setColor(attributes.getEffectColor()); if (attributes.getEffectType() == EffectType.WAVE_UNDERSCORE) { drawWave(g, end.x, end.x + charWidth - 1, y); } else if (attributes.getEffectType() == EffectType.BOLD_DOTTED_LINE) { final int dottedAt = SystemInfo.isMac ? y - 1 : y; UIUtil.drawBoldDottedLine((Graphics2D)g, end.x, end.x + charWidth - 1, dottedAt, getBackgroundColor(attributes), attributes.getEffectColor(), false); } else if (attributes.getEffectType() == EffectType.STRIKEOUT) { int y1 = y - getCharHeight() / 2 - 1; UIUtil.drawLine(g, end.x, y1, end.x + charWidth - 1, y1); } else if (attributes.getEffectType() == EffectType.BOLD_LINE_UNDERSCORE) { UIUtil.drawLine(g, end.x, y - 1, end.x + charWidth - 1, y - 1); UIUtil.drawLine(g, end.x, y, end.x + charWidth - 1, y); } else if (attributes.getEffectType() != EffectType.BOXED) { UIUtil.drawLine(g, end.x, y, end.x + charWidth - 1, y); } } } } @Override public int getMaxWidthInRange(int startOffset, int endOffset) { int width = 0; int start = offsetToVisualLine(startOffset); int end = offsetToVisualLine(endOffset); for (int i = start; i <= end; i++) { int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, i) + 1; int lineWidth = visualPositionToXY(new VisualPosition(i, lastColumn)).x; if (lineWidth > width) { width = lineWidth; } } return width; } private void paintBackgrounds(@NotNull Graphics g, @NotNull Rectangle clip, @NotNull LogicalPosition clipStartPosition, @NotNull VisualPosition clipStartVisualPos, int clipStartOffset, int clipEndOffset) { Color defaultBackground = getBackgroundColor(); g.setColor(defaultBackground); g.fillRect(clip.x, clip.y, clip.width, clip.height); int lineHeight = getLineHeight(); int visibleLine = yPositionToVisibleLine(clip.y); Point position = new Point(0, visibleLine * lineHeight); CharSequence prefixText = myPrefixText == null ? null : new CharArrayCharSequence(myPrefixText); if (clipStartVisualPos.line == 0 && prefixText != null) { position.x = drawBackground(g, myPrefixAttributes.getBackgroundColor(), prefixText, 0, prefixText.length(), position, myPrefixAttributes.getFontType(), defaultBackground, clip); } if (clipStartPosition.line >= myDocument.getLineCount() || clipStartPosition.line < 0) { if (position.x > 0) flushBackground(g, clip); return; } myLastBackgroundPosition = null; myLastBackgroundColor = null; int start = clipStartOffset; if (!myPurePaintingMode) { getSoftWrapModel().registerSoftWrapsIfNecessary(); } LineIterator lIterator = createLineIterator(); lIterator.start(start); if (lIterator.atEnd()) { return; } IterationState iterationState = new IterationState(this, start, clipEndOffset, isPaintSelection()); TextAttributes attributes = iterationState.getMergedAttributes(); Color backColor = getBackgroundColor(attributes); int fontType = attributes.getFontType(); int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1); // There is a possible case that we need to draw background from the start of soft wrap-introduced visual line. Given position // has valid 'y' coordinate then at it shouldn't be affected by soft wrap that corresponds to the visual line start offset. // Hence, we store information about soft wrap to be skipped for further processing and adjust 'x' coordinate value if necessary. TIntHashSet softWrapsToSkip = new TIntHashSet(); SoftWrap softWrap = getSoftWrapModel().getSoftWrap(start); if (softWrap != null) { softWrapsToSkip.add(softWrap.getStart()); Color color = null; if (backColor != null && !backColor.equals(defaultBackground)) { color = backColor; } // There is a possible case that target clip points to soft wrap-introduced visual line and that it's an active // line (caret cursor is located on it). We want to draw corresponding 'caret line' background for soft wraps-introduced // virtual space then. if (color == null && position.y == getCaretModel().getVisualPosition().line * getLineHeight()) { color = getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR); } if (color != null) { drawBackground(g, color, softWrap.getIndentInPixels(), position, defaultBackground, clip); } position.x = softWrap.getIndentInPixels(); } // There is a possible case that caret is located at soft-wrapped line. We don't need to paint caret row background // on a last visual line of that soft-wrapped line then. Below is a holder for the flag that indicates if caret row // background is already drawn. boolean[] caretRowPainted = new boolean[1]; CharSequence text = myDocument.getImmutableCharSequence(); while (!iterationState.atEnd() && !lIterator.atEnd()) { int hEnd = iterationState.getEndOffset(); int lEnd = lIterator.getEnd(); if (hEnd >= lEnd) { FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); if (collapsedFolderAt == null) { position.x = drawSoftWrapAwareBackground(g, backColor, text, start, lEnd - lIterator.getSeparatorLength(), position, fontType, defaultBackground, clip, softWrapsToSkip, caretRowPainted); paintAfterLineEndBackgroundSegments(g, iterationState, position, defaultBackground, lineHeight); if (lIterator.getLineNumber() < lastLineIndex) { if (backColor != null && !backColor.equals(defaultBackground)) { g.setColor(backColor); g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); } } else { if (iterationState.hasPastFileEndBackgroundSegments()) { paintAfterLineEndBackgroundSegments(g, iterationState, position, defaultBackground, lineHeight); } paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight, defaultBackground, caretRowPainted); break; } position.x = 0; if (position.y > clip.y + clip.height) break; position.y += lineHeight; start = lEnd; } else if (collapsedFolderAt.getEndOffset() == clipEndOffset) { softWrap = mySoftWrapModel.getSoftWrap(collapsedFolderAt.getStartOffset()); if (softWrap != null) { position.x = drawSoftWrapAwareBackground( g, backColor, text, collapsedFolderAt.getStartOffset(), collapsedFolderAt.getStartOffset(), position, fontType, defaultBackground, clip, softWrapsToSkip, caretRowPainted ); } CharSequence chars = collapsedFolderAt.getPlaceholderText(); position.x = drawBackground(g, backColor, chars, 0, chars.length(), position, fontType, defaultBackground, clip); } lIterator.advance(); } else { FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); if (collapsedFolderAt != null) { softWrap = mySoftWrapModel.getSoftWrap(collapsedFolderAt.getStartOffset()); if (softWrap != null) { position.x = drawSoftWrapAwareBackground( g, backColor, text, collapsedFolderAt.getStartOffset(), collapsedFolderAt.getStartOffset(), position, fontType, defaultBackground, clip, softWrapsToSkip, caretRowPainted ); } CharSequence chars = collapsedFolderAt.getPlaceholderText(); position.x = drawBackground(g, backColor, chars, 0, chars.length(), position, fontType, defaultBackground, clip); } else if (hEnd > lEnd - lIterator.getSeparatorLength()) { position.x = drawSoftWrapAwareBackground( g, backColor, text, start, lEnd - lIterator.getSeparatorLength(), position, fontType, defaultBackground, clip, softWrapsToSkip, caretRowPainted ); } else { position.x = drawSoftWrapAwareBackground( g, backColor, text, start, hEnd, position, fontType, defaultBackground, clip, softWrapsToSkip, caretRowPainted ); } iterationState.advance(); attributes = iterationState.getMergedAttributes(); backColor = getBackgroundColor(attributes); fontType = attributes.getFontType(); start = iterationState.getStartOffset(); } } flushBackground(g, clip); if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) { paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight, defaultBackground, caretRowPainted); } // Perform additional activity if soft wrap is added or removed during repainting. if (mySoftWrapsChanged) { mySoftWrapsChanged = false; clearTextWidthCache(); validateSize(); // Repaint editor to the bottom in order to ensure that its content is shown correctly after new soft wrap introduction. repaintToScreenBottom(EditorUtil.yPositionToLogicalLine(this, position)); // Repaint gutter at all space that is located after active clip in order to ensure that line numbers are correctly redrawn // in accordance with the newly introduced soft wrap(s). myGutterComponent.repaint(0, clip.y, myGutterComponent.getWidth(), myGutterComponent.getHeight() - clip.y); } } private void paintAfterLineEndBackgroundSegments(@NotNull Graphics g, @NotNull IterationState iterationState, @NotNull Point position, @NotNull Color defaultBackground, int lineHeight) { while (iterationState.hasPastLineEndBackgroundSegment()) { TextAttributes backgroundAttributes = iterationState.getPastLineEndBackgroundAttributes(); int width = EditorUtil.getSpaceWidth(backgroundAttributes.getFontType(), this) * iterationState.getPastLineEndBackgroundSegmentWidth(); Color color = getBackgroundColor(backgroundAttributes); if (color != null && !color.equals(defaultBackground)) { g.setColor(color); g.fillRect(position.x, position.y, width, lineHeight); } position.x += width; iterationState.advanceToNextPastLineEndBackgroundSegment(); } } private void paintRectangularSelection(@NotNull Graphics g) { final SelectionModel model = getSelectionModel(); if (!model.hasBlockSelection()) return; final LogicalPosition blockStart = model.getBlockStart(); final LogicalPosition blockEnd = model.getBlockEnd(); assert blockStart != null; assert blockEnd != null; final Point start = logicalPositionToXY(blockStart); final Point end = logicalPositionToXY(blockEnd); g.setColor(myScheme.getColor(EditorColors.SELECTION_BACKGROUND_COLOR)); final int y; final int height; if (start.y <= end.y) { y = start.y; height = end.y - y + getLineHeight(); } else { y = end.y; height = start.y - end.y + getLineHeight(); } final int x = Math.min(start.x, end.x); final int width = Math.max(2, Math.abs(end.x - start.x)); g.fillRect(x, y, width, height); } private void paintAfterFileEndBackground(@NotNull IterationState iterationState, @NotNull Graphics g, @NotNull Point position, @NotNull Rectangle clip, int lineHeight, @NotNull Color defaultBackground, @NotNull boolean[] caretRowPainted) { Color backColor = iterationState.getPastFileEndBackground(); if (backColor == null || backColor.equals(defaultBackground)) { return; } if (caretRowPainted[0] && backColor.equals(getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR))) { return; } g.setColor(backColor); g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight); } private int drawSoftWrapAwareBackground(@NotNull Graphics g, Color backColor, @NotNull CharSequence text, int start, int end, @NotNull Point position, @JdkConstants.FontStyle int fontType, @NotNull Color defaultBackground, @NotNull Rectangle clip, @NotNull TIntHashSet softWrapsToSkip, @NotNull boolean[] caretRowPainted) { int startToUse = start; // Given 'end' offset is exclusive though SoftWrapModel.getSoftWrapsForRange() uses inclusive end offset. // Hence, we decrement it if necessary. Please note that we don't do that if start is equal to end. That is the case, // for example, for soft-wrapped collapsed fold region - we need to draw soft wrap before it. int softWrapRetrievalEndOffset = end; if (end > start) { softWrapRetrievalEndOffset--; } List softWraps = getSoftWrapModel().getSoftWrapsForRange(start, softWrapRetrievalEndOffset); for (SoftWrap softWrap : softWraps) { int softWrapStart = softWrap.getStart(); if (softWrapsToSkip.contains(softWrapStart)) { continue; } if (startToUse < softWrapStart) { position.x = drawBackground(g, backColor, text, startToUse, softWrapStart, position, fontType, defaultBackground, clip); } boolean drawCustomBackgroundAtSoftWrapVirtualSpace = !Comparing.equal(backColor, defaultBackground) && (softWrapStart > start || Comparing.equal(myLastBackgroundColor, backColor)); drawSoftWrap( g, softWrap, position, fontType, backColor, drawCustomBackgroundAtSoftWrapVirtualSpace, defaultBackground, clip, caretRowPainted ); startToUse = softWrapStart; } if (startToUse < end) { position.x = drawBackground(g, backColor, text, startToUse, end, position, fontType, defaultBackground, clip); } return position.x; } private void drawSoftWrap(@NotNull Graphics g, @NotNull SoftWrap softWrap, @NotNull Point position, @JdkConstants.FontStyle int fontType, @Nullable Color backColor, boolean drawCustomBackgroundAtSoftWrapVirtualSpace, @NotNull Color defaultBackground, @NotNull Rectangle clip, @NotNull boolean[] caretRowPainted) { // The main idea is to to do the following: // *) update given drawing position coordinates in accordance with the current soft wrap; // *) draw background at soft wrap-introduced virtual space if necessary; CharSequence softWrapText = softWrap.getText(); int activeRowY = getCaretModel().getVisualPosition().line * getLineHeight(); int afterSoftWrapWidth = clip.x + clip.width - position.x; if (drawCustomBackgroundAtSoftWrapVirtualSpace && backColor != null) { drawBackground(g, backColor, afterSoftWrapWidth, position, defaultBackground, clip); } else if (position.y == activeRowY) { // Draw 'active line' background after soft wrap. Color caretRowColor = getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR); drawBackground(g, caretRowColor, afterSoftWrapWidth, position, defaultBackground, clip); caretRowPainted[0] = true; } paintSelectionOnFirstSoftWrapLineIfNecessary(g, position, clip, defaultBackground, fontType); int i = CharArrayUtil.lastIndexOf(softWrapText, "\n", softWrapText.length()) + 1; int width = getTextSegmentWidth(softWrapText, i, softWrapText.length(), 0, fontType, clip) + getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.AFTER_SOFT_WRAP); position.x = 0; position.y += getLineHeight(); if (drawCustomBackgroundAtSoftWrapVirtualSpace && backColor != null) { drawBackground(g, backColor, width, position, defaultBackground, clip); } else if (position.y == activeRowY) { // Draw 'active line' background for the soft wrap-introduced virtual space. Color caretRowColor = getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR); drawBackground(g, caretRowColor, width, position, defaultBackground, clip); } position.x = 0; paintSelectionOnSecondSoftWrapLineIfNecessary(g, position, clip, defaultBackground, fontType, softWrap); position.x = width; } /** * End user is allowed to perform selection by visual coordinates (e.g. by dragging mouse with left button hold). There is a possible * case that such a move intersects with soft wrap introduced virtual space. We want to draw corresponding selection background * there then. *

* This method encapsulates functionality of drawing selection background on the first soft wrap line (e.g. on a visual line where * it is applied). * * @param g graphics to draw on * @param position current position (assumed to be position of soft wrap appliance) * @param clip target drawing area boundaries * @param defaultBackground default background * @param fontType current font type */ private void paintSelectionOnFirstSoftWrapLineIfNecessary(@NotNull Graphics g, @NotNull Point position, @NotNull Rectangle clip, @NotNull Color defaultBackground, @JdkConstants.FontStyle int fontType) { // There is a possible case that the user performed selection at soft wrap virtual space. We need to paint corresponding background // there then. VisualPosition selectionStartPosition = getSelectionModel().getSelectionStartPosition(); VisualPosition selectionEndPosition = getSelectionModel().getSelectionEndPosition(); if (selectionStartPosition.equals(selectionEndPosition)) { return; } int currentVisualLine = position.y / getLineHeight(); int lastColumn = EditorUtil.getLastVisualLineColumnNumber(this, currentVisualLine); // Check if the first soft wrap line is within the visual selection. if (currentVisualLine < selectionStartPosition.line || currentVisualLine > selectionEndPosition.line || currentVisualLine == selectionEndPosition.line && selectionEndPosition.column <= lastColumn) { return; } // Adjust 'x' if selection starts at soft wrap virtual space. final int columnsToSkip = selectionStartPosition.column - lastColumn; if (columnsToSkip > 0) { position.x += getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED); position.x += (columnsToSkip - 1) * EditorUtil.getSpaceWidth(Font.PLAIN, this); } // Calculate selection width. final int width; if (selectionEndPosition.line > currentVisualLine) { width = clip.x + clip.width - position.x; } else if (selectionStartPosition.line < currentVisualLine || selectionStartPosition.column <= lastColumn) { width = getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED) + (selectionEndPosition.column - lastColumn - 1) * EditorUtil.getSpaceWidth(fontType, this); } else { width = (selectionEndPosition.column - selectionStartPosition.column) * EditorUtil.getSpaceWidth(fontType, this); } drawBackground(g, getColorsScheme().getColor(EditorColors.SELECTION_BACKGROUND_COLOR), width, position, defaultBackground, clip); } /** * End user is allowed to perform selection by visual coordinates (e.g. by dragging mouse with left button hold). There is a possible * case that such a move intersects with soft wrap introduced virtual space. We want to draw corresponding selection background * there then. *

* This method encapsulates functionality of drawing selection background on the second soft wrap line (e.g. on a visual line after * the one where it is applied). * * @param g graphics to draw on * @param position current position (assumed to be position of soft wrap appliance) * @param clip target drawing area boundaries * @param defaultBackground default background * @param fontType current font type * @param softWrap target soft wrap which second line virtual space may contain selection */ private void paintSelectionOnSecondSoftWrapLineIfNecessary(@NotNull Graphics g, @NotNull Point position, @NotNull Rectangle clip, @NotNull Color defaultBackground, @JdkConstants.FontStyle int fontType, @NotNull SoftWrap softWrap) { // There is a possible case that the user performed selection at soft wrap virtual space. We need to paint corresponding background // there then. VisualPosition selectionStartPosition = getSelectionModel().getSelectionStartPosition(); VisualPosition selectionEndPosition = getSelectionModel().getSelectionEndPosition(); if (selectionStartPosition.equals(selectionEndPosition)) { return; } int currentVisualLine = position.y / getLineHeight(); // Check if the second soft wrap line is within the visual selection. if (currentVisualLine < selectionStartPosition.line || currentVisualLine > selectionEndPosition.line || currentVisualLine == selectionStartPosition.line && selectionStartPosition.column >= softWrap.getIndentInColumns()) { return; } // Adjust 'x' if selection starts at soft wrap virtual space. if (selectionStartPosition.line == currentVisualLine && selectionStartPosition.column > 0) { position.x += selectionStartPosition.column * EditorUtil.getSpaceWidth(fontType, this); } // Calculate selection width. final int width; if (selectionEndPosition.line > currentVisualLine || selectionEndPosition.column >= softWrap.getIndentInColumns()) { width = softWrap.getIndentInPixels() - position.x; } else { width = selectionEndPosition.column * EditorUtil.getSpaceWidth(fontType, this) - position.x; } drawBackground(g, getColorsScheme().getColor(EditorColors.SELECTION_BACKGROUND_COLOR), width, position, defaultBackground, clip); } private int drawBackground(@NotNull Graphics g, Color backColor, @NotNull CharSequence text, int start, int end, @NotNull Point position, @JdkConstants.FontStyle int fontType, @NotNull Color defaultBackground, @NotNull Rectangle clip) { int width = getTextSegmentWidth(text, start, end, position.x, fontType, clip); return drawBackground(g, backColor, width, position, defaultBackground, clip); } private int drawBackground(@NotNull Graphics g, @Nullable Color backColor, int width, @NotNull Point position, @NotNull Color defaultBackground, @NotNull Rectangle clip) { if (backColor != null && !backColor.equals(defaultBackground) && clip.intersects(position.x, position.y, width, getLineHeight())) { if (backColor.equals(myLastBackgroundColor) && myLastBackgroundPosition.y == position.y && myLastBackgroundPosition.x + myLastBackgroundWidth == position.x) { myLastBackgroundWidth += width; } else { flushBackground(g, clip); myLastBackgroundColor = backColor; myLastBackgroundPosition = new Point(position); myLastBackgroundWidth = width; } } return position.x + width; } private void flushBackground(@NotNull Graphics g, @NotNull final Rectangle clip) { if (myLastBackgroundColor != null) { final Point position = myLastBackgroundPosition; final int w = myLastBackgroundWidth; final int height = getLineHeight(); if (clip.intersects(position.x, position.y, w, height)) { g.setColor(myLastBackgroundColor); g.fillRect(position.x, position.y, w, height); } myLastBackgroundColor = null; } } @NotNull private LineIterator createLineIterator() { return myDocument.createLineIterator(); } private void paintText(@NotNull Graphics g, @NotNull Rectangle clip, @NotNull LogicalPosition clipStartPosition, int clipStartOffset, int clipEndOffset) { myCurrentFontType = null; myLastCache = null; final int plainSpaceWidth = EditorUtil.getSpaceWidth(Font.PLAIN, this); final int boldSpaceWidth = EditorUtil.getSpaceWidth(Font.BOLD, this); final int italicSpaceWidth = EditorUtil.getSpaceWidth(Font.ITALIC, this); final int boldItalicSpaceWidth = EditorUtil.getSpaceWidth(Font.BOLD | Font.ITALIC, this); boolean spacesHaveSameWidth = plainSpaceWidth == boldSpaceWidth && plainSpaceWidth == italicSpaceWidth && plainSpaceWidth == boldItalicSpaceWidth; myCommonSpaceWidth = spacesHaveSameWidth ? boldSpaceWidth : -1; int lineHeight = getLineHeight(); int visibleLine = clip.y / lineHeight; // The main idea is that there is a possible case that we need to perform painting starting from soft-wrapped logical line. // We may want to skip necessary number of visual lines then. Hence, we remember logical position that corresponds to the starting // visual line in order to use it for further processing. As soon as necessary number of visual lines is skipped, logical // position is expected to be set to null as an indication that no soft wrap-introduced visual lines should be skipped on // current painting iteration. Ref logicalPosition = new Ref(clipStartPosition); int startLine = clipStartPosition.line; int start = clipStartOffset; Point position = new Point(0, visibleLine * lineHeight); if (startLine == 0 && myPrefixText != null) { position.x = drawStringWithSoftWraps(g, new CharArrayCharSequence(myPrefixText), 0, myPrefixText.length, position, clip, myPrefixAttributes.getEffectColor(), myPrefixAttributes.getEffectType(), myPrefixAttributes.getFontType(), myPrefixAttributes.getForegroundColor(), logicalPosition); } if (startLine >= myDocument.getLineCount() || startLine < 0) { if (position.x > 0) flushCachedChars(g); return; } LineIterator lIterator = createLineIterator(); lIterator.start(start); if (lIterator.atEnd()) { return; } IterationState iterationState = new IterationState(this, start, clipEndOffset, isPaintSelection()); TextAttributes attributes = iterationState.getMergedAttributes(); Color currentColor = attributes.getForegroundColor(); if (currentColor == null) { currentColor = getForegroundColor(); } Color effectColor = attributes.getEffectColor(); EffectType effectType = attributes.getEffectType(); int fontType = attributes.getFontType(); g.setColor(currentColor); CharSequence chars = myDocument.getImmutableCharSequence(); while (!iterationState.atEnd() && !lIterator.atEnd()) { int hEnd = iterationState.getEndOffset(); int lEnd = lIterator.getEnd(); if (hEnd >= lEnd) { FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start); if (collapsedFolderAt == null) { int i = drawStringWithSoftWraps(g, chars, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType, fontType, currentColor, logicalPosition); final VirtualFile file = getVirtualFile(); if (myProject != null && file != null && !isOneLineMode()) { for (EditorLinePainter painter : EditorLinePainter.EP_NAME.getExtensions()) { Collection extensions = painter.getLineExtensions(myProject, file, lIterator.getLineNumber()); if (extensions != null && !extensions.isEmpty()) { for (LineExtensionInfo info : extensions) { drawStringWithSoftWraps(g, info.getText(), 0, info.getText().length(), position, clip, info.getEffectColor() == null ? effectColor : info.getEffectColor(), info.getEffectType() == null ? effectType : info.getEffectType(), info.getFontType(), info.getColor() == null ? currentColor : info.getColor(), logicalPosition); } } } } position.x = 0; if (position.y > clip.y + clip.height) { break; } position.y += lineHeight; start = lEnd; } // myBorderEffect.eolReached(g, this); lIterator.advance(); } else { FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); if (collapsedFolderAt != null) { SoftWrap softWrap = mySoftWrapModel.getSoftWrap(collapsedFolderAt.getStartOffset()); if (softWrap != null) { position.x = drawStringWithSoftWraps( g, chars, collapsedFolderAt.getStartOffset(), collapsedFolderAt.getStartOffset(), position, clip, effectColor, effectType, fontType, currentColor, logicalPosition ); } int foldingXStart = position.x; position.x = drawString( g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor ); //drawStringWithSoftWraps(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, // fontType, currentColor, logicalPosition); BorderEffect.paintFoldedEffect(g, foldingXStart, position.y, position.x, getLineHeight(), effectColor, effectType); } else { position.x = drawStringWithSoftWraps(g, chars, start, Math.min(hEnd, lEnd - lIterator.getSeparatorLength()), position, clip, effectColor, effectType, fontType, currentColor, logicalPosition); } iterationState.advance(); attributes = iterationState.getMergedAttributes(); currentColor = attributes.getForegroundColor(); if (currentColor == null) { currentColor = getForegroundColor(); } effectColor = attributes.getEffectColor(); effectType = attributes.getEffectType(); fontType = attributes.getFontType(); start = iterationState.getStartOffset(); } } FoldRegion collapsedFolderAt = iterationState.getCurrentFold(); if (collapsedFolderAt != null) { int foldingXStart = position.x; int foldingXEnd = drawStringWithSoftWraps(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType, currentColor, logicalPosition); BorderEffect.paintFoldedEffect(g, foldingXStart, position.y, foldingXEnd, getLineHeight(), effectColor, effectType); // myBorderEffect.collapsedFolderReached(g, this); } final SoftWrap softWrap = mySoftWrapModel.getSoftWrap(clipEndOffset); if (softWrap != null) { mySoftWrapModel.paint(g, SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED, position.x, position.y, getLineHeight()); } flushCachedChars(g); } private boolean paintPlaceholderText(@NotNull Graphics g, @NotNull Rectangle clip) { CharSequence hintText = myPlaceholderText; if (myDocument.getTextLength() > 0 || hintText == null || hintText.length() == 0) { return false; } if (KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner() == myEditorComponent) { // There is a possible case that placeholder text was painted and the editor gets focus now. We want to over-paint previously // used placeholder text then. myLastBackgroundColor = getBackgroundColor(); myLastBackgroundPosition = new Point(0, 0); myLastBackgroundWidth = myLastPaintedPlaceholderWidth; flushBackground(g, clip); return false; } else { myLastPaintedPlaceholderWidth = drawString( g, hintText, 0, hintText.length(), new Point(0, 0), clip, null, null, Font.PLAIN, myFoldingModel.getPlaceholderAttributes().getForegroundColor() ); flushCachedChars(g); return true; } } public boolean isPaintSelection() { return myPaintSelection || !isOneLineMode() || IJSwingUtilities.hasFocus(getContentComponent()); } public void setPaintSelection(boolean paintSelection) { myPaintSelection = paintSelection; } @Override @NotNull @NonNls public String dumpState() { return "prefix: '" + (myPrefixText == null ? "none" : new String(myPrefixText)) + "', allow caret inside tab: " + mySettings.isCaretInsideTabs() + ", allow caret after line end: " + mySettings.isVirtualSpace() + ", soft wraps: " + (mySoftWrapModel.isSoftWrappingEnabled() ? "on" : "off") + ", soft wraps data: " + getSoftWrapModel().dumpState() + "\n\nfolding data: " + getFoldingModel().dumpState() + (myDocument instanceof DocumentImpl ? "\n\ndocument info: " + ((DocumentImpl)myDocument).dumpState() : ""); } private class CachedFontContent { final CharSequence[] data = new CharSequence[CACHED_CHARS_BUFFER_SIZE]; final int[] starts = new int[CACHED_CHARS_BUFFER_SIZE]; final int[] ends = new int[CACHED_CHARS_BUFFER_SIZE]; final int[] x = new int[CACHED_CHARS_BUFFER_SIZE]; final int[] y = new int[CACHED_CHARS_BUFFER_SIZE]; final Color[] color = new Color[CACHED_CHARS_BUFFER_SIZE]; int myCount = 0; @NotNull final FontInfo myFontType; final boolean myHasBreakSymbols; final int spaceWidth; @Nullable private CharSequence myLastData; private CachedFontContent(@NotNull FontInfo fontInfo) { myFontType = fontInfo; spaceWidth = fontInfo.charWidth(' '); myHasBreakSymbols = fontInfo.hasGlyphsToBreakDrawingIteration(); } private void flushContent(@NotNull Graphics g) { if (myCount != 0) { if (myCurrentFontType != myFontType) { myCurrentFontType = myFontType; g.setFont(myFontType.getFont()); } Color currentColor = null; for (int i = 0; i < myCount; i++) { if (!Comparing.equal(color[i], currentColor)) { currentColor = color[i]; g.setColor(currentColor != null ? currentColor : JBColor.black); } drawChars(g, data[i], starts[i], ends[i], x[i], y[i]); color[i] = null; data[i] = null; } myCount = 0; myLastData = null; } } private void addContent(@NotNull Graphics g, CharSequence _data, int _start, int _end, int _x, int _y, @Nullable Color _color) { final int count = myCount; if (count > 0) { final int lastCount = count - 1; final Color lastColor = color[lastCount]; if (_data == myLastData && _start == ends[lastCount] && (_color == null || lastColor == null || _color.equals(lastColor)) && _y == y[lastCount] /* there is a possible case that vertical position is adjusted because of soft wrap */ && (!myHasBreakSymbols || !myFontType.getSymbolsToBreakDrawingIteration().contains(_data.charAt(ends[lastCount] - 1)))) { ends[lastCount] = _end; if (lastColor == null) color[lastCount] = _color; return; } } myLastData = _data; data[count] = _data; x[count] = _x; y[count] = _y; starts[count] = _start; ends[count] = _end; color[count] = _color; myCount++; if (count >= CACHED_CHARS_BUFFER_SIZE - 1) { flushContent(g); } } } private void flushCachedChars(@NotNull Graphics g) { for (CachedFontContent cache : myFontCache) { cache.flushContent(g); } myLastCache = null; } private void paintCaretCursor(@NotNull Graphics g) { // There is a possible case that visual caret position is changed because of newly added or removed soft wraps. // We check if that's the case and ask caret model to recalculate visual position if necessary. myCaretCursor.paint(g); } private void paintLineMarkersSeparators(@NotNull final Graphics g, @NotNull final Rectangle clip, @NotNull MarkupModelEx markupModel, int clipStartOffset, int clipEndOffset) { markupModel.processRangeHighlightersOverlappingWith(clipStartOffset, clipEndOffset, new Processor() { @Override public boolean process(@NotNull RangeHighlighterEx lineMarker) { if (!lineMarker.getEditorFilter().avaliableIn(EditorImpl.this)) return true; paintLineMarkerSeparator(lineMarker, clip, g); return true; } }); } private void paintLineMarkerSeparator(@NotNull RangeHighlighter marker, @NotNull Rectangle clip, @NotNull Graphics g) { Color separatorColor = marker.getLineSeparatorColor(); if (separatorColor == null) { return; } int line = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument() .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset()); if (line < 0 || line >= myDocument.getLineCount()) { return; } // There is a possible case that particular logical line occupies more than one visual line (because of soft wraps processing), // hence, we need to consider that during calculating 'y' position for the last visual line used for the target logical // line representation. int y; SeparatorPlacement placement = marker.getLineSeparatorPlacement(); if (placement == SeparatorPlacement.TOP) { y = visibleLineToY(logicalToVisualLine(line)); } else if (line + 1 >= myDocument.getLineCount()) { y = visibleLineToY(offsetToVisualLine(myDocument.getTextLength()) + 1); } else { y = logicalLineToY(line + 1); } if (y < clip.y || y > clip.y + clip.height) return; int endShift = clip.x + clip.width; g.setColor(separatorColor); if (mySettings.isRightMarginShown() && myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR) != null) { endShift = Math.min(endShift, mySettings.getRightMargin(myProject) * EditorUtil.getSpaceWidth(Font.PLAIN, this)); } final LineSeparatorRenderer lineSeparatorRenderer = marker.getLineSeparatorRenderer(); if (lineSeparatorRenderer != null) { lineSeparatorRenderer.drawLine(g, 0, endShift, y - 1); } else { UIUtil.drawLine(g, 0, y - 1, endShift, y - 1); } } private int drawStringWithSoftWraps(@NotNull Graphics g, @NotNull final String text, @NotNull Point position, @NotNull Rectangle clip, Color effectColor, EffectType effectType, @JdkConstants.FontStyle int fontType, Color fontColor, @NotNull Ref startDrawingLogicalPosition) { return drawStringWithSoftWraps(g, text, 0, text.length(), position, clip, effectColor, effectType, fontType, fontColor, startDrawingLogicalPosition); } private int drawStringWithSoftWraps(@NotNull Graphics g, final CharSequence text, final int start, final int end, @NotNull Point position, @NotNull Rectangle clip, Color effectColor, EffectType effectType, @JdkConstants.FontStyle int fontType, Color fontColor, @NotNull Ref startDrawingLogicalPosition) { int startToUse = start; // There is a possible case that starting logical line is split by soft-wraps and it's part after the split should be drawn. // We need to skip necessary number of visual lines then. int softWrapLinesToSkip = 0; if (startDrawingLogicalPosition.get() != null) { softWrapLinesToSkip = startDrawingLogicalPosition.get().softWrapLinesOnCurrentLogicalLine; } SoftWrap lastSkippedSoftWrap = null; if (softWrapLinesToSkip > 0) { List softWraps = getSoftWrapModel().getSoftWrapsForLine(startDrawingLogicalPosition.get().line); for (SoftWrap softWrap : softWraps) { softWrapLinesToSkip--; // Assuming that soft wrap has a single line feed all the time if (softWrapLinesToSkip <= 0) { lastSkippedSoftWrap = softWrap; startToUse = softWrap.getStart(); break; } } } startToUse = Math.max(startToUse, start); if (startToUse >= end && getSoftWrapModel().getSoftWrap(startToUse) == null) { return position.x; } startDrawingLogicalPosition.set(null); // Given 'end' offset is exclusive though SoftWrapModel.getSoftWrapsForRange() uses inclusive end offset. // Hence, we decrement it if necessary. Please note that we don't do that if start is equal to end. That is the case, // for example, for soft-wrapped collapsed fold region - we need to draw soft wrap before it. int softWrapRetrievalEndOffset = end; if (startToUse < end) { softWrapRetrievalEndOffset--; } outer: for (SoftWrap softWrap : getSoftWrapModel().getSoftWrapsForRange(startToUse, softWrapRetrievalEndOffset)) { char[] softWrapChars = softWrap.getChars(); CharArrayCharSequence softWrapSeq = new CharArrayCharSequence(softWrapChars); if (softWrap.equals(lastSkippedSoftWrap)) { // If we are here that means that we are located on soft wrap-introduced visual line just after soft wrap. Hence, we need // to draw soft wrap indent if any and 'after soft wrap' sign. int i = CharArrayUtil.lastIndexOf(softWrapChars, '\n', 0, softWrapChars.length); if (i < softWrapChars.length - 1) { position.x = 0; // Soft wrap starts new visual line position.x = drawString( g, softWrapSeq, i + 1, softWrapChars.length, position, clip, null, null, fontType, fontColor ); } position.x += mySoftWrapModel.paint(g, SoftWrapDrawingType.AFTER_SOFT_WRAP, position.x, position.y, getLineHeight()); myForceRefreshFont = true; continue; } // Draw token text before the wrap. if (softWrap.getStart() > startToUse) { position.x = drawString( g, text, startToUse, softWrap.getStart(), position, clip, null, null, fontType, fontColor ); } startToUse = softWrap.getStart(); // We don't draw every soft wrap symbol one-by-one but whole visual line. Current variable holds index that points // to the first soft wrap symbol that is not drawn yet. int softWrapSegmentStartIndex = 0; for (int i = 0; i < softWrapChars.length; i++) { // Delay soft wraps symbols drawing until EOL is found. if (softWrapChars[i] != '\n') { continue; } // Draw soft wrap symbols on current visual line if any. if (i - softWrapSegmentStartIndex > 0) { drawString( g, softWrapSeq, softWrapSegmentStartIndex, i, position, clip, null, null, fontType, fontColor ); } mySoftWrapModel.paint(g, SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED, position.x, position.y, getLineHeight()); myForceRefreshFont = true; // Reset 'x' coordinate because of new line start. position.x = 0; // Stop the processing if we drew the whole clip. if (position.y > clip.y + clip.height) { break outer; } position.y += getLineHeight(); softWrapSegmentStartIndex = i + 1; } // Draw remaining soft wrap symbols from its last line if any. if (softWrapSegmentStartIndex < softWrapChars.length) { position.x += drawString( g, softWrapSeq, softWrapSegmentStartIndex, softWrapChars.length, position, clip, null, null, fontType, fontColor ); } position.x += mySoftWrapModel.paint(g, SoftWrapDrawingType.AFTER_SOFT_WRAP, position.x, position.y, getLineHeight()); myForceRefreshFont = true; } return position.x = drawString(g, text, startToUse, end, position, clip, effectColor, effectType, fontType, fontColor); } private int drawString(@NotNull Graphics g, final CharSequence text, int start, int end, @NotNull Point position, @NotNull Rectangle clip, @Nullable Color effectColor, @Nullable EffectType effectType, @JdkConstants.FontStyle int fontType, Color fontColor) { if (start >= end) return position.x; boolean isInClip = getLineHeight() + position.y >= clip.y && position.y <= clip.y + clip.height; if (!isInClip) return position.x; int y = getAscent() + position.y; int x = position.x; return drawTabbedString(g, text, start, end, x, y, effectColor, effectType, fontType, fontColor, clip); } public int getAscent() { return getLineHeight() - getDescent(); } private int drawString(@NotNull Graphics g, @NotNull String text, @NotNull Point position, @NotNull Rectangle clip, Color effectColor, EffectType effectType, @JdkConstants.FontStyle int fontType, Color fontColor) { boolean isInClip = getLineHeight() + position.y >= clip.y && position.y <= clip.y + clip.height; if (!isInClip) return position.x; int y = getAscent() + position.y; int x = position.x; return drawTabbedString(g, text, 0, text.length(), x, y, effectColor, effectType, fontType, fontColor, clip); } private int drawTabbedString(@NotNull Graphics g, CharSequence text, int start, int end, int x, int y, @Nullable Color effectColor, EffectType effectType, @JdkConstants.FontStyle int fontType, Color fontColor, @NotNull final Rectangle clip) { int xStart = x; for (int i = start; i < end; i++) { if (text.charAt(i) != '\t') continue; x = drawTablessString(text, start, i, g, x, y, fontType, fontColor, clip); int x1 = EditorUtil.nextTabStop(x, this); drawTabPlacer(g, y, x, x1); x = x1; start = i + 1; } x = drawTablessString(text, start, end, g, x, y, fontType, fontColor, clip); if (effectColor != null) { final Color savedColor = g.getColor(); // myBorderEffect.flushIfCantProlong(g, this, effectType, effectColor); int xEnd = x; if (xStart < clip.x && xEnd < clip.x || xStart > clip.x + clip.width && xEnd > clip.x + clip.width) { return x; } if (xEnd > clip.x + clip.width) { xEnd = clip.x + clip.width; } if (xStart < clip.x) { xStart = clip.x; } if (effectType == EffectType.LINE_UNDERSCORE) { g.setColor(effectColor); UIUtil.drawLine(g, xStart, y + 1, xEnd, y + 1); g.setColor(savedColor); } else if (effectType == EffectType.BOLD_LINE_UNDERSCORE) { g.setColor(effectColor); UIUtil.drawLine(g, xStart, y, xEnd, y); UIUtil.drawLine(g, xStart, y + 1, xEnd, y + 1); g.setColor(savedColor); } else if (effectType == EffectType.STRIKEOUT) { g.setColor(effectColor); int y1 = y - getCharHeight() / 2; UIUtil.drawLine(g, xStart, y1, xEnd, y1); g.setColor(savedColor); } else if (effectType == EffectType.WAVE_UNDERSCORE) { g.setColor(effectColor); drawWave(g, xStart, xEnd, y + 1); g.setColor(savedColor); } else if (effectType == EffectType.BOLD_DOTTED_LINE) { final Color bgColor = getBackgroundColor(); final int dottedAt = SystemInfo.isMac ? y : y + 1; UIUtil.drawBoldDottedLine((Graphics2D)g, xStart, xEnd, dottedAt, bgColor, effectColor, false); } } return x; } private int drawTablessString(final CharSequence text, int start, final int end, @NotNull final Graphics g, int x, final int y, @JdkConstants.FontStyle final int fontType, final Color fontColor, @NotNull final Rectangle clip) { int endX = x; if (start < end) { FontInfo font = EditorUtil.fontForChar(text.charAt(start), fontType, this); for (int j = start; j < end; j++) { final char c = text.charAt(j); FontInfo newFont = EditorUtil.fontForChar(c, fontType, this); if (font != newFont || endX > clip.x + clip.width) { if (!(x < clip.x && endX < clip.x || x > clip.x + clip.width && endX > clip.x + clip.width)) { drawCharsCached(g, text, start, j, x, y, fontType, fontColor); } start = j; x = endX; font = newFont; } if (x < clip.x && endX < clip.x) { start = j; x = endX; font = newFont; } else if (x > clip.x + clip.width) { return endX; } // We experienced the following situation: // * the editor was configured to use monospaced font; // * the document contained either english or russian symbols; // * different fonts were used to display english and russian symbols; // * the fonts mentioned above have different space width; // So, the problem was when white space followed russian word - the white space width was calculated using the english font // but drawn using the russian font, so, there was a visual inconsistency at the editor. final int charWidth = font.charWidth(c); if (c == ' ' && myCommonSpaceWidth > 0 && myLastCache != null && (charWidth != myCommonSpaceWidth || charWidth != myLastCache.spaceWidth)) { myForceRefreshFont = true; } endX += charWidth; if (newFont.hasGlyphsToBreakDrawingIteration() && newFont.getSymbolsToBreakDrawingIteration().contains(c)) { drawCharsCached(g, text, start, j + 1, x, y, fontType, fontColor); x = endX; start = j + 1; } } if (!(x < clip.x && endX < clip.x || x > clip.x + clip.width && endX > clip.x + clip.width)) { drawCharsCached(g, text, start, end, x, y, fontType, fontColor); } } return endX; } private void drawTabPlacer(Graphics g, int y, int start, int stop) { if (mySettings.isWhitespacesShown()) { myTabPainter.paint(g, y, start, stop); } } private void drawCharsCached(@NotNull Graphics g, CharSequence data, int start, int end, int x, int y, @JdkConstants.FontStyle int fontType, Color color) { if (!myForceRefreshFont && myCommonSpaceWidth > 0 && myLastCache != null && spacesOnly(data, start, end)) { myLastCache.addContent(g, data, start, end, x, y, null); } else { myForceRefreshFont = false; FontInfo fnt = EditorUtil.fontForChar(data.charAt(start), fontType, this); drawCharsCached(g, data, start, end, x, y, fnt, color); } } private void drawCharsCached(@NotNull Graphics g, @NotNull CharSequence data, int start, int end, int x, int y, @NotNull FontInfo fnt, Color color) { CachedFontContent cache = null; for (CachedFontContent fontCache : myFontCache) { if (fontCache.myFontType == fnt) { cache = fontCache; break; } } if (cache == null) { cache = new CachedFontContent(fnt); myFontCache.add(cache); } myLastCache = cache; cache.addContent(g, data, start, end, x, y, color); } private static boolean spacesOnly(CharSequence chars, int start, int end) { for (int i = start; i < end; i++) { if (chars.charAt(i) != ' ') return false; } return true; } private static final char IDEOGRAPHIC_SPACE = '\u3000'; // http://www.marathon-studios.com/unicode/U3000/Ideographic_Space private void drawChars(@NotNull Graphics g, CharSequence data, int start, int end, int x, int y) { g.drawString(data.subSequence(start, end).toString(), x, y); if (mySettings.isWhitespacesShown()) { Color oldColor = g.getColor(); g.setColor(myScheme.getColor(EditorColors.WHITESPACES_COLOR)); final FontMetrics metrics = g.getFontMetrics(); for (int i = start; i < end; i++) { final char c = data.charAt(i); final int charWidth = isOracleRetina ? GraphicsUtil.charWidth(c, g.getFont()) : metrics.charWidth(c); if (c == ' ') { g.fillRect(x + (charWidth >> 1), y, 1, 1); } else if (c == IDEOGRAPHIC_SPACE) { final int charHeight = getCharHeight(); g.drawRect(x + 2, y - charHeight, charWidth - 4, charHeight); } x += charWidth; } g.setColor(oldColor); } } private static final int WAVE_HEIGHT = 2; private static final int WAVE_SEGMENT_LENGTH = 4; private static void drawWave(Graphics g, int xStart, int xEnd, int y) { int startSegment = xStart / WAVE_SEGMENT_LENGTH; int endSegment = xEnd / WAVE_SEGMENT_LENGTH; for (int i = startSegment; i < endSegment; i++) { drawWaveSegment(g, WAVE_SEGMENT_LENGTH * i, y); } int x = WAVE_SEGMENT_LENGTH * endSegment; UIUtil.drawLine(g, x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); } private static void drawWaveSegment(Graphics g, int x, int y) { UIUtil.drawLine(g, x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y); UIUtil.drawLine(g, x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT); } private int getTextSegmentWidth(@NotNull CharSequence text, int start, int end, int xStart, @JdkConstants.FontStyle int fontType, @NotNull Rectangle clip) { int x = xStart; for (int i = start; i < end && xStart < clip.x + clip.width; i++) { char c = text.charAt(i); if (c == '\t') { x = EditorUtil.nextTabStop(x, this); } else { x += EditorUtil.charWidth(c, fontType, this); } if (x > clip.x + clip.width) { break; } } return x - xStart; } @Override public int getLineHeight() { assertReadAccess(); int lineHeight = myLineHeight; if (lineHeight < 0) { FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); int fontMetricsHeight = fontMetrics.getHeight(); lineHeight = (int)(fontMetricsHeight * (isOneLineMode() ? 1 : myScheme.getLineSpacing())); if (lineHeight <= 0) { lineHeight = fontMetricsHeight; if (lineHeight <= 0) { lineHeight = 12; } } assert lineHeight > 0 : lineHeight; myLineHeight = lineHeight; } return lineHeight; } public int getDescent() { if (myDescent != -1) { return myDescent; } FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); myDescent = fontMetrics.getDescent(); return myDescent; } @NotNull public FontMetrics getFontMetrics(@JdkConstants.FontStyle int fontType) { if (myPlainFontMetrics == null) { assertIsDispatchThread(); myPlainFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); myBoldFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD)); myItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.ITALIC)); myBoldItalicFontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.BOLD_ITALIC)); } if (fontType == Font.PLAIN) return myPlainFontMetrics; if (fontType == Font.BOLD) return myBoldFontMetrics; if (fontType == Font.ITALIC) return myItalicFontMetrics; if (fontType == (Font.BOLD | Font.ITALIC)) return myBoldItalicFontMetrics; LOG.error("Unknown font type: " + fontType); return myPlainFontMetrics; } private int getCharHeight() { if (myCharHeight == -1) { assertIsDispatchThread(); FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN)); myCharHeight = fontMetrics.charWidth('a'); } return myCharHeight; } public int getPreferredHeight() { if (ourIsUnitTestMode && getUserData(DO_DOCUMENT_UPDATE_TEST) == null) { return 1; } return getHeightWithoutCaret(); } public Dimension getPreferredSize() { if (ourIsUnitTestMode && getUserData(DO_DOCUMENT_UPDATE_TEST) == null) { return new Dimension(1, 1); } final Dimension draft = getSizeWithoutCaret(); final int additionalSpace = mySoftWrapModel.isRespectAdditionalColumns() ? mySettings.getAdditionalColumnsCount() * EditorUtil.getSpaceWidth(Font.PLAIN, this) : 0; if (!myDocument.isInBulkUpdate()) { for (Caret caret : myCaretModel.getAllCarets()) { if (caret.isUpToDate()) { int caretX = visualPositionToXY(caret.getVisualPosition()).x; draft.width = Math.max(caretX, draft.width); } } } draft.width += additionalSpace; return draft; } private Dimension getSizeWithoutCaret() { Dimension size = mySizeContainer.getContentSize(); if (isOneLineMode()) return new Dimension(size.width, getLineHeight()); if (mySettings.isAdditionalPageAtBottom()) { int lineHeight = getLineHeight(); int visibleAreaHeight = getScrollingModel().getVisibleArea().height; int virtualPageHeight; // There is a possible case that user with 'show additional page at bottom' scrolls to that virtual page; switched to another // editor (another tab); and then returns to the previously used editor (the one scrolled to virtual page). We want to preserve // correct view size then because viewport position is set to the end of the original text otherwise. if (visibleAreaHeight <= 0 && myVirtualPageHeight > 0) { virtualPageHeight = myVirtualPageHeight; } else { myVirtualPageHeight = virtualPageHeight = Math.max(visibleAreaHeight - 2 * lineHeight, lineHeight); } if (myVirtualPageHeight > 0) { return new Dimension(size.width, size.height + virtualPageHeight); } return size; } return getContentSize(); } private int getHeightWithoutCaret() { if (isOneLineMode()) return getLineHeight(); int size = mySizeContainer.getContentHeight(); if (mySettings.isAdditionalPageAtBottom()) { int lineHeight = getLineHeight(); return size + Math.max(getScrollingModel().getVisibleArea().height - 2 * lineHeight, lineHeight); } return size + mySettings.getAdditionalLinesCount() * getLineHeight(); } @NotNull @Override public Dimension getContentSize() { Dimension size = mySizeContainer.getContentSize(); return new Dimension(size.width, size.height + mySettings.getAdditionalLinesCount() * getLineHeight()); } @NotNull @Override public JScrollPane getScrollPane() { return myScrollPane; } @Override public void setBorder(Border border) { myScrollPane.setBorder(border); } @Override public Insets getInsets() { return myScrollPane.getInsets(); } @Override public int logicalPositionToOffset(@NotNull LogicalPosition pos) { return logicalPositionToOffset(pos, null); } public int logicalPositionToOffset(@NotNull LogicalPosition pos, @Nullable StringBuilder debugBuffer) { assertReadAccess(); if (myDocument.getLineCount() == 0) return 0; if (pos.line < 0) throw new IndexOutOfBoundsException("Wrong line: " + pos.line); if (pos.column < 0) throw new IndexOutOfBoundsException("Wrong column:" + pos.column); if (pos.line >= myDocument.getLineCount()) { return myDocument.getTextLength(); } int start = myDocument.getLineStartOffset(pos.line); if (pos.column == 0) return start; int end = myDocument.getLineEndOffset(pos.line); CharSequence text = myDocument.getImmutableCharSequence(); return EditorUtil.calcOffset(this, text, start, end, pos.column, EditorUtil.getTabSize(this), debugBuffer); } @Override public void setLastColumnNumber(int val) { assertIsDispatchThread(); myLastColumnNumber = val; } @Override public int getLastColumnNumber() { assertReadAccess(); return myLastColumnNumber; } /** * @return information about total number of lines that can be viewed by user. I.e. this is a number of all document * lines (considering that single logical document line may be represented on multiple visual lines because of * soft wraps appliance) minus number of folded lines */ public int getVisibleLineCount() { return getVisibleLogicalLinesCount() + getSoftWrapModel().getSoftWrapsIntroducedLinesNumber(); } /** * @return number of visible logical lines. Generally, that is a total logical lines number minus number of folded lines */ private int getVisibleLogicalLinesCount() { return getDocument().getLineCount() - myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1); } @Override @NotNull public VisualPosition logicalToVisualPosition(@NotNull LogicalPosition logicalPos) { return logicalToVisualPosition(logicalPos, true); } @Override @NotNull public VisualPosition logicalToVisualPosition(@NotNull LogicalPosition logicalPos, boolean softWrapAware) { return doLogicalToVisualPosition(logicalPos, softWrapAware,0); } @NotNull private VisualPosition doLogicalToVisualPosition(@NotNull LogicalPosition logicalPos, boolean softWrapAware, // TODO den remove as soon as the problem is fixed. int stackDepth) { assertReadAccess(); if (!myFoldingModel.isFoldingEnabled() && !mySoftWrapModel.isSoftWrappingEnabled()) { return new VisualPosition(logicalPos.line, logicalPos.column); } int offset = logicalPositionToOffset(logicalPos); FoldRegion outermostCollapsed = myFoldingModel.getCollapsedRegionAtOffset(offset); if (outermostCollapsed != null && offset > outermostCollapsed.getStartOffset()) { if (offset < getDocument().getTextLength()) { offset = outermostCollapsed.getStartOffset(); LogicalPosition foldStart = offsetToLogicalPosition(offset); // TODO den remove as soon as the problem is fixed. if (stackDepth > 15) { LOG.error("Detected potential StackOverflowError at logical->visual position mapping. Given logical position: '" + logicalPos + "'. State: " + dumpState()); stackDepth = -1; } return doLogicalToVisualPosition(foldStart, true, stackDepth+1); } else { offset = outermostCollapsed.getEndOffset() + 3; // WTF? } } int line = logicalPos.line; int column = logicalPos.column; int foldedLinesCountBefore = myFoldingModel.getFoldedLinesCountBefore(offset); line -= foldedLinesCountBefore; if (line < 0) { LogMessageEx.error( LOG, "Invalid LogicalPosition -> VisualPosition processing", String.format( "Given logical position: %s; matched line: %d; fold lines before: %d, state: %s", logicalPos, line, foldedLinesCountBefore, dumpState() )); } FoldRegion[] topLevel = myFoldingModel.fetchTopLevel(); LogicalPosition anchorFoldingPosition = logicalPos; for (int idx = myFoldingModel.getLastTopLevelIndexBefore(offset); idx >= 0 && topLevel != null; idx--) { FoldRegion region = topLevel[idx]; if (region.isValid()) { if (region.getDocument().getLineNumber(region.getEndOffset()) == anchorFoldingPosition.line && region.getEndOffset() <= offset) { LogicalPosition foldStart = offsetToLogicalPosition(region.getStartOffset()); LogicalPosition foldEnd = offsetToLogicalPosition(region.getEndOffset()); column += foldStart.column + region.getPlaceholderText().length() - foldEnd.column; offset = region.getStartOffset(); anchorFoldingPosition = foldStart; } else { break; } } } VisualPosition softWrapUnawarePosition = new VisualPosition(line, Math.max(0, column)); if (softWrapAware) { return mySoftWrapModel.adjustVisualPosition(logicalPos, softWrapUnawarePosition); } return softWrapUnawarePosition; } @Nullable private FoldRegion getLastCollapsedBeforePosition(@NotNull VisualPosition visualPos) { FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel(); if (topLevelCollapsed == null) return null; int start = 0; int end = topLevelCollapsed.length - 1; int i = 0; while (start <= end) { i = (start + end) / 2; FoldRegion region = topLevelCollapsed[i]; if (!region.isValid()) { // Folding model is inconsistent (update in progress). return null; } int regionVisualLine = offsetToVisualLine(region.getEndOffset() - 1); if (regionVisualLine < visualPos.line) { start = i + 1; } else { if (regionVisualLine > visualPos.line) { end = i - 1; } else { VisualPosition visFoldEnd = offsetToVisualPosition(region.getEndOffset() - 1); if (visFoldEnd.column < visualPos.column) { start = i + 1; } else { if (visFoldEnd.column > visualPos.column) { end = i - 1; } else { i--; break; } } } } } while (i >= 0 && i < topLevelCollapsed.length) { if (topLevelCollapsed[i].isValid()) break; i--; } if (i >= 0 && i < topLevelCollapsed.length) { FoldRegion region = topLevelCollapsed[i]; VisualPosition visFoldEnd = offsetToVisualPosition(region.getEndOffset() - 1); if (visFoldEnd.line > visualPos.line || visFoldEnd.line == visualPos.line && visFoldEnd.column > visualPos.column) { i--; if (i >= 0) { return topLevelCollapsed[i]; } return null; } return region; } return null; } @Override @NotNull public LogicalPosition visualToLogicalPosition(@NotNull VisualPosition visiblePos) { return visualToLogicalPosition(visiblePos, true); } @Override @NotNull public LogicalPosition visualToLogicalPosition(@NotNull VisualPosition visiblePos, boolean softWrapAware) { assertReadAccess(); if (softWrapAware) { return mySoftWrapModel.visualToLogicalPosition(visiblePos); } if (!myFoldingModel.isFoldingEnabled()) return new LogicalPosition(visiblePos.line, visiblePos.column); int line = visiblePos.line; int column = visiblePos.column; FoldRegion lastCollapsedBefore = getLastCollapsedBeforePosition(visiblePos); if (lastCollapsedBefore != null) { int logFoldEndLine = offsetToLogicalLine(lastCollapsedBefore.getEndOffset(), false); int visFoldEndLine = logicalToVisualLine(logFoldEndLine); line = logFoldEndLine + visiblePos.line - visFoldEndLine; if (visFoldEndLine == visiblePos.line) { LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset(), false); VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd, false); if (visiblePos.column >= visFoldEnd.column) { column = logFoldEnd.column + visiblePos.column - visFoldEnd.column; } else { return offsetToLogicalPosition(lastCollapsedBefore.getStartOffset(), false); } } } if (column < 0) column = 0; return new LogicalPosition(line, column); } int offsetToLogicalLine(int offset) { return offsetToLogicalLine(offset, true); } private int offsetToLogicalLine(int offset, boolean softWrapAware) { int textLength = myDocument.getTextLength(); if (textLength == 0) return 0; if (offset > textLength || offset < 0) { throw new IndexOutOfBoundsException("Wrong offset: " + offset + " textLength: " + textLength); } int lineIndex = myDocument.getLineNumber(offset); LOG.assertTrue(lineIndex >= 0 && lineIndex < myDocument.getLineCount()); if (softWrapAware && getSoftWrapModel().isSoftWrappingEnabled()) { int column = calcColumnNumber(offset, lineIndex, false, myDocument.getImmutableCharSequence()); return mySoftWrapModel.adjustLogicalPosition(new LogicalPosition(lineIndex, column), offset).line; } else { return lineIndex; } } @Override public int calcColumnNumber(int offset, int lineIndex) { return calcColumnNumber(offset, lineIndex, true, myDocument.getImmutableCharSequence()); } public int calcColumnNumber(int offset, int lineIndex, boolean softWrapAware, @NotNull CharSequence documentCharSequence) { if (myDocument.getTextLength() == 0) return 0; int lineStartOffset = myDocument.getLineStartOffset(lineIndex); if (lineStartOffset == offset) return 0; int column = EditorUtil.calcColumnNumber(this, documentCharSequence, lineStartOffset, offset); if (softWrapAware) { int line = offsetToLogicalLine(offset, false); return mySoftWrapModel.adjustLogicalPosition(new LogicalPosition(line, column), offset).column; } else { return column; } } private LogicalPosition getLogicalPositionForScreenPos(int x, int y, boolean trimToLineWidth) { if (x < 0) { x = 0; } LogicalPosition pos = xyToLogicalPosition(new Point(x, y)); int column = pos.column; int line = pos.line; int softWrapLinesBeforeTargetLogicalLine = pos.softWrapLinesBeforeCurrentLogicalLine; int softWrapLinesOnTargetLogicalLine = pos.softWrapLinesOnCurrentLogicalLine; int softWrapColumns = pos.softWrapColumnDiff; if (line < 0) { line = 0; column = 0; softWrapLinesBeforeTargetLogicalLine = 0; softWrapLinesOnTargetLogicalLine = 0; softWrapColumns = 0; } final int totalLines = myDocument.getLineCount(); if (totalLines <= 0) { return new LogicalPosition(0, 0); } if (line >= totalLines && totalLines > 0) { int visibleLineCount = getVisibleLineCount(); int newY = visibleLineCount > 0 ? visibleLineToY(visibleLineCount - 1) : 0; if (newY > 0 && newY == y) { newY = visibleLineToY(getVisibleLogicalLinesCount()); } if (newY >= y) { LogMessageEx.error(LOG, "cycled moveCaretToScreenPos() detected", String.format("x=%d, y=%d\nstate=%s", x, y, dumpState())); throw new IllegalStateException("cycled moveCaretToScreenPos() detected"); } return getLogicalPositionForScreenPos(x, newY, trimToLineWidth); } if (!mySettings.isVirtualSpace() && !mySelectionModel.hasBlockSelection() && trimToLineWidth) { int lineEndOffset = myDocument.getLineEndOffset(line); int lineEndColumn = calcColumnNumber(lineEndOffset, line); if (column > lineEndColumn) { column = lineEndColumn; if (softWrapColumns != 0) { softWrapColumns -= column - lineEndColumn; } } } if (!mySettings.isCaretInsideTabs()) { int offset = logicalPositionToOffset(new LogicalPosition(line, column)); CharSequence text = myDocument.getImmutableCharSequence(); if (offset >= 0 && offset < myDocument.getTextLength()) { if (text.charAt(offset) == '\t') { column = calcColumnNumber(offset, line); } } } return new LogicalPosition( line, column, softWrapLinesBeforeTargetLogicalLine, softWrapLinesOnTargetLogicalLine, softWrapColumns, pos.foldedLines, pos.foldingColumnDiff ); } private boolean checkIgnore(@NotNull MouseEvent e, boolean isFinalCheck) { if (!myIgnoreMouseEventsConsecutiveToInitial) { myInitialMouseEvent = null; return false; } if (myInitialMouseEvent!= null && (e.getComponent() != myInitialMouseEvent.getComponent() || !e.getPoint().equals(myInitialMouseEvent.getPoint()))) { myIgnoreMouseEventsConsecutiveToInitial = false; myInitialMouseEvent = null; return false; } if (isFinalCheck) { myIgnoreMouseEventsConsecutiveToInitial = false; myInitialMouseEvent = null; } e.consume(); return true; } private void processMouseReleased(@NotNull MouseEvent e) { if (checkIgnore(e, true)) return; if (e.getSource() == myGutterComponent && !(myMousePressedEvent != null && myMousePressedEvent.isConsumed())) { myGutterComponent.mouseReleased(e); } if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) { return; } // if (myMousePressedInsideSelection) getSelectionModel().removeSelection(); final FoldRegion region = getFoldingModel().getFoldingPlaceholderAt(e.getPoint()); if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) { getFoldingModel().runBatchFoldingOperation(new Runnable() { @Override public void run() { myFoldingModel.flushCaretShift(); region.setExpanded(true); } }); // The call below is performed because gutter's height is not updated sometimes, i.e. it sticks to the value that corresponds // to the situation when fold region is collapsed. That causes bottom of the gutter to not be repainted and that looks really ugly. myGutterComponent.updateSize(); } // The general idea is to check if the user performed 'caret position change click' (left click most of the time) inside selection // and, in the case of the positive answer, clear selection. Please note that there is a possible case that mouse click // is performed inside selection but it triggers context menu. We don't want to drop the selection then. if (myMousePressedEvent != null && myMousePressedEvent.getClickCount() == 1 && myMousePressedInsideSelection && !myMousePressedEvent.isShiftDown() && !myMousePressedEvent.isPopupTrigger()) { getSelectionModel().removeSelection(); } } @NotNull @Override public DataContext getDataContext() { return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent())); } @NotNull private DataContext getProjectAwareDataContext(@NotNull final DataContext original) { if (CommonDataKeys.PROJECT.getData(original) == myProject) return original; return new DataContext() { @Override public Object getData(String dataId) { if (CommonDataKeys.PROJECT.is(dataId)) { return myProject; } return original.getData(dataId); } }; } @Override public EditorMouseEventArea getMouseEventArea(@NotNull MouseEvent e) { if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA; int x = myGutterComponent.convertX(e.getX()); return myGutterComponent.getEditorMouseAreaByOffset(x); } private void requestFocus() { final IdeFocusManager focusManager = IdeFocusManager.getInstance(myProject); if (focusManager.getFocusOwner() != myEditorComponent) { //IDEA-64501 focusManager.requestFocus(myEditorComponent, true); } } private void validateMousePointer(@NotNull MouseEvent e) { if (e.getSource() == myGutterComponent) { FoldRegion foldingAtCursor = myGutterComponent.findFoldingAnchorAt(e.getX(), e.getY()); myGutterComponent.setActiveFoldRegion(foldingAtCursor); if (foldingAtCursor != null) { myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else { myGutterComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } } else { myGutterComponent.setActiveFoldRegion(null); if (getSelectionModel().hasSelection() && (e.getModifiersEx() & (InputEvent.BUTTON1_DOWN_MASK | InputEvent.BUTTON2_DOWN_MASK)) == 0) { int offset = logicalPositionToOffset(xyToLogicalPosition(e.getPoint())); if (getSelectionModel().getSelectionStart() <= offset && offset < getSelectionModel().getSelectionEnd()) { myEditorComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); return; } } myEditorComponent.setCursor(UIUtil.getTextCursor(getBackgroundColor())); } } private void runMouseDraggedCommand(@NotNull final MouseEvent e) { if (myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) { return; } myCommandProcessor.executeCommand(myProject, new Runnable() { @Override public void run() { processMouseDragged(e); } }, "", MOUSE_DRAGGED_GROUP, UndoConfirmationPolicy.DEFAULT, getDocument()); } private void processMouseDragged(@NotNull MouseEvent e) { if (SwingUtilities.isRightMouseButton(e)) { return; } if (getMouseEventArea(e) == EditorMouseEventArea.LINE_MARKERS_AREA) { // The general idea is that we don't want to change caret position on gutter marker area click (e.g. on setting a breakpoint) // but do want to allow bulk selection on gutter marker mouse drag. However, when a drag is performed, the first event is // a 'mouse pressed' event, that's why we remember target line on 'mouse pressed' processing and use that information on // further dragging (if any). if (myDragOnGutterSelectionStartLine >= 0) { mySelectionModel.removeSelection(); myCaretModel.moveToOffset(myDragOnGutterSelectionStartLine < myDocument.getLineCount() ? myDocument.getLineStartOffset(myDragOnGutterSelectionStartLine) : myDocument.getTextLength()); } myDragOnGutterSelectionStartLine = - 1; } Rectangle visibleArea = getScrollingModel().getVisibleArea(); int x = e.getX(); if (e.getSource() == myGutterComponent) { x = 0; } int dx = 0; if (x < visibleArea.x && visibleArea.x > 0) { dx = x - visibleArea.x; } else { if (x > visibleArea.x + visibleArea.width) { dx = x - visibleArea.x - visibleArea.width; } } int dy = 0; int y = e.getY(); if (y < visibleArea.y && visibleArea.y > 0) { dy = y - visibleArea.y; } else { if (y > visibleArea.y + visibleArea.height) { dy = y - visibleArea.y - visibleArea.height; } } if (dx == 0 && dy == 0) { myScrollingTimer.stop(); SelectionModel selectionModel = getSelectionModel(); Caret leadCaret = getLeadCaret(); int oldSelectionStart = leadCaret.getLeadSelectionOffset(); VisualPosition oldVisLeadSelectionStart = leadCaret.getLeadSelectionPosition(); int oldCaretOffset = getCaretModel().getOffset(); LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition(); boolean multiCaretSelection = myCaretModel.supportsMultipleCarets() && (isColumnMode() || e.isAltDown()); LogicalPosition newLogicalCaret = getLogicalPositionForScreenPos(x, y, !multiCaretSelection); if (multiCaretSelection) { myMultiSelectionInProgress = true; myTargetMultiSelectionPosition = xyToVisualPosition(new Point(Math.max(x, 0), Math.max(y, 0))); getScrollingModel().scrollTo(newLogicalCaret, ScrollType.RELATIVE); } else { getCaretModel().moveToLogicalPosition(newLogicalCaret); getScrollingModel().scrollToCaret(ScrollType.RELATIVE); } int newCaretOffset = getCaretModel().getOffset(); VisualPosition newVisualCaret = getCaretModel().getVisualPosition(); int caretShift = newCaretOffset - mySavedSelectionStart; if (myMousePressedEvent != null && getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.EDITING_AREA && getMouseEventArea(myMousePressedEvent) != EditorMouseEventArea.LINE_NUMBERS_AREA) { selectionModel.setSelection(oldSelectionStart, newCaretOffset); } else { if (isColumnMode() || e.isAltDown()) { if (myCaretModel.supportsMultipleCarets()) { if (myLastMousePressedLocation != null && (myCurrentDragIsSubstantial || !newLogicalCaret.equals(myLastMousePressedLocation))) { setBlockSelectionAndBlockActions(e, myLastMousePressedLocation, newLogicalCaret); } } else { final LogicalPosition blockStart = selectionModel.hasBlockSelection() ? selectionModel.getBlockStart() : oldLogicalCaret; if (blockStart != null) { setBlockSelectionAndBlockActions(e, blockStart, getCaretModel().getLogicalPosition()); } } } else { if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { if (caretShift < 0) { int newSelection = newCaretOffset; if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { newSelection = myCaretModel.getWordAtCaretStart(); } else { if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { newSelection = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0))); } } if (newSelection < 0) newSelection = newCaretOffset; selectionModel.setSelection(mySavedSelectionEnd, newSelection); getCaretModel().moveToOffset(newSelection); } else { int newSelection = newCaretOffset; if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { newSelection = myCaretModel.getWordAtCaretEnd(); } else { if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { newSelection = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0))); } } if (newSelection < 0) newSelection = newCaretOffset; selectionModel.setSelection(mySavedSelectionStart, newSelection); getCaretModel().moveToOffset(newSelection); } cancelAutoResetForMouseSelectionState(); return; } if (!myMousePressedInsideSelection) { // There is a possible case that lead selection position should be adjusted in accordance with the mouse move direction. // E.g. consider situation when user selects the whole line by clicking at 'line numbers' area. 'Line end' is considered // to be lead selection point then. However, when mouse is dragged down we want to consider 'line start' to be // lead selection point. if ((myMousePressArea == EditorMouseEventArea.LINE_NUMBERS_AREA || myMousePressArea == EditorMouseEventArea.LINE_MARKERS_AREA) && selectionModel.hasSelection()) { if (newCaretOffset >= selectionModel.getSelectionEnd()) { oldSelectionStart = selectionModel.getSelectionStart(); oldVisLeadSelectionStart = selectionModel.getSelectionStartPosition(); } else if (newCaretOffset <= selectionModel.getSelectionStart()) { oldSelectionStart = selectionModel.getSelectionEnd(); oldVisLeadSelectionStart = selectionModel.getSelectionEndPosition(); } } if (oldVisLeadSelectionStart != null) { setSelectionAndBlockActions(e, oldVisLeadSelectionStart, oldSelectionStart, newVisualCaret, newCaretOffset); } else { setSelectionAndBlockActions(e, oldSelectionStart, newCaretOffset); } cancelAutoResetForMouseSelectionState(); } else { if (caretShift != 0) { if (myMousePressedEvent != null) { if (mySettings.isDndEnabled()) { boolean isCopy = UIUtil.isControlKeyDown(e) || isViewer() || !getDocument().isWritable(); mySavedCaretOffsetForDNDUndoHack = oldCaretOffset; getContentComponent().getTransferHandler() .exportAsDrag(getContentComponent(), e, isCopy ? TransferHandler.COPY : TransferHandler.MOVE); } else { selectionModel.removeSelection(); } } } } } } } else { myScrollingTimer.start(dx, dy); onSubstantialDrag(e); } } private Caret getLeadCaret() { List allCarets = myCaretModel.getAllCarets(); Caret firstCaret = allCarets.get(0); if (firstCaret == myCaretModel.getPrimaryCaret()) { return allCarets.get(allCarets.size() - 1); } else { return firstCaret; } } private void setSelectionAndBlockActions(@NotNull MouseEvent mouseDragEvent, int startOffset, int endOffset) { mySelectionModel.setSelection(startOffset, endOffset); if (myCurrentDragIsSubstantial || startOffset != endOffset) { onSubstantialDrag(mouseDragEvent); } } private void setSelectionAndBlockActions(@NotNull MouseEvent mouseDragEvent, VisualPosition startPosition, int startOffset, VisualPosition endPosition, int endOffset) { mySelectionModel.setSelection(startPosition, startOffset, endPosition, endOffset); if (myCurrentDragIsSubstantial || startOffset != endOffset || !Comparing.equal(startPosition, endPosition)) { onSubstantialDrag(mouseDragEvent); } } private void setBlockSelectionAndBlockActions(@NotNull MouseEvent mouseDragEvent, @NotNull LogicalPosition startPosition, @NotNull LogicalPosition endPosition) { mySelectionModel.setBlockSelection(startPosition, endPosition); if (myCurrentDragIsSubstantial || !startPosition.equals(endPosition)) { onSubstantialDrag(mouseDragEvent); } } private void onSubstantialDrag(@NotNull MouseEvent mouseDragEvent) { IdeEventQueue.getInstance().blockNextEvents(mouseDragEvent, IdeEventQueue.BlockMode.ACTIONS); myCurrentDragIsSubstantial = true; } private static class RepaintCursorCommand implements Runnable { private long mySleepTime = 500; private boolean myIsBlinkCaret = true; @Nullable private EditorImpl myEditor = null; @NotNull private final MyRepaintRunnable myRepaintRunnable; private ScheduledFuture mySchedulerHandle; private RepaintCursorCommand() { myRepaintRunnable = new MyRepaintRunnable(); } private class MyRepaintRunnable implements Runnable { @Override public void run() { if (myEditor != null) { myEditor.myCaretCursor.repaint(); } } } public void start() { if (mySchedulerHandle != null) { mySchedulerHandle.cancel(false); } mySchedulerHandle = JobScheduler.getScheduler().scheduleWithFixedDelay(this, mySleepTime, mySleepTime, TimeUnit.MILLISECONDS); } private void setBlinkPeriod(int blinkPeriod) { mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10; start(); } private void setBlinkCaret(boolean value) { myIsBlinkCaret = value; } @Override public void run() { if (myEditor != null) { CaretCursor activeCursor = myEditor.myCaretCursor; long time = System.currentTimeMillis(); time -= activeCursor.myStartTime; if (time > mySleepTime) { boolean toRepaint = true; if (myIsBlinkCaret) { activeCursor.myIsShown = !activeCursor.myIsShown; } else { toRepaint = !activeCursor.myIsShown; activeCursor.myIsShown = true; } if (toRepaint) { SwingUtilities.invokeLater(myRepaintRunnable); } } } } } void updateCaretCursor() { myUpdateCursor = true; } private void setCursorPosition() { if (myCaretModel.supportsMultipleCarets()) { final List caretPoints = new ArrayList(); for (Caret caret : getCaretModel().getAllCarets()) { VisualPosition caretPosition = caret.getVisualPosition(); Point pos1 = visualPositionToXY(caretPosition); Point pos2 = visualPositionToXY(new VisualPosition(caretPosition.line, caretPosition.column + 1)); caretPoints.add(new CaretRectangle(pos1, pos2.x - pos1.x, caret)); } myCaretCursor.setPositions(caretPoints.toArray(new CaretRectangle[caretPoints.size()])); } else { VisualPosition caretPosition = getCaretModel().getVisualPosition(); Point pos1 = visualPositionToXY(caretPosition); Point pos2 = visualPositionToXY(new VisualPosition(caretPosition.line, caretPosition.column + 1)); myCaretCursor.setPosition(pos1, pos2.x - pos1.x); } } @Override public boolean setCaretVisible(boolean b) { boolean old = myCaretCursor.isActive(); if (b) { myCaretCursor.activate(); } else { myCaretCursor.passivate(); } return old; } @Override public boolean setCaretEnabled(boolean enabled) { boolean old = myCaretCursor.isEnabled(); myCaretCursor.setEnabled(enabled); return old; } @Override public void addFocusListener(@NotNull FocusChangeListener listener) { myFocusListeners.add(listener); } @Override public void addFocusListener(@NotNull FocusChangeListener listener, @NotNull Disposable parentDisposable) { ContainerUtil.add(listener, myFocusListeners, parentDisposable); } @Override @Nullable public Project getProject() { return myProject; } @Override public boolean isOneLineMode() { return myIsOneLineMode; } @Override public boolean isEmbeddedIntoDialogWrapper() { return myEmbeddedIntoDialogWrapper; } @Override public void setEmbeddedIntoDialogWrapper(boolean b) { assertIsDispatchThread(); myEmbeddedIntoDialogWrapper = b; myScrollPane.setFocusable(!b); myEditorComponent.setFocusCycleRoot(!b); myEditorComponent.setFocusable(b); } @Override public void setOneLineMode(boolean isOneLineMode) { myIsOneLineMode = isOneLineMode; getScrollPane().setInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, null); reinitSettings(); } @Override public void stopOptimizedScrolling() { } private static class CaretRectangle { private final Point myPoint; private final int myWidth; private final Caret myCaret; private CaretRectangle(Point point, int width, Caret caret) { myPoint = point; myWidth = Math.max(width, 2); myCaret = caret; } } private class CaretCursor { private Point myLocation; private int myWidth; private CaretRectangle[] myLocations; private boolean myEnabled; @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) private boolean myIsShown = false; private long myStartTime = 0; private CaretCursor() { myLocation = new Point(0, 0); myLocations = new CaretRectangle[] {new CaretRectangle(new Point(0, 0), 0, null)}; setEnabled(true); } public boolean isEnabled() { return myEnabled; } public void setEnabled(boolean enabled) { myEnabled = enabled; } private void activate() { final boolean blink = mySettings.isBlinkCaret(); final int blinkPeriod = mySettings.getCaretBlinkPeriod(); synchronized (ourCaretBlinkingCommand) { ourCaretBlinkingCommand.myEditor = EditorImpl.this; ourCaretBlinkingCommand.setBlinkCaret(blink); ourCaretBlinkingCommand.setBlinkPeriod(blinkPeriod); myIsShown = true; } } public boolean isActive() { synchronized (ourCaretBlinkingCommand) { return myIsShown; } } private void passivate() { synchronized (ourCaretBlinkingCommand) { myIsShown = false; } } private void setPosition(Point location, int width) { myStartTime = System.currentTimeMillis(); myLocation = location; myWidth = Math.max(width, 2); myIsShown = true; repaint(); } private void setPositions(CaretRectangle[] locations) { myStartTime = System.currentTimeMillis(); myLocations = locations; myIsShown = true; repaint(); } private void repaint() { if (myCaretModel.supportsMultipleCarets()) { for (CaretRectangle location : myLocations) { myEditorComponent.repaintEditorComponent(location.myPoint.x, location.myPoint.y, location.myWidth, getLineHeight()); } } else { myEditorComponent.repaintEditorComponent(myLocation.x, myLocation.y, myWidth, getLineHeight()); } } private void paint(@NotNull Graphics g) { if (!isEnabled() || !myIsShown || !IJSwingUtilities.hasFocus(getContentComponent()) || isRendererMode()) return; if (myCaretModel.supportsMultipleCarets()) { for (CaretRectangle location : myLocations) { paintAt(g, location.myPoint.x, location.myPoint.y, location.myWidth, location.myCaret); } } else { paintAt(g, myLocation.x, myLocation.y, myWidth, null); } } private void paintAt(@NotNull Graphics g, int x, int y, int width, Caret caret) { int lineHeight = getLineHeight(); Rectangle viewRectangle = getScrollingModel().getVisibleArea(); if (x - viewRectangle.x < 0) { return; } g.setColor(myScheme.getColor(EditorColors.CARET_COLOR)); if (!paintBlockCaret()) { if (UIUtil.isRetina()) { g.fillRect(x, y, mySettings.getLineCursorWidth(), lineHeight); } else { for (int i = 0; i < mySettings.getLineCursorWidth(); i++) { UIUtil.drawLine(g, x + i, y, x + i, y + lineHeight - 1); } } } else { Color caretColor = myScheme.getColor(EditorColors.CARET_COLOR); if (caretColor == null) caretColor = new JBColor(Gray._0, Gray._255); g.setColor(caretColor); g.fillRect(x, y, width, lineHeight - 1); final LogicalPosition startPosition = caret == null ? getCaretModel().getLogicalPosition() : caret.getLogicalPosition(); final int offset = logicalPositionToOffset(startPosition); CharSequence chars = myDocument.getImmutableCharSequence(); if (chars.length() > offset && myDocument.getTextLength() > offset) { FoldRegion folding = myFoldingModel.getCollapsedRegionAtOffset(offset); final char ch; if (folding == null || folding.isExpanded()) { ch = chars.charAt(offset); } else { VisualPosition visual = caret == null ? getCaretModel().getVisualPosition() : caret.getVisualPosition(); VisualPosition foldingPosition = offsetToVisualPosition(folding.getStartOffset()); if (visual.line == foldingPosition.line) { ch = folding.getPlaceholderText().charAt(visual.column - foldingPosition.column); } else { ch = chars.charAt(offset); } } //don't worry it's cheap. Cache is not required IterationState state = new IterationState(EditorImpl.this, offset, offset + 1, true); TextAttributes attributes = state.getMergedAttributes(); FontInfo info = EditorUtil.fontForChar(ch, attributes.getFontType(), EditorImpl.this); if (info != null) { g.setFont(info.getFont()); } //todo[kb] //in case of italic style we paint out of the cursor block. Painting the symbol to a dedicated buffered image //solves the problem, but still looks weird because it leaves colored pixels at right. g.setColor(CURSOR_FOREGROUND); g.drawChars(new char[]{ch}, 0, 1, x, y + getAscent()); } } } } boolean paintBlockCaret() { return myIsInsertMode == mySettings.isBlockCursor(); } private class ScrollingTimer { Timer myTimer; private static final int TIMER_PERIOD = 100; private static final int CYCLE_SIZE = 20; private int myXCycles; private int myYCycles; private int myDx; private int myDy; private int xPassedCycles = 0; private int yPassedCycles = 0; private void start(int dx, int dy) { myDx = 0; myDy = 0; if (dx > 0) { myXCycles = CYCLE_SIZE / dx + 1; myDx = 1 + dx / CYCLE_SIZE; } else { if (dx < 0) { myXCycles = -CYCLE_SIZE / dx + 1; myDx = -1 + dx / CYCLE_SIZE; } } if (dy > 0) { myYCycles = CYCLE_SIZE / dy + 1; myDy = 1 + dy / CYCLE_SIZE; } else { if (dy < 0) { myYCycles = -CYCLE_SIZE / dy + 1; myDy = -1 + dy / CYCLE_SIZE; } } if (myTimer != null) { return; } myTimer = UIUtil.createNamedTimer("Editor scroll timer", TIMER_PERIOD, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { myCommandProcessor.executeCommand(myProject, new DocumentRunnable(myDocument, myProject) { @Override public void run() { // We experienced situation when particular editor was disposed but the timer was still on. if (isDisposed()) { myTimer.stop(); return; } int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); VisualPosition caretPosition = myMultiSelectionInProgress ? myTargetMultiSelectionPosition : getCaretModel().getVisualPosition(); int column = caretPosition.column; xPassedCycles++; if (xPassedCycles >= myXCycles) { xPassedCycles = 0; column += myDx; } int line = caretPosition.line; yPassedCycles++; if (yPassedCycles >= myYCycles) { yPassedCycles = 0; line += myDy; } line = Math.max(0, line); column = Math.max(0, column); VisualPosition pos = new VisualPosition(line, column); if (!myMultiSelectionInProgress) { getCaretModel().moveToVisualPosition(pos); getScrollingModel().scrollToCaret(ScrollType.RELATIVE); } int newCaretOffset = getCaretModel().getOffset(); int caretShift = newCaretOffset - mySavedSelectionStart; if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { if (caretShift < 0) { int newSelection = newCaretOffset; if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { newSelection = myCaretModel.getWordAtCaretStart(); } else { if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { newSelection = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line, 0))); } } if (newSelection < 0) newSelection = newCaretOffset; mySelectionModel.setSelection(validateOffset(mySavedSelectionEnd), newSelection); getCaretModel().moveToOffset(newSelection); } else { int newSelection = newCaretOffset; if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) { newSelection = myCaretModel.getWordAtCaretEnd(); } else { if (getMouseSelectionState() == MOUSE_SELECTION_STATE_LINE_SELECTED) { newSelection = logicalPositionToOffset( visualToLogicalPosition(new VisualPosition(getCaretModel().getVisualPosition().line + 1, 0))); } } if (newSelection < 0) newSelection = newCaretOffset; mySelectionModel.setSelection(validateOffset(mySavedSelectionStart), newSelection); getCaretModel().moveToOffset(newSelection); } return; } if (myMultiSelectionInProgress && myLastMousePressedLocation != null) { myTargetMultiSelectionPosition = pos; LogicalPosition newLogicalPosition = visualToLogicalPosition(pos); getScrollingModel().scrollTo(newLogicalPosition, ScrollType.RELATIVE); mySelectionModel.setBlockSelection(myLastMousePressedLocation, newLogicalPosition); } else if (mySelectionModel.hasBlockSelection()) { mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(), getCaretModel().getLogicalPosition()); } else { mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset()); } } }, EditorBundle.message("move.cursor.command.name"), DocCommandGroupId.noneGroupId(getDocument()), UndoConfirmationPolicy.DEFAULT, getDocument()); } }); myTimer.start(); } private void stop() { if (myTimer != null) { myTimer.stop(); myTimer = null; } } private int validateOffset(int offset) { if (offset < 0) return 0; if (offset > myDocument.getTextLength()) return myDocument.getTextLength(); return offset; } } private static final Field decrButtonField = ReflectionUtil.getDeclaredField(BasicScrollBarUI.class, "decrButton"); private static final Field incrButtonField = ReflectionUtil.getDeclaredField(BasicScrollBarUI.class, "incrButton"); class MyScrollBar extends JBScrollBar implements IdeGlassPane.TopComponent { @NonNls private static final String APPLE_LAF_AQUA_SCROLL_BAR_UI_CLASS = "apple.laf.AquaScrollBarUI"; private ScrollBarUI myPersistentUI; @Nullable private RepaintCallback myRepaintCallback; private MyScrollBar(@JdkConstants.AdjustableOrientation int orientation) { super(orientation); } void setPersistentUI(ScrollBarUI ui) { myPersistentUI = ui; setUI(ui); } @Override public boolean canBePreprocessed(MouseEvent e) { return JBScrollPane.canBePreprocessed(e, this); } @Override public void setUI(ScrollBarUI ui) { if (myPersistentUI == null) myPersistentUI = ui; super.setUI(myPersistentUI); } @Override public void paint(Graphics g) { super.paint(g); if (myRepaintCallback != null) { myRepaintCallback.call(g); } } /** * This is helper method. It returns height of the top (decrease) scroll bar * button. Please note, that it's possible to return real height only if scroll bar * is instance of BasicScrollBarUI. Otherwise it returns fake (but good enough :) ) * value. */ int getDecScrollButtonHeight() { ScrollBarUI barUI = getUI(); Insets insets = getInsets(); if (barUI instanceof ButtonlessScrollBarUI) { return insets.top + ((ButtonlessScrollBarUI)barUI).getDecrementButtonHeight(); } else if (barUI instanceof BasicScrollBarUI) { try { JButton decrButtonValue = (JButton)decrButtonField.get(barUI); LOG.assertTrue(decrButtonValue != null); return insets.top + decrButtonValue.getHeight(); } catch (Exception exc) { throw new IllegalStateException(exc); } } else { return insets.top + 15; } } /** * This is helper method. It returns height of the bottom (increase) scroll bar * button. Please note, that it's possible to return real height only if scroll bar * is instance of BasicScrollBarUI. Otherwise it returns fake (but good enough :) ) * value. */ int getIncScrollButtonHeight() { ScrollBarUI barUI = getUI(); Insets insets = getInsets(); if (barUI instanceof ButtonlessScrollBarUI) { return insets.top + ((ButtonlessScrollBarUI)barUI).getIncrementButtonHeight(); } if (barUI instanceof BasicScrollBarUI) { try { JButton incrButtonValue = (JButton)incrButtonField.get(barUI); LOG.assertTrue(incrButtonValue != null); return insets.bottom + incrButtonValue.getHeight(); } catch (Exception exc) { throw new IllegalStateException(exc); } } if (APPLE_LAF_AQUA_SCROLL_BAR_UI_CLASS.equals(barUI.getClass().getName())) { return insets.bottom + 30; } return insets.bottom + 15; } @Override public int getUnitIncrement(int direction) { JViewport vp = myScrollPane.getViewport(); Rectangle vr = vp.getViewRect(); return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction); } @Override public int getBlockIncrement(int direction) { JViewport vp = myScrollPane.getViewport(); Rectangle vr = vp.getViewRect(); return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction); } public void registerRepaintCallback(@Nullable RepaintCallback callback) { myRepaintCallback = callback; } } private MyEditable getViewer() { if (myEditable == null) { myEditable = new MyEditable(); } return myEditable; } @Override public CopyProvider getCopyProvider() { return getViewer(); } @Override public CutProvider getCutProvider() { return getViewer(); } @Override public PasteProvider getPasteProvider() { return getViewer(); } @Override public DeleteProvider getDeleteProvider() { return getViewer(); } private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider { @Override public void performCopy(@NotNull DataContext dataContext) { executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext); } @Override public boolean isCopyEnabled(@NotNull DataContext dataContext) { return true; } @Override public boolean isCopyVisible(@NotNull DataContext dataContext) { return getSelectionModel().hasSelection(true) || getSelectionModel().hasBlockSelection(); } @Override public void performCut(@NotNull DataContext dataContext) { executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext); } @Override public boolean isCutEnabled(@NotNull DataContext dataContext) { return !isViewer(); } @Override public boolean isCutVisible(@NotNull DataContext dataContext) { if (!isCutEnabled(dataContext)) return false; return getSelectionModel().hasSelection(true) || getSelectionModel().hasBlockSelection(); } @Override public void performPaste(@NotNull DataContext dataContext) { executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext); } @Override public boolean isPastePossible(@NotNull DataContext dataContext) { // Copy of isPasteEnabled. See interface method javadoc. return !isViewer(); } @Override public boolean isPasteEnabled(@NotNull DataContext dataContext) { return !isViewer(); } @Override public void deleteElement(@NotNull DataContext dataContext) { executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext); } @Override public boolean canDeleteElement(@NotNull DataContext dataContext) { return !isViewer(); } private void executeAction(@NotNull String actionId, @NotNull DataContext dataContext) { EditorAction action = (EditorAction)ActionManager.getInstance().getAction(actionId); if (action != null) { action.actionPerformed(EditorImpl.this, dataContext); } } } @Override public void setColorsScheme(@NotNull EditorColorsScheme scheme) { assertIsDispatchThread(); myScheme = scheme; reinitSettings(); } @Override @NotNull public EditorColorsScheme getColorsScheme() { return myScheme; } void assertIsDispatchThread() { ApplicationManager.getApplication().assertIsDispatchThread(); } private static void assertReadAccess() { ApplicationManager.getApplication().assertReadAccessAllowed(); } @Override public void setVerticalScrollbarOrientation(int type) { assertIsDispatchThread(); int currentHorOffset = myScrollingModel.getHorizontalScrollOffset(); myScrollBarOrientation = type; if (type == VERTICAL_SCROLLBAR_LEFT) { myScrollPane.setLayout(new LeftHandScrollbarLayout()); } else { myScrollPane.setLayout(new ScrollPaneLayout()); } myScrollingModel.scrollHorizontally(currentHorOffset); } @Override public void setVerticalScrollbarVisible(boolean b) { myScrollPane .setVerticalScrollBarPolicy(b ? ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS : ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); } @Override public void setHorizontalScrollbarVisible(boolean b) { myScrollPane.setHorizontalScrollBarPolicy( b ? ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED : ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); } @Override public int getVerticalScrollbarOrientation() { return myScrollBarOrientation; } public boolean isMirrored() { return myScrollBarOrientation != EditorEx.VERTICAL_SCROLLBAR_RIGHT; } @NotNull MyScrollBar getVerticalScrollBar() { return myVerticalScrollBar; } private int getMouseSelectionState() { return myMouseSelectionState; } private void setMouseSelectionState(int mouseSelectionState) { if (getMouseSelectionState() == mouseSelectionState) return; myMouseSelectionState = mouseSelectionState; myMouseSelectionChangeTimestamp = System.currentTimeMillis(); myMouseSelectionStateAlarm.cancelAllRequests(); if (myMouseSelectionState != MOUSE_SELECTION_STATE_NONE) { if (myMouseSelectionStateResetRunnable == null) { myMouseSelectionStateResetRunnable = new Runnable() { @Override public void run() { resetMouseSelectionState(null); } }; } myMouseSelectionStateAlarm.addRequest(myMouseSelectionStateResetRunnable, Registry.intValue("editor.mouseSelectionStateResetTimeout"), ModalityState.stateForComponent(myEditorComponent)); } } private void resetMouseSelectionState(@Nullable MouseEvent event) { setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); MouseEvent e = event != null ? event : myMouseMovedEvent; if (e != null) { validateMousePointer(e); } } private void cancelAutoResetForMouseSelectionState() { myMouseSelectionStateAlarm.cancelAllRequests(); } void replaceInputMethodText(@NotNull InputMethodEvent e) { getInputMethodRequests(); myInputMethodRequestsHandler.replaceInputMethodText(e); } void inputMethodCaretPositionChanged(@NotNull InputMethodEvent e) { getInputMethodRequests(); myInputMethodRequestsHandler.setInputMethodCaretPosition(e); } @NotNull InputMethodRequests getInputMethodRequests() { if (myInputMethodRequestsHandler == null) { myInputMethodRequestsHandler = new MyInputMethodHandler(); myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler); } return myInputMethodRequestsSwingWrapper; } @Override public boolean processKeyTyped(@NotNull KeyEvent e) { if (e.getID() != KeyEvent.KEY_TYPED) return false; char c = e.getKeyChar(); if (UIUtil.isReallyTypedEvent(e)) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction processKeyTyped(c); return true; } else { return false; } } void beforeModalityStateChanged() { myScrollingModel.beforeModalityStateChanged(); } public EditorDropHandler getDropHandler() { return myDropHandler; } public void setDropHandler(@NotNull EditorDropHandler dropHandler) { myDropHandler = dropHandler; } private static class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests { private final InputMethodRequests myDelegate; private MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) { myDelegate = delegate; } @Override public Rectangle getTextLocation(final TextHitInfo offset) { return execute(new Computable() { @Override public Rectangle compute() { return myDelegate.getTextLocation(offset); } }); } @Override public TextHitInfo getLocationOffset(final int x, final int y) { return execute(new Computable() { @Override public TextHitInfo compute() { return myDelegate.getLocationOffset(x, y); } }); } @Override public int getInsertPositionOffset() { return execute(new Computable() { @Override public Integer compute() { return myDelegate.getInsertPositionOffset(); } }); } @Override public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex, final AttributedCharacterIterator.Attribute[] attributes) { return execute(new Computable() { @Override public AttributedCharacterIterator compute() { return myDelegate.getCommittedText(beginIndex, endIndex, attributes); } }); } @Override public int getCommittedTextLength() { return execute(new Computable() { @Override public Integer compute() { return myDelegate.getCommittedTextLength(); } }); } @Override @Nullable public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { return null; } @Override public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) { return execute(new Computable() { @Override public AttributedCharacterIterator compute() { return myDelegate.getSelectedText(attributes); } }); } private static T execute(final Computable computable) { if (ApplicationManager.getApplication().isDispatchThread()) { return computable.compute(); } else { final Ref ref = Ref.create(); try { GuiUtils.invokeAndWait(new Runnable() { @Override public void run() { ref.set(computable.compute()); } }); } catch (InterruptedException e) { LOG.error(e); } catch (InvocationTargetException e) { LOG.error(e); } return ref.get(); } } } private class MyInputMethodHandler implements InputMethodRequests { private String composedText; private ProperTextRange composedTextRange; @NotNull @Override public Rectangle getTextLocation(TextHitInfo offset) { Point caret = logicalPositionToXY(getCaretModel().getLogicalPosition()); Rectangle r = new Rectangle(caret, new Dimension(1, getLineHeight())); Point p = getContentComponent().getLocationOnScreen(); r.translate(p.x, p.y); return r; } @Override @Nullable public TextHitInfo getLocationOffset(int x, int y) { if (composedText != null) { Point p = getContentComponent().getLocationOnScreen(); p.x = x - p.x; p.y = y - p.y; int pos = logicalPositionToOffset(xyToLogicalPosition(p)); if (composedTextRange.containsOffset(pos)) { return TextHitInfo.leading(pos - composedTextRange.getStartOffset()); } } return null; } @Override public int getInsertPositionOffset() { int composedStartIndex = 0; int composedEndIndex = 0; if (composedText != null) { composedStartIndex = composedTextRange.getStartOffset(); composedEndIndex = composedTextRange.getEndOffset(); } int caretIndex = getCaretModel().getOffset(); if (caretIndex < composedStartIndex) { return caretIndex; } if (caretIndex < composedEndIndex) { return composedStartIndex; } return caretIndex - (composedEndIndex - composedStartIndex); } private String getText(int startIdx, int endIdx) { if (startIdx >= 0 && endIdx > startIdx) { CharSequence chars = getDocument().getImmutableCharSequence(); return chars.subSequence(startIdx, endIdx).toString(); } return ""; } @Override public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) { int composedStartIndex = 0; int composedEndIndex = 0; if (composedText != null) { composedStartIndex = composedTextRange.getStartOffset(); composedEndIndex = composedTextRange.getEndOffset(); } String committed; if (beginIndex < composedStartIndex) { if (endIndex <= composedStartIndex) { committed = getText(beginIndex, endIndex - beginIndex); } else { int firstPartLength = composedStartIndex - beginIndex; committed = getText(beginIndex, firstPartLength) + getText(composedEndIndex, endIndex - beginIndex - firstPartLength); } } else { committed = getText(beginIndex + composedEndIndex - composedStartIndex, endIndex - beginIndex); } return new AttributedString(committed).getIterator(); } @Override public int getCommittedTextLength() { int length = getDocument().getTextLength(); if (composedText != null) { length -= composedText.length(); } return length; } @Override @Nullable public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) { return null; } @Override @Nullable public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) { String text = getSelectionModel().getSelectedText(); return text == null ? null : new AttributedString(text).getIterator(); } private void createComposedString(int composedIndex, @NotNull AttributedCharacterIterator text) { StringBuffer strBuf = new StringBuffer(); // create attributed string with no attributes for (char c = text.setIndex(composedIndex); c != CharacterIterator.DONE; c = text.next()) { strBuf.append(c); } composedText = new String(strBuf); } private void setInputMethodCaretPosition(@NotNull InputMethodEvent e) { if (composedText != null) { int dot = composedTextRange.getStartOffset(); TextHitInfo caretPos = e.getCaret(); if (caretPos != null) { dot += caretPos.getInsertionIndex(); } getCaretModel().moveToOffset(dot); getScrollingModel().scrollToCaret(ScrollType.RELATIVE); } } private void runUndoTransparent(@NotNull final Runnable runnable) { CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() { @Override public void run() { CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { @Override public void run() { ApplicationManager.getApplication().runWriteAction(runnable); } }, "", getDocument(), UndoConfirmationPolicy.DEFAULT, getDocument()); } }); } private void replaceInputMethodText(@NotNull InputMethodEvent e) { int commitCount = e.getCommittedCharacterCount(); AttributedCharacterIterator text = e.getText(); // old composed text deletion final Document doc = getDocument(); if (composedText != null) { if (!isViewer() && doc.isWritable()) { runUndoTransparent(new Runnable() { @Override public void run() { int docLength = doc.getTextLength(); ProperTextRange range = composedTextRange.intersection(new TextRange(0, docLength)); if (range != null) { doc.deleteString(range.getStartOffset(), range.getEndOffset()); } } }); } composedText = null; } if (text != null) { text.first(); // committed text insertion if (commitCount > 0) { //noinspection ForLoopThatDoesntUseLoopVariable for (char c = text.current(); commitCount > 0; c = text.next(), commitCount--) { if (c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction processKeyTyped(c); } } } // new composed text insertion if (!isViewer() && doc.isWritable()) { int composedTextIndex = text.getIndex(); if (composedTextIndex < text.getEndIndex()) { createComposedString(composedTextIndex, text); runUndoTransparent(new Runnable() { @Override public void run() { EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false); } }); composedTextRange = ProperTextRange.from(getCaretModel().getOffset(), composedText.length()); } } } } } private class MyMouseAdapter extends MouseAdapter { private boolean mySelectionTweaked; @Override public void mousePressed(@NotNull MouseEvent e) { requestFocus(); runMousePressedCommand(e); } @Override public void mouseReleased(@NotNull MouseEvent e) { myMousePressArea = null; runMouseReleasedCommand(e); if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() && Math.abs(e.getX() - myMousePressedEvent.getX()) < EditorUtil.getSpaceWidth(Font.PLAIN, EditorImpl.this) && Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) { runMouseClickedCommand(e); } } @Override public void mouseEntered(@NotNull MouseEvent e) { runMouseEnteredCommand(e); } @Override public void mouseExited(@NotNull MouseEvent e) { runMouseExitedCommand(e); EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { myGutterComponent.mouseExited(e); } TooltipController.getInstance().cancelTooltip(FOLDING_TOOLTIP_GROUP, e, true); } private void runMousePressedCommand(@NotNull final MouseEvent e) { myLastMousePressedLocation = xyToLogicalPosition(e.getPoint()); myCurrentDragIsSubstantial = false; final int clickOffset = logicalPositionToOffset(myLastMousePressedLocation); putUserData(EditorActionUtil.EXPECTED_CARET_OFFSET, clickOffset); mySelectionTweaked = false; myMousePressedEvent = e; EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); for (EditorMouseListener mouseListener : myMouseListeners) { mouseListener.mousePressed(event); } putUserData(EditorActionUtil.EXPECTED_CARET_OFFSET, null); if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { myDragOnGutterSelectionStartLine = EditorUtil.yPositionToLogicalLine(EditorImpl.this, e); } // On some systems (for example on Linux) popup trigger is MOUSE_PRESSED event. // But this trigger is always consumed by popup handler. In that case we have to // also move caret. if (event.isConsumed() && !(event.getMouseEvent().isPopupTrigger() || event.getArea() == EditorMouseEventArea.EDITING_AREA)) { return; } if (myCommandProcessor != null) { Runnable runnable = new Runnable() { @Override public void run() { if (processMousePressed(e) && myProject != null && !myProject.isDefault()) { IdeDocumentHistory.getInstance(myProject).includeCurrentCommandAsNavigation(); } } }; myCommandProcessor .executeCommand(myProject, runnable, "", DocCommandGroupId.noneGroupId(getDocument()), UndoConfirmationPolicy.DEFAULT, getDocument()); } else { processMousePressed(e); } } private void runMouseClickedCommand(@NotNull final MouseEvent e) { EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); for (EditorMouseListener listener : myMouseListeners) { listener.mouseClicked(event); if (event.isConsumed()) { e.consume(); return; } } } private void runMouseReleasedCommand(@NotNull final MouseEvent e) { myMultiSelectionInProgress = false; myDragOnGutterSelectionStartLine = -1; if (!mySelectionTweaked) { tweakSelectionIfNecessary(e); } if (e.isConsumed()) { return; } myScrollingTimer.stop(); EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); for (EditorMouseListener listener : myMouseListeners) { listener.mouseReleased(event); if (event.isConsumed()) { e.consume(); return; } } if (myCommandProcessor != null) { Runnable runnable = new Runnable() { @Override public void run() { processMouseReleased(e); } }; myCommandProcessor .executeCommand(myProject, runnable, "", DocCommandGroupId.noneGroupId(getDocument()), UndoConfirmationPolicy.DEFAULT, getDocument()); } else { processMouseReleased(e); } } private void runMouseEnteredCommand(@NotNull MouseEvent e) { EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); for (EditorMouseListener listener : myMouseListeners) { listener.mouseEntered(event); if (event.isConsumed()) { e.consume(); return; } } } private void runMouseExitedCommand(@NotNull MouseEvent e) { EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); for (EditorMouseListener listener : myMouseListeners) { listener.mouseExited(event); if (event.isConsumed()) { e.consume(); return; } } } private boolean processMousePressed(@NotNull MouseEvent e) { myInitialMouseEvent = e; if (myMouseSelectionState != MOUSE_SELECTION_STATE_NONE && System.currentTimeMillis() - myMouseSelectionChangeTimestamp > Registry.intValue( "editor.mouseSelectionStateResetTimeout")) { resetMouseSelectionState(e); } int x = e.getX(); int y = e.getY(); if (x < 0) x = 0; if (y < 0) y = 0; final EditorMouseEventArea eventArea = getMouseEventArea(e); myMousePressArea = eventArea; boolean isNavigation = false; if (eventArea == EditorMouseEventArea.FOLDING_OUTLINE_AREA) { final FoldRegion range = myGutterComponent.findFoldingAnchorAt(x, y); if (range != null) { final boolean expansion = !range.isExpanded(); int scrollShift = y - getScrollingModel().getVerticalScrollOffset(); Runnable processor = new Runnable() { @Override public void run() { myFoldingModel.flushCaretShift(); range.setExpanded(expansion); } }; getFoldingModel().runBatchFoldingOperation(processor); y = myGutterComponent.getHeadCenterY(range); getScrollingModel().scrollVertically(y - scrollShift); myGutterComponent.updateSize(); validateMousePointer(e); e.consume(); return isNavigation; } } if (e.getSource() == myGutterComponent) { if (eventArea == EditorMouseEventArea.LINE_MARKERS_AREA || eventArea == EditorMouseEventArea.ANNOTATIONS_AREA || eventArea == EditorMouseEventArea.LINE_NUMBERS_AREA) { if (tweakSelectionIfNecessary(e)) { mySelectionTweaked = true; } else { myGutterComponent.mousePressed(e); } if (e.isConsumed()) return isNavigation; } x = 0; } int oldSelectionStart = mySelectionModel.getLeadSelectionOffset(); final int oldStart = mySelectionModel.getSelectionStart(); final int oldEnd = mySelectionModel.getSelectionEnd(); boolean toggleCaret = myCaretModel.supportsMultipleCarets() && e.getSource() != myGutterComponent && isToggleCaretEvent(e); boolean lastPressCreatedCaret = myLastPressCreatedCaret; if (e.getClickCount() == 1) { myLastPressCreatedCaret = false; } // Don't move caret on mouse press above gutter line markers area (a place where break points, 'override', 'implements' etc icons // are drawn) and annotations area. E.g. we don't want to change caret position if a user sets new break point (clicks // at 'line markers' area). if (e.getSource() != myGutterComponent || (eventArea != EditorMouseEventArea.LINE_MARKERS_AREA && eventArea != EditorMouseEventArea.ANNOTATIONS_AREA)) { LogicalPosition pos = getLogicalPositionForScreenPos(x, y, true); if (toggleCaret) { VisualPosition visualPosition = logicalToVisualPosition(pos); Caret caret = getCaretModel().getCaretAt(visualPosition); if (e.getClickCount() == 1) { if (caret == null) { myLastPressCreatedCaret = getCaretModel().addCaret(visualPosition) != null; } else { getCaretModel().removeCaret(caret); } } else if (e.getClickCount() == 3 && lastPressCreatedCaret) { getCaretModel().moveToLogicalPosition(pos); } } else { getCaretModel().removeSecondaryCarets(); getCaretModel().moveToLogicalPosition(pos); } } if (e.isPopupTrigger()) return isNavigation; requestFocus(); int caretOffset = getCaretModel().getOffset(); int newStart = mySelectionModel.getSelectionStart(); int newEnd = mySelectionModel.getSelectionEnd(); if (oldStart != newStart && oldEnd != newEnd) { if (oldStart == oldEnd && newStart == newEnd) { isNavigation = true; } } myMouseSelectedRegion = myFoldingModel.getFoldingPlaceholderAt(new Point(x, y)); myMousePressedInsideSelection = mySelectionModel.hasSelection() && caretOffset >= mySelectionModel.getSelectionStart() && caretOffset <= mySelectionModel.getSelectionEnd(); if (!myMousePressedInsideSelection && mySelectionModel.hasBlockSelection()) { int[] starts = mySelectionModel.getBlockSelectionStarts(); int[] ends = mySelectionModel.getBlockSelectionEnds(); for (int i = 0; i < starts.length; i++) { if (caretOffset >= starts[i] && caretOffset < ends[i]) { myMousePressedInsideSelection = true; break; } } } if (getMouseEventArea(e) == EditorMouseEventArea.LINE_NUMBERS_AREA && e.getClickCount() == 1) { mySelectionModel.selectLineAtCaret(); setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); mySavedSelectionStart = mySelectionModel.getSelectionStart(); mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); return isNavigation; } if (e.isShiftDown() && !e.isControlDown() && !e.isAltDown()) { if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { if (caretOffset < mySavedSelectionStart) { mySelectionModel.setSelection(mySavedSelectionEnd, caretOffset); } else { mySelectionModel.setSelection(mySavedSelectionStart, caretOffset); } } else { int startToUse = oldSelectionStart; if (mySelectionModel.isUnknownDirection() && caretOffset > startToUse) { startToUse = Math.min(oldStart, oldEnd); } mySelectionModel.setSelection(startToUse, caretOffset); } } else { if (!myMousePressedInsideSelection && (getSelectionModel().hasSelection() || getSelectionModel().hasBlockSelection())) { setMouseSelectionState(MOUSE_SELECTION_STATE_NONE); mySelectionModel.setSelection(caretOffset, caretOffset); } else { if (!e.isPopupTrigger() && (eventArea == EditorMouseEventArea.EDITING_AREA || eventArea == EditorMouseEventArea.LINE_NUMBERS_AREA) && (!toggleCaret || lastPressCreatedCaret)) { switch (e.getClickCount()) { case 2: selectWordAtCaret(mySettings.isMouseClickSelectionHonorsCamelWords() && mySettings.isCamelWords()); break; case 3: if (HONOR_CAMEL_HUMPS_ON_TRIPLE_CLICK && mySettings.isCamelWords()) { // We want to differentiate between triple and quadruple clicks when 'select by camel humps' is on. The former // is assumed to select 'hump' while the later points to the whole word. selectWordAtCaret(false); break; } //noinspection fallthrough case 4: mySelectionModel.selectLineAtCaret(); setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED); mySavedSelectionStart = mySelectionModel.getSelectionStart(); mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); mySelectionModel.setUnknownDirection(true); break; } } } } return isNavigation; } } private static boolean isToggleCaretEvent(MouseEvent e) { KeymapManager keymapManager = KeymapManager.getInstance(); if (keymapManager == null) { return false; } Keymap keymap = keymapManager.getActiveKeymap(); if (keymap == null) { return false; } String[] actionIds = keymap.getActionIds(new MouseShortcut(e.getButton(), e.getModifiersEx(), 1)); if (actionIds == null) { return false; } for (String actionId : actionIds) { if (IdeActions.ACTION_EDITOR_ADD_OR_REMOVE_CARET.equals(actionId)) { return true; } } return false; } private void selectWordAtCaret(boolean honorCamelCase) { mySelectionModel.selectWordAtCaret(honorCamelCase); setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED); mySavedSelectionStart = mySelectionModel.getSelectionStart(); mySavedSelectionEnd = mySelectionModel.getSelectionEnd(); getCaretModel().moveToOffset(mySavedSelectionEnd); } /** * Allows to answer if given event should tweak editor selection. * * @param e event for occurred mouse action * @return true if action that produces given event will trigger editor selection change; false otherwise */ private boolean tweakSelectionEvent(@NotNull MouseEvent e) { return getSelectionModel().hasSelection() && e.getButton() == MouseEvent.BUTTON1 && e.isShiftDown() && getMouseEventArea(e) == EditorMouseEventArea.LINE_NUMBERS_AREA; } /** * Checks if editor selection should be changed because of click at the given point at gutter and proceeds if necessary. *

* The main idea is that selection can be changed during left mouse clicks on the gutter line numbers area with hold * Shift button. The selection should be adjusted if necessary. * * @param e event for mouse click on gutter area * @return true if editor's selection is changed because of the click; false otherwise */ private boolean tweakSelectionIfNecessary(@NotNull MouseEvent e) { if (!tweakSelectionEvent(e)) { return false; } int startSelectionOffset = getSelectionModel().getSelectionStart(); int startVisLine = offsetToVisualLine(startSelectionOffset); int endSelectionOffset = getSelectionModel().getSelectionEnd(); int endVisLine = offsetToVisualLine(endSelectionOffset - 1); int clickVisLine = yPositionToVisibleLine(e.getPoint().y); if (clickVisLine < startVisLine) { // Expand selection at backward direction. int startOffset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(clickVisLine, 0))); getSelectionModel().setSelection(startOffset, endSelectionOffset); getCaretModel().moveToOffset(startOffset); } else if (clickVisLine > endVisLine) { // Expand selection at forward direction. int endLineOffset = EditorUtil.getVisualLineEndOffset(this, clickVisLine); getSelectionModel().setSelection(getSelectionModel().getSelectionStart(), endLineOffset); getCaretModel().moveToOffset(endLineOffset, true); } else if (startVisLine == endVisLine) { // Remove selection getSelectionModel().removeSelection(); } else { // Reduce selection in backward direction. if (getSelectionModel().getLeadSelectionOffset() == endSelectionOffset) { if (clickVisLine == startVisLine) { clickVisLine++; } int startOffset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(clickVisLine, 0))); getSelectionModel().setSelection(startOffset, endSelectionOffset); getCaretModel().moveToOffset(startOffset); } else { // Reduce selection is forward direction. if (clickVisLine == endVisLine) { clickVisLine--; } int endLineOffset = EditorUtil.getVisualLineEndOffset(this, clickVisLine); getSelectionModel().setSelection(startSelectionOffset, endLineOffset); getCaretModel().moveToOffset(endLineOffset); } } e.consume(); return true; } private static final TooltipGroup FOLDING_TOOLTIP_GROUP = new TooltipGroup("FOLDING_TOOLTIP_GROUP", 10); private class MyMouseMotionListener implements MouseMotionListener { @Override public void mouseDragged(@NotNull MouseEvent e) { validateMousePointer(e); runMouseDraggedCommand(e); EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) { myGutterComponent.mouseDragged(e); } for (EditorMouseMotionListener listener : myMouseMotionListeners) { listener.mouseDragged(event); } } @Override public void mouseMoved(@NotNull MouseEvent e) { if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) { if (myMousePressedEvent != null && myMousePressedEvent.getComponent() == e.getComponent()) { Point lastPoint = myMousePressedEvent.getPoint(); Point point = e.getPoint(); int deadZone = Registry.intValue("editor.mouseSelectionStateResetDeadZone"); if (Math.abs(lastPoint.x - point.x) >= deadZone || Math.abs(lastPoint.y - point.y) >= deadZone) { resetMouseSelectionState(e); } } else { validateMousePointer(e); } } else { validateMousePointer(e); } myMouseMovedEvent = e; EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e)); if (e.getSource() == myGutterComponent) { myGutterComponent.mouseMoved(e); } if (event.getArea() == EditorMouseEventArea.EDITING_AREA) { FoldRegion fold = myFoldingModel.getFoldingPlaceholderAt(e.getPoint()); TooltipController controller = TooltipController.getInstance(); if (fold != null && !fold.shouldNeverExpand()) { DocumentFragment range = createDocumentFragment(fold); final Point p = SwingUtilities.convertPoint((Component)e.getSource(), e.getPoint(), getComponent().getRootPane().getLayeredPane()); controller.showTooltip(EditorImpl.this, p, new DocumentFragmentTooltipRenderer(range), false, FOLDING_TOOLTIP_GROUP); } else { controller.cancelTooltip(FOLDING_TOOLTIP_GROUP, e, true); } } for (EditorMouseMotionListener listener : myMouseMotionListeners) { listener.mouseMoved(event); } } @NotNull private DocumentFragment createDocumentFragment(@NotNull FoldRegion fold) { final FoldingGroup group = fold.getGroup(); final int foldStart = fold.getStartOffset(); if (group != null) { final int endOffset = myFoldingModel.getEndOffset(group); if (offsetToVisualLine(endOffset) == offsetToVisualLine(foldStart)) { return new DocumentFragment(myDocument, foldStart, endOffset); } } final int oldEnd = fold.getEndOffset(); return new DocumentFragment(myDocument, foldStart, oldEnd); } } private class MyColorSchemeDelegate extends DelegateColorScheme { private final FontPreferences myFontPreferences = new FontPreferences(); private final Map myOwnAttributes = ContainerUtilRt.newHashMap(); private final Map myOwnColors = ContainerUtilRt.newHashMap(); private final EditorColorsScheme myCustomGlobalScheme; private Map myFontsMap = null; private int myMaxFontSize = OptionsConstants.MAX_EDITOR_FONT_SIZE; private int myFontSize = -1; private String myFaceName = null; private MyColorSchemeDelegate(@Nullable EditorColorsScheme globalScheme) { super(globalScheme == null ? EditorColorsManager.getInstance().getGlobalScheme() : globalScheme); myCustomGlobalScheme = globalScheme; updateGlobalScheme(); } private EditorColorsScheme getGlobal() { return getDelegate(); } protected void initFonts() { String editorFontName = getEditorFontName(); int editorFontSize = getEditorFontSize(); myFontPreferences.clear(); myFontPreferences.register(editorFontName, editorFontSize); myFontsMap = new EnumMap(EditorFontType.class); Font plainFont = new Font(editorFontName, Font.PLAIN, editorFontSize); Font boldFont = new Font(editorFontName, Font.BOLD, editorFontSize); Font italicFont = new Font(editorFontName, Font.ITALIC, editorFontSize); Font boldItalicFont = new Font(editorFontName, Font.BOLD | Font.ITALIC, editorFontSize); myFontsMap.put(EditorFontType.PLAIN, plainFont); myFontsMap.put(EditorFontType.BOLD, boldFont); myFontsMap.put(EditorFontType.ITALIC, italicFont); myFontsMap.put(EditorFontType.BOLD_ITALIC, boldItalicFont); reinitSettings(); } @Override public TextAttributes getAttributes(TextAttributesKey key) { if (myOwnAttributes.containsKey(key)) return myOwnAttributes.get(key); return getGlobal().getAttributes(key); } @Override public void setAttributes(TextAttributesKey key, TextAttributes attributes) { myOwnAttributes.put(key, attributes); } @Override public Color getColor(ColorKey key) { if (myOwnColors.containsKey(key)) return myOwnColors.get(key); return getGlobal().getColor(key); } @Override public void setColor(ColorKey key, Color color) { myOwnColors.put(key, color); // These two are here because those attributes are cached and I do not whant the clients to call editor's reinit // settings in this case. myCaretModel.reinitSettings(); mySelectionModel.reinitSettings(); } @Override public int getEditorFontSize() { if (myFontSize == -1) { return getGlobal().getEditorFontSize(); } return myFontSize; } @Override public void setEditorFontSize(int fontSize) { if (fontSize < MIN_FONT_SIZE) fontSize = MIN_FONT_SIZE; if (fontSize > myMaxFontSize) fontSize = myMaxFontSize; myFontSize = fontSize; initFonts(); } @NotNull @Override public FontPreferences getFontPreferences() { return myFontPreferences.getEffectiveFontFamilies().isEmpty() ? getGlobal().getFontPreferences() : myFontPreferences; } @Override public void setFontPreferences(@NotNull FontPreferences preferences) { preferences.copyTo(myFontPreferences); initFonts(); } @Override public String getEditorFontName() { if (myFaceName == null) { return getGlobal().getEditorFontName(); } return myFaceName; } @Override public void setEditorFontName(String fontName) { myFaceName = fontName; initFonts(); } @Override public Font getFont(EditorFontType key) { if (myFontsMap != null) { Font font = myFontsMap.get(key); if (font != null) return font; } return getGlobal().getFont(key); } @Override public void setFont(EditorFontType key, Font font) { if (myFontsMap == null) { initFonts(); } myFontsMap.put(key, font); reinitSettings(); } @Override @Nullable public Object clone() { return null; } public void updateGlobalScheme() { setDelegate(myCustomGlobalScheme == null ? EditorColorsManager.getInstance().getGlobalScheme() : myCustomGlobalScheme); } @Override public void setDelegate(@NotNull EditorColorsScheme delegate) { super.setDelegate(delegate); int globalFontSize = getGlobal().getEditorFontSize(); myMaxFontSize = Math.max(OptionsConstants.MAX_EDITOR_FONT_SIZE, globalFontSize); } @Override public void setConsoleFontSize(int fontSize) { getGlobal().setConsoleFontSize(fontSize); reinitSettings(); } } private static class MyTransferHandler extends TransferHandler { private RangeMarker myDraggedRange = null; private static Editor getEditor(@NotNull JComponent comp) { EditorComponentImpl editorComponent = (EditorComponentImpl)comp; return editorComponent.getEditor(); } @Override public boolean importData(final JComponent comp, @NotNull final Transferable t) { final EditorImpl editor = (EditorImpl)getEditor(comp); final EditorDropHandler dropHandler = editor.getDropHandler(); if (dropHandler != null && dropHandler.canHandleDrop(t.getTransferDataFlavors())) { dropHandler.handleDrop(t, editor.getProject(), null); return true; } final int caretOffset = editor.getCaretModel().getOffset(); if (myDraggedRange != null && myDraggedRange.getStartOffset() <= caretOffset && caretOffset < myDraggedRange.getEndOffset()) { return false; } if (myDraggedRange != null) { editor.getCaretModel().moveToOffset(editor.mySavedCaretOffsetForDNDUndoHack); } CommandProcessor.getInstance().executeCommand(editor.myProject, new Runnable() { @Override public void run() { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { try { editor.getSelectionModel().removeSelection(); final int offset; if (myDraggedRange != null) { editor.getCaretModel().moveToOffset(caretOffset); offset = caretOffset; } else { offset = editor.getCaretModel().getOffset(); } if (editor.getDocument().getRangeGuard(offset, offset) != null) { return; } editor.putUserData(LAST_PASTED_REGION, null); EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_PASTE); LOG.assertTrue(pasteHandler instanceof EditorTextInsertHandler); ((EditorTextInsertHandler)pasteHandler).execute(editor, editor.getDataContext(), new Producer() { @Override public Transferable produce() { return t; } }); TextRange range = editor.getUserData(LAST_PASTED_REGION); if (range != null) { editor.getCaretModel().moveToOffset(range.getStartOffset()); editor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset()); } } catch (Exception exception) { LOG.error(exception); } } }); } }, EditorBundle.message("paste.command.name"), DND_COMMAND_KEY, UndoConfirmationPolicy.DEFAULT, editor.getDocument()); return true; } @Override public boolean canImport(JComponent comp, @NotNull DataFlavor[] transferFlavors) { Editor editor = getEditor(comp); final EditorDropHandler dropHandler = ((EditorImpl)editor).getDropHandler(); if (dropHandler != null && dropHandler.canHandleDrop(transferFlavors)) { return true; } if (editor.isViewer()) return false; int offset = editor.getCaretModel().getOffset(); if (editor.getDocument().getRangeGuard(offset, offset) != null) return false; for (DataFlavor transferFlavor : transferFlavors) { if (transferFlavor.equals(DataFlavor.stringFlavor)) return true; } return false; } @Override @Nullable protected Transferable createTransferable(JComponent c) { Editor editor = getEditor(c); String s = editor.getSelectionModel().getSelectedText(); if (s == null) return null; int selectionStart = editor.getSelectionModel().getSelectionStart(); int selectionEnd = editor.getSelectionModel().getSelectionEnd(); myDraggedRange = editor.getDocument().createRangeMarker(selectionStart, selectionEnd); return new StringSelection(s); } @Override public int getSourceActions(JComponent c) { return COPY_OR_MOVE; } @Override protected void exportDone(@NotNull final JComponent source, @Nullable Transferable data, int action) { if (data == null) return; final Component last = DnDManager.getInstance().getLastDropHandler(); if (last != null && !(last instanceof EditorComponentImpl)) return; final Editor editor = getEditor(source); if (action == MOVE && !editor.isViewer() && myDraggedRange != null) { if (!FileDocumentManager.getInstance().requestWriting(editor.getDocument(), editor.getProject())) { return; } CommandProcessor.getInstance().executeCommand(((EditorImpl)editor).myProject, new Runnable() { @Override public void run() { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { Document doc = editor.getDocument(); doc.startGuardedBlockChecking(); try { doc.deleteString(myDraggedRange.getStartOffset(), myDraggedRange.getEndOffset()); } catch (ReadOnlyFragmentModificationException e) { EditorActionManager.getInstance().getReadonlyFragmentModificationHandler(doc).handle(e); } finally { doc.stopGuardedBlockChecking(); } } }); } }, EditorBundle.message("move.selection.command.name"), DND_COMMAND_KEY, UndoConfirmationPolicy.DEFAULT, editor.getDocument()); } myDraggedRange = null; } } class EditorDocumentAdapter implements PrioritizedDocumentListener { @Override public void beforeDocumentChange(@NotNull DocumentEvent e) { beforeChangedUpdate(e); } @Override public void documentChanged(@NotNull DocumentEvent e) { changedUpdate(e); } @Override public int getPriority() { return EditorDocumentPriorities.EDITOR_DOCUMENT_ADAPTER; } } private class EditorDocumentBulkUpdateAdapter implements DocumentBulkUpdateListener { @Override public void updateStarted(@NotNull Document doc) { if (doc != getDocument()) return; bulkUpdateStarted(); } @Override public void updateFinished(@NotNull Document doc) { if (doc != getDocument()) return; bulkUpdateFinished(); } } @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) private class EditorSizeContainer { /** * Holds logical line widths in pixels. */ private TIntArrayList myLineWidths; private int maxCalculatedLine = -1; /** * Holds value that indicates if line widths recalculation should be performed. */ private volatile boolean myIsDirty; /** * Holds number of the last logical line affected by the last document change. */ private int myOldEndLine; private Dimension mySize; private int myMaxWidth = -1; public synchronized void reset() { int lineCount = getDocument().getLineCount(); myLineWidths = new TIntArrayList(lineCount + 300); insertNewLines(lineCount, 0); maxCalculatedLine = -1; myIsDirty = true; } private void insertNewLines(int lineCount, int index) { int[] values = new int[lineCount]; Arrays.fill(values, -1); myLineWidths.insert(index, values); if (index <= maxCalculatedLine) { maxCalculatedLine += lineCount; } } @SuppressWarnings({"NonPrivateFieldAccessedInSynchronizedContext"}) public synchronized void beforeChange(@NotNull DocumentEvent e) { if (myDocument.isInBulkUpdate()) { myMaxWidth = mySize == null ? -1 : mySize.width; } myOldEndLine = offsetToLogicalLine(e.getOffset() + e.getOldLength()); } /** * Notifies current size container about document content change. *

* Every change is assumed to be identified by three characteristics - start, ole end and new end lines. * Example: *

     * 
    *
  1. * Consider that we have the following document initially: *
         *        line 1
         *        line 2
         *        line 3
         *      
    *
  2. *
  3. * Let's assume that the user selected the last two lines and typed 'new line' (that effectively removed selected text). * Current document state: *
         *        line 1
         *        new line
         *      
    *
  4. *
  5. * Current method is expected to be called with the following parameters: *
      *
    • startLine is 1'
    • *
    • oldEndLine is 2'
    • *
    • newEndLine is 1'
    • *
    *
  6. *
*
* * @param startLine logical line that contains changed fragment start offset * @param newEndLine logical line that contains changed fragment end * @param oldEndLine logical line that contained changed fragment end */ public synchronized void update(int startLine, int newEndLine, int oldEndLine) { final int lineWidthSize = myLineWidths.size(); if (lineWidthSize == 0 || myDocument.getTextLength() <= 0) { reset(); } else { final int min = Math.min(oldEndLine, newEndLine); final boolean toAddNewLines = min >= lineWidthSize; if (toAddNewLines) { insertNewLines(min - lineWidthSize + 1, lineWidthSize); } for (int i = min; i > startLine - 1; i--) { myLineWidths.set(i, -1); if (maxCalculatedLine == i) maxCalculatedLine--; } if (newEndLine > oldEndLine) { insertNewLines(newEndLine - oldEndLine, oldEndLine + 1); } else if (oldEndLine > newEndLine && !toAddNewLines && newEndLine + 1 < lineWidthSize) { int length = Math.min(oldEndLine, lineWidthSize) - newEndLine - 1; int index = newEndLine + 1; myLineWidths.remove(index, length); if (index <= maxCalculatedLine) { maxCalculatedLine -= length; } } myIsDirty = true; } } /** * Notifies current container about visual width change of the target logical line. *

* Please note that there is a possible case that particular logical line is represented in more than one visual lines, * hence, this method may be called multiple times with the same logical line argument but different with values. Current * container is expected to store max of the given values then. * * @param logicalLine logical line which visual width is changed * @param widthInPixels visual width of the given logical line */ public synchronized void updateLineWidthIfNecessary(int logicalLine, int widthInPixels) { if (logicalLine < myLineWidths.size()) { int currentWidth = myLineWidths.get(logicalLine); if (widthInPixels > currentWidth) { myLineWidths.set(logicalLine, widthInPixels); } if (widthInPixels > myMaxWidth) { myMaxWidth = widthInPixels; } maxCalculatedLine = Math.max(maxCalculatedLine, logicalLine); } } public synchronized void changedUpdate(@NotNull DocumentEvent e) { int startLine = e.getOldLength() == 0 ? myOldEndLine : myDocument.getLineNumber(e.getOffset()); int newEndLine = e.getNewLength() == 0 ? startLine : myDocument.getLineNumber(e.getOffset() + e.getNewLength()); int oldEndLine = myOldEndLine; update(startLine, newEndLine, oldEndLine); } @SuppressWarnings({"NonPrivateFieldAccessedInSynchronizedContext", "AssignmentToForLoopParameter"}) private void validateSizes() { if (!myIsDirty) return; synchronized (this) { if (!myIsDirty) return; int lineCount = Math.min(myLineWidths.size(), myDocument.getLineCount()); if (myMaxWidth != -1 && myDocument.isInBulkUpdate()) { mySize = new Dimension(myMaxWidth, getLineHeight() * lineCount); myIsDirty = false; return; } final CharSequence text = myDocument.getImmutableCharSequence(); int documentLength = myDocument.getTextLength(); int x = 0; boolean lastLineLengthCalculated = false; List softWraps = getSoftWrapModel().getRegisteredSoftWraps(); int softWrapsIndex = -1; CharWidthCache charWidthCache = new CharWidthCache(EditorImpl.this); for (int line = 0; line < lineCount; line++) { if (myLineWidths.getQuick(line) != -1) continue; if (line == lineCount - 1) { lastLineLengthCalculated = true; } x = 0; int offset = myDocument.getLineStartOffset(line); if (offset >= myDocument.getTextLength()) { myLineWidths.set(line, 0); maxCalculatedLine = Math.max(maxCalculatedLine, line); break; } if (softWrapsIndex < 0) { softWrapsIndex = getSoftWrapModel().getSoftWrapIndex(offset); if (softWrapsIndex < 0) { softWrapsIndex = -softWrapsIndex - 1; } } int endLine; if (maxCalculatedLine < line + 1) { endLine = lineCount; } else { for (endLine = line + 1; endLine < maxCalculatedLine; endLine++) { if (myLineWidths.getQuick(endLine) != -1) { break; } } } int endOffset = endLine >= lineCount ? documentLength : myDocument.getLineEndOffset(endLine); for ( FoldRegion region = myFoldingModel.getCollapsedRegionAtOffset(endOffset); region != null && endOffset < myDocument.getTextLength(); region = myFoldingModel.getCollapsedRegionAtOffset(endOffset)) { final int lineNumber = myDocument.getLineNumber(region.getEndOffset()); endOffset = myDocument.getLineEndOffset(lineNumber); } if (endOffset > myDocument.getTextLength()) { break; } IterationState state = new IterationState(EditorImpl.this, offset, endOffset, false); int fontType = state.getMergedAttributes().getFontType(); int maxPreviousSoftWrappedWidth = -1; while (offset < documentLength && line < lineCount) { char c = text.charAt(offset); if (offset >= state.getEndOffset()) { state.advance(); fontType = state.getMergedAttributes().getFontType(); } while (softWrapsIndex < softWraps.size() && line < lineCount) { SoftWrap softWrap = softWraps.get(softWrapsIndex); if (softWrap.getStart() > offset) { break; } softWrapsIndex++; if (softWrap.getStart() == offset) { maxPreviousSoftWrappedWidth = Math.max(maxPreviousSoftWrappedWidth, x); x = softWrap.getIndentInPixels(); } } FoldRegion collapsed = state.getCurrentFold(); if (collapsed != null) { String placeholder = collapsed.getPlaceholderText(); for (int i = 0; i < placeholder.length(); i++) { x += charWidthCache.charWidth(placeholder.charAt(i), fontType); } offset = collapsed.getEndOffset(); line = myDocument.getLineNumber(offset); } else if (c == '\t') { x = EditorUtil.nextTabStop(x, EditorImpl.this); offset++; } else if (c == '\n') { int width = Math.max(x, maxPreviousSoftWrappedWidth); myLineWidths.set(line, width); maxCalculatedLine = Math.max(maxCalculatedLine, line); if (line + 1 >= lineCount || myLineWidths.getQuick(line + 1) != -1) break; offset++; x = 0; //noinspection AssignmentToForLoopParameter line++; if (line == lineCount - 1) { lastLineLengthCalculated = true; } } else { x += charWidthCache.charWidth(c, fontType); offset++; } } } if (lineCount > 0 && lastLineLengthCalculated) { myLineWidths.set(lineCount - 1, x); // Last line can be non-zero length and won't be caught by in-loop procedure since latter only react on \n's maxCalculatedLine = Math.max(maxCalculatedLine, lineCount - 1); } // There is a following possible situation: // 1. Big document is opened at editor; // 2. Soft wraps are calculated for the current visible area; // 2. The user scrolled down; // 3. The user significantly reduced visible area width (say, reduced it twice); // 4. Soft wraps are calculated for the current visible area; // We need to consider only the widths for the logical lines that are completely shown at the current visible area then. // I.e. we shouldn't use widths of the lines that are not shown for max width calculation because previous widths are calculated // for another visible area width. int startToUse = 0; int endToUse = Math.min(lineCount, myLineWidths.size()); if (endToUse > 0 && getSoftWrapModel().isSoftWrappingEnabled()) { Rectangle visibleArea = getScrollingModel().getVisibleArea(); startToUse = EditorUtil.yPositionToLogicalLine(EditorImpl.this, visibleArea.getLocation()); endToUse = Math.min(endToUse, EditorUtil.yPositionToLogicalLine(EditorImpl.this, visibleArea.y + visibleArea.height)); if (endToUse <= startToUse) { // There is a possible case that there is the only soft-wrapped line, i.e. end == start. We still want to update the // size container's width then. endToUse = Math.min(myLineWidths.size(), startToUse + 1); } } int maxWidth = 0; for (int i = startToUse; i < endToUse; i++) { maxWidth = Math.max(maxWidth, myLineWidths.getQuick(i)); } mySize = new Dimension(maxWidth, getLineHeight() * Math.max(getVisibleLineCount(), 1)); myIsDirty = false; } } @NotNull private Dimension getContentSize() { validateSizes(); return mySize; } private int getContentHeight() { return getVisibleLineCount() * getLineHeight(); } } @Override @NotNull public EditorGutter getGutter() { return getGutterComponentEx(); } @Override public int calcColumnNumber(@NotNull CharSequence text, int start, int offset, int tabSize) { IterationState state = new IterationState(this, start, offset, false); int fontType = state.getMergedAttributes().getFontType(); int column = 0; int x = 0; int spaceSize = EditorUtil.getSpaceWidth(fontType, this); for (int i = start; i < offset; i++) { if (i >= state.getEndOffset()) { state.advance(); fontType = state.getMergedAttributes().getFontType(); } SoftWrap softWrap = getSoftWrapModel().getSoftWrap(i); if (softWrap != null) { x = softWrap.getIndentInPixels(); } char c = text.charAt(i); if (c == '\t') { int prevX = x; x = EditorUtil.nextTabStop(x, this); column += EditorUtil.columnsNumber(c, x, prevX, spaceSize); } else { x += EditorUtil.charWidth(c, fontType, this); column++; } } return column; } boolean isInDistractionFreeMode() { return EditorUtil.isRealFileEditor(this) && (Registry.is("editor.distraction.free.mode") || isInPresentationMode()); } boolean isInPresentationMode() { return UISettings.getInstance().PRESENTATION_MODE && EditorUtil.isRealFileEditor(this); } @Override public void putInfo(@NotNull Map info) { final VisualPosition visual = getCaretModel().getVisualPosition(); info.put("caret", visual.getLine() + ":" + visual.getColumn()); } private class MyScrollPane extends JBScrollPane { private MyScrollPane() { setViewportBorder(new EmptyBorder(0, 0, 0, 0)); } @Override public void layout() { super.layout(); if (isInDistractionFreeMode()) { // re-calc gutter extra size after editor size is set // & layout once again to avoid blinking myGutterComponent.updateSize(); super.layout(); } } @Override protected void processMouseWheelEvent(@NotNull MouseWheelEvent e) { if (mySettings.isWheelFontChangeEnabled() && !MouseGestureManager.getInstance().hasTrackpad()) { if (EditorUtil.isChangeFontSize(e)) { int size = myScheme.getEditorFontSize() - e.getWheelRotation(); if (size >= MIN_FONT_SIZE) { setFontSize(size, SwingUtilities.convertPoint(this, e.getPoint(), getViewport())); } return; } } super.processMouseWheelEvent(e); } @NotNull @Override public JScrollBar createVerticalScrollBar() { return new MyScrollBar(Adjustable.VERTICAL); } @Override protected void setupCorners() { super.setupCorners(); setBorder(new TablessBorder()); setCorner(getVerticalScrollbarOrientation() == EditorEx.VERTICAL_SCROLLBAR_LEFT ? LOWER_RIGHT_CORNER : LOWER_LEFT_CORNER, new JPanel() { @Override public void paint(@NotNull Graphics g) { if (UISettings.getInstance().PRESENTATION_MODE) { return; } final Rectangle bounds = getBounds(); int width = bounds.width; int height = bounds.height; g.setColor(ButtonlessScrollBarUI.getTrackBackground()); g.fillRect(0, 0, width, height); int shortner = 0; if (myGutterComponent.isFoldingOutlineShown()) { shortner = myGutterComponent.getFoldingAreaWidth() / 2; } g.setColor(myGutterComponent.getBackground()); g.fillRect(0, 0, width - shortner, height); g.setColor(ButtonlessScrollBarUI.getTrackBorderColor()); g.drawLine(width - 1 - shortner, 0, width - 1 - shortner, height); g.drawLine(0, 0, width - 1, 0); } }); } } private class TablessBorder extends SideBorder { private TablessBorder() { super(JBColor.border(), SideBorder.ALL); } @Override public void paintBorder(@NotNull Component c, @NotNull Graphics g, int x, int y, int width, int height) { if (c instanceof JComponent) { Insets insets = ((JComponent)c).getInsets(); if (insets.left > 0) { super.paintBorder(c, g, x, y, width, height); } else { g.setColor(UIUtil.getPanelBackground()); g.drawLine(x, y, x + width, y); g.setColor(Gray._0.withAlpha(90)); g.drawLine(x, y, x + width, y); } } } @NotNull @Override public Insets getBorderInsets(Component c) { Container splitters = SwingUtilities.getAncestorOfClass(EditorsSplitters.class, c); boolean thereIsSomethingAbove = !SystemInfo.isMac || UISettings.getInstance().SHOW_MAIN_TOOLBAR || UISettings.getInstance().SHOW_NAVIGATION_BAR || toolWindowIsNotEmpty(); return splitters == null ? super.getBorderInsets(c) : new Insets(thereIsSomethingAbove ? 1 : 0, 0, 0, 0); } public boolean toolWindowIsNotEmpty() { if (myProject == null) return false; ToolWindowManagerEx m = ToolWindowManagerEx.getInstanceEx(myProject); if (m == null) return false; return !m.getIdsOn(ToolWindowAnchor.TOP).isEmpty(); } @Override public boolean isBorderOpaque() { return true; } } private class MyHeaderPanel extends JPanel { private int myOldHeight = 0; private MyHeaderPanel() { super(new BorderLayout()); } @Override public void revalidate() { myOldHeight = getHeight(); super.revalidate(); } @Override protected void validateTree() { int height = myOldHeight; super.validateTree(); height -= getHeight(); if (height != 0) { myVerticalScrollBar.setValue(myVerticalScrollBar.getValue() - height); } myOldHeight = getHeight(); } } private class MyTextDrawingCallback implements TextDrawingCallback { @Override public void drawChars(@NotNull Graphics g, @NotNull char[] data, int start, int end, int x, int y, Color color, @NotNull FontInfo fontInfo) { drawCharsCached(g, new CharArrayCharSequence(data), start, end, x, y, fontInfo, color); } } }