function BreakpointManager($main_elt, rest_interface) {
var breakpoints = {}, // keyed by filename, then by line number
actions = {},
$breakpointsList = $main_elt.find('#breakpoints-list'),
breakpointPaneItemTemplate = Handlebars.compile( $('#breakpoint-pane-item-template').html() ),
breakpointConditionTemplate = Handlebars.compile( $('#breakpoint-code-template').html() ),
breakpointRightClickTemplate = Handlebars.compile( $('#breakpoint-right-click-template').html() );
function getBPAction(type, filename, line) {
var storage = type === 'breakpoint' ? breakpoints : actions;
if (storage[filename]) {
return storage[filename][line];
} else {
return undefined;
}
};
this.getBreakpoint = function(filename, line) {
return getBPAction('breakpoint', filename, line);
};
this.getAction = function(filename, line) {
return getBPAction('action', filename, line);
};
function resolveForFilenameAndLine(type, filename, line) {
var storage = type === 'breakpoint' ? breakpoints : actions,
bp;
if (! filename in storage) {
storage[filename] = {};
}
bp = storage[filename][line];
if (!bp) {
bp = new Breakpoint({filename: filename, line: line});
storage[filename][line] = bp;
}
return bp;
}
function notifyBreakpointChange(signal, filename, arg) {
$('.breakpoint-listener[data-filename="'+filename+'"]')
.trigger(signal, arg);
}
function removeBPAction (type, filename, line) {
var storage = type === 'breakpoint' ? breakpoints : actions,
deleteMethod = rest_interface[ type == 'breakpoint' ? 'deleteBreakpoint' : 'deleteAction'],
bp;
if ((! storage[filename]) || (! (bp = storage[filename][line]))) {
return;
}
delete storage[filename][line];
deleteMethod.call(rest_interface, bp.href)
.done(function() {
updateForDeletedBPAction(type, bp);
})
.fail(function(jqxhr, text_status, error_thrown) {
alert('Removing '+type+' failed: '+error_thrown);
});
};
function filenameForCodePane($codePane) {
$codePane.find('[data-filename]').attr('data-filename');
}
function matchingLineElementsForBreakpoint(bp, $codePane) {
if ($codePane) {
return $codePane.find('.code-line[data-lineno="'+bp.line+'"]');
} else {
return $('.breakpoint-listener[data-filename="'+bp.filename+'"] .code-line[data-lineno="'+bp.line+'"]');
}
}
function updateBreakpointPaneForDeletedBPAction(type, bp) {
var $elt = $breakpointsList.find('[data-filename="'+bp.filename+'"][data-lineno="'+bp.line+'"]'),
otherType = type == 'breakpoint' ? 'action' : 'breakpoint',
otherTypeClass = otherType + '-details';
otherTypeIsNone = $elt.find('.'+otherTypeClass+' .none').length > 0;
if (otherTypeIsNone) {
// Both the breakpoint and action are removed - remove the element
$elt.remove();
} else {
// just blank out the breakpoint or action
var $subElt = $elt.find('.'+type+'-details .bpaction-code').html(breakpointConditionTemplate({}));
}
}
function updateForDeletedBPAction(type, bp) {
var all_classes = type + ' conditional-' + type + ' inactive-'+type;
// clear code pane line elements
matchingLineElementsForBreakpoint(bp)
.removeClass(all_classes);
// remove item from the breakpoint pane
updateBreakpointPaneForDeletedBPAction(type, bp);
};
function updateBreakpointPaneElementForChangedBPAction(type, bp) {
var filename = bp.filename,
line = bp.line,
eltClass = type + '-details',
matchFileAndLine = '.breakpoint-marker[data-filename="'+filename+'"][data-lineno="'+line+'"]',
$existingElts = $(matchFileAndLine + ' .'+eltClass),
$eltsInBreakpointList = $breakpointsList.find(matchFileAndLine);
// Update already-existing on-screen items
// Either in the breakpoints list or the right-click menu
$existingElts.find('input[type="checkbox"]').prop('checked', bp.inactive ? false : true);
$existingElts.find('.bpaction-code').html(breakpointConditionTemplate({condition: bp.code}));
if ($eltsInBreakpointList.length == 0) {
// Add a new list item to the bottom of the list
var params = type == 'breakpoint'
? {condition: bp.code, conditionEnabled: ! bp.inactive}
: {action: bp.code, actionEnabled: ! bp.inactive};
params.filename = bp.filename;
params.lineno = bp.line;
$breakpointsList.append( breakpointPaneItemTemplate(params));
}
}
function markCodePaneLineNumbersForBPActions(type, bplist, $codePane) {
var all_classes = type + ' conditional-' + type + ' inactive-'+type;
bplist.forEach(function(bp) {
var new_classes = type;
if (bp.code != '1') {
new_classes += ' conditional-'+type;
}
if (bp.inactive) {
new_classes += ' inactive-'+type;
}
// Update code pane lines
matchingLineElementsForBreakpoint(bp, $codePane)
.removeClass(all_classes)
.addClass(new_classes);
});
}
function updateForChangedBPAction(type, bp) {
markCodePaneLineNumbersForBPActions(type, [bp]);
// Update breakpoint pane
updateBreakpointPaneElementForChangedBPAction(type, bp);
};
this.markCodePaneLineNumbersForBreakpointsAndActions = function($codePane) {
var filename = $codePane.attr('data-filename'),
bpactions = { breakpoint: breakpoints[filename], action: actions[filename] };
['breakpoint','action'].forEach(function(type) {
var bpaction_list = [];
for (var line in bpactions[type]) {
bpaction_list.push(bpactions[type][line]);
}
markCodePaneLineNumbersForBPActions(type, bpaction_list, $codePane);
});
};
function storeCreatedBPAction(type, bp) {
var storage = type === 'breakpoint' ? breakpoints : actions,
filename = bp.filename;
if (! storage[filename]) {
storage[filename] = {};
}
storage[filename][bp.line] = bp;
}
function createBPAction(type, params) {
var create_method = rest_interface[ type === 'breakpoint' ? 'createBreakpoint' : 'createAction' ];
create_method.call(rest_interface, params)
.done(function(data) {
var bp = new Breakpoint(data);
storeCreatedBPAction(type, bp);
updateForChangedBPAction(type, bp);
})
.fail(function(jqxhr, text_status, error_thrown) {
alert('Setting breakpoint failed: '+error_thrown);
});
};
this.createBreakpoint = function(params) {
if (('filename' in params) && ('line' in params)) {
if (this.getBreakpoint(params.filename, params.line)) {
return false;
}
if (! ('code' in params)) {
params['code'] = '1';
}
createBPAction('breakpoint', params);
return true;
} else {
return false;
}
};
function breakableLineClicked(e) {
var $elt = $(e.target),
filename = $elt.closest('.program-code').attr('data-filename'),
line = $elt.closest('.code-line').attr('data-lineno');
if (this.getBreakpoint(filename, line)) {
removeBPAction('breakpoint',filename, line);
} else {
createBPAction('breakpoint', {filename: filename, line: line, code: '1'})
}
}
// synchronize our list of breakpoints/actions with the debugged program
this.sync = function() {
var original_bpactions = { breakpoint: breakpoints, action: actions },
new_bpactions = { breakpoint: {}, action: {} };
function previously_existed(stored, filename, line) {
return stored[filename] && stored[filename][line];
}
function copy_to_permanent(perm, temp, filename, line) {
var bp = temp[filename][line];
delete temp[filename][line];
if (! perm[filename]) {
perm[filename] = {};
}
perm[filename][line] = bp;
return bp;
}
['breakpoint','action'].forEach(function(bpType) {
var getter_method = rest_interface[ bpType === 'breakpoint' ? 'getBreakpoints' : 'getActions' ];
getter_method.call(rest_interface)
.fail(function(jqxhr, text_status, error_thrown) {
alert('Getting ' + bpType +' failed: ' + error_thrown);
})
.done(function(bpaction_list) {
bpaction_list.forEach(function(bp_params) {
if (previously_existed(original_bpactions[bpType], bp_params.filename, bp_params.line)) {
var bp = copy_to_permanent(new_bpactions[bpType], original_bpactions[bpType], bp_params.filename, bp_params.line),
changed = (bp.code != bp_params.code) || (bp.inactive != bp_params.inactive);
if (changed) {
updateForChangedBPAction(bpType, bp);
}
} else {
var bp = new Breakpoint(bp_params);
storeCreatedBPAction(bpType, bp);
updateForChangedBPAction(bpType, bp);
}
});
});
});
// These are the object's main list of breakpoints/actions
breakpoints = new_bpactions.breakpoint;
actions = new_bpactions.action;
};
// Remove the breakpoint popover if the user clicks outside of it
var breakpointPopover;
function clearBreakpointEditorPopover(e) {
if (breakpointPopover && ($(e.target).closest('.popover').length == 0)) {
e.preventDefault();
e.stopPropagation();
breakpointPopover.popover('destroy');
breakpointPopover = undefined;
}
}
function breakableLineRightClicked(e) {
e.preventDefault();
var $target_lineno = $(e.target),
filename = $target_lineno.closest('[data-filename]').attr('data-filename'),
line = $target_lineno.closest('[data-lineno]').attr('data-lineno'),
breakpoint = this.getBreakpoint(filename, line),
action = this.getAction(filename, line),
menu;
menu = breakpointRightClickTemplate({ filename: filename, lineno: line,
conditionEnabled: (breakpoint && ! breakpoint.inactive),
condition: (breakpoint && breakpoint.code),
actionEnabled: (action && ! action.inactive),
action: ( action && action.code)
});
if (breakpointPopover) {
breakpointPopover.popover('destroy');
}
breakpointPopover = $target_lineno.popover({ html: true,
trigger: 'manual',
placement: 'right',
title: filename + ' ' + line,
container: $main_elt,
content: menu })
.popover('show');
}
function inactiveBreakpointCheckboxClicked(e) {
var $checkbox = $(e.target),
state = $checkbox.is(':checked'),
$listItem = $checkbox.closest('.breakpoint-marker'),
filename = $listItem.attr('data-filename'),
line = $listItem.attr('data-lineno'),
isBreakpoint = $checkbox.closest('.bpaction-details').hasClass('breakpoint-details'),
bpType = isBreakpoint ? 'breakpoint' : 'action',
bp = isBreakpoint ? this.getBreakpoint(filename, line) : this.getAction(filename, line),
changeMethod = rest_interface[ isBreakpoint ? 'changeBreakpoint' : 'changeAction' ].bind(rest_interface);
if (bp === undefined) {
// When we get here, the checkbox is already checked?!
// we don't get a chance to preventDefault
$checkbox.attr('checked', false);
return;
}
changeMethod(bp.href, { inactive: state ? 0 : 1 })
.fail(function(jqxhr, text_status, error_thrown) {
alert('Changing ' + bpType + ' failed: '+ error_thrown);
})
.done(function() {
bp.inactive = $checkbox.is(':checked') ? 0 : 1;
updateForChangedBPAction(bpType, bp);
});
}
function submitChangedBreakpointCondition(e) {
e.preventDefault();
var $form = $(e.target),
$input = $form.find('input'),
$container = $form.closest('.bpaction-code'),
bp = e.data.bp,
isBreakpoint = e.data.isBreakpoint,
bpType = isBreakpoint ? 'breakpoint' : 'action',
isNewBp = bp === undefined ? true : false,
newCondition = $input.val();
if (isNewBp) {
createBPAction(bpType, {filename: e.data.filename, line: e.data.line, code: newCondition});
} else {
var changeMethod = isBreakpoint
? rest_interface.changeBreakpoint.bind(rest_interface)
: rest_interface.changeAction.bind(rest_interface);
changeMethod(bp.href, { code: newCondition })
.fail(function(jqxhr, text_status, error_thrown) {
$container.empty().append( breakpointConditionTemplate({ condition: bp.code }));
alert('Changing breakpoint failed: ' + error_thrown);
})
.done(function() {
bp.code = newCondition;
updateForChangedBPAction(bpType, bp);
});
}
}
function editBreakpointCondition(e) {
var $elt = $(e.target),
isBreakpoint = $elt.closest('.bpaction-details').hasClass('breakpoint-details'),
$listItem = $elt.closest('.breakpoint-marker'),
filename = $listItem.attr('data-filename'),
line = $listItem.attr('data-lineno'),
bp = isBreakpoint ? this.getBreakpoint(filename, line) : this.getAction(filename, line),
bpCode = bp ? bp.code : '',
$form = $('<form><input type="text" value="' + bpCode + '"></form>'),
submitData = { bp: bp, isBreakpoint: isBreakpoint, filename: filename, line: line };
$form.on('submit', submitData, submitChangedBreakpointCondition.bind(this))
.find('input')
.keyup(function(e) {
if (e.keyCode == 27) { // escape - abort editing
e.preventDefault();
$elt.empty().append( breakpointConditionTemplate({ condition: bp ? bp.code : undefined }));
}
});
$elt.empty().append($form);
$form.find('input').focus().select();
}
function removeFromBreakpointList(e) {
var $target = $(e.target),
$bpListItem = $target.closest('.breakpoint-pane-item'),
filename = $bpListItem.attr('data-filename'),
line = $bpListItem.attr('data-lineno');
removeBPAction('breakpoint',filename, line);
removeBPAction('action',filename, line);
}
$main_elt.on('click', '.code-line:not(.unbreakable) .lineno', breakableLineClicked.bind(this))
.on('contextmenu', '.code-line:not(.unbreakable) .lineno', breakableLineRightClicked.bind(this))
.on('click', clearBreakpointEditorPopover.bind(this))
.on('click', 'input[type="checkbox"].bpaction-toggle', inactiveBreakpointCheckboxClicked.bind(this))
.on('dblclick', '.bpaction-code', editBreakpointCondition.bind(this))
.on('click', '.remove-breakpoint', removeFromBreakpointList.bind(this));
}
function Breakpoint(params) {
var bp = this;
['filename','line','code','inactive','href'].forEach(function(key) {
bp[key] = params[key];
});
return this;
}