
Using CodeMirror's lint addon on the UI to display the blame annotations. It works on both sides of the side-by-side diff and also supports the auto-merge commit. It requires manual step to enable the annotations to avoid any unnecessary git processing and network traffic between the server and client. Introduces a new dependency on blame-cache in gerrit-server to reuse BlameCache. In a following change, the gutter showing the blame info will be made clickable. Clicking on the gutter will open a new tab that takes the user to the corresponding change in Gerrit. The commit SHA-1 hashes are currently not selectable. Making it so might require an upstream change in CodeMirror's lint addon. Bug: Issue 1642 Change-Id: I6267d30cbee448f8137e11c7120959dc424eaeeb
223 lines
6.5 KiB
Java
223 lines
6.5 KiB
Java
// Copyright (C) 2015 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 net.codemirror.lib;
|
|
|
|
import static com.google.gwt.dom.client.Style.Display.INLINE_BLOCK;
|
|
import static com.google.gwt.dom.client.Style.Unit.PX;
|
|
import static net.codemirror.lib.CodeMirror.style;
|
|
import static net.codemirror.lib.CodeMirror.LineClassWhere.WRAP;
|
|
|
|
import com.google.gerrit.client.FormatUtil;
|
|
import com.google.gerrit.client.RangeInfo;
|
|
import com.google.gerrit.client.blame.BlameInfo;
|
|
import com.google.gerrit.client.diff.DisplaySide;
|
|
import com.google.gerrit.client.rpc.Natives;
|
|
import com.google.gwt.core.client.GWT;
|
|
import com.google.gwt.core.client.JavaScriptObject;
|
|
import com.google.gwt.core.client.JsArray;
|
|
import com.google.gwt.core.client.JsArrayString;
|
|
import com.google.gwt.dom.client.Element;
|
|
import com.google.gwt.i18n.client.DateTimeFormat;
|
|
import com.google.gwt.user.client.DOM;
|
|
|
|
import net.codemirror.lib.CodeMirror.LineHandle;
|
|
|
|
import java.util.Date;
|
|
import java.util.Objects;
|
|
|
|
/** Additional features added to CodeMirror by Gerrit Code Review. */
|
|
public class Extras {
|
|
public static final String ANNOTATION_GUTTER_ID = "CodeMirror-lint-markers";
|
|
|
|
static final BlameConfig C = GWT.create(BlameConfig.class);
|
|
|
|
static final native Extras get(CodeMirror c) /*-{ return c.gerritExtras }-*/;
|
|
private static native void set(CodeMirror c, Extras e)
|
|
/*-{ c.gerritExtras = e }-*/;
|
|
|
|
static void attach(CodeMirror c) {
|
|
set(c, new Extras(c));
|
|
}
|
|
|
|
private final CodeMirror cm;
|
|
private Element margin;
|
|
private DisplaySide side;
|
|
private double charWidthPx;
|
|
private double lineHeightPx;
|
|
private LineHandle activeLine;
|
|
private boolean annotated;
|
|
|
|
private Extras(CodeMirror cm) {
|
|
this.cm = cm;
|
|
}
|
|
|
|
public DisplaySide side() {
|
|
return side;
|
|
}
|
|
|
|
public void side(DisplaySide s) {
|
|
side = s;
|
|
}
|
|
|
|
public double charWidthPx() {
|
|
if (charWidthPx <= 1) {
|
|
int len = 100;
|
|
StringBuilder s = new StringBuilder();
|
|
for (int i = 0; i < len; i++) {
|
|
s.append('m');
|
|
}
|
|
|
|
Element e = DOM.createSpan();
|
|
e.getStyle().setDisplay(INLINE_BLOCK);
|
|
e.setInnerText(s.toString());
|
|
|
|
cm.measure().appendChild(e);
|
|
charWidthPx = ((double) e.getOffsetWidth()) / len;
|
|
e.removeFromParent();
|
|
}
|
|
return charWidthPx;
|
|
}
|
|
|
|
public double lineHeightPx() {
|
|
if (lineHeightPx <= 1) {
|
|
Element p = DOM.createDiv();
|
|
int lines = 1;
|
|
for (int i = 0; i < lines; i++) {
|
|
Element e = DOM.createDiv();
|
|
p.appendChild(e);
|
|
|
|
Element pre = DOM.createElement("pre");
|
|
pre.setInnerText("gqyŚŻŹŃ");
|
|
e.appendChild(pre);
|
|
}
|
|
|
|
cm.measure().appendChild(p);
|
|
lineHeightPx = ((double) p.getOffsetHeight()) / lines;
|
|
p.removeFromParent();
|
|
}
|
|
return lineHeightPx;
|
|
}
|
|
|
|
public void lineLength(int columns) {
|
|
if (margin == null) {
|
|
margin = DOM.createDiv();
|
|
margin.setClassName(style().margin());
|
|
cm.mover().appendChild(margin);
|
|
}
|
|
margin.getStyle().setMarginLeft(columns * charWidthPx(), PX);
|
|
}
|
|
|
|
public void showTabs(boolean show) {
|
|
Element e = cm.getWrapperElement();
|
|
if (show) {
|
|
e.addClassName(style().showTabs());
|
|
} else {
|
|
e.removeClassName(style().showTabs());
|
|
}
|
|
}
|
|
|
|
public final boolean hasActiveLine() {
|
|
return activeLine != null;
|
|
}
|
|
|
|
public final LineHandle activeLine() {
|
|
return activeLine;
|
|
}
|
|
|
|
public final boolean activeLine(LineHandle line) {
|
|
if (Objects.equals(activeLine, line)) {
|
|
return false;
|
|
}
|
|
|
|
if (activeLine != null) {
|
|
cm.removeLineClass(activeLine, WRAP, style().activeLine());
|
|
}
|
|
activeLine = line;
|
|
cm.addLineClass(activeLine, WRAP, style().activeLine());
|
|
return true;
|
|
}
|
|
|
|
public final void clearActiveLine() {
|
|
if (activeLine != null) {
|
|
cm.removeLineClass(activeLine, WRAP, style().activeLine());
|
|
activeLine = null;
|
|
}
|
|
}
|
|
|
|
public boolean isAnnotated() {
|
|
return annotated;
|
|
}
|
|
|
|
public final void clearAnnotations() {
|
|
JsArrayString gutters = ((JsArrayString) JsArrayString.createArray());
|
|
cm.setOption("gutters", gutters);
|
|
annotated = false;
|
|
}
|
|
|
|
public final void setAnnotations(JsArray<BlameInfo> blameInfos) {
|
|
if (blameInfos.length() > 0) {
|
|
setBlameInfo(blameInfos);
|
|
JsArrayString gutters = ((JsArrayString) JsArrayString.createArray());
|
|
gutters.push(ANNOTATION_GUTTER_ID);
|
|
cm.setOption("gutters", gutters);
|
|
annotated = true;
|
|
DateTimeFormat format = DateTimeFormat.getFormat(
|
|
DateTimeFormat.PredefinedFormat.DATE_SHORT);
|
|
JsArray<LintLine> annotations = JsArray.createArray().cast();
|
|
for (BlameInfo blameInfo : Natives.asList(blameInfos)) {
|
|
for (RangeInfo range : Natives.asList(blameInfo.ranges())) {
|
|
Date commitTime = new Date(blameInfo.time() * 1000L);
|
|
String shortId = blameInfo.id().substring(0, 8);
|
|
String shortBlame = C.shortBlameMsg(
|
|
shortId, format.format(commitTime), blameInfo.author());
|
|
String detailedBlame = C.detailedBlameMsg(blameInfo.id(),
|
|
blameInfo.author(), FormatUtil.mediumFormat(commitTime),
|
|
blameInfo.commitMsg());
|
|
|
|
annotations.push(LintLine.create(shortBlame, detailedBlame, shortId,
|
|
Pos.create(range.start() - 1)));
|
|
}
|
|
}
|
|
cm.setOption("lint", getAnnotation(annotations));
|
|
}
|
|
}
|
|
|
|
private native JavaScriptObject getAnnotation(JsArray<LintLine> annotations) /*-{
|
|
return {
|
|
getAnnotations: function(text, options, cm) { return annotations; }
|
|
};
|
|
}-*/;
|
|
|
|
public final native JsArray<BlameInfo> getBlameInfo() /*-{
|
|
return this.blameInfos;
|
|
}-*/;
|
|
|
|
public final native void setBlameInfo(JsArray<BlameInfo> blameInfos) /*-{
|
|
this['blameInfos'] = blameInfos;
|
|
}-*/;
|
|
|
|
public final void toggleAnnotation() {
|
|
toggleAnnotation(getBlameInfo());
|
|
}
|
|
|
|
public final void toggleAnnotation(JsArray<BlameInfo> blameInfos) {
|
|
if (isAnnotated()) {
|
|
clearAnnotations();
|
|
} else {
|
|
setAnnotations(blameInfos);
|
|
}
|
|
}
|
|
}
|