com.google.gerrit.client.diff.SideBySide2.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.client.diff.SideBySide2.java

Source

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

package com.google.gerrit.client.diff;

import static com.google.gerrit.reviewdb.client.AccountDiffPreference.WHOLE_FILE_CONTEXT;
import static java.lang.Double.POSITIVE_INFINITY;

import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.JumpKeys;
import com.google.gerrit.client.account.DiffPreferences;
import com.google.gerrit.client.change.ChangeScreen2;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.changes.ChangeList;
import com.google.gerrit.client.diff.DiffInfo.FileMeta;
import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.extensions.common.ListChangesOption;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.KeyCommand;
import com.google.gwtexpui.globalkey.client.KeyCommandSet;
import com.google.gwtexpui.globalkey.client.ShowHelpCommand;

import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.BeforeSelectionChangeHandler;
import net.codemirror.lib.CodeMirror.GutterClickHandler;
import net.codemirror.lib.CodeMirror.LineClassWhere;
import net.codemirror.lib.CodeMirror.LineHandle;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.KeyMap;
import net.codemirror.lib.LineCharacter;
import net.codemirror.lib.ModeInjector;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

public class SideBySide2 extends Screen {
    private static final KeyMap RENDER_ENTIRE_FILE_KEYMAP = KeyMap.create().on("Ctrl-F", false);

    interface Binder extends UiBinder<FlowPanel, SideBySide2> {
    }

    private static final Binder uiBinder = GWT.create(Binder.class);

    enum FileSize {
        SMALL(0), LARGE(500), HUGE(4000);

        final int lines;

        FileSize(int n) {
            this.lines = n;
        }
    }

    @UiField(provided = true)
    Header header;

    @UiField(provided = true)
    DiffTable diffTable;

    private final Change.Id changeId;
    private final PatchSet.Id base;
    private final PatchSet.Id revision;
    private final String path;
    private DisplaySide startSide;
    private int startLine;
    private DiffPreferences prefs;

    private CodeMirror cmA;
    private CodeMirror cmB;
    private Element columnMarginA;
    private Element columnMarginB;
    private HandlerRegistration resizeHandler;
    private ScrollSynchronizer scrollSynchronizer;
    private DiffInfo diff;
    private FileSize fileSize;
    private ChunkManager chunkManager;
    private CommentManager commentManager;
    private SkipManager skipManager;

    private KeyCommandSet keysNavigation;
    private KeyCommandSet keysAction;
    private KeyCommandSet keysComment;
    private List<HandlerRegistration> handlers;
    private PreferencesAction prefsAction;
    private int reloadVersionId;

    public SideBySide2(PatchSet.Id base, PatchSet.Id revision, String path, DisplaySide startSide, int startLine) {
        this.base = base;
        this.revision = revision;
        this.changeId = revision.getParentKey();
        this.path = path;
        this.startSide = startSide;
        this.startLine = startLine;

        prefs = DiffPreferences.create(Gerrit.getAccountDiffPreference());
        handlers = new ArrayList<>(6);
        keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
        header = new Header(keysNavigation, base, revision, path);
        diffTable = new DiffTable(this, base, revision, path);
        add(uiBinder.createAndBindUi(this));
        addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
    }

    @Override
    protected void onInitUI() {
        super.onInitUI();
        setHeaderVisible(false);
    }

