|
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 "use strict"; |
|
7 |
|
8 // A time interval sufficient for the options popup panel to finish hiding |
|
9 // itself. |
|
10 const POPUP_HIDDEN_DELAY = 100; // ms |
|
11 |
|
12 /** |
|
13 * Functions handling the toolbar view: close button, expand/collapse button, |
|
14 * pause/resume and stepping buttons etc. |
|
15 */ |
|
16 function ToolbarView() { |
|
17 dumpn("ToolbarView was instantiated"); |
|
18 |
|
19 this._onTogglePanesPressed = this._onTogglePanesPressed.bind(this); |
|
20 this._onResumePressed = this._onResumePressed.bind(this); |
|
21 this._onStepOverPressed = this._onStepOverPressed.bind(this); |
|
22 this._onStepInPressed = this._onStepInPressed.bind(this); |
|
23 this._onStepOutPressed = this._onStepOutPressed.bind(this); |
|
24 } |
|
25 |
|
26 ToolbarView.prototype = { |
|
27 /** |
|
28 * Initialization function, called when the debugger is started. |
|
29 */ |
|
30 initialize: function() { |
|
31 dumpn("Initializing the ToolbarView"); |
|
32 |
|
33 this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle"); |
|
34 this._resumeButton = document.getElementById("resume"); |
|
35 this._stepOverButton = document.getElementById("step-over"); |
|
36 this._stepInButton = document.getElementById("step-in"); |
|
37 this._stepOutButton = document.getElementById("step-out"); |
|
38 this._resumeOrderTooltip = new Tooltip(document); |
|
39 this._resumeOrderTooltip.defaultPosition = TOOLBAR_ORDER_POPUP_POSITION; |
|
40 |
|
41 let resumeKey = ShortcutUtils.prettifyShortcut(document.getElementById("resumeKey")); |
|
42 let stepOverKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepOverKey")); |
|
43 let stepInKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepInKey")); |
|
44 let stepOutKey = ShortcutUtils.prettifyShortcut(document.getElementById("stepOutKey")); |
|
45 this._resumeTooltip = L10N.getFormatStr("resumeButtonTooltip", resumeKey); |
|
46 this._pauseTooltip = L10N.getFormatStr("pauseButtonTooltip", resumeKey); |
|
47 this._stepOverTooltip = L10N.getFormatStr("stepOverTooltip", stepOverKey); |
|
48 this._stepInTooltip = L10N.getFormatStr("stepInTooltip", stepInKey); |
|
49 this._stepOutTooltip = L10N.getFormatStr("stepOutTooltip", stepOutKey); |
|
50 |
|
51 this._instrumentsPaneToggleButton.addEventListener("mousedown", this._onTogglePanesPressed, false); |
|
52 this._resumeButton.addEventListener("mousedown", this._onResumePressed, false); |
|
53 this._stepOverButton.addEventListener("mousedown", this._onStepOverPressed, false); |
|
54 this._stepInButton.addEventListener("mousedown", this._onStepInPressed, false); |
|
55 this._stepOutButton.addEventListener("mousedown", this._onStepOutPressed, false); |
|
56 |
|
57 this._stepOverButton.setAttribute("tooltiptext", this._stepOverTooltip); |
|
58 this._stepInButton.setAttribute("tooltiptext", this._stepInTooltip); |
|
59 this._stepOutButton.setAttribute("tooltiptext", this._stepOutTooltip); |
|
60 }, |
|
61 |
|
62 /** |
|
63 * Destruction function, called when the debugger is closed. |
|
64 */ |
|
65 destroy: function() { |
|
66 dumpn("Destroying the ToolbarView"); |
|
67 |
|
68 this._instrumentsPaneToggleButton.removeEventListener("mousedown", this._onTogglePanesPressed, false); |
|
69 this._resumeButton.removeEventListener("mousedown", this._onResumePressed, false); |
|
70 this._stepOverButton.removeEventListener("mousedown", this._onStepOverPressed, false); |
|
71 this._stepInButton.removeEventListener("mousedown", this._onStepInPressed, false); |
|
72 this._stepOutButton.removeEventListener("mousedown", this._onStepOutPressed, false); |
|
73 }, |
|
74 |
|
75 /** |
|
76 * Display a warning when trying to resume a debuggee while another is paused. |
|
77 * Debuggees must be unpaused in a Last-In-First-Out order. |
|
78 * |
|
79 * @param string aPausedUrl |
|
80 * The URL of the last paused debuggee. |
|
81 */ |
|
82 showResumeWarning: function(aPausedUrl) { |
|
83 let label = L10N.getFormatStr("resumptionOrderPanelTitle", aPausedUrl); |
|
84 let defaultStyle = "default-tooltip-simple-text-colors"; |
|
85 this._resumeOrderTooltip.setTextContent({ messages: [label], isAlertTooltip: true }); |
|
86 this._resumeOrderTooltip.show(this._resumeButton); |
|
87 }, |
|
88 |
|
89 /** |
|
90 * Sets the resume button state based on the debugger active thread. |
|
91 * |
|
92 * @param string aState |
|
93 * Either "paused" or "attached". |
|
94 */ |
|
95 toggleResumeButtonState: function(aState) { |
|
96 // If we're paused, check and show a resume label on the button. |
|
97 if (aState == "paused") { |
|
98 this._resumeButton.setAttribute("checked", "true"); |
|
99 this._resumeButton.setAttribute("tooltiptext", this._resumeTooltip); |
|
100 } |
|
101 // If we're attached, do the opposite. |
|
102 else if (aState == "attached") { |
|
103 this._resumeButton.removeAttribute("checked"); |
|
104 this._resumeButton.setAttribute("tooltiptext", this._pauseTooltip); |
|
105 } |
|
106 }, |
|
107 |
|
108 /** |
|
109 * Listener handling the toggle button click event. |
|
110 */ |
|
111 _onTogglePanesPressed: function() { |
|
112 DebuggerView.toggleInstrumentsPane({ |
|
113 visible: DebuggerView.instrumentsPaneHidden, |
|
114 animated: true, |
|
115 delayed: true |
|
116 }); |
|
117 }, |
|
118 |
|
119 /** |
|
120 * Listener handling the pause/resume button click event. |
|
121 */ |
|
122 _onResumePressed: function() { |
|
123 if (DebuggerController.activeThread.paused) { |
|
124 let warn = DebuggerController._ensureResumptionOrder; |
|
125 DebuggerController.StackFrames.currentFrameDepth = -1; |
|
126 DebuggerController.activeThread.resume(warn); |
|
127 } else { |
|
128 DebuggerController.activeThread.interrupt(); |
|
129 } |
|
130 }, |
|
131 |
|
132 /** |
|
133 * Listener handling the step over button click event. |
|
134 */ |
|
135 _onStepOverPressed: function() { |
|
136 if (DebuggerController.activeThread.paused) { |
|
137 DebuggerController.StackFrames.currentFrameDepth = -1; |
|
138 let warn = DebuggerController._ensureResumptionOrder; |
|
139 DebuggerController.activeThread.stepOver(warn); |
|
140 } |
|
141 }, |
|
142 |
|
143 /** |
|
144 * Listener handling the step in button click event. |
|
145 */ |
|
146 _onStepInPressed: function() { |
|
147 if (DebuggerController.activeThread.paused) { |
|
148 DebuggerController.StackFrames.currentFrameDepth = -1; |
|
149 let warn = DebuggerController._ensureResumptionOrder; |
|
150 DebuggerController.activeThread.stepIn(warn); |
|
151 } |
|
152 }, |
|
153 |
|
154 /** |
|
155 * Listener handling the step out button click event. |
|
156 */ |
|
157 _onStepOutPressed: function() { |
|
158 if (DebuggerController.activeThread.paused) { |
|
159 DebuggerController.StackFrames.currentFrameDepth = -1; |
|
160 let warn = DebuggerController._ensureResumptionOrder; |
|
161 DebuggerController.activeThread.stepOut(warn); |
|
162 } |
|
163 }, |
|
164 |
|
165 _instrumentsPaneToggleButton: null, |
|
166 _resumeButton: null, |
|
167 _stepOverButton: null, |
|
168 _stepInButton: null, |
|
169 _stepOutButton: null, |
|
170 _resumeOrderTooltip: null, |
|
171 _resumeTooltip: "", |
|
172 _pauseTooltip: "", |
|
173 _stepOverTooltip: "", |
|
174 _stepInTooltip: "", |
|
175 _stepOutTooltip: "" |
|
176 }; |
|
177 |
|
178 /** |
|
179 * Functions handling the options UI. |
|
180 */ |
|
181 function OptionsView() { |
|
182 dumpn("OptionsView was instantiated"); |
|
183 |
|
184 this._toggleAutoPrettyPrint = this._toggleAutoPrettyPrint.bind(this); |
|
185 this._togglePauseOnExceptions = this._togglePauseOnExceptions.bind(this); |
|
186 this._toggleIgnoreCaughtExceptions = this._toggleIgnoreCaughtExceptions.bind(this); |
|
187 this._toggleShowPanesOnStartup = this._toggleShowPanesOnStartup.bind(this); |
|
188 this._toggleShowVariablesOnlyEnum = this._toggleShowVariablesOnlyEnum.bind(this); |
|
189 this._toggleShowVariablesFilterBox = this._toggleShowVariablesFilterBox.bind(this); |
|
190 this._toggleShowOriginalSource = this._toggleShowOriginalSource.bind(this); |
|
191 } |
|
192 |
|
193 OptionsView.prototype = { |
|
194 /** |
|
195 * Initialization function, called when the debugger is started. |
|
196 */ |
|
197 initialize: function() { |
|
198 dumpn("Initializing the OptionsView"); |
|
199 |
|
200 this._button = document.getElementById("debugger-options"); |
|
201 this._autoPrettyPrint = document.getElementById("auto-pretty-print"); |
|
202 this._pauseOnExceptionsItem = document.getElementById("pause-on-exceptions"); |
|
203 this._ignoreCaughtExceptionsItem = document.getElementById("ignore-caught-exceptions"); |
|
204 this._showPanesOnStartupItem = document.getElementById("show-panes-on-startup"); |
|
205 this._showVariablesOnlyEnumItem = document.getElementById("show-vars-only-enum"); |
|
206 this._showVariablesFilterBoxItem = document.getElementById("show-vars-filter-box"); |
|
207 this._showOriginalSourceItem = document.getElementById("show-original-source"); |
|
208 |
|
209 this._autoPrettyPrint.setAttribute("checked", Prefs.autoPrettyPrint); |
|
210 this._pauseOnExceptionsItem.setAttribute("checked", Prefs.pauseOnExceptions); |
|
211 this._ignoreCaughtExceptionsItem.setAttribute("checked", Prefs.ignoreCaughtExceptions); |
|
212 this._showPanesOnStartupItem.setAttribute("checked", Prefs.panesVisibleOnStartup); |
|
213 this._showVariablesOnlyEnumItem.setAttribute("checked", Prefs.variablesOnlyEnumVisible); |
|
214 this._showVariablesFilterBoxItem.setAttribute("checked", Prefs.variablesSearchboxVisible); |
|
215 this._showOriginalSourceItem.setAttribute("checked", Prefs.sourceMapsEnabled); |
|
216 }, |
|
217 |
|
218 /** |
|
219 * Destruction function, called when the debugger is closed. |
|
220 */ |
|
221 destroy: function() { |
|
222 dumpn("Destroying the OptionsView"); |
|
223 // Nothing to do here yet. |
|
224 }, |
|
225 |
|
226 /** |
|
227 * Listener handling the 'gear menu' popup showing event. |
|
228 */ |
|
229 _onPopupShowing: function() { |
|
230 this._button.setAttribute("open", "true"); |
|
231 window.emit(EVENTS.OPTIONS_POPUP_SHOWING); |
|
232 }, |
|
233 |
|
234 /** |
|
235 * Listener handling the 'gear menu' popup hiding event. |
|
236 */ |
|
237 _onPopupHiding: function() { |
|
238 this._button.removeAttribute("open"); |
|
239 }, |
|
240 |
|
241 /** |
|
242 * Listener handling the 'gear menu' popup hidden event. |
|
243 */ |
|
244 _onPopupHidden: function() { |
|
245 window.emit(EVENTS.OPTIONS_POPUP_HIDDEN); |
|
246 }, |
|
247 |
|
248 /** |
|
249 * Listener handling the 'auto pretty print' menuitem command. |
|
250 */ |
|
251 _toggleAutoPrettyPrint: function(){ |
|
252 Prefs.autoPrettyPrint = |
|
253 this._autoPrettyPrint.getAttribute("checked") == "true"; |
|
254 }, |
|
255 |
|
256 /** |
|
257 * Listener handling the 'pause on exceptions' menuitem command. |
|
258 */ |
|
259 _togglePauseOnExceptions: function() { |
|
260 Prefs.pauseOnExceptions = |
|
261 this._pauseOnExceptionsItem.getAttribute("checked") == "true"; |
|
262 |
|
263 DebuggerController.activeThread.pauseOnExceptions( |
|
264 Prefs.pauseOnExceptions, |
|
265 Prefs.ignoreCaughtExceptions); |
|
266 }, |
|
267 |
|
268 _toggleIgnoreCaughtExceptions: function() { |
|
269 Prefs.ignoreCaughtExceptions = |
|
270 this._ignoreCaughtExceptionsItem.getAttribute("checked") == "true"; |
|
271 |
|
272 DebuggerController.activeThread.pauseOnExceptions( |
|
273 Prefs.pauseOnExceptions, |
|
274 Prefs.ignoreCaughtExceptions); |
|
275 }, |
|
276 |
|
277 /** |
|
278 * Listener handling the 'show panes on startup' menuitem command. |
|
279 */ |
|
280 _toggleShowPanesOnStartup: function() { |
|
281 Prefs.panesVisibleOnStartup = |
|
282 this._showPanesOnStartupItem.getAttribute("checked") == "true"; |
|
283 }, |
|
284 |
|
285 /** |
|
286 * Listener handling the 'show non-enumerables' menuitem command. |
|
287 */ |
|
288 _toggleShowVariablesOnlyEnum: function() { |
|
289 let pref = Prefs.variablesOnlyEnumVisible = |
|
290 this._showVariablesOnlyEnumItem.getAttribute("checked") == "true"; |
|
291 |
|
292 DebuggerView.Variables.onlyEnumVisible = pref; |
|
293 }, |
|
294 |
|
295 /** |
|
296 * Listener handling the 'show variables searchbox' menuitem command. |
|
297 */ |
|
298 _toggleShowVariablesFilterBox: function() { |
|
299 let pref = Prefs.variablesSearchboxVisible = |
|
300 this._showVariablesFilterBoxItem.getAttribute("checked") == "true"; |
|
301 |
|
302 DebuggerView.Variables.searchEnabled = pref; |
|
303 }, |
|
304 |
|
305 /** |
|
306 * Listener handling the 'show original source' menuitem command. |
|
307 */ |
|
308 _toggleShowOriginalSource: function() { |
|
309 let pref = Prefs.sourceMapsEnabled = |
|
310 this._showOriginalSourceItem.getAttribute("checked") == "true"; |
|
311 |
|
312 // Don't block the UI while reconfiguring the server. |
|
313 window.once(EVENTS.OPTIONS_POPUP_HIDDEN, () => { |
|
314 // The popup panel needs more time to hide after triggering onpopuphidden. |
|
315 window.setTimeout(() => { |
|
316 DebuggerController.reconfigureThread(pref); |
|
317 }, POPUP_HIDDEN_DELAY); |
|
318 }, false); |
|
319 }, |
|
320 |
|
321 _button: null, |
|
322 _pauseOnExceptionsItem: null, |
|
323 _showPanesOnStartupItem: null, |
|
324 _showVariablesOnlyEnumItem: null, |
|
325 _showVariablesFilterBoxItem: null, |
|
326 _showOriginalSourceItem: null |
|
327 }; |
|
328 |
|
329 /** |
|
330 * Functions handling the stackframes UI. |
|
331 */ |
|
332 function StackFramesView() { |
|
333 dumpn("StackFramesView was instantiated"); |
|
334 |
|
335 this._onStackframeRemoved = this._onStackframeRemoved.bind(this); |
|
336 this._onSelect = this._onSelect.bind(this); |
|
337 this._onScroll = this._onScroll.bind(this); |
|
338 this._afterScroll = this._afterScroll.bind(this); |
|
339 } |
|
340 |
|
341 StackFramesView.prototype = Heritage.extend(WidgetMethods, { |
|
342 /** |
|
343 * Initialization function, called when the debugger is started. |
|
344 */ |
|
345 initialize: function() { |
|
346 dumpn("Initializing the StackFramesView"); |
|
347 |
|
348 this.widget = new BreadcrumbsWidget(document.getElementById("stackframes")); |
|
349 this.widget.addEventListener("select", this._onSelect, false); |
|
350 this.widget.addEventListener("scroll", this._onScroll, true); |
|
351 window.addEventListener("resize", this._onScroll, true); |
|
352 |
|
353 this.autoFocusOnFirstItem = false; |
|
354 this.autoFocusOnSelection = false; |
|
355 |
|
356 // This view's contents are also mirrored in a different container. |
|
357 this._mirror = DebuggerView.StackFramesClassicList; |
|
358 }, |
|
359 |
|
360 /** |
|
361 * Destruction function, called when the debugger is closed. |
|
362 */ |
|
363 destroy: function() { |
|
364 dumpn("Destroying the StackFramesView"); |
|
365 |
|
366 this.widget.removeEventListener("select", this._onSelect, false); |
|
367 this.widget.removeEventListener("scroll", this._onScroll, true); |
|
368 window.removeEventListener("resize", this._onScroll, true); |
|
369 }, |
|
370 |
|
371 /** |
|
372 * Adds a frame in this stackframes container. |
|
373 * |
|
374 * @param string aTitle |
|
375 * The frame title (function name). |
|
376 * @param string aUrl |
|
377 * The frame source url. |
|
378 * @param string aLine |
|
379 * The frame line number. |
|
380 * @param number aDepth |
|
381 * The frame depth in the stack. |
|
382 * @param boolean aIsBlackBoxed |
|
383 * Whether or not the frame is black boxed. |
|
384 */ |
|
385 addFrame: function(aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) { |
|
386 // Blackboxed stack frames are collapsed into a single entry in |
|
387 // the view. By convention, only the first frame is displayed. |
|
388 if (aIsBlackBoxed) { |
|
389 if (this._prevBlackBoxedUrl == aUrl) { |
|
390 return; |
|
391 } |
|
392 this._prevBlackBoxedUrl = aUrl; |
|
393 } else { |
|
394 this._prevBlackBoxedUrl = null; |
|
395 } |
|
396 |
|
397 // Create the element node for the stack frame item. |
|
398 let frameView = this._createFrameView.apply(this, arguments); |
|
399 |
|
400 // Append a stack frame item to this container. |
|
401 this.push([frameView], { |
|
402 index: 0, /* specifies on which position should the item be appended */ |
|
403 attachment: { |
|
404 title: aTitle, |
|
405 url: aUrl, |
|
406 line: aLine, |
|
407 depth: aDepth |
|
408 }, |
|
409 // Make sure that when the stack frame item is removed, the corresponding |
|
410 // mirrored item in the classic list is also removed. |
|
411 finalize: this._onStackframeRemoved |
|
412 }); |
|
413 |
|
414 // Mirror this newly inserted item inside the "Call Stack" tab. |
|
415 this._mirror.addFrame(aTitle, aUrl, aLine, aDepth); |
|
416 }, |
|
417 |
|
418 /** |
|
419 * Selects the frame at the specified depth in this container. |
|
420 * @param number aDepth |
|
421 */ |
|
422 set selectedDepth(aDepth) { |
|
423 this.selectedItem = aItem => aItem.attachment.depth == aDepth; |
|
424 }, |
|
425 |
|
426 /** |
|
427 * Gets the currently selected stack frame's depth in this container. |
|
428 * This will essentially be the opposite of |selectedIndex|, which deals |
|
429 * with the position in the view, where the last item added is actually |
|
430 * the bottommost, not topmost. |
|
431 * @return number |
|
432 */ |
|
433 get selectedDepth() { |
|
434 return this.selectedItem.attachment.depth; |
|
435 }, |
|
436 |
|
437 /** |
|
438 * Specifies if the active thread has more frames that need to be loaded. |
|
439 */ |
|
440 dirty: false, |
|
441 |
|
442 /** |
|
443 * Customization function for creating an item's UI. |
|
444 * |
|
445 * @param string aTitle |
|
446 * The frame title to be displayed in the list. |
|
447 * @param string aUrl |
|
448 * The frame source url. |
|
449 * @param string aLine |
|
450 * The frame line number. |
|
451 * @param number aDepth |
|
452 * The frame depth in the stack. |
|
453 * @param boolean aIsBlackBoxed |
|
454 * Whether or not the frame is black boxed. |
|
455 * @return nsIDOMNode |
|
456 * The stack frame view. |
|
457 */ |
|
458 _createFrameView: function(aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) { |
|
459 let container = document.createElement("hbox"); |
|
460 container.id = "stackframe-" + aDepth; |
|
461 container.className = "dbg-stackframe"; |
|
462 |
|
463 let frameDetails = SourceUtils.trimUrlLength( |
|
464 SourceUtils.getSourceLabel(aUrl), |
|
465 STACK_FRAMES_SOURCE_URL_MAX_LENGTH, |
|
466 STACK_FRAMES_SOURCE_URL_TRIM_SECTION); |
|
467 |
|
468 if (aIsBlackBoxed) { |
|
469 container.classList.add("dbg-stackframe-black-boxed"); |
|
470 } else { |
|
471 let frameTitleNode = document.createElement("label"); |
|
472 frameTitleNode.className = "plain dbg-stackframe-title breadcrumbs-widget-item-tag"; |
|
473 frameTitleNode.setAttribute("value", aTitle); |
|
474 container.appendChild(frameTitleNode); |
|
475 |
|
476 frameDetails += SEARCH_LINE_FLAG + aLine; |
|
477 } |
|
478 |
|
479 let frameDetailsNode = document.createElement("label"); |
|
480 frameDetailsNode.className = "plain dbg-stackframe-details breadcrumbs-widget-item-id"; |
|
481 frameDetailsNode.setAttribute("value", frameDetails); |
|
482 container.appendChild(frameDetailsNode); |
|
483 |
|
484 return container; |
|
485 }, |
|
486 |
|
487 /** |
|
488 * Function called each time a stack frame item is removed. |
|
489 * |
|
490 * @param object aItem |
|
491 * The corresponding item. |
|
492 */ |
|
493 _onStackframeRemoved: function(aItem) { |
|
494 dumpn("Finalizing stackframe item: " + aItem); |
|
495 |
|
496 // Remove the mirrored item in the classic list. |
|
497 let depth = aItem.attachment.depth; |
|
498 this._mirror.remove(this._mirror.getItemForAttachment(e => e.depth == depth)); |
|
499 |
|
500 // Forget the previously blackboxed stack frame url. |
|
501 this._prevBlackBoxedUrl = null; |
|
502 }, |
|
503 |
|
504 /** |
|
505 * The select listener for the stackframes container. |
|
506 */ |
|
507 _onSelect: function(e) { |
|
508 let stackframeItem = this.selectedItem; |
|
509 if (stackframeItem) { |
|
510 // The container is not empty and an actual item was selected. |
|
511 let depth = stackframeItem.attachment.depth; |
|
512 DebuggerController.StackFrames.selectFrame(depth); |
|
513 |
|
514 // Mirror the selected item in the classic list. |
|
515 this.suppressSelectionEvents = true; |
|
516 this._mirror.selectedItem = e => e.attachment.depth == depth; |
|
517 this.suppressSelectionEvents = false; |
|
518 } |
|
519 }, |
|
520 |
|
521 /** |
|
522 * The scroll listener for the stackframes container. |
|
523 */ |
|
524 _onScroll: function() { |
|
525 // Update the stackframes container only if we have to. |
|
526 if (!this.dirty) { |
|
527 return; |
|
528 } |
|
529 // Allow requests to settle down first. |
|
530 setNamedTimeout("stack-scroll", STACK_FRAMES_SCROLL_DELAY, this._afterScroll); |
|
531 }, |
|
532 |
|
533 /** |
|
534 * Requests the addition of more frames from the controller. |
|
535 */ |
|
536 _afterScroll: function() { |
|
537 let scrollPosition = this.widget.getAttribute("scrollPosition"); |
|
538 let scrollWidth = this.widget.getAttribute("scrollWidth"); |
|
539 |
|
540 // If the stackframes container scrolled almost to the end, with only |
|
541 // 1/10 of a breadcrumb remaining, load more content. |
|
542 if (scrollPosition - scrollWidth / 10 < 1) { |
|
543 this.ensureIndexIsVisible(CALL_STACK_PAGE_SIZE - 1); |
|
544 this.dirty = false; |
|
545 |
|
546 // Loads more stack frames from the debugger server cache. |
|
547 DebuggerController.StackFrames.addMoreFrames(); |
|
548 } |
|
549 }, |
|
550 |
|
551 _mirror: null, |
|
552 _prevBlackBoxedUrl: null |
|
553 }); |
|
554 |
|
555 /* |
|
556 * Functions handling the stackframes classic list UI. |
|
557 * Controlled by the DebuggerView.StackFrames isntance. |
|
558 */ |
|
559 function StackFramesClassicListView() { |
|
560 dumpn("StackFramesClassicListView was instantiated"); |
|
561 |
|
562 this._onSelect = this._onSelect.bind(this); |
|
563 } |
|
564 |
|
565 StackFramesClassicListView.prototype = Heritage.extend(WidgetMethods, { |
|
566 /** |
|
567 * Initialization function, called when the debugger is started. |
|
568 */ |
|
569 initialize: function() { |
|
570 dumpn("Initializing the StackFramesClassicListView"); |
|
571 |
|
572 this.widget = new SideMenuWidget(document.getElementById("callstack-list")); |
|
573 this.widget.addEventListener("select", this._onSelect, false); |
|
574 |
|
575 this.emptyText = L10N.getStr("noStackFramesText"); |
|
576 this.autoFocusOnFirstItem = false; |
|
577 this.autoFocusOnSelection = false; |
|
578 |
|
579 // This view's contents are also mirrored in a different container. |
|
580 this._mirror = DebuggerView.StackFrames; |
|
581 }, |
|
582 |
|
583 /** |
|
584 * Destruction function, called when the debugger is closed. |
|
585 */ |
|
586 destroy: function() { |
|
587 dumpn("Destroying the StackFramesClassicListView"); |
|
588 |
|
589 this.widget.removeEventListener("select", this._onSelect, false); |
|
590 }, |
|
591 |
|
592 /** |
|
593 * Adds a frame in this stackframes container. |
|
594 * |
|
595 * @param string aTitle |
|
596 * The frame title (function name). |
|
597 * @param string aUrl |
|
598 * The frame source url. |
|
599 * @param string aLine |
|
600 * The frame line number. |
|
601 * @param number aDepth |
|
602 * The frame depth in the stack. |
|
603 */ |
|
604 addFrame: function(aTitle, aUrl, aLine, aDepth) { |
|
605 // Create the element node for the stack frame item. |
|
606 let frameView = this._createFrameView.apply(this, arguments); |
|
607 |
|
608 // Append a stack frame item to this container. |
|
609 this.push([frameView], { |
|
610 attachment: { |
|
611 depth: aDepth |
|
612 } |
|
613 }); |
|
614 }, |
|
615 |
|
616 /** |
|
617 * Customization function for creating an item's UI. |
|
618 * |
|
619 * @param string aTitle |
|
620 * The frame title to be displayed in the list. |
|
621 * @param string aUrl |
|
622 * The frame source url. |
|
623 * @param string aLine |
|
624 * The frame line number. |
|
625 * @param number aDepth |
|
626 * The frame depth in the stack. |
|
627 * @return nsIDOMNode |
|
628 * The stack frame view. |
|
629 */ |
|
630 _createFrameView: function(aTitle, aUrl, aLine, aDepth) { |
|
631 let container = document.createElement("hbox"); |
|
632 container.id = "classic-stackframe-" + aDepth; |
|
633 container.className = "dbg-classic-stackframe"; |
|
634 container.setAttribute("flex", "1"); |
|
635 |
|
636 let frameTitleNode = document.createElement("label"); |
|
637 frameTitleNode.className = "plain dbg-classic-stackframe-title"; |
|
638 frameTitleNode.setAttribute("value", aTitle); |
|
639 frameTitleNode.setAttribute("crop", "center"); |
|
640 |
|
641 let frameDetailsNode = document.createElement("hbox"); |
|
642 frameDetailsNode.className = "plain dbg-classic-stackframe-details"; |
|
643 |
|
644 let frameUrlNode = document.createElement("label"); |
|
645 frameUrlNode.className = "plain dbg-classic-stackframe-details-url"; |
|
646 frameUrlNode.setAttribute("value", SourceUtils.getSourceLabel(aUrl)); |
|
647 frameUrlNode.setAttribute("crop", "center"); |
|
648 frameDetailsNode.appendChild(frameUrlNode); |
|
649 |
|
650 let frameDetailsSeparator = document.createElement("label"); |
|
651 frameDetailsSeparator.className = "plain dbg-classic-stackframe-details-sep"; |
|
652 frameDetailsSeparator.setAttribute("value", SEARCH_LINE_FLAG); |
|
653 frameDetailsNode.appendChild(frameDetailsSeparator); |
|
654 |
|
655 let frameLineNode = document.createElement("label"); |
|
656 frameLineNode.className = "plain dbg-classic-stackframe-details-line"; |
|
657 frameLineNode.setAttribute("value", aLine); |
|
658 frameDetailsNode.appendChild(frameLineNode); |
|
659 |
|
660 container.appendChild(frameTitleNode); |
|
661 container.appendChild(frameDetailsNode); |
|
662 |
|
663 return container; |
|
664 }, |
|
665 |
|
666 /** |
|
667 * The select listener for the stackframes container. |
|
668 */ |
|
669 _onSelect: function(e) { |
|
670 let stackframeItem = this.selectedItem; |
|
671 if (stackframeItem) { |
|
672 // The container is not empty and an actual item was selected. |
|
673 // Mirror the selected item in the breadcrumbs list. |
|
674 let depth = stackframeItem.attachment.depth; |
|
675 this._mirror.selectedItem = e => e.attachment.depth == depth; |
|
676 } |
|
677 }, |
|
678 |
|
679 _mirror: null |
|
680 }); |
|
681 |
|
682 /** |
|
683 * Functions handling the filtering UI. |
|
684 */ |
|
685 function FilterView() { |
|
686 dumpn("FilterView was instantiated"); |
|
687 |
|
688 this._onClick = this._onClick.bind(this); |
|
689 this._onInput = this._onInput.bind(this); |
|
690 this._onKeyPress = this._onKeyPress.bind(this); |
|
691 this._onBlur = this._onBlur.bind(this); |
|
692 } |
|
693 |
|
694 FilterView.prototype = { |
|
695 /** |
|
696 * Initialization function, called when the debugger is started. |
|
697 */ |
|
698 initialize: function() { |
|
699 dumpn("Initializing the FilterView"); |
|
700 |
|
701 this._searchbox = document.getElementById("searchbox"); |
|
702 this._searchboxHelpPanel = document.getElementById("searchbox-help-panel"); |
|
703 this._filterLabel = document.getElementById("filter-label"); |
|
704 this._globalOperatorButton = document.getElementById("global-operator-button"); |
|
705 this._globalOperatorLabel = document.getElementById("global-operator-label"); |
|
706 this._functionOperatorButton = document.getElementById("function-operator-button"); |
|
707 this._functionOperatorLabel = document.getElementById("function-operator-label"); |
|
708 this._tokenOperatorButton = document.getElementById("token-operator-button"); |
|
709 this._tokenOperatorLabel = document.getElementById("token-operator-label"); |
|
710 this._lineOperatorButton = document.getElementById("line-operator-button"); |
|
711 this._lineOperatorLabel = document.getElementById("line-operator-label"); |
|
712 this._variableOperatorButton = document.getElementById("variable-operator-button"); |
|
713 this._variableOperatorLabel = document.getElementById("variable-operator-label"); |
|
714 |
|
715 this._fileSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("fileSearchKey")); |
|
716 this._globalSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("globalSearchKey")); |
|
717 this._filteredFunctionsKey = ShortcutUtils.prettifyShortcut(document.getElementById("functionSearchKey")); |
|
718 this._tokenSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("tokenSearchKey")); |
|
719 this._lineSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("lineSearchKey")); |
|
720 this._variableSearchKey = ShortcutUtils.prettifyShortcut(document.getElementById("variableSearchKey")); |
|
721 |
|
722 this._searchbox.addEventListener("click", this._onClick, false); |
|
723 this._searchbox.addEventListener("select", this._onInput, false); |
|
724 this._searchbox.addEventListener("input", this._onInput, false); |
|
725 this._searchbox.addEventListener("keypress", this._onKeyPress, false); |
|
726 this._searchbox.addEventListener("blur", this._onBlur, false); |
|
727 |
|
728 let placeholder = L10N.getFormatStr("emptySearchText", this._fileSearchKey); |
|
729 this._searchbox.setAttribute("placeholder", placeholder); |
|
730 |
|
731 this._globalOperatorButton.setAttribute("label", SEARCH_GLOBAL_FLAG); |
|
732 this._functionOperatorButton.setAttribute("label", SEARCH_FUNCTION_FLAG); |
|
733 this._tokenOperatorButton.setAttribute("label", SEARCH_TOKEN_FLAG); |
|
734 this._lineOperatorButton.setAttribute("label", SEARCH_LINE_FLAG); |
|
735 this._variableOperatorButton.setAttribute("label", SEARCH_VARIABLE_FLAG); |
|
736 |
|
737 this._filterLabel.setAttribute("value", |
|
738 L10N.getFormatStr("searchPanelFilter", this._fileSearchKey)); |
|
739 this._globalOperatorLabel.setAttribute("value", |
|
740 L10N.getFormatStr("searchPanelGlobal", this._globalSearchKey)); |
|
741 this._functionOperatorLabel.setAttribute("value", |
|
742 L10N.getFormatStr("searchPanelFunction", this._filteredFunctionsKey)); |
|
743 this._tokenOperatorLabel.setAttribute("value", |
|
744 L10N.getFormatStr("searchPanelToken", this._tokenSearchKey)); |
|
745 this._lineOperatorLabel.setAttribute("value", |
|
746 L10N.getFormatStr("searchPanelGoToLine", this._lineSearchKey)); |
|
747 this._variableOperatorLabel.setAttribute("value", |
|
748 L10N.getFormatStr("searchPanelVariable", this._variableSearchKey)); |
|
749 }, |
|
750 |
|
751 /** |
|
752 * Destruction function, called when the debugger is closed. |
|
753 */ |
|
754 destroy: function() { |
|
755 dumpn("Destroying the FilterView"); |
|
756 |
|
757 this._searchbox.removeEventListener("click", this._onClick, false); |
|
758 this._searchbox.removeEventListener("select", this._onInput, false); |
|
759 this._searchbox.removeEventListener("input", this._onInput, false); |
|
760 this._searchbox.removeEventListener("keypress", this._onKeyPress, false); |
|
761 this._searchbox.removeEventListener("blur", this._onBlur, false); |
|
762 }, |
|
763 |
|
764 /** |
|
765 * Gets the entered operator and arguments in the searchbox. |
|
766 * @return array |
|
767 */ |
|
768 get searchData() { |
|
769 let operator = "", args = []; |
|
770 |
|
771 let rawValue = this._searchbox.value; |
|
772 let rawLength = rawValue.length; |
|
773 let globalFlagIndex = rawValue.indexOf(SEARCH_GLOBAL_FLAG); |
|
774 let functionFlagIndex = rawValue.indexOf(SEARCH_FUNCTION_FLAG); |
|
775 let variableFlagIndex = rawValue.indexOf(SEARCH_VARIABLE_FLAG); |
|
776 let tokenFlagIndex = rawValue.lastIndexOf(SEARCH_TOKEN_FLAG); |
|
777 let lineFlagIndex = rawValue.lastIndexOf(SEARCH_LINE_FLAG); |
|
778 |
|
779 // This is not a global, function or variable search, allow file/line flags. |
|
780 if (globalFlagIndex != 0 && functionFlagIndex != 0 && variableFlagIndex != 0) { |
|
781 // Token search has precedence over line search. |
|
782 if (tokenFlagIndex != -1) { |
|
783 operator = SEARCH_TOKEN_FLAG; |
|
784 args.push(rawValue.slice(0, tokenFlagIndex)); // file |
|
785 args.push(rawValue.substr(tokenFlagIndex + 1, rawLength)); // token |
|
786 } else if (lineFlagIndex != -1) { |
|
787 operator = SEARCH_LINE_FLAG; |
|
788 args.push(rawValue.slice(0, lineFlagIndex)); // file |
|
789 args.push(+rawValue.substr(lineFlagIndex + 1, rawLength) || 0); // line |
|
790 } else { |
|
791 args.push(rawValue); |
|
792 } |
|
793 } |
|
794 // Global searches dissalow the use of file or line flags. |
|
795 else if (globalFlagIndex == 0) { |
|
796 operator = SEARCH_GLOBAL_FLAG; |
|
797 args.push(rawValue.slice(1)); |
|
798 } |
|
799 // Function searches dissalow the use of file or line flags. |
|
800 else if (functionFlagIndex == 0) { |
|
801 operator = SEARCH_FUNCTION_FLAG; |
|
802 args.push(rawValue.slice(1)); |
|
803 } |
|
804 // Variable searches dissalow the use of file or line flags. |
|
805 else if (variableFlagIndex == 0) { |
|
806 operator = SEARCH_VARIABLE_FLAG; |
|
807 args.push(rawValue.slice(1)); |
|
808 } |
|
809 |
|
810 return [operator, args]; |
|
811 }, |
|
812 |
|
813 /** |
|
814 * Returns the current search operator. |
|
815 * @return string |
|
816 */ |
|
817 get searchOperator() this.searchData[0], |
|
818 |
|
819 /** |
|
820 * Returns the current search arguments. |
|
821 * @return array |
|
822 */ |
|
823 get searchArguments() this.searchData[1], |
|
824 |
|
825 /** |
|
826 * Clears the text from the searchbox and any changed views. |
|
827 */ |
|
828 clearSearch: function() { |
|
829 this._searchbox.value = ""; |
|
830 this.clearViews(); |
|
831 }, |
|
832 |
|
833 /** |
|
834 * Clears all the views that may pop up when searching. |
|
835 */ |
|
836 clearViews: function() { |
|
837 DebuggerView.GlobalSearch.clearView(); |
|
838 DebuggerView.FilteredSources.clearView(); |
|
839 DebuggerView.FilteredFunctions.clearView(); |
|
840 this._searchboxHelpPanel.hidePopup(); |
|
841 }, |
|
842 |
|
843 /** |
|
844 * Performs a line search if necessary. |
|
845 * (Jump to lines in the currently visible source). |
|
846 * |
|
847 * @param number aLine |
|
848 * The source line number to jump to. |
|
849 */ |
|
850 _performLineSearch: function(aLine) { |
|
851 // Make sure we're actually searching for a valid line. |
|
852 if (aLine) { |
|
853 DebuggerView.editor.setCursor({ line: aLine - 1, ch: 0 }, "center"); |
|
854 } |
|
855 }, |
|
856 |
|
857 /** |
|
858 * Performs a token search if necessary. |
|
859 * (Search for tokens in the currently visible source). |
|
860 * |
|
861 * @param string aToken |
|
862 * The source token to find. |
|
863 */ |
|
864 _performTokenSearch: function(aToken) { |
|
865 // Make sure we're actually searching for a valid token. |
|
866 if (!aToken) { |
|
867 return; |
|
868 } |
|
869 DebuggerView.editor.find(aToken); |
|
870 }, |
|
871 |
|
872 /** |
|
873 * The click listener for the search container. |
|
874 */ |
|
875 _onClick: function() { |
|
876 // If there's some text in the searchbox, displaying a panel would |
|
877 // interfere with double/triple click default behaviors. |
|
878 if (!this._searchbox.value) { |
|
879 this._searchboxHelpPanel.openPopup(this._searchbox); |
|
880 } |
|
881 }, |
|
882 |
|
883 /** |
|
884 * The input listener for the search container. |
|
885 */ |
|
886 _onInput: function() { |
|
887 this.clearViews(); |
|
888 |
|
889 // Make sure we're actually searching for something. |
|
890 if (!this._searchbox.value) { |
|
891 return; |
|
892 } |
|
893 |
|
894 // Perform the required search based on the specified operator. |
|
895 switch (this.searchOperator) { |
|
896 case SEARCH_GLOBAL_FLAG: |
|
897 // Schedule a global search for when the user stops typing. |
|
898 DebuggerView.GlobalSearch.scheduleSearch(this.searchArguments[0]); |
|
899 break; |
|
900 case SEARCH_FUNCTION_FLAG: |
|
901 // Schedule a function search for when the user stops typing. |
|
902 DebuggerView.FilteredFunctions.scheduleSearch(this.searchArguments[0]); |
|
903 break; |
|
904 case SEARCH_VARIABLE_FLAG: |
|
905 // Schedule a variable search for when the user stops typing. |
|
906 DebuggerView.Variables.scheduleSearch(this.searchArguments[0]); |
|
907 break; |
|
908 case SEARCH_TOKEN_FLAG: |
|
909 // Schedule a file+token search for when the user stops typing. |
|
910 DebuggerView.FilteredSources.scheduleSearch(this.searchArguments[0]); |
|
911 this._performTokenSearch(this.searchArguments[1]); |
|
912 break; |
|
913 case SEARCH_LINE_FLAG: |
|
914 // Schedule a file+line search for when the user stops typing. |
|
915 DebuggerView.FilteredSources.scheduleSearch(this.searchArguments[0]); |
|
916 this._performLineSearch(this.searchArguments[1]); |
|
917 break; |
|
918 default: |
|
919 // Schedule a file only search for when the user stops typing. |
|
920 DebuggerView.FilteredSources.scheduleSearch(this.searchArguments[0]); |
|
921 break; |
|
922 } |
|
923 }, |
|
924 |
|
925 /** |
|
926 * The key press listener for the search container. |
|
927 */ |
|
928 _onKeyPress: function(e) { |
|
929 // This attribute is not implemented in Gecko at this time, see bug 680830. |
|
930 e.char = String.fromCharCode(e.charCode); |
|
931 |
|
932 // Perform the required action based on the specified operator. |
|
933 let [operator, args] = this.searchData; |
|
934 let isGlobalSearch = operator == SEARCH_GLOBAL_FLAG; |
|
935 let isFunctionSearch = operator == SEARCH_FUNCTION_FLAG; |
|
936 let isVariableSearch = operator == SEARCH_VARIABLE_FLAG; |
|
937 let isTokenSearch = operator == SEARCH_TOKEN_FLAG; |
|
938 let isLineSearch = operator == SEARCH_LINE_FLAG; |
|
939 let isFileOnlySearch = !operator && args.length == 1; |
|
940 |
|
941 // Depending on the pressed keys, determine to correct action to perform. |
|
942 let actionToPerform; |
|
943 |
|
944 // Meta+G and Ctrl+N focus next matches. |
|
945 if ((e.char == "g" && e.metaKey) || e.char == "n" && e.ctrlKey) { |
|
946 actionToPerform = "selectNext"; |
|
947 } |
|
948 // Meta+Shift+G and Ctrl+P focus previous matches. |
|
949 else if ((e.char == "G" && e.metaKey) || e.char == "p" && e.ctrlKey) { |
|
950 actionToPerform = "selectPrev"; |
|
951 } |
|
952 // Return, enter, down and up keys focus next or previous matches, while |
|
953 // the escape key switches focus from the search container. |
|
954 else switch (e.keyCode) { |
|
955 case e.DOM_VK_RETURN: |
|
956 var isReturnKey = true; |
|
957 // If the shift key is pressed, focus on the previous result |
|
958 actionToPerform = e.shiftKey ? "selectPrev" : "selectNext"; |
|
959 break; |
|
960 case e.DOM_VK_DOWN: |
|
961 actionToPerform = "selectNext"; |
|
962 break; |
|
963 case e.DOM_VK_UP: |
|
964 actionToPerform = "selectPrev"; |
|
965 break; |
|
966 } |
|
967 |
|
968 // If there's no action to perform, or no operator, file line or token |
|
969 // were specified, then this is either a broken or empty search. |
|
970 if (!actionToPerform || (!operator && !args.length)) { |
|
971 DebuggerView.editor.dropSelection(); |
|
972 return; |
|
973 } |
|
974 |
|
975 e.preventDefault(); |
|
976 e.stopPropagation(); |
|
977 |
|
978 // Jump to the next/previous entry in the global search, or perform |
|
979 // a new global search immediately |
|
980 if (isGlobalSearch) { |
|
981 let targetView = DebuggerView.GlobalSearch; |
|
982 if (!isReturnKey) { |
|
983 targetView[actionToPerform](); |
|
984 } else if (targetView.hidden) { |
|
985 targetView.scheduleSearch(args[0], 0); |
|
986 } |
|
987 return; |
|
988 } |
|
989 |
|
990 // Jump to the next/previous entry in the function search, perform |
|
991 // a new function search immediately, or clear it. |
|
992 if (isFunctionSearch) { |
|
993 let targetView = DebuggerView.FilteredFunctions; |
|
994 if (!isReturnKey) { |
|
995 targetView[actionToPerform](); |
|
996 } else if (targetView.hidden) { |
|
997 targetView.scheduleSearch(args[0], 0); |
|
998 } else { |
|
999 if (!targetView.selectedItem) { |
|
1000 targetView.selectedIndex = 0; |
|
1001 } |
|
1002 this.clearSearch(); |
|
1003 } |
|
1004 return; |
|
1005 } |
|
1006 |
|
1007 // Perform a new variable search immediately. |
|
1008 if (isVariableSearch) { |
|
1009 let targetView = DebuggerView.Variables; |
|
1010 if (isReturnKey) { |
|
1011 targetView.scheduleSearch(args[0], 0); |
|
1012 } |
|
1013 return; |
|
1014 } |
|
1015 |
|
1016 // Jump to the next/previous entry in the file search, perform |
|
1017 // a new file search immediately, or clear it. |
|
1018 if (isFileOnlySearch) { |
|
1019 let targetView = DebuggerView.FilteredSources; |
|
1020 if (!isReturnKey) { |
|
1021 targetView[actionToPerform](); |
|
1022 } else if (targetView.hidden) { |
|
1023 targetView.scheduleSearch(args[0], 0); |
|
1024 } else { |
|
1025 if (!targetView.selectedItem) { |
|
1026 targetView.selectedIndex = 0; |
|
1027 } |
|
1028 this.clearSearch(); |
|
1029 } |
|
1030 return; |
|
1031 } |
|
1032 |
|
1033 // Jump to the next/previous instance of the currently searched token. |
|
1034 if (isTokenSearch) { |
|
1035 let methods = { selectNext: "findNext", selectPrev: "findPrev" }; |
|
1036 DebuggerView.editor[methods[actionToPerform]](); |
|
1037 return; |
|
1038 } |
|
1039 |
|
1040 // Increment/decrement the currently searched caret line. |
|
1041 if (isLineSearch) { |
|
1042 let [, line] = args; |
|
1043 let amounts = { selectNext: 1, selectPrev: -1 }; |
|
1044 |
|
1045 // Modify the line number and jump to it. |
|
1046 line += !isReturnKey ? amounts[actionToPerform] : 0; |
|
1047 let lineCount = DebuggerView.editor.lineCount(); |
|
1048 let lineTarget = line < 1 ? 1 : line > lineCount ? lineCount : line; |
|
1049 this._doSearch(SEARCH_LINE_FLAG, lineTarget); |
|
1050 return; |
|
1051 } |
|
1052 }, |
|
1053 |
|
1054 /** |
|
1055 * The blur listener for the search container. |
|
1056 */ |
|
1057 _onBlur: function() { |
|
1058 this.clearViews(); |
|
1059 }, |
|
1060 |
|
1061 /** |
|
1062 * Called when a filtering key sequence was pressed. |
|
1063 * |
|
1064 * @param string aOperator |
|
1065 * The operator to use for filtering. |
|
1066 */ |
|
1067 _doSearch: function(aOperator = "", aText = "") { |
|
1068 this._searchbox.focus(); |
|
1069 this._searchbox.value = ""; // Need to clear value beforehand. Bug 779738. |
|
1070 |
|
1071 if (aText) { |
|
1072 this._searchbox.value = aOperator + aText; |
|
1073 return; |
|
1074 } |
|
1075 if (DebuggerView.editor.somethingSelected()) { |
|
1076 this._searchbox.value = aOperator + DebuggerView.editor.getSelection(); |
|
1077 return; |
|
1078 } |
|
1079 if (SEARCH_AUTOFILL.indexOf(aOperator) != -1) { |
|
1080 let cursor = DebuggerView.editor.getCursor(); |
|
1081 let content = DebuggerView.editor.getText(); |
|
1082 let location = DebuggerView.Sources.selectedValue; |
|
1083 let source = DebuggerController.Parser.get(content, location); |
|
1084 let identifier = source.getIdentifierAt({ line: cursor.line+1, column: cursor.ch }); |
|
1085 |
|
1086 if (identifier && identifier.name) { |
|
1087 this._searchbox.value = aOperator + identifier.name; |
|
1088 this._searchbox.select(); |
|
1089 this._searchbox.selectionStart += aOperator.length; |
|
1090 return; |
|
1091 } |
|
1092 } |
|
1093 this._searchbox.value = aOperator; |
|
1094 }, |
|
1095 |
|
1096 /** |
|
1097 * Called when the source location filter key sequence was pressed. |
|
1098 */ |
|
1099 _doFileSearch: function() { |
|
1100 this._doSearch(); |
|
1101 this._searchboxHelpPanel.openPopup(this._searchbox); |
|
1102 }, |
|
1103 |
|
1104 /** |
|
1105 * Called when the global search filter key sequence was pressed. |
|
1106 */ |
|
1107 _doGlobalSearch: function() { |
|
1108 this._doSearch(SEARCH_GLOBAL_FLAG); |
|
1109 this._searchboxHelpPanel.hidePopup(); |
|
1110 }, |
|
1111 |
|
1112 /** |
|
1113 * Called when the source function filter key sequence was pressed. |
|
1114 */ |
|
1115 _doFunctionSearch: function() { |
|
1116 this._doSearch(SEARCH_FUNCTION_FLAG); |
|
1117 this._searchboxHelpPanel.hidePopup(); |
|
1118 }, |
|
1119 |
|
1120 /** |
|
1121 * Called when the source token filter key sequence was pressed. |
|
1122 */ |
|
1123 _doTokenSearch: function() { |
|
1124 this._doSearch(SEARCH_TOKEN_FLAG); |
|
1125 this._searchboxHelpPanel.hidePopup(); |
|
1126 }, |
|
1127 |
|
1128 /** |
|
1129 * Called when the source line filter key sequence was pressed. |
|
1130 */ |
|
1131 _doLineSearch: function() { |
|
1132 this._doSearch(SEARCH_LINE_FLAG); |
|
1133 this._searchboxHelpPanel.hidePopup(); |
|
1134 }, |
|
1135 |
|
1136 /** |
|
1137 * Called when the variable search filter key sequence was pressed. |
|
1138 */ |
|
1139 _doVariableSearch: function() { |
|
1140 this._doSearch(SEARCH_VARIABLE_FLAG); |
|
1141 this._searchboxHelpPanel.hidePopup(); |
|
1142 }, |
|
1143 |
|
1144 /** |
|
1145 * Called when the variables focus key sequence was pressed. |
|
1146 */ |
|
1147 _doVariablesFocus: function() { |
|
1148 DebuggerView.showInstrumentsPane(); |
|
1149 DebuggerView.Variables.focusFirstVisibleItem(); |
|
1150 }, |
|
1151 |
|
1152 _searchbox: null, |
|
1153 _searchboxHelpPanel: null, |
|
1154 _globalOperatorButton: null, |
|
1155 _globalOperatorLabel: null, |
|
1156 _functionOperatorButton: null, |
|
1157 _functionOperatorLabel: null, |
|
1158 _tokenOperatorButton: null, |
|
1159 _tokenOperatorLabel: null, |
|
1160 _lineOperatorButton: null, |
|
1161 _lineOperatorLabel: null, |
|
1162 _variableOperatorButton: null, |
|
1163 _variableOperatorLabel: null, |
|
1164 _fileSearchKey: "", |
|
1165 _globalSearchKey: "", |
|
1166 _filteredFunctionsKey: "", |
|
1167 _tokenSearchKey: "", |
|
1168 _lineSearchKey: "", |
|
1169 _variableSearchKey: "", |
|
1170 }; |
|
1171 |
|
1172 /** |
|
1173 * Functions handling the filtered sources UI. |
|
1174 */ |
|
1175 function FilteredSourcesView() { |
|
1176 dumpn("FilteredSourcesView was instantiated"); |
|
1177 |
|
1178 this._onClick = this._onClick.bind(this); |
|
1179 this._onSelect = this._onSelect.bind(this); |
|
1180 } |
|
1181 |
|
1182 FilteredSourcesView.prototype = Heritage.extend(ResultsPanelContainer.prototype, { |
|
1183 /** |
|
1184 * Initialization function, called when the debugger is started. |
|
1185 */ |
|
1186 initialize: function() { |
|
1187 dumpn("Initializing the FilteredSourcesView"); |
|
1188 |
|
1189 this.anchor = document.getElementById("searchbox"); |
|
1190 this.widget.addEventListener("select", this._onSelect, false); |
|
1191 this.widget.addEventListener("click", this._onClick, false); |
|
1192 }, |
|
1193 |
|
1194 /** |
|
1195 * Destruction function, called when the debugger is closed. |
|
1196 */ |
|
1197 destroy: function() { |
|
1198 dumpn("Destroying the FilteredSourcesView"); |
|
1199 |
|
1200 this.widget.removeEventListener("select", this._onSelect, false); |
|
1201 this.widget.removeEventListener("click", this._onClick, false); |
|
1202 this.anchor = null; |
|
1203 }, |
|
1204 |
|
1205 /** |
|
1206 * Schedules searching for a source. |
|
1207 * |
|
1208 * @param string aToken |
|
1209 * The function to search for. |
|
1210 * @param number aWait |
|
1211 * The amount of milliseconds to wait until draining. |
|
1212 */ |
|
1213 scheduleSearch: function(aToken, aWait) { |
|
1214 // The amount of time to wait for the requests to settle. |
|
1215 let maxDelay = FILE_SEARCH_ACTION_MAX_DELAY; |
|
1216 let delay = aWait === undefined ? maxDelay / aToken.length : aWait; |
|
1217 |
|
1218 // Allow requests to settle down first. |
|
1219 setNamedTimeout("sources-search", delay, () => this._doSearch(aToken)); |
|
1220 }, |
|
1221 |
|
1222 /** |
|
1223 * Finds file matches in all the displayed sources. |
|
1224 * |
|
1225 * @param string aToken |
|
1226 * The string to search for. |
|
1227 */ |
|
1228 _doSearch: function(aToken, aStore = []) { |
|
1229 // Don't continue filtering if the searched token is an empty string. |
|
1230 // In contrast with function searching, in this case we don't want to |
|
1231 // show a list of all the files when no search token was supplied. |
|
1232 if (!aToken) { |
|
1233 return; |
|
1234 } |
|
1235 |
|
1236 for (let item of DebuggerView.Sources.items) { |
|
1237 let lowerCaseLabel = item.attachment.label.toLowerCase(); |
|
1238 let lowerCaseToken = aToken.toLowerCase(); |
|
1239 if (lowerCaseLabel.match(lowerCaseToken)) { |
|
1240 aStore.push(item); |
|
1241 } |
|
1242 |
|
1243 // Once the maximum allowed number of results is reached, proceed |
|
1244 // with building the UI immediately. |
|
1245 if (aStore.length >= RESULTS_PANEL_MAX_RESULTS) { |
|
1246 this._syncView(aStore); |
|
1247 return; |
|
1248 } |
|
1249 } |
|
1250 |
|
1251 // Couldn't reach the maximum allowed number of results, but that's ok, |
|
1252 // continue building the UI. |
|
1253 this._syncView(aStore); |
|
1254 }, |
|
1255 |
|
1256 /** |
|
1257 * Updates the list of sources displayed in this container. |
|
1258 * |
|
1259 * @param array aSearchResults |
|
1260 * The results array, containing search details for each source. |
|
1261 */ |
|
1262 _syncView: function(aSearchResults) { |
|
1263 // If there are no matches found, keep the popup hidden and avoid |
|
1264 // creating the view. |
|
1265 if (!aSearchResults.length) { |
|
1266 window.emit(EVENTS.FILE_SEARCH_MATCH_NOT_FOUND); |
|
1267 return; |
|
1268 } |
|
1269 |
|
1270 for (let item of aSearchResults) { |
|
1271 // Create the element node for the location item. |
|
1272 let itemView = this._createItemView( |
|
1273 SourceUtils.trimUrlLength(item.attachment.label), |
|
1274 SourceUtils.trimUrlLength(item.value, 0, "start") |
|
1275 ); |
|
1276 |
|
1277 // Append a location item to this container for each match. |
|
1278 this.push([itemView], { |
|
1279 index: -1, /* specifies on which position should the item be appended */ |
|
1280 attachment: { |
|
1281 url: item.value |
|
1282 } |
|
1283 }); |
|
1284 } |
|
1285 |
|
1286 // There's at least one item displayed in this container. Don't select it |
|
1287 // automatically if not forced (by tests) or in tandem with an operator. |
|
1288 if (this._autoSelectFirstItem || DebuggerView.Filtering.searchOperator) { |
|
1289 this.selectedIndex = 0; |
|
1290 } |
|
1291 this.hidden = false; |
|
1292 |
|
1293 // Signal that file search matches were found and displayed. |
|
1294 window.emit(EVENTS.FILE_SEARCH_MATCH_FOUND); |
|
1295 }, |
|
1296 |
|
1297 /** |
|
1298 * The click listener for this container. |
|
1299 */ |
|
1300 _onClick: function(e) { |
|
1301 let locationItem = this.getItemForElement(e.target); |
|
1302 if (locationItem) { |
|
1303 this.selectedItem = locationItem; |
|
1304 DebuggerView.Filtering.clearSearch(); |
|
1305 } |
|
1306 }, |
|
1307 |
|
1308 /** |
|
1309 * The select listener for this container. |
|
1310 * |
|
1311 * @param object aItem |
|
1312 * The item associated with the element to select. |
|
1313 */ |
|
1314 _onSelect: function({ detail: locationItem }) { |
|
1315 if (locationItem) { |
|
1316 DebuggerView.setEditorLocation(locationItem.attachment.url, undefined, { |
|
1317 noCaret: true, |
|
1318 noDebug: true |
|
1319 }); |
|
1320 } |
|
1321 } |
|
1322 }); |
|
1323 |
|
1324 /** |
|
1325 * Functions handling the function search UI. |
|
1326 */ |
|
1327 function FilteredFunctionsView() { |
|
1328 dumpn("FilteredFunctionsView was instantiated"); |
|
1329 |
|
1330 this._onClick = this._onClick.bind(this); |
|
1331 this._onSelect = this._onSelect.bind(this); |
|
1332 } |
|
1333 |
|
1334 FilteredFunctionsView.prototype = Heritage.extend(ResultsPanelContainer.prototype, { |
|
1335 /** |
|
1336 * Initialization function, called when the debugger is started. |
|
1337 */ |
|
1338 initialize: function() { |
|
1339 dumpn("Initializing the FilteredFunctionsView"); |
|
1340 |
|
1341 this.anchor = document.getElementById("searchbox"); |
|
1342 this.widget.addEventListener("select", this._onSelect, false); |
|
1343 this.widget.addEventListener("click", this._onClick, false); |
|
1344 }, |
|
1345 |
|
1346 /** |
|
1347 * Destruction function, called when the debugger is closed. |
|
1348 */ |
|
1349 destroy: function() { |
|
1350 dumpn("Destroying the FilteredFunctionsView"); |
|
1351 |
|
1352 this.widget.removeEventListener("select", this._onSelect, false); |
|
1353 this.widget.removeEventListener("click", this._onClick, false); |
|
1354 this.anchor = null; |
|
1355 }, |
|
1356 |
|
1357 /** |
|
1358 * Schedules searching for a function in all of the sources. |
|
1359 * |
|
1360 * @param string aToken |
|
1361 * The function to search for. |
|
1362 * @param number aWait |
|
1363 * The amount of milliseconds to wait until draining. |
|
1364 */ |
|
1365 scheduleSearch: function(aToken, aWait) { |
|
1366 // The amount of time to wait for the requests to settle. |
|
1367 let maxDelay = FUNCTION_SEARCH_ACTION_MAX_DELAY; |
|
1368 let delay = aWait === undefined ? maxDelay / aToken.length : aWait; |
|
1369 |
|
1370 // Allow requests to settle down first. |
|
1371 setNamedTimeout("function-search", delay, () => { |
|
1372 // Start fetching as many sources as possible, then perform the search. |
|
1373 let urls = DebuggerView.Sources.values; |
|
1374 let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(urls); |
|
1375 sourcesFetched.then(aSources => this._doSearch(aToken, aSources)); |
|
1376 }); |
|
1377 }, |
|
1378 |
|
1379 /** |
|
1380 * Finds function matches in all the sources stored in the cache, and groups |
|
1381 * them by location and line number. |
|
1382 * |
|
1383 * @param string aToken |
|
1384 * The string to search for. |
|
1385 * @param array aSources |
|
1386 * An array of [url, text] tuples for each source. |
|
1387 */ |
|
1388 _doSearch: function(aToken, aSources, aStore = []) { |
|
1389 // Continue parsing even if the searched token is an empty string, to |
|
1390 // cache the syntax tree nodes generated by the reflection API. |
|
1391 |
|
1392 // Make sure the currently displayed source is parsed first. Once the |
|
1393 // maximum allowed number of results are found, parsing will be halted. |
|
1394 let currentUrl = DebuggerView.Sources.selectedValue; |
|
1395 let currentSource = aSources.filter(([sourceUrl]) => sourceUrl == currentUrl)[0]; |
|
1396 aSources.splice(aSources.indexOf(currentSource), 1); |
|
1397 aSources.unshift(currentSource); |
|
1398 |
|
1399 // If not searching for a specific function, only parse the displayed source, |
|
1400 // which is now the first item in the sources array. |
|
1401 if (!aToken) { |
|
1402 aSources.splice(1); |
|
1403 } |
|
1404 |
|
1405 for (let [location, contents] of aSources) { |
|
1406 let parsedSource = DebuggerController.Parser.get(contents, location); |
|
1407 let sourceResults = parsedSource.getNamedFunctionDefinitions(aToken); |
|
1408 |
|
1409 for (let scriptResult of sourceResults) { |
|
1410 for (let parseResult of scriptResult) { |
|
1411 aStore.push({ |
|
1412 sourceUrl: scriptResult.sourceUrl, |
|
1413 scriptOffset: scriptResult.scriptOffset, |
|
1414 functionName: parseResult.functionName, |
|
1415 functionLocation: parseResult.functionLocation, |
|
1416 inferredName: parseResult.inferredName, |
|
1417 inferredChain: parseResult.inferredChain, |
|
1418 inferredLocation: parseResult.inferredLocation |
|
1419 }); |
|
1420 |
|
1421 // Once the maximum allowed number of results is reached, proceed |
|
1422 // with building the UI immediately. |
|
1423 if (aStore.length >= RESULTS_PANEL_MAX_RESULTS) { |
|
1424 this._syncView(aStore); |
|
1425 return; |
|
1426 } |
|
1427 } |
|
1428 } |
|
1429 } |
|
1430 |
|
1431 // Couldn't reach the maximum allowed number of results, but that's ok, |
|
1432 // continue building the UI. |
|
1433 this._syncView(aStore); |
|
1434 }, |
|
1435 |
|
1436 /** |
|
1437 * Updates the list of functions displayed in this container. |
|
1438 * |
|
1439 * @param array aSearchResults |
|
1440 * The results array, containing search details for each source. |
|
1441 */ |
|
1442 _syncView: function(aSearchResults) { |
|
1443 // If there are no matches found, keep the popup hidden and avoid |
|
1444 // creating the view. |
|
1445 if (!aSearchResults.length) { |
|
1446 window.emit(EVENTS.FUNCTION_SEARCH_MATCH_NOT_FOUND); |
|
1447 return; |
|
1448 } |
|
1449 |
|
1450 for (let item of aSearchResults) { |
|
1451 // Some function expressions don't necessarily have a name, but the |
|
1452 // parser provides us with an inferred name from an enclosing |
|
1453 // VariableDeclarator, AssignmentExpression, ObjectExpression node. |
|
1454 if (item.functionName && item.inferredName && |
|
1455 item.functionName != item.inferredName) { |
|
1456 let s = " " + L10N.getStr("functionSearchSeparatorLabel") + " "; |
|
1457 item.displayedName = item.inferredName + s + item.functionName; |
|
1458 } |
|
1459 // The function doesn't have an explicit name, but it could be inferred. |
|
1460 else if (item.inferredName) { |
|
1461 item.displayedName = item.inferredName; |
|
1462 } |
|
1463 // The function only has an explicit name. |
|
1464 else { |
|
1465 item.displayedName = item.functionName; |
|
1466 } |
|
1467 |
|
1468 // Some function expressions have unexpected bounds, since they may not |
|
1469 // necessarily have an associated name defining them. |
|
1470 if (item.inferredLocation) { |
|
1471 item.actualLocation = item.inferredLocation; |
|
1472 } else { |
|
1473 item.actualLocation = item.functionLocation; |
|
1474 } |
|
1475 |
|
1476 // Create the element node for the function item. |
|
1477 let itemView = this._createItemView( |
|
1478 SourceUtils.trimUrlLength(item.displayedName + "()"), |
|
1479 SourceUtils.trimUrlLength(item.sourceUrl, 0, "start"), |
|
1480 (item.inferredChain || []).join(".") |
|
1481 ); |
|
1482 |
|
1483 // Append a function item to this container for each match. |
|
1484 this.push([itemView], { |
|
1485 index: -1, /* specifies on which position should the item be appended */ |
|
1486 attachment: item |
|
1487 }); |
|
1488 } |
|
1489 |
|
1490 // There's at least one item displayed in this container. Don't select it |
|
1491 // automatically if not forced (by tests). |
|
1492 if (this._autoSelectFirstItem) { |
|
1493 this.selectedIndex = 0; |
|
1494 } |
|
1495 this.hidden = false; |
|
1496 |
|
1497 // Signal that function search matches were found and displayed. |
|
1498 window.emit(EVENTS.FUNCTION_SEARCH_MATCH_FOUND); |
|
1499 }, |
|
1500 |
|
1501 /** |
|
1502 * The click listener for this container. |
|
1503 */ |
|
1504 _onClick: function(e) { |
|
1505 let functionItem = this.getItemForElement(e.target); |
|
1506 if (functionItem) { |
|
1507 this.selectedItem = functionItem; |
|
1508 DebuggerView.Filtering.clearSearch(); |
|
1509 } |
|
1510 }, |
|
1511 |
|
1512 /** |
|
1513 * The select listener for this container. |
|
1514 */ |
|
1515 _onSelect: function({ detail: functionItem }) { |
|
1516 if (functionItem) { |
|
1517 let sourceUrl = functionItem.attachment.sourceUrl; |
|
1518 let scriptOffset = functionItem.attachment.scriptOffset; |
|
1519 let actualLocation = functionItem.attachment.actualLocation; |
|
1520 |
|
1521 DebuggerView.setEditorLocation(sourceUrl, actualLocation.start.line, { |
|
1522 charOffset: scriptOffset, |
|
1523 columnOffset: actualLocation.start.column, |
|
1524 align: "center", |
|
1525 noDebug: true |
|
1526 }); |
|
1527 } |
|
1528 }, |
|
1529 |
|
1530 _searchTimeout: null, |
|
1531 _searchFunction: null, |
|
1532 _searchedToken: "" |
|
1533 }); |
|
1534 |
|
1535 /** |
|
1536 * Preliminary setup for the DebuggerView object. |
|
1537 */ |
|
1538 DebuggerView.Toolbar = new ToolbarView(); |
|
1539 DebuggerView.Options = new OptionsView(); |
|
1540 DebuggerView.Filtering = new FilterView(); |
|
1541 DebuggerView.FilteredSources = new FilteredSourcesView(); |
|
1542 DebuggerView.FilteredFunctions = new FilteredFunctionsView(); |
|
1543 DebuggerView.StackFrames = new StackFramesView(); |
|
1544 DebuggerView.StackFramesClassicList = new StackFramesClassicListView(); |