Web Inspector: Modify the type profiler runtime protocol to transfer some computation...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / SourceCodeTextEditor.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.SourceCodeTextEditor = function(sourceCode)
27 {
28     console.assert(sourceCode instanceof WebInspector.SourceCode);
29
30     this._sourceCode = sourceCode;
31     this._breakpointMap = {};
32     this._issuesLineNumberMap = {};
33     this._contentPopulated = false;
34     this._invalidLineNumbers = {0: true};
35     this._ignoreContentDidChange = 0;
36
37     WebInspector.TextEditor.call(this, null, null, this);
38
39     this._typeTokenAnnotator = null;
40     this._typeTokenScrollHandler = null;
41
42     // FIXME: Currently this just jumps between resources and related source map resources. It doesn't "jump to symbol" yet.
43     this._updateTokenTrackingControllerState();
44
45     this.element.classList.add(WebInspector.SourceCodeTextEditor.StyleClassName);
46
47     if (this._supportsDebugging) {
48         WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.DisabledStateDidChange, this._breakpointStatusDidChange, this);
49         WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.AutoContinueDidChange, this._breakpointStatusDidChange, this);
50         WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.ResolvedStateDidChange, this._breakpointStatusDidChange, this);
51         WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.LocationDidChange, this._updateBreakpointLocation, this);
52
53         WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.BreakpointsEnabledDidChange, this._breakpointsEnabledDidChange, this);
54         WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.BreakpointAdded, this._breakpointAdded, this);
55         WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.BreakpointRemoved, this._breakpointRemoved, this);
56         WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, this._activeCallFrameDidChange, this);
57
58         WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.Paused, this._debuggerDidPause, this);
59         WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.Resumed, this._debuggerDidResume, this);
60         if (WebInspector.debuggerManager.activeCallFrame)
61             this._debuggerDidPause();
62
63         this._activeCallFrameDidChange();
64     }
65
66     WebInspector.issueManager.addEventListener(WebInspector.IssueManager.Event.IssueWasAdded, this._issueWasAdded, this);
67
68     if (this._sourceCode instanceof WebInspector.SourceMapResource || this._sourceCode.sourceMaps.length > 0)
69         WebInspector.notifications.addEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateTokenTrackingControllerState, this);
70     else
71         this._sourceCode.addEventListener(WebInspector.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this);
72
73     sourceCode.requestContent(this._contentAvailable.bind(this));
74
75     // FIXME: Cmd+L shorcut doesn't actually work.
76     new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Command, "L", this.showGoToLineDialog.bind(this), this.element);
77     new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Control, "G", this.showGoToLineDialog.bind(this), this.element);
78 };
79
80 WebInspector.Object.addConstructorFunctions(WebInspector.SourceCodeTextEditor);
81
82 WebInspector.SourceCodeTextEditor.StyleClassName = "source-code";
83 WebInspector.SourceCodeTextEditor.LineErrorStyleClassName = "error";
84 WebInspector.SourceCodeTextEditor.LineWarningStyleClassName = "warning";
85 WebInspector.SourceCodeTextEditor.PopoverDebuggerContentStyleClassName = "debugger-popover-content";
86 WebInspector.SourceCodeTextEditor.HoveredExpressionHighlightStyleClassName = "hovered-expression-highlight";
87 WebInspector.SourceCodeTextEditor.DurationToMouseOverTokenToMakeHoveredToken = 500;
88 WebInspector.SourceCodeTextEditor.DurationToMouseOutOfHoveredTokenToRelease = 1000;
89 WebInspector.SourceCodeTextEditor.DurationToUpdateTypeTokensAfterScrolling = 100;
90
91 WebInspector.SourceCodeTextEditor.AutoFormatMinimumLineLength = 500;
92
93 WebInspector.SourceCodeTextEditor.Event = {
94     ContentWillPopulate: "source-code-text-editor-content-will-populate",
95     ContentDidPopulate: "source-code-text-editor-content-did-populate"
96 };
97
98 WebInspector.SourceCodeTextEditor.prototype = {
99     constructor: WebInspector.SourceCodeTextEditor,
100
101     // Public
102
103     get sourceCode()
104     {
105         return this._sourceCode;
106     },
107
108     shown: function()
109     {
110         WebInspector.TextEditor.prototype.shown.call(this);
111
112         if (this._typeTokenAnnotator)
113             this._typeTokenAnnotator.resume();
114     },
115
116     hidden: function()
117     {
118         WebInspector.TextEditor.prototype.hidden.call(this);
119
120         this.tokenTrackingController.removeHighlightedRange();
121
122         this._dismissPopover();
123         
124         this._dismissEditingController(true);
125
126         if (this._typeTokenAnnotator)
127             this._typeTokenAnnotator.pause();
128     },
129
130     close: function()
131     {
132         if (this._supportsDebugging) {
133             WebInspector.Breakpoint.removeEventListener(WebInspector.Breakpoint.Event.DisabledStateDidChange, this._breakpointStatusDidChange, this);
134             WebInspector.Breakpoint.removeEventListener(WebInspector.Breakpoint.Event.AutoContinueDidChange, this._breakpointStatusDidChange, this);
135             WebInspector.Breakpoint.removeEventListener(WebInspector.Breakpoint.Event.ResolvedStateDidChange, this._breakpointStatusDidChange, this);
136             WebInspector.Breakpoint.removeEventListener(WebInspector.Breakpoint.Event.LocationDidChange, this._updateBreakpointLocation, this);
137
138             WebInspector.debuggerManager.removeEventListener(WebInspector.DebuggerManager.Event.BreakpointsEnabledDidChange, this._breakpointsEnabledDidChange, this);
139             WebInspector.debuggerManager.removeEventListener(WebInspector.DebuggerManager.Event.BreakpointAdded, this._breakpointAdded, this);
140             WebInspector.debuggerManager.removeEventListener(WebInspector.DebuggerManager.Event.BreakpointRemoved, this._breakpointRemoved, this);
141             WebInspector.debuggerManager.removeEventListener(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, this._activeCallFrameDidChange, this);
142
143             if (this._activeCallFrameSourceCodeLocation) {
144                 this._activeCallFrameSourceCodeLocation.removeEventListener(WebInspector.SourceCodeLocation.Event.LocationChanged, this._activeCallFrameSourceCodeLocationChanged, this);
145                 delete this._activeCallFrameSourceCodeLocation;
146             }
147         }
148
149         WebInspector.issueManager.removeEventListener(WebInspector.IssueManager.Event.IssueWasAdded, this._issueWasAdded, this);
150
151         WebInspector.notifications.removeEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateTokenTrackingControllerState, this);
152         this._sourceCode.removeEventListener(WebInspector.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this);
153     },
154
155     canBeFormatted: function()
156     {
157         // Currently we assume that source map resources are formatted how the author wants it.
158         // We could allow source map resources to be formatted, we would then need to make
159         // SourceCodeLocation watch listen for mappedResource's formatting changes, and keep
160         // a formatted location alongside the regular mapped location.
161         if (this._sourceCode instanceof WebInspector.SourceMapResource)
162             return false;
163
164         return WebInspector.TextEditor.prototype.canBeFormatted.call(this);
165     },
166
167     canShowTypeAnnotations: function()
168     {
169         return !!this._typeTokenAnnotator;
170     },
171
172     customPerformSearch: function(query)
173     {
174         function searchResultCallback(error, matches)
175         {
176             // Bail if the query changed since we started.
177             if (this.currentSearchQuery !== query)
178                 return;
179
180             if (error || !matches || !matches.length) {
181                 // Report zero matches.
182                 this.dispatchEventToListeners(WebInspector.TextEditor.Event.NumberOfSearchResultsDidChange);
183                 return;
184             }
185
186             var queryRegex = new RegExp(query.escapeForRegExp(), "gi");
187             var searchResults = [];
188
189             for (var i = 0; i < matches.length; ++i) {
190                 var matchLineNumber = matches[i].lineNumber;
191                 var line = this.line(matchLineNumber);
192
193                 // Reset the last index to reuse the regex on a new line.
194                 queryRegex.lastIndex = 0;
195
196                 // Search the line and mark the ranges.
197                 var lineMatch = null;
198                 while (queryRegex.lastIndex + query.length <= line.length && (lineMatch = queryRegex.exec(line))) {
199                     var resultTextRange = new WebInspector.TextRange(matchLineNumber, lineMatch.index, matchLineNumber, queryRegex.lastIndex);
200                     searchResults.push(resultTextRange);
201                 }
202             }
203
204             this.addSearchResults(searchResults);
205
206             this.dispatchEventToListeners(WebInspector.TextEditor.Event.NumberOfSearchResultsDidChange);
207         }
208
209         if (this._sourceCode instanceof WebInspector.SourceMapResource)
210             return false;
211
212         if (this._sourceCode instanceof WebInspector.Resource)
213             PageAgent.searchInResource(this._sourceCode.parentFrame.id, this._sourceCode.url, query, false, false, searchResultCallback.bind(this));
214         else if (this._sourceCode instanceof WebInspector.Script)
215             DebuggerAgent.searchInContent(this._sourceCode.id, query, false, false, searchResultCallback.bind(this));
216         return true;
217     },
218
219     showGoToLineDialog: function()
220     {
221         if (!this._goToLineDialog) {
222             this._goToLineDialog = new WebInspector.GoToLineDialog;
223             this._goToLineDialog.delegate = this;
224         }
225
226         this._goToLineDialog.present(this.element);
227     },
228
229     isGoToLineDialogValueValid: function(goToLineDialog, lineNumber)
230     {
231         return !isNaN(lineNumber) && lineNumber > 0 && lineNumber <= this.lineCount;
232     },
233
234     goToLineDialogValueWasValidated: function(goToLineDialog, lineNumber)
235     {
236         var position = new WebInspector.SourceCodePosition(lineNumber - 1, 0);
237         var range = new WebInspector.TextRange(lineNumber - 1, 0, lineNumber, 0);
238         this.revealPosition(position, range, false, true);
239     },
240
241     goToLineDialogWasDismissed: function()
242     {
243         this.focus();
244     },
245
246     contentDidChange: function(replacedRanges, newRanges)
247     {
248         WebInspector.TextEditor.prototype.contentDidChange.call(this, replacedRanges, newRanges);
249
250         if (this._ignoreContentDidChange > 0)
251             return;
252
253         for (var range of newRanges)
254             this._updateEditableMarkers(range);
255     },
256
257     toggleTypeAnnotations: function()
258     {
259         if (!this._typeTokenAnnotator)
260             return false;
261
262         var isActivated = this._typeTokenAnnotator.toggleTypeAnnotations();
263         if (isActivated) {
264             RuntimeAgent.enableTypeProfiler();
265             this._enableScrollEventsForTypeTokenAnnotator();
266         } else {
267             // We disable type profiling when exiting the inspector, so no need to call it here. If we were
268             // to call it here, we would have JavaScriptCore compile out all the necessary type profiling information,
269             // so if a user were to quickly press then unpress the button, we wouldn't be able to re-show type information.
270             this._disableScrollEventsForTypeTokenAnnotator();
271         }
272         WebInspector.showJavaScriptTypeInformationSetting.value = isActivated;
273
274         this._updateTokenTrackingControllerState();
275         return isActivated;
276     },
277
278     showPopoverForTypes: function(types, bounds, title)
279     {
280         var content = document.createElement("div");
281         content.className = "object expandable";
282
283         var titleElement = document.createElement("div");
284         titleElement.className = "title";
285         titleElement.textContent = title;
286         content.appendChild(titleElement);
287
288         var section = new WebInspector.TypePropertiesSection(types);
289         section.expanded = true;
290         section.element.classList.add("body");
291         content.appendChild(section.element);
292
293         this._showPopover(content, bounds);
294     },
295
296     // Protected
297
298     prettyPrint: function(pretty)
299     {
300         WebInspector.TextEditor.prototype.prettyPrint.call(this, pretty);
301
302         if (this._typeTokenAnnotator && this._typeTokenAnnotator.isActive)
303             this._typeTokenAnnotator.reset();
304     },
305
306     // Private
307
308     _unformattedLineInfoForEditorLineInfo: function(lineInfo)
309     {
310         if (this.formatterSourceMap)
311             return this.formatterSourceMap.formattedToOriginal(lineInfo.lineNumber, lineInfo.columnNumber);
312         return lineInfo;
313     },
314
315     _sourceCodeLocationForEditorPosition: function(position)
316     {
317         var lineInfo = {lineNumber: position.line, columnNumber: position.ch};
318         var unformattedLineInfo = this._unformattedLineInfoForEditorLineInfo(lineInfo);
319         return this.sourceCode.createSourceCodeLocation(unformattedLineInfo.lineNumber, unformattedLineInfo.columnNumber);
320     },
321
322     _editorLineInfoForSourceCodeLocation: function(sourceCodeLocation)
323     {
324         if (this._sourceCode instanceof WebInspector.SourceMapResource)
325             return {lineNumber: sourceCodeLocation.displayLineNumber, columnNumber: sourceCodeLocation.displayColumnNumber};
326         return {lineNumber: sourceCodeLocation.formattedLineNumber, columnNumber: sourceCodeLocation.formattedColumnNumber};
327     },
328
329     _breakpointForEditorLineInfo: function(lineInfo)
330     {
331         if (!this._breakpointMap[lineInfo.lineNumber])
332             return null;
333         return this._breakpointMap[lineInfo.lineNumber][lineInfo.columnNumber];
334     },
335
336     _addBreakpointWithEditorLineInfo: function(breakpoint, lineInfo)
337     {
338         if (!this._breakpointMap[lineInfo.lineNumber])
339             this._breakpointMap[lineInfo.lineNumber] = {};
340
341         this._breakpointMap[lineInfo.lineNumber][lineInfo.columnNumber] = breakpoint;
342     },
343
344     _removeBreakpointWithEditorLineInfo: function(breakpoint, lineInfo)
345     {
346         console.assert(breakpoint === this._breakpointMap[lineInfo.lineNumber][lineInfo.columnNumber]);
347
348         delete this._breakpointMap[lineInfo.lineNumber][lineInfo.columnNumber];
349
350         if (isEmptyObject(this._breakpointMap[lineInfo.lineNumber]))
351             delete this._breakpointMap[lineInfo.lineNumber];
352     },
353
354     _contentWillPopulate: function(content)
355     {
356         this.dispatchEventToListeners(WebInspector.SourceCodeTextEditor.Event.ContentWillPopulate);
357
358         // We only do the rest of this work before the first populate.
359         if (this._contentPopulated)
360             return;
361
362         if (this._supportsDebugging) {
363             this._breakpointMap = {};
364
365             var breakpoints = WebInspector.debuggerManager.breakpointsForSourceCode(this._sourceCode);
366             for (var i = 0; i < breakpoints.length; ++i) {
367                 var breakpoint = breakpoints[i];
368                 console.assert(this._matchesBreakpoint(breakpoint));
369                 var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation);
370                 this._addBreakpointWithEditorLineInfo(breakpoint, lineInfo);
371                 this.setBreakpointInfoForLineAndColumn(lineInfo.lineNumber, lineInfo.columnNumber, this._breakpointInfoForBreakpoint(breakpoint));
372             }
373         }
374
375         if (this._sourceCode instanceof WebInspector.Resource)
376             this.mimeType = this._sourceCode.syntheticMIMEType;
377         else if (this._sourceCode instanceof WebInspector.Script)
378             this.mimeType = "text/javascript";
379
380         // Automatically format the content if it looks minified and it can be formatted.
381         console.assert(!this.formatted);
382         if (this.canBeFormatted()) {
383             var lastNewlineIndex = 0;
384             while (true) {
385                 var nextNewlineIndex = content.indexOf("\n", lastNewlineIndex);
386                 if (nextNewlineIndex === -1) {
387                     if (content.length - lastNewlineIndex > WebInspector.SourceCodeTextEditor.AutoFormatMinimumLineLength)
388                         this.autoFormat = true;
389                     break;
390                 }
391
392                 if (nextNewlineIndex - lastNewlineIndex > WebInspector.SourceCodeTextEditor.AutoFormatMinimumLineLength) {
393                     this.autoFormat = true;
394                     break;
395                 }
396
397                 lastNewlineIndex = nextNewlineIndex + 1;
398             }
399         }
400     },
401
402     _contentDidPopulate: function()
403     {
404         this._contentPopulated = true;
405
406         this.dispatchEventToListeners(WebInspector.SourceCodeTextEditor.Event.ContentDidPopulate);
407
408         // We add the issues each time content is populated. This is needed because lines might not exist
409         // if we tried added them before when the full content wasn't avaiable. (When populating with
410         // partial script content this can be called multiple times.)
411
412         this._issuesLineNumberMap = {};
413
414         var issues = WebInspector.issueManager.issuesForSourceCode(this._sourceCode);
415         for (var i = 0; i < issues.length; ++i) {
416             var issue = issues[i];
417             console.assert(this._matchesIssue(issue));
418             this._addIssue(issue);
419         }
420
421         this._updateEditableMarkers();
422     },
423
424     _populateWithContent: function(content)
425     {
426         content = content || "";
427
428         this._contentWillPopulate(content);
429         this.string = content;
430
431         this._makeTypeTokenAnnotator();
432
433         this._contentDidPopulate();
434     },
435
436     _contentAvailable: function(sourceCode, content, base64Encoded)
437     {
438         console.assert(sourceCode === this._sourceCode);
439         console.assert(!base64Encoded);
440
441         // Abort if the full content populated while waiting for this async callback.
442         if (this._fullContentPopulated)
443             return;
444
445         this._fullContentPopulated = true;
446         this._invalidLineNumbers = {};
447
448         this._populateWithContent(content);
449     },
450
451     _breakpointStatusDidChange: function(event)
452     {
453         this._updateBreakpointStatus(event.target);
454     },
455
456     _breakpointsEnabledDidChange: function()
457     {
458         console.assert(this._supportsDebugging);
459
460         var breakpoints = WebInspector.debuggerManager.breakpointsForSourceCode(this._sourceCode);
461         for (var breakpoint of breakpoints)
462             this._updateBreakpointStatus(breakpoint);
463     },
464
465     _updateBreakpointStatus: function(breakpoint)
466     {
467         console.assert(this._supportsDebugging);
468
469         if (!this._contentPopulated)
470             return;
471
472         if (!this._matchesBreakpoint(breakpoint))
473             return;
474
475         var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation);
476         this.setBreakpointInfoForLineAndColumn(lineInfo.lineNumber, lineInfo.columnNumber, this._breakpointInfoForBreakpoint(breakpoint));
477     },
478
479     _updateBreakpointLocation: function(event)
480     {
481         console.assert(this._supportsDebugging);
482
483         if (!this._contentPopulated)
484             return;
485
486         var breakpoint = event.target;
487         if (!this._matchesBreakpoint(breakpoint))
488             return;
489
490         if (this._ignoreAllBreakpointLocationUpdates)
491             return;
492
493         if (breakpoint === this._ignoreLocationUpdateBreakpoint)
494             return;
495
496         var sourceCodeLocation = breakpoint.sourceCodeLocation;
497
498         if (this._sourceCode instanceof WebInspector.SourceMapResource) {
499             // Update our breakpoint location if the display location changed.
500             if (sourceCodeLocation.displaySourceCode !== this._sourceCode)
501                 return;
502             var oldLineInfo = {lineNumber: event.data.oldDisplayLineNumber, columnNumber: event.data.oldDisplayColumnNumber};
503             var newLineInfo = {lineNumber: sourceCodeLocation.displayLineNumber, columnNumber: sourceCodeLocation.displayColumnNumber};
504         } else {
505             // Update our breakpoint location if the original location changed.
506             if (sourceCodeLocation.sourceCode !== this._sourceCode)
507                 return;
508             var oldLineInfo = {lineNumber: event.data.oldFormattedLineNumber, columnNumber: event.data.oldFormattedColumnNumber};
509             var newLineInfo = {lineNumber: sourceCodeLocation.formattedLineNumber, columnNumber: sourceCodeLocation.formattedColumnNumber};
510         }
511
512         var existingBreakpoint = this._breakpointForEditorLineInfo(oldLineInfo);
513         if (!existingBreakpoint)
514             return;
515
516         console.assert(breakpoint === existingBreakpoint);
517
518         this.setBreakpointInfoForLineAndColumn(oldLineInfo.lineNumber, oldLineInfo.columnNumber, null);
519         this.setBreakpointInfoForLineAndColumn(newLineInfo.lineNumber, newLineInfo.columnNumber, this._breakpointInfoForBreakpoint(breakpoint));
520
521         this._removeBreakpointWithEditorLineInfo(breakpoint, oldLineInfo);
522         this._addBreakpointWithEditorLineInfo(breakpoint, newLineInfo);
523     },
524
525     _breakpointAdded: function(event)
526     {
527         console.assert(this._supportsDebugging);
528
529         if (!this._contentPopulated)
530             return;
531
532         var breakpoint = event.data.breakpoint;
533         if (!this._matchesBreakpoint(breakpoint))
534             return;
535
536         if (breakpoint === this._ignoreBreakpointAddedBreakpoint)
537             return;
538
539         var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation);
540         this._addBreakpointWithEditorLineInfo(breakpoint, lineInfo);
541         this.setBreakpointInfoForLineAndColumn(lineInfo.lineNumber, lineInfo.columnNumber, this._breakpointInfoForBreakpoint(breakpoint));
542     },
543
544     _breakpointRemoved: function(event)
545     {
546         console.assert(this._supportsDebugging);
547
548         if (!this._contentPopulated)
549             return;
550
551         var breakpoint = event.data.breakpoint;
552         if (!this._matchesBreakpoint(breakpoint))
553             return;
554
555         if (breakpoint === this._ignoreBreakpointRemovedBreakpoint)
556             return;
557
558         var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation);
559         this._removeBreakpointWithEditorLineInfo(breakpoint, lineInfo);
560         this.setBreakpointInfoForLineAndColumn(lineInfo.lineNumber, lineInfo.columnNumber, null);
561     },
562
563     _activeCallFrameDidChange: function()
564     {
565         console.assert(this._supportsDebugging);
566
567         if (this._activeCallFrameSourceCodeLocation) {
568             this._activeCallFrameSourceCodeLocation.removeEventListener(WebInspector.SourceCodeLocation.Event.LocationChanged, this._activeCallFrameSourceCodeLocationChanged, this);
569             delete this._activeCallFrameSourceCodeLocation;
570         }
571
572         var activeCallFrame = WebInspector.debuggerManager.activeCallFrame;
573         if (!activeCallFrame || !this._matchesSourceCodeLocation(activeCallFrame.sourceCodeLocation)) {
574             this.executionLineNumber = NaN;
575             this.executionColumnNumber = NaN;
576             return;
577         }
578
579         this._dismissPopover();
580
581         this._activeCallFrameSourceCodeLocation = activeCallFrame.sourceCodeLocation;
582         this._activeCallFrameSourceCodeLocation.addEventListener(WebInspector.SourceCodeLocation.Event.LocationChanged, this._activeCallFrameSourceCodeLocationChanged, this);
583
584         // Don't return early if the line number didn't change. The execution state still
585         // could have changed (e.g. continuing in a loop with a breakpoint inside).
586
587         var lineInfo = this._editorLineInfoForSourceCodeLocation(activeCallFrame.sourceCodeLocation);
588         this.executionLineNumber = lineInfo.lineNumber;
589         this.executionColumnNumber = lineInfo.columnNumber;
590
591         // If we have full content or this source code isn't a Resource we can return early.
592         // Script source code populates from the request started in the constructor.
593         if (this._fullContentPopulated || !(this._sourceCode instanceof WebInspector.Resource) || this._requestingScriptContent)
594             return;
595
596         // Since we are paused in the debugger we need to show some content, and since the Resource
597         // content hasn't populated yet we need to populate with content from the Scripts by URL.
598         // Document resources will attempt to populate the scripts as inline (in <script> tags.)
599         // Other resources are assumed to be full scripts (JavaScript resources).
600         if (this._sourceCode.type === WebInspector.Resource.Type.Document)
601             this._populateWithInlineScriptContent();
602         else
603             this._populateWithScriptContent();
604     },
605
606     _activeCallFrameSourceCodeLocationChanged: function(event)
607     {
608         console.assert(!isNaN(this.executionLineNumber));
609         if (isNaN(this.executionLineNumber))
610             return;
611
612         console.assert(WebInspector.debuggerManager.activeCallFrame);
613         console.assert(this._activeCallFrameSourceCodeLocation === WebInspector.debuggerManager.activeCallFrame.sourceCodeLocation);
614
615         var lineInfo = this._editorLineInfoForSourceCodeLocation(this._activeCallFrameSourceCodeLocation);
616         this.executionLineNumber = lineInfo.lineNumber;
617         this.executionColumnNumber = lineInfo.columnNumber;
618     },
619
620     _populateWithInlineScriptContent: function()
621     {
622         console.assert(this._sourceCode instanceof WebInspector.Resource);
623         console.assert(!this._fullContentPopulated);
624         console.assert(!this._requestingScriptContent);
625
626         var scripts = this._sourceCode.scripts;
627         console.assert(scripts.length);
628         if (!scripts.length)
629             return;
630
631         var pendingRequestCount = scripts.length;
632
633         // If the number of scripts hasn't change since the last populate, then there is nothing to do.
634         if (this._inlineScriptContentPopulated === pendingRequestCount)
635             return;
636
637         this._inlineScriptContentPopulated = pendingRequestCount;
638
639         function scriptContentAvailable(error, content)
640         {
641             // Return early if we are still waiting for content from other scripts.
642             if (--pendingRequestCount)
643                 return;
644
645             delete this._requestingScriptContent;
646
647             // Abort if the full content populated while waiting for these async callbacks.
648             if (this._fullContentPopulated)
649                 return;
650
651             const scriptOpenTag = "<script>";
652             const scriptCloseTag = "</script>";
653
654             var content = "";
655             var lineNumber = 0;
656             var columnNumber = 0;
657
658             this._invalidLineNumbers = {};
659
660             for (var i = 0; i < scripts.length; ++i) {
661                 // Fill the line gap with newline characters.
662                 for (var newLinesCount = scripts[i].range.startLine - lineNumber; newLinesCount > 0; --newLinesCount) {
663                     if (!columnNumber)
664                         this._invalidLineNumbers[scripts[i].range.startLine - newLinesCount] = true;
665                     columnNumber = 0;
666                     content += "\n";
667                 }
668
669                 // Fill the column gap with space characters.
670                 for (var spacesCount = scripts[i].range.startColumn - columnNumber - scriptOpenTag.length; spacesCount > 0; --spacesCount)
671                     content += " ";
672
673                 // Add script tags and content.
674                 content += scriptOpenTag;
675                 content += scripts[i].content;
676                 content += scriptCloseTag;
677
678                 lineNumber = scripts[i].range.endLine;
679                 columnNumber = scripts[i].range.endColumn + scriptCloseTag.length;
680             }
681
682             this._populateWithContent(content);
683         }
684
685         this._requestingScriptContent = true;
686
687         var boundScriptContentAvailable = scriptContentAvailable.bind(this);
688         for (var i = 0; i < scripts.length; ++i)
689             scripts[i].requestContent(boundScriptContentAvailable);
690     },
691
692     _populateWithScriptContent: function()
693     {
694         console.assert(this._sourceCode instanceof WebInspector.Resource);
695         console.assert(!this._fullContentPopulated);
696         console.assert(!this._requestingScriptContent);
697
698         // We can assume this resource only has one script that starts at line/column 0.
699         var scripts = this._sourceCode.scripts;
700         console.assert(scripts.length === 1);
701         if (!scripts.length)
702             return;
703
704         console.assert(scripts[0].range.startLine === 0);
705         console.assert(scripts[0].range.startColumn === 0);
706
707         function scriptContentAvailable(error, content)
708         {
709             delete this._requestingScriptContent;
710
711             // Abort if the full content populated while waiting for this async callback.
712             if (this._fullContentPopulated)
713                 return;
714
715             // This is the full content.
716             this._fullContentPopulated = true;
717
718             this._populateWithContent(content);
719         }
720
721         this._requestingScriptContent = true;
722
723         scripts[0].requestContent(scriptContentAvailable.bind(this));
724     },
725
726     _matchesSourceCodeLocation: function(sourceCodeLocation)
727     {
728         if (this._sourceCode instanceof WebInspector.SourceMapResource)
729             return sourceCodeLocation.displaySourceCode === this._sourceCode;
730         if (this._sourceCode instanceof WebInspector.Resource)
731             return sourceCodeLocation.sourceCode.url === this._sourceCode.url;
732         if (this._sourceCode instanceof WebInspector.Script)
733             return sourceCodeLocation.sourceCode === this._sourceCode;
734         return false;
735     },
736
737     _matchesBreakpoint: function(breakpoint)
738     {
739         console.assert(this._supportsDebugging);
740         if (this._sourceCode instanceof WebInspector.SourceMapResource)
741             return breakpoint.sourceCodeLocation.displaySourceCode === this._sourceCode;
742         if (this._sourceCode instanceof WebInspector.Resource)
743             return breakpoint.url === this._sourceCode.url;
744         if (this._sourceCode instanceof WebInspector.Script)
745             return breakpoint.url === this._sourceCode.url || breakpoint.scriptIdentifier === this._sourceCode.id;
746         return false;
747     },
748
749     _matchesIssue: function(issue)
750     {
751         if (this._sourceCode instanceof WebInspector.Resource)
752             return issue.url === this._sourceCode.url;
753         // FIXME: Support issues for Scripts based on id, not only by URL.
754         if (this._sourceCode instanceof WebInspector.Script)
755             return issue.url === this._sourceCode.url;
756         return false;
757     },
758
759     _issueWasAdded: function(event)
760     {
761         var issue = event.data.issue;
762         if (!this._matchesIssue(issue))
763             return;
764
765         this._addIssue(issue);
766     },
767
768     _addIssue: function(issue)
769     {
770         var lineNumberIssues = this._issuesLineNumberMap[issue.lineNumber];
771         if (!lineNumberIssues)
772             lineNumberIssues = this._issuesLineNumberMap[issue.lineNumber] = [];
773
774         lineNumberIssues.push(issue);
775
776         if (issue.level === WebInspector.IssueMessage.Level.Error)
777             this.addStyleClassToLine(issue.lineNumber, WebInspector.SourceCodeTextEditor.LineErrorStyleClassName);
778         else if (issue.level === WebInspector.IssueMessage.Level.Warning)
779             this.addStyleClassToLine(issue.lineNumber, WebInspector.SourceCodeTextEditor.LineWarningStyleClassName);
780         else
781             console.error("Unknown issue level");
782
783         // FIXME <rdar://problem/10854857>: Show the issue message on the line as a bubble.
784     },
785
786     _breakpointInfoForBreakpoint: function(breakpoint)
787     {
788         return {resolved: breakpoint.resolved, disabled: breakpoint.disabled, autoContinue: breakpoint.autoContinue};
789     },
790
791     get _supportsDebugging()
792     {
793         if (this._sourceCode instanceof WebInspector.Resource)
794             return this._sourceCode.type === WebInspector.Resource.Type.Document || this._sourceCode.type === WebInspector.Resource.Type.Script;
795         if (this._sourceCode instanceof WebInspector.Script)
796             return true;
797         return false;
798     },
799
800     // TextEditor Delegate
801
802     textEditorBaseURL: function(textEditor)
803     {
804         return this._sourceCode.url;
805     },
806
807     textEditorShouldHideLineNumber: function(textEditor, lineNumber)
808     {
809         return lineNumber in this._invalidLineNumbers;
810     },
811
812     textEditorGutterContextMenu: function(textEditor, lineNumber, columnNumber, editorBreakpoints, event)
813     {
814         if (!this._supportsDebugging)
815             return;
816
817         event.preventDefault();
818
819         var contextMenu = new WebInspector.ContextMenu(event);
820
821         // Paused. Add Continue to Here option only if we have a script identifier for the location.
822         if (WebInspector.debuggerManager.paused) {
823             var editorLineInfo = {lineNumber:lineNumber, columnNumber:columnNumber};
824             var unformattedLineInfo = this._unformattedLineInfoForEditorLineInfo(editorLineInfo);
825             var sourceCodeLocation = this._sourceCode.createSourceCodeLocation(unformattedLineInfo.lineNumber, unformattedLineInfo.columnNumber);
826
827             if (sourceCodeLocation.sourceCode instanceof WebInspector.Script)
828                 var script = sourceCodeLocation.sourceCode;
829             else if (sourceCodeLocation.sourceCode instanceof WebInspector.Resource)
830                 var script = sourceCodeLocation.sourceCode.scriptForLocation(sourceCodeLocation);
831
832             if (script) {
833                 function continueToLocation()
834                 {
835                     WebInspector.debuggerManager.continueToLocation(script.id, sourceCodeLocation.lineNumber, sourceCodeLocation.columnNumber);
836                 }
837
838                 contextMenu.appendItem(WebInspector.UIString("Continue to Here"), continueToLocation);
839                 contextMenu.appendSeparator();
840             }
841         }
842
843         var breakpoints = [];
844         for (var i = 0; i < editorBreakpoints.length; ++i) {
845             var lineInfo = editorBreakpoints[i];
846             var breakpoint = this._breakpointForEditorLineInfo(lineInfo);
847             console.assert(breakpoint);
848             if (breakpoint)
849                 breakpoints.push(breakpoint);
850         }
851
852         // No breakpoints.
853         if (!breakpoints.length) {
854             function addBreakpoint()
855             {
856                 var data = this.textEditorBreakpointAdded(this, lineNumber, columnNumber);
857                 this.setBreakpointInfoForLineAndColumn(data.lineNumber, data.columnNumber, data.breakpointInfo);
858             }
859
860             contextMenu.appendItem(WebInspector.UIString("Add Breakpoint"), addBreakpoint.bind(this));
861             contextMenu.show();
862             return;
863         }
864
865         // Single breakpoint.
866         if (breakpoints.length === 1) {
867             var breakpoint = breakpoints[0];
868             function revealInSidebar()
869             {
870                 WebInspector.debuggerSidebarPanel.show();
871                 var treeElement = WebInspector.debuggerSidebarPanel.treeElementForRepresentedObject(breakpoint);
872                 if (treeElement)
873                     treeElement.revealAndSelect();
874             }
875
876             breakpoint.appendContextMenuItems(contextMenu, event.target);
877             contextMenu.appendSeparator();
878             contextMenu.appendItem(WebInspector.UIString("Reveal in Debugger Navigation Sidebar"), revealInSidebar);
879             contextMenu.show();
880             return;
881         }
882
883         // Multiple breakpoints.
884         var shouldDisable = false;
885         for (var i = 0; i < breakpoints.length; ++i) {
886             if (!breakpoints[i].disabled) {
887                 shouldDisable = true;
888                 break;
889             }
890         }
891
892         function removeBreakpoints()
893         {
894             for (var i = 0; i < breakpoints.length; ++i) {
895                 var breakpoint = breakpoints[i];
896                 if (WebInspector.debuggerManager.isBreakpointRemovable(breakpoint))
897                     WebInspector.debuggerManager.removeBreakpoint(breakpoint);
898             }
899         }
900
901         function toggleBreakpoints()
902         {
903             for (var i = 0; i < breakpoints.length; ++i)
904                 breakpoints[i].disabled = shouldDisable;
905         }
906
907         if (shouldDisable)
908             contextMenu.appendItem(WebInspector.UIString("Disable Breakpoints"), toggleBreakpoints.bind(this));
909         else
910             contextMenu.appendItem(WebInspector.UIString("Enable Breakpoints"), toggleBreakpoints.bind(this));
911         contextMenu.appendItem(WebInspector.UIString("Delete Breakpoints"), removeBreakpoints.bind(this));
912         contextMenu.show();
913     },
914
915     textEditorBreakpointAdded: function(textEditor, lineNumber, columnNumber)
916     {
917         if (!this._supportsDebugging)
918             return null;
919
920         var editorLineInfo = {lineNumber:lineNumber, columnNumber:columnNumber};
921         var unformattedLineInfo = this._unformattedLineInfoForEditorLineInfo(editorLineInfo);
922         var sourceCodeLocation = this._sourceCode.createSourceCodeLocation(unformattedLineInfo.lineNumber, unformattedLineInfo.columnNumber);
923         var breakpoint = new WebInspector.Breakpoint(sourceCodeLocation);
924
925         var lineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation);
926         this._addBreakpointWithEditorLineInfo(breakpoint, lineInfo);
927
928         this._ignoreBreakpointAddedBreakpoint = breakpoint;
929
930         var shouldSkipEventDispatch = false;
931         var shouldSpeculativelyResolveBreakpoint = true;
932         WebInspector.debuggerManager.addBreakpoint(breakpoint, shouldSkipEventDispatch, shouldSpeculativelyResolveBreakpoint);
933         delete this._ignoreBreakpointAddedBreakpoint;
934
935         // Return the more accurate location and breakpoint info.
936         return {
937             breakpointInfo: this._breakpointInfoForBreakpoint(breakpoint),
938             lineNumber: lineInfo.lineNumber,
939             columnNumber: lineInfo.columnNumber
940         };
941     },
942
943     textEditorBreakpointRemoved: function(textEditor, lineNumber, columnNumber)
944     {
945         console.assert(this._supportsDebugging);
946         if (!this._supportsDebugging)
947             return;
948
949         var lineInfo = {lineNumber: lineNumber, columnNumber: columnNumber};
950         var breakpoint = this._breakpointForEditorLineInfo(lineInfo);
951         console.assert(breakpoint);
952         if (!breakpoint)
953             return;
954
955         this._removeBreakpointWithEditorLineInfo(breakpoint, lineInfo);
956
957         this._ignoreBreakpointRemovedBreakpoint = breakpoint;
958         WebInspector.debuggerManager.removeBreakpoint(breakpoint);
959         delete this._ignoreBreakpointAddedBreakpoint;
960     },
961
962     textEditorBreakpointMoved: function(textEditor, oldLineNumber, oldColumnNumber, newLineNumber, newColumnNumber)
963     {
964         console.assert(this._supportsDebugging);
965         if (!this._supportsDebugging)
966             return;
967
968         var oldLineInfo = {lineNumber: oldLineNumber, columnNumber: oldColumnNumber};
969         var breakpoint = this._breakpointForEditorLineInfo(oldLineInfo);
970         console.assert(breakpoint);
971         if (!breakpoint)
972             return;
973
974         this._removeBreakpointWithEditorLineInfo(breakpoint, oldLineInfo);
975
976         var newLineInfo = {lineNumber: newLineNumber, columnNumber: newColumnNumber};
977         var unformattedNewLineInfo = this._unformattedLineInfoForEditorLineInfo(newLineInfo);
978         this._ignoreLocationUpdateBreakpoint = breakpoint;
979         breakpoint.sourceCodeLocation.update(this._sourceCode, unformattedNewLineInfo.lineNumber, unformattedNewLineInfo.columnNumber);
980         delete this._ignoreLocationUpdateBreakpoint;
981
982         var accurateNewLineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation);
983         this._addBreakpointWithEditorLineInfo(breakpoint, accurateNewLineInfo);
984
985         if (accurateNewLineInfo.lineNumber !== newLineInfo.lineNumber || accurateNewLineInfo.columnNumber !== newLineInfo.columnNumber)
986             this.updateBreakpointLineAndColumn(newLineInfo.lineNumber, newLineInfo.columnNumber, accurateNewLineInfo.lineNumber, accurateNewLineInfo.columnNumber);
987     },
988
989     textEditorBreakpointClicked: function(textEditor, lineNumber, columnNumber)
990     {
991         console.assert(this._supportsDebugging);
992         if (!this._supportsDebugging)
993             return;
994
995         var breakpoint = this._breakpointForEditorLineInfo({lineNumber: lineNumber, columnNumber: columnNumber});
996         console.assert(breakpoint);
997         if (!breakpoint)
998             return;
999
1000         breakpoint.cycleToNextMode();
1001     },
1002
1003     textEditorUpdatedFormatting: function(textEditor)
1004     {
1005         this._ignoreAllBreakpointLocationUpdates = true;
1006         this._sourceCode.formatterSourceMap = this.formatterSourceMap;
1007         delete this._ignoreAllBreakpointLocationUpdates;
1008
1009         // Always put the source map on both the Script and Resource if both exist. For example,
1010         // if this SourceCode is a Resource, then there might also be a Script. In the debugger,
1011         // the backend identifies call frames with Script line and column information, and the
1012         // Script needs the formatter source map to produce the proper display line and column.
1013         if (this._sourceCode instanceof WebInspector.Resource && !(this._sourceCode instanceof WebInspector.SourceMapResource)) {
1014             var scripts = this._sourceCode.scripts;
1015             for (var i = 0; i < scripts.length; ++i)
1016                 scripts[i].formatterSourceMap = this.formatterSourceMap;
1017         } else if (this._sourceCode instanceof WebInspector.Script) {
1018             if (this._sourceCode.resource)
1019                 this._sourceCode.resource.formatterSourceMap = this.formatterSourceMap;
1020         }
1021
1022         // Some breakpoints may have moved, some might not have. Just go through
1023         // and remove and reinsert all the breakpoints.
1024
1025         var oldBreakpointMap = this._breakpointMap;
1026         this._breakpointMap = {};
1027
1028         for (var lineNumber in oldBreakpointMap) {
1029             for (var columnNumber in oldBreakpointMap[lineNumber]) {
1030                 var breakpoint = oldBreakpointMap[lineNumber][columnNumber];
1031                 var newLineInfo = this._editorLineInfoForSourceCodeLocation(breakpoint.sourceCodeLocation);
1032                 this._addBreakpointWithEditorLineInfo(breakpoint, newLineInfo);
1033                 this.setBreakpointInfoForLineAndColumn(lineNumber, columnNumber, null);
1034                 this.setBreakpointInfoForLineAndColumn(newLineInfo.lineNumber, newLineInfo.columnNumber, this._breakpointInfoForBreakpoint(breakpoint));
1035             }
1036         }
1037     },
1038
1039     _debuggerDidPause: function(event)
1040     {
1041         this._updateTokenTrackingControllerState();
1042         if (this._typeTokenAnnotator && this._typeTokenAnnotator.isActive)
1043             this._typeTokenAnnotator.refresh();
1044     },
1045
1046     _debuggerDidResume: function(event)
1047     {
1048         this._updateTokenTrackingControllerState();
1049         this._dismissPopover();
1050         if (this._typeTokenAnnotator && this._typeTokenAnnotator.isActive)
1051             this._typeTokenAnnotator.refresh();
1052     },
1053
1054     _sourceCodeSourceMapAdded: function(event)
1055     {
1056         WebInspector.notifications.addEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateTokenTrackingControllerState, this);
1057         this._sourceCode.removeEventListener(WebInspector.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this);
1058
1059         this._updateTokenTrackingControllerState();
1060     },
1061
1062     _updateTokenTrackingControllerState: function()
1063     {
1064         var mode = WebInspector.CodeMirrorTokenTrackingController.Mode.None;
1065         if (WebInspector.debuggerManager.paused)
1066             mode = WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression;
1067         else if (this._typeTokenAnnotator && this._typeTokenAnnotator.isActive)
1068             mode = WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptTypeInformation;
1069         else if (this._hasColorMarkers())
1070             mode = WebInspector.CodeMirrorTokenTrackingController.Mode.MarkedTokens;
1071         else if ((this._sourceCode instanceof WebInspector.SourceMapResource || this._sourceCode.sourceMaps.length !== 0) && WebInspector.modifierKeys.metaKey && !WebInspector.modifierKeys.altKey && !WebInspector.modifierKeys.shiftKey)
1072             mode = WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens;
1073
1074         this.tokenTrackingController.enabled = mode !== WebInspector.CodeMirrorTokenTrackingController.Mode.None;
1075
1076         if (mode === this.tokenTrackingController.mode)
1077             return;
1078
1079         switch (mode) {
1080         case WebInspector.CodeMirrorTokenTrackingController.Mode.MarkedTokens:
1081             this.tokenTrackingController.mouseOverDelayDuration = 0;
1082             this.tokenTrackingController.mouseOutReleaseDelayDuration = 0;
1083             break;
1084         case WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens:
1085             this.tokenTrackingController.mouseOverDelayDuration = 0;
1086             this.tokenTrackingController.mouseOutReleaseDelayDuration = 0;
1087             this.tokenTrackingController.classNameForHighlightedRange = WebInspector.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName;
1088             this._dismissPopover();
1089             break;
1090         case WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression:
1091         case WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptTypeInformation:
1092             this.tokenTrackingController.mouseOverDelayDuration = WebInspector.SourceCodeTextEditor.DurationToMouseOverTokenToMakeHoveredToken;
1093             this.tokenTrackingController.mouseOutReleaseDelayDuration = WebInspector.SourceCodeTextEditor.DurationToMouseOutOfHoveredTokenToRelease;
1094             this.tokenTrackingController.classNameForHighlightedRange = WebInspector.SourceCodeTextEditor.HoveredExpressionHighlightStyleClassName;
1095             break;
1096         }
1097
1098         this.tokenTrackingController.mode = mode;
1099     },
1100
1101     _hasColorMarkers: function()
1102     {
1103         for (var marker of this.markers) {
1104             if (marker.type === WebInspector.TextMarker.Type.Color)
1105                 return true;
1106         }
1107         return false;
1108     },
1109
1110     // CodeMirrorTokenTrackingController Delegate
1111
1112     tokenTrackingControllerCanReleaseHighlightedRange: function(tokenTrackingController, element)
1113     {
1114         if (!this._popover)
1115             return true;
1116
1117         if (!window.getSelection().isCollapsed && this._popover.element.contains(window.getSelection().anchorNode))
1118             return false;
1119
1120         return true;
1121     },
1122
1123     tokenTrackingControllerHighlightedRangeReleased: function(tokenTrackingController)
1124     {
1125         if (!this._mouseIsOverPopover)
1126             this._dismissPopover();
1127     },
1128
1129     tokenTrackingControllerHighlightedRangeWasClicked: function(tokenTrackingController)
1130     {
1131         if (this.tokenTrackingController.mode !== WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens)
1132             return;
1133
1134         // Links are handled by TextEditor.
1135         if (/\blink\b/.test(this.tokenTrackingController.candidate.hoveredToken.type))
1136             return;
1137
1138         var sourceCodeLocation = this._sourceCodeLocationForEditorPosition(this.tokenTrackingController.candidate.hoveredTokenRange.start);
1139         if (this.sourceCode instanceof WebInspector.SourceMapResource)
1140             WebInspector.resourceSidebarPanel.showOriginalOrFormattedSourceCodeLocation(sourceCodeLocation);
1141         else
1142             WebInspector.resourceSidebarPanel.showSourceCodeLocation(sourceCodeLocation);
1143     },
1144
1145     tokenTrackingControllerNewHighlightCandidate: function(tokenTrackingController, candidate)
1146     {
1147         if (this.tokenTrackingController.mode === WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens) {
1148             this.tokenTrackingController.highlightRange(candidate.hoveredTokenRange);
1149             return;
1150         }
1151
1152         if (this.tokenTrackingController.mode === WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression) {
1153             this._tokenTrackingControllerHighlightedJavaScriptExpression(candidate);
1154             return;
1155         }
1156
1157         if (this.tokenTrackingController.mode === WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptTypeInformation) {
1158             this._tokenTrackingControllerHighlightedJavaScriptTypeInformation(candidate);
1159             return;
1160         }
1161
1162         if (this.tokenTrackingController.mode === WebInspector.CodeMirrorTokenTrackingController.Mode.MarkedTokens) {
1163             var markers = this.markersAtPosition(candidate.hoveredTokenRange.start);
1164             if (markers.length > 0)
1165                 this._tokenTrackingControllerHighlightedMarkedExpression(candidate, markers);
1166             else
1167                 this._dismissEditingController();
1168         }
1169     },
1170
1171     tokenTrackingControllerMouseOutOfHoveredMarker: function(tokenTrackingController, hoveredMarker)
1172     {
1173         this._dismissEditingController();
1174     },
1175
1176     _tokenTrackingControllerHighlightedJavaScriptExpression: function(candidate)
1177     {
1178         console.assert(candidate.expression);
1179
1180         function populate(error, result, wasThrown)
1181         {
1182             if (error || wasThrown)
1183                 return;
1184
1185             if (candidate !== this.tokenTrackingController.candidate)
1186                 return;
1187
1188             var data = WebInspector.RemoteObject.fromPayload(result);
1189             switch (data.type) {
1190             case "function":
1191                 this._showPopoverForFunction(data);
1192                 break;
1193             case "object":
1194                 if (data.subtype === "regexp") 
1195                     this._showPopoverForRegExp(data);
1196                 else
1197                     this._showPopoverForObject(data);
1198                 break;
1199             case "string":
1200                 this._showPopoverForString(data);
1201                 break;
1202             case "number":
1203                 this._showPopoverForNumber(data);
1204                 break;
1205             case "boolean":
1206                 this._showPopoverForBoolean(data);
1207                 break;
1208             case "undefined":
1209                 this._showPopoverForUndefined(data);
1210                 break;
1211             }
1212         }
1213
1214         DebuggerAgent.evaluateOnCallFrame.invoke({callFrameId: WebInspector.debuggerManager.activeCallFrame.id, expression: candidate.expression, objectGroup: "popover", doNotPauseOnExceptionsAndMuteConsole: true}, populate.bind(this));
1215     },
1216
1217     _tokenTrackingControllerHighlightedJavaScriptTypeInformation: function(candidate)
1218     {
1219         console.assert(candidate.expression);
1220
1221         var sourceCode = this._sourceCode;
1222         var sourceID = sourceCode.scripts[0].id;
1223         var range = candidate.hoveredTokenRange;
1224         var offset = this.currentPositionToOriginalOffset({line: range.start.line, ch: range.start.ch});
1225
1226         var allRequests = [{
1227             typeInformationDescriptor: WebInspector.ScriptSyntaxTree.TypeProfilerSearchDescriptor.NormalExpression,
1228             sourceID: sourceID,
1229             divot: offset
1230         }];
1231
1232         function handler(error, allTypes) {
1233             if (error)
1234                 return;
1235
1236             if (candidate !== this.tokenTrackingController.candidate)
1237                 return;
1238
1239             console.assert(allTypes.length === 1);
1240             if (!allTypes.length)
1241                 return;
1242             var types = allTypes[0];
1243             if (types.isValid) {
1244                 var popoverTitle = WebInspector.TypeTokenView.titleForPopover(WebInspector.TypeTokenView.TitleType.Variable, candidate.expression);
1245                 this.showPopoverForTypes(types, null, popoverTitle);
1246             }
1247         }
1248
1249         RuntimeAgent.getRuntimeTypesForVariablesAtOffsets(allRequests, handler.bind(this));
1250     },
1251
1252     _showPopover: function(content, bounds)
1253     {
1254         console.assert(this.tokenTrackingController.candidate || bounds);
1255
1256         var shouldHighlightRange = false;
1257         var candidate = this.tokenTrackingController.candidate;
1258         // If bounds is falsey, this is a popover introduced from a hover event.
1259         // Otherwise, this is called from TypeTokenAnnotator.
1260         if (!bounds) {
1261             if (!candidate)
1262                 return;
1263
1264             var rects = this.rectsForRange(candidate.hoveredTokenRange);
1265             bounds = WebInspector.Rect.unionOfRects(rects);
1266
1267             shouldHighlightRange = true;
1268         }
1269
1270         content.classList.add(WebInspector.SourceCodeTextEditor.PopoverDebuggerContentStyleClassName);
1271
1272         this._popover = this._popover || new WebInspector.Popover(this);
1273         this._popover.presentNewContentWithFrame(content, bounds.pad(5), [WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MAX_X]);
1274         if (shouldHighlightRange)
1275             this.tokenTrackingController.highlightRange(candidate.expressionRange);
1276
1277         this._trackPopoverEvents();
1278     },
1279
1280     _showPopoverForFunction: function(data)
1281     {
1282         var candidate = this.tokenTrackingController.candidate;
1283
1284         function didGetDetails(error, response)
1285         {
1286             if (error) {
1287                 console.error(error);
1288                 this._dismissPopover();
1289                 return;
1290             }
1291
1292             // Nothing to do if the token has changed since the time we
1293             // asked for the function details from the backend.
1294             if (candidate !== this.tokenTrackingController.candidate)
1295                 return;
1296
1297             var wrapper = document.createElement("div");
1298             wrapper.className = "body console-formatted-function";
1299             wrapper.textContent = data.description;
1300
1301             var content = document.createElement("div");
1302             content.className = "function";
1303
1304             var title = content.appendChild(document.createElement("div"));
1305             title.className = "title";
1306             title.textContent = response.name || response.inferredName || response.displayName || WebInspector.UIString("(anonymous function)");
1307
1308             content.appendChild(wrapper);
1309
1310             this._showPopover(content);
1311         }
1312         DebuggerAgent.getFunctionDetails(data.objectId, didGetDetails.bind(this));
1313     },
1314
1315     _showPopoverForObject: function(data)
1316     {
1317         if (data.subtype === "null") {
1318             this._showPopoverForNull(data);
1319             return;
1320         }
1321
1322         var content = document.createElement("div");
1323         content.className = "object expandable";
1324
1325         var titleElement = document.createElement("div");
1326         titleElement.className = "title";
1327         titleElement.textContent = data.description;
1328         content.appendChild(titleElement);
1329
1330         var section = new WebInspector.ObjectPropertiesSection(data);
1331         section.expanded = true;
1332         section.element.classList.add("body");
1333         content.appendChild(section.element);
1334
1335         this._showPopover(content);
1336     },
1337
1338     _showPopoverForString: function(data)
1339     {
1340         var content = document.createElement("div");
1341         content.className = "string console-formatted-string";
1342         content.textContent = "\"" + data.description + "\"";
1343
1344         this._showPopover(content);
1345     },
1346
1347     _showPopoverForRegExp: function(data)
1348     {
1349         var content = document.createElement("div");
1350         content.className = "regexp console-formatted-regexp";
1351         content.textContent = data.description;
1352
1353         this._showPopover(content);
1354     },
1355
1356     _showPopoverForNumber: function(data)
1357     {
1358         var content = document.createElement("span");
1359         content.className = "number console-formatted-number";
1360         content.textContent = data.description;
1361
1362         this._showPopover(content);
1363     },
1364
1365     _showPopoverForBoolean: function(data)
1366     {
1367         var content = document.createElement("span");
1368         content.className = "boolean console-formatted-boolean";
1369         content.textContent = data.description;
1370
1371         this._showPopover(content);
1372     },
1373
1374     _showPopoverForNull: function(data)
1375     {
1376         var content = document.createElement("span");
1377         content.className = "boolean console-formatted-null";
1378         content.textContent = data.description;
1379
1380         this._showPopover(content);
1381     },
1382
1383     _showPopoverForUndefined: function(data)
1384     {
1385         var content = document.createElement("span");
1386         content.className = "boolean console-formatted-undefined";
1387         content.textContent = data.description;
1388
1389         this._showPopover(content);
1390     },
1391
1392     willDismissPopover: function(popover)
1393     {
1394         this.tokenTrackingController.removeHighlightedRange();
1395
1396         RuntimeAgent.releaseObjectGroup("popover");
1397     },
1398
1399     _dismissPopover: function()
1400     {
1401         if (!this._popover)
1402             return;
1403
1404         this._popover.dismiss();
1405
1406         if (this._popoverEventListeners && this._popoverEventListenersAreRegistered) {
1407             this._popoverEventListenersAreRegistered = false;
1408             this._popoverEventListeners.unregister();
1409         }
1410     },
1411
1412     _trackPopoverEvents: function()
1413     {
1414         if (!this._popoverEventListeners) 
1415             this._popoverEventListeners = new WebInspector.EventListenerSet(this, "Popover listeners");
1416         if (!this._popoverEventListenersAreRegistered) {
1417             this._popoverEventListenersAreRegistered = true;
1418             this._popoverEventListeners.register(this._popover.element, "mouseover", this._popoverMouseover);
1419             this._popoverEventListeners.register(this._popover.element, "mouseout", this._popoverMouseout);
1420             this._popoverEventListeners.install();
1421         }
1422     },
1423
1424     _popoverMouseover: function(event)
1425     {
1426         this._mouseIsOverPopover = true;
1427     },
1428
1429     _popoverMouseout: function(event)
1430     {
1431         this._mouseIsOverPopover = this._popover.element.contains(event.relatedTarget);
1432     },
1433
1434     _updateEditableMarkers: function(range)
1435     {
1436         this.createColorMarkers(range);
1437         this.createGradientMarkers(range);
1438
1439         this._updateTokenTrackingControllerState();
1440     },
1441
1442     _tokenTrackingControllerHighlightedMarkedExpression: function(candidate, markers)
1443     {
1444         // Look for the outermost editable marker.
1445         var editableMarker;
1446         for (var marker of markers) {
1447             if (!marker.range || (marker.type !== WebInspector.TextMarker.Type.Color && marker.type !== WebInspector.TextMarker.Type.Gradient))
1448                 continue;
1449
1450             if (!editableMarker || (marker.range.startLine < editableMarker.range.startLine || (marker.range.startLine === editableMarker.range.startLine && marker.range.startColumn < editableMarker.range.startColumn)))
1451                 editableMarker = marker;
1452         }
1453
1454         if (!editableMarker) {
1455             this.tokenTrackingController.hoveredMarker = null;
1456             return;
1457         }
1458
1459         if (this.tokenTrackingController.hoveredMarker === editableMarker)
1460             return;
1461
1462         this._dismissEditingController();
1463
1464         this.tokenTrackingController.hoveredMarker = editableMarker;
1465
1466         this._editingController = this.editingControllerForMarker(editableMarker);
1467
1468         if (marker.type === WebInspector.TextMarker.Type.Color) {
1469             var color = this._editingController.value;
1470             if (!color || !color.valid) {
1471                 editableMarker.clear();
1472                 delete this._editingController;
1473                 return;
1474             }
1475         }
1476
1477         this._editingController.delegate = this;
1478         this._editingController.presentHoverMenu();
1479     },
1480
1481     _dismissEditingController: function(discrete)
1482     {
1483         if (this._editingController)
1484             this._editingController.dismissHoverMenu(discrete);
1485         
1486         this.tokenTrackingController.hoveredMarker = null;
1487         delete this._editingController;
1488     },
1489
1490     // CodeMirrorEditingController Delegate
1491     
1492     editingControllerDidStartEditing: function(editingController)
1493     {
1494         // We can pause the token tracking controller during editing, it will be reset
1495         // to the expected state by calling _updateEditableMarkers() in the
1496         // editingControllerDidFinishEditing delegate.
1497         this.tokenTrackingController.enabled = false;
1498
1499         // We clear the marker since we'll reset it after editing.
1500         editingController.marker.clear();
1501         
1502         // We ignore content changes made as a result of color editing.
1503         this._ignoreContentDidChange++;
1504     },
1505     
1506     editingControllerDidFinishEditing: function(editingController)
1507     {
1508         this._updateEditableMarkers(editingController.range);
1509
1510         this._ignoreContentDidChange--;
1511
1512         delete this._editingController;
1513     },
1514
1515     _makeTypeTokenAnnotator: function()
1516     {
1517         if (!RuntimeAgent.getRuntimeTypesForVariablesAtOffsets)
1518             return;
1519
1520         var script = null;
1521         // FIXME: This needs to me modified to work with HTML files with inline script tags.
1522         if (this._sourceCode instanceof WebInspector.Script)
1523             script = this._sourceCode;
1524         else if (this._sourceCode instanceof WebInspector.Resource && this._sourceCode.type === WebInspector.Resource.Type.Script && this._sourceCode.scripts.length)
1525             script = this._sourceCode.scripts[0];
1526
1527         if (!script)
1528             return;
1529
1530         this._typeTokenAnnotator = new WebInspector.TypeTokenAnnotator(this, script);
1531         if (WebInspector.showJavaScriptTypeInformationSetting.value) {
1532             this._typeTokenAnnotator.reset();
1533             this._enableScrollEventsForTypeTokenAnnotator();
1534         }
1535     },
1536
1537     _enableScrollEventsForTypeTokenAnnotator: function()
1538     {
1539         // Pause updating type tokens while scrolling to prevent frame loss.
1540         console.assert(!this._typeTokenScrollHandler);
1541         this._typeTokenScrollHandler = this._makeTypeTokenScrollEventHandler();
1542         this.addScrollHandler(this._typeTokenScrollHandler);
1543     },
1544
1545     _disableScrollEventsForTypeTokenAnnotator: function()
1546     {
1547         console.assert(this._typeTokenScrollHandler);
1548         this.removeScrollHandler(this._typeTokenScrollHandler);
1549         this._typeTokenScrollHandler = null;
1550     },
1551
1552     _makeTypeTokenScrollEventHandler: function()
1553     {
1554         var typeTokenAnnotator = this._typeTokenAnnotator;
1555         var timeoutIdentifier = null;
1556         function scrollHandler()
1557         {
1558             if (timeoutIdentifier)
1559                 clearTimeout(timeoutIdentifier);
1560             else
1561                 typeTokenAnnotator.pause();
1562
1563             timeoutIdentifier = setTimeout(function() {
1564                 timeoutIdentifier = null;
1565                 typeTokenAnnotator.resume();
1566             }, WebInspector.SourceCodeTextEditor.DurationToUpdateTypeTokensAfterScrolling);
1567         }
1568
1569         return scrollHandler;
1570     }
1571 };
1572
1573 WebInspector.SourceCodeTextEditor.prototype.__proto__ = WebInspector.TextEditor.prototype;