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

Java tutorial

Introduction

Here is the source code for com.google.gerrit.client.diff.DiffScreen.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 impl ied.
// 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.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
import static java.lang.Double.POSITIVE_INFINITY;

import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.DiffPreferences;
import com.google.gerrit.client.change.ChangeScreen;
import com.google.gerrit.client.change.FileTable;
import com.google.gerrit.client.changes.ChangeApi;
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.info.ChangeInfo;
import com.google.gerrit.client.info.ChangeInfo.CommitInfo;
import com.google.gerrit.client.info.ChangeInfo.EditInfo;
import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.info.FileInfo;
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.client.GeneralPreferencesInfo.DiffView;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
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.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.UiField;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
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 java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.BeforeSelectionChangeHandler;
import net.codemirror.lib.CodeMirror.GutterClickHandler;
import net.codemirror.lib.CodeMirror.LineHandle;
import net.codemirror.lib.KeyMap;
import net.codemirror.lib.Pos;
import net.codemirror.mode.ModeInfo;
import net.codemirror.mode.ModeInjector;
import net.codemirror.theme.ThemeLoader;

/** Base class for SideBySide and Unified */
abstract class DiffScreen extends Screen {
    private static final KeyMap RENDER_ENTIRE_FILE_KEYMAP = KeyMap.create().propagate("Ctrl-F").propagate("Ctrl-G")
            .propagate("Shift-Ctrl-G");

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

        final int lines;

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

    private final Change.Id changeId;
    final DiffObject base;
    final PatchSet.Id revision;
    final String path;
    final DiffPreferences prefs;
    final SkipManager skipManager;

    private DisplaySide startSide;
    private int startLine;
    private Change.Status changeStatus;

    private HandlerRegistration resizeHandler;
    private DiffInfo diff;
    private FileSize fileSize;
    private EditInfo edit;

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

    @UiField(provided = true)
    Header header;

    DiffScreen(DiffObject base, DiffObject revision, String path, DisplaySide startSide, int startLine,
            DiffView diffScreenType) {
        this.base = base;
        this.revision = revision.asPatchSetId();
        this.changeId = revision.asPatchSetId().getParentKey();
        this.path = path;
        this.startSide = startSide;
        this.startLine = startLine;

        prefs = DiffPreferences.create(Gerrit.getDiffPreferences());
        handlers = new ArrayList<>(6);
        keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
        header = new Header(keysNavigation, base, revision, path, diffScreenType, prefs);
        skipManager = new SkipManager(this);
    }

    @Override
    protected void onInitUI() {
        super.onInitUI();
        setHeaderVisible(false);
        setWindowTitle(FileInfo.getFileName(path));
    }

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

        CallbackGroup group1 = new CallbackGroup();
        final CallbackGroup group2 = new CallbackGroup();

        CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
            final AsyncCallback<Void> themeCallback = group2.addEmpty();

            @Override
            public void onSuccess(Void result) {
                // Load theme after CM library to ensure theme can override CSS.
                ThemeLoader.loadTheme(prefs.theme(), themeCallback);
            }

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

