// 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.util.CssUtils; import com.google.collide.client.util.Elements; import com.google.collide.client.util.PathUtil; import com.google.collide.client.util.dom.DomUtils; import com.google.collide.client.util.logging.Log; 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.SortedList; import com.google.collide.shared.util.StringUtils; import com.google.common.annotations.VisibleForTesting; 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.events.Event; import elemental.events.EventListener; import elemental.html.Element; /** * UI component that displays all breakpoints set in the project. * * TODO: Text changes of the lines that have breakpoints should be * reflected in this UI pane. */ public class DebuggingSidebarBreakpointsPane extends UiComponent< DebuggingSidebarBreakpointsPane.View> { public interface Css extends CssResource { String root(); String section(); String sectionHeader(); String sectionFileName(); String sectionFilePath(); String breakpoint(); String breakpointIcon(); String breakpointInactive(); String breakpointLine(); String breakpointLineNumber(); } interface Resources extends ClientBundle { @Source("DebuggingSidebarBreakpointsPane.css") Css workspaceEditorDebuggingSidebarBreakpointsPaneCss(); @Source("file.png") DataResource fileImageResource(); @Source("breakpointActive.png") ImageResource breakpointActiveResource(); @Source("breakpointInactive.png") ImageResource breakpointInactiveResource(); } /** * Listener for the user clicks on the breakpoints. */ interface Listener { void onBreakpointIconClick(Breakpoint breakpoint); void onBreakpointLineClick(Breakpoint breakpoint); } static class View extends CompositeView<ViewEvents> { private final Css css; private final EventListener breakpointClickListener = new EventListener() { @Override public void handleEvent(Event evt) { Element target = (Element) evt.getTarget(); Element breakpoint = CssUtils.getAncestorOrSelfWithClassName(target, css.breakpoint()); Element section = CssUtils.getAncestorOrSelfWithClassName(breakpoint, css.section()); int breakpointIndex = DomUtils.getSiblingIndexWithClassName(breakpoint, css.breakpoint()); int sectionIndex = DomUtils.getSiblingIndexWithClassName(section, css.section()); if (target.hasClassName(css.breakpointIcon())) { getDelegate().onBreakpointIconClick(sectionIndex, breakpointIndex); } else { getDelegate().onBreakpointLineClick(sectionIndex, breakpointIndex); } } }; View(Resources resources) { css = resources.workspaceEditorDebuggingSidebarBreakpointsPaneCss(); Element rootElement = Elements.createDivElement(css.root()); setElement(rootElement); } @VisibleForTesting void addBreakpointSection(int sectionIndex) { getElement().insertBefore(createSection(), getSectionElement(sectionIndex)); } @VisibleForTesting void removeBreakpointSection(int sectionIndex) { getSectionElement(sectionIndex).removeFromParent(); } @VisibleForTesting void addBreakpoint(int sectionIndex, int breakpointIndex) { getSectionElement(sectionIndex).insertBefore(createBreakpoint(), getBreakpointElement(sectionIndex, breakpointIndex)); } @VisibleForTesting void removeBreakpoint(int sectionIndex, int breakpointIndex) { getBreakpointElement(sectionIndex, breakpointIndex).removeFromParent(); } private void updateBreakpointSection(int sectionIndex, String fileName, String path) { Element section = getSectionElement(sectionIndex); DomUtils.getFirstElementByClassName(section, css.sectionFileName()).setTextContent(fileName); DomUtils.getFirstElementByClassName(section, css.sectionFilePath()).setTextContent(path); } private void updateBreakpoint(int sectionIndex, int breakpointIndex, boolean active, String line, int lineNumber) { line = StringUtils.nullToEmpty(line).trim(); Element breakpoint = getBreakpointElement(sectionIndex, breakpointIndex); CssUtils.setClassNameEnabled(breakpoint, css.breakpointInactive(), !active); DomUtils.getFirstElementByClassName(breakpoint, css.breakpointLine()).setTextContent(line); // TODO: i18n? DomUtils.getFirstElementByClassName(breakpoint, css.breakpointLineNumber()) .setTextContent("line " + (lineNumber + 1)); // Set a tooltip. // TODO: Do we actually need this? String tooltip = line + " line " + (lineNumber + 1); breakpoint.setTitle(tooltip); } private String getBreakpointLineText(int sectionIndex, int breakpointIndex) { Element breakpoint = getBreakpointElement(sectionIndex, breakpointIndex); Element line = DomUtils.getFirstElementByClassName(breakpoint, css.breakpointLine()); return line.getTextContent(); } private Element createSection() { Element section = Elements.createDivElement(css.section()); // TODO: i18n? Element separator = Elements.createSpanElement(); separator.setInnerHTML("—"); Element sectionHeader = Elements.createDivElement(css.sectionHeader()); sectionHeader.appendChild(Elements.createSpanElement(css.sectionFileName())); sectionHeader.appendChild(separator); sectionHeader.appendChild(Elements.createSpanElement(css.sectionFilePath())); section.appendChild(sectionHeader); return section; } private Element createBreakpoint() { Element breakpoint = Elements.createDivElement(css.breakpoint()); breakpoint.appendChild(Elements.createDivElement(css.breakpointIcon())); breakpoint.appendChild(Elements.createSpanElement(css.breakpointLine())); breakpoint.appendChild(Elements.createSpanElement(css.breakpointLineNumber())); breakpoint.addEventListener(Event.CLICK, breakpointClickListener, false); return breakpoint; } private Element getSectionElement(int sectionIndex) { return DomUtils.getNthChildWithClassName(getElement(), sectionIndex, css.section()); } private Element getBreakpointElement(int sectionIndex, int breakpointIndex) { Element section = getSectionElement(sectionIndex); return DomUtils.getNthChildWithClassName(section, breakpointIndex, css.breakpoint()); } } /** * The view events. */ private interface ViewEvents { void onBreakpointIconClick(int sectionIndex, int breakpointIndex); void onBreakpointLineClick(int sectionIndex, int breakpointIndex); } static DebuggingSidebarBreakpointsPane create(View view) { return new DebuggingSidebarBreakpointsPane(view); } private static final SortedList.Comparator<Breakpoint> SORTING_FUNCTION = new SortedList.Comparator<Breakpoint>() { @Override public int compare(Breakpoint a, Breakpoint b) { int result = a.getPath().compareTo(b.getPath()); if (result != 0) { return result; } return a.getLineNumber() - b.getLineNumber(); } }; private Listener delegateListener; private final SortedList<Breakpoint> breakpoints = new SortedList<Breakpoint>(SORTING_FUNCTION); private final JsonArray<Integer> breakpointCountBySections = JsonCollections.createArray(); private final class ViewEventsImpl implements ViewEvents { @Override public void onBreakpointIconClick(int sectionIndex, int breakpointIndex) { Breakpoint breakpoint = getBreakpoint(sectionIndex, breakpointIndex); if (breakpoint == null) { Log.error(getClass(), "Failed to find a breakpoint at " + sectionIndex + ":" + breakpointIndex); return; } if (delegateListener != null) { delegateListener.onBreakpointIconClick(breakpoint); } } @Override public void onBreakpointLineClick(int sectionIndex, int breakpointIndex) { Breakpoint breakpoint = getBreakpoint(sectionIndex, breakpointIndex); if (breakpoint == null) { Log.error(getClass(), "Failed to find a breakpoint at " + sectionIndex + ":" + breakpointIndex); return; } if (delegateListener != null) { delegateListener.onBreakpointLineClick(breakpoint); } } } @VisibleForTesting DebuggingSidebarBreakpointsPane(View view) { super(view); view.setDelegate(new ViewEventsImpl()); } void setListener(Listener listener) { delegateListener = listener; } void addBreakpoint(Breakpoint breakpoint) { int index = breakpoints.add(breakpoint); int section = -1; int breakpointCount = 0; while (breakpointCount < index && section + 1 < breakpointCountBySections.size()) { ++section; breakpointCount += breakpointCountBySections.get(section); } // Now, the breakpoint can be inserted into the following sections: // 1) {@code section}, if it exists and should contain the new breakpoint // 2) {@code section + 1}, if it exists and should contain the new breakpoint // 3) A new section int breakpointIndexInSection; if (index > 0 && breakpoint.getPath().compareTo(breakpoints.get(index - 1).getPath()) == 0) { int num = breakpointCountBySections.get(section); breakpointCountBySections.set(section, num + 1); breakpointIndexInSection = index - breakpointCount + num; } else if (index + 1 < breakpoints.size() && breakpoint.getPath().compareTo(breakpoints.get(index + 1).getPath()) == 0) { ++section; int num = breakpointCountBySections.get(section); breakpointCountBySections.set(section, num + 1); breakpointIndexInSection = index - breakpointCount; } else { ++section; breakpointCountBySections.splice(section, 0, 1); breakpointIndexInSection = 0; getView().addBreakpointSection(section); getView().updateBreakpointSection(section, breakpoint.getPath().getBaseName(), PathUtil.createExcludingLastN(breakpoint.getPath(), 1).getPathString()); } getView().addBreakpoint(section, breakpointIndexInSection); getView().updateBreakpoint(section, breakpointIndexInSection, breakpoint.isActive(), "", breakpoint.getLineNumber()); } void removeBreakpoint(Breakpoint breakpoint) { int index = breakpoints.findIndex(breakpoint); if (index < 0) { Log.error(getClass(), "Failed to remove a breakpoint: " + breakpoint); return; } breakpoints.remove(index); Position position = getBreakpointPosition(index); int section = position.sectionIndex; int breakpointsInSection = breakpointCountBySections.get(section); getView().removeBreakpoint(section, position.breakpointIndex); if (breakpointsInSection > 1) { breakpointCountBySections.set(section, breakpointsInSection - 1); } else { breakpointCountBySections.splice(section, 1); getView().removeBreakpointSection(section); } } void updateBreakpoint(Breakpoint breakpoint, String line) { int index = breakpoints.findIndex(breakpoint); if (index < 0) { Log.error(getClass(), "Failed to update a breakpoint: " + breakpoint); return; } Position position = getBreakpointPosition(index); getView().updateBreakpoint(position.sectionIndex, position.breakpointIndex, breakpoint.isActive(), line, breakpoint.getLineNumber()); } boolean hasBreakpoint(Breakpoint breakpoint) { return breakpoints.findIndex(breakpoint) >= 0; } String getBreakpointLineText(Breakpoint breakpoint) { int index = breakpoints.findIndex(breakpoint); if (index < 0) { Log.error(getClass(), "Failed to find a breakpoint: " + breakpoint); return ""; } Position position = getBreakpointPosition(index); return getView().getBreakpointLineText(position.sectionIndex, position.breakpointIndex); } int getBreakpointCount() { return breakpoints.size(); } private Position getBreakpointPosition(int index) { int section = -1; int breakpointCount = 0; while (breakpointCount <= index && section + 1 < breakpointCountBySections.size()) { ++section; breakpointCount += breakpointCountBySections.get(section); } int breakpointsInSection = breakpointCountBySections.get(section); int breakpointIndexInSection = index - breakpointCount + breakpointsInSection; return new Position(section, breakpointIndexInSection); } private Breakpoint getBreakpoint(int sectionIndex, int breakpointIndex) { int index = breakpointIndex; for (int i = 0; i < sectionIndex; ++i) { index += breakpointCountBySections.get(i); } return breakpoints.get(index); } private static class Position { private final int sectionIndex; private final int breakpointIndex; private Position(int sectionIndex, int breakpointIndex) { this.sectionIndex = sectionIndex; this.breakpointIndex = breakpointIndex; } } }