package com.intellij.openapi.editor.impl;

import com.intellij.Patches;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.hint.TooltipController;
import com.intellij.codeInsight.hint.TooltipGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.DataConstants;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.util.concurrency.ThreadingAssertions;
import com.intellij.util.concurrency.ThreadingAssertions;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.actionSystem.EditorAction;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.editor.actionSystem.EditorActionManager;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.ex.util.EmptyEditorHighlighter;
import com.intellij.openapi.editor.impl.event.MarkupModelEvent;
import com.intellij.openapi.editor.impl.event.MarkupModelListener;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.JScrollPane2;
import com.intellij.util.Alarm;
import com.intellij.util.IJSwingUtilities;
import gnu.trove.THashMap;
import gnu.trove.TIntArrayList;
import org.jdom.Element;

import javax.swing.plaf.ScrollBarUI;
import javax.swing.plaf.basic.BasicScrollBarUI;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.font.TextHitInfo;
import java.awt.im.InputMethodRequests;
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.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.TooManyListenersException;

public class EditorImpl implements EditorEx {
  private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorImpl");
  private static final Key DND_COMMAND_KEY = Key.create("DndCommand");
  private final DocumentImpl myDocument;

  private JPanel myPanel;
  private JScrollPane myScrollPane;
  private EditorComponentImpl myEditorComponent;
  private EditorGutterComponentImpl myGutterComponent;


  private CommandProcessor myCommandProcessor;
  private MyScrollBar myVerticalScrollBar;
  private MyScrollBar myHorizontalScrollBar;

  private ArrayList<EditorMouseListener> myMouseListeners = new ArrayList<EditorMouseListener>();
  private ArrayList<EditorMouseMotionListener> myMouseMotionListeners = new ArrayList<EditorMouseMotionListener>();

  private int myCharHeight = -1;
  private int myLineHeight = -1;
  private int myDescent = -1;

  private boolean myIsInsertMode = true;

  private final CaretCursor myCaretCursor;
  private final ScrollingTimer myScrollingTimer = new ScrollingTimer();

  private final Object MOUSE_DRAGGED_GROUP = Key.create("MouseDraggedGroup");
  private final THashMap<Object, Object> myUserDataMap = new THashMap<Object, Object>();

  private final DocumentListener myEditorDocumentAdapter;

  private SettingsImpl mySettings;

  private boolean isReleased = false;

  private MouseEvent myMousePressedEvent = null;

  private int mySavedSelectionStart = -1;
  private int mySavedSelectionEnd = -1;
  private int myLastColumnNumber = 0;

  private final PropertyChangeSupport myPropertyChangeSupport = new PropertyChangeSupport(this);
  private MyEditable myEditable;

  private EditorColorsScheme myScheme;
  private final boolean myIsViewer;
  private final SelectionModelImpl mySelectionModel;
  private final EditorMarkupModelImpl myMarkupModel;
  private final FoldingModelImpl myFoldingModel;
  private final ScrollingModelImpl myScrollingModel;
  private final CaretModelImpl myCaretModel;

  private static final RepaintCursorThread ourCaretThread;

//  private final BorderEffect myBorderEffect = new BorderEffect();

  private int myMouseSelectionState = MOUSE_SELECTION_STATE_NONE;
  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 final MarkupModelListener myMarkupModelListener;

  private EditorHighlighter myHighlighter;

  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 CachedFontContent myPlainCache = new CachedFontContent(Font.PLAIN);
  private CachedFontContent myBoldCache = new CachedFontContent(Font.BOLD);
  private CachedFontContent myBoldItalicCache = new CachedFontContent(Font.BOLD + Font.ITALIC);
  private CachedFontContent myItalicCache = new CachedFontContent(Font.ITALIC);

  private int myCurrentFontType = Font.PLAIN;

  private boolean myIsBlockSelectionMode;
  private EditorSizeContainer mySizeContainer = new EditorSizeContainer();

  private Runnable myCursorUpdater;
  private int myCaretUpdateVShift;
  Project myProject;
  private long myMouseSelectionChangeTimestamp;
  private int mySavedCaretOffsetForDNDUndoHack;
  private ArrayList<FocusChangeListener> myFocusListeners = new ArrayList<FocusChangeListener>();

  private MyInputMethodHandler myInputMethodRequestsHandler;
  private InputMethodRequests myInputMethodRequestsSwingWrapper;
  private boolean myIsOneLineMode;
  private boolean myIsRendererMode;
  private VirtualFile myVirtualFile;
  private boolean myIsColumnMode = false;
  private Color myForcedBackground = null;
  private Dimension myPreferredSize;
  private Runnable myGutterSizeUpdater = null;
  private Alarm myAppleRepaintAlarm;

  static {
    ourCaretThread = new RepaintCursorThread();
    ourCaretThread.start();
  }


  public EditorImpl(Document document, boolean viewer, Project project) {
    myProject = project;
    myDocument = (DocumentImpl)document;
    myScheme = new MyColorSchemeDelegate();
    myIsViewer = viewer;
    mySettings = new SettingsImpl(this);

    mySelectionModel = new SelectionModelImpl(this);
    myMarkupModel = new EditorMarkupModelImpl(this);
    myFoldingModel = new FoldingModelImpl(this);
    myCaretModel = new CaretModelImpl(this);
    myIsBlockSelectionMode = false;
    mySizeContainer.reset();

    myCommandProcessor = CommandProcessor.getInstance();

    myEditorDocumentAdapter = new EditorDocumentAdapter();

    myMarkupModelListener = new MarkupModelListener() {
      public void rangeHighlighterChanged(MarkupModelEvent event) {
        RangeHighlighterImpl rangeHighlighter = (RangeHighlighterImpl)event.getHighlighter();
        if (rangeHighlighter.isValid()) {
          int start = rangeHighlighter.getAffectedAreaStartOffset();
          int end = rangeHighlighter.getAffectedAreaEndOffset();
          int startLine = myDocument.getLineNumber(start);
          int endLine = myDocument.getLineNumber(end);
          repaintLines(Math.max(0, startLine - 1), Math.min(endLine + 1, getDocument().getLineCount()));
        }
        else {
          repaint(0, getDocument().getTextLength());
        }
        ((EditorMarkupModelImpl)getMarkupModel()).repaint();
        ((EditorMarkupModelImpl)getMarkupModel()).markDirtied();
        GutterIconRenderer renderer = rangeHighlighter.getGutterIconRenderer();
        if (renderer != null) {
          updateGutterSize();
        }
        updateCaretCursor();
      }
    };

    ((MarkupModelImpl)myDocument.getMarkupModel(myProject)).addMarkupModelListener(myMarkupModelListener);
    ((MarkupModelImpl)getMarkupModel()).addMarkupModelListener(myMarkupModelListener);

    myDocument.addDocumentListener(myFoldingModel);
    myDocument.addDocumentListener(myCaretModel);
    myDocument.addDocumentListener(mySelectionModel);
    myDocument.addDocumentListener(myEditorDocumentAdapter);

    myCaretCursor = new CaretCursor();

    myFoldingModel.flushCaretShift();
    myScrollbarOrientation = EditorEx.VERTICAL_SCROLLBAR_RIGHT;

    EditorHighlighter highlighter = new EmptyEditorHighlighter(myScheme.getAttributes(HighlighterColors.TEXT));
    setHighlighter(highlighter);

    initComponent();

    myScrollingModel = new ScrollingModelImpl(this);

    myGutterComponent.updateSize();
    myEditorComponent.setSize(getContentSize());

    Dimension preferredSize = getPreferredSize();
    myScrollPane.getViewport().setViewSize(preferredSize);

    if (Patches.APPLE_BUG_ID_3716835) {
      myScrollingModel.addVisibleAreaListener(new VisibleAreaListener() {
        public void visibleAreaChanged(VisibleAreaEvent e) {
          if (myAppleRepaintAlarm == null) {
            myAppleRepaintAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
          }
          myAppleRepaintAlarm.cancelAllRequests();
          myAppleRepaintAlarm.addRequest(new Runnable() {
            public void run() {
              repaint(0, myDocument.getTextLength());
            }
          }, 50, ModalityState.stateForComponent(myEditorComponent));
        }
      });
    }

    updateCaretCursor();
  }

  public boolean isViewer() {
    return myIsViewer || myIsRendererMode;
  }

  public boolean isRendererMode() {
    return myIsRendererMode;
  }

  public void setRendererMode(boolean isRendererMode) {
    myIsRendererMode = isRendererMode;
  }

  public void setFile(VirtualFile vFile) {
    myVirtualFile = vFile;
    reinitSettings();
  }

  public VirtualFile getVirtualFile() {
    return myVirtualFile;
  }

  public SelectionModel getSelectionModel() {
    return mySelectionModel;
  }

  public MarkupModel getMarkupModel() {
    return myMarkupModel;
  }

  public FoldingModel getFoldingModel() {
    return myFoldingModel;
  }

  public CaretModel getCaretModel() {
    return myCaretModel;
  }

  public ScrollingModel getScrollingModel() {
    return myScrollingModel;
  }

  public EditorSettings getSettings() {
    ThreadingAssertions.assertEventDispatchThread();
    return mySettings;
  }

  public void reinitSettings() {
    ThreadingAssertions.assertEventDispatchThread();
    myCharHeight = -1;
    myLineHeight = -1;
    myDescent = -1;
    myPlainFontMetrics = null;

    myPlainCache.reinitSettings();
    myBoldCache.reinitSettings();
    myItalicCache.reinitSettings();
    myBoldItalicCache.reinitSettings();

    myCaretModel.reinitSettings();
    mySelectionModel.reinitSettings();
    mySettings.reinitSettings();
    ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret());
    ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod());
    mySizeContainer.reset();
    myFoldingModel.refreshSettings();
    myFoldingModel.rebuild();

    myHighlighter.setColorScheme(myScheme);

    myGutterComponent.revalidate();
    myEditorComponent.repaint();

    updateCaretCursor();
  }

  public void release() {
    isReleased = true;
    myDocument.removeDocumentListener(myHighlighter);
    myDocument.removeDocumentListener(myEditorDocumentAdapter);
    myDocument.removeDocumentListener(myFoldingModel);
    myDocument.removeDocumentListener(myCaretModel);
    myDocument.removeDocumentListener(mySelectionModel);
    MarkupModelImpl markupModel = (MarkupModelImpl)myDocument.getMarkupModel(myProject, false);
    if (markupModel != null) markupModel.removeMarkupModelListener(myMarkupModelListener);
    myMarkupModel.dispose();
    myLineHeight = -1;
    myCharHeight = -1;
    myDescent = -1;
    myPlainFontMetrics = null;
    myScrollingModel.dispose();
    myGutterComponent.dispose();
  }

  public <T> void putUserData(Key<T> key, T value) {
    if (value != null) {
      myUserDataMap.put(key, value);
    }
    else {
      myUserDataMap.remove(key);
    }
  }

  public <T> T getUserData(Key<T> key) {
    return (T)myUserDataMap.get(key);
  }

  private void initComponent() {
    myEditorComponent = new EditorComponentImpl(this);
//    myStatusBar = new EditorStatusBarImpl();

    myScrollPane = new JScrollPane2() {
      protected void processMouseWheelEvent(MouseWheelEvent e) {
        if (mySettings.isWheelFontChangeEnabled()) {
          boolean changeFontSize = SystemInfo.isMac
                                   ? !e.isControlDown() && e.isMetaDown() && !e.isAltDown() && !e.isShiftDown()
                                   : e.isControlDown() && !e.isMetaDown() && !e.isAltDown() && !e.isShiftDown();
          if (changeFontSize) {
            setFontSize(myScheme.getEditorFontSize() + e.getWheelRotation());
            return;
          }
        }

        super.processMouseWheelEvent(e);
      }
    };
    myPanel = new JPanel() {
      public void addNotify() {
        super.addNotify();
        if (((JComponent)getParent()).getBorder() != null) myScrollPane.setBorder(null);
      }
    };

    //myPanel.setLayout(new BoxLayout(myPanel, BoxLayout.Y_AXIS));
    myPanel.setLayout(new BorderLayout());

    myVerticalScrollBar = new MyScrollBar(JScrollBar.VERTICAL);
    myHorizontalScrollBar = new MyScrollBar(JScrollBar.HORIZONTAL);

    myGutterComponent = new EditorGutterComponentImpl(this);
    myGutterComponent.setOpaque(true);

    myScrollPane.setVerticalScrollBar(myVerticalScrollBar);
    myScrollPane.setHorizontalScrollBar(myHorizontalScrollBar);
    myScrollPane.setViewportView(myEditorComponent);
    //myScrollPane.setBorder(null);
    myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
    myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);


    myScrollPane.setRowHeaderView(myGutterComponent);
    stopOptimizedScrolling();

    myEditorComponent.setTransferHandler(new MyTransferHandler());
    myEditorComponent.setAutoscrolls(true);

