462 lines
18 KiB
JavaScript
462 lines
18 KiB
JavaScript
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.BreakpointPopover = class BreakpointPopover extends WI.Popover
|
|
{
|
|
constructor(delegate, breakpoint)
|
|
{
|
|
console.assert(!breakpoint || breakpoint instanceof WI.Breakpoint, breakpoint);
|
|
|
|
super(delegate);
|
|
|
|
this._breakpoint = breakpoint || null;
|
|
|
|
console.assert(this._breakpoint?.constructor.ReferencePage || this.constructor.ReferencePage, "Should have a link to a reference page.");
|
|
|
|
this._contentElement = null;
|
|
this._conditionCodeMirror = null;
|
|
this._ignoreCountInputElement = null;
|
|
this._actionsContainerElement = null;
|
|
this._actionViews = [];
|
|
this._optionsRowElement = null;
|
|
this._autoContinueCheckboxElement = null;
|
|
|
|
this._targetElement = null;
|
|
|
|
this.windowResizeHandler = this._presentOverTargetElement.bind(this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static appendContextMenuItems(contextMenu, breakpoint, targetElement)
|
|
{
|
|
if (breakpoint.editable && targetElement) {
|
|
contextMenu.appendItem(WI.UIString("Edit Breakpoint\u2026"), () => {
|
|
const delegate = null;
|
|
let popover;
|
|
if (breakpoint instanceof WI.EventBreakpoint)
|
|
popover = new WI.EventBreakpointPopover(delegate, breakpoint);
|
|
else if (breakpoint instanceof WI.URLBreakpoint)
|
|
popover = new WI.URLBreakpointPopover(delegate, breakpoint);
|
|
else
|
|
popover = new WI.BreakpointPopover(delegate, breakpoint);
|
|
popover.show(targetElement);
|
|
});
|
|
}
|
|
|
|
if (!breakpoint.disabled) {
|
|
contextMenu.appendItem(WI.UIString("Disable Breakpoint"), () => {
|
|
breakpoint.disabled = !breakpoint.disabled;
|
|
});
|
|
|
|
if (breakpoint.editable && breakpoint.autoContinue) {
|
|
contextMenu.appendItem(WI.UIString("Cancel Automatic Continue"), () => {
|
|
breakpoint.autoContinue = !breakpoint.autoContinue;
|
|
});
|
|
}
|
|
} else {
|
|
contextMenu.appendItem(WI.UIString("Enable Breakpoint"), () => {
|
|
breakpoint.disabled = !breakpoint.disabled;
|
|
});
|
|
}
|
|
|
|
if (breakpoint.editable && !breakpoint.autoContinue && !breakpoint.disabled && breakpoint.actions.length) {
|
|
contextMenu.appendItem(WI.UIString("Set to Automatically Continue"), () => {
|
|
breakpoint.autoContinue = !breakpoint.autoContinue;
|
|
});
|
|
}
|
|
|
|
if (breakpoint.removable) {
|
|
contextMenu.appendItem(WI.UIString("Delete Breakpoint"), () => {
|
|
breakpoint.remove();
|
|
});
|
|
} else {
|
|
contextMenu.appendItem(WI.UIString("Reset Breakpoint", "Reset Breakpoint @ Breakpoint Context Menu", "Context menu action for resetting the breakpoint to its initial configuration."), () => {
|
|
breakpoint.reset();
|
|
});
|
|
}
|
|
|
|
if (breakpoint instanceof WI.JavaScriptBreakpoint && breakpoint.sourceCodeLocation.hasMappedLocation()) {
|
|
contextMenu.appendSeparator();
|
|
contextMenu.appendItem(WI.UIString("Reveal in Original Resource"), () => {
|
|
const options = {
|
|
ignoreNetworkTab: true,
|
|
ignoreSearchTab: true,
|
|
};
|
|
WI.showOriginalOrFormattedSourceCodeLocation(breakpoint.sourceCodeLocation, options);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Public
|
|
|
|
get breakpoint() { return this._breakpoint; }
|
|
|
|
show(targetElement)
|
|
{
|
|
this._targetElement = targetElement;
|
|
|
|
this._contentElement = document.createElement("div");
|
|
this._contentElement.classList.add("edit-breakpoint-popover-content");
|
|
|
|
if (this._breakpoint) {
|
|
let toggleLabelElement = this._contentElement.appendChild(document.createElement("label"));
|
|
toggleLabelElement.className = "toggle";
|
|
|
|
let toggleCheckboxElement = toggleLabelElement.appendChild(document.createElement("input"));
|
|
toggleCheckboxElement.type = "checkbox";
|
|
toggleCheckboxElement.checked = !this._breakpoint.disabled;
|
|
toggleCheckboxElement.addEventListener("change", this._handleEnabledCheckboxChange.bind(this));
|
|
|
|
toggleLabelElement.appendChild(document.createTextNode(this._breakpoint.displayName));
|
|
}
|
|
|
|
this._tableElement = this._contentElement.appendChild(document.createElement("table"));
|
|
|
|
if (!this._breakpoint)
|
|
this.populateContent();
|
|
|
|
if (this._breakpoint?.editable || this.constructor.supportsEditing) {
|
|
let conditionLabelElement = document.createElement("label");
|
|
conditionLabelElement.textContent = WI.UIString("Condition");
|
|
|
|
let conditionEditorElement = document.createElement("div");
|
|
conditionEditorElement.classList.add("editor", WI.SyntaxHighlightedStyleClassName);
|
|
|
|
this._conditionCodeMirror = WI.CodeMirrorEditor.create(conditionEditorElement, {
|
|
extraKeys: {Tab: false},
|
|
lineWrapping: false,
|
|
mode: "text/javascript",
|
|
matchBrackets: true,
|
|
placeholder: WI.UIString("Conditional expression"),
|
|
scrollbarStyle: null,
|
|
value: this._breakpoint?.condition || "",
|
|
});
|
|
|
|
let conditionCodeMirrorInputField = this._conditionCodeMirror.getInputField();
|
|
conditionCodeMirrorInputField.id = "edit-breakpoint-popover-content-condition";
|
|
conditionLabelElement.setAttribute("for", conditionCodeMirrorInputField.id);
|
|
|
|
this.addRow("condition", conditionLabelElement, conditionEditorElement);
|
|
|
|
this._conditionCodeMirror.addKeyMap({
|
|
"Enter": () => { this.dismiss(); },
|
|
"Esc": () => { this.dismiss(); },
|
|
});
|
|
|
|
this._conditionCodeMirror.on("beforeChange", this._handleConditionCodeMirrorBeforeChange.bind(this));
|
|
if (this._breakpoint)
|
|
this._conditionCodeMirror.on("change", this._handleConditionCodeMirrorChange.bind(this));
|
|
|
|
let completionController = new WI.CodeMirrorCompletionController(this.codeMirrorCompletionControllerMode, this._conditionCodeMirror, this);
|
|
completionController.addExtendedCompletionProvider("javascript", WI.javaScriptRuntimeCompletionProvider);
|
|
|
|
let ignoreCountLabelElement = document.createElement("label");
|
|
ignoreCountLabelElement.textContent = WI.UIString("Ignore");
|
|
|
|
let ignoreCountContentFragment = document.createDocumentFragment();
|
|
|
|
this._ignoreCountInputElement = ignoreCountContentFragment.appendChild(document.createElement("input"));
|
|
this._ignoreCountInputElement.id = "edit-breakpoint-popover-ignore-count";
|
|
this._ignoreCountInputElement.type = "number";
|
|
this._ignoreCountInputElement.min = 0;
|
|
this._ignoreCountInputElement.value = this._breakpoint?.ignoreCount || 0;
|
|
this._ignoreCountInputElement.addEventListener("change", this._handleIgnoreCountInputChange.bind(this));
|
|
|
|
this._ignoreCountText = ignoreCountContentFragment.appendChild(document.createElement("label"));
|
|
this._updateIgnoreCountText();
|
|
|
|
ignoreCountLabelElement.setAttribute("for", this._ignoreCountInputElement.id);
|
|
this._ignoreCountText.setAttribute("for", this._ignoreCountInputElement.id);
|
|
|
|
this.addRow("ignore-count", ignoreCountLabelElement, ignoreCountContentFragment);
|
|
|
|
let actionsLabelElement = document.createElement("label");
|
|
actionsLabelElement.textContent = WI.UIString("Action");
|
|
|
|
this._actionsContainerElement = document.createElement("div");
|
|
|
|
if (!this._breakpoint?.actions.length)
|
|
this._createAddActionButton();
|
|
else {
|
|
this._contentElement.classList.add("wide");
|
|
|
|
for (let i = 0; i < this._breakpoint.actions.length; ++i) {
|
|
let breakpointActionView = new WI.BreakpointActionView(this._breakpoint.actions[i], this, {omitFocus: true});
|
|
this._insertBreakpointActionView(breakpointActionView);
|
|
}
|
|
}
|
|
|
|
this.addRow("actions", actionsLabelElement, this._actionsContainerElement);
|
|
|
|
let optionsLabelElement = document.createElement("label");
|
|
optionsLabelElement.textContent = WI.UIString("Options");
|
|
|
|
let optionsDocumentFragment = document.createDocumentFragment();
|
|
|
|
this._autoContinueCheckboxElement = optionsDocumentFragment.appendChild(document.createElement("input"));
|
|
this._autoContinueCheckboxElement.id = "edit-breakpoint-popover-auto-continue";
|
|
this._autoContinueCheckboxElement.type = "checkbox";
|
|
this._autoContinueCheckboxElement.checked = this._breakpoint?.autoContinue || false;
|
|
this._autoContinueCheckboxElement.addEventListener("change", this._handleAutoContinueCheckboxChange.bind(this));
|
|
|
|
let optionsCheckboxLabel = optionsDocumentFragment.appendChild(document.createElement("label"));
|
|
optionsCheckboxLabel.textContent = WI.UIString("Automatically continue after evaluating");
|
|
|
|
optionsLabelElement.setAttribute("for", this._autoContinueCheckboxElement.id);
|
|
optionsCheckboxLabel.setAttribute("for", this._autoContinueCheckboxElement.id);
|
|
|
|
this._optionsRowElement = this.addRow("options", optionsLabelElement, optionsDocumentFragment);
|
|
if (!this._breakpoint?.actions.length)
|
|
this._optionsRowElement.classList.add("hidden");
|
|
|
|
// CodeMirror needs to refresh after the popover is shown as otherwise it doesn't appear.
|
|
setTimeout(() => {
|
|
this._conditionCodeMirror.refresh();
|
|
if (this._breakpoint)
|
|
this._conditionCodeMirror.focus();
|
|
|
|
this.update();
|
|
});
|
|
}
|
|
|
|
this._contentElement.appendChild(WI.createReferencePageLink(this._breakpoint?.constructor.ReferencePage || this.constructor.ReferencePage, this._breakpoint ? "configuration" : ""));
|
|
|
|
this.content = this._contentElement;
|
|
|
|
this._presentOverTargetElement();
|
|
}
|
|
|
|
dismiss()
|
|
{
|
|
this._breakpoint ??= this.createBreakpoint({
|
|
condition: this._conditionCodeMirror ? this._conditionCodeMirror.getValue().trim() : "",
|
|
actions: this._actionViews.map((breakpointActionView) => breakpointActionView.action),
|
|
ignoreCount: this._ignoreCountInputElement ? this._parseIgnoreCountNumber() : 0,
|
|
autoContinue: this._autoContinueCheckboxElement ? this._autoContinueCheckboxElement.checked : false,
|
|
});
|
|
|
|
// Remove Evaluate and Probe actions that have no data.
|
|
let emptyActions = this._breakpoint?.actions.filter(function(action) {
|
|
if (action.type === WI.BreakpointAction.Type.Sound)
|
|
return false;
|
|
return !action.data?.trim();
|
|
}) || [];
|
|
for (let action of emptyActions)
|
|
this._breakpoint.removeAction(action);
|
|
|
|
super.dismiss();
|
|
}
|
|
|
|
// CodeMirrorCompletionController delegate
|
|
|
|
completionControllerShouldAllowEscapeCompletion()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// BreakpointActionView delegate
|
|
|
|
breakpointActionViewCodeMirrorCompletionControllerMode(breakpointActionView, codeMirror)
|
|
{
|
|
return this.codeMirrorCompletionControllerMode;
|
|
}
|
|
|
|
breakpointActionViewAppendActionView(breakpointActionView, newBreakpointAction)
|
|
{
|
|
this._breakpoint?.addAction(newBreakpointAction, {precedingAction: breakpointActionView.action});
|
|
|
|
let newBreakpointActionView = new WI.BreakpointActionView(newBreakpointAction, this);
|
|
|
|
let index = this._actionViews.indexOf(breakpointActionView) + 1;
|
|
this._insertBreakpointActionView(newBreakpointActionView, index);
|
|
this._optionsRowElement.classList.remove("hidden");
|
|
|
|
this.update();
|
|
}
|
|
|
|
breakpointActionViewRemoveActionView(breakpointActionView)
|
|
{
|
|
this._breakpoint?.removeAction(breakpointActionView.action);
|
|
|
|
breakpointActionView.element.remove();
|
|
this._actionViews.remove(breakpointActionView);
|
|
|
|
if (!this._actionViews.length) {
|
|
this._createAddActionButton();
|
|
this._optionsRowElement.classList.add("hidden");
|
|
this._autoContinueCheckboxElement.checked = false;
|
|
}
|
|
|
|
this.update();
|
|
}
|
|
|
|
breakpointActionViewResized(breakpointActionView)
|
|
{
|
|
this.update();
|
|
}
|
|
|
|
// Protected
|
|
|
|
get codeMirrorCompletionControllerMode()
|
|
{
|
|
// Overridden by subclasses if needed.
|
|
|
|
if (this._breakpoint === WI.debuggerManager.allExceptionsBreakpoint || this._breakpoint === WI.debuggerManager.uncaughtExceptionsBreakpoint)
|
|
return WI.CodeMirrorCompletionController.Mode.ExceptionBreakpoint;
|
|
|
|
return WI.CodeMirrorCompletionController.Mode.Basic;
|
|
}
|
|
|
|
populateContent()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
addRow(className, label, content)
|
|
{
|
|
let rowElement = this._tableElement.appendChild(document.createElement("tr"));
|
|
rowElement.className = className;
|
|
|
|
let headerElement = rowElement.appendChild(document.createElement("th"));
|
|
headerElement.append(label);
|
|
|
|
let dataElement = rowElement.appendChild(document.createElement("td"));
|
|
dataElement.append(content);
|
|
|
|
return rowElement;
|
|
}
|
|
|
|
createBreakpoint(options = {})
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
// Private
|
|
|
|
_presentOverTargetElement()
|
|
{
|
|
if (!this._targetElement)
|
|
return;
|
|
|
|
let targetFrame = WI.Rect.rectFromClientRect(this._targetElement.getBoundingClientRect());
|
|
this.present(targetFrame.pad(2), [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]);
|
|
}
|
|
|
|
_parseIgnoreCountNumber()
|
|
{
|
|
let ignoreCount = 0;
|
|
if (this._ignoreCountInputElement.value) {
|
|
ignoreCount = parseInt(this._ignoreCountInputElement.value, 10);
|
|
if (isNaN(ignoreCount) || ignoreCount < 0)
|
|
ignoreCount = 0;
|
|
}
|
|
return ignoreCount;
|
|
}
|
|
|
|
_updateIgnoreCountText()
|
|
{
|
|
if (this._parseIgnoreCountNumber() === 1)
|
|
this._ignoreCountText.textContent = WI.UIString("time before stopping");
|
|
else
|
|
this._ignoreCountText.textContent = WI.UIString("times before stopping");
|
|
}
|
|
|
|
_createAddActionButton()
|
|
{
|
|
this._contentElement.classList.remove("wide");
|
|
this._actionsContainerElement.removeChildren();
|
|
|
|
let addActionButton = this._actionsContainerElement.appendChild(document.createElement("button"));
|
|
addActionButton.textContent = WI.UIString("Add Action");
|
|
addActionButton.addEventListener("click", this._handleAddActionButtonClick.bind(this));
|
|
}
|
|
|
|
_insertBreakpointActionView(breakpointActionView, index = this._actionViews.length)
|
|
{
|
|
if (index >= this._actionViews.length) {
|
|
this._actionsContainerElement.appendChild(breakpointActionView.element);
|
|
this._actionViews.push(breakpointActionView);
|
|
} else {
|
|
this._actionsContainerElement.insertBefore(breakpointActionView.element, this._actionViews[index].element);
|
|
this._actionViews.splice(index, 0, breakpointActionView)
|
|
}
|
|
}
|
|
|
|
_handleEnabledCheckboxChange(event)
|
|
{
|
|
this._breakpoint.disabled = !event.target.checked;
|
|
}
|
|
|
|
_handleConditionCodeMirrorBeforeChange(codeMirror, change)
|
|
{
|
|
if (change.update) {
|
|
let newText = change.text.join("").replace(/\n/g, "");
|
|
change.update(change.from, change.to, [newText]);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
_handleConditionCodeMirrorChange(codeMirror, change)
|
|
{
|
|
this._breakpoint.condition = this._conditionCodeMirror.getValue().trim();
|
|
}
|
|
|
|
_handleIgnoreCountInputChange(event)
|
|
{
|
|
let ignoreCount = this._parseIgnoreCountNumber();
|
|
this._ignoreCountInputElement.value = ignoreCount;
|
|
|
|
if (this._breakpoint)
|
|
this._breakpoint.ignoreCount = ignoreCount;
|
|
|
|
this._updateIgnoreCountText();
|
|
}
|
|
|
|
_handleAddActionButtonClick(event)
|
|
{
|
|
this._contentElement.classList.add("wide");
|
|
|
|
this._actionsContainerElement.removeChildren();
|
|
|
|
let action = new WI.BreakpointAction(WI.BreakpointAction.Type.Evaluate);
|
|
this._breakpoint?.addAction(action);
|
|
|
|
let breakpointActionView = new WI.BreakpointActionView(action, this);
|
|
this._insertBreakpointActionView(breakpointActionView);
|
|
|
|
this._optionsRowElement.classList.remove("hidden");
|
|
|
|
this.update();
|
|
}
|
|
|
|
_handleAutoContinueCheckboxChange(event)
|
|
{
|
|
if (this._breakpoint)
|
|
this._breakpoint.autoContinue = this._autoContinueCheckboxElement.checked;
|
|
}
|
|
};
|