/*
 * Copyright 2003-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jetbrains.mps.nodeEditor;

import com.intellij.ide.CopyProvider;
import com.intellij.ide.CutProvider;
import com.intellij.ide.DataManager;
import com.intellij.ide.PasteProvider;
import com.intellij.ide.SelectInContext;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.KeyboardShortcut;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.actionSystem.Separator;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.impl.LeftHandScrollbarLayout;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.openapi.wm.IdeGlassPane;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.openapi.wm.ex.StatusBarEx;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.components.JBScrollBar;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.ui.ButtonlessScrollBarUI;
import com.intellij.util.ui.UIUtil;
import jetbrains.mps.RuntimeFlags;
import jetbrains.mps.classloading.ClassLoaderManager;
import jetbrains.mps.classloading.MPSClassesListener;
import jetbrains.mps.classloading.MPSClassesListenerAdapter;
import jetbrains.mps.editor.runtime.cells.ReadOnlyUtil;
import jetbrains.mps.editor.runtime.commands.EditorCommand;
import jetbrains.mps.editor.runtime.commands.EditorCommandAdapter;
import jetbrains.mps.editor.runtime.impl.cellActions.CellAction_CommentOrUncommentCurrentSelectedNode;
import jetbrains.mps.editor.runtime.style.StyleAttributes;
import jetbrains.mps.errors.IErrorReporter;
import jetbrains.mps.ide.MPSCoreComponents;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.ide.actions.MPSActions;
import jetbrains.mps.ide.actions.MPSCommonDataKeys;
import jetbrains.mps.ide.editor.MPSEditorDataKeys;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.ide.projectView.ProjectViewSelectInProvider;
import jetbrains.mps.ide.tooltips.MPSToolTipManager;
import jetbrains.mps.ide.tooltips.TooltipComponent;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.AttributeOperations;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.module.ReloadableModuleBase;
import jetbrains.mps.nodeEditor.NodeEditorActions.CompleteSmart;
import jetbrains.mps.nodeEditor.NodeEditorActions.ShowMessage;
import jetbrains.mps.nodeEditor.actions.ActionHandlerImpl;
import jetbrains.mps.nodeEditor.actions.CursorPositionTracker;
import jetbrains.mps.nodeEditor.assist.DefaultContextAssistantManager;
import jetbrains.mps.nodeEditor.assist.DisabledContextAssistantManager;
import jetbrains.mps.nodeEditor.cellActions.CellAction_CopyNode;
import jetbrains.mps.nodeEditor.cellActions.CellAction_CutNode;
import jetbrains.mps.nodeEditor.cellActions.CellAction_PasteNode;
import jetbrains.mps.nodeEditor.cellActions.CellAction_PasteNodeRelative;
import jetbrains.mps.nodeEditor.cellActions.CellAction_SideTransform;
import jetbrains.mps.nodeEditor.cellActions.SideTransformSubstituteInfo.Side;
import jetbrains.mps.nodeEditor.cellMenu.NodeSubstituteChooser;
import jetbrains.mps.nodeEditor.cellMenu.NodeSubstitutePatternEditor;
import jetbrains.mps.nodeEditor.cells.APICellAdapter;
import jetbrains.mps.nodeEditor.cells.CellFinderUtil;
import jetbrains.mps.nodeEditor.cells.CellFinderUtil.Finder;
import jetbrains.mps.nodeEditor.cells.EditorCell;
import jetbrains.mps.nodeEditor.cells.EditorCell_Basic;
import jetbrains.mps.nodeEditor.cells.EditorCell_Collection;
import jetbrains.mps.nodeEditor.cells.EditorCell_Constant;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.nodeEditor.cells.EditorCell_Property;
import jetbrains.mps.nodeEditor.commands.CommandContextImpl;
import jetbrains.mps.nodeEditor.commands.CommandContextWrapper;
import jetbrains.mps.nodeEditor.configuration.EditorConfiguration;
import jetbrains.mps.nodeEditor.configuration.EditorConfigurationBuilder;
import jetbrains.mps.nodeEditor.folding.CallAction_ToggleCellFolding;
import jetbrains.mps.nodeEditor.folding.CellAction_FoldCell;
import jetbrains.mps.nodeEditor.folding.CellAction_UnfoldCell;
import jetbrains.mps.nodeEditor.folding.CollapseAllCellAction;
import jetbrains.mps.nodeEditor.folding.CollapseRecursivelyCellAction;
import jetbrains.mps.nodeEditor.highlighter.EditorComponentCreateListener;
import jetbrains.mps.nodeEditor.highlighter.EditorHighlighter;
import jetbrains.mps.nodeEditor.keymaps.AWTKeymapHandler;
import jetbrains.mps.nodeEditor.keymaps.KeymapHandler;
import jetbrains.mps.nodeEditor.leftHighlighter.LeftEditorHighlighter;
import jetbrains.mps.nodeEditor.selection.SelectionInternal;
import jetbrains.mps.nodeEditor.selection.SelectionManagerImpl;
import jetbrains.mps.nodeEditor.sidetransform.EditorCell_STHint;
import jetbrains.mps.nodeEditor.ui.InputMethodListenerImpl;
import jetbrains.mps.nodeEditor.ui.InputMethodRequestsImpl;
import jetbrains.mps.nodeEditor.updater.UpdaterImpl;
import jetbrains.mps.nodefs.MPSNodeVirtualFile;
import jetbrains.mps.openapi.editor.ActionHandler;
import jetbrains.mps.openapi.editor.assist.ContextAssistant;
import jetbrains.mps.openapi.editor.assist.ContextAssistantManager;
import jetbrains.mps.openapi.editor.cells.CellAction;
import jetbrains.mps.openapi.editor.cells.CellActionType;
import jetbrains.mps.openapi.editor.cells.CellMessagesUtil;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.EditorCellContext;
import jetbrains.mps.openapi.editor.cells.KeyMapAction;
import jetbrains.mps.openapi.editor.cells.SubstituteAction;
import jetbrains.mps.openapi.editor.cells.SubstituteInfo;
import jetbrains.mps.openapi.editor.commands.CommandContext;
import jetbrains.mps.openapi.editor.message.EditorMessageOwner;
import jetbrains.mps.openapi.editor.message.SimpleEditorMessage;
import jetbrains.mps.openapi.editor.selection.Selection;
import jetbrains.mps.openapi.editor.selection.SelectionListener;
import jetbrains.mps.openapi.editor.selection.SelectionManager;
import jetbrains.mps.openapi.editor.selection.SingularSelection;
import jetbrains.mps.openapi.editor.style.StyleRegistry;
import jetbrains.mps.openapi.editor.update.Updater;
import jetbrains.mps.openapi.navigation.EditorNavigator;
import jetbrains.mps.project.MPSProject;
import jetbrains.mps.smodel.ModelAccess;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.typesystem.inference.DefaultTypecheckingContextOwner;
import jetbrains.mps.typesystem.inference.ITypeContextOwner;
import jetbrains.mps.typesystem.inference.NonReusableTypecheckingContextOwner;
import jetbrains.mps.typesystem.inference.TypeCheckingContext;
import jetbrains.mps.typesystem.inference.TypeContextManager;
import jetbrains.mps.typesystem.inference.util.ConcurrentSubtypingCache;
import jetbrains.mps.typesystem.inference.util.SubtypingCache;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.ComputeRunnable;
import jetbrains.mps.util.Pair;
import jetbrains.mps.util.annotation.ToRemove;
import jetbrains.mps.workbench.ActionPlace;
import jetbrains.mps.workbench.action.ActionUtils;
import jetbrains.mps.workbench.action.BaseAction;
import org.apache.log4j.LogManager;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.util.Condition;

import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.ScrollBarUI;
import javax.swing.plaf.basic.BasicScrollBarUI;
import javax.swing.text.DefaultEditorKit;
import java.awt.Adjustable;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FocusTraversalPolicy;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.im.InputMethodRequests;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;

public abstract class EditorComponent extends JComponent implements Scrollable, DataProvider, ITypeContextOwner, TooltipComponent,
                                                                    jetbrains.mps.openapi.editor.EditorComponent {

  private static final Logger LOG = Logger.wrap(LogManager.getLogger(EditorComponent.class));
  public static final String EDITOR_POPUP_MENU_ACTIONS = MPSActions.EDITOR_POPUP_GROUP;

  private static final int SCROLL_GAP = 15;
  private final ClassLoaderManager myClassLoaderManager;

  private String myDefaultPopupGroupId = MPSActions.EDITOR_POPUP_GROUP;
  private InputMethodRequests myInputMethodRequests;

  public static void turnOnAliasingIfPossible(Graphics2D g) {
    if (EditorSettings.getInstance().isUseAntialiasing()) {
      Toolkit tk = Toolkit.getDefaultToolkit();
      //noinspection HardCodedStringLiteral
      Map map = (Map) tk.getDesktopProperty("awt.font.desktophints");
      if (map != null) {
        g.addRenderingHints(map);
      } else {
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
      }
      g.setRenderingHint(RenderingHints.KEY_TEXT_LCD_CONTRAST, UIUtil.getLcdContrastValue());
    }
  }

  private final Object myAdditionalPaintersLock = new Object();

  private Map<jetbrains.mps.openapi.editor.cells.EditorCell, Boolean> myCollapseStates = new HashMap<>();
  private Set<EditorCell> myBracesEnabledCells = new HashSet<>();

  private CellTracker myCellTracker = new CellTracker();

  private boolean myDisposed = false;
  // additional debugging field
  private StackTraceElement[] myModelDisposedStackTrace = null;
  private Throwable myDisposedTrace = null;

  private Set<AdditionalPainter> myAdditionalPainters = new TreeSet<>((o1, o2) -> {
    if (o1.isAbove(o2, EditorComponent.this)) {
      return 1;
    }
    if (o2.isAbove(o1, EditorComponent.this)) {
      return -1;
    }
    return o1.equals(o2) ? 0 : Integer.signum(System.identityHashCode(o1) - System.identityHashCode(o2));
  });
  private Map<Object, AdditionalPainter> myItemsToAdditionalPainters = new HashMap<>();

  private final List<LeftMarginMouseListener> myLeftMarginPressListeners = new ArrayList<>(0);

  private EditorSettingsListener mySettingsListener = new EditorSettingsListener() {
    @Override
    public void settingsChanged() {
      getModelAccess().runReadInEDT(() -> {
        if (isDisposed()) {
          return;
        }
        rebuildEditorContent();
      });
    }
  };
  private MPSClassesListener myClassesListener = new MPSClassesListenerAdapter() {
    @Override
    public void afterClassesLoaded(Set<? extends ReloadableModuleBase> modules) {
      getModelAccess().runReadInEDT(() -> {
        if (isDisposed() || isModuleDisposed() || isProjectDisposed() || isNodeDisposed()) {
          return;
        }
        rebuildEditorContent();
        myNodeSubstituteChooser.clearContent();
      });
    }
  };

  private boolean myReadOnly = false;
  private String myLastWrittenStatus = "";

  @NotNull
  private final SRepository myRepository;

  //TODO: make @NotNull after separating UI-less logic into AbstractEditorComponent class
  private JScrollPane myScrollPane;
  //TODO: make @NotNull after separating UI-less logic into AbstractEditorComponent class
  private MyScrollBar myVerticalScrollBar;
  //TODO: make @NotNull after separating UI-less logic into AbstractEditorComponent class
  private JComponent myContainer;

  protected EditorCell myRootCell;
  private int myShiftX = 15;
  private int myShiftY = 10;

  private SelectionManagerImpl mySelectionManager = new SelectionManagerImpl(this);
  @NotNull
  private final CommandContextImpl myCommandContext;
  private final UpdaterImpl myUpdater;

  private Stack<KeyboardHandler> myKbdHandlersStack;
  private MouseListener myMouseEventHandler;
  private HashMap<CellActionType, CellAction> myActionMap;

  private NodeSubstituteChooser myNodeSubstituteChooser;
  private NodeInformationDialog myNodeInformationDialog;

  private List<EditorDisposeListener> myDisposeListeners = new ArrayList<>();
  private final NodeHighlightManager myHighlightManager;

  private MessagesGutter myMessagesGutter;
  private LeftEditorHighlighter myLeftHighlighter;
  @Nullable
  protected SNode myNode;
  private boolean myNoVirtualFile;

  @Nullable
  protected SNodeReference myNodePointer;
  @NotNull
  private EditorContext myEditorContext;
  @NotNull
  private final EditorConfiguration myEditorConfiguration;
  private final EditorMessageOwner myOwner = new EditorMessageOwner() {
  };

  private IntentionsSupport myIntentionsSupport;
  @SuppressWarnings({"UnusedDeclaration"})
  private AutoValidator myAutoValidator;
  private SearchPanel mySearchPanel = null;
  private JPanel myUpperPanel = null;
  private Map<String, JComponent> myUpperComponents = new HashMap<>();
  @SuppressWarnings({"UnusedDeclaration"})
  private ReferenceUnderliner myReferenceUnderliner = new ReferenceUnderliner();
  private BracesHighlighter myBracesHighlighter = new BracesHighlighter(this);
  private boolean myPopupMenuEnabled = true;
  private boolean myIsInFiguresHierarchy = false;

  private KeymapHandler<KeyEvent> myKeymapHandler = new AWTKeymapHandler();
  private ActionHandler myActionHandler = new ActionHandlerImpl(this);

  @NotNull
  private final EditorHighlighter myHighlighter = new EditorHighlighter(this);

  @NotNull
  private final EditorComponentFocusTracker myFocusTracker = new EditorComponentFocusTracker(this);

  public EditorComponent(@NotNull SRepository repository) {
    this(repository, EditorConfigurationBuilder.buildDefault());
  }

  /**
   * @deprecated since MPS 3.4 use {@link #EditorComponent(SRepository, EditorConfiguration)}
   */
  @Deprecated
  public EditorComponent(@NotNull SRepository repository, boolean showErrorsGutter, boolean rightToLeft) {
    this(repository, new EditorConfigurationBuilder().showErrorsGutter(showErrorsGutter).rightToLeft(rightToLeft).build());
  }

  /**
   * @deprecated since MPS 3.4 use {@link #EditorComponent(SRepository, EditorConfiguration)}
   */
  @Deprecated
  protected EditorComponent(@NotNull SRepository repository, boolean showErrorsGutter, boolean rightToLeft, boolean createUI) {
    this(repository, new EditorConfigurationBuilder().showErrorsGutter(showErrorsGutter).rightToLeft(rightToLeft).withUI(createUI).build());
  }

  protected EditorComponent(@NotNull SRepository repository, @NotNull EditorConfiguration configuration) {
    myRepository = repository;
    myEditorConfiguration = configuration;
    myCommandContext = createCommandContext();
    myUpdater = createUpdater(myCommandContext);
    myHighlightManager = new NodeHighlightManager(this);

    if (ApplicationManager.getApplication() != null && ApplicationManager.getApplication().getComponent(MPSCoreComponents.class) != null) {
      myClassLoaderManager = ApplicationManager.getApplication().getComponent(MPSCoreComponents.class).getClassLoaderManager();
    } else {
      myClassLoaderManager = ClassLoaderManager.getInstance();
    }

    setLayout(new EditorComponentLayoutManager(this));
    setEditorContext(null, repository);
    myRootCell = new EditorCell_Constant(getEditorContext(), null, "");
    myRootCell.setSelectable(false);

    setBackground(StyleRegistry.getInstance().getEditorBackground());

    setFocusCycleRoot(true);
    setFocusTraversalPolicy(new FocusTraversalPolicy() {
      @Override
      public Component getComponentAfter(Container aContainer, Component aComponent) {
        if (myIsInFiguresHierarchy) {
          executeComponentAction(CellActionType.NEXT);
        }
        return myIsInFiguresHierarchy ? aContainer : null;
      }

      @Override
      public Component getComponentBefore(Container aContainer, Component aComponent) {
        if (myIsInFiguresHierarchy) {
          executeComponentAction(CellActionType.PREV);
        }
        return myIsInFiguresHierarchy ? aContainer : null;
      }

      @Override
      public Component getFirstComponent(Container aContainer) {
        return myIsInFiguresHierarchy ? aContainer : null;
      }

      @Override
      public Component getLastComponent(Container aContainer) {
        return myIsInFiguresHierarchy ? aContainer : null;
      }

      @Override
      public Component getDefaultComponent(Container aContainer) {
        return myIsInFiguresHierarchy ? aContainer : null;
      }
    });
    setFocusTraversalKeysEnabled(false);

    setDoubleBuffered(true);

    myNodeSubstituteChooser = new NodeSubstituteChooser(this);

    // --- keyboard handling ---
    myKbdHandlersStack = new Stack<>();
    myKbdHandlersStack.push(new EditorComponentKeyboardHandler(myKeymapHandler));

    // --- init action map --
    myActionMap = new HashMap<>();
    // -- navigation
    myActionMap.put(CellActionType.LEFT, new NodeEditorActions.MoveLeft());
    myActionMap.put(CellActionType.RIGHT, new NodeEditorActions.MoveRight());
    CursorPositionTracker cursorPositionTracker = new CursorPositionTracker(getEditorContext());
    myActionMap.put(CellActionType.UP, new NodeEditorActions.MoveUp(cursorPositionTracker));
    myActionMap.put(CellActionType.DOWN, new NodeEditorActions.MoveDown(cursorPositionTracker));
    myActionMap.put(CellActionType.NEXT, new NodeEditorActions.MoveNext());
    myActionMap.put(CellActionType.PREV, new NodeEditorActions.MovePrev());
    myActionMap.put(CellActionType.LOCAL_HOME, new NodeEditorActions.MoveLocal(true));
    myActionMap.put(CellActionType.LOCAL_END, new NodeEditorActions.MoveLocal(false));

    myActionMap.put(CellActionType.ROOT_HOME, new NodeEditorActions.MoveToRoot(true));
    myActionMap.put(CellActionType.ROOT_END, new NodeEditorActions.MoveToRoot(false));
    myActionMap.put(CellActionType.HOME, new NodeEditorActions.MoveHome());
    myActionMap.put(CellActionType.END, new NodeEditorActions.MoveEnd());
    myActionMap.put(CellActionType.PAGE_DOWN, new NodeEditorActions.MovePageUp());
    myActionMap.put(CellActionType.PAGE_UP, new NodeEditorActions.MovePageDown());

    myActionMap.put(CellActionType.SELECT_UP, new NodeEditorActions.SelectUp());
    myActionMap.put(CellActionType.SELECT_DOWN, new NodeEditorActions.SelectDown());
    myActionMap.put(CellActionType.SELECT_RIGHT, new NodeEditorActions.SideSelect(CellSide.RIGHT));
    myActionMap.put(CellActionType.SELECT_LEFT, new NodeEditorActions.SideSelect(CellSide.LEFT));
    myActionMap.put(CellActionType.SELECT_NEXT, new NodeEditorActions.EnlargeSelection(true));
    myActionMap.put(CellActionType.SELECT_PREVIOUS, new NodeEditorActions.EnlargeSelection(false));

    myActionMap.put(CellActionType.COPY, new CellAction_CopyNode());
    myActionMap.put(CellActionType.CUT, new CellAction_CutNode());
    myActionMap.put(CellActionType.PASTE, new CellAction_PasteNode());
    myActionMap.put(CellActionType.PASTE_BEFORE, new CellAction_PasteNodeRelative(true));
    myActionMap.put(CellActionType.PASTE_AFTER, new CellAction_PasteNodeRelative(false));

    myActionMap.put(CellActionType.FOLD, new CellAction_FoldCell());
    myActionMap.put(CellActionType.UNFOLD, new CellAction_UnfoldCell());
    myActionMap.put(CellActionType.FOLD_ALL, new CollapseAllCellAction(true));
    myActionMap.put(CellActionType.UNFOLD_ALL, new CollapseAllCellAction(false));
    myActionMap.put(CellActionType.FOLD_RECURSIVELY, new CollapseRecursivelyCellAction(true));
    myActionMap.put(CellActionType.UNFOLD_RECURSIVELY, new CollapseRecursivelyCellAction(false));
    myActionMap.put(CellActionType.TOGGLE_FOLDING, new CallAction_ToggleCellFolding());

    myActionMap.put(CellActionType.RIGHT_TRANSFORM, new CellAction_SideTransform(Side.RIGHT));
    myActionMap.put(CellActionType.LEFT_TRANSFORM, new CellAction_SideTransform(Side.LEFT));

    myActionMap.put(CellActionType.COMPLETE, new NodeEditorActions.Complete());
    myActionMap.put(CellActionType.COMPLETE_SMART, new CompleteSmart());

    myActionMap.put(CellActionType.SHOW_MESSAGE, new ShowMessage());

    myActionMap.put(CellActionType.COMMENT, new CellAction_CommentOrUncommentCurrentSelectedNode());

    registerKeyboardAction(new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        goToNextErrorCell(false);
      }
    }, KeyStroke.getKeyStroke("F2"), WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

    registerKeyboardAction(new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        goToNextErrorCell(true);
      }
    }, KeyStroke.getKeyStroke("shift F2"), WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

    registerKeyboardAction(new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        goToNextHighlightedCell(false);
      }
    }, KeyStroke.getKeyStroke("F3"), WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

    registerKeyboardAction(new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        goToNextHighlightedCell(true);
      }
    }, KeyStroke.getKeyStroke("shift F3"), WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

    registerKeyboardAction(new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        final jetbrains.mps.openapi.editor.cells.EditorCell cell = getSelectedCell();
        if (cell == null) {
          return;
        }
        getModelAccess().runReadAction(() -> showPopupMenu(cell.getX(), cell.getY()));
      }
    }, KeyStroke.getKeyStroke("CONTEXT_MENU"), WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);

    addMouseListener(new MouseAdapter() {
      @Override
      public void mousePressed(final MouseEvent e) {
        if (areMouseEventsBlocked()) {
          return;
        }
        if (e.isPopupTrigger()) {
          processPopupMenu(e);
        } else {
          processMousePressed(e);
        }
      }

      @Override
      public void mouseClicked(MouseEvent e) {
        if (areMouseEventsBlocked()) {
          return;
        }
        jetbrains.mps.openapi.editor.cells.EditorCell selectedCell = getSelectedCell();
        boolean inSelectedCell = selectedCell != null && myRootCell.findLeaf(e.getX(), e.getY()) == selectedCell;
        if (inSelectedCell) {
          Selection selection = getSelectionManager().getSelection();
          if (selection.canExecuteAction(CellActionType.CLICK)) {
            selection.executeAction(CellActionType.CLICK);
          } else if (e.getClickCount() == 2 && selectedCell instanceof EditorCell_Label) {
            ((EditorCell_Label) selectedCell).selectWordOrAll();
            repaintExternalComponent();
          }
        }
      }

      @Override
      public void mouseReleased(MouseEvent e) {
        if (areMouseEventsBlocked()) {
          return;
        }
        if (e.isPopupTrigger()) {
          processPopupMenu(e);
        }
        super.mouseReleased(e);
      }
    });

    addKeyListener(new KeyAdapter() {
      @Override
      public void keyPressed(final KeyEvent e) {
        processKeyPressed(e);
      }

      @Override
      public void keyTyped(KeyEvent e) {
        processKeyTyped(e);
      }

      @Override
      public void keyReleased(final KeyEvent e) {
        processKeyReleased(e);
      }
    });

    addFocusListener(new FocusListener() {
      @Override
      public void focusGained(FocusEvent e) {
        if (isDisposed()) {
          return;
        }
        setDefaultSelection();
        activateCaretBlinker();
      }

      @Override
      public void focusLost(FocusEvent e) {
        closeSubstituteChooser(e.getOppositeComponent());
        commitAllCellValues();
        deActivateCaretBlinker();
      }
    });

    myAutoValidator = new AutoValidator(this);
    attachListeners();
    enablePasteFromHistory();

    if (configuration.withUI) {
      createUI(configuration);
    }
  }

  // TODO:
  // - extract all UI-free common logic into a super-class (AbstractEditorComponent)
  // - let HeadlessEditorComponent extend AbstractEditorComponent
  // - make this method again a part of constructor for this class
  private void createUI(EditorConfiguration editorConfiguration) {
    myVerticalScrollBar = new MyScrollBar(Adjustable.VERTICAL);

    myScrollPane = ScrollPaneFactory.createScrollPane();
    if (editorConfiguration.rightToLeft) {
      myScrollPane.setLayout(new LeftHandScrollbarLayout());
    }
    myScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
    myScrollPane.setVerticalScrollBar(myVerticalScrollBar);
    myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    myScrollPane.setViewportView(this);
    myScrollPane.getViewport().addChangeListener(new ChangeListener() {

      @Override
      public void stateChanged(ChangeEvent e) {
        if (!getNodeSubstituteChooser().isVisible()) {
          return;
        }
        Point point = getNodeSubstituteChooser().calcPatternEditorLocation();
        Rectangle viewRect = getViewport().getViewRect();
        if (isInsideEditor(point, viewRect)) {
          getNodeSubstituteChooser().moveToContextCell();
        } else {
          deactivateSubstituteChooser();
        }
      }

      private boolean isInsideEditor(Point point, Rectangle viewRect) {
        return isShowing() && point != null
               && point.getX() >= 0 && point.getX() <= getLocationOnScreen().getX() + viewRect.getX() + viewRect.getWidth()
               && point.getY() >= 0 &&
               point.getY() <= getLocationOnScreen().getY() + viewRect.getY() + viewRect.getHeight() + myScrollPane.getHorizontalScrollBar().getHeight();
      }
    });

    myContainer = new JPanel() {
      @Override
      public void addNotify() {
        super.addNotify();
        myIsInFiguresHierarchy = true;
      }

      @Override
      public void removeNotify() {
        myIsInFiguresHierarchy = false;
        super.removeNotify();
      }
    };
    myContainer.setMinimumSize(new Dimension(0, 0));
    myContainer.setLayout(new BorderLayout());
    myContainer.add(myScrollPane, BorderLayout.CENTER);

    myMessagesGutter = new MessagesGutter(this, editorConfiguration.rightToLeft);
    if (editorConfiguration.showErrorsGutter) {
      getVerticalScrollBar().setPersistentUI(myMessagesGutter);
    } else {
      getVerticalScrollBar().setPersistentUI(new ButtonlessScrollBarUI() {
        @Override
        public boolean alwaysShowTrack() {
          return true;
        }
      });
    }
    myLeftHighlighter = new LeftEditorHighlighter(this, editorConfiguration.rightToLeft);
    myLeftHighlighter.addMouseListener(new MouseAdapter() {
      @Override
      public void mousePressed(MouseEvent e) {
        for (LeftMarginMouseListener listener : new ArrayList<>(myLeftMarginPressListeners)) {
          listener.mousePressed(e, EditorComponent.this);
        }
      }

      @Override
      public void mouseReleased(MouseEvent e) {
        for (LeftMarginMouseListener listener : new ArrayList<>(myLeftMarginPressListeners)) {
          listener.mouseReleased(e, EditorComponent.this);
        }
      }

      @Override
      public void mouseClicked(MouseEvent e) {
        for (LeftMarginMouseListener listener : new ArrayList<>(myLeftMarginPressListeners)) {
          listener.mouseClicked(e, EditorComponent.this);
        }
      }
    });
    myScrollPane.setRowHeaderView(myLeftHighlighter);

    myIntentionsSupport = new IntentionsSupport(this);

    if (MPSToolTipManager.getInstance() != null) {
      MPSToolTipManager.getInstance().registerComponent(this);
    }

    getSelectionManager().addSelectionListener(new SelectionListener() {
      @Override
      public void selectionChanged(jetbrains.mps.openapi.editor.EditorComponent editorComponent, Selection oldSelection, Selection newSelection) {
        if (oldSelection == newSelection) {
          return;
        }
        deactivateSubstituteChooser();
        updateStatusBarMessage();

        if (oldSelection != null) {
          for (jetbrains.mps.openapi.editor.cells.EditorCell editorCell : oldSelection.getSelectedCells()) {
            repaint(editorCell);
          }
        }
        if (newSelection != null) {
          for (jetbrains.mps.openapi.editor.cells.EditorCell editorCell : newSelection.getSelectedCells()) {
            repaint(editorCell);
          }
        }
        myLeftHighlighter.repaint();
      }
    });
  }

  boolean hasUI() {
    return myEditorConfiguration.withUI;
  }

  private void enablePasteFromHistory() {
    // Registering DefaultEditorKit.pasteAction in the action map enables 'Paste from History'
    getActionMap().put(DefaultEditorKit.pasteAction, new AbstractAction() {
      @Override
      public void actionPerformed(ActionEvent e) {
        performPaste();
      }
    });
  }

  protected UpdaterImpl createUpdater(CommandContextImpl commandContext) {
    return new UpdaterImpl(this, commandContext);
  }

  protected CommandContextImpl createCommandContext() {
    return new CommandContextImpl(this);
  }

  protected void attachListeners() {
    EditorSettings.getInstance().addEditorSettingsListener(mySettingsListener);
    myClassLoaderManager.addClassesHandler(myClassesListener);
  }

  protected void notifyCreation() {
    jetbrains.mps.project.Project project = ProjectHelper.getProject(myRepository);
    if (project == null) {
      return;
    }
    Project ideaProject = ProjectHelper.toIdeaProject(project);
    if (ideaProject == null) {
      return;
    }
    EditorComponentCreateListener listener = ideaProject.getMessageBus().syncPublisher(EditorComponentCreateListener.EDITOR_COMPONENT_CREATION);
    listener.editorComponentCreated(EditorComponent.this);
  }

  protected void notifyDisposal() {
    jetbrains.mps.project.Project project = ProjectHelper.getProject(myRepository);
    if (project == null) {
      return;
    }
    if (project.isDisposed()) {
      LOG.error("Trying to notify disposal of EditorComponent related to disposed project. This may cause memory leaks.");
      return;
    }
    Project ideaProject = ProjectHelper.toIdeaProject(project);
    if (ideaProject == null) {
      return;
    }
    EditorComponentCreateListener listener = ideaProject.getMessageBus().syncPublisher(EditorComponentCreateListener.EDITOR_COMPONENT_CREATION);
    listener.editorComponentDisposed(this);
  }

  public boolean onEscape() {
    return false;
  }

  /**
   * From now on only NodeEditorComponent has virtual file.
   * This method will be removed in the next release.
   *
   * @param noVirtualFile
   * @deprecated since MPS 3.4
   */
  @Deprecated
  public void setNoVirtualFile(boolean noVirtualFile) {
  }

  public int getShiftX() {
    return myShiftX;
  }

  public JViewport getViewport() {
    assert hasUI();
    return myScrollPane.getViewport();
  }

  Point getViewPosition() {
    return hasUI() ? getViewport().getViewPosition() : new Point(0, 0);
  }

  void setViewPosition(Point point) {
    if (!hasUI()) {
      return;
    }
    getViewport().setViewPosition(point);
  }

  @NotNull
  public MyScrollBar getVerticalScrollBar() {
    assert hasUI();
    return myVerticalScrollBar;
  }

  @Override
  public SNode getSelectedNode() {
    jetbrains.mps.openapi.editor.cells.EditorCell selectedCell = getSelectedCell();
    if (selectedCell == null) {
      return null;
    }
    return selectedCell.getSNode();
  }

  @Override
  public List<SNode> getSelectedNodes() {
    Selection selection = mySelectionManager.getSelection();
    return selection != null ? selection.getSelectedNodes() : Collections.emptyList();
  }

  public String[] getEditorHintsForNode(SNode node) {
    jetbrains.mps.openapi.editor.cells.EditorCell nodeCell = findNodeCell(node);
    if (nodeCell != null) {
      EditorCellContext cellContext = nodeCell.getCellContext();
      if (cellContext == null) {
        return null;
      }
      final Collection<String> nodeContextHints = cellContext.getHints();
      return nodeContextHints.toArray(new String[nodeContextHints.size()]);
    }
    return null;
  }

  public EditorMessageOwner getHighlightMessagesOwner() {
    return myOwner;
  }

  private void goToNextErrorCell(boolean backwards) {
    if (getSelectedCell() == null) {
      return;
    }
    new CellNavigator(this) {
      @Override
      boolean isSuitableCell(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
        return CellMessagesUtil.hasErrorMessages(cell);

      }
    }.goToNextCell(backwards);
  }

  private void goToNextHighlightedCell(boolean backwards) {
    if (getSelectedCell() == null) {
      return;
    }
    new CellNavigator(this) {
      @Override
      boolean isSuitableCell(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
        for (SimpleEditorMessage m : getHighlightManager().getMessagesFor(cell.getSNode())) {
          if (m.getOwner() == getHighlightMessagesOwner()) {
            return true;
          }
        }
        return false;
      }
    }.goToNextCell(backwards);
  }

  @Override
  public SNode getEditedNode() {
    return myNode;
  }

  /**
   * From now on only NodeEditorComponent has virtual file.
   * This method will be removed in the next release.
   *
   * @deprecated since MPS 3.4
   */
  @Deprecated
  @Nullable
  public MPSNodeVirtualFile getVirtualFile() {
    return null;
  }

  @Override
  public void touch() {
  }

  @Override
  public SNodeReference getEditedNodePointer() {
    return myNodePointer;
  }

  @Override
  public String getMPSTooltipText(final MouseEvent event) {
    return ModelAccess.instance().tryRead(new Computable<String>() {
      @Override
      public String compute() {
        if (isDisposed()) {
          return null;
        }

        jetbrains.mps.openapi.editor.cells.EditorCell cell = myRootCell.findLeaf(event.getX(), event.getY());
        if (cell == null) {
          return null;
        }
        return getMessagesTextFor(cell);
      }
    });
  }

  @Override
  public Point getToolTipLocation(final MouseEvent event) {
    return ModelAccess.instance().tryRead(new Computable<Point>() {
      @Override
      public Point compute() {
        if (isDisposed()) {
          return null;
        }

        jetbrains.mps.openapi.editor.cells.EditorCell cell = myRootCell.findLeaf(event.getX(), event.getY());
        if (cell == null) {
          return null;
        }
        if (getMessagesTextFor(cell) != null) {
          return new Point(event.getX(), event.getY());
        } else {
          return null;
        }
      }
    });
  }

  public void updateStatusBarMessage() {
    if (!isFocusOwner()) {
      return;
    }
    getModelAccess().runReadInEDT(new Runnable() {
      @Override
      public void run() {
        if (!isFocusOwner() || getCurrentProject() == null || isProjectDisposed()) {
          return;
        }

        jetbrains.mps.openapi.editor.cells.EditorCell selection = getSelectedCell();
        String info = "";
        if (selection != null) {
          HighlighterMessage message = getHighlighterMessageFor(selection);
          if (message != null) {
            info = message.getMessage();
          }
        }

        jetbrains.mps.project.Project project = getCurrentProject();
        IdeFrame ideFrame = WindowManager.getInstance().getIdeFrame(ProjectHelper.toIdeaProject(project));
        StatusBarEx statusBar = (StatusBarEx) ideFrame.getStatusBar();

        //current info is significant or the editor removes its own message
        if (!info.equals("") || myLastWrittenStatus.equals(statusBar.getInfo())) {
          statusBar.setInfo(info);
          if (!info.equals("")) {
            myLastWrittenStatus = info;
          }
        }
      }
    });
  }

  @Override
  public TypeCheckingContext createTypecheckingContext(SNode sNode, TypeContextManager typeContextManager) {
    return (new DefaultTypecheckingContextOwner()).createTypecheckingContext(sNode, typeContextManager);
  }

  @Override
  public boolean reuseTypecheckingContext() {
    return true;
  }

  @Override
  public SubtypingCache createSubtypingCache() {
    return new ConcurrentSubtypingCache();
  }

  public String getMessagesTextFor(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    List<HighlighterMessage> messages = getHighlighterMessagesFor(cell);
    if (messages.isEmpty()) {
      return null;
    }
    StringBuilder result = new StringBuilder();
    for (ListIterator<HighlighterMessage> it = messages.listIterator(messages.size()); it.hasPrevious(); ) {
      SimpleEditorMessage message = it.previous();
      if (result.length() != 0) {
        result.append("\n");
      }
      result.append(message.getMessage());
    }
    return result.toString();
  }

  @NotNull
  private List<HighlighterMessage> getHighlighterMessagesFor(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    jetbrains.mps.openapi.editor.cells.EditorCell parent = cell;
    while (parent != null) {
      if (cell.getBottom() < parent.getBottom() && parent.getSNode() != cell.getSNode()) {
        return Collections.emptyList();
      }
      List<HighlighterMessage> messages = CellMessagesUtil.getMessages(parent, HighlighterMessage.class);
      if (!messages.isEmpty()) {
        return messages;
      }
      parent = parent.getParent();
    }

    return Collections.emptyList();
  }

  private HighlighterMessage getHighlighterMessageFor(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    List<HighlighterMessage> messages = getHighlighterMessagesFor(cell);
    ListIterator<HighlighterMessage> it = messages.listIterator(messages.size());
    return it.hasPrevious() ? it.previous() : null;
  }

  @Nullable
  public IErrorReporter getErrorReporterFor(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    HighlighterMessage message = getHighlighterMessageFor(cell);
    if (message == null) {
      return null;
    }
    return message.getErrorReporter();
  }

  public void showMessageTooltip() {
    jetbrains.mps.openapi.editor.cells.EditorCell cell = getSelectedCell();
    if (cell == null) {
      return;
    }
    String text = getMessagesTextFor(cell);
    Point point = new Point(cell.getX(), cell.getY() + cell.getHeight());
    if (MPSToolTipManager.getInstance() != null) {
      MPSToolTipManager.getInstance().showToolTip(text, this, point);
    }
  }

  public void hideMessageToolTip() {
    if (MPSToolTipManager.getInstance() != null) {
      MPSToolTipManager.getInstance().hideToolTip();
    }
  }

  protected boolean notifiesCreation() {
    return false;
  }

  public void editNode(final SNode node) {
    if (isDisposed()) {
      return;
    }
    clearModelDisposedTrace();

    getModelAccess().runReadAction(new Runnable() {
      @Override
      public void run() {
        if (node != null) {
          assert node.getModel() != null : "Can't edit a node that is not registered in a model";
          assert SNodeUtil.isAccessible(node, myRepository) :
              "editNode() accepts nodes from its own repository only (model = " + node.getModel() + ", repository = " + node.getModel().getRepository() + ")";
        }

        if (myNode != null && notifiesCreation()) {
          notifyDisposal();
        }

        final boolean needNewTypecheckingContext = updateContainingRoot(node);
        if (needNewTypecheckingContext) {
          releaseTypeCheckingContext();
        }

        myNode = node;
        if (myNode != null) {
          myNodePointer = myNode.getReference();
          SModel model = node.getModel();
          assert model != null : "Can't edit a node that is not registered in a model";
          setEditorContext(model, myRepository);
          myReadOnly = model.isReadOnly();
        } else {
          myNodePointer = null;
          setEditorContext(null, myRepository);
          myReadOnly = true;
        }
        myCommandContext.updateContextNode();

        if (needNewTypecheckingContext) {
          acquireTypeCheckingContext();
        }

        rebuildEditorContent();

        if (myNode != null && notifiesCreation()) {
          notifyCreation();
        }
      }
    });
  }

  public void addAdditionalPainter(AdditionalPainter additionalPainter) {
    synchronized (myAdditionalPaintersLock) {
      if (!myAdditionalPainters.contains(additionalPainter)) {
        myAdditionalPainters.add(additionalPainter);
        myItemsToAdditionalPainters.put(additionalPainter.getItem(), additionalPainter);
        additionalPainter.afterAdding(this);
      }
    }
  }

  public void removeAdditionalPainter(AdditionalPainter additionalPainter) {
    synchronized (myAdditionalPaintersLock) {
      if (myAdditionalPainters.contains(additionalPainter)) {
        additionalPainter.beforeRemoval(this);
        myAdditionalPainters.remove(additionalPainter);
        myItemsToAdditionalPainters.remove(additionalPainter.getItem());
      }
    }
  }

  public void removeAdditionalPainterByItem(Object item) {
    synchronized (myAdditionalPaintersLock) {
      AdditionalPainter additionalPainter = myItemsToAdditionalPainters.get(item);
      if (additionalPainter != null) {
        additionalPainter.beforeRemoval(this);
        myItemsToAdditionalPainters.remove(item);
        myAdditionalPainters.remove(additionalPainter);
      }
    }
  }

  public Color getAdditionalCellFontColor(@NotNull EditorCell_Label cell) {
    synchronized (myAdditionalPaintersLock) {
      for (AdditionalPainter additionalPainter : myAdditionalPainters) {
        Rectangle coverageArea = additionalPainter.getCoverageArea(this);
        if (coverageArea != null) {
          if (coverageArea.contains(cell.getX(), cell.getY(), cell.getWidth(), cell.getHeight())) {
            Color color = additionalPainter.getCellsFontColor(cell);
            if (color != null) {
              return color;
            }
          }
        }
      }
      return null;
    }
  }

  public AdditionalPainter getAdditionalPainterByItem(Object item) {
    synchronized (myAdditionalPaintersLock) {
      return myItemsToAdditionalPainters.get(item);
    }
  }

  public List<AdditionalPainter> getAdditionalPainters() {
    List<AdditionalPainter> result;
    synchronized (myAdditionalPaintersLock) {
      result = new ArrayList<>(myAdditionalPainters);
    }
    return result;
  }

  // TODO pool this method up to NodeEditorComponent
  @NotNull
  public MessagesGutter getMessagesGutter() {
    assert hasUI();
    return myMessagesGutter;
  }

  @NotNull
  public LeftEditorHighlighter getLeftEditorHighlighter() {
    assert hasUI();
    return myLeftHighlighter;
  }

  @NotNull
  public SearchPanel getSearchPanel() {
    assert hasUI();
    if (mySearchPanel == null) {
      mySearchPanel = new SearchPanel(this);
    }
    return mySearchPanel;
  }

  public boolean isSearchPanelVisible() {
    return mySearchPanel != null && mySearchPanel.isVisible();
  }

  public JPanel getUpperPanel() {
    assert hasUI();
    if (myUpperPanel == null) {
      myUpperPanel = new JPanel();
      myUpperPanel.setLayout(new GridLayout(0, 1));
      myContainer.add(myUpperPanel, BorderLayout.NORTH);
    }
    return myUpperPanel;
  }

  public void addUpperComponent(JComponent component) {
    getUpperPanel().add(component);
  }

  public void addUpperComponent(JComponent component, String id) {
    getUpperPanel().add(component);
    myUpperComponents.put(id, component);
  }

  public void removeUpperComponent(JComponent component) {
    if (myUpperPanel == null) {
      return;
    }
    getUpperPanel().remove(component);
    for (String key : new HashSet<>(myUpperComponents.keySet())) {
      if (component == myUpperComponents) {
        myUpperComponents.remove(key);
      }
    }
  }

  public void removeUpperComponent(String id) {
    JComponent component = myUpperComponents.get(id);
    if (component != null) {
      removeUpperComponent(component);
    }
  }

  protected Set<SimpleEditorMessage> getMessages() {
    return new LinkedHashSet<>(myHighlightManager.getMessages());
  }

  private EditorCell_WithComponent findCellForComponent(Component component, jetbrains.mps.openapi.editor.cells.EditorCell root) {
    if (root instanceof EditorCell_WithComponent && ((EditorCell_WithComponent) root).getComponent() == component) {
      return (EditorCell_WithComponent) root;
    }

    if (root instanceof EditorCell_Collection) {
      EditorCell_Collection collection = (EditorCell_Collection) root;
      for (jetbrains.mps.openapi.editor.cells.EditorCell cell : collection) {
        EditorCell_WithComponent result = findCellForComponent(component, cell);
        if (result != null) {
          return result;
        }
      }
    }

    return null;
  }

  private void processPopupMenu(final MouseEvent e) {
    getModelAccess().runReadAction(() -> showPopupMenu(e));
  }

  private void showPopupMenu(MouseEvent e) {
    showPopupMenu(e.getX(), e.getY());
  }

  private void showPopupMenu(int x, int y) {
    if (!myPopupMenuEnabled) {
      return;
    }
    DefaultActionGroup baseGroup = ActionUtils.getDefaultGroup(myDefaultPopupGroupId);
    if (baseGroup == null) {
      return;
    }

    baseGroup.setPopup(false);

    DefaultActionGroup group = ActionUtils.groupFromActions(
        baseGroup,
        new Separator(),
        getCellActionsGroup()
    );

    JPopupMenu popupMenu = ActionManager.getInstance().createActionPopupMenu(ActionPlaces.EDITOR_POPUP, group).getComponent();
    popupMenu.show(EditorComponent.this, x, y);
  }

  protected String getDefaultPopupGroupId() {
    return myDefaultPopupGroupId;
  }

  protected void setDefaultPopupGroupId(String id) {
    myDefaultPopupGroupId = id;
  }

  private DefaultActionGroup getCellActionsGroup() {
    DefaultActionGroup result = new DefaultActionGroup("Cell actions", true);
    result.setPopup(false);
    jetbrains.mps.openapi.editor.cells.EditorCell cell = getSelectedCell();

    final EditorContext editorContext = createEditorContextForActions();
    for (final KeyMapAction action : myKeymapHandler.getAllRegisteredActions(cell, editorContext)) {
      try {
        if (!(action.isShownInPopupMenu() && action.canExecute(editorContext))) {
          continue;
        }
        BaseAction mpsAction = new MyBaseAction(action, editorContext);
        mpsAction.addPlace(ActionPlace.EDITOR);
        result.add(mpsAction);
      } catch (Throwable t) {
        LOG.error(t);
      }
    }
    return result;
  }

  private EditorContext createEditorContextForActions() {
    return new EditorContext(this, getEditedNode() != null ? getEditedNode().getModel() : null, myRepository, myEditorConfiguration,
                             new DisabledContextAssistantManager());
  }

  @NotNull
  public JComponent getExternalComponent() {
    assert hasUI();
    return myContainer;
  }

  public void repaintExternalComponent() {
    if (!hasUI()) {
      return;
    }
    getExternalComponent().repaint();
  }

  public void validateExternalComponent() {
    if (!hasUI()) {
      return;
    }
    getExternalComponent().validate();
  }

  @NotNull
  @Override
  public EditorContext getEditorContext() {
// TODO: uncomment this assertion. Was commented out because this method is called indirectly from the dispose() method (failing tests).
//    assert !isDisposed();
    return myEditorContext;
  }

  @NotNull
  protected EditorConfiguration getEditorConfiguration() {
    return myEditorConfiguration;
  }

  @NotNull
  protected SRepository getRepository() {
    return myRepository;
  }

  /**
   * Creating a cell representing empty editor content. Empty means editor has no node (getEditedNode() == null)
   * or currently editing node is not within a model (getEditedNode().getModel() == null)
   *
   * @return new EditorCell
   */
  public jetbrains.mps.openapi.editor.cells.EditorCell createEmptyCell() {
    return new EditorCell_Constant(getEditorContext(), getEditedNode(), getEditedNode() == null ? "<no node>" : "<node is not inside a model>");
  }

  public void setCollapseState(jetbrains.mps.openapi.editor.cells.EditorCell cell, Boolean collapsed) {
    if (collapsed == null) {
      resetCollapseState(cell);
    } else {
      myCollapseStates.put(cell, collapsed);
    }
    for (AdditionalPainter painter : getAdditionalPainters()) {
      painter.onUpdate(this);
    }
  }

  public void resetCollapseState(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    myCollapseStates.remove(cell);
  }

  public List<Pair<jetbrains.mps.openapi.editor.cells.EditorCell, Boolean>> getCollapseStates() {
    List<Pair<jetbrains.mps.openapi.editor.cells.EditorCell, Boolean>> result = new ArrayList<>();
    for (Entry<jetbrains.mps.openapi.editor.cells.EditorCell, Boolean> collapseState : myCollapseStates.entrySet()) {
      result.add(new Pair<>(collapseState.getKey(), collapseState.getValue()));
    }
    return result;
  }

  public void setBracesEnabled(EditorCell cell, boolean enabled) {
    if (enabled) {
      myBracesEnabledCells.add(cell);
    } else {
      myBracesEnabledCells.remove(cell);
    }
  }

  public Set<EditorCell> getBracesEnabledCells() {
    return new HashSet<>(myBracesEnabledCells);
  }

  void clearBracesEnabledCells() {
    myBracesEnabledCells.clear();
  }

  @Override
  public void dispose() {
    assertInEDT();
    if (myDisposed) {
      throw new IllegalStateException(myDisposedTrace);
    }
    fireEditorWillBeDisposed();
    myDisposed = true;
    myDisposedTrace = new Throwable("Editor was disposed by: ");
    if (!RuntimeFlags.isTestMode()) {
      hideMessageToolTip();
    }

    releaseTypeCheckingContext();
    myHighlightManager.dispose();

    detachListeners();
    // we expect this method to be executed at least inside model read
    // TODO: add assertion here
    myAutoValidator.dispose();
    myUpdater.dispose();

    if (hasUI()) {
      myLeftHighlighter.dispose();
      myMessagesGutter.dispose();
    }

    if (myNodeSubstituteChooser != null) {
      myNodeSubstituteChooser.dispose();
    }
    if (myRootCell != null) {
      ((EditorCell_Basic) myRootCell).onRemove();
      myRootCell = null;
    }
    mySelectionManager.dispose();

    myLeftMarginPressListeners.clear();

    myFocusTracker.dispose();
  }

  protected void detachListeners() {
    EditorSettings.getInstance().removeEditorSettingsListener(mySettingsListener);
    myClassLoaderManager.removeClassesHandler(myClassesListener);
  }

  public boolean hasValidSelectedNode() {
    SNode selectedNode = getSelectedNode();
    return selectedNode != null && SNodeUtil.isAccessible(selectedNode, myRepository);
  }

  @Override
  public boolean isDisposed() {
    return myDisposed;
  }

  public void assertModelNotDisposed() {
    boolean old = ModelAccess.instance().setReadEnabledFlag(true);
    try {
      assert myModelDisposedStackTrace == null : getModelDisposedMessage();
      if (myNode == null) {
        return;
      }
      SModel model = myNode.getModel();
      if (model == null) {
        return;
      }
      assert model.getRepository() != null : getNodeDisposedMessage(model);
    } finally {
      ModelAccess.instance().setReadEnabledFlag(old);
    }
  }

  private String getNodeDisposedMessage(SModel model) {
    StringBuilder sb = new StringBuilder("editor (" + this + ") is invalid");
    if (myNode != null) {
      sb.append(", myNode is disposed");
      StackTraceElement[] modelDisposedTrace = ((jetbrains.mps.smodel.SModelInternal) model).getDisposedStacktrace();
      if (modelDisposedTrace != null) {
        for (StackTraceElement element : modelDisposedTrace) {
          sb.append("\nat ");
          sb.append(element);
        }
      }
    } else {
      sb.append(", myNode == null");
    }
    sb.append("____________________________");
    return sb.toString();
  }

  private String getModelDisposedMessage() {
    StringBuilder sb = new StringBuilder("Model was disposed through:");
    for (StackTraceElement element : myModelDisposedStackTrace) {
      sb.append("\nat ");
      sb.append(element);
    }
    sb.append("\n");
    sb.append("EditorComponent.myDisposed == ");
    sb.append(isDisposed());
    sb.append("\n");
    return sb.toString();
  }

  // This method should be called each time we set new node for and editor
  protected void clearModelDisposedTrace() {
    myModelDisposedStackTrace = null;
  }

  public void setModelDisposedTrace(StackTraceElement[] trace) {
    myModelDisposedStackTrace = trace;
  }

  /*
    Can be used to check if editor is in valid state or not.
    Editor can be in invalid state then corresponding model
    was reloaded, but current editor instance was not
    updated yet.
   */
  public boolean isInvalid() {
    return isInvalidLightweight() || !SNodeUtil.isAccessible(getEditedNode(), myRepository);
  }

  /*
    Lightweight check for editor validity state. Similar to isInvalid,
    but can be called outside of read action.
   */
  private boolean isInvalidLightweight() {
    return isDisposed() || getEditedNode() == null;
  }

  public void setRootCell(@NotNull jetbrains.mps.openapi.editor.cells.EditorCell rootCell) {
    if (getComponents().length > 0) {
      removeAll();
    }
    ((EditorCell_Basic) myRootCell).onRemove();

    myRootCell = (EditorCell) rootCell;

    ((EditorCell_Basic) myRootCell).onAdd();
    for (EditorCell_WithComponent component : getCellTracker().getComponentCells()) {
      add(component.getComponent());
    }
    for (AdditionalPainter painter : getAdditionalPainters()) {
      painter.onUpdate(this);
    }
  }

  @Override
  public EditorCell getRootCell() {
    return myRootCell;
  }

  @NotNull
  public NodeHighlightManager getHighlightManager() {
    return myHighlightManager;
  }

  public CellActionType getActionType(KeyEvent keyEvent, EditorContext editorContext) {
    if (keyPressed(keyEvent) && keyEvent.getKeyCode() == KeyEvent.VK_HOME && shiftDown(keyEvent)) {
      return CellActionType.SELECT_HOME;
    }
    if (keyPressed(keyEvent) && keyEvent.getKeyCode() == KeyEvent.VK_END && shiftDown(keyEvent)) {
      return CellActionType.SELECT_END;
    }
    if (keyPressed(keyEvent) && keyEvent.getKeyCode() == KeyEvent.VK_PAGE_DOWN && noKeysDown(keyEvent)) {
      return CellActionType.PAGE_DOWN;
    }
    if (keyPressed(keyEvent) && keyEvent.getKeyCode() == KeyEvent.VK_PAGE_UP && noKeysDown(keyEvent)) {
      return CellActionType.PAGE_UP;
    }
    if (keyPressed(keyEvent) && keyEvent.getKeyCode() == KeyEvent.VK_TAB && noKeysDown(keyEvent)) {
      return CellActionType.NEXT;
    }
    if (keyPressed(keyEvent) && keyEvent.getKeyCode() == KeyEvent.VK_TAB && shiftDown(keyEvent)) {
      return CellActionType.PREV;
    }
    if (keyEvent.getModifiers() == KeyEvent.CTRL_MASK && keyEvent.getKeyCode() == KeyEvent.VK_F1) {
      return CellActionType.SHOW_MESSAGE;
    }

    // ---
    if (keyTyped(keyEvent) && keyEvent.getKeyChar() == ' ' && noKeysDown(keyEvent)) {
      jetbrains.mps.openapi.editor.cells.EditorCell selectedCell = editorContext.getNodeEditorComponent().getSelectedCell();

      if (!(selectedCell instanceof EditorCell_STHint)) {
        if (!(selectedCell instanceof EditorCell_Label)) {
          return CellActionType.RIGHT_TRANSFORM;
        }
        EditorCell_Label labelCell = (EditorCell_Label) selectedCell;

        // caret at the end of text ?
        String text = labelCell.getText();
        int caretPosition = labelCell.getCaretPosition();
        //System.out.println("text:" + text + " len:" + text.length() + "caret at:" + caretPosition);
        if (caretPosition == text.length()) {
          if (caretPosition == 0 && labelCell instanceof EditorCell_Constant) {
            //empty unbound constant cells should ignore the space key when empty
            return CellActionType.SELECT_END;
          } else {
            return CellActionType.RIGHT_TRANSFORM;
          }
        }

        if (caretPosition == 0) {
          return CellActionType.LEFT_TRANSFORM;
        }
      }
    }
    return null;
  }

  private boolean ctrlShiftDown(KeyEvent keyEvent) {
    return keyEvent.getModifiers() == (KeyEvent.CTRL_MASK + KeyEvent.SHIFT_MASK);
  }

  private boolean shiftDown(KeyEvent keyEvent) {
    return keyEvent.getModifiers() == KeyEvent.SHIFT_MASK;
  }

  private boolean noKeysDown(KeyEvent keyEvent) {
    return keyEvent.getModifiers() == 0;
  }

  private boolean keyTyped(KeyEvent keyEvent) {
    return keyEvent.getID() == KeyEvent.KEY_TYPED;
  }

  private boolean keyPressed(KeyEvent keyEvent) {
    return keyEvent.getID() == KeyEvent.KEY_PRESSED;
  }

  private boolean ctrlDown(KeyEvent keyEvent) {
    return keyEvent.getModifiers() == KeyEvent.CTRL_MASK;
  }

  boolean executeComponentAction(CellActionType type) {
    final CellAction action = getComponentAction(type);
    if (action != null && action.executeInCommand()) {
      getModelAccess().executeCommand(new EditorCommand(getCommandContext()) {
        @Override
        protected void doExecute() {
          action.execute(getEditorContext());
        }
      });
      return true;
    }
    return false;
  }

  @Override
  public CellAction getComponentAction(final CellActionType type) {
    return runRead(new Computable<CellAction>() {
      @Override
      public CellAction compute() {
        CellAction action = myActionMap.get(type);
        if (action != null && action.canExecute(getEditorContext())) {
          return action;
        }
        return null;
      }
    });
  }

  public void relayout() {
    doRelayout();
    revalidate();
    repaintExternalComponent();
  }

  private void doRelayout() {
    // TODO: check for myDisposed in all methods calling this one
    if (isDisposed()) {
      return;
    }

    myRootCell.setX(myShiftX);
    myRootCell.setY(myShiftY);
    myRootCell.relayout();
    if (!hasUI()) {
      return;
    }
    myLeftHighlighter.relayout(true);
    if (mySearchPanel != null && mySearchPanel.isVisible()) {
      mySearchPanel.search(false);
    }
  }

  public void leftHighlightCell(EditorCell cell, Color c) {
    if (!hasUI()) {
      return;
    }
    myLeftHighlighter.highlight(cell, cell, c);
  }

  public void leftHighlightCells(EditorCell cell, EditorCell cell2, Color c) {
    if (!hasUI()) {
      return;
    }
    myLeftHighlighter.highlight(cell, cell2, c);
  }

  public void leftUnhighlightCell(EditorCell cell) {
    if (!hasUI()) {
      return;
    }
    myLeftHighlighter.unHighlight(cell);
  }

  @Override
  public void selectNode(final SNode node) {
    getModelAccess().runReadAction(() -> {
      EditorCell nodeCell = findNodeCell(node);
      if (nodeCell != null) {
        changeSelection(nodeCell);
      }
    });
  }

  public void selectNode(final SNode node, final String cellId) {
    getModelAccess().runReadAction(() -> {
      EditorCell nodeCell = findCellWithId(node, cellId);
      if (nodeCell != null) {
        changeSelection(nodeCell);
      }
    });
  }

  @Override
  @Nullable
  public EditorCell findNodeCell(final SNode node) {
    return findNodeCell(node, false);
  }

  @Override
  @Nullable
  public EditorCell findNodeCell(final SNode node, boolean findUnderFolded) {
    jetbrains.mps.openapi.editor.cells.EditorCell bigCell = myUpdater.getBigCell(node);
    if (bigCell != null && (bigCell.getRootParent() != getRootCell() || (!findUnderFolded && CellTraversalUtil.getFoldedParent(bigCell) != null))) {
      return null;
    }
    return (EditorCell) bigCell;
  }

  public EditorCell findNodeCellWithRole(SNode node, String role) {
    EditorCell rootCell = findNodeCell(node);
    if (rootCell == null) {
      return null;
    }
    return (EditorCell) findNodeCellWithRole(rootCell, role, node);
  }

  private jetbrains.mps.openapi.editor.cells.EditorCell findNodeCellWithRole(jetbrains.mps.openapi.editor.cells.EditorCell rootCell, String role,
                                                                             SNode node) {
    if (role == null) {
      return null;
    }
    if (role.equals(rootCell.getRole()) && node == rootCell.getSNode()) {
      return rootCell;
    }
    if (rootCell instanceof EditorCell_Collection) {
      EditorCell_Collection collection = (EditorCell_Collection) rootCell;
      for (jetbrains.mps.openapi.editor.cells.EditorCell child : collection) {
        jetbrains.mps.openapi.editor.cells.EditorCell result = findNodeCellWithRole(child, role, node);
        if (result != null) {
          return result;
        }
      }
    }
    return null;
  }

  @Override
  public EditorCell findCellWithId(final SNode node, final @NotNull String id) {
    final EditorCell bigCell = findNodeCell(node);

    if (bigCell == null) {
      return null;
    }

    final jetbrains.mps.openapi.editor.cells.EditorCell[] result = new jetbrains.mps.openapi.editor.cells.EditorCell[]{null};
    myRepository.getModelAccess().runReadAction(() -> result[0] = findCellWithIdWithingBigCell(bigCell, id, node));

    return (EditorCell) result[0];
  }

  private jetbrains.mps.openapi.editor.cells.EditorCell findCellWithIdWithingBigCell(jetbrains.mps.openapi.editor.cells.EditorCell root, String id,
                                                                                     SNode node) {
    if (id == null) {
      return null;
    }

    if (id.equals(root.getCellId()) && root.getSNode() == node) {
      return root;
    }

    if (root instanceof jetbrains.mps.openapi.editor.cells.EditorCell_Collection) {
      for (jetbrains.mps.openapi.editor.cells.EditorCell child : ((jetbrains.mps.openapi.editor.cells.EditorCell_Collection) root)) {
        SNode childNode = child.getSNode();
        if (childNode == node || (childNode != null && AttributeOperations.isAttribute(childNode) && childNode.getParent() == node)) {
          jetbrains.mps.openapi.editor.cells.EditorCell result = findCellWithIdWithingBigCell(child, id, node);
          if (result != null) {
            return result;
          }
        }
      }
    }

    return null;
  }

  @Override
  public void rebuildEditorContent() {
    assertInEDT();
    getUpdater().update();
    relayout();
  }

  protected void assertInEDT() {
    LOG.assertLog(ThreadUtils.isInEDT(), "You should do this in EDT");
  }

  private void fireEditorWillBeDisposed() {
    for (EditorDisposeListener listener : new ArrayList<>(myDisposeListeners)) {
      listener.editorWillBeDisposed(this);
    }
    myDisposeListeners.clear();
  }

  /**
   * Adds a listener to be called when this instance is disposed. It is not necessary to remove a listener when it has been called since the collection of
   * listeners is cleared during the disposal.
   *
   * @param listener the listener to add
   */
  public void addDisposeListener(@NotNull EditorDisposeListener listener) {
    if (!myDisposeListeners.contains(listener)) {
      myDisposeListeners.add(listener);
    }
  }

  /**
   * Removes a listener so that it is no longer called when this instance is disposed. It is not necessary to remove a listener when it has been called since
   * the collection of listeners is cleared during the disposal.
   *
   * @param listener the listener to remove
   */
  public void removeDisposeListener(@NotNull EditorDisposeListener listener) {
    myDisposeListeners.remove(listener);
  }

  public jetbrains.mps.openapi.editor.cells.EditorCell findCellWeak(int x, int y) {
    jetbrains.mps.openapi.editor.cells.EditorCell cell = myRootCell.findLeaf(x, y);
    if (cell == null) {
      cell = myRootCell.findNearestLeafOnLine(x, y, Condition.TRUE_CONDITION);
    }
    return cell;
  }

  private void processMousePressed(MouseEvent mouseEvent) {
    requestFocus();
    processCoordSelection(mouseEvent);

    if (mouseEvent.getButton() == MouseEvent.BUTTON2) {
      goByCurrentReference();
    }

    boolean ctrlDown = com.intellij.openapi.util.SystemInfo.isMac ? mouseEvent.isMetaDown() : mouseEvent.isControlDown();
    if (ctrlDown) {
      if (mouseEvent.isAltDown()) {
        showCellError();
      } else {
        goByCurrentReference();
      }
    }
  }

  public void goByCurrentReference() {
    final DataContext dataContext = DataManager.getInstance().getDataContext(this);
    getModelAccess().executeCommand(new EditorCommand(getCommandContext()) {
      @Override
      protected void doExecute() {
        AnAction action = ActionManager.getInstance().getAction(MPSActions.EDITOR_GOTO_DECLARATION);
        if (action != null) {
          AnActionEvent event = ActionUtils.createEvent(ActionPlaces.EDITOR_POPUP, dataContext);
          ActionUtils.updateAndPerformAction(action, event);
        }
      }
    });
  }

  @NotNull
  private org.jetbrains.mps.openapi.module.ModelAccess getModelAccess() {
    return myRepository.getModelAccess();
  }

  public void showCellError() {
    final jetbrains.mps.openapi.editor.cells.EditorCell selectedCell = getSelectedCell();
    if (selectedCell != null) {
      getModelAccess().runReadAction(new Runnable() {
        @Override
        public void run() {
          final HighlighterMessage message = getHighlighterMessageFor(selectedCell);
          if (message == null || message.getErrorReporter() == null) {
            return;
          }
          final IErrorReporter herror = message.getErrorReporter();
          ThreadUtils.runInUIThreadNoWait(() -> {
            String s = message.getMessage();
            final Window window = SwingUtilities.windowForComponent(EditorComponent.this);
            final MPSErrorDialog dialog = new MPSErrorDialog(window, s, message.getStatus().getPresentation(), false);
            if (herror.getRuleNode() != null) {
              final boolean hasAdditionalRuleIds = !herror.getAdditionalRulesIds().isEmpty();
              final JButton button = new JButton();
              class ToRuleAction extends AbstractAction {
                private final SNodeReference myRule;
                private final JDialog myToDispose;

                public ToRuleAction(String title, SNodeReference rule, JDialog toDispose) {
                  super(title);
                  myRule = rule;
                  myToDispose = toDispose;
                }

                @Override
                public void actionPerformed(ActionEvent e) {
                  new EditorNavigator(getCurrentProject()).shallSelect(true).open(myRule);
                  myToDispose.dispose();
                }
              }
              AbstractAction action = new ToRuleAction("Go To Rule", herror.getRuleNode(), dialog) {
                @Override
                public void actionPerformed(ActionEvent e) {
                  if (hasAdditionalRuleIds) {
                    JPopupMenu popupMenu = new JPopupMenu();
                    for (final SNodeReference id : herror.getAdditionalRulesIds()) {
                      popupMenu.add(new ToRuleAction("Go To Rule " + id.getNodeId(), id, dialog));
                    }
                    popupMenu.add(new ToRuleAction("Go To Immediate Rule", herror.getRuleNode(), dialog));
                    popupMenu.show(button, 0, button.getHeight());
                  } else {
                    super.actionPerformed(e);
                  }
                }
              };
              button.setAction(action);
              dialog.addButton(button);
            }
            dialog.initializeUI();
            dialog.setVisible(true);
          });
        }
      });
    }
  }

  public void addLeftMarginPressListener(LeftMarginMouseListener listener) {
    myLeftMarginPressListeners.add(listener);
  }

  public void removeLeftMarginPressListener(LeftMarginMouseListener listener) {
    myLeftMarginPressListeners.remove(listener);
  }

  public List<LeftMarginMouseListener> getLeftMarginPressListeners() {
    return Collections.unmodifiableList(myLeftMarginPressListeners);
  }

  public void clearLeftMarginListeners() {
    myLeftMarginPressListeners.clear();
  }

  private void processCoordSelection(MouseEvent mouseEvent) {
    jetbrains.mps.openapi.editor.cells.EditorCell newSelectedCell = myRootCell.findLeaf(mouseEvent.getX(), mouseEvent.getY());
    if (newSelectedCell != null && CellTraversalUtil.getFoldedParent(newSelectedCell) != null) {
      // mouse was pressed on a cell representing folded collection
      return;
    }
    if (newSelectedCell == null || !newSelectedCell.isSelectable()) {
      newSelectedCell = myRootCell.findNearestLeafOnLine(mouseEvent.getX(), mouseEvent.getY(), jetbrains.mps.openapi.editor.cells.CellConditions.SELECTABLE);
    }

    if (newSelectedCell != null && (mouseEvent.getButton() != MouseEvent.BUTTON3 || !isUnderSelection(getSelectionManager().getSelection(), newSelectedCell))) {
      mySelectionManager.setSelection(newSelectedCell);
      ((EditorCell) newSelectedCell).processMousePressed(mouseEvent);
    }
  }

  private boolean isUnderSelection(Selection selection, jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    if (selection == null) {
      return false;
    }
    for (jetbrains.mps.openapi.editor.cells.EditorCell selectedCell : selection.getSelectedCells()) {
      if (CellTraversalUtil.isAncestorOrEquals(selectedCell, cell)) {
        return true;
      }
    }
    return false;
  }

  public void clearSelectionStack() {
    getSelectionManager().clearSelection();
  }

  public void pushSelection(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    getSelectionManager().pushSelection(getSelectionManager().createSelection(cell));
  }

