Move syntax highlighting back to client

Mozilla Rhino is just slow running the JavaScript for prettify.
Its quicker inside of most modern browsers, especially ones that
have a good JIT like Goole Chrome or Safari 4.

Move the rendering back onto the client side.  To do this correctly
we have to ship the entire file to the client.  So what we do is,
we send the entire "A" file, the old image, and only the new lines
from the "B" file, the new image.  When formatting locally on the
client we recreate the full "B" file in order to syntax highlight it.

If a file is considered large, over 9000 lines, we still only send
part of the file and we disable the syntax highlighting.  This way
the client doesn't get bogged down in rendering lots of text when
the file is something really massive.

Bug: issue 439
Change-Id: Ib70c6526af09f84ebbfe467cfbb27c75ca7c9ad7
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce 2010-02-16 11:02:36 -08:00
parent 6caaa60a2d
commit 47cfa6d8cf
11 changed files with 414 additions and 180 deletions

View File

@ -14,9 +14,13 @@
package com.google.gerrit.common.data; package com.google.gerrit.common.data;
import com.google.gerrit.common.data.PatchScriptSettings.Whitespace; import com.google.gerrit.common.data.PatchScriptSettings.Whitespace;
import com.google.gerrit.prettify.client.ClientSideFormatter;
import com.google.gerrit.prettify.common.EditList;
import com.google.gerrit.prettify.common.PrettyFormatter;
import com.google.gerrit.prettify.common.PrettySettings;
import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.prettify.common.SparseHtmlFile;
import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.Change;
import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.diff.Edit;
@ -86,11 +90,45 @@ public class PatchScript {
return b; return b;
} }
public SparseHtmlFile getSparseHtmlFileA() {
PrettySettings s = new PrettySettings(settings.getPrettySettings());
s.setFileName(a.getPath());
s.setShowWhiteSpaceErrors(false);
PrettyFormatter f = ClientSideFormatter.FACTORY.get();
f.setPrettySettings(s);
f.setEditFilter(PrettyFormatter.A);
f.setEditList(getEditList());
f.format(a);
return f;
}
public SparseHtmlFile getSparseHtmlFileB() {
PrettySettings s = new PrettySettings(settings.getPrettySettings());
s.setFileName(b.getPath());
PrettyFormatter f = ClientSideFormatter.FACTORY.get();
f.setPrettySettings(s);
f.setEditFilter(PrettyFormatter.B);
f.setEditList(getEditList());
if (s.isSyntaxHighlighting() && a.isWholeFile() && !b.isWholeFile()) {
f.format(b.completeWithContext(a, getEditList()));
} else {
f.format(b);
}
return f;
}
public List<Edit> getEdits() { public List<Edit> getEdits() {
return edits; return edits;
} }
public Iterable<EditList.Hunk> getHunks() { public Iterable<EditList.Hunk> getHunks() {
return new EditList(edits, getContext(), a.size(), b.size()).getHunks(); return getEditList().getHunks();
}
private EditList getEditList() {
return new EditList(edits, getContext(), a.size(), b.size());
} }
} }

View File

@ -26,7 +26,7 @@ import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.common.data.AccountInfoCache; import com.google.gerrit.common.data.AccountInfoCache;
import com.google.gerrit.common.data.CommentDetail; import com.google.gerrit.common.data.CommentDetail;
import com.google.gerrit.common.data.PatchScript; import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.SparseFileContent; import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.reviewdb.Patch; import com.google.gerrit.reviewdb.Patch;
import com.google.gerrit.reviewdb.PatchLineComment; import com.google.gerrit.reviewdb.PatchLineComment;
import com.google.gerrit.reviewdb.PatchSet; import com.google.gerrit.reviewdb.PatchSet;

View File