    @Override
    protected void onLoad() {
        super.onLoad();

        CallbackGroup cmGroup = new CallbackGroup();
        CodeMirror.initLibrary(cmGroup.add(CallbackGroup.<Void>emptyCallback()));
        final CallbackGroup group = new CallbackGroup();
        final AsyncCallback<Void> modeInjectorCb = group.add(CallbackGroup.<Void>emptyCallback());

        DiffApi.diff(revision, path).base(base).wholeFile().intraline(prefs.intralineDifference())
                .ignoreWhitespace(prefs.ignoreWhitespace()).get(cmGroup.addFinal(new GerritCallback<DiffInfo>() {
                    @Override
                    public void onSuccess(DiffInfo diffInfo) {
                        diff = diffInfo;
                        fileSize = bucketFileSize(diffInfo);
                        if (prefs.syntaxHighlighting()) {
                            if (fileSize.compareTo(FileSize.SMALL) > 0) {
                                modeInjectorCb.onSuccess(null);
                            } else {
                                injectMode(diffInfo, modeInjectorCb);
                            }
                        } else {
                            modeInjectorCb.onSuccess(null);
                        }
                    }
                }));

        final CommentsCollections comments = new CommentsCollections();
        comments.load(base, revision, path, group);

        RestApi call = ChangeApi.detail(changeId.get());
        ChangeList.addOptions(call, EnumSet.of(ListChangesOption.ALL_REVISIONS));
        call.get(group.add(new GerritCallback<ChangeInfo>() {
            @Override
            public void onSuccess(ChangeInfo info) {
                info.revisions().copyKeysIntoChildren("name");
                JsArray<RevisionInfo> list = info.revisions().values();
                RevisionInfo.sortRevisionInfoByNumber(list);
                diffTable.set(prefs, list, diff);
                header.setChangeInfo(info);
            }
        }));

        ConfigInfoCache.get(changeId,
                group.addFinal(new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide2.this) {
                    @Override
                    protected void preDisplay(ConfigInfoCache.Entry result) {
                        commentManager = new CommentManager(SideBySide2.this, base, revision, path,
                                result.getCommentLinkProcessor());
                        setTheme(result.getTheme());
                        display(comments);
                    }
                }));
    }

    @Override
    public void onShowView() {
        super.onShowView();
        Window.enableScrolling(false);
        JumpKeys.enable(false);
        if (prefs.hideTopMenu()) {
            Gerrit.setHeaderVisible(false);
        }
        resizeHandler = Window.addResizeHandler(new ResizeHandler() {
            @Override
            public void onResize(ResizeEvent event) {
                resizeCodeMirror();
            }
        });

        final int height = getCodeMirrorHeight();
        operation(new Runnable() {
            @Override
            public void run() {
                cmA.setHeight(height);
                cmB.setHeight(height);
                cmA.refresh();
                cmB.refresh();
            }
        });
        setLineLength(prefs.lineLength());
        diffTable.refresh();

        if (startLine == 0) {
            DiffChunkInfo d = chunkManager.getFirst();
            if (d != null) {
                if (d.isEdit() && d.getSide() == DisplaySide.A) {
                    startSide = DisplaySide.B;
                    startLine = lineOnOther(d.getSide(), d.getStart()).getLine() + 1;
                } else {
                    startSide = d.getSide();
                    startLine = d.getStart() + 1;
                }
            }
        }
        if (startSide != null && startLine > 0) {
            int line = startLine - 1;
            CodeMirror cm = getCmFromSide(startSide);
            if (cm.lineAtHeight(height - 20) < line) {
                cm.scrollToY(cm.heightAtLine(line, "local") - 0.5 * height);
            }
            cm.setCursor(LineCharacter.create(line));
            cm.focus();
        } else {
            cmA.setCursor(LineCharacter.create(0));
            cmA.focus();
        }
        if (Gerrit.isSignedIn() && prefs.autoReview()) {
            header.autoReview();
        }
        prefetchNextFile();
    }

    @Override
    protected void onUnload() {
        super.onUnload();

        removeKeyHandlerRegistrations();
        if (commentManager != null) {
            CallbackGroup group = new CallbackGroup();
            commentManager.saveAllDrafts(group);
            group.done();
        }
        if (resizeHandler != null) {
            resizeHandler.removeHandler();
            resizeHandler = null;
        }
        if (cmA != null) {
            cmA.getWrapperElement().removeFromParent();
        }
        if (cmB != null) {
            cmB.getWrapperElement().removeFromParent();
        }
        if (prefsAction != null) {
            prefsAction.hide();
        }

        Window.enableScrolling(true);
        Gerrit.setHeaderVisible(true);
        JumpKeys.enable(true);
    }

    private void removeKeyHandlerRegistrations() {
        for (HandlerRegistration h : handlers) {
            h.removeHandler();
        }
        handlers.clear();
    }

    private void registerCmEvents(final CodeMirror cm) {
        cm.on("beforeSelectionChange", onSelectionChange(cm));
        cm.on("cursorActivity", updateActiveLine(cm));
        cm.on("gutterClick", onGutterClick(cm));
        cm.on("focus", updateActiveLine(cm));
        cm.addKeyMap(KeyMap.create().on("A", upToChange(true)).on("U", upToChange(false))
                .on("[", header.navigate(Direction.PREV)).on("]", header.navigate(Direction.NEXT))
                .on("R", header.toggleReviewed()).on("O", commentManager.toggleOpenBox(cm))
                .on("Enter", commentManager.toggleOpenBox(cm)).on("C", commentManager.insertNewDraft(cm))
                .on("N", maybeNextVimSearch(cm)).on("P", chunkManager.diffChunkNav(cm, Direction.PREV))
                .on("Shift-A", diffTable.toggleA()).on("Shift-M", header.reviewedAndNext())
                .on("Shift-N", maybePrevVimSearch(cm)).on("Shift-P", commentManager.commentNav(cm, Direction.PREV))
                .on("Shift-O", commentManager.openCloseAll(cm))
                .on("Shift-Left", moveCursorToSide(cm, DisplaySide.A))
                .on("Shift-Right", moveCursorToSide(cm, DisplaySide.B)).on("I", new Runnable() {
                    public void run() {
                        switch (getIntraLineStatus()) {
                        case OFF:
                        case OK:
                            toggleShowIntraline();
                            break;
                        default:
                            break;
                        }
                    }
                }).on("','", new Runnable() {
                    @Override
                    public void run() {
                        prefsAction.show();
                    }
                }).on("Shift-/", new Runnable() {
                    @Override
                    public void run() {
                        new ShowHelpCommand().onKeyPress(null);
                    }
                }).on("Space", new Runnable() {
                    @Override
                    public void run() {
                        CodeMirror.handleVimKey(cm, "<C-d>");
                    }
                }).on("Shift-Space", new Runnable() {
                    @Override
                    public void run() {
                        CodeMirror.handleVimKey(cm, "<C-u>");
                    }
                }).on("Ctrl-F", new Runnable() {
                    @Override
                    public void run() {
                        CodeMirror.handleVimKey(cm, "/");
                    }
                }).on("Ctrl-A", new Runnable() {
                    @Override
                    public void run() {
                        cm.execCommand("selectAll");
                    }
                }));
        if (prefs.renderEntireFile()) {
            cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
        }
    }

    private BeforeSelectionChangeHandler onSelectionChange(final CodeMirror cm) {
        return new BeforeSelectionChangeHandler() {
            private InsertCommentBubble bubble;

            @Override
            public void handle(CodeMirror cm, LineCharacter anchor, LineCharacter head) {
                if (anchor == head || (anchor.getLine() == head.getLine() && anchor.getCh() == head.getCh())) {
                    if (bubble != null) {
                        bubble.setVisible(false);
                    }
                    return;
                } else if (bubble == null) {
                    init(anchor);
                } else {
                    bubble.setVisible(true);
                }
                bubble.position(cm.charCoords(head, "local"));
            }

            private void init(LineCharacter anchor) {
                bubble = new InsertCommentBubble(commentManager, cm);
                add(bubble);
                cm.addWidget(anchor, bubble.getElement(), false);
            }
        };
    }

    @Override
    public void registerKeys() {
        super.registerKeys();

        keysNavigation.add(new UpToChangeCommand2(revision, 0, 'u'));
        keysNavigation.add(new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_LEFT, PatchUtil.C.focusSideA()),
                new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_RIGHT, PatchUtil.C.focusSideB()));
        keysNavigation.add(new NoOpKeyCommand(0, 'j', PatchUtil.C.lineNext()),
                new NoOpKeyCommand(0, 'k', PatchUtil.C.linePrev()));
        keysNavigation.add(new NoOpKeyCommand(0, 'n', PatchUtil.C.chunkNext2()),
                new NoOpKeyCommand(0, 'p', PatchUtil.C.chunkPrev2()));
        keysNavigation.add(new NoOpKeyCommand(KeyCommand.M_SHIFT, 'n', PatchUtil.C.commentNext()),
                new NoOpKeyCommand(KeyCommand.M_SHIFT, 'p', PatchUtil.C.commentPrev()));
        keysNavigation.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 'f', Gerrit.C.keySearch()));

        keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
        keysAction.add(new NoOpKeyCommand(0, KeyCodes.KEY_ENTER, PatchUtil.C.expandComment()));
        keysAction.add(new NoOpKeyCommand(0, 'o', PatchUtil.C.expandComment()));
        keysAction.add(new NoOpKeyCommand(KeyCommand.M_SHIFT, 'o', PatchUtil.C.expandAllCommentsOnCurrentLine()));
        if (Gerrit.isSignedIn()) {
            keysAction.add(new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
                @Override
                public void onKeyPress(KeyPressEvent event) {
                    header.toggleReviewed().run();
                }
            });
        }
        keysAction.add(new KeyCommand(KeyCommand.M_SHIFT, 'm', PatchUtil.C.markAsReviewedAndGoToNext()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                header.reviewedAndNext().run();
            }
        });
        keysAction.add(new KeyCommand(0, 'a', PatchUtil.C.openReply()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                upToChange(true).run();
            }
        });
        keysAction.add(new KeyCommand(KeyCommand.M_SHIFT, 'a', PatchUtil.C.toggleSideA()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                diffTable.toggleA().run();
            }
        });
        keysAction.add(new KeyCommand(0, ',', PatchUtil.C.showPreferences()) {
            @Override
            public void onKeyPress(KeyPressEvent event) {
                prefsAction.show();
            }
        });
        if (getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF
                || getIntraLineStatus() == DiffInfo.IntraLineStatus.OK) {
            keysAction.add(new KeyCommand(0, 'i', PatchUtil.C.toggleIntraline()) {
                @Override
                public void onKeyPress(KeyPressEvent event) {
                    toggleShowIntraline();
                }
            });
        }

        if (Gerrit.isSignedIn()) {
            keysAction.add(new NoOpKeyCommand(0, 'c', PatchUtil.C.commentInsert()));
            keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet());
            keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's', PatchUtil.C.commentSaveDraft()));
            keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE, PatchUtil.C.commentCancelEdit()));
        } else {
            keysComment = null;
        }

        removeKeyHandlerRegistrations();
        handlers.add(GlobalKey.add(this, keysAction));
        handlers.add(GlobalKey.add(this, keysNavigation));
        if (keysComment != null) {
            handlers.add(GlobalKey.add(this, keysComment));
        }
        handlers.add(ShowHelpCommand.addFocusHandler(new FocusHandler() {
            @Override
            public void onFocus(FocusEvent event) {
                cmB.focus();
            }
        }));
    }

    private void display(final CommentsCollections comments) {
        setThemeStyles(prefs.theme().isDark());
        setShowTabs(prefs.showTabs());
        setShowIntraline(prefs.intralineDifference());
        if (prefs.showLineNumbers()) {
            diffTable.addStyleName(DiffTable.style.showLineNumbers());
        }

        cmA = newCM(diff.meta_a(), diff.text_a(), DisplaySide.A, diffTable.cmA);
        cmB = newCM(diff.meta_b(), diff.text_b(), DisplaySide.B, diffTable.cmB);
        diffTable.overview.init(cmB);
        chunkManager = new ChunkManager(this, cmA, cmB, diffTable.overview);
        skipManager = new SkipManager(this, commentManager);

        columnMarginA = DOM.createDiv();
        columnMarginB = DOM.createDiv();
        columnMarginA.setClassName(DiffTable.style.columnMargin());
        columnMarginB.setClassName(DiffTable.style.columnMargin());
        cmA.getMoverElement().appendChild(columnMarginA);
        cmB.getMoverElement().appendChild(columnMarginB);

        if (prefs.renderEntireFile() && !canEnableRenderEntireFile(prefs)) {
            // CodeMirror is too slow to layout an entire huge file.
            prefs.renderEntireFile(false);
        }

        operation(new Runnable() {
            public void run() {
                // Estimate initial CM3 height, fixed up in onShowView.
                int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18);
                cmA.setHeight(height);
                cmB.setHeight(height);

                render(diff);
                commentManager.render(comments, prefs.expandAllComments());
                skipManager.render(prefs.context(), diff);
            }
        });

        registerCmEvents(cmA);
        registerCmEvents(cmB);
        scrollSynchronizer = new ScrollSynchronizer(diffTable, cmA, cmB, chunkManager.getLineMapper());

        prefsAction = new PreferencesAction(this, prefs);
        header.init(prefsAction);

        if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
            Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
                @Override
                public boolean execute() {
                    if (prefs.syntaxHighlighting() && isAttached()) {
                        setSyntaxHighlighting(prefs.syntaxHighlighting());
                    }
                    return false;
                }
            }, 250);
        }
    }

    private CodeMirror newCM(DiffInfo.FileMeta meta, String contents, DisplaySide side, Element parent) {
        String mode = fileSize == FileSize.SMALL ? getContentType(meta) : null;
        return CodeMirror.create(side, parent,
                Configuration.create().set("readOnly", true).set("cursorBlinkRate", 0).set("cursorHeight", 0.85)
                        .set("lineNumbers", prefs.showLineNumbers()).set("tabSize", prefs.tabSize())
                        .set("mode", mode).set("lineWrapping", false).set("styleSelectedText", true)
                        .set("showTrailingSpace", prefs.showWhitespaceErrors()).set("keyMap", "vim_ro")
                        .set("theme", prefs.theme().name().toLowerCase()).set("value", meta != null ? contents : "")
                        .set("viewportMargin", prefs.renderEntireFile() ? POSITIVE_INFINITY : 10));
    }

    DiffInfo.IntraLineStatus getIntraLineStatus() {
        return diff.intraline_status();
    }

    boolean canEnableRenderEntireFile(DiffPreferences prefs) {
        return fileSize.compareTo(FileSize.HUGE) < 0
                || (prefs.context() != WHOLE_FILE_CONTEXT && prefs.context() < 100);
    }

    String getContentType() {
        return getContentType(diff.meta_b());
    }

    void setThemeStyles(boolean d) {
        if (d) {
            diffTable.addStyleName(DiffTable.style.dark());
        } else {
            diffTable.removeStyleName(DiffTable.style.dark());
        }
    }

    void setShowTabs(boolean b) {
        if (b) {
            diffTable.addStyleName(DiffTable.style.showTabs());
        } else {
            diffTable.removeStyleName(DiffTable.style.showTabs());
        }
    }

    void setLineLength(int columns) {
        columnMarginA.getStyle().setMarginLeft(columns * cmA.defaultCharWidth(), Unit.PX);

        columnMarginB.getStyle().setMarginLeft(columns * cmB.defaultCharWidth(), Unit.PX);
    }

    void setShowLineNumbers(boolean b) {
        cmA.setOption("lineNumbers", b);
        cmB.setOption("lineNumbers", b);
        if (b) {
            diffTable.addStyleName(DiffTable.style.showLineNumbers());
        } else {
            diffTable.removeStyleName(DiffTable.style.showLineNumbers());
        }
    }

    void setShowIntraline(boolean b) {
        if (b && getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF) {
            reloadDiffInfo();
        } else if (b) {
            diffTable.removeStyleName(DiffTable.style.noIntraline());
        } else {
            diffTable.addStyleName(DiffTable.style.noIntraline());
        }
    }

    private void toggleShowIntraline() {
        prefs.intralineDifference(!prefs.intralineDifference());
        setShowIntraline(prefs.intralineDifference());
        prefsAction.update();
    }

    void setSyntaxHighlighting(boolean b) {
        if (b) {
            injectMode(diff, new AsyncCallback<Void>() {
                @Override
                public void onSuccess(Void result) {
                    if (prefs.syntaxHighlighting()) {
                        cmA.setOption("mode", getContentType(diff.meta_a()));
                        cmB.setOption("mode", getContentType(diff.meta_b()));
                    }
                }

                @Override
                public void onFailure(Throwable caught) {
                    prefs.syntaxHighlighting(false);
                }
            });
        } else {
            cmA.setOption("mode", (String) null);
            cmB.setOption("mode", (String) null);
        }
    }

    void setContext(final int context) {
        operation(new Runnable() {
            @Override
            public void run() {
                skipManager.removeAll();
                skipManager.render(context, diff);
                diffTable.overview.refresh();
            }
        });
    }

    private void render(DiffInfo diff) {
        header.setNoDiff(diff);
        chunkManager.render(diff);
    }

    CodeMirror otherCm(CodeMirror me) {
        return me == cmA ? cmB : cmA;
    }

    CodeMirror getCmFromSide(DisplaySide side) {
        return side == DisplaySide.A ? cmA : cmB;
    }

    LineOnOtherInfo lineOnOther(DisplaySide side, int line) {
        return chunkManager.getLineMapper().lineOnOther(side, line);
    }

    private void clearActiveLine(CodeMirror cm) {
        if (cm.hasActiveLine()) {
            LineHandle activeLine = cm.getActiveLine();
            cm.removeLineClass(activeLine, LineClassWhere.WRAP, DiffTable.style.activeLine());
            cm.setActiveLine(null);
        }
    }

    private Runnable updateActiveLine(final CodeMirror cm) {
        final CodeMirror other = otherCm(cm);
        return new Runnable() {
            public void run() {
                // The rendering of active lines has to be deferred. Reflow
                // caused by adding and removing styles chokes Firefox when arrow
                // key (or j/k) is held down. Performance on Chrome is fine
                // without the deferral.
                //
                Scheduler.get().scheduleDeferred(new ScheduledCommand() {
                    @Override
                    public void execute() {
                        operation(new Runnable() {
                            public void run() {
                                LineHandle handle = cm.getLineHandleVisualStart(cm.getCursor("end").getLine());
                                if (cm.hasActiveLine() && cm.getActiveLine().equals(handle)) {
                                    return;
                                }

                                clearActiveLine(cm);
                                clearActiveLine(other);
                                cm.setActiveLine(handle);
                                cm.addLineClass(handle, LineClassWhere.WRAP, DiffTable.style.activeLine());
                                LineOnOtherInfo info = lineOnOther(cm.side(), cm.getLineNumber(handle));
                                if (info.isAligned()) {
                                    LineHandle oLineHandle = other.getLineHandle(info.getLine());
                                    other.setActiveLine(oLineHandle);
                                    other.addLineClass(oLineHandle, LineClassWhere.WRAP,
                                            DiffTable.style.activeLine());
                                }
                            }
                        });
                    }
                });
            }
        };
    }

    private GutterClickHandler onGutterClick(final CodeMirror cm) {
        return new GutterClickHandler() {
            @Override
            public void handle(CodeMirror instance, int line, String gutter, NativeEvent clickEvent) {
                if (clickEvent.getButton() == NativeEvent.BUTTON_LEFT && !clickEvent.getMetaKey()
                        && !clickEvent.getAltKey() && !clickEvent.getCtrlKey() && !clickEvent.getShiftKey()) {
                    if (!(cm.hasActiveLine() && cm.getLineNumber(cm.getActiveLine()) == line)) {
                        cm.setCursor(LineCharacter.create(line));
                    }
                    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
                        @Override
                        public void execute() {
                            commentManager.insertNewDraft(cm).run();
                        }
                    });
                }
            }
        };
    }

    private Runnable upToChange(final boolean openReplyBox) {
        return new Runnable() {
            public void run() {
                CallbackGroup group = new CallbackGroup();
                commentManager.saveAllDrafts(group);
                group.done();
                group.addListener(new GerritCallback<Void>() {
                    @Override
                    public void onSuccess(Void result) {
                        String b = base != null ? String.valueOf(base.get()) : null;
                        String rev = String.valueOf(revision.get());
                        Gerrit.display(PageLinks.toChange(changeId, b, rev),
                                new ChangeScreen2(changeId, b, rev, openReplyBox));
                    }
                });
            }
        };
    }

    private Runnable moveCursorToSide(final CodeMirror cmSrc, DisplaySide sideDst) {
        final CodeMirror cmDst = getCmFromSide(sideDst);
        if (cmDst == cmSrc) {
            return new Runnable() {
                @Override
                public void run() {
                }
            };
        }

        final DisplaySide sideSrc = cmSrc.side();
        return new Runnable() {
            public void run() {
                if (cmSrc.hasActiveLine()) {
                    cmDst.setCursor(LineCharacter
                            .create(lineOnOther(sideSrc, cmSrc.getLineNumber(cmSrc.getActiveLine())).getLine()));
                }
                cmDst.focus();
            }
        };
    }

    private Runnable maybePrevVimSearch(final CodeMirror cm) {
        return new Runnable() {
            @Override
            public void run() {
                if (cm.hasVimSearchHighlight()) {
                    CodeMirror.handleVimKey(cm, "N");
                } else {
                    commentManager.commentNav(cm, Direction.NEXT).run();
                }
            }
        };
    }

    private Runnable maybeNextVimSearch(final CodeMirror cm) {
        return new Runnable() {
            @Override
            public void run() {
                if (cm.hasVimSearchHighlight()) {
                    CodeMirror.handleVimKey(cm, "n");
                } else {
                    chunkManager.diffChunkNav(cm, Direction.NEXT).run();
                }
            }
        };
    }

    void updateRenderEntireFile() {
        cmA.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
        cmB.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
        if (prefs.renderEntireFile()) {
            cmA.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
            cmB.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
        }

        cmA.setOption("viewportMargin", prefs.renderEntireFile() ? POSITIVE_INFINITY : 10);
        cmB.setOption("viewportMargin", prefs.renderEntireFile() ? POSITIVE_INFINITY : 10);
    }

    void resizeCodeMirror() {
        int height = getCodeMirrorHeight();
        cmA.setHeight(height);
        cmB.setHeight(height);
        diffTable.overview.refresh();
    }

    private int getCodeMirrorHeight() {
        int rest = Gerrit.getHeaderFooterHeight() + header.getOffsetHeight() + diffTable.getHeaderHeight() + 5; // Estimate
        return Window.getClientHeight() - rest;
    }

    void syncScroll(DisplaySide masterSide) {
        if (scrollSynchronizer != null) {
            scrollSynchronizer.syncScroll(masterSide);
        }
    }

    private String getContentType(DiffInfo.FileMeta meta) {
        return prefs.syntaxHighlighting() && meta != null && meta.content_type() != null
                ? ModeInjector.getContentType(meta.content_type())
                : null;
    }

    private void injectMode(DiffInfo diffInfo, AsyncCallback<Void> cb) {
        new ModeInjector().add(getContentType(diffInfo.meta_a())).add(getContentType(diffInfo.meta_b())).inject(cb);
    }

    DiffPreferences getPrefs() {
        return prefs;
    }

    ChunkManager getChunkManager() {
        return chunkManager;
    }

    CommentManager getCommentManager() {
        return commentManager;
    }

    SkipManager getSkipManager() {
        return skipManager;
    }

    void operation(final Runnable apply) {
        cmA.operation(new Runnable() {
            @Override
            public void run() {
                cmB.operation(new Runnable() {
                    @Override
                    public void run() {
                        apply.run();
                    }
                });
            }
        });
    }

    private void prefetchNextFile() {
        String nextPath = header.getNextPath();
        if (nextPath != null) {
            DiffApi.diff(revision, nextPath).base(base).wholeFile().intraline(prefs.intralineDifference())
                    .ignoreWhitespace(prefs.ignoreWhitespace()).get(new AsyncCallback<DiffInfo>() {
                        @Override
                        public void onSuccess(DiffInfo info) {
                            new ModeInjector().add(getContentType(info.meta_a())).add(getContentType(info.meta_b()))
                                    .inject(CallbackGroup.<Void>emptyCallback());
                        }

                        @Override
                        public void onFailure(Throwable caught) {
                        }
                    });
        }
    }

    void reloadDiffInfo() {
        final int id = ++reloadVersionId;
        DiffApi.diff(revision, path).base(base).wholeFile().intraline(prefs.intralineDifference())
                .ignoreWhitespace(prefs.ignoreWhitespace()).get(new GerritCallback<DiffInfo>() {
                    @Override
                    public void onSuccess(DiffInfo diffInfo) {
                        if (id == reloadVersionId && isAttached()) {
                            diff = diffInfo;
                            operation(new Runnable() {
                                @Override
                                public void run() {
                                    skipManager.removeAll();
                                    chunkManager.reset();
                                    diffTable.overview.clearDiffMarkers();
                                    setShowIntraline(prefs.intralineDifference());
                                    render(diff);
                                    skipManager.render(prefs.context(), diff);
                                }
                            });
                        }
                    }
                });
    }

    private static FileSize bucketFileSize(DiffInfo diff) {
        FileMeta a = diff.meta_a();
        FileMeta b = diff.meta_b();
        FileSize[] sizes = FileSize.values();
        for (int i = sizes.length - 1; 0 <= i; i--) {
            FileSize s = sizes[i];
            if ((a != null && s.lines <= a.lines()) || (b != null && s.lines <= b.lines())) {
                return s;
            }
        }
        return FileSize.SMALL;
    }
}