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