/*  Default mode till 1.4.0
 *   myScrollPane.getViewport().setScrollMode(JViewport.BLIT_SCROLL_MODE);
 */
    myPanel.add(myScrollPane);

    myEditorComponent.addKeyListener(new KeyAdapter() {
      public void keyTyped(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() {
      public void focusGained(FocusEvent e) {
        myCaretCursor.activate();
        int caretLine = getCaretModel().getLogicalPosition().line;
        repaintLines(caretLine, caretLine);
        fireFocusGained();
      }

      public void focusLost(FocusEvent e) {
        synchronized (ourCaretThread) {
          if (ourCaretThread.myEditor == EditorImpl.this) {
            ourCaretThread.myEditor = null;
          }
        }
        int caretLine = getCaretModel().getLogicalPosition().line;
        repaintLines(caretLine, caretLine);
        fireFocusLost();
      }
    });

//    myBorderEffect.reset();

    try {
      myEditorComponent.getDropTarget().addDropTargetListener(new DropTargetAdapter() {
        public void drop(DropTargetDropEvent dtde) {
        }

        public void dragOver(DropTargetDragEvent dtde) {
          Point location = dtde.getLocation();

          moveCaretToScreenPos(location.x, location.y);
          getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
        }
      });
    }
    catch (TooManyListenersException e) {
    }
  }

  public void setFontSize(final int fontSize) {
    int oldFontSize = myScheme.getEditorFontSize();
    myScheme.setEditorFontSize(fontSize);
    myPropertyChangeSupport.firePropertyChange(EditorEx.PROP_FONT_SIZE, oldFontSize, fontSize);
  }

  private void 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.isWaitingForSecondKeyStroke() || ProgressManager.getInstance().hasModalProgressIndicator()) {
      return;
    }
    ActionManagerEx actionManager = ActionManagerEx.getInstanceEx();
    DataContext dataContext = getDataContext();
    actionManager.fireBeforeEditorTyping(c, dataContext);
    EditorActionManager.getInstance().getTypedAction().actionPerformed(this, c, dataContext);
  }

  private void fireFocusLost() {
    FocusChangeListener[] listeners = getFocusListeners();
    for (int i = 0; i < listeners.length; i++) {
      FocusChangeListener listener = listeners[i];
      listener.focusLost(this);
    }
  }

  private FocusChangeListener[] getFocusListeners() {
    return myFocusListeners.toArray(new FocusChangeListener[myFocusListeners.size()]);
  }

  private void fireFocusGained() {
    FocusChangeListener[] listeners = getFocusListeners();
    for (int i = 0; i < listeners.length; i++) {
      FocusChangeListener listener = listeners[i];
      listener.focusGained(this);
    }
  }

  public void setHighlighter(EditorHighlighter highlighter) {
    ThreadingAssertions.assertEventDispatchThread();
    if (myHighlighter != null) {
      getDocument().removeDocumentListener(myHighlighter);
    }

    getDocument().addDocumentListener(highlighter);
    highlighter.setText(getDocument().getCharsSequence());
    myHighlighter = highlighter;
    myHighlighter.setEditor(this);
    if (myPanel != null) {
      reinitSettings();
    }
  }

  public EditorHighlighter getHighlighter() {
    ThreadingAssertions.assertEventDispatchThread();
    return myHighlighter;
  }

  public JComponent getContentComponent() {
    return myEditorComponent;
  }

  public EditorGutterComponentEx getGutterComponentEx() {
    return myGutterComponent;
  }

  public void addPropertyChangeListener(PropertyChangeListener listener) {
    myPropertyChangeSupport.addPropertyChangeListener(listener);
  }

  public void removePropertyChangeListener(PropertyChangeListener listener) {
    myPropertyChangeSupport.removePropertyChangeListener(listener);
  }

  public void setInsertMode(boolean mode) {
    ThreadingAssertions.assertEventDispatchThread();
    boolean oldValue = myIsInsertMode;
    myIsInsertMode = mode;
    myPropertyChangeSupport.firePropertyChange(EditorEx.PROP_INSERT_MODE, oldValue, mode);
    //Repaint the caret line by moving caret to the same place
    LogicalPosition caretPosition = getCaretModel().getLogicalPosition();
    getCaretModel().moveToLogicalPosition(caretPosition);
  }

  public boolean isInsertMode() {
    return myIsInsertMode;
  }

  public void setColumnMode(boolean mode) {
    ThreadingAssertions.assertEventDispatchThread();
    boolean oldValue = myIsColumnMode;
    myIsColumnMode = mode;
    myPropertyChangeSupport.firePropertyChange(PROP_COLUMN_MODE, oldValue, mode);
  }

  public boolean isColumnMode() {
    return myIsColumnMode;
  }

  public void setBlockSelectionMode(boolean isBlockSelectionMode) {
    myIsBlockSelectionMode = isBlockSelectionMode;
  }

  public boolean isBlockSelectionMode() {
    return myIsBlockSelectionMode;
  }

  private int yPositionToVisibleLineNumber(int y) {
    return y / getLineHeight();
  }

  public int getSpaceWidth(int  fontType) {
    int width = charWidth(fontType, ' ');
    return width > 0 ? width : 1;
  }

  public VisualPosition xyToVisualPosition(Point p) {
    int line = yPositionToVisibleLineNumber(p.y);

    int x = 0;
    int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0)));
    int textLength = myDocument.getTextLength();

    if (offset >= textLength) return new VisualPosition(line, 0);

    int column = 0;
    int prevX = 0;
    CharSequence text = myDocument.getCharsNoThreadCheck();
    char c = ' ';
    IterationState state = new IterationState(this, offset, false);

    int fontType = state.getMergedAttributes().getFontType();
    int spaceSize = getSpaceWidth(fontType);

    outer:
        while (true) {
          if (offset >= textLength) break;

          if (offset >= state.getEndOffset()) {
            state.advance();
            fontType = state.getMergedAttributes().getFontType();
          }

          FoldRegion region = state.getCurrentFold();
          if (region != null) {
            char[] placeholder = region.getPlaceholderText().toCharArray();
            for (int j = 0; j < placeholder.length; j++) {
              c = placeholder[j];
              x += charWidth(fontType, c);
              if (x >= p.x) break outer;
              column++;
            }
            offset = region.getEndOffset();
          }
          else {
            prevX = x;
            c = text.charAt(offset);
            if (c == '\n') {
              break;
            }
            if (c == '\t') {
              x = nextTabStop(x);
            }
            else {
              x += charWidth(fontType, c);
            }

            if (x >= p.x) break;

            if (c == '\t') {
              column += (x - prevX) / spaceSize;
            }
            else {
              column++;
            }

            offset++;
          }
        }

    int charWidth = charWidth(fontType, c);

    if (x >= p.x && c == '\t') {
      if (mySettings.isCaretInsideTabs()) {
        column += (p.x - prevX) / spaceSize;
        if ((p.x - prevX) % spaceSize > spaceSize / 2) column++;
      }
      else {
        if ((x - p.x) * 2 < x - prevX) {
          column += (x - prevX) / spaceSize;
        }
      }
    }
    else {
      if (x >= p.x) {
        if ((x - p.x) * 2 < charWidth) column++;
      }
      else {
        column += (p.x - x) / getSpaceWidth(fontType);
      }
    }

    return new VisualPosition(line, column);
  }

  public VisualPosition offsetToVisualPosition(int offset) {
    return logicalToVisualPosition(offsetToLogicalPosition(offset));
  }

  public LogicalPosition offsetToLogicalPosition(int offset) {
    int line = calcLogicalLineNumber(offset);
    int column = calcColumnNumber(offset, line);
    return new LogicalPosition(line, column);
  }

  public LogicalPosition xyToLogicalPosition(Point p) {
    final Point pp;
    if (p.x >= 0 && p.y >= 0) {
      pp = p;
    }
    else {
      pp = new Point(Math.max(p.x, 0), Math.max(p.y, 0));
    }

    return visualToLogicalPosition(xyToVisualPosition(pp));
  }

  public Point logicalPositionToXY(LogicalPosition pos) {
    VisualPosition visible = logicalToVisualPosition(pos);
    int y = visibleLineNumberToYPosition(visible.line);

    int lineStartOffset;

    if (pos.line == 0) {
      lineStartOffset = 0;
    }
    else {
      if (pos.line >= myDocument.getLineCount()) {
        lineStartOffset = myDocument.getTextLength();
      }
      else {
        lineStartOffset = myDocument.getLineStartOffset(pos.line);
      }
    }

    int x = getTabbedTextWidth(lineStartOffset, visible);
    return new Point(x, y);
  }

  public Point visualPositionToXY(VisualPosition visible) {
    int y = visibleLineNumberToYPosition(visible.line);
    int logLine = visualToLogicalPosition(new VisualPosition(visible.line, 0)).line;

    int lineStartOffset;

    if (logLine == 0) {
      lineStartOffset = 0;
    }
    else {
      if (logLine >= myDocument.getLineCount()) {
        lineStartOffset = myDocument.getTextLength();
      }
      else {
        lineStartOffset = myDocument.getLineStartOffset(logLine);
      }
    }

    int x = getTabbedTextWidth(lineStartOffset, visible);
    return new Point(x, y);
  }

  private int getTabbedTextWidth(int lineStartOffset, VisualPosition pos) {
    if (pos.column == 0) return 0;

    int x = 0;
    int offset = lineStartOffset;
    CharSequence text = myDocument.getCharsNoThreadCheck();
    int textLength = myDocument.getTextLength();
    IterationState state = new IterationState(this, offset, false);
    int fontType = state.getMergedAttributes().getFontType();
    int spaceSize = getSpaceWidth(fontType);

    int column = 0;
    outer:
        while (column < pos.column) {
          if (offset >= textLength) break;

          if (offset >= state.getEndOffset()) {
            state.advance();
            fontType = state.getMergedAttributes().getFontType();
          }

          FoldRegion region = state.getCurrentFold();

          if (region != null) {
            char[] placeholder = region.getPlaceholderText().toCharArray();
            for (int j = 0; j < placeholder.length; j++) {
              x += charWidth(fontType, placeholder[j]);
              column++;
              if (column >= pos.column) break outer;
            }
            offset = region.getEndOffset();
          }
          else {
            char c = text.charAt(offset);
            if (c == '\n') {
              break;
            }
            if (c == '\t') {
              int prevX = x;
              x = nextTabStop(x);
              column += (x - prevX) / spaceSize;
            }
            else {
              x += charWidth(fontType, c);
              column++;
            }
            offset++;
          }
        }

    if (column != pos.column) {
      x += getSpaceWidth(fontType) * (pos.column - column);
    }

    return x;
  }

  public int nextTabStop(int x) {
    int tabSize = mySettings.getTabSize(myProject);
    if (tabSize <= 0) {
      tabSize = 1;
    }

    tabSize *= getSpaceWidth(Font.PLAIN);

    int nTabs = x / tabSize;
    return (nTabs + 1) * tabSize;
  }

  public int visibleLineNumberToYPosition(int lineNum) {
    if (lineNum < 0) throw new IndexOutOfBoundsException("Wrong line: " + lineNum);
    return lineNum * getLineHeight();
  }

  public void repaint(int startOffset, int endOffset) {
    ThreadingAssertions.assertEventDispatchThread();
    if (myScrollPane == null) {
      return;
    }
    if (endOffset > myDocument.getTextLength()) {
      endOffset = myDocument.getTextLength();
    }
    if (startOffset < endOffset) {
      int startLine = myDocument.getLineNumber(startOffset);
      int endLine = myDocument.getLineNumber(endOffset);
      repaintLines(startLine, endLine);
    }
  }

  public void repaintLines(int startLine, int endLine) {
    Rectangle visibleRect = getScrollingModel().getVisibleArea();
    int yStartLine = logicalPositionToXY(new LogicalPosition(startLine, 0)).y;
    int yEndLine = logicalPositionToXY(new LogicalPosition(endLine, 0)).y + getLineHeight() + WAVE_HEIGHT;

    myEditorComponent.repaintEditorComponent(visibleRect.x,
                                             yStartLine,
                                             visibleRect.x + visibleRect.width,
                                             yEndLine - yStartLine);
    myGutterComponent.repaint(0, yStartLine, myGutterComponent.getWidth(), yEndLine - yStartLine);
  }

  private void beforeChangedUpdate(DocumentEvent e) {
    Rectangle viewRect = getScrollingModel().getVisibleArea();
    Point pos = visualPositionToXY(getCaretModel().getVisualPosition());
    myCaretUpdateVShift = pos.y - viewRect.y;
    mySizeContainer.beforeChange(e);
  }

  private void changedUpdate(DocumentEvent e) {
    if (myScrollPane == null) return;

    stopOptimizedScrolling();
    mySelectionModel.removeBlockSelection();

    mySizeContainer.changedUpdate(e);
    int startVisualLine = offsetToVisualPosition(e.getOffset()).line;
    int endVisualLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line;

    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) {
        updateGutterSize();
      }
    }

    updateCaretCursor();
    repaintLines(startVisualLine, endVisualLine);

    Point caretLocation = visualPositionToXY(getCaretModel().getVisualPosition());
    int scrollOffset = caretLocation.y - myCaretUpdateVShift;
    getScrollingModel().scrollVertically(scrollOffset);
  }

  private void updateGutterSize() {
    if (myGutterSizeUpdater != null) return;
    myGutterSizeUpdater = new Runnable() {
      public void run() {
        myGutterComponent.updateSize();
        myGutterSizeUpdater = null;
      }
    };
  }

  void validateSize() {
    if (myGutterSizeUpdater != null) {
      SwingUtilities.invokeLater(myGutterSizeUpdater);
    }

    Dimension dim = getPreferredSize();

    if (!dim.equals(myPreferredSize)) {
      myPreferredSize = dim;

      stopOptimizedScrolling();
      myScrollPane.getViewport().setViewSize(myPreferredSize);
      int lineNum = Math.max(1, getDocument().getLineCount());

      myGutterComponent.setLineNumberAreaWidth(getFontMetrics(Font.PLAIN).stringWidth(Integer.toString((lineNum + 2))) + 6);
      final JViewport rowHeader = myScrollPane.getRowHeader();
      if (rowHeader != null) {
        rowHeader.setViewSize(myGutterComponent.getPreferredSize());
      }

      getScrollPane().revalidate();

      myEditorComponent.repaint();
      myScrollPane.repaint();
      myMarkupModel.repaint();
    }
  }

  void recalcSizeAndRepaint() {
    mySizeContainer.reset();

    validateSize();

    Dimension size = getPreferredSize();

    myScrollPane.getVerticalScrollBar().setMaximum(size.height);
    myScrollPane.getHorizontalScrollBar().setMaximum(size.width);

    getScrollPane().revalidate();
    myMarkupModel.repaint();

    stopOptimizedScrolling();
    myEditorComponent.repaintEditorComponent();

    myGutterComponent.repaint();
  }

  public Document getDocument() {
    return myDocument;
  }

  public JComponent getComponent() {
    return myPanel;
  }

  public void addEditorMouseListener(EditorMouseListener listener) {
    myMouseListeners.add(listener);
  }

  public void removeEditorMouseListener(EditorMouseListener listener) {
    boolean success = myMouseListeners.remove(listener);
    LOG.assertTrue(success);
  }

  public void addEditorMouseMotionListener(EditorMouseMotionListener listener) {
    myMouseMotionListeners.add(listener);
  }

  public void removeEditorMouseMotionListener(EditorMouseMotionListener listener) {
    boolean success = myMouseMotionListeners.remove(listener);
    LOG.assertTrue(success);
  }

  public boolean isDisposed() {
    return isReleased;
  }

  public void paint(Graphics g) {
    validateSize();
    startOptimizedScrolling();

    if (myCursorUpdater != null) {
      myCursorUpdater.run();
      myCursorUpdater = null;
    }

    Rectangle clip = getClipBounds(g);

    if (clip == null) {
      return;
    }

    Rectangle viewRect = getScrollingModel().getVisibleArea();
    if (viewRect == null) {
      return;
    }

    if (isReleased) {
      g.setColor(new Color(128, 255, 128));
      g.fillRect(clip.x, clip.y, clip.width, clip.height);

      return;
    }

    Color background = getBackroundColor();
    g.setColor(background);
    g.fillRect(clip.x, clip.y, clip.width, clip.height);

    paintBackgrounds(g, clip);
    paintRectangularSelection(g);
    paintRightMargin(g, clip);
    final MarkupModel docMarkup = myDocument.getMarkupModel(myProject);
    if (docMarkup != null) {
      paintLineMarkersSeparators(g, clip, docMarkup);
    }
    paintLineMarkersSeparators(g, clip, myMarkupModel);
    paintText(g, clip);
    paintSegmentHighlightersBorderAndAfterEndOfLine(g, clip);
    BorderEffect borderEffect = new BorderEffect(this, g);
    borderEffect.paintHighlighters(getHighlighter());
    if (docMarkup != null) {
      borderEffect.paintHighlighters(docMarkup.getAllHighlighters());
    }
    borderEffect.paintHighlighters((getMarkupModel()).getAllHighlighters());
    paintCaretCursor(g);

    paintComposedTextDecoration((Graphics2D)g);
  }

  public void setBackgroundColor(Color color) {
    myForcedBackground = color;
  }

  public void resetBackgourndColor() {
    myForcedBackground = null;
  }

  public Color getBackroundColor() {
    if (myForcedBackground != null) return myForcedBackground;

    Color color = myScheme.getColor(EditorColors.BACKGROUND_COLOR);
    if (color == null) color = Color.white;
    if (myDocument.isWritable()) {
      return color;
    }
    Color readOnlyColor = myScheme.getColor(EditorColors.READONLY_BACKGROUND_COLOR);
    return readOnlyColor != null ? readOnlyColor : color;
  }

  private void paintComposedTextDecoration(Graphics2D g) {
    if (myInputMethodRequestsHandler != null && myInputMethodRequestsHandler.composedText != null) {
      VisualPosition visStart = offsetToVisualPosition(
        Math.min(myInputMethodRequestsHandler.composedTextStart, myDocument.getTextLength()));
      int y = visibleLineNumberToYPosition(visStart.line) + getLineHeight() - getDescent() + 1;
      Point p1 = visualPositionToXY(visStart);
      Point p2 = logicalPositionToXY(
        offsetToLogicalPosition(Math.min(myInputMethodRequestsHandler.composedTextEnd, 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);
      g.drawLine(p1.x, y, p2.x, y);
      g.setStroke(saved);
    }
  }

  private static Rectangle getClipBounds(Graphics g) {
    return g.getClipBounds();
  }

  private void paintRightMargin(Graphics g, Rectangle clip) {
    Color rightMargin = myScheme.getColor(EditorColors.RIGHT_MARGIN_COLOR);
    if (!mySettings.isRightMarginShown() || rightMargin == null) {
      return;
    }
    int x = mySettings.getRightMargin(myProject) * getSpaceWidth(Font.PLAIN);
    if (x >= clip.x && x < clip.x + clip.width) {
      g.setColor(rightMargin);
      g.drawLine(x, clip.y, x, clip.y + clip.height);
    }
  }

  private void paintSegmentHighlightersBorderAndAfterEndOfLine(Graphics g, Rectangle clip) {
    if (myDocument.getLineCount() == 0) return;
    int startLineNumber = yPositionToVisibleLineNumber(clip.y);
    int endLineNumber = yPositionToVisibleLineNumber(clip.y + clip.height) + 1;

    RangeHighlighter[] segmentHighlighters;
    final MarkupModel docMarkup = myDocument.getMarkupModel(myProject);
    if (docMarkup != null) {
      segmentHighlighters = docMarkup.getAllHighlighters();
      for (int i = 0; i < segmentHighlighters.length; i++) {
        paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx)segmentHighlighters[i], startLineNumber,
                                              endLineNumber);
      }
    }

    segmentHighlighters = getMarkupModel().getAllHighlighters();
    for (int i = 0; i < segmentHighlighters.length; i++) {
      RangeHighlighter segmentHighlighter = segmentHighlighters[i];
      paintSegmentHighlighterAfterEndOfLine(g, (RangeHighlighterEx)segmentHighlighter, startLineNumber, endLineNumber);
    }
  }

  private void paintSegmentHighlighterAfterEndOfLine(Graphics g, RangeHighlighterEx segmentHighlighter,
                                                     int startLineNumber, int endLineNumber) {
    if (!segmentHighlighter.isValid()) {
      return;
    }
    if (segmentHighlighter.isAfterEndOfLine()) {
      int startOffset = segmentHighlighter.getStartOffset();
      int visibleStartLine = offsetToVisualPosition(startOffset).line;

      if (!getFoldingModel().isOffsetCollapsed(startOffset)) {
        if (visibleStartLine >= startLineNumber && visibleStartLine <= endLineNumber) {
          int logStartLine = offsetToLogicalPosition(startOffset).line;
          LogicalPosition logPosition = offsetToLogicalPosition(myDocument.getLineEndOffset(logStartLine));
          Point end = logicalPositionToXY(logPosition);
          int charWidth = getSpaceWidth(Font.PLAIN);
          int lineHeight = getLineHeight();
          TextAttributes attributes = segmentHighlighter.getTextAttributes();
          if (attributes != null && attributes.getBackgroundColor() != null) {
            g.setColor(attributes.getBackgroundColor());
            g.fillRect(end.x, end.y, charWidth, lineHeight);
          }
          if (attributes != null && attributes.getEffectColor() != null) {
            int y = visibleLineNumberToYPosition(visibleStartLine) + getLineHeight() - getDescent() + 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.BOXED) {
              g.drawLine(end.x, y, end.x + charWidth - 1, y);
            }
          }
        }
      }
    }
  }

  public int getMaxWidthInRange(int startOffset, int endOffset) {
    int width = 0;
    VisualPosition start = offsetToVisualPosition(startOffset);
    VisualPosition end = offsetToVisualPosition(endOffset);

    for (int i = start.line; i <= end.line; 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(Graphics g, Rectangle clip) {
    int lineHeight = getLineHeight();

    int visibleLineNumber = clip.y / lineHeight;

    int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line;

    if (startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) {
      return;
    }

    int start = myDocument.getLineStartOffset(startLineNumber);

    IterationState iterationState = new IterationState(this, start, true);

    LineIterator lIterator = createLineIterator();
    lIterator.start(start);
    if (lIterator.atEnd()) {
      return;
    }

    TextAttributes attributes = iterationState.getMergedAttributes();
    Color backColor = attributes.getBackgroundColor();
    Point position = new Point(0, visibleLineNumber * lineHeight);
    int fontType = attributes.getFontType();
    CharSequence text = myDocument.getCharsNoThreadCheck();
    int lastLineIndex = Math.max(0, myDocument.getLineCount() - 1);
    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 = drawBackground(g, backColor, text.subSequence(start, lEnd - lIterator.getSeparatorLength()),
                                      position, fontType);

          if (lIterator.getLineNumber() < lastLineIndex) {
            if (backColor != null) {
              Color saved = g.getColor();
              g.setColor(backColor);
              g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight);
              g.setColor(saved);
            }
          }
          else {
            paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight);
            break;
          }

          position.x = 0;
          if (position.y > clip.y + clip.height) break;
          position.y += lineHeight;
          start = lEnd;
        }

        lIterator.advance();
      }
      else {
        FoldRegion collapsedFolderAt = iterationState.getCurrentFold();
        if (collapsedFolderAt != null) {
          position.x = drawBackground(g, backColor, collapsedFolderAt.getPlaceholderText(),
                                      position, fontType);
        }
        else {
          if (hEnd > lEnd - lIterator.getSeparatorLength()) {
            position.x = drawBackground(g, backColor, text.subSequence(start, lEnd - lIterator.getSeparatorLength()),
                                        position, fontType);
          }
          else {
            position.x = drawBackground(g, backColor, text.subSequence(start, hEnd), position, fontType);
          }
        }

        iterationState.advance();
        attributes = iterationState.getMergedAttributes();
        backColor = attributes.getBackgroundColor();
        fontType = attributes.getFontType();
        start = iterationState.getStartOffset();
      }
    }

    if (lIterator.getLineNumber() >= lastLineIndex && position.y <= clip.y + clip.height) {
      paintAfterFileEndBackground(iterationState, g, position, clip, lineHeight);
    }
  }

  private void paintRectangularSelection(Graphics g) {
    final SelectionModel model = getSelectionModel();
    if (!model.hasBlockSelection()) return;
    final Point start = logicalPositionToXY(model.getBlockStart());
    final Point end = logicalPositionToXY(model.getBlockEnd());
    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 static void paintAfterFileEndBackground(IterationState iterationState, Graphics g, Point position,
                                                  Rectangle clip, int lineHeight) {
    Color backColor = iterationState.getPastFileEndBackground();
    if (backColor != null) {
      Color saved = g.getColor();
      g.setColor(backColor);
      g.fillRect(position.x, position.y, clip.x + clip.width - position.x, lineHeight);
      g.setColor(saved);
    }
  }

  private int drawBackground(Graphics g,
                             Color backColor,
                             CharSequence text,
                             Point position,
                             int fontType) {
    int w = getTextSegmentWidth(text, position.x, fontType);

    if (backColor != null) {
      Color savedColor = g.getColor();
      g.setColor(backColor);
      g.fillRect(position.x, position.y, w, getLineHeight());
      g.setColor(savedColor);
    }

    return position.x + w;
  }

  private LineIterator createLineIterator() {
    return myDocument.createLineIterator();
  }

  private void paintText(Graphics g, Rectangle clip) {
    int lineHeight = getLineHeight();

    int visibleLineNumber = clip.y / lineHeight;

    int startLineNumber = xyToLogicalPosition(new Point(0, clip.y)).line;

    if (startLineNumber >= myDocument.getLineCount() || startLineNumber < 0) {
      return;
    }

    int start = myDocument.getLineStartOffset(startLineNumber);

    IterationState iterationState = new IterationState(this, start, true);

    LineIterator lIterator = createLineIterator();
    lIterator.start(start);
    if (lIterator.atEnd()) {
      return;
    }

    TextAttributes attributes = iterationState.getMergedAttributes();
    Color currentColor = attributes.getForegroundColor();
    if (currentColor == null) {
      currentColor = Color.black;
    }
    Color effectColor = attributes.getEffectColor();
    EffectType effectType = attributes.getEffectType();
    int fontType = attributes.getFontType();
    myCurrentFontType = fontType;
    Font currentFont = getFont(fontType);
    g.setColor(currentColor);
    g.setFont(currentFont);
    Point position = new Point(0, visibleLineNumber * lineHeight);
    while (!iterationState.atEnd() && !lIterator.atEnd()) {
      int hEnd = iterationState.getEndOffset();
      int lEnd = lIterator.getEnd();
      if (hEnd >= lEnd) {
        FoldRegion collapsedFolderAt = myFoldingModel.getCollapsedRegionAtOffset(start);
        if (collapsedFolderAt == null) {
          drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor, effectType,
                     fontType, currentColor);
          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) {
          int foldingXStart = position.x;
          position.x = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType,
                                  fontType, currentColor);
          BorderEffect.paintFoldedEffect(g, foldingXStart, position.y, position.x, getLineHeight(), effectColor,
                                         effectType);

        }
        else {
          if (hEnd > lEnd - lIterator.getSeparatorLength()) {
            position.x = drawString(g, start, lEnd - lIterator.getSeparatorLength(), position, clip, effectColor,
                                    effectType, fontType, currentColor);
          }
          else {
            position.x = drawString(g, start, hEnd, position, clip, effectColor, effectType, fontType, currentColor);
          }
        }

        iterationState.advance();
        attributes = iterationState.getMergedAttributes();
        Color color = attributes.getForegroundColor();
        if (color == null) {
          color = Color.black;
        }
        if (color != currentColor) {
          g.setColor(color);
          currentColor = color;
        }
        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 = drawString(g, collapsedFolderAt.getPlaceholderText(), position, clip, effectColor, effectType, fontType,
                                   currentColor);
      BorderEffect.paintFoldedEffect(g, foldingXStart, position.y, foldingXEnd, getLineHeight(), effectColor,
                                     effectType);
//      myBorderEffect.collapsedFolderReached(g, this);
    }

    flushCachedChars(g);
  }

  private class CachedFontContent {
    int[] charWidths = null;
    final CharSequence[] data = new CharSequence[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;
    final int myFontType;
    private FontMetrics myFontMetrics;

    public CachedFontContent(int fontType) {
      myFontType = fontType;
    }

    public void reinitSettings() {
      myFontMetrics = null;
      charWidths = null;
    }

    public int charWidth(char c) {
      if (c >= 256) return fontMetrics().charWidth(c);
      if (charWidths == null) {
        charWidths = new int[256];
        FontMetrics fontMetrics = fontMetrics();
        for (int i = 0; i < 256; i++) {
          charWidths[i] = fontMetrics.charWidth(i);
        }
      }
      return charWidths[c];
    }

    private FontMetrics fontMetrics() {
      if (myFontMetrics == null) {
        myFontMetrics = getFontMetrics(myFontType);
      }
      return myFontMetrics;
    }

    public void flushContent(Graphics g) {
      if (myCount != 0) {
        if (myCurrentFontType != myFontType) {
          myCurrentFontType = myFontType;
          g.setFont(getFont(myFontType));
        }

        Color currentColor = null;
        for (int i = 0; i < myCount; i++) {
          if (!color[i].equals(currentColor)) {
            currentColor = color[i];
            g.setColor(currentColor);
          }

          drawChars(g, data[i], x[i], y[i]);
          color[i] = null;
          data[i] = null;
        }

        myCount = 0;
      }
    }

    public void addContent(Graphics g, CharSequence data, int x, int y, Color color) {
      this.data[myCount] = data;
      this.x[myCount] = x;
      this.y[myCount] = y;
      this.color[myCount] = color;

      myCount++;
      if (myCount >= CACHED_CHARS_BUFFER_SIZE) {
        flushContent(g);
      }
    }
  }

  private void flushCachedChars(Graphics g) {
    myPlainCache.flushContent(g);
    myBoldCache.flushContent(g);
    myBoldItalicCache.flushContent(g);
    myItalicCache.flushContent(g);
  }

  private void paintCaretCursor(Graphics g) {
    myCaretCursor.paint(g);
  }

  private void paintLineMarkersSeparators(Graphics g, Rectangle clip, MarkupModel markupModel) {
    if (markupModel == null) return;
    RangeHighlighter[] lineMarkers = markupModel.getAllHighlighters();
    for (int i = 0; i < lineMarkers.length; i++) {
      RangeHighlighter lineMarker = lineMarkers[i];
      paintLineMarkerSeparator(lineMarker, clip, g);
    }
  }

  private void paintLineMarkerSeparator(RangeHighlighter marker, Rectangle clip, Graphics g) {
    if (!marker.isValid()) {
      return;
    }
    Color separatorColor = marker.getLineSeparatorColor();
    if (separatorColor != null) {
      int lineNumber = marker.getLineSeparatorPlacement() == SeparatorPlacement.TOP ? marker.getDocument()
        .getLineNumber(marker.getStartOffset()) : marker.getDocument().getLineNumber(marker.getEndOffset());
      if (lineNumber < 0 || lineNumber >= myDocument.getLineCount()) {
        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) * getSpaceWidth(Font.PLAIN));
      }

      int y = visibleLineNumberToYPosition(logicalToVisualPosition(new LogicalPosition(lineNumber, 0)).line);

      if (marker.getLineSeparatorPlacement() != SeparatorPlacement.TOP) {
        y += getLineHeight();
      }

      g.drawLine(0, y - 1, endShift, y - 1);
    }
  }

  private Font getFont(int type) {
    if (type == Font.BOLD) {
      return myScheme.getFont(EditorFontType.BOLD);
    }
    else {
      if (type == Font.ITALIC) {
        return myScheme.getFont(EditorFontType.ITALIC);
      }
      else {
        if (type == Font.ITALIC + Font.BOLD) {
          return myScheme.getFont(EditorFontType.BOLD_ITALIC);
        }
        else {
          return myScheme.getFont(EditorFontType.PLAIN);
        }
      }
    }
  }

  private int drawString(Graphics g, int start, int end, Point position, Rectangle clip, Color effectColor,
                         EffectType effectType, int fontType, Color fontColor) {
    if (start >= end) return position.x;

    CharSequence text = myDocument.getCharsNoThreadCheck();
    boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height);

    if (!isInClip) return position.x;

    int y = getLineHeight() - getDescent() + position.y;
    int x = position.x;
    return drawTabbedString(g, text.subSequence(start, end), x, y, effectColor, effectType, fontType, fontColor);
  }

  private int drawString(Graphics g, String text, Point position, Rectangle clip, Color effectColor,
                         EffectType effectType, int fontType, Color fontColor) {
    boolean isInClip = (getLineHeight() + position.y >= clip.y) && (position.y <= clip.y + clip.height);

    if (!isInClip) return position.x;

    int y = getLineHeight() - getDescent() + position.y;
    int x = position.x;
    return drawTabbedString(g, text, x, y, effectColor, effectType, fontType,
                            fontColor);
  }

  private int drawTabbedString(Graphics g, CharSequence text, int x, int y, Color effectColor,
                               EffectType effectType, int fontType, Color fontColor) {
    int xStart = x;

    int start = 0;

    for (int i = 0; i < text.length(); i++) {
      if (text.charAt(i) != '\t') continue;

      if (i > start) {
        drawCharsCached(g, text.subSequence(start, i), x, y, fontType, fontColor);
        for (int j = start; j < i; j++) {
          char c = text.charAt(j);
          x += charWidth(fontType, c);
        }
      }

      int x1 = nextTabStop(x);
      drawTabPlacer(g, y, x, x1);
      x = x1;
      start = i + 1;
    }

    if (start < text.length()) {
      drawCharsCached(g, text.subSequence(start, text.length()), x, y, fontType, fontColor);
      for (int j = start; j < text.length(); j++) x += charWidth(fontType, text.charAt(j));
    }

    if (effectColor != null) {
      Color savedColor = g.getColor();

      int w = getTextSegmentWidth(text, xStart, fontType);
//      myBorderEffect.flushIfCantProlong(g, this, effectType, effectColor);
      if (effectType == EffectType.LINE_UNDERSCORE) {
        g.setColor(effectColor);
        g.drawLine(xStart, y + 1, xStart + w, y + 1);
        g.setColor(savedColor);
      }
      else {
        if (effectType == EffectType.STRIKEOUT) {
          g.setColor(effectColor);
          int y1 = y - getCharHeight() / 2;
          g.drawLine(xStart, y1, xStart + w, y1);
          g.setColor(savedColor);
        }
        else {
          if (effectType == EffectType.WAVE_UNDERSCORE) {
            g.setColor(effectColor);
            drawWave(g, xStart, xStart + w, y + 1);
            g.setColor(savedColor);
          } /*else {
            if (effectType == EffectType.BOXED) {
              int yStart = y - getLineHeight() + getDescent();
              if (!myBorderEffect.isDetached()) myBorderEffect.start(effectColor, start);
              myBorderEffect.setEndOffset(offset + length);
            }
          }*/
        }
      }
    } /*else myBorderEffect.flushBorder(g, this);*/

    return x;
  }

  public int charWidth(int fontType, char c) {
    switch(fontType) {
      case Font.PLAIN: return myPlainCache.charWidth(c);
      case Font.BOLD: return myBoldCache.charWidth(c);
      case Font.ITALIC: return myItalicCache.charWidth(c);
      case Font.BOLD | Font.ITALIC: return myBoldItalicCache.charWidth(c);
    }

    return 0;
  }

  private void drawTabPlacer(Graphics g, int y, int start, int stop) {
    if (mySettings.isWhitespacesShown()) {
      stop -= g.getFontMetrics().charWidth(' ') / 2;
      Color oldColor = g.getColor();
      g.setColor(myScheme.getColor(EditorColors.WHITESPACES_COLOR));
      final int charHeight = getCharHeight();
      final int halfCharHeight = charHeight / 2;
      int mid = y - halfCharHeight;
      int top = y - charHeight;
      g.drawLine(start, mid, stop, mid);
      g.drawLine(stop, y, stop, top);
      g.fillPolygon(new int[]{stop - halfCharHeight, stop - halfCharHeight, stop},
                    new int[]{y, y - charHeight, y - halfCharHeight}, 3);
      g.setColor(oldColor);
    }
  }

  private void drawCharsCached(Graphics g,
                               CharSequence data,
                               int x,
                               int y,
                               int fontType,
                               Color color) {
    if (!SystemInfo.isMac) {
      if (fontType == myCurrentFontType) {
        drawChars(g, data, x, y);
      }
      else if (fontType == Font.PLAIN) {
        myPlainCache.addContent(g, data, x, y, color);
      }
      else if (fontType == Font.BOLD) {
        myBoldCache.addContent(g, data, x, y, color);
      }
      else if (fontType == Font.ITALIC) {
        myItalicCache.addContent(g, data, x, y, color);
      }
      else {
        myBoldItalicCache.addContent(g, data, x, y, color);
      }
    }
    else {
      if (fontType != myCurrentFontType) {
        myCurrentFontType = fontType;
        g.setFont(getFont(fontType));
      }

      drawChars(g, data, x, y);
    }
  }

  private void drawChars(Graphics g, CharSequence data, int x, int y) {
    g.drawString(data.toString(), x, y);

    if (mySettings.isWhitespacesShown()) {
      Color oldColor = g.getColor();
      g.setColor(myScheme.getColor(EditorColors.WHITESPACES_COLOR));
      final FontMetrics metrics = g.getFontMetrics();
      int halfSpaceWidth = metrics.charWidth(' ') / 2;
      for (int i = 0; i < data.length(); i++) {
        if (data.charAt(i) == ' ') {
          g.fillRect(x + halfSpaceWidth, y, 1, 1);
        }
        x += metrics.charWidth(data.charAt(i));
      }
      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;
    g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y);
  }

  private static void drawWaveSegment(Graphics g, int x, int y) {
    g.drawLine(x, y + WAVE_HEIGHT, x + WAVE_SEGMENT_LENGTH / 2, y);
    g.drawLine(x + WAVE_SEGMENT_LENGTH / 2, y, x + WAVE_SEGMENT_LENGTH, y + WAVE_HEIGHT);
  }

  private int getTextSegmentWidth(CharSequence text, int xStart, int fontType) {
    int start = 0;
    int x = xStart;

    for (int i = 0; i < text.length(); i++) {
      if (text.charAt(i) != '\t') continue;

      if (i > start) {
        for (int j = start; j < i; j++) x += charWidth(fontType, text.charAt(j));
      }
      x = nextTabStop(x);
      start = i + 1;
    }

    if (start < text.length()) {
      for (int j = start; j < text.length(); j++) x += charWidth(fontType, text.charAt(j));
    }

    return x - xStart;
  }

  public int getLineHeight() {
    if (myLineHeight != -1) return myLineHeight;

    ThreadingAssertions.assertEventDispatchThread();

    FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN));
    myLineHeight = (int)(fontMetrics.getHeight() * (isOneLineMode() ? 1 : myScheme.getLineSpacing()));
    if (myLineHeight == 0) {
      myLineHeight = fontMetrics.getHeight();
      if (myLineHeight == 0) {
        myLineHeight = 12;
      }
    }

    return myLineHeight;
  }

  int getDescent() {
    if (myDescent != -1) {
      return myDescent;
    }
    FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN));
    myDescent = fontMetrics.getDescent();
    return myDescent;
  }

  public FontMetrics getFontMetrics(int fontType) {
    if (myPlainFontMetrics == null) {
      ThreadingAssertions.assertEventDispatchThread();
      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.assertTrue(false, "Unknown font type: " + fontType);

    return myPlainFontMetrics;
  }

  private int getCharHeight() {
    if (myCharHeight == -1) {
      ThreadingAssertions.assertEventDispatchThread();
      FontMetrics fontMetrics = myEditorComponent.getFontMetrics(myScheme.getFont(EditorFontType.PLAIN));
      myCharHeight = fontMetrics.charWidth('a');
    }
    return myCharHeight;
  }

  public Dimension getPreferredSize() {
    Dimension draft = getSizeWithoutCaret();
    int caretX = visualPositionToXY(getCaretModel().getVisualPosition()).x;
    draft.width = Math.max(caretX, draft.width) + mySettings.getAdditionalColumnsCount() * getSpaceWidth(Font.PLAIN);
    return draft;
  }

  private Dimension getSizeWithoutCaret() {
    Dimension size = mySizeContainer.getContentSize();
    if (isOneLineMode()) return new Dimension(size.width, getLineHeight());
    if (mySettings.isAdditionalPageAtBottom()) {
      int lineHeight = getLineHeight();
      return new Dimension(size.width,
                           size.height +
                           Math.max(getScrollingModel().getVisibleArea().height - 2 * lineHeight, lineHeight));
    }

    return getContentSize();
  }

  public Dimension getContentSize() {
    Dimension size = mySizeContainer.getContentSize();
    return new Dimension(size.width, size.height + mySettings.getAdditionalLinesCount() * getLineHeight());
  }

  public JScrollPane getScrollPane() {
    return myScrollPane;
  }

  public int logicalPositionToOffset(LogicalPosition pos) {
    ThreadingAssertions.assertEventDispatchThread();
    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);
    int end = myDocument.getLineEndOffset(pos.line);

    CharSequence text = myDocument.getCharsNoThreadCheck();

    if (pos.column == 0) return start;
    return EditorUtil.calcOffset(this, text, start, end, pos.column, mySettings.getTabSize(myProject));
  }

  public void setLastColumnNumber(int val) {
    ThreadingAssertions.assertEventDispatchThread();
    myLastColumnNumber = val;
  }

  public int getLastColumnNumber() {
    ThreadingAssertions.assertEventDispatchThread();
    return myLastColumnNumber;
  }

  int getVisibleLineCount() {
    int line = getDocument().getLineCount();
    line -= myFoldingModel.getFoldedLinesCountBefore(getDocument().getTextLength() + 1);
    return line;
  }

  public VisualPosition logicalToVisualPosition(LogicalPosition logicalPos) {
    ThreadingAssertions.assertEventDispatchThread();
    if (!myFoldingModel.isFoldingEnabled()) 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() - 1) {
        offset = outermostCollapsed.getStartOffset();
        LogicalPosition foldStart = offsetToLogicalPosition(offset);
        return logicalToVisualPosition(foldStart);
      }
      else {
        offset = outermostCollapsed.getEndOffset() + 3;
      }
    }

    int line = logicalPos.line;
    int column = logicalPos.column;

    line -= myFoldingModel.getFoldedLinesCountBefore(offset);

    FoldRegion[] toplevel = myFoldingModel.fetchTopLevel();
    for (int idx = myFoldingModel.getLastTopLevelIndexBefore(offset); idx >= 0; idx--) {
      FoldRegion region = toplevel[idx];
      if (region.isValid()) {
        if (region.getDocument().getLineNumber(region.getEndOffset()) == logicalPos.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();
          logicalPos = foldStart;
        }
        else {
          break;
        }
      }
    }

    LOG.assertTrue(line >= 0);

    return new VisualPosition(line, Math.max(0, column));
  }

  private FoldRegion getLastCollapsedBeforePosition(VisualPosition visual) {
    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];
      LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1);
      VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd);
      if (visFoldEnd.line < visual.line) {
        start = i + 1;
      }
      else {
        if (visFoldEnd.line > visual.line) {
          end = i - 1;
        }
        else {
          if (visFoldEnd.column < visual.column) {
            start = i + 1;
          }
          else {
            if (visFoldEnd.column > visual.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];
      LogicalPosition logFoldEnd = offsetToLogicalPosition(region.getEndOffset() - 1);
      VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd);
      if (visFoldEnd.line > visual.line || visFoldEnd.line == visual.line && visFoldEnd.column > visual.column) {
        i--;
        if (i >= 0) {
          return topLevelCollapsed[i];
        }
        else {
          return null;
        }
      }
      return region;
    }

    return null;
  }

  public LogicalPosition visualToLogicalPosition(VisualPosition visiblePos) {
    ThreadingAssertions.assertEventDispatchThread();
    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) {
      LogicalPosition logFoldEnd = offsetToLogicalPosition(lastCollapsedBefore.getEndOffset());
      VisualPosition visFoldEnd = logicalToVisualPosition(logFoldEnd);

      line = logFoldEnd.line + (visiblePos.line - visFoldEnd.line);
      if (visFoldEnd.line == visiblePos.line) {
        if (visiblePos.column >= visFoldEnd.column) {
          column = logFoldEnd.column + (visiblePos.column - visFoldEnd.column);
        }
        else {
          return offsetToLogicalPosition(lastCollapsedBefore.getStartOffset());
        }
      }
    }

    if (column < 0) column = 0;

    return new LogicalPosition(line, column);
  }

  private int calcLogicalLineNumber(int offset) {
    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());

    return lineIndex;
  }

  private int calcColumnNumber(int offset, int lineIndex) {
    if (myDocument.getTextLength() == 0) return 0;

    CharSequence text = myDocument.getCharsSequence();
    int start = myDocument.getLineStartOffset(lineIndex);
    if (start == offset) return 0;
    return EditorUtil.calcColumnNumber(this, text, start, offset, mySettings.getTabSize(myProject));
  }

  private void moveCaretToScreenPos(int x, int y) {
    if (x < 0) {
      x = 0;
    }

    LogicalPosition pos = xyToLogicalPosition(new Point(x, y));

    int columnNumber = pos.column;
    int lineNumber = pos.line;

    if (lineNumber >= myDocument.getLineCount()) {
      lineNumber = myDocument.getLineCount() - 1;
    }
    if (!mySettings.isVirtualSpace()) {
      if (lineNumber >= 0) {
        int lineEndOffset = myDocument.getLineEndOffset(lineNumber);
        int lineEndColumnNumber = calcColumnNumber(lineEndOffset, lineNumber);
        if (columnNumber > lineEndColumnNumber) {
          columnNumber = lineEndColumnNumber;
        }
      }
    }
    if (lineNumber < 0) {
      lineNumber = 0;
      columnNumber = 0;
    }
    if (!mySettings.isCaretInsideTabs()) {
      int offset = logicalPositionToOffset(new LogicalPosition(lineNumber, columnNumber));
      CharSequence text = myDocument.getCharsSequence();
      if (offset >= 0 && offset < myDocument.getTextLength()) {
        if (text.charAt(offset) == '\t') {
          columnNumber = calcColumnNumber(offset, lineNumber);
        }
      }
    }
    LogicalPosition pos1 = new LogicalPosition(lineNumber, columnNumber);
    getCaretModel().moveToLogicalPosition(pos1);
  }

  private void runMousePressedCommand(final MouseEvent e) {
    myMousePressedEvent = e;
    EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e));

    EditorMouseListener[] mouseListeners = myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]);
    for (int i = 0; i < mouseListeners.length; i++) {
      mouseListeners[i].mousePressed(event);
    }

    // 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() {
        public void run() {
          processMousePressed(e);
        }
      };
      myCommandProcessor.executeCommand(myProject, runnable, "", null);
    }
    else {
      processMousePressed(e);
    }
  }

  private void runMouseClickedCommand(final MouseEvent e) {
    EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e));
    EditorMouseListener[] listeners = myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]);
    for (int i = 0; i < listeners.length; i++) {
      listeners[i].mouseClicked(event);
      if (event.isConsumed()) {
        e.consume();
        return;
      }
    }
  }

  private void runMouseReleasedCommand(final MouseEvent e) {
    myScrollingTimer.stop();
    EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e));
    EditorMouseListener[] listeners = myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]);
    for (int i = 0; i < listeners.length; i++) {
      listeners[i].mouseReleased(event);
      if (event.isConsumed()) {
        e.consume();
        return;
      }
    }

    if (myCommandProcessor != null) {
      Runnable runnable = new Runnable() {
        public void run() {
          processMouseReleased(e);
        }
      };
      myCommandProcessor.executeCommand(myProject, runnable, "", null);
    }
    else {
      processMouseReleased(e);
    }
  }

  private void runMouseEnteredCommand(MouseEvent e) {
    EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e));
    EditorMouseListener[] listeners = myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]);
    for (int i = 0; i < listeners.length; i++) {
      listeners[i].mouseEntered(event);
      if (event.isConsumed()) {
        e.consume();
        return;
      }
    }
  }

  private void runMouseExitedCommand(MouseEvent e) {
    EditorMouseEvent event = new EditorMouseEvent(this, e, getMouseEventArea(e));
    EditorMouseListener[] listeners = myMouseListeners.toArray(new EditorMouseListener[myMouseListeners.size()]);
    for (int i = 0; i < listeners.length; i++) {
      listeners[i].mouseExited(event);
      if (event.isConsumed()) {
        e.consume();
        return;
      }
    }
  }

  private void processMousePressed(MouseEvent e) {
    if (myMouseSelectionState != MOUSE_SELECTION_STATE_NONE &&
        System.currentTimeMillis() - myMouseSelectionChangeTimestamp > 1000) {
      setMouseSelectionState(MOUSE_SELECTION_STATE_NONE);
    }

    int x = e.getX();
    int y = e.getY();

    if (x < 0) x = 0;
    if (y < 0) y = 0;

    final EditorMouseEventArea eventArea = getMouseEventArea(e);
    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() {
          public void run() {
            myFoldingModel.flushCaretShift();
            range.setExpanded(expansion);
          }
        };
        getFoldingModel().runBatchFoldingOperation(processor);
        y = myGutterComponent.getHeadCenterY(range);
        getScrollingModel().scrollVertically(y - scrollShift);
        return;
      }
    }

    if (e.getSource() == myGutterComponent) {
      if (eventArea == EditorMouseEventArea.LINE_MARKERS_AREA ||
          eventArea == EditorMouseEventArea.ANNOTATIONS_AREA) {
        myGutterComponent.mousePressed(e);
        if (e.isConsumed()) return;
      }
      x = 0;
    }

    int oldSelectionStart = mySelectionModel.getLeadSelectionOffset();
    moveCaretToScreenPos(x, y);

    if (e.isPopupTrigger()) return;

    requestFocus();

    int caretOffset = getCaretModel().getOffset();

    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;
    }

    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 {
        mySelectionModel.setSelection(oldSelectionStart, caretOffset);
      }
    }
    else {
      if (!myMousePressedInsideSelection && getSelectionModel().hasSelection()) {
        setMouseSelectionState(MOUSE_SELECTION_STATE_NONE);
        mySelectionModel.setSelection(caretOffset, caretOffset);
      }
      else {
        if (!e.isPopupTrigger()) {
          switch (e.getClickCount()) {
            case 2:
              mySelectionModel.selectWordAtCaret(mySettings.isMouseClickSelectionHonorsCamelWords());
              setMouseSelectionState(MOUSE_SELECTION_STATE_WORD_SELECTED);
              mySavedSelectionStart = mySelectionModel.getSelectionStart();
              mySavedSelectionEnd = mySelectionModel.getSelectionEnd();
              getCaretModel().moveToOffset(mySavedSelectionEnd);
              break;

            case 3:
              mySelectionModel.selectLineAtCaret();
              setMouseSelectionState(MOUSE_SELECTION_STATE_LINE_SELECTED);
              mySavedSelectionStart = mySelectionModel.getSelectionStart();
              mySavedSelectionEnd = mySelectionModel.getSelectionEnd();
              break;
          }
        }
      }
    }
  }

  private static boolean isControlKeyDown(MouseEvent mouseEvent) {
    return SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown();
  }

  private void processMouseReleased(MouseEvent e) {
    if (e.getSource() == myGutterComponent) {
      myGutterComponent.mouseReleased(e);
    }

    if (getMouseEventArea(e) != EditorMouseEventArea.EDITING_AREA || e.getY() < 0 || e.getX() < 0) {
      return;
    }

//    if (myMousePressedInsideSelection) getSelectionModel().removeSelection();
    final FoldRegion region = ((FoldingModelEx)getFoldingModel()).getFoldingPlaceholderAt(e.getPoint());
    if (e.getX() >= 0 && e.getY() >= 0 && region != null && region == myMouseSelectedRegion) {
      getFoldingModel().runBatchFoldingOperation(new Runnable() {
        public void run() {
          myFoldingModel.flushCaretShift();
          region.setExpanded(true);
        }
      });
    }

    if (myMousePressedEvent != null && myMousePressedEvent.getClickCount() == 1 && myMousePressedInsideSelection) {
      getSelectionModel().removeSelection();
    }
  }

  public DataContext getDataContext() {
    return getProjectAwareDataContext(DataManager.getInstance().getDataContext(getContentComponent()));
  }

  public DataContext getProjectAwareDataContext(final DataContext original) {
    if (original.getData(DataConstants.PROJECT) == myProject) return original;

    return new DataContext() {
      public Object getData(String dataId) {
        if (DataConstants.PROJECT.equals(dataId)) {
          return myProject;
        }
        return original.getData(dataId);
      }
    };
  }


  public EditorMouseEventArea getMouseEventArea(MouseEvent e) {
    if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA;

    int x = myGutterComponent.convertX(e.getX());

    if (x >= myGutterComponent.getLineNumberAreaOffset() &&
        x < myGutterComponent.getLineNumberAreaOffset() + myGutterComponent.getLineNumberAreaWidth()) {
      return EditorMouseEventArea.LINE_NUMBERS_AREA;
    }

    if (x >= myGutterComponent.getAnnotationsAreaOffset() &&
        x <= myGutterComponent.getAnnotationsAreaOffset() + myGutterComponent.getAnnotationsAreaWidth()) {
      return EditorMouseEventArea.ANNOTATIONS_AREA;
    }

    if (x >= myGutterComponent.getLineMarkerAreaOffset() &&
        x < myGutterComponent.getLineMarkerAreaOffset() + myGutterComponent.getLineMarkerAreaWidth()) {
      return EditorMouseEventArea.LINE_MARKERS_AREA;
    }

    if (x >= myGutterComponent.getFoldingAreaOffset() &&
        x < myGutterComponent.getFoldingAreaOffset() + myGutterComponent.getFoldingAreaWidth()) {
      return EditorMouseEventArea.FOLDING_OUTLINE_AREA;
    }

    return null;
  }

  private void requestFocus() {
    myEditorComponent.requestFocus();
  }

  private void validateMousePointer(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() & (MouseEvent.BUTTON1_DOWN_MASK | MouseEvent.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(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
    }
  }

  private void runMouseDraggedCommand(final MouseEvent e) {
    if (myCommandProcessor == null || myMousePressedEvent != null && myMousePressedEvent.isConsumed()) {
      return;
    }
    myCommandProcessor.executeCommand(myProject, new Runnable() {
      public void run() {
        processMouseDragged(e);
      }
    }, "", MOUSE_DRAGGED_GROUP);
  }

  private void processMouseDragged(MouseEvent e) {
    if (SwingUtilities.isRightMouseButton(e)) {
      return;
    }
    Rectangle rect = getScrollingModel().getVisibleArea();

    int dx = 0;
    int x = e.getX();

    if (e.getSource() == myGutterComponent) {
      x = 0;
    }

    if (x < rect.x) {
      dx = x - rect.x;
    }
    else {
      if (x > rect.x + rect.width) {
        dx = x - rect.x - rect.width;
      }
    }

    int dy = 0;
    int y = e.getY();
    if (y < rect.y) {
      dy = y - rect.y;
    }
    else {
      if (y > rect.y + rect.height) {
        dy = y - rect.y - rect.height;
      }
    }
    if (dx == 0 && dy == 0) {
      myScrollingTimer.stop();

      SelectionModel selectionModel = getSelectionModel();
      int oldSelectionStart = selectionModel.getLeadSelectionOffset();
      int oldCaretOffset = getCaretModel().getOffset();
      LogicalPosition oldLogicalCaret = getCaretModel().getLogicalPosition();
      moveCaretToScreenPos(x, y);
      getScrollingModel().scrollToCaret(ScrollType.RELATIVE);

      int newCaretOffset = getCaretModel().getOffset();
      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()) {
          final LogicalPosition blockStart = selectionModel.hasBlockSelection()
                                             ? selectionModel.getBlockStart()
                                             : oldLogicalCaret;
          selectionModel.setBlockSelection(blockStart, getCaretModel().getLogicalPosition());
        }
        else {
          if (getMouseSelectionState() != MOUSE_SELECTION_STATE_NONE) {
            if (caretShift < 0) {
              int newSelection = newCaretOffset;
              if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) {
                newSelection = mySelectionModel.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 = mySelectionModel.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);
            }
            return;
          }

          if (!myMousePressedInsideSelection) {
            selectionModel.setSelection(oldSelectionStart, newCaretOffset);
          }
          else {
            if (caretShift != 0) {
              if (myMousePressedEvent != null) {
                if (mySettings.isDndEnabled()) {
                  boolean isCopy = isControlKeyDown(e) || isViewer() || !getDocument().isWritable();
                  mySavedCaretOffsetForDNDUndoHack = oldCaretOffset;
                  getContentComponent().getTransferHandler().exportAsDrag(getContentComponent(), e,
                                                                          isCopy
                                                                          ? TransferHandler.COPY
                                                                          : TransferHandler.MOVE);
                }
                else {
                  selectionModel.removeSelection();
                }
                myMousePressedEvent = null;
              }
            }
          }
        }
      }
    }
    else {
      myScrollingTimer.start(dx, dy);
    }
  }

  private static class RepaintCursorThread extends Thread {
    private long mySleepTime = 500;
    private boolean myIsBlinkCaret = true;
    private EditorImpl myEditor = null;
    private boolean isStopped = false;
    private MyRepaintRunnable myRepaintRunnable;

    public RepaintCursorThread() {
      super("EditorCaretThread");
      myRepaintRunnable = new MyRepaintRunnable();
    }

    private class MyRepaintRunnable implements Runnable {
      public void run() {
        if (myEditor != null) {
          myEditor.myCaretCursor.repaint();
        }
      }
    }

    public void setBlinkPeriod(int blinkPeriod) {
      mySleepTime = blinkPeriod > 10 ? blinkPeriod : 10;
    }

    public void setBlinkCaret(boolean value) {
      myIsBlinkCaret = value;
    }

    public synchronized void stopThread() {
      isStopped = true;
    }

    public void run() {
      while (true) {
        try {
          Thread.sleep(myIsBlinkCaret ? mySleepTime : 1000);
        }
        catch (InterruptedException e) {
        }

        synchronized (this) {
          if (isStopped) {
            break;
          }
        }

        if (myEditor == null) {
          continue;
        }
        CaretCursor activeCursor = myEditor.myCaretCursor;

        long time = System.currentTimeMillis();
        time -= activeCursor.myStartTime;

        if (time > mySleepTime) {
          boolean toRepaint = true;
          if (myIsBlinkCaret) {
            activeCursor.isVisible = !activeCursor.isVisible;
          }
          else {

            toRepaint = !activeCursor.isVisible;
            activeCursor.isVisible = true;
          }

          if (toRepaint) {
            SwingUtilities.invokeLater(myRepaintRunnable);
          }
        }
      }
    }
  }

  void updateCaretCursor() {
    if (!IJSwingUtilities.hasFocus(getContentComponent())) {
      stopOptimizedScrolling();
    }

    if (myCursorUpdater == null) {
      myCursorUpdater = new Runnable() {
        public void run() {
          if (myCursorUpdater == null) return;
          myCursorUpdater = null;

          if (getDocument().getMarkupModel(myProject) == null) return;

          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);
        }
      };
    }
  }

  public void setCaretVisible(boolean b) {
    if (b) {
      myCaretCursor.activate();
    }
    else {
      myCaretCursor.passivate();
    }
  }

  public void addFocusListener(FocusChangeListener listener) {
    myFocusListeners.add(listener);
  }

  public Project getProject() {
    return myProject;
  }

  public boolean isOneLineMode() {
    return myIsOneLineMode;
  }

  public void setOneLineMode(boolean isOneLineMode) {
    myIsOneLineMode = isOneLineMode;
    getScrollPane().setInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, null);
    reinitSettings();
  }

  public void stopOptimizedScrolling() {
    if (SystemInfo.isMac) return;
    myEditorComponent.setOpaque(false);
  }

  private void startOptimizedScrolling() {
    if (SystemInfo.isMac) return;
    myEditorComponent.setOpaque(true);
  }

  private class CaretCursor {
    private Point myLocation;
    private int myWidth;
    private boolean isVisible = true;
    private long myStartTime = 0;

    public CaretCursor() {
      myLocation = new Point(0, 0);
    }

    public void activate() {
      synchronized (ourCaretThread) {
        ourCaretThread.myEditor = EditorImpl.this;
        ourCaretThread.setBlinkCaret(mySettings.isBlinkCaret());
        ourCaretThread.setBlinkPeriod(mySettings.getCaretBlinkPeriod());
        isVisible = true;
      }
    }

    public void passivate() {
      synchronized (ourCaretThread) {
        isVisible = false;
      }
    }

    public void setPosition(Point location, int width) {
      myStartTime = System.currentTimeMillis();
      myLocation = location;
      isVisible = true;
      myWidth = Math.max(width, 2);
      repaint();
    }

    private void repaint() {
      myEditorComponent.repaintEditorComponent(myLocation.x,
                                                               myLocation.y,
                                                               myWidth,
                                                               getLineHeight());
    }

    public void paint(Graphics g) {
      if (!isVisible || !IJSwingUtilities.hasFocus(getContentComponent()) || isRendererMode()) return;

      int x = myLocation.x;
      int lineHeight = getLineHeight();
      int y = myLocation.y;

      Rectangle viewRect = getScrollingModel().getVisibleArea();
      if (x - viewRect.x < 0) {
        return;
      }


      g.setColor(myScheme.getColor(EditorColors.CARET_COLOR));
      if (!SystemInfo.isMac) {
        Color background = myScheme.getColor(EditorColors.CARET_ROW_COLOR);
        if (background == null) background = getBackroundColor();
        g.setXORMode(background != null ? background : Color.white);
      }

      if (EditorImpl.this.myIsInsertMode != mySettings.isBlockCursor()) {
        for (int i = 0; i < mySettings.getLineCursorWidth(); i++) {
          g.drawLine(x + i, y, x + i, y + lineHeight - 1);
        }
      }
      else {
        g.fillRect(x, y, myWidth, lineHeight - 1);
      }

      g.setPaintMode();
    }
  }

  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;

    public 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 = new Timer(TIMER_PERIOD,
                          new ActionListener() {
                            public void actionPerformed(ActionEvent e) {
                              myCommandProcessor.executeCommand(myProject, new Runnable() {
                                public void run() {
                                  int oldSelectionStart = mySelectionModel.getLeadSelectionOffset();
                                  LogicalPosition caretPosition = getCaretModel().getLogicalPosition();
                                  int columnNumber = caretPosition.column;
                                  xPassedCycles++;
                                  if (xPassedCycles >= myXCycles) {
                                    xPassedCycles = 0;
                                    columnNumber += myDx;
                                  }

                                  int lineNumber = caretPosition.line;
                                  yPassedCycles++;
                                  if (yPassedCycles >= myYCycles) {
                                    yPassedCycles = 0;
                                    lineNumber += myDy;
                                  }

                                  LogicalPosition pos = new LogicalPosition(lineNumber, columnNumber);
                                  getCaretModel().moveToLogicalPosition(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 = mySelectionModel.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(mySavedSelectionEnd, newSelection);
                                      getCaretModel().moveToOffset(newSelection);
                                    }
                                    else {
                                      int newSelection = newCaretOffset;
                                      if (getMouseSelectionState() == MOUSE_SELECTION_STATE_WORD_SELECTED) {
                                        newSelection = mySelectionModel.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(mySavedSelectionStart, newSelection);
                                      getCaretModel().moveToOffset(newSelection);
                                    }
                                    return;
                                  }

                                  if (mySelectionModel.hasBlockSelection()) {
                                    mySelectionModel.setBlockSelection(mySelectionModel.getBlockStart(),
                                                                       getCaretModel().getLogicalPosition());
                                  }
                                  else {
                                    mySelectionModel.setSelection(oldSelectionStart, getCaretModel().getOffset());
                                  }
                                }
                              },
                                                                "Move Cursor", null);
                            }
                          });
      myTimer.start();
    }

    public void stop() {
      if (myTimer != null) {
        myTimer.stop();
        myTimer = null;
      }
    }

  }

  class MyScrollBar extends JScrollBar {

    public MyScrollBar(int orientation) {
      super(orientation);
    }

    /**
     * This is helper method. It returns height of the top (descrease) scrollbar
     * button. Please note, that it's possible to return real height only if scrollbar
     * is instance of BasicScrollBarUI. Otherwise it returns fake (but good enough :) )
     * value.
     */
    int getDecScrollButtonHeight() {
      ScrollBarUI ui = getUI();
      Insets insets = getInsets();
      if (ui instanceof BasicScrollBarUI) {
        try {
          Field decrButtonField = BasicScrollBarUI.class.getDeclaredField("decrButton");
          decrButtonField.setAccessible(true);
          JButton decrButtonValue = (JButton)decrButtonField.get(ui);
          LOG.assertTrue(decrButtonValue != null);
          return insets.top + decrButtonValue.getHeight();
        }
        catch (Exception exc) {
          exc.printStackTrace();
          throw new IllegalStateException(exc.getMessage());
        }
      }
      else {
        return insets.top + 15;
      }
    }

    /**
     * This is helper method. It returns height of the bottom (increase) scrollbar
     * button. Please note, that it's possible to return real height only if scrollbar
     * is instance of BasicScrollBarUI. Otherwise it returns fake (but good enough :) )
     * value.
     */
    int getIncScrollButtonHeight() {
      ScrollBarUI ui = getUI();
      Insets insets = getInsets();
      if (ui instanceof BasicScrollBarUI) {
        try {
          Field incrButtonField = BasicScrollBarUI.class.getDeclaredField("incrButton");
          incrButtonField.setAccessible(true);
          JButton incrButtonValue = (JButton)incrButtonField.get(ui);
          LOG.assertTrue(incrButtonValue != null);
          return insets.bottom + incrButtonValue.getHeight();
        }
        catch (Exception exc) {
          exc.printStackTrace();
          throw new IllegalStateException(exc.getMessage());
        }
      }
      else if ("apple.laf.AquaScrollBarUI".equals(ui.getClass().getName())) {
        return insets.bottom + 30;
      }
      else {
        return insets.bottom + 15;
      }
    }

    public int getUnitIncrement(int direction) {
      JViewport vp = myScrollPane.getViewport();
      Rectangle vr = vp.getViewRect();
      return myEditorComponent.getScrollableUnitIncrement(vr, SwingConstants.VERTICAL, direction);
    }

    public int getBlockIncrement(int direction) {
      JViewport vp = myScrollPane.getViewport();
      Rectangle vr = vp.getViewRect();
      return myEditorComponent.getScrollableBlockIncrement(vr, SwingConstants.VERTICAL, direction);
    }
  }

  private MyEditable getViewer() {
    if (myEditable == null) {
      myEditable = new MyEditable();
    }
    return myEditable;
  }

  public CopyProvider getCopyProvider() {
    return getViewer();
  }

  public CutProvider getCutProvider() {
    return getViewer();
  }

  public PasteProvider getPasteProvider() {

    return getViewer();
  }

  public DeleteProvider getDeleteProvider() {
    return getViewer();
  }

  private class MyEditable implements CutProvider, CopyProvider, PasteProvider, DeleteProvider {
    public void performCopy(DataContext dataContext) {
      executeAction(IdeActions.ACTION_EDITOR_COPY, dataContext);
    }

    public boolean isCopyEnabled(DataContext dataContext) {
      return true;
    }

    public void performCut(DataContext dataContext) {
      executeAction(IdeActions.ACTION_EDITOR_CUT, dataContext);
    }

    public boolean isCutEnabled(DataContext dataContext) {
      return !isViewer() && getDocument().isWritable();
    }

    public void performPaste(DataContext dataContext) {
      executeAction(IdeActions.ACTION_EDITOR_PASTE, dataContext);
    }

    public boolean isPastePossible(DataContext dataContext) {
      // Copy of isPasteEnabled. See interface method javadoc.
      return !isViewer() && getDocument().isWritable();
    }

    public boolean isPasteEnabled(DataContext dataContext) {
      return !isViewer() && getDocument().isWritable();
    }

    public void deleteElement(DataContext dataContext) {
      executeAction(IdeActions.ACTION_EDITOR_DELETE, dataContext);
    }

    public boolean canDeleteElement(DataContext dataContext) {
      return !isViewer() && getDocument().isWritable();
    }

    private void executeAction(String actionId, DataContext dataContext) {
      EditorAction action = (EditorAction)ActionManager.getInstance().getAction(actionId);
      if (action != null) {
        action.actionPerformed(EditorImpl.this, dataContext);
      }
    }
  }

  public void setColorsScheme(EditorColorsScheme scheme) {
    ThreadingAssertions.assertEventDispatchThread();
    myScheme = scheme;
    reinitSettings();
  }

  public EditorColorsScheme getColorsScheme() {
    ThreadingAssertions.assertEventDispatchThread();
    return myScheme;
  }

  public void setVerticalScrollbarOrientation(int type) {
    ThreadingAssertions.assertEventDispatchThread();
    int currentHorOffset = myScrollingModel.getHorizontalScrollOffset();
    myScrollbarOrientation = type;
    if (type == EditorEx.VERTICAL_SCROLLBAR_LEFT) {
      myScrollPane.setLayout(new LeftHandScrollbarLayout());
    }
    else {
      myScrollPane.setLayout(new ScrollPaneLayout());
    }
    myScrollingModel.scrollHorizontally(currentHorOffset);
  }

  public void setVerticalScrollbarVisible(boolean b) {
    if (b) {
      myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
    }
    else {
      myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
    }
  }

  public void setHorizontalScrollbarVisible(boolean b) {
    if (b) {
      myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    }
    else {
      myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
    }
  }

  public int getVerticalScrollbarOrientation() {
    return myScrollbarOrientation;
  }

  public MyScrollBar getVerticalScrollBar() {
    return myVerticalScrollBar;
  }

  public JPanel getPanel() {
    return myPanel;
  }

  private int getMouseSelectionState() {
    return myMouseSelectionState;
  }

  private void setMouseSelectionState(int mouseSelectionState) {
    myMouseSelectionState = mouseSelectionState;
    myMouseSelectionChangeTimestamp = System.currentTimeMillis();
  }


  public void replaceInputMethodText(InputMethodEvent e) {
    getInputMethodRequests();
    myInputMethodRequestsHandler.replaceInputMethodText(e);
  }

  public void inputMethodCaretPositionChanged(InputMethodEvent e) {
    getInputMethodRequests();
    myInputMethodRequestsHandler.setInputMethodCaretPosition(e);
  }

  public InputMethodRequests getInputMethodRequests() {
    if (myInputMethodRequestsHandler == null) {
      myInputMethodRequestsHandler = new MyInputMethodHandler();
      myInputMethodRequestsSwingWrapper = new MyInputMethodHandleSwingThreadWrapper(myInputMethodRequestsHandler);
    }
    return myInputMethodRequestsSwingWrapper;
  }

  public boolean processKeyTyped(KeyEvent e) {
    if (e.getID() != KeyEvent.KEY_TYPED) return false;
    char c = e.getKeyChar();
    if (isReallyTypedEvent(e) && c >= 0x20 && c != 0x7F) { // Hack just like in javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction
      processKeyTyped(c);
      return true;
    }
    else {
      return false;
    }
  }

  private static boolean isReallyTypedEvent(KeyEvent e) {
    int modifiers = e.getModifiers();
    if (SystemInfo.isMac) {
      return !e.isMetaDown() && !e.isControlDown();
    }

    return (modifiers & ActionEvent.ALT_MASK) == (modifiers & ActionEvent.CTRL_MASK);
  }

  public void beforeModalityStateChanged() {
    myScrollingModel.beforeModalityStateChanged();
  }

  private static class MyInputMethodHandleSwingThreadWrapper implements InputMethodRequests {
    private InputMethodRequests myDelegate;

    public MyInputMethodHandleSwingThreadWrapper(InputMethodRequests delegate) {
      myDelegate = delegate;
    }

    public Rectangle getTextLocation(final TextHitInfo offset) {
      if (ApplicationManager.getApplication().isDispatchThread()) return myDelegate.getTextLocation(offset);

      final Rectangle[] r = new Rectangle[1];
      try {
        SwingUtilities.invokeAndWait(new Runnable() {
          public void run() {
            r[0] = myDelegate.getTextLocation(offset);
          }
        });
      }
      catch (InterruptedException e) {
        LOG.error(e);
      }
      catch (InvocationTargetException e) {
        LOG.error(e);
      }
      return r[0];
    }

    public TextHitInfo getLocationOffset(final int x, final int y) {
      if (ApplicationManager.getApplication().isDispatchThread()) return myDelegate.getLocationOffset(x, y);

      final TextHitInfo[] r = new TextHitInfo[1];
      try {
        SwingUtilities.invokeAndWait(new Runnable() {
          public void run() {
            r[0] = myDelegate.getLocationOffset(x, y);
          }
        });
      }
      catch (InterruptedException e) {
        LOG.error(e);
      }
      catch (InvocationTargetException e) {
        LOG.error(e);
      }
      return r[0];
    }

    public int getInsertPositionOffset() {
      if (ApplicationManager.getApplication().isDispatchThread()) return myDelegate.getInsertPositionOffset();

      final int[] r = new int[1];
      try {
        SwingUtilities.invokeAndWait(new Runnable() {
          public void run() {
            r[0] = myDelegate.getInsertPositionOffset();
          }
        });
      }
      catch (InterruptedException e) {
        LOG.error(e);
      }
      catch (InvocationTargetException e) {
        LOG.error(e);
      }
      return r[0];
    }

    public AttributedCharacterIterator getCommittedText(final int beginIndex, final int endIndex,
                                                        final AttributedCharacterIterator.Attribute[] attributes) {
      if (ApplicationManager.getApplication().isDispatchThread()) {
        return myDelegate.getCommittedText(beginIndex,
                                           endIndex,
                                           attributes);
      }
      final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1];
      try {
        SwingUtilities.invokeAndWait(new Runnable() {
          public void run() {
            r[0] = myDelegate.getCommittedText(beginIndex, endIndex, attributes);
          }
        });
      }
      catch (InterruptedException e) {
        LOG.error(e);
      }
      catch (InvocationTargetException e) {
        LOG.error(e);
      }
      return r[0];
    }

    public int getCommittedTextLength() {
      if (ApplicationManager.getApplication().isDispatchThread()) return myDelegate.getCommittedTextLength();
      final int[] r = new int[1];
      try {
        SwingUtilities.invokeAndWait(new Runnable() {
          public void run() {
            r[0] = myDelegate.getCommittedTextLength();
          }
        });
      }
      catch (InterruptedException e) {
        LOG.error(e);
      }
      catch (InvocationTargetException e) {
        LOG.error(e);
      }
      return r[0];
    }

    public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) {
      return null;
    }

    public AttributedCharacterIterator getSelectedText(final AttributedCharacterIterator.Attribute[] attributes) {
      if (ApplicationManager.getApplication().isDispatchThread()) return myDelegate.getSelectedText(attributes);

      final AttributedCharacterIterator[] r = new AttributedCharacterIterator[1];
      try {
        SwingUtilities.invokeAndWait(new Runnable() {
          public void run() {
            r[0] = myDelegate.getSelectedText(attributes);
          }
        });
      }
      catch (InterruptedException e) {
        LOG.error(e);
      }
      catch (InvocationTargetException e) {
        LOG.error(e);
      }
      return r[0];
    }
  }

  private class MyInputMethodHandler implements InputMethodRequests {
    private String composedText;
    private int composedTextStart;
    private int composedTextEnd;

    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;
    }

    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 (pos >= composedTextStart && pos <= composedTextEnd) {
          return TextHitInfo.leading(pos - composedTextStart);
        }
      }
      return null;
    }

    public int getInsertPositionOffset() {
      int composedStartIndex = 0;
      int composedEndIndex = 0;
      if (composedText != null) {
        composedStartIndex = composedTextStart;
        composedEndIndex = composedTextEnd;
      }

      int caretIndex = getCaretModel().getOffset();

      if (caretIndex < composedStartIndex) {
        return caretIndex;
      }
      else {
        if (caretIndex < composedEndIndex) {
          return composedStartIndex;
        }
        else {
          return caretIndex - (composedEndIndex - composedStartIndex);
        }
      }
    }

    private String getText(int startIdx, int endIdx) {
      CharSequence chars = getDocument().getCharsSequence();
      return chars.subSequence(startIdx, endIdx).toString();
    }

    public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex,
                                                        AttributedCharacterIterator.Attribute[] attributes) {
      int composedStartIndex = 0;
      int composedEndIndex = 0;
      if (composedText != null) {
        composedStartIndex = composedTextStart;
        composedEndIndex = composedTextEnd;
      }

      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();
    }

    public int getCommittedTextLength() {
      int length = getDocument().getTextLength();
      if (composedText != null) {
        length -= composedText.length();
      }
      return length;
    }

    public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) {
      return null;
    }

    public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) {
      String text = getSelectionModel().getSelectedText();
      return text == null ? null : new AttributedString(text).getIterator();
    }

    private void createComposedString(int composedIndex, 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(InputMethodEvent e) {
      if (composedText != null) {
        int dot;
        dot = composedTextStart;

        TextHitInfo caretPos = e.getCaret();
        if (caretPos != null) {
          int index = caretPos.getInsertionIndex();
          dot += index;
        }

        getCaretModel().moveToOffset(dot);
        getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
      }
    }

    private void runUndoTransparent(final Runnable runnable) {
      CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() {
        public void run() {
          CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
            public void run() {
              ApplicationManager.getApplication().runWriteAction(runnable);
            }
          }, "", null);
        }
      });
    }

    private void replaceInputMethodText(InputMethodEvent e) {
      int commitCount = e.getCommittedCharacterCount();
      AttributedCharacterIterator text = e.getText();
      int composedTextIndex;

      // old composed text deletion
      final Document doc = getDocument();

      if (composedText != null) {
        if (!isViewer() && doc.isWritable()) {
          runUndoTransparent(new Runnable() {
            public void run() {
              doc.deleteString(Math.max(0, composedTextStart), Math.min(composedTextEnd, doc.getTextLength()));
            }
          });
        }
        composedText = null;
      }

      if (text != null) {
        text.first();

        // committed text insertion
        if (commitCount > 0) {
          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()) {
          composedTextIndex = text.getIndex();
          if (composedTextIndex < text.getEndIndex()) {
            createComposedString(composedTextIndex, text);

            runUndoTransparent(new Runnable() {
              public void run() {
                EditorModificationUtil.insertStringAtCaret(EditorImpl.this, composedText, false, false);
              }
            });

            composedTextStart = getCaretModel().getOffset();
            composedTextEnd = getCaretModel().getOffset() + composedText.length();
          }
        }
      }
    }
  }

  private class MyMouseAdapter extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      runMousePressedCommand(e);
    }

    public void mouseReleased(MouseEvent e) {
      runMouseReleasedCommand(e);
      if (!e.isConsumed() && myMousePressedEvent != null && !myMousePressedEvent.isConsumed() &&
          Math.abs(e.getX() - myMousePressedEvent.getX()) < getSpaceWidth(Font.PLAIN) &&
          Math.abs(e.getY() - myMousePressedEvent.getY()) < getLineHeight()) {
        runMouseClickedCommand(e);
      }
      myMousePressedEvent = null;
    }

    public void mouseEntered(MouseEvent e) {
      runMouseEnteredCommand(e);
    }

    public void mouseExited(MouseEvent e) {
      runMouseExitedCommand(e);
      EditorMouseEvent event = new EditorMouseEvent(EditorImpl.this, e, getMouseEventArea(e));
      if (event.getArea() == EditorMouseEventArea.LINE_MARKERS_AREA) {
        myGutterComponent.mouseExited(e);
      }

      HintManager.getInstance().getTooltipController().cancelTooltip(FOLDING_TOOLTIP_GROUP);
    }
  }

  private static final TooltipGroup FOLDING_TOOLTIP_GROUP = new TooltipGroup("FOLDING_TOOLTIP_GROUP", 10);

  private class MyMouseMotionListener implements MouseMotionListener {
    public void mouseDragged(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);
      }

      EditorMouseMotionListener[] listeners = myMouseMotionListeners.toArray(
        new EditorMouseMotionListener[myMouseMotionListeners.size()]);
      for (int i = 0; i < listeners.length; i++) {
        listeners[i].mouseDragged(event);
      }
    }

    public void mouseMoved(MouseEvent e) {
      validateMousePointer(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 = HintManager.getInstance().getTooltipController();
        if (fold != null) {
          DocumentFragment range = new DocumentFragment(myDocument, fold.getStartOffset(), fold.getEndOffset());
          final Point p = SwingUtilities.convertPoint((Component)e.getSource(),
                                                      e.getPoint(),
                                                      getComponent().getRootPane().getLayeredPane());
          controller.showTooltip(EditorImpl.this, p, range, false, FOLDING_TOOLTIP_GROUP);
        }
        else {
          controller.cancelTooltip(FOLDING_TOOLTIP_GROUP);
        }
      }

      EditorMouseMotionListener[] listeners = myMouseMotionListeners.toArray(
        new EditorMouseMotionListener[myMouseMotionListeners.size()]);
      for (int i = 0; i < listeners.length; i++) {
        listeners[i].mouseMoved(event);
      }
    }
  }

  private class MyColorSchemeDelegate implements EditorColorsScheme {
    private HashMap<TextAttributesKey, TextAttributes> myOwnAttributes = new HashMap<TextAttributesKey, TextAttributes>();
    private HashMap<ColorKey, Color> myOwnColors = new HashMap<ColorKey, Color>();
    private HashMap<EditorFontType, Font> myFontsMap = null;
    private Integer myFontSize = null;
    private String myFaceName = null;


    private EditorColorsScheme getGlobal() {
      return EditorColorsManager.getInstance().getGlobalScheme();
    }

    public String getName() {
      return getGlobal().getName();
    }

    protected void initFonts() {
      String editorFontName = getEditorFontName();
      int editorFontSize = getEditorFontSize();

      myFontsMap = new HashMap<EditorFontType, Font>();

      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();
    }

    public void setName(String name) {
      getGlobal().setName(name);
    }

    public TextAttributes getAttributes(TextAttributesKey key) {
      if (myOwnAttributes.containsKey(key)) return myOwnAttributes.get(key);
      return getGlobal().getAttributes(key);
    }

    public void setAttributes(TextAttributesKey key, TextAttributes attributes) {
      myOwnAttributes.put(key, attributes);
    }

    public Color getColor(ColorKey key) {
      if (myOwnColors.containsKey(key)) return myOwnColors.get(key);
      return getGlobal().getColor(key);
    }

    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();
    }

    public int getEditorFontSize() {
      if (myFontSize != null) return myFontSize.intValue();
      return getGlobal().getEditorFontSize();
    }

    public void setEditorFontSize(int fontSize) {
      if (fontSize < 8) fontSize = 8;
      if (fontSize > 20) fontSize = 20;
      myFontSize = new Integer(fontSize);
      initFonts();
    }

    public String getEditorFontName() {
      if (myFaceName != null) return myFaceName;
      return getGlobal().getEditorFontName();
    }

    public void setEditorFontName(String fontName) {
      myFaceName = fontName;
      initFonts();
    }

    public Font getFont(EditorFontType key) {
      if (myFontsMap != null) {
        Font font = myFontsMap.get(key);
        if (font != null) return font;
      }
      return getGlobal().getFont(key);
    }

    public void setFont(EditorFontType key, Font font) {
      if (myFontsMap == null) {
        initFonts();
      }
      myFontsMap.put(key, font);
      reinitSettings();
    }

    public float getLineSpacing() {
      return getGlobal().getLineSpacing();
    }

    public void setLineSpacing(float lineSpacing) {
      getGlobal().setLineSpacing(lineSpacing);
    }

    public Object clone() {
      return null;
    }

    public void readExternal(Element element) throws InvalidDataException {
    }

    public void writeExternal(Element element) throws WriteExternalException {
    }
  }

  private static class MyTransferHandler extends TransferHandler {
    private RangeMarker myDraggedRange = null;

    private static Editor getEditor(JComponent comp) {
      EditorComponentImpl editorComponent = (EditorComponentImpl)comp;
      return editorComponent.getEditor();
    }

    public boolean importData(final JComponent comp, final Transferable t) {
      final EditorImpl editor = (EditorImpl)getEditor(comp);

      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() {
        public void run() {
          ApplicationManager.getApplication().runWriteAction(new Runnable() {
            public void run() {
              try {
                final int offset;
                editor.getSelectionModel().removeSelection();
                if (myDraggedRange != null) {
                  editor.getCaretModel().moveToOffset(caretOffset);
                  offset = caretOffset;
                }
                else {
                  offset = editor.getCaretModel().getOffset();
                }
                if (editor.getDocument().getRangeGuard(offset, offset) != null) return;

                EditorActionHandler pasteHandler = EditorActionManager.getInstance().getActionHandler(
                  IdeActions.ACTION_EDITOR_PASTE);
                Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
                Transferable backup = clipboard.getContents(this);
                ClipboardOwner clipboardOwner = new ClipboardOwner() {
                  public void lostOwnership(Clipboard clipboard, Transferable contents) {
                  }
                };
                clipboard.setContents(t, clipboardOwner);

                editor.putUserData(LAST_PASTED_REGION, null);
                pasteHandler.execute(editor, editor.getDataContext());
                clipboard.setContents(backup, clipboardOwner);

                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);
              }
            }
          });
        }
      }, "Paste", DND_COMMAND_KEY);

      return true;
    }

    public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) {
      Editor editor = getEditor(comp);
      if (editor.isViewer()) return false;
      if (!editor.getDocument().isWritable()) return false;

      int offset = editor.getCaretModel().getOffset();
      if (editor.getDocument().getRangeGuard(offset, offset) != null) return false;

      for (int i = 0; i < transferFlavors.length; i++) {
        DataFlavor transferFlavor = transferFlavors[i];
        if (transferFlavor.equals(DataFlavor.stringFlavor)) return true;
      }

      return false;
    }

    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);
    }

    public int getSourceActions(JComponent c) {
      return COPY_OR_MOVE;
    }

    protected void exportDone(final JComponent source, Transferable data, int action) {
      if (data == null) return;

      if (action == MOVE && !getEditor(source).isViewer() && getEditor(source).getDocument().isWritable()) {
        CommandProcessor.getInstance().executeCommand(((EditorImpl)getEditor(source)).myProject, new Runnable() {
          public void run() {
            ApplicationManager.getApplication().runWriteAction(new Runnable() {
              public void run() {
                Document doc = getEditor(source).getDocument();
                doc.startGuardedBlockChecking();
                try {
                  doc.deleteString(myDraggedRange.getStartOffset(), myDraggedRange.getEndOffset());
                }
                catch (ReadOnlyFragmentModificationException e) {
                  EditorActionManager.getInstance().getReadonlyFragmentModificationHandler().handle(e);
                }
                finally {
                  doc.stopGuardedBlockChecking();
                }
              }
            });
          }
        }, "Move selection", DND_COMMAND_KEY);
      }

      myDraggedRange = null;
    }
  }

  class EditorDocumentAdapter extends DocumentAdapter {
    public void beforeDocumentChange(DocumentEvent e) {
      beforeChangedUpdate(e);
    }

    public void documentChanged(DocumentEvent e) {
      changedUpdate(e);
    }
  }

  private class EditorSizeContainer {
    private TIntArrayList myLineWidths;
    private boolean myIsDirty;
    private int myOldEndLine;
    private Dimension mySize;

    public void reset() {
      int visLinesCount = getVisibleLineCount();
      myLineWidths = new TIntArrayList(visLinesCount + 300);
      int[] values = new int[visLinesCount];
      Arrays.fill(values, -1);
      myLineWidths.add(values);
      myIsDirty = true;
    }

    public void beforeChange(DocumentEvent e) {
      myOldEndLine = offsetToVisualPosition(e.getOffset() + e.getOldLength()).line;
    }

    public void changedUpdate(DocumentEvent e) {
      int startLine = offsetToVisualPosition(e.getOffset()).line;
      int newEndLine = offsetToVisualPosition(e.getOffset() + e.getNewLength()).line;
      int oldEndLine = myOldEndLine;

      if (myLineWidths.size() == 0) {
        reset();
      }
      else {
        int min = Math.min(oldEndLine, newEndLine);
        for (int i = startLine; i <= min; i++) myLineWidths.set(i, -1);
        if (newEndLine > oldEndLine) {
          int[] delta = new int[newEndLine - oldEndLine];
          Arrays.fill(delta, -1);
          myLineWidths.insert(oldEndLine + 1, delta);
        }
        else if (oldEndLine > newEndLine) {
          myLineWidths.remove(newEndLine + 1, oldEndLine - newEndLine);
        }
        myIsDirty = true;
      }
    }

    private void validateSizes() {
      if (!myIsDirty) return;

      CharSequence text = myDocument.getCharsNoThreadCheck();
      int lineCount = myLineWidths.size();
      int end = myDocument.getTextLength();
      int x = 0;
      for (int line = 0; line < lineCount; line++) {
        if (myLineWidths.getQuick(line) != -1) continue;
        x = 0;
        int offset = logicalPositionToOffset(visualToLogicalPosition(new VisualPosition(line, 0)));

        if (offset >= myDocument.getTextLength()) {
          myLineWidths.set(line, 0);
          break;
        }

        IterationState state = new IterationState(EditorImpl.this, offset, false);
        int fontType = state.getMergedAttributes().getFontType();

        while (offset < end) {
          char c = text.charAt(offset);
          if (offset >= state.getEndOffset()) {
            state.advance();
            fontType = state.getMergedAttributes().getFontType();
          }

          FoldRegion collapsed = state.getCurrentFold();
          if (collapsed != null) {
            String placeholder = collapsed.getPlaceholderText();
            for (int i = 0; i < placeholder.length(); i++) {
              x += charWidth(fontType, placeholder.charAt(i));
            }
            offset = collapsed.getEndOffset();
          }
          else {
            if (c == '\t') {
              x = nextTabStop(x);
              offset++;
            }
            else {
              if (c == '\n') {
                myLineWidths.set(line, x);
                if (line + 1 >= lineCount || myLineWidths.getQuick(line + 1) != -1) break;
                offset++;
                x = 0;
                line++;
              }
              else {
                x += charWidth(fontType, c);
                offset++;
              }
            }
          }
        }
      }

      if (lineCount > 0) {
        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
      }

      int maxWidth = 0;
      for (int i = 0; i < lineCount; i++) {
        maxWidth = Math.max(maxWidth, myLineWidths.getQuick(i));
      }

      mySize = new Dimension(maxWidth, getLineHeight() * lineCount);

      myIsDirty = false;
    }

    public Dimension getContentSize() {
      validateSizes();
      return mySize;
    }
  }

  public EditorGutter getGutter() {
    return getGutterComponentEx();
  }
}
