| // 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.Nullable; |
| 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.gerrit.reviewdb.client.Project; |
| 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; |
| } |
| } |
| |
| @Nullable private Project.NameKey project; |
| 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( |
| @Nullable Project.NameKey project, |
| DiffObject base, |
| DiffObject revision, |
| String path, |
| DisplaySide startSide, |
| int startLine, |
| DiffView diffScreenType) { |
| this.project = project; |
| 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, project, 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(Project.NameKey.asStringOrNull(project), 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( |
| Project.NameKey.asStringOrNull(project), |
| 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(project, base, revision, path); |
| comments.load(group2); |
| |
| countParents(group2); |
| |
| RestApi call = ChangeApi.detail(Project.NameKey.asStringOrNull(project), 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(); |
| project = info.projectNameKey(); |
| 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(Project.NameKey.asStringOrNull(project), 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 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(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(project, 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; |
| } |
| } |
| |
| @Nullable |
| public Project.NameKey getProject() { |
| return project; |
| } |
| |
| 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(project, 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(project, changeId, base.asString(), rev), |
| new ChangeScreen( |
| project, 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(Project.NameKey.asStringOrNull(project), 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(Project.NameKey.asStringOrNull(project), 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(CodeMirror cm) { |
| return new GutterClickHandler() { |
| @Override |
| public void handle( |
| CodeMirror instance, int line, 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(); |
| } |