        DiffApi.diff(revision, path).base(base.asPatchSetId()).wholeFile().intraline(prefs.intralineDifference())
                .ignoreWhitespace(prefs.ignoreWhitespace()).get(group1.addFinal(new GerritCallback<DiffInfo>() {
                    final AsyncCallback<Void> modeInjectorCb = group2.addEmpty();

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

        if (Gerrit.isSignedIn()) {
            ChangeApi.edit(changeId.get(), group2.add(new AsyncCallback<EditInfo>() {
                @Override
                public void onSuccess(EditInfo result) {
                    edit = result;
                }

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

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

        countParents(group2);

        RestApi call = ChangeApi.detail(changeId.get());
        ChangeList.addOptions(call, EnumSet.of(ListChangesOption.ALL_REVISIONS));
        call.get(group2.add(new AsyncCallback<ChangeInfo>() {
            @Override
            public void onSuccess(ChangeInfo info) {
                changeStatus = info.status();
                info.revisions().copyKeysIntoChildren("name");
                if (edit != null) {
                    edit.setName(edit.commit().commit());
                    info.setEdit(edit);
                    info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
                }
                String currentRevision = info.currentRevision();
                boolean current = currentRevision != null
                        && revision.get() == info.revision(currentRevision)._number();
                JsArray<RevisionInfo> list = info.revisions().values();
                RevisionInfo.sortRevisionInfoByNumber(list);
                getDiffTable().set(prefs, list, parents, diff, edit != null, current, changeStatus.isOpen(),
                        diff.binary());
                header.setChangeInfo(info);
            }

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

        ConfigInfoCache.get(changeId, group2.addFinal(getScreenLoadCallback(comments)));
    }

    private void countParents(CallbackGroup cbg) {
        ChangeApi.revision(changeId.get(), revision.getId()).view("commit")
                .get(cbg.add(new AsyncCallback<CommitInfo>() {
                    @Override
                    public void onSuccess(CommitInfo info) {
                        parents = info.parents().length();
                    }

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

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

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

    KeyCommandSet getKeysNavigation() {
        return keysNavigation;
    }

    KeyCommandSet getKeysAction() {
        return keysAction;
    }

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

        removeKeyHandlerRegistrations();
        if (getCommentManager() != null) {
            CallbackGroup group = new CallbackGroup();
            getCommentManager().saveAllDrafts(group);
            group.done();
        }
        if (resizeHandler != null) {
            resizeHandler.removeHandler();
            resizeHandler = null;
        }
        for (CodeMirror cm : getCms()) {
            if (cm != null) {
                cm.getWrapperElement().removeFromParent();
            }
        }
        if (prefsAction != null) {
            prefsAction.hide();
        }

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

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

    void registerCmEvents(CodeMirror cm) {
        cm.on("cursorActivity", updateActiveLine(cm));
        cm.on("focus", updateActiveLine(cm));
        KeyMap keyMap = 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", getCommentManager().toggleOpenBox(cm))
                .on("N", maybeNextVimSearch(cm)).on("Ctrl-Alt-E", openEditScreen(cm))
                .on("P", getChunkManager().diffChunkNav(cm, Direction.PREV)).on("Shift-M", header.reviewedAndNext())
                .on("Shift-N", maybePrevVimSearch(cm))
                .on("Shift-P", getCommentManager().commentNav(cm, Direction.PREV))
                .on("Shift-O", getCommentManager().openCloseAll(cm)).on("I", () -> {
                    switch (getIntraLineStatus()) {
                    case OFF:
                    case OK:
                        toggleShowIntraline();
                        break;
                    case FAILURE:
                    case TIMEOUT:
                    default:
                        break;
                    }
                }).on("','", prefsAction::show).on("Shift-/", () -> new ShowHelpCommand().onKeyPress(null))
                .on("Space", () -> cm.vim().handleKey("<C-d>")).on("Shift-Space", () -> cm.vim().handleKey("<C-u>"))
                .on("Ctrl-F", () -> cm.execCommand("find")).on("Ctrl-G", () -> cm.execCommand("findNext"))
                .on("Enter", maybeNextCmSearch(cm)).on("Shift-Ctrl-G", () -> cm.execCommand("findPrev"))
                .on("Shift-Enter", () -> cm.execCommand("findPrev")).on("Esc", () -> {
                    cm.setCursor(cm.getCursor());
                    cm.execCommand("clearSearch");
                    cm.vim().handleEx("nohlsearch");
                }).on("Ctrl-A", () -> cm.execCommand("selectAll"))
                .on("G O", () -> Gerrit.display(PageLinks.toChangeQuery("status:open")))
                .on("G M", () -> Gerrit.display(PageLinks.toChangeQuery("status:merged")))
                .on("G A", () -> Gerrit.display(PageLinks.toChangeQuery("status:abandoned")));
        if (Gerrit.isSignedIn()) {
            keyMap.on("G I", () -> Gerrit.display(PageLinks.MINE))
                    .on("G D", () -> Gerrit.display(PageLinks.toChangeQuery("owner:self is:draft")))
                    .on("G C", () -> Gerrit.display(PageLinks.toChangeQuery("has:draft")))
                    .on("G W", () -> Gerrit.display(PageLinks.toChangeQuery("is:watched status:open")))
                    .on("G S", () -> Gerrit.display(PageLinks.toChangeQuery("is:starred")));
        }

        if (revision.get() != 0) {
            cm.on("beforeSelectionChange", onSelectionChange(cm));
            cm.on("gutterClick", onGutterClick(cm));
            keyMap.on("C", getCommentManager().newDraftCallback(cm));
        }
        CodeMirror.normalizeKeyMap(keyMap); // Needed to for multi-stroke keymaps
        cm.addKeyMap(keyMap);
    }

    void maybeRegisterRenderEntireFileKeyMap(CodeMirror cm) {
        if (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, Pos anchor, Pos head) {
                if (anchor.equals(head)) {
                    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(Pos anchor) {
                bubble = new InsertCommentBubble(getCommentManager(), cm);
                add(bubble);
                cm.addWidget(anchor, bubble.getElement());
            }
        };
    }

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

        keysNavigation.add(new UpToChangeCommand(revision, 0, 'u'));
        keysNavigation.add(new NoOpKeyCommand(0, 'j', PatchUtil.C.lineNext()),
                new NoOpKeyCommand(0, 'k', PatchUtil.C.linePrev()));
        keysNavigation.add(new NoOpKeyCommand(0, 'n', PatchUtil.C.chunkNext()),
                new NoOpKeyCommand(0, 'p', PatchUtil.C.chunkPrev()));
        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 NoOpKeyCommand(KeyCommand.M_CTRL | KeyCommand.M_ALT, 'e', Gerrit.C.keyEditor()));
        }
        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(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;
        }
    }

    void registerHandlers() {
        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(getFocusHandler()));
    }

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

    abstract CodeMirror newCm(DiffInfo.FileMeta meta, String contents, Element parent);

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

    void setShowLineNumbers(boolean b) {
        if (b) {
            getDiffTable().addStyleName(Resources.I.diffTableStyle().showLineNumbers());
        } else {
            getDiffTable().removeStyleName(Resources.I.diffTableStyle().showLineNumbers());
        }
    }

    void setShowIntraline(boolean b) {
        if (b && getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF) {
            reloadDiffInfo();
        } else if (b) {
            getDiffTable().removeStyleName(Resources.I.diffTableStyle().noIntraline());
        } else {
            getDiffTable().addStyleName(Resources.I.diffTableStyle().noIntraline());
        }
    }

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

    abstract void setSyntaxHighlighting(boolean b);

    void setContext(int context) {
        operation(() -> {
            skipManager.removeAll();
            skipManager.render(context, diff);
            updateRenderEntireFile();
        });
    }

    private int adjustCommitMessageLine(int line) {
        /* When commit messages are shown in the diff screen they include
          a header block that looks like this:
            
          1 Parent:     deadbeef (Parent commit title)
          2 Author:     A. U. Thor <author@example.com>
          3 AuthorDate: 2015-02-27 19:20:52 +0900
          4 Commit:     A. U. Thor <author@example.com>
          5 CommitDate: 2015-02-27 19:20:52 +0900
          6 [blank line]
          7 Commit message title
          8
          9 Commit message body
         10 ...
         11 ...
            
        If the commit is a merge commit, both parent commits are listed in the
        first two lines instead of a 'Parent' line:
            
          1 Merge Of:   deadbeef (Parent 1 commit title)
          2             beefdead (Parent 2 commit title)
            
        */

        // Offset to compensate for header lines until the blank line
        // after 'CommitDate'
        int offset = 6;

        // Adjust for merge commits, which have two parent lines
        if (diff.textB().startsWith("Merge")) {
            offset += 1;
        }

        // If the cursor is inside the header line, reset to the first line of the
        // commit message. Otherwise if the cursor is on an actual line of the commit
        // message, adjust the line number to compensate for the header lines, so the
        // focus is on the correct line.
        if (line <= offset) {
            return 1;
        }
        return line - offset;
    }

    private Runnable openEditScreen(CodeMirror cm) {
        return () -> {
            LineHandle handle = cm.extras().activeLine();
            int line = cm.getLineNumber(handle) + 1;
            if (Patch.COMMIT_MSG.equals(path)) {
                line = adjustCommitMessageLine(line);
            }
            String token = Dispatcher.toEditScreen(revision, path, line);
            if (!Gerrit.isSignedIn()) {
                Gerrit.doSignIn(token);
            } else {
                Gerrit.display(token);
            }
        };
    }

    void updateRenderEntireFile() {
        boolean entireFile = renderEntireFile();
        for (CodeMirror cm : getCms()) {
            cm.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
            if (entireFile) {
                cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
            }
            cm.setOption("viewportMargin", entireFile ? POSITIVE_INFINITY : 10);
        }
    }

    void resizeCodeMirror() {
        int height = header.getOffsetHeight() + getDiffTable().getHeaderHeight();
        for (CodeMirror cm : getCms()) {
            cm.adjustHeight(height);
        }
    }

    abstract ChunkManager getChunkManager();

    abstract CommentManager getCommentManager();

    Change.Status getChangeStatus() {
        return changeStatus;
    }

    int getStartLine() {
        return startLine;
    }

    void setStartLine(int startLine) {
        this.startLine = startLine;
    }

    DisplaySide getStartSide() {
        return startSide;
    }

    void setStartSide(DisplaySide startSide) {
        this.startSide = startSide;
    }

    DiffInfo getDiff() {
        return diff;
    }

    FileSize getFileSize() {
        return fileSize;
    }

    PreferencesAction getPrefsAction() {
        return prefsAction;
    }

    void setPrefsAction(PreferencesAction prefsAction) {
        this.prefsAction = prefsAction;
    }

    abstract void operation(Runnable apply);

    private Runnable upToChange(boolean openReplyBox) {
        return () -> {
            CallbackGroup group = new CallbackGroup();
            getCommentManager().saveAllDrafts(group);
            group.done();
            group.addListener(new GerritCallback<Void>() {
                @Override
                public void onSuccess(Void result) {
                    String rev = String.valueOf(revision.get());
                    Gerrit.display(PageLinks.toChange(changeId, base.asString(), rev),
                            new ChangeScreen(changeId, base, rev, openReplyBox, FileTable.Mode.REVIEW));
                }
            });
        };
    }

    private Runnable maybePrevVimSearch(CodeMirror cm) {
        return () -> {
            if (cm.vim().hasSearchHighlight()) {
                cm.vim().handleKey("N");
            } else {
                getCommentManager().commentNav(cm, Direction.NEXT).run();
            }
        };
    }

    private Runnable maybeNextVimSearch(CodeMirror cm) {
        return () -> {
            if (cm.vim().hasSearchHighlight()) {
                cm.vim().handleKey("n");
            } else {
                getChunkManager().diffChunkNav(cm, Direction.NEXT).run();
            }
        };
    }

    Runnable maybeNextCmSearch(CodeMirror cm) {
        return () -> {
            if (cm.hasSearchHighlight()) {
                cm.execCommand("findNext");
            } else {
                cm.execCommand("clearSearch");
                getCommentManager().toggleOpenBox(cm).run();
            }
        };
    }

    boolean renderEntireFile() {
        return prefs.renderEntireFile() && canRenderEntireFile(prefs);
    }

    boolean canRenderEntireFile(DiffPreferences prefs) {
        // CodeMirror is too slow to layout an entire huge file.
        return fileSize.compareTo(FileSize.HUGE) < 0
                || (prefs.context() != WHOLE_FILE_CONTEXT && prefs.context() < 100);
    }

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

    void setThemeStyles(boolean d) {
        if (d) {
            getDiffTable().addStyleName(Resources.I.diffTableStyle().dark());
        } else {
            getDiffTable().removeStyleName(Resources.I.diffTableStyle().dark());
        }
    }

    void setShowTabs(boolean show) {
        for (CodeMirror cm : getCms()) {
            cm.extras().showTabs(show);
        }
    }

    void setLineLength(int columns) {
        for (CodeMirror cm : getCms()) {
            cm.extras().lineLength(columns);
        }
    }

    String getContentType(DiffInfo.FileMeta meta) {
        if (prefs.syntaxHighlighting() && meta != null && meta.contentType() != null) {
            ModeInfo m = ModeInfo.findMode(meta.contentType(), path);
            return m != null ? m.mime() : null;
        }
        return null;
    }

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

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

    abstract void setAutoHideDiffHeader(boolean hide);

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

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

    void reloadDiffInfo() {
        int id = ++reloadVersionId;
        DiffApi.diff(revision, path).base(base.asPatchSetId()).wholeFile().intraline(prefs.intralineDifference())
                .ignoreWhitespace(prefs.ignoreWhitespace()).get(new GerritCallback<DiffInfo>() {
                    @Override
                    public void onSuccess(DiffInfo diffInfo) {
                        if (id == reloadVersionId && isAttached()) {
                            diff = diffInfo;
                            operation(() -> {
                                skipManager.removeAll();
                                getChunkManager().reset();
                                getDiffTable().scrollbar.removeDiffAnnotations();
                                setShowIntraline(prefs.intralineDifference());
                                render(diff);
                                skipManager.render(prefs.context(), diff);
                            });
                        }
                    }
                });
    }

    private static FileSize bucketFileSize(DiffInfo diff) {
        FileMeta a = diff.metaA();
        FileMeta b = diff.metaB();
        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;
    }

    abstract Runnable updateActiveLine(CodeMirror cm);

    private GutterClickHandler onGutterClick(final CodeMirror cm) {
        return new GutterClickHandler() {
            @Override
            public void handle(CodeMirror instance, final int line, final String gutterClass,
                    NativeEvent clickEvent) {
                if (Element.as(clickEvent.getEventTarget()).hasClassName(getLineNumberClassName())
                        && clickEvent.getButton() == NativeEvent.BUTTON_LEFT && !clickEvent.getMetaKey()
                        && !clickEvent.getAltKey() && !clickEvent.getCtrlKey() && !clickEvent.getShiftKey()) {
                    cm.setCursor(Pos.create(line));
                    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
                        @Override
                        public void execute() {
                            getCommentManager().newDraftOnGutterClick(cm, gutterClass, line + 1);
                        }
                    });
                }
            }
        };
    }

    abstract FocusHandler getFocusHandler();

    abstract CodeMirror[] getCms();

    abstract CodeMirror getCmFromSide(DisplaySide side);

    abstract DiffTable getDiffTable();

    abstract int getCmLine(int line, DisplaySide side);

    abstract String getLineNumberClassName();

    LineOnOtherInfo lineOnOther(DisplaySide side, int line) {
        return getChunkManager().lineMapper.lineOnOther(side, line);
    }

    abstract ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(CommentsCollections comments);

    abstract boolean isSideBySide();
}