c9-core/plugins/c9.ide.run.debug/liveinspect.js

415 wiersze
14 KiB
JavaScript

define(function(require, exports, module) {
main.consumes = [
"Plugin", "ui", "debugger", "immediate.debugnode", "language",
"tabManager", "callstack", "ace"
];
main.provides = ["liveinspect"];
return main;
function main(options, imports, register) {
var Plugin = imports.Plugin;
var ui = imports.ui;
var ace = imports.ace;
var language = imports.language;
var debug = imports.debugger;
var tabManager = imports.tabManager;
var callstack = imports.callstack;
var evaluator = imports["immediate.debugnode"];
// postfix plugin because debugger is restricted keyword
var Range = require("ace/range").Range;
/***** Initialization *****/
var plugin = new Plugin("Ajax.org", main.consumes);
var emit = plugin.getEmitter();
var activeTimeout = null;
var windowHtml = null;
var currentExpression = null;
var currentTab = null;
var marker = null;
var evalCounter = 0;
var SHOW_TIMEOUT = 350;
var isContextMenuVisible = false;
var dbg, worker, theme;
var loaded = false;
function load() {
if (loaded) return false;
loaded = true;
// Set and clear the dbg variable
debug.on("attach", function(e) {
dbg = e.implementation;
});
debug.on("detach", function(e) {
dbg = null;
});
debug.on("stateChange", function(e) {
plugin[e.action]();
});
// Hook into the language worker
language.once("initWorker", function(e) {
worker = e.worker;
// listen to the worker's response
worker.on("inspect", function(event) {
if (!event || !event.data) {
return hide();
}
// create an expression that the debugger understands
if (event.data.value) {
liveWatch(event.data);
}
});
}, plugin);
// bind mouse events to all open editors
ace.on("create", function(e) {
var editor = e.editor;
var ace = editor.ace;
ace.on("mousemove", function(e) {
onEditorMouseMove(e, editor.pane);
});
ace.on("mousedown", onEditorClick);
ace.on("changeSelection", onEditorClick);
ace.on("mousewheel", onEditorClick);
}, plugin);
ace.on("themeChange", function(e) {
theme = e.theme;
if (!theme || !drawn) return;
windowHtml.className = "liveinspect immediate "
+ (theme.isDark ? "dark" : "");
windowHtml.firstChild.className = (theme.isDark ? "ace_dark" : "");
}, plugin);
}
var drawn = false;
function draw() {
if (drawn) return;
drawn = true;
// Create UI elements
windowHtml = document.body.appendChild(document.createElement("div"));
// get respective HTML elements
windowHtml.style.position = "absolute";
windowHtml.className = "liveinspect immediate "
+ (!theme || theme.isDark ? "dark" : "");
windowHtml.innerHTML = "<div class='"
+ (!theme || theme.isDark ? "ace_dark" : "")
+ "'></div>";
ace.getElement("menu", function(menu) {
menu.on("prop.visible", function(e) {
isContextMenuVisible = e.value;
});
});
// when hovering over the inspector window we should ignore all further listeners
windowHtml.addEventListener("mousemove", function() {
if (activeTimeout) {
clearTimeout(activeTimeout);
activeTimeout = null;
}
});
// we should track mouse movement over the whole window
apf.addListener(document, "mousemove", onDocumentMouseMove);
emit("draw");
}
/***** Methods *****/
/**
* Determine whether the current file is the current frame where the
* debugger is in.
*/
function isCurrentFrame(pane) {
var frame = callstack.activeFrame;
var tab = frame && (tab = tabManager.findTab(frame.path))
&& pane.activeTab == tab;
if (!tab)
return false;
// @todo check if we are still in the current function
// var line = frame.getAttribute("line");
// var column = frame.getAttribute("column");
return true;
}
/**
* onMouseMove handler that is being used to show / hide the inline quick watch
*/
function onEditorMouseMove (ev, pane) {
if (activeTimeout) {
clearTimeout(activeTimeout);
activeTimeout = null;
}
if (!dbg || dbg.state != 'stopped' || isContextMenuVisible)
return;
activeTimeout = setTimeout(function () {
activeTimeout = null;
if (!isCurrentFrame(pane))
return hide();
var pos = ev.getDocumentPosition();
var line = ev.editor.session.getLine(pos.row);
if (pos.column == line.length || pos.column < line.search(/\S/))
return hide();
if (!ev.editor.selection.isEmpty()) {
var range = ev.editor.getSelectionRange();
var selectionVisible = range.end.row > ev.editor.getFirstVisibleRow()
&& range.start.row < ev.editor.getLastVisibleRow();
if (!selectionVisible)
return hide();
}
worker.emit("inspect", { data: { row: pos.row, column: pos.column }});
// hide it, and set left / top so it gets positioned right when showing again
if (!marker || !marker.range.contains(pos.row, pos.column))
hide();
draw();
}, SHOW_TIMEOUT);
}
/**
* onDocumentMove handler to clear the timeout
*/
function onDocumentMouseMove (ev) {
if (!activeTimeout || !currentTab)
return;
// see whether we hover over the editor or the quickwatch window
var mouseMoveAllowed = false;
var eles = [ currentTab.editor.ace.renderer.scroller, windowHtml ];
// only the visible ones
eles.filter(function (ele) { return ele.offsetWidth || ele.offsetHeight; })
.forEach(function (ele) {
// then detect real position
var pos = ele.getBoundingClientRect();
var left = pos.left;
var top = pos.top;
// x boundaries
if (ev.pageX >= left && ev.pageX <= (left + ele.offsetWidth)) {
// y boundaries
if (ev.pageY >= top && ev.pageY <= (top + ele.offsetHeight)) {
// we are in the editor, so return; this will be handled
mouseMoveAllowed = true;
}
}
});
if (mouseMoveAllowed) return;
clearTimeout(activeTimeout);
activeTimeout = windowHtml.style.display == "block"
? setTimeout(hide, 400) : null;
}
/**
* When clicking in the editor window, hide live inspect
*/
function onEditorClick() {
hide();
}
/**
* Execute live watching
*/
function liveWatch(data) {
if (!dbg) return;
var expr = data.value;
// already visible, and same expression?
if (windowHtml && windowHtml.style.display == "block"
&& expr === currentExpression)
return;
// if there is any modal window open, then don't show
var windows = getNumericProperties(document.querySelectorAll(".winadv") || {})
.filter(function (w) {
return w.style.display !== "none" && w.style.visibility !== "hidden";
});
if (windows.length)
return;
// Filter functions, literals
if (data.value.match(/^function(?: |\()|^[\d\.\-\^]+$|^\".*\"$|^\[.*\]$|^\{.*\}$|^\/.*\/$/))
return;
// if context menu open, then also disable
// if (mnuCtxEditor && mnuCtxEditor.visible) {
// return;
// }
// show spinner while evaluating
windowHtml.firstChild.innerHTML = '<div class="session_btn running" style="margin:0">'
+ '<strong style="left:18px"></strong></div>';
done();
if (!marker)
return;
dbg.on("break", updateOnBreak, plugin);
marker.data = data;
var evalId = ++evalCounter;
// evaluate the expression in the debugger, and receive model as callback
evaluator.evaluate(expr, {
addWidget: function(state) {
windowHtml.firstChild.innerHTML = "";
windowHtml.firstChild.appendChild(state.el);
},
session: { repl: { onWidgetChanged: function() {
} }},
setError: function(err) {
windowHtml.firstChild.innerHTML = '<span class="error"><span>';
windowHtml.querySelector(".error").textContent =
err && err.message || "error";
},
setWaiting: function(show) {
if (!show)
done();
}
}, function() {
if (marker && evalId === evalCounter)
done();
});
function done() {
// store it
currentExpression = expr;
var tab = tabManager.findTab(data.path);
if (!tab || !tab.isActive())
return hide();
currentTab = tab;
addMarker(data);
var pos = data.pos;
var ace = tab.document.editor.ace;
var coords = ace.renderer.textToScreenCoordinates(pos.sl, pos.sc);
windowHtml.style.maxWidth = Math.min(800, window.innerWidth
- coords.pageX - 30) + "px";
windowHtml.style.maxHeight = Math.min(250, window.innerHeight
- coords.pageY - ace.renderer.lineHeight - 10) + "px";
windowHtml.style.left = coords.pageX + "px";
windowHtml.style.top = (coords.pageY + ace.renderer.lineHeight) + "px";
// show window
windowHtml.style.display = "block";
}
}
function hide() {
if (windowHtml)
windowHtml.style.display = "none";
if (marker) {
marker.session.removeMarker(marker.id);
marker = null;
}
if (activeTimeout)
activeTimeout = clearTimeout(activeTimeout);
if (dbg)
dbg.off("break", updateOnBreak);
}
function addMarker(data) {
if (marker)
marker.session.removeMarker(marker.id);
var tab = tabManager.findTab(data.path);
var doc = tab && tab.document;
if (!doc)
return;
var pos = data.pos;
var session = doc.getSession().session;
if (pos.el != pos.sl && data.value.indexOf("\n") == -1) {
pos.el = pos.sl;
pos.ec = session.getLine(pos.sl).length;
}
var range = new Range(pos.sl, pos.sc, pos.el, pos.ec);
marker = {
id: session.addMarker(range, "ace_bracket ace_highlight", "text", true),
session: session,
range: range,
data: data
};
}
function getNumericProperties(obj) {
return Object.keys(obj)
.filter(function (k) { return !isNaN(k); })
.map(function (k) { return obj[k]; });
}
function updateOnBreak() {
currentExpression = null;
if (marker && dbg) {
dbg.off("break", updateOnBreak);
if (marker.data)
liveWatch(marker.data);
}
}
/***** Lifecycle *****/
plugin.on("load", function() {
load();
});
plugin.on("enable", function() {
});
plugin.on("disable", function() {
hide();
});
plugin.on("unload", function() {
loaded = false;
drawn = false;
});
/***** Register and define API *****/
/**
*
**/
plugin.freezePublicAPI({
});
register(null, {
liveinspect: plugin
});
}
});