// 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.AppContext; import com.google.collide.client.code.debugging.DebuggerApiTypes.CallFrame; import com.google.collide.client.code.debugging.DebuggerApiTypes.Location; import com.google.collide.client.code.debugging.DebuggerApiTypes.OnPausedResponse; import com.google.collide.client.code.debugging.DebuggerApiTypes.OnScriptParsedResponse; import com.google.collide.client.code.debugging.DebuggerApiTypes.RemoteObject; import com.google.collide.client.code.debugging.DebuggerApiTypes.Scope; import com.google.collide.client.editor.Editor; import com.google.collide.client.testing.DebugAttributeSetter; import com.google.collide.client.util.CssUtils; import com.google.collide.client.util.Elements; import com.google.collide.client.util.JsIntegerMap; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.util.JsonCollections; import com.google.collide.shared.util.StringUtils; import com.google.common.base.Preconditions; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.DataResource; import com.google.gwt.resources.client.ImageResource; import elemental.css.CSSStyleDeclaration; import elemental.html.Element; /** * A renderer for the debugging model. */ public class DebuggingModelRenderer { /** * CssResource for the debugging model UI. */ public interface Css extends CssResource { String breakpoint(); String breakpointInactive(); String executionLine(); String gutterExecutionLine(); } /** * ClientBundle for the debugging model UI. */ public interface Resources extends ClientBundle { @Source({"DebuggingModelRenderer.css", "com/google/collide/client/editor/constants.css"}) Css workspaceEditorDebuggingModelCss(); @Source("gutterExecutionLine.png") ImageResource gutterExecutionLine(); @Source("breakpointGutterActive.png") DataResource breakpointGutterActiveResource(); @Source("breakpointGutterInactive.png") DataResource breakpointGutterInactiveResource(); } public static DebuggingModelRenderer create(AppContext appContext, Editor editor, DebuggingSidebar debuggingSidebar, DebuggerState debuggerState) { return new DebuggingModelRenderer(appContext, editor, debuggingSidebar, debuggerState); } private final Css css; private final Editor editor; private final DebuggingSidebar debuggingSidebar; private final DebuggerState debuggerState; private JsIntegerMap<Element> lineNumberToElementCache = JsIntegerMap.create(); private AnchoredExecutionLine anchoredExecutionLine; private DebuggingModelRenderer(AppContext appContext, Editor editor, DebuggingSidebar debuggingSidebar, DebuggerState debuggerState) { this.css = appContext.getResources().workspaceEditorDebuggingModelCss(); this.editor = editor; this.debuggingSidebar = debuggingSidebar; this.debuggerState = debuggerState; } void renderBreakpointOnGutter(int lineNumber, boolean active) { Element element = lineNumberToElementCache.get(lineNumber); if (element == null) { element = Elements.createDivElement(css.breakpoint()); element.getStyle().setTop(editor.getBuffer().calculateLineTop(lineNumber), CSSStyleDeclaration.Unit.PX); new DebugAttributeSetter().add("linenumber", String.valueOf(lineNumber + 1)).on(element); editor.getLeftGutter().addUnmanagedElement(element); lineNumberToElementCache.put(lineNumber, element); } CssUtils.setClassNameEnabled(element, css.breakpointInactive(), !active); } void removeBreakpointOnGutter(int lineNumber) { Element element = lineNumberToElementCache.get(lineNumber); if (element != null) { editor.getLeftGutter().removeUnmanagedElement(element); lineNumberToElementCache.erase(lineNumber); } } void renderDebuggerState() { debuggingSidebar.setActive(debuggerState.isActive()); debuggingSidebar.setPaused(debuggerState.isPaused()); debuggingSidebar.clearCallStack(); if (debuggerState.isPaused()) { OnPausedResponse onPausedResponse = Preconditions.checkNotNull( debuggerState.getOnPausedResponse()); JsonArray<CallFrame> callFrames = onPausedResponse.getCallFrames(); for (int i = 0, n = callFrames.size(); i < n; ++i) { CallFrame callFrame = callFrames.get(i); Location location = callFrame.getLocation(); OnScriptParsedResponse onScriptParsedResponse = debuggerState.getOnScriptParsedResponse( location.getScriptId()); // TODO: What about i18n? String title = StringUtils.ensureNotEmpty(callFrame.getFunctionName(), "(anonymous function)"); String subtitle = getShortenedScriptUrl(onScriptParsedResponse) + ":" + (location.getLineNumber() + 1); debuggingSidebar.addCallFrame(title, subtitle); } } } void renderDebuggerCallFrame() { CallFrame callFrame = debuggerState.getActiveCallFrame(); if (callFrame == null) { // Debugger is not paused. Remove the previous scope tree UI. debuggingSidebar.setScopeVariablesRootNodes(null); debuggingSidebar.refreshWatchExpressions(); return; } // Render the Scope Variables pane. JsonArray<RemoteObjectNode> rootNodes = JsonCollections.createArray(); JsonArray<Scope> scopeChain = callFrame.getScopeChain(); for (int i = 0, n = scopeChain.size(); i < n; ++i) { Scope scope = scopeChain.get(i); String name = StringUtils.capitalizeFirstLetter(scope.getType().toString()); RemoteObject remoteObject = scope.getObject(); RemoteObjectNode.Builder scopeNodeBuilder = new RemoteObjectNode.Builder(name, remoteObject) .setOrderIndex(i) .setWritable(false) .setDeletable(false) .setTransient(scope.isTransient()); // Append the call frame "this" object to the top scope. if (i == 0 && callFrame.getThis() != null) { RemoteObjectNode thisNode = new RemoteObjectNode.Builder("this", callFrame.getThis()) .setWritable(false) .setDeletable(false) .build(); RemoteObjectNode scopeNode = scopeNodeBuilder .setHasChildren(true) // At least will contain the "this" child. .build(); scopeNode.addChild(thisNode); rootNodes.add(scopeNode); } else { rootNodes.add(scopeNodeBuilder.build()); } } debuggingSidebar.setScopeVariablesRootNodes(rootNodes); debuggingSidebar.refreshWatchExpressions(); } void renderExecutionLine(int lineNumber) { removeExecutionLine(); anchoredExecutionLine = AnchoredExecutionLine.create(editor, lineNumber, css.executionLine(), css.gutterExecutionLine()); } void removeExecutionLine() { if (anchoredExecutionLine != null) { anchoredExecutionLine.teardown(); anchoredExecutionLine = null; } } private static String getShortenedScriptUrl(OnScriptParsedResponse onScriptParsedResponse) { String url = onScriptParsedResponse != null ? onScriptParsedResponse.getUrl() : null; if (StringUtils.isNullOrEmpty(url)) { // TODO: What about i18n? return "unknown"; } if (onScriptParsedResponse.isContentScript()) { return url; // No shortening. } while (url.endsWith("/")) { url = url.substring(0, url.length() - 1); } int pos = url.lastIndexOf("/"); if (pos >= 0) { return url.substring(pos + 1); } return url; } void handleDocumentChanged() { lineNumberToElementCache = JsIntegerMap.create(); removeExecutionLine(); } }