// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.collide.client.code.debugging;
import com.google.collide.client.code.debugging.DebuggerApiTypes.ConsoleMessage;
import com.google.collide.client.code.debugging.DebuggerApiTypes.ConsoleMessageLevel;
import com.google.collide.client.code.debugging.DebuggerApiTypes.ConsoleMessageType;
import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObject;
import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObjectSubType;
import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObjectType;
import com.google.collide.client.code.debugging.DebuggerApiTypes.StackTraceItem;
import com.google.collide.client.util.CssUtils;
import com.google.collide.client.util.Elements;
import com.google.collide.client.util.dom.DomUtils;
import com.google.collide.json.client.JsoArray;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.mvp.CompositeView;
import com.google.collide.mvp.UiComponent;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.StringUtils;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.ImageResource;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.html.AnchorElement;
import elemental.html.Element;
/**
* Console View.
*
*/
public class ConsoleView extends UiComponent<ConsoleView.View> {
public interface Css extends CssResource {
String root();
String consoleMessages();
String messageRoot();
String messageLink();
String repeatCountBubble();
String consoleObject();
String consolePrimitiveValue();
String consoleDebugLevel();
String consoleErrorLevel();
String consoleLogLevel();
String consoleTipLevel();
String consoleWarningLevel();
String consoleStackTrace();
String consoleStackTraceCollapsed();
String consoleStackTraceExpanded();
String consoleStackTraceController();
String consoleStackTraceItem();
}
interface Resources extends ClientBundle,
RemoteObjectTree.Resources,
RemoteObjectNodeRenderer.Resources {
@Source("ConsoleView.css")
Css workspaceEditorConsoleViewCss();
@Source("errorIcon.png")
ImageResource errorIcon();
@Source("warningIcon.png")
ImageResource warningIcon();
@Source("triangleRight.png")
ImageResource triangleRight();
@Source("triangleDown.png")
ImageResource triangleDown();
}
/**
* Listener of this pane's events.
*/
interface Listener {
void onLocationLinkClick(String url, int lineNumber);
}
/**
* The view for the sidebar call stack pane.
*/
static class View extends CompositeView<ViewEvents> {
private final Css css;
private final Resources resources;
private final RemoteObjectNodeRenderer nodeRenderer;
private final JsonArray<RemoteObjectTree> remoteObjectTrees = JsonCollections.createArray();
private final Element consoleMessages;
private final EventListener clickListener = new EventListener() {
@Override
public void handleEvent(Event evt) {
Element target = (Element) evt.getTarget();
if (target.hasClassName(css.messageLink())) {
evt.preventDefault();
evt.stopPropagation();
AnchorElement anchor = (AnchorElement) target;
int lineNumber = 0;
String anchorText = anchor.getTextContent();
int pos = anchorText.lastIndexOf(':');
if (pos != -1) {
try {
lineNumber = (int) Double.parseDouble(anchorText.substring(pos + 1));
// Safe convert from one-based number to zero-based.
lineNumber = Math.max(0, lineNumber - 1);
} catch (NumberFormatException e) {
// Ignore.
}
}
getDelegate().onLocationLinkClick(anchor.getHref(), lineNumber);
} else if (target.hasClassName(css.consoleStackTraceController())) {
Element messageRoot = CssUtils.getAncestorOrSelfWithClassName(target, css.messageRoot());
if (messageRoot != null) {
boolean expanded = messageRoot.hasClassName(css.consoleStackTraceExpanded());
CssUtils.setClassNameEnabled(messageRoot, css.consoleStackTraceExpanded(), !expanded);
CssUtils.setClassNameEnabled(messageRoot, css.consoleStackTraceCollapsed(), expanded);
}
}
}
};
View(Resources resources) {
this.resources = resources;
css = resources.workspaceEditorConsoleViewCss();
nodeRenderer = new RemoteObjectNodeRenderer(resources);
consoleMessages = Elements.createDivElement(css.consoleMessages());
consoleMessages.addEventListener(Event.CLICK, clickListener, false);
Element rootElement = Elements.createDivElement(css.root());
rootElement.appendChild(consoleMessages);
setElement(rootElement);
}
private void appendConsoleMessage(ConsoleMessage message, DebuggerState debuggerState) {
boolean forceObjectFormat = false;
JsonArray<RemoteObject> parameters;
if (message.getType() != null) {
switch (message.getType()) {
case TRACE:
// Discard all parameters.
parameters = JsoArray.from(DebuggerApiUtils.createRemoteObject("console.trace()"));
break;
case ASSERT:
parameters = JsoArray.from(DebuggerApiUtils.createRemoteObject("Assertion failed:"));
parameters.addAll(message.getParameters());
break;
case DIR:
case DIRXML:
// Use only first parameter, if any.
parameters = message.getParameters().size() > 0 ?
message.getParameters().slice(0, 1) :
JsoArray.from(DebuggerApiTypes.UNDEFINED_REMOTE_OBJECT);
forceObjectFormat = true;
break;
case ENDGROUP:
// TODO: Support console.group*() some day.
// Until then, just ignore the console.groupEnd().
return;
default:
parameters = message.getParameters();
break;
}
} else {
parameters = message.getParameters();
}
if (parameters.size() == 0) {
if (StringUtils.isNullOrEmpty(message.getText())) {
parameters = JsoArray.from(DebuggerApiTypes.UNDEFINED_REMOTE_OBJECT);
} else {
parameters = JsoArray.from(DebuggerApiUtils.createRemoteObject(message.getText()));
}
}
Element messageElement = Elements.createDivElement(css.messageRoot());
// Add message link first.
JsonArray<StackTraceItem> stackTrace = message.getStackTrace();
StackTraceItem topFrame = stackTrace.isEmpty() ? null : stackTrace.get(0);
if (topFrame != null && !StringUtils.isNullOrEmpty(topFrame.getUrl())) {
messageElement.appendChild(formatLocationLink(
topFrame.getUrl(), topFrame.getLineNumber(), topFrame.getColumnNumber()));
} else if (!StringUtils.isNullOrEmpty(message.getUrl())) {
messageElement.appendChild(formatLocationLink(
message.getUrl(), message.getLineNumber(), 0));
}
// Add the Stack Trace expand/collapse controller.
final boolean shouldDisplayStackTrace = !stackTrace.isEmpty() &&
(message.getType() == ConsoleMessageType.TRACE ||
message.getLevel() == ConsoleMessageLevel.ERROR);
if (shouldDisplayStackTrace) {
if (ConsoleMessageType.TRACE.equals(message.getType())) {
messageElement.addClassName(css.consoleStackTraceExpanded());
} else {
messageElement.addClassName(css.consoleStackTraceCollapsed());
}
messageElement.appendChild(Elements.createSpanElement(css.consoleStackTraceController()));
}
// Add all message arguments.
for (int i = 0, n = parameters.size(); i < n; ++i) {
if (i > 0) {
messageElement.appendChild(Elements.createTextNode(" "));
}
messageElement.appendChild(
formatRemoteObjectInConsole(parameters.get(i), debuggerState, forceObjectFormat));
}
if (message.getLevel() != null) {
switch (message.getLevel()) {
case DEBUG:
messageElement.addClassName(css.consoleDebugLevel());
break;
case ERROR:
messageElement.addClassName(css.consoleErrorLevel());
break;
case LOG:
messageElement.addClassName(css.consoleLogLevel());
break;
case TIP:
messageElement.addClassName(css.consoleTipLevel());
break;
case WARNING:
messageElement.addClassName(css.consoleWarningLevel());
break;
}
}
if (shouldDisplayStackTrace) {
messageElement.appendChild(formatStackTrace(stackTrace));
}
updateConsoleMessageCount(messageElement, message.getRepeatCount());
consoleMessages.appendChild(messageElement);
}
private void updateLastConsoleMessageCount(int repeatCount) {
Element messageElement = (Element) consoleMessages.getLastChild();
if (messageElement == null) {
return;
}
updateConsoleMessageCount(messageElement, repeatCount);
}
private void updateConsoleMessageCount(Element messageElement, int repeatCount) {
Element repeatCountElement = DomUtils.getFirstElementByClassName(
messageElement, css.repeatCountBubble());
if (repeatCountElement == null) {
if (repeatCount > 1) {
repeatCountElement = Elements.createSpanElement(css.repeatCountBubble());
repeatCountElement.setTextContent(Integer.toString(repeatCount));
messageElement.insertBefore(repeatCountElement, messageElement.getFirstChild());
} else {
// Do nothing.
}
} else {
if (repeatCount > 1) {
repeatCountElement.setTextContent(Integer.toString(repeatCount));
} else {
repeatCountElement.removeFromParent();
}
}
}
private Element formatRemoteObjectInConsole(RemoteObject remoteObject,
DebuggerState debuggerState, boolean forceObjectFormat) {
if (forceObjectFormat && remoteObject.hasChildren()) {
return formatRemoteObjectInConsoleAsObject(remoteObject, debuggerState);
}
RemoteObjectType type = remoteObject.getType();
RemoteObjectSubType subType = remoteObject.getSubType();
if (type == RemoteObjectType.OBJECT && (subType == null ||
subType == RemoteObjectSubType.ARRAY || subType == RemoteObjectSubType.NODE)) {
// TODO: Display small ARRAYs inlined.
// TODO: Display NODE objects as XML tree some day.
return formatRemoteObjectInConsoleAsObject(remoteObject, debuggerState);
}
Element messageElement = Elements.createSpanElement(css.consolePrimitiveValue());
if (!RemoteObjectType.STRING.equals(type)) {
String className = nodeRenderer.getTokenClassName(remoteObject);
if (!StringUtils.isNullOrEmpty(className)) {
messageElement.addClassName(className);
}
}
messageElement.setTextContent(remoteObject.getDescription());
return messageElement;
}
private Element formatRemoteObjectInConsoleAsObject(RemoteObject remoteObject,
DebuggerState debuggerState) {
RemoteObjectTree remoteObjectTree =
RemoteObjectTree.create(new RemoteObjectTree.View(resources), resources, debuggerState);
remoteObjectTrees.add(remoteObjectTree);
RemoteObjectNode newRoot = RemoteObjectNode.createRoot();
RemoteObjectNode child = new RemoteObjectNode.Builder("", remoteObject)
.setDeletable(false)
.setWritable(false)
.build();
newRoot.addChild(child);
remoteObjectTree.setRoot(newRoot);
Element messageElement = Elements.createSpanElement(css.consoleObject());
messageElement.appendChild(remoteObjectTree.getView().getElement());
return messageElement;
}
private Element formatLocationLink(String url, int lineNumber, int columnNumber) {
// TODO: Do real URL parsing to get the path's last component.
String locationName = url;
int pos = locationName.lastIndexOf('/');
if (pos != -1) {
locationName = locationName.substring(pos + 1);
if (StringUtils.isNullOrEmpty(locationName)) {
locationName = "/";
}
}
AnchorElement anchor = Elements.createAnchorElement(css.messageLink());
anchor.setHref(url);
anchor.setTarget("_blank");
anchor.setTitle(url);
anchor.setTextContent(locationName + ":" + lineNumber);
return anchor;
}
private Element formatStackTrace(JsonArray<StackTraceItem> stackTrace) {
Element stackTraceElement = Elements.createDivElement(css.consoleStackTrace());
for (int i = 0, n = stackTrace.size(); i < n; ++i) {
StackTraceItem item = stackTrace.get(i);
Element itemElement = Elements.createDivElement(css.consoleStackTraceItem());
itemElement.appendChild(
formatLocationLink(item.getUrl(), item.getLineNumber(), item.getColumnNumber()));
itemElement.appendChild(Elements.createTextNode(
StringUtils.ensureNotEmpty(item.getFunctionName(), "(anonymous function)")));
stackTraceElement.appendChild(itemElement);
}
return stackTraceElement;
}
private void clearConsoleMessages() {
for (int i = 0, n = remoteObjectTrees.size(); i < n; ++i) {
remoteObjectTrees.get(i).teardown();
}
remoteObjectTrees.clear();
consoleMessages.setInnerHTML("");
}
}
/**
* The view events.
*/
private interface ViewEvents {
// TODO: Add the button into the UI.
void onClearConsoleButtonClick();
void onLocationLinkClick(String url, int lineNumber);
}
static ConsoleView create(View view, DebuggerState debuggerState) {
return new ConsoleView(view, debuggerState);
}
private final DebuggerState debuggerState;
private Listener listener;
private final DebuggerState.ConsoleListener consoleListener =
new DebuggerState.ConsoleListener() {
@Override
public void onConsoleMessage(ConsoleMessage message) {
getView().appendConsoleMessage(message, debuggerState);
}
@Override
public void onConsoleMessageRepeatCountUpdated(ConsoleMessage message, int repeatCount) {
getView().updateLastConsoleMessageCount(repeatCount);
}
@Override
public void onConsoleMessagesCleared() {
getView().clearConsoleMessages();
}
};
private ConsoleView(View view, DebuggerState debuggerState) {
super(view);
this.debuggerState = debuggerState;
debuggerState.getConsoleListenerRegistrar().add(consoleListener);
view.setDelegate(new ViewEvents() {
@Override
public void onClearConsoleButtonClick() {
if (ConsoleView.this.debuggerState.isActive()) {
// TODO: Implement it in the debugger API.
} else {
getView().clearConsoleMessages();
}
}
@Override
public void onLocationLinkClick(String url, int lineNumber) {
if (listener != null) {
listener.onLocationLinkClick(url, lineNumber);
}
}
});
}
void setListener(Listener listener) {
this.listener = listener;
}
void show() {
// Do nothing.
}
void hide() {
getView().clearConsoleMessages();
}
}