@ -21,9 +21,9 @@ import static com.google.gerrit.client.patches.PatchLine.Type.REPLACE;
import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.Gerrit;
import com.google.gerrit.common.data.CommentDetail; import com.google.gerrit.common.data.CommentDetail;
import com.google.gerrit.common.data.EditList;
import com.google.gerrit.common.data.PatchScript; import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.SparseFileContent; import com.google.gerrit.prettify.common.EditList;
import com.google.gerrit.prettify.common.SparseHtmlFile;
import com.google.gerrit.reviewdb.Patch; import com.google.gerrit.reviewdb.Patch;
import com.google.gerrit.reviewdb.PatchLineComment; import com.google.gerrit.reviewdb.PatchLineComment;
import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.GWT;
@ -67,8 +67,8 @@ public class SideBySideTable extends AbstractPatchContentTable {
@Override @Override
protected void render(final PatchScript script) { protected void render(final PatchScript script) {
final SparseFileContent a = script.getA(); final SparseHtmlFile a = script.getSparseHtmlFileA();
final SparseFileContent b = script.getB(); final SparseHtmlFile b = script.getSparseHtmlFileB();
final ArrayList<PatchLine> lines = new ArrayList<PatchLine>(); final ArrayList<PatchLine> lines = new ArrayList<PatchLine>();
final SafeHtmlBuilder nc = new SafeHtmlBuilder(); final SafeHtmlBuilder nc = new SafeHtmlBuilder();
@ -86,7 +86,7 @@ public class SideBySideTable extends AbstractPatchContentTable {
while (hunk.next()) { while (hunk.next()) {
if (hunk.isContextLine()) { if (hunk.isContextLine()) {
openLine(nc); openLine(nc);
final SafeHtml ctx = a.get(hunk.getCurA()); final SafeHtml ctx = a.getSafeHtmlLine(hunk.getCurA());
appendLineText(nc, hunk.getCurA(), CONTEXT, ctx); appendLineText(nc, hunk.getCurA(), CONTEXT, ctx);
if (ignoreWS && b.contains(hunk.getCurB())) { if (ignoreWS && b.contains(hunk.getCurB())) {
appendLineText(nc, hunk.getCurB(), CONTEXT, b, hunk.getCurB()); appendLineText(nc, hunk.getCurB(), CONTEXT, b, hunk.getCurB());
@ -273,8 +273,8 @@ public class SideBySideTable extends AbstractPatchContentTable {
private void appendLineText(final SafeHtmlBuilder m, private void appendLineText(final SafeHtmlBuilder m,
final int lineNumberMinusOne, final PatchLine.Type type, final int lineNumberMinusOne, final PatchLine.Type type,
final SparseFileContent src, final int i) { final SparseHtmlFile src, final int i) {
appendLineText(m, lineNumberMinusOne, type, src.get(i)); appendLineText(m, lineNumberMinusOne, type, src.getSafeHtmlLine(i));
} }
private void appendLineText(final SafeHtmlBuilder m, private void appendLineText(final SafeHtmlBuilder m,

View File

@ -20,11 +20,11 @@ import static com.google.gerrit.client.patches.PatchLine.Type.INSERT;
import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.Gerrit;
import com.google.gerrit.common.data.CommentDetail; import com.google.gerrit.common.data.CommentDetail;
import com.google.gerrit.common.data.EditList;
import com.google.gerrit.common.data.PatchScript; import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.SparseFileContent;
import com.google.gerrit.common.data.EditList.Hunk;
import com.google.gerrit.common.data.PatchScript.DisplayMethod; import com.google.gerrit.common.data.PatchScript.DisplayMethod;
import com.google.gerrit.prettify.common.EditList;
import com.google.gerrit.prettify.common.SparseHtmlFile;
import com.google.gerrit.prettify.common.EditList.Hunk;
import com.google.gerrit.reviewdb.Patch; import com.google.gerrit.reviewdb.Patch;
import com.google.gerrit.reviewdb.PatchLineComment; import com.google.gerrit.reviewdb.PatchLineComment;
import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.GWT;
@ -86,8 +86,8 @@ public class UnifiedDiffTable extends AbstractPatchContentTable {
@Override @Override
protected void render(final PatchScript script) { protected void render(final PatchScript script) {
final SparseFileContent a = script.getA(); final SparseHtmlFile a = script.getSparseHtmlFileA();
final SparseFileContent b = script.getB(); final SparseHtmlFile b = script.getSparseHtmlFileB();
final SafeHtmlBuilder nc = new SafeHtmlBuilder(); final SafeHtmlBuilder nc = new SafeHtmlBuilder();
// Display the patch header // Display the patch header
@ -153,8 +153,10 @@ public class UnifiedDiffTable extends AbstractPatchContentTable {
closeLine(nc); closeLine(nc);
hunk.incA(); hunk.incA();
lines.add(new PatchLine(DELETE, hunk.getCurA(), 0)); lines.add(new PatchLine(DELETE, hunk.getCurA(), 0));
if (a.size() == hunk.getCurA() && a.isMissingNewlineAtEnd()) if (a.size() == hunk.getCurA()
&& script.getA().isMissingNewlineAtEnd()) {
appendNoLF(nc); appendNoLF(nc);
}
} else if (hunk.isInsertedB()) { } else if (hunk.isInsertedB()) {
openLine(nc); openLine(nc);
@ -164,11 +166,13 @@ public class UnifiedDiffTable extends AbstractPatchContentTable {
closeLine(nc); closeLine(nc);
hunk.incB(); hunk.incB();
lines.add(new PatchLine(INSERT, 0, hunk.getCurB())); lines.add(new PatchLine(INSERT, 0, hunk.getCurB()));
if (b.size() == hunk.getCurB() && b.isMissingNewlineAtEnd()) if (b.size() == hunk.getCurB()
&& script.getB().isMissingNewlineAtEnd()) {
appendNoLF(nc); appendNoLF(nc);
} }
} }
} }
}
resetHtml(nc); resetHtml(nc);
initScript(script); initScript(script);
@ -304,8 +308,8 @@ public class UnifiedDiffTable extends AbstractPatchContentTable {
} }
private void appendLineText(final SafeHtmlBuilder m, private void appendLineText(final SafeHtmlBuilder m,
final PatchLine.Type type, final SparseFileContent src, final int i) { final PatchLine.Type type, final SparseHtmlFile src, final int i) {
final SafeHtml text = src.get(i); final SafeHtml text = src.getSafeHtmlLine(i);
m.openTd(); m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().diffText()); m.addStyleName(Gerrit.RESOURCES.css().diffText());
switch (type) { switch (type) {

View File

@ -15,14 +15,11 @@
package com.google.gerrit.httpd.rpc.patch; package com.google.gerrit.httpd.rpc.patch;
import com.google.gerrit.common.data.CommentDetail; import com.google.gerrit.common.data.CommentDetail;
import com.google.gerrit.common.data.EditList;
import com.google.gerrit.common.data.PatchScript; import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchScriptSettings; import com.google.gerrit.common.data.PatchScriptSettings;
import com.google.gerrit.common.data.SparseFileContent;
import com.google.gerrit.common.data.PatchScript.DisplayMethod; import com.google.gerrit.common.data.PatchScript.DisplayMethod;
import com.google.gerrit.prettify.common.PrettyFactory; import com.google.gerrit.prettify.common.EditList;
import com.google.gerrit.prettify.common.PrettyFormatter; import com.google.gerrit.prettify.common.SparseFileContent;
import com.google.gerrit.prettify.common.PrettySettings;
import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchLineComment; import com.google.gerrit.reviewdb.PatchLineComment;
import com.google.gerrit.reviewdb.Patch.PatchType; import com.google.gerrit.reviewdb.Patch.PatchType;
@ -65,7 +62,6 @@ class PatchScriptBuilder {
}; };
private final List<String> header; private final List<String> header;
private final PrettyFactory prettyFactory;
private Repository db; private Repository db;
private Change change; private Change change;
private PatchScriptSettings settings; private PatchScriptSettings settings;
@ -77,11 +73,11 @@ class PatchScriptBuilder {
private List<Edit> edits; private List<Edit> edits;
private final FileTypeRegistry registry; private final FileTypeRegistry registry;
private int context;
@Inject @Inject
PatchScriptBuilder(final FileTypeRegistry ftr, final PrettyFactory pf) { PatchScriptBuilder(final FileTypeRegistry ftr) {
header = new ArrayList<String>(); header = new ArrayList<String>();
prettyFactory = pf;
a = new Side(); a = new Side();
b = new Side(); b = new Side();
registry = ftr; registry = ftr;
@ -97,6 +93,7 @@ class PatchScriptBuilder {
void setSettings(final PatchScriptSettings s) { void setSettings(final PatchScriptSettings s) {
settings = s; settings = s;
context = settings.getContext();
} }
void setTrees(final ObjectId a, final ObjectId b) { void setTrees(final ObjectId a, final ObjectId b) {
@ -104,10 +101,6 @@ class PatchScriptBuilder {
bId = b; bId = b;
} }
private int context() {
return settings.getContext();
}
PatchScript toPatchScript(final PatchListEntry contentWS, PatchScript toPatchScript(final PatchListEntry contentWS,
final CommentDetail comments, final PatchListEntry contentAct) final CommentDetail comments, final PatchListEntry contentAct)
throws IOException { throws IOException {
@ -133,7 +126,7 @@ class PatchScriptBuilder {
if (a.mode == FileMode.GITLINK || b.mode == FileMode.GITLINK) { if (a.mode == FileMode.GITLINK || b.mode == FileMode.GITLINK) {
} else if (a.src == b.src && a.size() <= context() } else if (a.src == b.src && a.size() <= context
&& contentAct.getEdits().isEmpty()) { && contentAct.getEdits().isEmpty()) {
// Odd special case; the files are identical (100% rename or copy) // Odd special case; the files are identical (100% rename or copy)
// and the user has asked for context that is larger than the file. // and the user has asked for context that is larger than the file.
@ -144,9 +137,22 @@ class PatchScriptBuilder {
} }
edits = new ArrayList<Edit>(1); edits = new ArrayList<Edit>(1);
edits.add(new Edit(a.size(), a.size())); edits.add(new Edit(a.size(), a.size()));
} else { } else {
if (BIG_FILE < Math.max(a.size(), b.size()) && 25 < context()) { if (BIG_FILE < Math.max(a.size(), b.size())) {
settings.setContext(25); // IF the file is really large, we disable things to avoid choking
// the browser client.
//
settings.setContext(Math.min(25, context));
settings.getPrettySettings().setSyntaxHighlighting(false);
context = settings.getContext();
} else if (settings.getPrettySettings().isSyntaxHighlighting()) {
// In order to syntax highlight the file properly we need to
// give the client the complete file contents. So force our
// context temporarily to the complete file size.
//
context = MAX_CONTEXT;
} }
packContent(); packContent();
} }
@ -306,7 +312,7 @@ class PatchScriptBuilder {
} }
private void packContent() { private void packContent() {
EditList list = new EditList(edits, context(), a.size(), b.size()); EditList list = new EditList(edits, context, a.size(), b.size());
for (final EditList.Hunk hunk : list.getHunks()) { for (final EditList.Hunk hunk : list.getHunks()) {
while (hunk.next()) { while (hunk.next()) {
if (hunk.isContextLine()) { if (hunk.isContextLine()) {
@ -330,7 +336,7 @@ class PatchScriptBuilder {
ObjectId id; ObjectId id;
FileMode mode; FileMode mode;
byte[] srcContent; byte[] srcContent;
PrettyFormatter src; Text src;
MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE; MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE;
DisplayMethod displayMethod = DisplayMethod.DIFF; DisplayMethod displayMethod = DisplayMethod.DIFF;
final SparseFileContent dst = new SparseFileContent(); final SparseFileContent dst = new SparseFileContent();
@ -387,26 +393,19 @@ class PatchScriptBuilder {
displayMethod = DisplayMethod.NONE; displayMethod = DisplayMethod.NONE;
} }
if (!reuse && displayMethod == DisplayMethod.DIFF) { if (!reuse) {
PrettySettings s = new PrettySettings(settings.getPrettySettings()); if (srcContent == Text.NO_BYTES) {
s.setFileName(path); src = Text.EMPTY;
src = prettyFactory.get();
if (other == null /* side A */) {
src.setEditFilter(PrettyFormatter.A);
s.setShowWhiteSpaceErrors(false);
} else { } else {
src.setEditFilter(PrettyFormatter.B); src = new Text(srcContent);
s.setShowWhiteSpaceErrors(s.isShowWhiteSpaceErrors());
} }
src.setEditList(edits);
src.format(s, Text.asString(srcContent, null));
} }
if (srcContent.length > 0 && srcContent[srcContent.length - 1] != '\n') { if (srcContent.length > 0 && srcContent[srcContent.length - 1] != '\n') {
dst.setMissingNewlineAtEnd(true); dst.setMissingNewlineAtEnd(true);
} }
dst.setSize(size()); dst.setSize(size());
dst.setPath(path);
} catch (IOException err) { } catch (IOException err) {
throw new IOException("Cannot read " + within.name() + ":" + path, err); throw new IOException("Cannot read " + within.name() + ":" + path, err);
} }

View File

@ -51,8 +51,8 @@ public class ClientSideFormatter extends PrettyFormatter {
/*-{ eval(js); }-*/; /*-{ eval(js); }-*/;
@Override @Override
protected String prettify(String html) { protected String prettify(String html, String type) {
return go(html, settings.getFilename(), settings.getTabSize()); return go(html, type, settings.getTabSize());
} }
private static native String go(String srcText, String srcType, int tabSize) private static native String go(String srcText, String srcType, int tabSize)

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package com.google.gerrit.common.data; package com.google.gerrit.prettify.common;
import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.diff.Edit;
@ -37,6 +37,10 @@ public class EditList {
return edits; return edits;
} }
public EditList getFullContext() {
return new EditList(edits, 5000000, aSize, bSize);
}
public Iterable<Hunk> getHunks() { public Iterable<Hunk> getHunks() {
return new Iterable<Hunk>() { return new Iterable<Hunk>() {
public Iterator<Hunk> iterator() { public Iterator<Hunk> iterator() {
@ -112,6 +116,10 @@ public class EditList {
return bCur; return bCur;
} }
public Edit getCurEdit() {
return curEdit;
}
public int getEndA() { public int getEndA() {
return aEnd; return aEnd;
} }

View File

@ -20,104 +20,161 @@ import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.ReplaceEdit; import org.eclipse.jgit.diff.ReplaceEdit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
public abstract class PrettyFormatter { public abstract class PrettyFormatter implements SparseHtmlFile {
public static abstract class EditFilter { public static abstract class EditFilter {
protected abstract int getBegin(Edit e); final String get(SparseFileContent src, EditList.Hunk hunk) {
return src.get(getCur(hunk));
protected abstract int getEnd(Edit e);
protected abstract String getStyleName();
protected final boolean in(int line, Edit e) {
return getBegin(e) <= line && line < getEnd(e);
} }
protected final boolean after(int line, Edit e) { abstract String getStyleName();
return getEnd(e) < line;
} abstract int getCur(EditList.Hunk hunk);
abstract int getBegin(Edit edit);
abstract int getEnd(Edit edit);
abstract boolean isModified(EditList.Hunk hunk);
abstract void incSelf(EditList.Hunk hunk);
abstract void incOther(EditList.Hunk hunk);
} }
public static final EditFilter A = new EditFilter() { public static final EditFilter A = new EditFilter() {
@Override @Override
protected String getStyleName() { String getStyleName() {
return "wdd"; return "wdd";
} }
@Override @Override
protected int getBegin(Edit e) { int getCur(EditList.Hunk hunk) {
return e.getBeginA(); return hunk.getCurA();
} }
@Override @Override
protected int getEnd(Edit e) { int getBegin(Edit edit) {
return e.getEndA(); return edit.getBeginA();
}
@Override
int getEnd(Edit edit) {
return edit.getEndA();
}
@Override
boolean isModified(EditList.Hunk hunk) {
return hunk.isDeletedA();
}
@Override
void incSelf(EditList.Hunk hunk) {
hunk.incA();
}
@Override
void incOther(EditList.Hunk hunk) {
hunk.incB();
} }
}; };
public static final EditFilter B = new EditFilter() { public static final EditFilter B = new EditFilter() {
@Override @Override
protected String getStyleName() { String getStyleName() {
return "wdi"; return "wdi";
} }
@Override @Override
protected int getBegin(Edit e) { int getCur(EditList.Hunk hunk) {
return e.getBeginB(); return hunk.getCurB();
} }
@Override @Override
protected int getEnd(Edit e) { int getBegin(Edit edit) {
return e.getEndB(); return edit.getBeginB();
}
@Override
int getEnd(Edit edit) {
return edit.getEndB();
}
@Override
boolean isModified(EditList.Hunk hunk) {
return hunk.isInsertedB();
}
@Override
void incSelf(EditList.Hunk hunk) {
hunk.incB();
}
@Override
void incOther(EditList.Hunk hunk) {
hunk.incA();
} }
}; };
protected List<String> lines = Collections.emptyList(); protected SparseFileContent content;
protected EditFilter side = A; protected EditFilter side;
protected List<Edit> lineEdits = Collections.emptyList(); protected EditList edits;
protected PrettySettings settings; protected PrettySettings settings;
private int col; private int col;
private int line; private int lineIdx;
private Tag lastTag; private Tag lastTag;
private StringBuilder buf; private StringBuilder buf;
/** @return the line of formatted HTML. */ public SafeHtml getSafeHtmlLine(int lineNo) {
public SafeHtml getLine(int lineNo) { return SafeHtml.asis(content.get(lineNo));
return SafeHtml.asis(lines.get(lineNo));
} }
/** @return the number of lines in this formatter. */
public int size() { public int size() {
return lines.size(); return content.size();
}
@Override
public boolean contains(int idx) {
return content.contains(idx);
} }
public void setEditFilter(EditFilter f) { public void setEditFilter(EditFilter f) {
side = f; side = f;
} }
public void setEditList(List<Edit> all) { public void setEditList(EditList all) {
lineEdits = all; edits = all;
}
public void setPrettySettings(PrettySettings how) {
settings = how;
} }
/** /**
* Parse and format a complete source code file. * Parse and format a complete source code file.
* *
* @param how the settings to apply to the formatter. * @param src raw content of the file to format. The line strings will be HTML
* @param srcText raw content of the file to format. The string will be HTML
* escaped before processing, so it must be the raw text. * escaped before processing, so it must be the raw text.
*/ */
public void format(PrettySettings how, String srcText) { public void format(SparseFileContent src) {
settings = how; content = new SparseFileContent();
lines = new ArrayList<String>(); content.setSize(src.size());
String html = toHTML(src);
if (settings.isSyntaxHighlighting() && getFileType() != null
&& src.isWholeFile()) {
// The prettify parsers don't like &#39; as an entity for the
// single quote character. Replace them all out so we don't
// confuse the parser.
//
html = html.replaceAll("&#39;", "'");
html = prettify(html, getFileType());
String html = toHTML(srcText);
if (settings.isSyntaxHighlighting()) {
html = prettify(html);
} else { } else {
html = expandTabs(html);
html = html.replaceAll("\n", "<br />"); html = html.replaceAll("\n", "<br />");
} }
@ -126,7 +183,7 @@ public abstract class PrettyFormatter {
lastTag = Tag.NULL; lastTag = Tag.NULL;
col = 0; col = 0;
line = 0; lineIdx = 0;
buf = new StringBuilder(); buf = new StringBuilder();
while (pos <= html.length()) { while (pos <= html.length()) {
@ -141,7 +198,7 @@ public abstract class PrettyFormatter {
htmlText(html.substring(textChunkStart, pos)); htmlText(html.substring(textChunkStart, pos));
} }
if (0 < buf.length()) { if (0 < buf.length()) {
lines.add(buf.toString()); content.addLine(src.mapIndexToLine(lineIdx), buf.toString());
} }
break; break;
} }
@ -164,10 +221,10 @@ public abstract class PrettyFormatter {
if (isBR(html, tagStart, tagEnd)) { if (isBR(html, tagStart, tagEnd)) {
lastTag.close(buf, html); lastTag.close(buf, html);
lines.add(buf.toString()); content.addLine(src.mapIndexToLine(lineIdx), buf.toString());
buf = new StringBuilder(); buf = new StringBuilder();
col = 0; col = 0;
line++; lineIdx++;
} else if (html.charAt(tagStart + 1) == '/') { } else if (html.charAt(tagStart + 1) == '/') {
lastTag = lastTag.pop(buf, html); lastTag = lastTag.pop(buf, html);
@ -222,7 +279,7 @@ public abstract class PrettyFormatter {
} }
/** Run the prettify engine over the text and return the result. */ /** Run the prettify engine over the text and return the result. */
protected abstract String prettify(String html); protected abstract String prettify(String html, String type);
private static boolean isBR(String html, int tagStart, int tagEnd) { private static boolean isBR(String html, int tagStart, int tagEnd) {
return tagEnd - tagStart == 5 // return tagEnd - tagStart == 5 //
@ -287,15 +344,9 @@ public abstract class PrettyFormatter {
} }
} }
private String toHTML(String src) { private String toHTML(SparseFileContent src) {
SafeHtml html = colorLineEdits(src); SafeHtml html = colorLineEdits(src);
// The prettify parsers don't like &#39; as an entity for the
// single quote character. Replace them all out so we don't
// confuse the parser.
//
html = html.replaceAll("&#39;", "'");
if (settings.isShowWhiteSpaceErrors()) { if (settings.isShowWhiteSpaceErrors()) {
// We need to do whitespace errors before showing tabs, because // We need to do whitespace errors before showing tabs, because
// these patterns rely on \t as a literal, before it expands. // these patterns rely on \t as a literal, before it expands.
@ -312,69 +363,84 @@ public abstract class PrettyFormatter {
return html.asString(); return html.asString();
} }
private SafeHtml colorLineEdits(String src) { private SafeHtml colorLineEdits(SparseFileContent src) {
SafeHtmlBuilder buf = new SafeHtmlBuilder(); SafeHtmlBuilder buf = new SafeHtmlBuilder();
int lIdx = 0; ReplaceEdit lastReplace = null;
Edit lCur = lIdx < lineEdits.size() ? lineEdits.get(lIdx) : null; List<Edit> charEdits = null;
int lastPos = 0;
int lastIdx = 0;
int pos = 0; EditList hunkGenerator = edits;
int line = 0; if (src.isWholeFile()) {
while (pos < src.length()) { hunkGenerator = hunkGenerator.getFullContext();
if (lCur instanceof ReplaceEdit && side.in(line, lCur)) {
List<Edit> wordEdits = ((ReplaceEdit) lCur).getInternalEdits();
if (!wordEdits.isEmpty()) {
// Copy the result using the word edits to guide us.
//
int last = 0;
for (Edit w : wordEdits) {
int b = side.getBegin(w);
int e = side.getEnd(w);
// If there is text between edits, copy it as-is.
//
int cnt = b - last;
if (0 < cnt) {
buf.append(src.substring(pos, pos + cnt));
pos += cnt;
last = b;
} }
// If this is an edit, wrap it in a span. for (final EditList.Hunk hunk : hunkGenerator.getHunks()) {
while (hunk.next()) {
if (hunk.isContextLine()) {
if (src.contains(side.getCur(hunk))) {
// If side is B and src isn't the complete file we can't
// add it to the buffer here. This can happen if the file
// was really large and we chose not to syntax highlight.
// //
cnt = e - b; buf.append(side.get(src, hunk));
if (0 < cnt) { buf.append('\n');
}
hunk.incBoth();
} else if (!side.isModified(hunk)) {
side.incOther(hunk);
} else if (hunk.getCurEdit() instanceof ReplaceEdit) {
if (lastReplace != hunk.getCurEdit()) {
lastReplace = (ReplaceEdit) hunk.getCurEdit();
charEdits = lastReplace.getInternalEdits();
lastPos = 0;
lastIdx = 0;
}
final String line = side.get(src, hunk) + "\n";
for (int c = 0; c < line.length();) {
if (charEdits.size() <= lastIdx) {
buf.append(line.substring(c));
break;
}
final Edit edit = charEdits.get(lastIdx);
final int b = side.getBegin(edit) - lastPos;
final int e = side.getEnd(edit) - lastPos;
if (c < b) {
// There is text at the start of this line that is common
// with the other side. Copy it with no style around it.
//
final int n = Math.min(b, line.length());
buf.append(line.substring(c, n));
c = n;
}
if (c < e) {
final int n = Math.min(e, line.length());
buf.openSpan(); buf.openSpan();
buf.setStyleName(side.getStyleName()); buf.setStyleName(side.getStyleName());
buf.append(src.substring(pos, pos + cnt)); buf.append(line.substring(c, n));
buf.closeSpan(); buf.closeSpan();
pos += cnt; c = n;
last = e;
}
} }
// We've consumed the entire region, so we are on the end. if (e <= c) {
// Fall through, what's left of this edit is only the tail lastIdx++;
// of the final line.
//
line = side.getEnd(lCur) - 1;
} }
} }
lastPos += line.length();
side.incSelf(hunk);
int lf = src.indexOf('\n', pos); } else {
if (lf < 0) buf.append(side.get(src, hunk));
lf = src.length(); buf.append('\n');
else side.incSelf(hunk);
lf++; }
buf.append(src.substring(pos, lf));
pos = lf;
line++;
if (lCur != null && side.after(line, lCur)) {
lIdx++;
lCur = lIdx < lineEdits.size() ? lineEdits.get(lIdx) : null;
} }
} }
return buf; return buf;
@ -394,4 +460,29 @@ public abstract class PrettyFormatter {
src = src.replaceFirst("([ \t][ \t]*)(\r?(</span>)?\n?)$", r); src = src.replaceFirst("([ \t][ \t]*)(\r?(</span>)?\n?)$", r);
return src; return src;
} }
private String expandTabs(String html) {
StringBuilder tmp = new StringBuilder();
int i = 0;
if (settings.isShowTabs()) {
i = 1;
}
for (; i < settings.getTabSize(); i++) {
tmp.append("&nbsp;");
}
return html.replaceAll("\t", tmp.toString());
}
private String getFileType() {
String srcType = settings.getFilename();
if (srcType == null) {
return null;
}
int dot = srcType.lastIndexOf('.');
if (0 < dot) {
srcType = srcType.substring(dot + 1);
}
return srcType;
}
} }

View File

@ -12,14 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package com.google.gerrit.common.data; package com.google.gerrit.prettify.common;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class SparseFileContent { public class SparseFileContent {
protected String path;
protected List<Range> ranges; protected List<Range> ranges;
protected int size; protected int size;
protected boolean missingNewlineAtEnd; protected boolean missingNewlineAtEnd;
@ -46,18 +45,50 @@ public class SparseFileContent {
missingNewlineAtEnd = missing; missingNewlineAtEnd = missing;
} }
public SafeHtml get(final int idx) { public String getPath() {
return path;
}
public void setPath(String filePath) {
path = filePath;
}
public boolean isWholeFile() {
if (size == 0) {
return true;
} else if (1 == ranges.size()) {
Range r = ranges.get(0);
return r.base == 0 && r.end() == size;
} else {
return false;
}
}
public String get(final int idx) {
final String line = getLine(idx); final String line = getLine(idx);
if (line == null) { if (line == null) {
throw new ArrayIndexOutOfBoundsException(idx); throw new ArrayIndexOutOfBoundsException(idx);
} }
return SafeHtml.asis(line); return line;
} }
public boolean contains(final int idx) { public boolean contains(final int idx) {
return getLine(idx) != null; return getLine(idx) != null;
} }
public int mapIndexToLine(int arrayIndex) {
final int origIndex = arrayIndex;
for (Range r : ranges) {
if (arrayIndex < r.lines.size()) {
return r.base + arrayIndex;
}
arrayIndex -= r.lines.size();
}
throw new ArrayIndexOutOfBoundsException(origIndex);
}
private String getLine(final int idx) { private String getLine(final int idx) {
// Most requests are sequential in nature, fetching the next // Most requests are sequential in nature, fetching the next
// line from the current range, or the next range. // line from the current range, or the next range.
@ -95,10 +126,6 @@ public class SparseFileContent {
return null; return null;
} }
public void addLine(final int i, final SafeHtml content) {
addLine(i, content.asString());
}
public void addLine(final int i, final String content) { public void addLine(final int i, final String content) {
final Range r; final Range r;
if (!ranges.isEmpty() && i == last().end()) { if (!ranges.isEmpty() && i == last().end()) {
@ -114,6 +141,51 @@ public class SparseFileContent {
return ranges.get(ranges.size() - 1); return ranges.get(ranges.size() - 1);
} }
public String asString() {
final StringBuilder b = new StringBuilder();
for (Range r : ranges) {
for (String l : r.lines) {
b.append(l);
b.append('\n');
}
}
if (0 < b.length() && isMissingNewlineAtEnd()) {
b.setLength(b.length() - 1);
}
return b.toString();
}
public SparseFileContent completeWithContext(SparseFileContent a,
EditList editList) {
ArrayList<String> lines = new ArrayList<String>(size);
for (final EditList.Hunk hunk : editList.getFullContext().getHunks()) {
while (hunk.next()) {
if (hunk.isContextLine()) {
lines.add(a.get(hunk.getCurA()));
hunk.incBoth();
} else if (hunk.isDeletedA()) {
hunk.incA();
} else if (hunk.isInsertedB()) {
lines.add(get(hunk.getCurB()));
hunk.incB();
}
}
}
Range range = new Range();
range.lines = lines;
SparseFileContent r = new SparseFileContent();
r.setSize(size());
r.setMissingNewlineAtEnd(isMissingNewlineAtEnd());
r.setPath(getPath());
r.ranges.add(range);
return r;
}
@Override @Override
public String toString() { public String toString() {
final StringBuilder b = new StringBuilder(); final StringBuilder b = new StringBuilder();

View File

@ -0,0 +1,28 @@
// Copyright (C) 2010 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.prettify.common;
import com.google.gwtexpui.safehtml.client.SafeHtml;
public interface SparseHtmlFile {
/** @return the line of formatted HTML. */
public SafeHtml getSafeHtmlLine(int lineNo);
/** @return the number of lines in this sparse list. */
public int size();
/** @return true if the line is valid in this sparse list. */
public boolean contains(final int idx);
}

View File

@ -80,19 +80,13 @@ class ServerPrettyFactory implements PrettyFactory, Provider<PrettyFormatter> {
public PrettyFormatter get() { public PrettyFormatter get() {
return new PrettyFormatter() { return new PrettyFormatter() {
@Override @Override
protected String prettify(String html) { protected String prettify(String html, String type) {
return prettyPrintOne(html, settings); return prettyPrintOne(html, type, settings);
} }
}; };
} }
private String prettyPrintOne(String srcText, PrettySettings how) { private String prettyPrintOne(String srcText, String type, PrettySettings how) {
String srcType = how.getFilename();
int dot = srcType.lastIndexOf('.');
if (0 < dot) {
srcType = srcType.substring(dot + 1);
}
Context cx = contextFactory.enterContext(); Context cx = contextFactory.enterContext();
try { try {
Scriptable callScope = cx.newObject(sharedScope); Scriptable callScope = cx.newObject(sharedScope);
@ -110,7 +104,7 @@ class ServerPrettyFactory implements PrettyFactory, Provider<PrettyFormatter> {
callScope.put("window", callScope, callWindow); callScope.put("window", callScope, callWindow);
callScope.put("srcText", callScope, srcText); callScope.put("srcText", callScope, srcText);
callScope.put("srcType", callScope, srcType); callScope.put("srcType", callScope, type);
String call = "prettyPrintOne(srcText, srcType)"; String call = "prettyPrintOne(srcText, srcType)";
return cx.evaluateString(callScope, call, "<call>", 1, null).toString(); return cx.evaluateString(callScope, call, "<call>", 1, null).toString();