// 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.bootstrap.BootstrapSession; import com.google.collide.client.code.FileSelectedPlace; import com.google.collide.client.code.RightSidebarExpansionEvent; import com.google.collide.client.code.popup.EditorPopupController; import com.google.collide.client.communication.ResourceUriUtils; import com.google.collide.client.document.DocumentManager; import com.google.collide.client.documentparser.DocumentParser; import com.google.collide.client.editor.Editor; import com.google.collide.client.editor.gutter.Gutter; import com.google.collide.client.history.Place; import com.google.collide.client.util.PathUtil; import com.google.collide.client.workspace.PopupBlockedInstructionalPopup; import com.google.collide.client.workspace.RunApplicationEvent; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.document.Document; import com.google.collide.shared.document.anchor.Anchor; import com.google.collide.shared.util.ListenerRegistrar; import com.google.common.base.Preconditions; import elemental.client.Browser; import elemental.html.Element; import elemental.html.Window; /** * A controller for the debugging model state. */ public class DebuggingModelController { public static DebuggingModelController create(Place currentPlace, AppContext appContext, DebuggingModel debuggingModel, Editor editor, EditorPopupController editorPopupController, DocumentManager documentManager) { DebuggingModelController dmc = new DebuggingModelController(currentPlace, appContext, debuggingModel, editor, editorPopupController, documentManager); dmc.populateDebuggingSidebar(); // Register the DebuggingModelController as a handler for clicks on the // Header's run button. currentPlace.registerSimpleEventHandler(RunApplicationEvent.TYPE, dmc.runApplicationHandler); return dmc; } /** * Flag indicating that "popup blocked" alert was shown once to user, so we * should not annoy user with it again. * * <p>This flag is used by and changed by {@link #createOrOpenPopup}. */ private static boolean doNotShowPopupBlockedInstruction; /** * Creates or reuses launchpad window. * * <p>This method should be called from user initiated event handler. * * <p>If popup window already exists, it is cleared. Launchpad is filled * with disclaimer that informs user that deployment is in progress. * * <p>Actually, popup is never blocked, because it is initiated by user. * But in case it is not truth, we show alert to user that instructs how to * enable popups. */ public static Window createOrOpenPopup(AppContext appContext) { Window popup = Browser.getWindow().open("", BootstrapSession.getBootstrapSession().getActiveClientId()); if (popup == null) { if (!doNotShowPopupBlockedInstruction) { doNotShowPopupBlockedInstruction = true; PopupBlockedInstructionalPopup.create(appContext.getResources()).show(); } } return popup; } /** * Handler for clicks on the run button on the workspace header. */ private final RunApplicationEvent.Handler runApplicationHandler = new RunApplicationEvent.Handler() { @Override public void onRunButtonClicked(RunApplicationEvent evt) { runApplication(evt.getUrl()); } }; private final DebuggingModel.DebuggingModelChangeListener debuggingModelChangeListener = new DebuggingModel.DebuggingModelChangeListener() { @Override public void onBreakpointAdded(Breakpoint newBreakpoint) { if (shouldProcessBreakpoint(newBreakpoint) && !breakpoints.contains(newBreakpoint)) { anchorBreakpointAndUpdateSidebar(newBreakpoint); debuggingModelRenderer.renderBreakpointOnGutter( newBreakpoint.getLineNumber(), newBreakpoint.isActive()); } debuggerState.setBreakpoint(newBreakpoint); debuggingSidebar.addBreakpoint(newBreakpoint); } @Override public void onBreakpointRemoved(Breakpoint oldBreakpoint) { if (shouldProcessBreakpoint(oldBreakpoint) && breakpoints.contains(oldBreakpoint)) { breakpoints.removeBreakpoint(oldBreakpoint); debuggingModelRenderer.removeBreakpointOnGutter(oldBreakpoint.getLineNumber()); } debuggerState.removeBreakpoint(oldBreakpoint); debuggingSidebar.removeBreakpoint(oldBreakpoint); } @Override public void onBreakpointReplaced(Breakpoint oldBreakpoint, Breakpoint newBreakpoint) { /* * If a breakpoint is not in the document being displayed, we do not * know the code line that it was attached to, thus we save the old * breakpoint's line, if any. */ String breakpointLine = debuggingSidebar.getBreakpointLineText(oldBreakpoint); onBreakpointRemoved(oldBreakpoint); onBreakpointAdded(newBreakpoint); if (!shouldProcessBreakpoint(newBreakpoint)) { debuggingSidebar.updateBreakpoint(newBreakpoint, breakpointLine); } } @Override public void onPauseOnExceptionsModeUpdated(DebuggingModel.PauseOnExceptionsMode oldMode, DebuggingModel.PauseOnExceptionsMode newMode) { // TODO: Implement this in the UI. // TODO: Update DebuggerState. } @Override public void onBreakpointsEnabledUpdated(boolean newValue) { debuggerState.setBreakpointsEnabled(newValue); debuggingSidebar.setAllBreakpointsActive(newValue); } }; private final Gutter.ClickListener leftGutterClickListener = new Gutter.ClickListener() { @Override public void onClick(int y) { int lineNumber = editor.getBuffer().convertYToLineNumber(y, true); for (int i = 0; i < breakpoints.size(); ++i) { Breakpoint breakpoint = breakpoints.get(i); if (breakpoint.getLineNumber() == lineNumber) { debuggingModel.removeBreakpoint(breakpoint); return; } } if (!isCurrentDocumentEligibleForDebugging()) { return; } Breakpoint breakpoint = new Breakpoint.Builder(path, lineNumber).build(); debuggingModel.addBreakpoint(breakpoint); // Show the sidebar if the very first breakpoint has just been set. maybeShowSidebar(); } }; private boolean isCurrentDocumentEligibleForDebugging() { Preconditions.checkNotNull(path); // TODO: Be more smart here. Also what about source mappings? String baseName = path.getBaseName().toLowerCase(); return baseName.endsWith(".js") || baseName.endsWith(".htm") || baseName.endsWith(".html"); } /** * Handler that receives events from the remote debugger about changes of it's * state (e.g. a {@code running} debugger changed to {@code paused}, etc.). */ private final DebuggerState.DebuggerStateListener debuggerStateListener = new DebuggerState.DebuggerStateListener() { @Override public void onDebuggerStateChange() { if (debuggerState.isPaused()) { showSidebar(); } debuggingModelRenderer.renderDebuggerState(); handleOnCallFrameSelect(0); } }; /** * Handler that receives user commands, such as clicks on the debugger control * buttons in the sidebar. */ private final DebuggingSidebar.DebuggerCommandListener userCommandListener = new DebuggingSidebar.DebuggerCommandListener() { @Override public void onPause() { debuggerState.pause(); } @Override public void onResume() { debuggerState.resume(); } @Override public void onStepOver() { debuggerState.stepOver(); } @Override public void onStepInto() { debuggerState.stepInto(); } @Override public void onStepOut() { debuggerState.stepOut(); } @Override public void onCallFrameSelect(int depth) { handleOnCallFrameSelect(depth); } @Override public void onBreakpointIconClick(Breakpoint breakpoint) { Breakpoint newBreakpoint = new Breakpoint.Builder(breakpoint) .setActive(!breakpoint.isActive()) .build(); debuggingModel.updateBreakpoint(breakpoint, newBreakpoint); } @Override public void onBreakpointLineClick(Breakpoint breakpoint) { maybeNavigateToDocument(breakpoint.getPath(), breakpoint.getLineNumber()); } @Override public void onActivateBreakpoints() { debuggingModel.setBreakpointsEnabled(true); } @Override public void onDeactivateBreakpoints() { debuggingModel.setBreakpointsEnabled(false); } @Override public void onLocationLinkClick(String url, int lineNumber) { SourceMapping sourceMapping = debuggerState.getSourceMapping(); if (sourceMapping == null) { // Ignore. Maybe the debugger has just been shutdown. return; } PathUtil path = sourceMapping.getLocalSourcePath(url); if (path != null) { maybeNavigateToDocument(path, lineNumber); } } }; /** * Handler that receives notifications when a breakpoint description changes. */ private final AnchoredBreakpoints.BreakpointDescriptionListener breakpointDescriptionListener = new AnchoredBreakpoints.BreakpointDescriptionListener() { @Override public void onBreakpointDescriptionChange(Breakpoint breakpoint, String newText) { debuggingSidebar.updateBreakpoint(breakpoint, newText); } }; private final AppContext appContext; private final Editor editor; private final DebuggingModel debuggingModel; private final ListenerRegistrar.Remover leftGutterClickListenerRemover; private final DebuggerState debuggerState; private final DebuggingSidebar debuggingSidebar; private final DebuggingModelRenderer debuggingModelRenderer; private final Place currentPlace; private final CssLiveEditController cssLiveEditController; private final EvaluationPopupController evaluationPopupController; private PathUtil path; private AnchoredBreakpoints breakpoints; /** * Indicator that sidebar has been discovered by user. */ private boolean sidebarDiscovered; private DebuggingModelController(Place currentPlace, AppContext appContext, DebuggingModel debuggingModel, Editor editor, EditorPopupController editorPopupController, DocumentManager documentManager) { this.appContext = appContext; this.editor = editor; this.currentPlace = currentPlace; this.debuggingModel = debuggingModel; this.leftGutterClickListenerRemover = editor.getLeftGutter().getClickListenerRegistrar().add(leftGutterClickListener); // Every time we enter workspace, we get a new debugging session id. String sessionId = BootstrapSession.getBootstrapSession().getActiveClientId() + ":" + System.currentTimeMillis(); this.debuggerState = DebuggerState.create(sessionId); this.debuggingSidebar = DebuggingSidebar.create(appContext.getResources(), debuggerState); this.debuggingModelRenderer = DebuggingModelRenderer.create(appContext, editor, debuggingSidebar, debuggerState); this.cssLiveEditController = new CssLiveEditController(debuggerState, documentManager); this.evaluationPopupController = EvaluationPopupController.create( appContext.getResources(), editor, editorPopupController, debuggerState); this.debuggingModel.addModelChangeListener(debuggingModelChangeListener); this.debuggerState.getDebuggerStateListenerRegistrar().add(debuggerStateListener); this.debuggingSidebar.getDebuggerCommandListenerRegistrar().add(userCommandListener); } public void setDocument(Document document, PathUtil path, DocumentParser parser) { if (breakpoints != null) { breakpoints.teardown(); debuggingModelRenderer.handleDocumentChanged(); } this.path = path; breakpoints = new AnchoredBreakpoints(debuggingModel, document); anchorBreakpoints(); maybeAnchorExecutionLine(); evaluationPopupController.setDocument(document, path, parser); breakpoints.setBreakpointDescriptionListener(breakpointDescriptionListener); } public Element getDebuggingSidebarElement() { return debuggingSidebar.getView().getElement(); } /** * @see #runApplication(String) */ public boolean runApplication(PathUtil applicationPath) { String baseUri = ResourceUriUtils.getAbsoluteResourceBaseUri(); SourceMapping sourceMapping = StaticSourceMapping.create(baseUri); return runApplication(sourceMapping, sourceMapping.getRemoteSourceUri(applicationPath)); } /** * Runs a given resource via debugger API if it is available, or just opens * the resource in a new window. * * @param absoluteResourceUri absolute resource URI * @return {@code true} if the application was started successfully via * debugger API, {@code false} if it was opened in a new window */ public boolean runApplication(String absoluteResourceUri) { final String baseUri; // Check if the URI points to a local path. String workspaceBaseUri = ResourceUriUtils.getAbsoluteResourceBaseUri(); if (absoluteResourceUri.startsWith(workspaceBaseUri + "/")) { baseUri = workspaceBaseUri; } else { baseUri = ResourceUriUtils.extractBaseUri(absoluteResourceUri); } SourceMapping sourceMapping = StaticSourceMapping.create(baseUri); return runApplication(sourceMapping, absoluteResourceUri); } private boolean runApplication(SourceMapping sourceMapping, String absoluteResourceUri) { if (debuggerState.isDebuggerAvailable()) { debuggerState.runDebugger(sourceMapping, absoluteResourceUri); JsonArray<Breakpoint> allBreakpoints = debuggingModel.getBreakpoints(); for (int i = 0; i < allBreakpoints.size(); ++i) { debuggerState.setBreakpoint(allBreakpoints.get(i)); } debuggerState.setBreakpointsEnabled(debuggingModel.isBreakpointsEnabled()); return true; } else { Window popup = createOrOpenPopup(appContext); if (popup != null) { popup.getLocation().assign(absoluteResourceUri); // Show the sidebar once to promote the Debugger Extension. maybeShowSidebar(); } return false; } } private void anchorBreakpoints() { JsonArray<Breakpoint> allBreakpoints = debuggingModel.getBreakpoints(); for (int i = 0; i < allBreakpoints.size(); ++i) { Breakpoint breakpoint = allBreakpoints.get(i); if (path.equals(breakpoint.getPath())) { anchorBreakpointAndUpdateSidebar(breakpoint); debuggingModelRenderer.renderBreakpointOnGutter( breakpoint.getLineNumber(), breakpoint.isActive()); } } } private void populateDebuggingSidebar() { JsonArray<Breakpoint> allBreakpoints = debuggingModel.getBreakpoints(); for (int i = 0; i < allBreakpoints.size(); ++i) { debuggingSidebar.addBreakpoint(allBreakpoints.get(i)); } } private void anchorBreakpointAndUpdateSidebar(Breakpoint breakpoint) { Anchor anchor = breakpoints.anchorBreakpoint(breakpoint); debuggingSidebar.updateBreakpoint(breakpoint, anchor.getLine().getText()); } private void maybeAnchorExecutionLine() { PathUtil callFramePath = debuggerState.getActiveCallFramePath(); if (callFramePath != null && callFramePath.equals(path)) { int lineNumber = debuggerState.getActiveCallFrameExecutionLineNumber(); if (lineNumber >= 0) { debuggingModelRenderer.renderExecutionLine(lineNumber); } } } private void showSidebar() { sidebarDiscovered = true; currentPlace.fireEvent(new RightSidebarExpansionEvent(true)); } /** * Shows sidebar in case user hasn't discovered yet, and at least one * breakpoint is set. */ private void maybeShowSidebar() { if (!sidebarDiscovered && debuggingModel.getBreakpointCount() != 0) { showSidebar(); } } private boolean shouldProcessBreakpoint(Breakpoint breakpoint) { return path != null && path.equals(breakpoint.getPath()); } private void handleOnCallFrameSelect(int callFrameDepth) { debuggerState.setActiveCallFrameIndex(callFrameDepth); debuggingModelRenderer.renderDebuggerCallFrame(); PathUtil callFramePath = debuggerState.getActiveCallFramePath(); if (callFramePath == null) { // Not paused, remove the execution line (if any). debuggingModelRenderer.removeExecutionLine(); return; } maybeAnchorExecutionLine(); int lineNumber = debuggerState.getActiveCallFrameExecutionLineNumber(); maybeNavigateToDocument(callFramePath, lineNumber); } private void maybeNavigateToDocument(PathUtil documentPath, int lineNumber) { if (documentPath.equals(this.path)) { editor.getFocusManager().focus(); editor.scrollTo(lineNumber, 0); } else { currentPlace.fireChildPlaceNavigation( FileSelectedPlace.PLACE.createNavigationEvent(documentPath, lineNumber)); } } public void cleanup() { debuggerState.shutdown(); leftGutterClickListenerRemover.remove(); } }