//  public EditorCell popSelection() {
//    Selection selection = getSelectionManager().popSelection();
//    return selection instanceof SingularSelection ? ((SingularSelection) selection).getEditorCell() : null;
//  }

  public jetbrains.mps.openapi.editor.cells.EditorCell peekSelection() {
    return getSelectedCell();
  }

  public boolean selectionStackContains(EditorCell cell) {
    for (Selection nextSelection : getSelectionManager().getSelectionStackIterable()) {
      if (nextSelection instanceof SingularSelection) {
        if (((SingularSelection) nextSelection).getEditorCell().equals(cell)) {
          return true;
        }
      }
    }
    return false;
  }

  @Override
  public final void changeSelection(@NotNull jetbrains.mps.openapi.editor.cells.EditorCell newSelectedCell) {
    mySelectionManager.setSelection(newSelectedCell);
    showCellInViewPort(newSelectedCell);
  }

  // TODO: replace this method with selection listener
  private void showCellInViewPort(@NotNull jetbrains.mps.openapi.editor.cells.EditorCell newSelectedCell) {
    if (!hasUI()) {
      return;
    }
    if (getVisibleRect().isEmpty()) {
      final JViewport viewport = getViewport();
      viewport.addChangeListener(new ChangeListener() {
        @Override
        public void stateChanged(ChangeEvent e) {
          if (!getVisibleRect().isEmpty()) {
            viewport.removeChangeListener(this);
            if (getSelectedCell() != null) {
              scrollToCell(getSelectedCell());
            }
          }
        }
      });
    } else {
      scrollToCell(newSelectedCell);
    }
  }

  @Override
  public void scrollToNode(SNode node) {
    EditorCell cell = findNodeCell(node);
    if (cell != null) {
      scrollToCell(cell);
    }
  }

  public void ensureSelectionVisible() {
    if (getSelectedCell() == null) {
      return;
    }
    scrollToCell(getSelectedCell());
  }

  @Override
  public void scrollToCell(@NotNull jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    if (!hasUI() || getVisibleRect().isEmpty()) {
      return;
    }

    int viewportWidth = getViewport().getWidth();

    int x0;
    int width;

    if (cell instanceof EditorCell_Label) {
      EditorCell_Label cellLabel = (EditorCell_Label) cell;
      int caretX = cellLabel.getCaretX();
      int charWidth = cellLabel.getCharWidth();
      width = 4 * charWidth;
      x0 = caretX - 2 * charWidth;
    } else if (getDeepestSelectedCell() instanceof EditorCell_Label && cell.getWidth() > viewportWidth) {
      EditorCell_Label cellLabel = (EditorCell_Label) getDeepestSelectedCell();
      int caretX = cellLabel.getCaretX();
      int charWidth = cellLabel.getCharWidth();
      x0 = Math.max(cell.getX(), caretX + 2 * charWidth - viewportWidth);
      width = viewportWidth;
    } else {
      x0 = cell.getX();
      width = cell.getWidth();
    }

    Rectangle visibleRect = getVisibleRect();
    Rectangle rectangle = new Rectangle(x0, visibleRect.y, width, visibleRect.height);
    if (!rectangle.isEmpty()) {
      boolean adjustHorizontally = !visibleRect.contains(rectangle);
      if (adjustHorizontally) {
        if (width <= viewportWidth) {
          int x1 = Math.max(0, x0 + width - viewportWidth);
          scrollToRectIfNotVisible(
              expandRectangleOneLine(
                  new Rectangle(x1, cell.getY(), x0 - x1 + width, cell.getHeight()
                  )
              )
          );
        } else {
          scrollToRectIfNotVisible(
              expandRectangleOneLine(
                  new Rectangle(x0 - SCROLL_GAP, cell.getY(), viewportWidth + SCROLL_GAP, cell.getHeight()
                  )
              )
          );
        }
      } else {
        scrollToRectIfNotVisible(
            expandRectangleOneLine(
                new Rectangle(x0, cell.getY(), width, cell.getHeight()
                )
            )
        );
      }
    }
  }

  private void scrollToRectIfNotVisible(Rectangle rect) {
    if (getVisibleRect().contains(rect)) {
      return;
    }

    scrollRectToVisible(rect);
  }

  private Rectangle expandRectangleOneLine(Rectangle r) {
    Font defaultFont = EditorSettings.getInstance().getDefaultEditorFont();
    FontMetrics fontMetrics = getFontMetrics(defaultFont);
    int height = fontMetrics.getHeight();
    return new Rectangle(r.x, r.y - height, r.width, r.height + 2 * height);
  }

  @Override
  protected void paintComponent(Graphics gg) {
    EditorSettings setting = EditorSettings.getInstance();
    Graphics2D g = (Graphics2D) gg;

    turnOnAliasingIfPossible(g);

    g.setColor(getBackground());
    Rectangle bounds = g.getClipBounds();

    g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);

    jetbrains.mps.openapi.editor.cells.EditorCell deepestCell = getDeepestSelectedCell();
    if (deepestCell instanceof EditorCell_Label && ((EditorCell) deepestCell).isInClipRegion(g)) {
      EditorCell_Label label = (EditorCell_Label) deepestCell;

      g.setColor(setting.getCaretRowColor());
      g.fillRect(0, deepestCell.getY(), getWidth(),
                 deepestCell.getHeight() - deepestCell.getTopInset() - deepestCell.getBottomInset());

      g.setColor(EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.IDENTIFIER_UNDER_CARET_ATTRIBUTES).getBackgroundColor());
      g.fillRect(deepestCell.getX() + label.getLeftInset(),
                 deepestCell.getY(),
                 deepestCell.getWidth() - label.getLeftInset() - label.getRightInset(),
                 deepestCell.getHeight() - deepestCell.getTopInset() - deepestCell.getBottomInset());
    }

    List<AdditionalPainter> additionalPainters = getAdditionalPainters();
    for (AdditionalPainter additionalPainter : additionalPainters) {
      if (additionalPainter.paintsBackground()) {
        additionalPainter.paintBackground(g, this);
      }
    }

    if (isDisposed()) {
      return;
    }
    myRootCell.relayout();

    if (myRootCell.isInClipRegion(g)) {
      g.setColor(EditorColorsManager.getInstance().getGlobalScheme().getColor(EditorColors.RIGHT_MARGIN_COLOR));
      int boundPosition = myRootCell.getX() + setting.getVerticalBoundWidth();
      g.drawLine(boundPosition, 0, boundPosition, getHeight());

      myRootCell.paint(g);
    }

    for (AdditionalPainter additionalPainter : additionalPainters) {
      if (additionalPainter.paintsAbove()) {
        additionalPainter.paint(g, this);
      }
    }

  }

  Dimension getPreferredComponentSize() {
    return isDisposed() ? new Dimension(0, 0) : new Dimension(myRootCell.getWidth() + myShiftX + 10, myRootCell.getHeight() + myShiftY + 10);
  }

  @Override
  public Dimension getPreferredScrollableViewportSize() {
    return getPreferredSize();
  }

  @Override
  public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
    if (orientation == SwingConstants.VERTICAL) {
      return 20;
    } else { // if orientation == SwingConstants.HORIZONTAL
      return 20;
    }
  }

  @Override
  public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
    return visibleRect.height;
  }

  @Override
  public boolean getScrollableTracksViewportWidth() {
    assert hasUI();
    return myScrollPane.getViewport().getWidth() > getPreferredSize().width;
  }

  @Override
  public boolean getScrollableTracksViewportHeight() {
    assert hasUI();
    return myScrollPane.getViewport().getHeight() > getPreferredSize().height;
  }

  @Override
  public jetbrains.mps.openapi.editor.cells.EditorCell getDeepestSelectedCell() {
    if (isDisposed()) {
      return null;
    }
    Selection deepestSelection = getSelectionManager().getDeepestSelection();
    return deepestSelection instanceof SingularSelection ? ((SingularSelection) deepestSelection).getEditorCell() : null;
  }

  @Nullable
  public jetbrains.mps.openapi.editor.cells.EditorCell getSelectedCell() {
    if (isDisposed()) {
      return null;
    }
    Selection currentSelection = getSelectionManager().getSelection();
    return currentSelection instanceof SingularSelection ? ((SingularSelection) currentSelection).getEditorCell() : null;
  }

  @NotNull
  public SelectionManager getSelectionManager() {
    return mySelectionManager;
  }

  @NotNull
  @Override
  public Updater getUpdater() {
    return myUpdater;
  }

  public KeyboardHandler peekKeyboardHandler() {
    return myKbdHandlersStack.peek();
  }

  public KeyboardHandler popKeyboardHandler() {
    return myKbdHandlersStack.pop();
  }

  public void pushKeyboardHandler(KeyboardHandler kbdHandler) {
    myKbdHandlersStack.push(kbdHandler);
  }

  public void setMouseEventHandler(MouseListener l) {
    assert myMouseEventHandler == null : "Mouse event handler was already specified";
    addMouseListener(myMouseEventHandler = l);
  }

  public void removeMouseEventHandler() {
    assert myMouseEventHandler != null : "Mouse event handler was was not specified";
    removeMouseListener(myMouseEventHandler);
    myMouseEventHandler = null;
  }

  private boolean areMouseEventsBlocked() {
    return myMouseEventHandler != null;
  }

  @NotNull
  public ITypeContextOwner getTypecheckingContextOwner() {
    return this;
  }

  protected void acquireTypeCheckingContext() {
    getModelAccess().runReadAction(() -> TypeContextManager.getInstance().acquireTypecheckingContext(getNodeForTypechecking(), EditorComponent.this));
  }

  protected void releaseTypeCheckingContext() {
    getModelAccess().runReadAction(() -> TypeContextManager.getInstance().releaseTypecheckingContext(EditorComponent.this));
  }

  /**
   * Returns false iff the containing root has been changed as a result of this method call.
   */
  protected boolean updateContainingRoot(SNode node) {
    return myNode != node;
  }

  public SNode getNodeForTypechecking() {
    return getEditedNode();
  }

  public void sendKeyEvent(KeyEvent keyEvent) {
    if (keyEvent.getID() == KeyEvent.KEY_PRESSED) {
      processKeyPressed(keyEvent);
    } else if (keyEvent.getID() == KeyEvent.KEY_RELEASED) {
      processKeyReleased(keyEvent);
    }
  }

  @Override
  public void update() {
    final jetbrains.mps.project.Project p = getCurrentProject();
    final Highlighter highlighter = p == null ? null : p.getComponent(Highlighter.class);
    getModelAccess().runReadAction(() -> {
      //TODO: check if it's necessary to clear updater caches here?..
      rebuildAfterReloadModel();
      if (highlighter != null) {
        highlighter.resetCheckedStateInBackground(EditorComponent.this);
      }
      rebuildEditorContent();
    });
  }

  public void processKeyPressed(final KeyEvent keyEvent) {
    if (keyEvent.isConsumed() || isDisposed()) {
      return;
    }

    // hardcoded "update" action
    if (keyEvent.getKeyCode() == KeyEvent.VK_F5 && noKeysDown(keyEvent)) {
      //this lock should be obtained before the following read action to avoid deadlock
      update();
      keyEvent.consume();
      return;
    }

    if (keyEvent.getKeyCode() == KeyEvent.VK_F11 && noKeysDown(keyEvent)) {
      relayout();
      keyEvent.consume();
      return;
    }

    if (isKeyboardHandlerProcessingEnabled(keyEvent) && peekKeyboardHandler().processKeyPressed(getEditorContext(), keyEvent)) {
      keyEvent.consume();
    }
    repaintExternalComponent();
  }

  public void processKeyReleased(final KeyEvent keyEvent) {
    if (keyEvent.isConsumed()) {
      return;
    }

    if (isKeyboardHandlerProcessingEnabled(keyEvent) && peekKeyboardHandler().processKeyReleased(getEditorContext(), keyEvent)) {
      keyEvent.consume();
    }
    repaintExternalComponent();
  }

  public void processKeyTyped(final KeyEvent keyEvent) {
    if (keyEvent.isConsumed()) {
      return;
    }

    if (isKeyboardHandlerProcessingEnabled(keyEvent) && peekKeyboardHandler().processKeyTyped(getEditorContext(), keyEvent)) {
      keyEvent.consume();
    }
    repaintExternalComponent();
  }

  private boolean isKeyboardHandlerProcessingEnabled(KeyEvent keyEvent) {
    if (!ReadOnlyUtil.isSelectionReadOnlyInEditor(this)) {
      return true;
    }
    CellActionType actionType = getActionType(keyEvent, getEditorContext());
    if (actionType != null) {
      switch (actionType) {
        case SELECT_LEFT:
        case SELECT_RIGHT:
        case SELECT_HOME:
        case SELECT_END:
        case PAGE_UP:
        case PAGE_DOWN:
        case NEXT:
        case PREV:
          return true;
      }
    }
    return false;
  }

  public CommandContext getCommandContext() {
    return myCommandContext;
  }

  private CommandContext getNoSelectionProcessingCommandContext() {
    // Hiding command start/stop notification.
    return new CommandContextWrapper(getCommandContext()) {
      @Override
      public void commandStarted() {
      }

      @Override
      public void commandFinished() {
      }
    };
  }

  <T> T runRead(final Computable<T> c) {
    final ComputeRunnable<T> r = new ComputeRunnable<>(c);
    getModelAccess().runReadAction(r);
    return r.getResult();
  }

  /**
   * @deprecated editor component does not always correspond to a project!
   */
  @Nullable
  @Deprecated
  @ToRemove(version = 3.5)
  protected final jetbrains.mps.project.Project getCurrentProject() {
    // there's no need in MPSProject, there's just no key for generic MPS project in MPSCommonDataKeys.
    final MPSProject p = MPSCommonDataKeys.MPS_PROJECT.getData(DataManager.getInstance().getDataContext(this));
    return p != null ? p : ProjectHelper.getProject(myRepository);
  }

  public boolean activateNodeSubstituteChooser(jetbrains.mps.openapi.editor.cells.EditorCell editorCell, boolean resetPattern) {
    return activateNodeSubstituteChooser(editorCell, resetPattern, false);
  }

  public boolean activateNodeSubstituteChooser(jetbrains.mps.openapi.editor.cells.EditorCell editorCell, boolean resetPattern, boolean isSmart) {
    if (myNodeSubstituteChooser.isVisible()) {
      return true;
      //todo: rebuild menu if smartness changed
    }

    // try to obtain substitute info
    SubstituteInfo substituteInfo = null;
    if (editorCell != null) {
      substituteInfo = editorCell.getSubstituteInfo();
    }

    return activateNodeSubstituteChooser(editorCell, substituteInfo, resetPattern, isSmart);
  }

  public boolean activateNodeSubstituteChooser(jetbrains.mps.openapi.editor.cells.EditorCell editorCell, SubstituteInfo substituteInfo,
                                               boolean resetPattern) {
    return activateNodeSubstituteChooser(editorCell, substituteInfo, resetPattern, false);
  }

  public boolean activateNodeSubstituteChooser(jetbrains.mps.openapi.editor.cells.EditorCell editorCell, SubstituteInfo substituteInfo, boolean resetPattern,
                                               boolean isSmart) {
    if (editorCell == null || substituteInfo == null) {
      return false;
    }

    // do substitute...
    LOG.debug("substitute info : " + substituteInfo);
    NodeSubstitutePatternEditor patternEditor = ((EditorCell) editorCell).createSubstitutePatternEditor();
    if (resetPattern) {
      patternEditor.toggleReplaceMode();
    }
    final String pattern = patternEditor.getPattern();

    // user changed text within this cell before pressing Ctrl+Space
    // or cell has no text at this moment
    boolean originalTextChanged = !patternEditor.getText().equals(substituteInfo.getOriginalText());
    // caret is at the end of line
    boolean atTheEndOfLine = pattern.equals(patternEditor.getText());
    // 1st - try to do substitution with current pattern (if cursor at the end of text)
    substituteInfo.invalidateActions();
    if (originalTextChanged || atTheEndOfLine) {
      List<SubstituteAction> matchingActions = getMatchingActions(editorCell, substituteInfo, isSmart, pattern);
      if (matchingActions.size() == 1 && pattern.length() > 0) {
        // Just one applicable action in the completion menu
        final SubstituteAction theAction = matchingActions.get(0);
        Pair<Boolean, Boolean> canSubstitute =
            new ModelAccessHelper(getRepository()).runReadAction(() -> new Pair(theAction.canSubstitute(pattern), theAction.canSubstituteStrictly(pattern)));

        // Invoking this action immediately if originalText was changed or
        // the cursor is at the end of line and !theAction.canSubstituteStrictly(pattern)
        // [means, action will change underlying code]
        if (canSubstitute.o1 && (originalTextChanged || editorCell.isErrorState() || (atTheEndOfLine && !canSubstitute.o2))) {
          getRepository().getModelAccess().executeCommand(new EditorCommand(getEditorContext()) {
            @Override
            protected void doExecute() {
              theAction.substitute(getEditorContext(), pattern);
            }
          });
          return true;
        }
      }
    }

    myNodeSubstituteChooser.setNodeSubstituteInfo(substituteInfo);
    myNodeSubstituteChooser.setPatternEditor(patternEditor);
    myNodeSubstituteChooser.setIsSmart(isSmart);
    myNodeSubstituteChooser.setContextCell(editorCell);
    myNodeSubstituteChooser.setVisible(true);
    return true;
  }

  private List<SubstituteAction> getMatchingActions(final jetbrains.mps.openapi.editor.cells.EditorCell editorCell, final SubstituteInfo substituteInfo,
                                                    final boolean isSmart, final String pattern) {
    return runRead(new Computable<List<SubstituteAction>>() {
      @Override
      public List<SubstituteAction> compute() {
        final ITypeContextOwner contextOwner = isSmart ? new NonReusableTypecheckingContextOwner() : getTypecheckingContextOwner();
        return TypeContextManager.getInstance().runTypeCheckingComputation(contextOwner, myNode,
                                                                           context -> isSmart ?
                                                                                      substituteInfo.getSmartMatchingActions(pattern, false, editorCell) :
                                                                                      substituteInfo.getMatchingActions(pattern, false)
        );
      }
    });
  }

  private void deactivateSubstituteChooser() {
    myNodeSubstituteChooser.setVisible(false);
  }

  public NodeSubstituteChooser getNodeSubstituteChooser() {
    return myNodeSubstituteChooser;
  }

  void setNodeInformationDialog(NodeInformationDialog dialog) {
    myNodeInformationDialog = dialog;
  }

  public boolean hasNodeInformationDialog() {
    return myNodeInformationDialog != null;
  }

  @Override
  public void paint(Graphics g) {
    super.paint(g);
    Selection selection = getSelectionManager().getSelection();
    if (selection != null) {
      ((SelectionInternal) selection).paintSelection((Graphics2D) g);
    }
  }

  public Set<SNode> getNodesCellDependOn(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    return myUpdater.getRelatedNodes(cell);
  }

  public Set<SNodeReference> getCopyOfRefTargetsCellDependsOn(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    return myUpdater.getRelatedRefTargets(cell);
  }

  @Nullable
  public EditorCell getBigValidCellForNode(SNode node) {
    EditorCell result = findNodeCell(node);
    if (isValid(result)) {
      return result;
    }
    return null;
  }

  public boolean isValid(jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    if (cell == null) {
      return false;
    }
    return ((EditorCell_Basic) cell).isInTree() && cell.getEditorComponent() == this;
  }

  public jetbrains.mps.openapi.editor.cells.EditorCell changeSelectionWRTFocusPolicy(@NotNull jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    jetbrains.mps.openapi.editor.cells.EditorCell focusPolicyCell = FocusPolicyUtil.findFocusedCell(cell);
    jetbrains.mps.openapi.editor.cells.EditorCell toSelect;
    if (focusPolicyCell == null || (focusPolicyCell == cell && !FocusPolicyUtil.hasFocusPolicy(focusPolicyCell))) {
      toSelect = CellFinderUtil.findChildByManyFinders(cell, Finder.FIRST_ERROR, Finder.FIRST_EDITABLE, Finder.FIRST_SELECTABLE_LEAF);
    } else {
      toSelect = focusPolicyCell;
    }
    if (toSelect == null) {
      toSelect = cell;
    }
    changeSelection(toSelect);

    if (toSelect instanceof EditorCell_Label) {
      EditorCell_Label label = (EditorCell_Label) toSelect;
      jetbrains.mps.editor.runtime.style.CaretPosition defaultCaretPosition = label.getStyle().get(StyleAttributes.DEFAULT_CARET_POSITION);
      if (defaultCaretPosition != null) {
        if (defaultCaretPosition == jetbrains.mps.editor.runtime.style.CaretPosition.FIRST) {
          label.home();
        }
        if (defaultCaretPosition == jetbrains.mps.editor.runtime.style.CaretPosition.LAST) {
          label.end();
        }
      } else if (!toSelect.isErrorState()) {
        label.end();
      }
    }

    return toSelect;
  }

  private void setEditorContext(@Nullable SModel model, @NotNull SRepository repository) {
    if (myEditorContext != null && myEditorContext.getModel() == model && myEditorContext.getRepository() == repository) {
      myEditorContext.reset();
      return;
    }
    myEditorContext = createEditorContext(model, repository);
  }

  /**
   * This method is called from the constructor, so you cannot use local variables and any other
   * EditorComponent state here!
   *
   * @param model
   * @param repository
   */
  @NotNull
  protected EditorContext createEditorContext(@Nullable SModel model, @NotNull SRepository repository) {
    return new EditorContext(this, model, repository, getEditorConfiguration(), createContextAssistantManager(repository));
  }

  protected ContextAssistantManager createContextAssistantManager(SRepository repository) {
    return DefaultContextAssistantManager.newInstance(this, repository);
  }

  @Override
  public boolean isReadOnly() {
    return myReadOnly;
  }

  public void setPopupMenuEnabled(boolean popupMenuEnabled) {
    myPopupMenuEnabled = popupMenuEnabled;
  }

  @Override
  @Nullable
  public Object getData(@NonNls String dataId) {
    if (mySearchPanel != null && mySearchPanel.isVisible() && mySearchPanel.isTextFieldFocused()) {
      return null;
    }
    if (isDisposed()) {
      return null;
    }
    //MPSDK
    if (dataId.equals(MPSCommonDataKeys.NODE.getName())) {
      return getSelectedNode();
    }
    if (dataId.equals(MPSCommonDataKeys.NODES.getName())) {
      return getSelectedNodes();
    }
    if (dataId.equals(MPSEditorDataKeys.CONTEXT_MODEL.getName())) {
      return runRead(new Computable() {
        @Override
        public Object compute() {
          SNode node = getRootCell().getSNode();
          if (node == null) {
            return null;
          }
          SModel model = node.getModel();
          if (model == null) {
            return null; //removed model
          }
          return model;
        }
      });
    }
    if (dataId.equals(MPSEditorDataKeys.CONTEXT_MODULE.getName())) {
      // todo: copy-paste
      return runRead(new Computable() {
        @Override
        public Object compute() {
          SNode node = getRootCell().getSNode();
          if (node == null) {
            return null;
          }
          SModel model = node.getModel();
          if (model == null) {
            return null; //removed model
          }
          return model.getModule();
        }
      });
    }
    if (dataId.equals(MPSEditorDataKeys.EDITOR_CONTEXT.getName())) {
      return createEditorContextForActions();
    }
    if (dataId.equals(MPSEditorDataKeys.EDITOR_CELL.getName())) {
      return getSelectedCell();
    }
    if (dataId.equals(MPSEditorDataKeys.EDITOR_COMPONENT.getName())) {
      return this;
    }
    if (dataId.equals(MPSCommonDataKeys.PLACE.getName())) {
      return ActionPlace.EDITOR;
    }

    //PDK
    if (dataId.equals(PlatformDataKeys.CUT_PROVIDER.getName())) {
      return new MyCutProvider();
    }
    if (dataId.equals(PlatformDataKeys.COPY_PROVIDER.getName())) {
      return new MyCopyProvider();
    }
    if (dataId.equals(PlatformDataKeys.PASTE_PROVIDER.getName()) && (isFocusOwner() || mySearchPanel == null || !mySearchPanel.isVisible())) {
      return new MyPasteProvider();
    }

    if (dataId.equals(SelectInContext.DATA_KEY.getName())) {
      ProjectViewSelectInProvider selectInHelper =
          ApplicationManager.getApplication() == null ? null : ApplicationManager.getApplication().getComponent(ProjectViewSelectInProvider.class);
      if (selectInHelper == null) {
        return null;
      }
      return selectInHelper.getContext(getCurrentProject(), myNodePointer);
    }

    //not found
    return null;
  }

  private void commitAllCellValues() {
    final List<EditorCell_Property> cellsToCommit = getCellsToCommit();
    if (cellsToCommit.isEmpty()) {
      return;
    }

    getModelAccess().executeCommand(new EditorCommandAdapter(() -> doCommitAll(cellsToCommit), getNoSelectionProcessingCommandContext()));
  }

  private void setDefaultSelection() {
    if (getSelectionManager().getSelection() != null) {
      return;
    }

    EditorCell rootCell = getRootCell();
    if (rootCell instanceof EditorCell_Collection) {
      jetbrains.mps.openapi.editor.cells.EditorCell focusPolicyCell = FocusPolicyUtil.findFocusedCell(rootCell);
      jetbrains.mps.openapi.editor.cells.EditorCell toSelect;
      if (focusPolicyCell == null || (focusPolicyCell == rootCell && !FocusPolicyUtil.hasFocusPolicy(focusPolicyCell))) {
        toSelect = CellFinderUtil.findChildByManyFinders(rootCell, Finder.FIRST_EDITABLE, Finder.FIRST_SELECTABLE_LEAF);
      } else {
        toSelect = focusPolicyCell;
      }
      if (toSelect == null) {
        toSelect = rootCell;
      }
      changeSelection(toSelect);
      return;
    }
    if (rootCell != null && rootCell.isSelectable()) {
      changeSelection(rootCell);
    }
  }

  private void closeSubstituteChooser(Component newFocusOwner) {
    if (myNodeSubstituteChooser.getWindow() != null &&
        (myNodeSubstituteChooser.getWindow().isAncestorOf(newFocusOwner) || myNodeSubstituteChooser.getWindow() == newFocusOwner)) {
      return;
    }
    deactivateSubstituteChooser();
  }

  private void activateCaretBlinker() {
    myEditorConfiguration.caretManager.setActiveEditor(this);
  }

  private void deActivateCaretBlinker() {
    myEditorConfiguration.caretManager.unsetActiveEditor(this);
  }

  private List<EditorCell_Property> getCellsToCommit() {
    List<EditorCell_Property> cells = new ArrayList<>();

    for (EditorCell_Property cell : getCellTracker().getTransactionalCells()) {
      if (cell.hasUncommittedValue()) {
        cells.add(cell);
      }
    }

    return cells;
  }

  private void doCommitAll(List<EditorCell_Property> cells) {
    for (EditorCell_Property cell : cells) {
      cell.commit();
    }
  }

  /**
   * It's possible that associated module was already removed from MPSModuleRepository (for example - transient models
   * modules are currently removed from MPSModuleRepository before next code generation session). In this case currently
   * open editor should be closed as a result of another notification processing. We need to suppress editor update
   * process in this case because an editor is not in valid state right now.
   */
  private boolean isModuleDisposed() {
    // TODO review
    return false; // myOperationContext != null && myOperationContext.getModule() == null;
  }

  private boolean isProjectDisposed() {
    final jetbrains.mps.project.Project p = getCurrentProject();
    // XXX NOTE, we check the project is there, i.e. missing project is not treated as disposed. Is it right?
    return p != null && p.isDisposed();
  }

  private boolean isNodeDisposed() {
    SNode node = getEditedNode();
    return node != null && !SNodeUtil.isAccessible(node, myEditorContext.getRepository());
  }

  public CellTracker getCellTracker() {
    return myCellTracker;
  }

  public BracesHighlighter getBracesHighlighter() {
    return myBracesHighlighter;
  }

  public void rebuildAfterReloadModel() {
    releaseTypeCheckingContext();
    if (myNodePointer != null) {
      myNode = myNodePointer.resolve(getRepository());
      myEditorContext = createEditorContext(myNode == null ? null : myNode.getModel(), myRepository);
      myUpdater.clearExplicitHints();
    }
    myCommandContext.updateContextNode();
    acquireTypeCheckingContext();
  }

  private static class MyBaseAction extends BaseAction implements DumbAware {
    private final KeyMapAction myAction;
    private final EditorContext myEditorContext;

    public MyBaseAction(KeyMapAction action, EditorContext editorContext) {
      super("" + action.getDescriptionText());
      myAction = action;
      myEditorContext = editorContext;
      String keyStrokeString = action.getKeyStroke();
      if (keyStrokeString == null || keyStrokeString.length() == 0) {
        return;
      }
      KeyStroke keyStroke = KeyStroke.getKeyStroke(keyStrokeString);
      if (keyStroke == null) {
        LOG.error("Invalid keystroke (" + keyStrokeString + ") specified for the action: " + action.getClass().getName());
        return;
      }
      KeyboardShortcut shortcut = new KeyboardShortcut(keyStroke, null);
      KeymapManager.getInstance().getKeymap(KeymapManager.DEFAULT_IDEA_KEYMAP).addShortcut(getActionId(), shortcut);
      setExecuteOutsideCommand(true);
    }

    @Override
    protected void doExecute(AnActionEvent e, Map<String, Object> _params) {
      myEditorContext.getRepository().getModelAccess().executeCommand(new EditorCommand(myEditorContext) {
        @Override
        protected void doExecute() {
          try {
            myAction.execute(myEditorContext);
          } catch (Throwable t) {
            LOG.error(t);
          }
        }
      });
    }
  }

  public interface EditorDisposeListener {
    void editorWillBeDisposed(@NotNull EditorComponent component);
  }

  public void repaint(@NotNull jetbrains.mps.openapi.editor.cells.EditorCell cell) {
    // The +1 for height takes into account decorations such as selection or border, which may currently be drawn outside the cell.
    repaint(0, cell.getY(), getWidth(), cell.getHeight() + 1);
  }

  @Override
  public ActionHandler getActionHandler() {
    return myActionHandler;
  }

  /**
   * Return true if UI focus "within" this editor component. Means: owned by this component or any child-components
   * (in case of component cells displayed inside this editor). Context assistant is a special case: if it is focused,
   * the editor is considered inactive.
   *
   * @return true if the focus is inside this EditorComponent
   */
  public boolean isActive() {
    if (isContextAssistantFocused()) {
      return false;
    }
    if (isFocusOwner()) {
      return true;
    }

    Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
    return isAncestorOf(focusOwner);
  }

  private boolean isContextAssistantFocused() {
    ContextAssistant activeAssistant = myEditorContext.getContextAssistantManager().getActiveAssistant();
    return activeAssistant != null && activeAssistant.hasFocus();
  }

  @NotNull
  public EditorHighlighter getHighlighter() {
    return myHighlighter;
  }

  @NotNull
  public EditorComponentFocusTracker getFocusTracker() {
    return myFocusTracker;
  }

  private class ReferenceUnderliner {
    private EditorCell myLastReferenceCell;

    private ReferenceUnderliner() {
      addKeyListener(new KeyAdapter() {
        @Override
        public void keyPressed(KeyEvent e) {
          if (e.getKeyCode() == getKeyCode()) {
            setControlOver();
          }
        }

        @Override
        public void keyReleased(KeyEvent e) {
          if (e.getKeyCode() == getKeyCode()) {
            clearControlOver();
          }
        }

        private int getKeyCode() {
          return com.intellij.openapi.util.SystemInfo.isMac ? KeyEvent.VK_META : KeyEvent.VK_CONTROL;
        }
      });
      addMouseMotionListener(new MouseMotionListener() {
        @Override
        public void mouseDragged(MouseEvent e) {
        }

        @Override
        public void mouseMoved(MouseEvent e) {
          if (!myEditorContext.getNodeEditorComponent().isFocusOwner()) {
            return;
          }
          if (isDisposed()) {
            myLastReferenceCell = null;
            return;
          }

          clearControlOver();
          if (!(com.intellij.openapi.util.SystemInfo.isMac ? e.isMetaDown() : e.isControlDown())) {
            myLastReferenceCell = null;
            return;
          }

          final jetbrains.mps.openapi.editor.cells.EditorCell editorCell = myRootCell.findLeaf(e.getX(), e.getY());
          if (editorCell == null) {
            myLastReferenceCell = null;
            return;
          }
          SNode snodeWRTReference = runRead(() -> isInvalid() ? null : APICellAdapter.getSNodeWRTReference(editorCell));
          if (editorCell.getSNode() == snodeWRTReference) {
            myLastReferenceCell = null;
            return;
          }
          myLastReferenceCell = (EditorCell) editorCell;

          setControlOver();
        }
      });
      addFocusListener(new FocusListener() {
        @Override
        public void focusGained(FocusEvent e) {
        }

        @Override
        public void focusLost(FocusEvent e) {
          clearControlOver();
          myLastReferenceCell = null;
        }
      });
    }

    private void clearControlOver() {
      if (myLastReferenceCell != null) {
        myLastReferenceCell.getStyle().set(StyleAttributes.CONTROL_OVERED_REFERENCE, false);
        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        repaintExternalComponent();
      }
    }

    private void setControlOver() {
      if (myLastReferenceCell != null) {
        myLastReferenceCell.getStyle().set(StyleAttributes.CONTROL_OVERED_REFERENCE, true);
        setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        repaintExternalComponent();
      }
    }
  }

  private class MyCutProvider implements CutProvider {
    @Override
    public void performCut(@NotNull final DataContext dataContext) {
      getModelAccess().executeCommandInEDT(new EditorCommand(getCommandContext()) {
        @Override
        protected void doExecute() {
          if (isInvalid() || !isCutEnabled(dataContext)) {
            return;
          }
          jetbrains.mps.openapi.editor.cells.EditorCell selectedCell = getSelectedCell();
          if (selectedCell != null) {
            myActionHandler.executeAction(selectedCell, CellActionType.CUT);
          } else {
            getSelectionManager().getSelection().executeAction(CellActionType.CUT);
          }
        }
      });
    }

    @Override
    public boolean isCutEnabled(@NotNull DataContext dataContext) {
      return !(isDisposed() || isInvalidLightweight() || getSelectionManager().getSelection() == null ||
               ReadOnlyUtil.canDeleteNodes(EditorComponent.this, getSelectedNodes()));
    }

    @Override
    public boolean isCutVisible(@NotNull DataContext dataContext) {
      return true;
    }
  }

  private class MyCopyProvider implements CopyProvider {
    @Override
    public void performCopy(@NotNull DataContext dataContext) {
      getModelAccess().executeCommandInEDT(new EditorCommand(getCommandContext()) {
        @Override
        protected void doExecute() {
          if (isDisposed() || isInvalid()) {
            return;
          }
          jetbrains.mps.openapi.editor.cells.EditorCell selectedCell = getSelectedCell();
          if (selectedCell != null) {
            myActionHandler.executeAction(selectedCell, CellActionType.COPY);
          } else {
            getSelectionManager().getSelection().executeAction(CellActionType.COPY);
          }
        }
      });
    }

    @Override
    public boolean isCopyEnabled(@NotNull DataContext dataContext) {
      return !isDisposed() && !isInvalidLightweight() && getSelectionManager().getSelection() != null;
    }

    @Override
    public boolean isCopyVisible(@NotNull DataContext dataContext) {
      return true;
    }
  }

  private class MyPasteProvider implements PasteProvider {
    @Override
    public void performPaste(@NotNull final DataContext dataContext) {
      EditorComponent.this.performPaste();
    }

    @Override
    public boolean isPastePossible(@NotNull DataContext dataContext) {
      return EditorComponent.this.isPastePossible();
    }

    @Override
    public boolean isPasteEnabled(@NotNull DataContext dataContext) {
      return true;
    }
  }

  private void performPaste() {
    getModelAccess().executeCommandInEDT(new EditorCommand(getCommandContext()) {
      @Override
      protected void doExecute() {
        if (isInvalid() || !isPastePossible()) {
          return;
        }
        jetbrains.mps.openapi.editor.cells.EditorCell selectedCell = getSelectedCell();
        if (selectedCell != null) {
          myActionHandler.executeAction(selectedCell, CellActionType.PASTE);
        } else {
          getSelectionManager().getSelection().executeAction(CellActionType.PASTE);
        }
      }
    });
  }

  private boolean isPastePossible() {
    return !(isDisposed() || isInvalidLightweight() || ReadOnlyUtil.isSelectionReadOnlyInEditor(EditorComponent.this) ||
             getSelectionManager().getSelection() == null);
  }

  @Override
  public InputMethodRequests getInputMethodRequests() {
// Uncomment at the moment https://youtrack.jetbrains.com/issue/JRE-252 is fixed
//    if (ReadOnlyUtil.isSelectionReadOnlyInEditor(this)) {
//      return null;
//    }

    hideMessageToolTip();

    if (myInputMethodRequests == null) {
      myInputMethodRequests = new InputMethodRequestsImpl(this);
      addInputMethodListener(new InputMethodListenerImpl(this));
    }
    return myInputMethodRequests;
  }

  /**
   * This is a copy of com.intellij.openapi.editor.impl.EditorImpl.MyScrollBar classwith some additional code
   */
  private static final Field decrButtonField;
  private static final Field incrButtonField;

  static {
    try {
      decrButtonField = BasicScrollBarUI.class.getDeclaredField("decrButton");
      decrButtonField.setAccessible(true);

      incrButtonField = BasicScrollBarUI.class.getDeclaredField("incrButton");
      incrButtonField.setAccessible(true);
    } catch (NoSuchFieldException e) {
      throw new IllegalStateException(e);
    }
  }

  class MyScrollBar extends JBScrollBar implements IdeGlassPane.TopComponent, TooltipComponent {
    @NonNls
    private static final String APPLE_LAF_AQUA_SCROLL_BAR_UI_CLASS = "apple.laf.AquaScrollBarUI";
    private ScrollBarUI myPersistentUI;

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

    void setPersistentUI(ScrollBarUI ui) {
      myPersistentUI = ui;
      setUI(ui);
    }

    @Override
    public boolean canBePreprocessed(MouseEvent e) {
      return JBScrollPane.canBePreprocessed(e, this);
    }

    @Override
    public void setUI(ScrollBarUI ui) {
      if (myPersistentUI == null) {
        myPersistentUI = ui;
      }
      super.setUI(myPersistentUI);
    }

    /**
     * This is helper method. It returns height of the top (decrease) scroll bar
     * button. Please note, that it's possible to return real height only if scroll bar
     * is instance of BasicScrollBarUI. Otherwise it returns fake (but good enough :) )
     * value.
     */
    int getDecScrollButtonHeight() {
      ScrollBarUI barUI = getUI();
      Insets insets = getInsets();
      if (barUI instanceof ButtonlessScrollBarUI) {
        return insets.top + ((ButtonlessScrollBarUI) barUI).getDecrementButtonHeight();
      } else if (barUI instanceof BasicScrollBarUI) {
        try {
          JButton decrButtonValue = (JButton) decrButtonField.get(barUI);
          LOG.assertLog(decrButtonValue != null);
          return insets.top + decrButtonValue.getHeight();
        } catch (Exception exc) {
          throw new IllegalStateException(exc);
        }
      } else {
        return insets.top + 15;
      }
    }

    /**
     * This is helper method. It returns height of the bottom (increase) scroll bar
     * button. Please note, that it's possible to return real height only if scroll bar
     * is instance of BasicScrollBarUI. Otherwise it returns fake (but good enough :) )
     * value.
     */
    int getIncScrollButtonHeight() {
      ScrollBarUI barUI = getUI();
      Insets insets = getInsets();
      if (barUI instanceof ButtonlessScrollBarUI) {
        return insets.top + ((ButtonlessScrollBarUI) barUI).getIncrementButtonHeight();
      } else if (barUI instanceof BasicScrollBarUI) {
        try {
          JButton incrButtonValue = (JButton) incrButtonField.get(barUI);
          LOG.assertLog(incrButtonValue != null);
          return insets.bottom + incrButtonValue.getHeight();
        } catch (Exception exc) {
          throw new IllegalStateException(exc.getMessage());
        }
      } else if (APPLE_LAF_AQUA_SCROLL_BAR_UI_CLASS.equals(barUI.getClass().getName())) {
        return insets.bottom + 30;
      } else {
        return insets.bottom + 15;
      }
    }

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

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

    @Override
    public String getMPSTooltipText(MouseEvent mouseEvent) {
      if (getUI() instanceof TooltipComponent) {
        return ((TooltipComponent) getUI()).getMPSTooltipText(mouseEvent);
      }
      return null;
    }
  }
}
