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.SourceCodeLocation = function(sourceCode, lineNumber, columnNumber)
27{
28    WebInspector.Object.call(this);
29
30    console.assert(sourceCode === null || sourceCode instanceof WebInspector.SourceCode);
31    console.assert(!(sourceCode instanceof WebInspector.SourceMapResource));
32    console.assert(typeof lineNumber === "number" && !isNaN(lineNumber) && lineNumber >= 0);
33    console.assert(typeof columnNumber === "number" && !isNaN(columnNumber) && columnNumber >= 0);
34
35    this._sourceCode = sourceCode || null;
36    this._lineNumber = lineNumber;
37    this._columnNumber = columnNumber;
38    this._resolveFormattedLocation();
39
40    if (this._sourceCode) {
41        this._sourceCode.addEventListener(WebInspector.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this);
42        this._sourceCode.addEventListener(WebInspector.SourceCode.Event.FormatterDidChange, this._sourceCodeFormatterDidChange, this);
43    }
44
45    this._resetMappedLocation();
46};
47
48WebInspector.SourceCodeLocation.DisplayLocationClassName = "display-location";
49
50WebInspector.SourceCodeLocation.LargeColumnNumber = 80;
51
52WebInspector.SourceCodeLocation.NameStyle = {
53    None: "none", // File name not included.
54    Short: "short", // Only the file name.
55    Full: "full" // Full URL is used.
56};
57
58WebInspector.SourceCodeLocation.ColumnStyle = {
59    Hidden: "hidden",             // column numbers are not included.
60    OnlyIfLarge: "only-if-large", // column numbers greater than 80 are shown.
61    Shown: "shown"                // non-zero column numbers are shown.
62};
63
64WebInspector.SourceCodeLocation.Event = {
65    LocationChanged: "source-code-location-location-changed",
66    DisplayLocationChanged: "source-code-location-display-location-changed"
67};
68
69WebInspector.SourceCodeLocation.prototype = {
70    constructor: WebInspector.SourceCodeLocation,
71
72    // Public
73
74    isEqual: function(other)
75    {
76        if (!other)
77            return false;
78        return this._sourceCode === other._sourceCode && this._lineNumber === other._lineNumber && this._columnNumber === other._columnNumber;
79    },
80
81    get sourceCode()
82    {
83        return this._sourceCode;
84    },
85
86    set sourceCode(sourceCode)
87    {
88        this.setSourceCode(sourceCode);
89    },
90
91    // Raw line and column in the original source code.
92
93    get lineNumber()
94    {
95        return this._lineNumber;
96    },
97
98    get columnNumber()
99    {
100        return this._columnNumber;
101    },
102
103    position: function()
104    {
105        return new WebInspector.SourceCodePosition(this.lineNumber, this.columnNumber);
106    },
107
108    // Formatted line and column if the original source code is pretty printed.
109    // This is the same as the raw location if there is no formatter.
110
111    get formattedLineNumber()
112    {
113        return this._formattedLineNumber;
114    },
115
116    get formattedColumnNumber()
117    {
118        return this._formattedColumnNumber;
119    },
120
121    formattedPosition: function()
122    {
123        return new WebInspector.SourceCodePosition(this.formattedLineNumber, this.formattedColumnNumber);
124    },
125
126    // Display line and column:
127    //   - Mapped line and column if the original source code has a source map.
128    //   - Otherwise this is the formatted / raw line and column.
129
130    get displaySourceCode()
131    {
132        this.resolveMappedLocation();
133        return this._mappedResource || this._sourceCode;
134    },
135
136    get displayLineNumber()
137    {
138        this.resolveMappedLocation();
139        return isNaN(this._mappedLineNumber) ? this._formattedLineNumber : this._mappedLineNumber;
140    },
141
142    get displayColumnNumber()
143    {
144        this.resolveMappedLocation();
145        return isNaN(this._mappedColumnNumber) ? this._formattedColumnNumber : this._mappedColumnNumber;
146    },
147
148    displayPosition: function()
149    {
150        return new WebInspector.SourceCodePosition(this.displayLineNumber, this.displayColumnNumber);
151    },
152
153    // User presentable location strings: "file:lineNumber:columnNumber".
154
155    originalLocationString: function(columnStyle, nameStyle, prefix)
156    {
157        return this._locationString(this.sourceCode, this.lineNumber, this.columnNumber, columnStyle, nameStyle, prefix);
158    },
159
160    formattedLocationString: function(columnStyle, nameStyle, prefix)
161    {
162        return this._locationString(this.sourceCode, this.formattedLineNumber, this.formattedColumn, columnStyle, nameStyle, prefix);
163    },
164
165    displayLocationString: function(columnStyle, nameStyle, prefix)
166    {
167        return this._locationString(this.displaySourceCode, this.displayLineNumber, this.displayColumnNumber, columnStyle, nameStyle, prefix);
168    },
169
170    tooltipString: function()
171    {
172        if (!this.hasDifferentDisplayLocation())
173            return this.originalLocationString(WebInspector.SourceCodeLocation.ColumnStyle.Shown, WebInspector.SourceCodeLocation.NameStyle.Full);
174
175        var tooltip = WebInspector.UIString("Located at %s").format(this.displayLocationString(WebInspector.SourceCodeLocation.ColumnStyle.Shown, WebInspector.SourceCodeLocation.NameStyle.Full));
176        tooltip += "\n" + WebInspector.UIString("Originally %s").format(this.originalLocationString(WebInspector.SourceCodeLocation.ColumnStyle.Shown, WebInspector.SourceCodeLocation.NameStyle.Full));
177        return tooltip;
178    },
179
180    hasMappedLocation: function()
181    {
182        this.resolveMappedLocation();
183        return this._mappedResource !== null;
184    },
185
186    hasFormattedLocation: function()
187    {
188        return this._formattedLineNumber !== this._lineNumber || this._formattedColumnNumber !== this._columnNumber;
189    },
190
191    hasDifferentDisplayLocation: function()
192    {
193       return this.hasMappedLocation() || this.hasFormattedLocation();
194    },
195
196    update: function(sourceCode, lineNumber, columnNumber)
197    {
198        console.assert(sourceCode === this._sourceCode || (this._mappedResource && sourceCode === this._mappedResource));
199        console.assert(typeof lineNumber === "number" && !isNaN(lineNumber) && lineNumber >= 0);
200        console.assert(typeof columnNumber === "number" && !isNaN(columnNumber) && columnNumber >= 0);
201
202        if (sourceCode === this._sourceCode && lineNumber === this._lineNumber && columnNumber === this._columnNumber)
203            return;
204        else if (this._mappedResource && sourceCode === this._mappedResource && lineNumber === this._mappedLineNumber && columnNumber === this._mappedColumnNumber)
205            return;
206
207        var newSourceCodeLocation = sourceCode.createSourceCodeLocation(lineNumber, columnNumber);
208        console.assert(newSourceCodeLocation.sourceCode === this._sourceCode);
209
210        this._makeChangeAndDispatchChangeEventIfNeeded(function() {
211            this._lineNumber = newSourceCodeLocation._lineNumber;
212            this._columnNumber = newSourceCodeLocation._columnNumber;
213            if (newSourceCodeLocation._mappedLocationIsResolved) {
214                this._mappedLocationIsResolved = true;
215                this._mappedResource = newSourceCodeLocation._mappedResource;
216                this._mappedLineNumber = newSourceCodeLocation._mappedLineNumber;
217                this._mappedColumnNumber = newSourceCodeLocation._mappedColumnNumber;
218            }
219        });
220    },
221
222    populateLiveDisplayLocationTooltip: function(element, prefix)
223    {
224        prefix = prefix || "";
225
226        element.title = prefix + this.tooltipString();
227
228        this.addEventListener(WebInspector.SourceCodeLocation.Event.DisplayLocationChanged, function(event) {
229            element.title = prefix + this.tooltipString();
230        }, this);
231    },
232
233    populateLiveDisplayLocationString: function(element, propertyName, columnStyle, nameStyle, prefix)
234    {
235        var currentDisplay = undefined;
236
237        function updateDisplayString(showAlternativeLocation, forceUpdate)
238        {
239            if (!forceUpdate && currentDisplay === showAlternativeLocation)
240                return;
241
242            currentDisplay = showAlternativeLocation;
243
244            if (!showAlternativeLocation) {
245                element[propertyName] = this.displayLocationString(columnStyle, nameStyle, prefix);
246                element.classList.toggle(WebInspector.SourceCodeLocation.DisplayLocationClassName, this.hasDifferentDisplayLocation());
247            } else if (this.hasDifferentDisplayLocation()) {
248                element[propertyName] = this.originalLocationString(columnStyle, nameStyle, prefix);
249                element.classList.remove(WebInspector.SourceCodeLocation.DisplayLocationClassName);
250            }
251        }
252
253        function mouseOverOrMove(event)
254        {
255            updateDisplayString.call(this, event.metaKey && !event.altKey && !event.shiftKey);
256        }
257
258        updateDisplayString.call(this, false);
259
260        this.addEventListener(WebInspector.SourceCodeLocation.Event.DisplayLocationChanged, function(event) {
261            updateDisplayString.call(this, currentDisplay, true);
262        }, this);
263
264        var boundMouseOverOrMove = mouseOverOrMove.bind(this);
265        element.addEventListener("mouseover", boundMouseOverOrMove);
266        element.addEventListener("mousemove", boundMouseOverOrMove);
267
268        element.addEventListener("mouseout", function(event) {
269            updateDisplayString.call(this, false);
270        }.bind(this));
271    },
272
273    // Protected
274
275    setSourceCode: function(sourceCode)
276    {
277        console.assert((this._sourceCode === null && sourceCode instanceof WebInspector.SourceCode) || (this._sourceCode instanceof WebInspector.SourceCode && sourceCode === null));
278
279        if (sourceCode === this._sourceCode)
280            return;
281
282        this._makeChangeAndDispatchChangeEventIfNeeded(function() {
283            if (this._sourceCode) {
284                this._sourceCode.removeEventListener(WebInspector.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this);
285                this._sourceCode.removeEventListener(WebInspector.SourceCode.Event.FormatterDidChange, this._sourceCodeFormatterDidChange, this);
286            }
287
288            this._sourceCode = sourceCode;
289
290            if (this._sourceCode) {
291                this._sourceCode.addEventListener(WebInspector.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this);
292                this._sourceCode.addEventListener(WebInspector.SourceCode.Event.FormatterDidChange, this._sourceCodeFormatterDidChange, this);
293            }
294        });
295    },
296
297    resolveMappedLocation: function()
298    {
299        if (this._mappedLocationIsResolved)
300            return;
301
302        console.assert(this._mappedResource === null);
303        console.assert(isNaN(this._mappedLineNumber));
304        console.assert(isNaN(this._mappedColumnNumber));
305
306        this._mappedLocationIsResolved = true;
307
308        if (!this._sourceCode)
309            return;
310
311        var sourceMaps = this._sourceCode.sourceMaps;
312        if (!sourceMaps.length)
313            return;
314
315        for (var i = 0; i < sourceMaps.length; ++i) {
316            var sourceMap = sourceMaps[i];
317            var entry = sourceMap.findEntry(this._lineNumber, this._columnNumber);
318            if (!entry || entry.length === 2)
319                continue;
320            console.assert(entry.length === 5);
321            var url = entry[2];
322            var sourceMapResource = sourceMap.resourceForURL(url);
323            if (!sourceMapResource)
324                return;
325            this._mappedResource = sourceMapResource;
326            this._mappedLineNumber = entry[3];
327            this._mappedColumnNumber = entry[4];
328            return;
329        }
330    },
331
332    // Private
333
334    _locationString: function(sourceCode, lineNumber, columnNumber, columnStyle, nameStyle, prefix)
335    {
336        console.assert(sourceCode);
337        if (!sourceCode)
338            return "";
339
340        columnStyle = columnStyle || WebInspector.SourceCodeLocation.ColumnStyle.OnlyIfLarge;
341        nameStyle = nameStyle || WebInspector.SourceCodeLocation.NameStyle.Short;
342        prefix = prefix || "";
343
344        var lineString = lineNumber + 1; // The user visible line number is 1-based.
345        if (columnStyle === WebInspector.SourceCodeLocation.ColumnStyle.Shown && columnNumber > 0)
346            lineString += ":" + (columnNumber + 1); // The user visible column number is 1-based.
347        else if (columnStyle === WebInspector.SourceCodeLocation.ColumnStyle.OnlyIfLarge && columnNumber > WebInspector.SourceCodeLocation.LargeColumnNumber)
348            lineString += ":" + (columnNumber + 1); // The user visible column number is 1-based.
349
350        switch (nameStyle) {
351        case WebInspector.SourceCodeLocation.NameStyle.None:
352            return prefix + lineString;
353
354        case WebInspector.SourceCodeLocation.NameStyle.Short:
355        case WebInspector.SourceCodeLocation.NameStyle.Full:
356            var lineSuffix = sourceCode.url ? ":" + lineString : WebInspector.UIString(" (line %s)").format(lineString);
357            return prefix + (nameStyle === WebInspector.SourceCodeLocation.NameStyle.Full && sourceCode.url ? sourceCode.url : sourceCode.displayName) + lineSuffix;
358
359        default:
360            console.error("Unknown nameStyle: " + nameStyle);
361            return prefix + lineString;
362        }
363    },
364
365    _resetMappedLocation: function()
366    {
367        this._mappedLocationIsResolved = false;
368        this._mappedResource = null;
369        this._mappedLineNumber = NaN;
370        this._mappedColumnNumber = NaN;
371    },
372
373    _setMappedLocation: function(mappedResource, mappedLineNumber, mappedColumnNumber)
374    {
375        // Called by SourceMapResource when it creates a SourceCodeLocation and already knows the resolved location.
376        this._mappedLocationIsResolved = true;
377        this._mappedResource = mappedResource;
378        this._mappedLineNumber = mappedLineNumber;
379        this._mappedColumnNumber = mappedColumnNumber;
380    },
381
382    _resolveFormattedLocation: function()
383    {
384        if (this._sourceCode && this._sourceCode.formatterSourceMap) {
385            var formattedLocation = this._sourceCode.formatterSourceMap.originalToFormatted(this._lineNumber, this._columnNumber);
386            this._formattedLineNumber = formattedLocation.lineNumber;
387            this._formattedColumnNumber = formattedLocation.columnNumber;
388        } else {
389            this._formattedLineNumber = this._lineNumber;
390            this._formattedColumnNumber = this._columnNumber;
391        }
392    },
393
394    _makeChangeAndDispatchChangeEventIfNeeded: function(changeFunction)
395    {
396        var oldSourceCode = this._sourceCode;
397        var oldLineNumber = this._lineNumber;
398        var oldColumnNumber = this._columnNumber;
399
400        var oldFormattedLineNumber = this._formattedLineNumber;
401        var oldFormattedColumnNumber = this._formattedColumnNumber;
402
403        var oldDisplaySourceCode = this.displaySourceCode;
404        var oldDisplayLineNumber = this.displayLineNumber;
405        var oldDisplayColumnNumber = this.displayColumnNumber;
406
407        this._resetMappedLocation();
408
409        if (changeFunction)
410            changeFunction.call(this);
411
412        this.resolveMappedLocation();
413        this._resolveFormattedLocation();
414
415        // If the display source code is non-null then the addresses are not NaN and can be compared.
416        var displayLocationChanged = false;
417        var newDisplaySourceCode = this.displaySourceCode;
418        if (oldDisplaySourceCode !== newDisplaySourceCode)
419            displayLocationChanged = true;
420        else if (newDisplaySourceCode && (oldDisplayLineNumber !== this.displayLineNumber || oldDisplayColumnNumber !== this.displayColumnNumber))
421            displayLocationChanged = true;
422
423        var anyLocationChanged = false;
424        if (displayLocationChanged)
425            anyLocationChanged = true;
426        else if (oldSourceCode !== this._sourceCode)
427            anyLocationChanged = true;
428        else if (this._sourceCode && (oldLineNumber !== this._lineNumber || oldColumnNumber !== this._columnNumber))
429            anyLocationChanged = true;
430        else if (this._sourceCode && (oldFormattedLineNumber !== this._formattedLineNumber || oldFormattedColumnNumber !== this._formattedColumnNumber))
431            anyLocationChanged = true;
432
433        if (displayLocationChanged || anyLocationChanged) {
434            var oldData = {
435                oldSourceCode: oldSourceCode,
436                oldLineNumber: oldLineNumber,
437                oldColumnNumber: oldColumnNumber,
438                oldFormattedLineNumber: oldFormattedLineNumber,
439                oldFormattedColumnNumber: oldFormattedColumnNumber,
440                oldDisplaySourceCode: oldDisplaySourceCode,
441                oldDisplayLineNumber: oldDisplayLineNumber,
442                oldDisplayColumnNumber: oldDisplayColumnNumber
443            };
444            if (displayLocationChanged)
445                this.dispatchEventToListeners(WebInspector.SourceCodeLocation.Event.DisplayLocationChanged, oldData);
446            if (anyLocationChanged)
447                this.dispatchEventToListeners(WebInspector.SourceCodeLocation.Event.LocationChanged, oldData);
448        }
449    },
450
451    _sourceCodeSourceMapAdded: function()
452    {
453        this._makeChangeAndDispatchChangeEventIfNeeded(null);
454    },
455
456    _sourceCodeFormatterDidChange: function()
457    {
458        this._makeChangeAndDispatchChangeEventIfNeeded(null);
459    }
460};
461
462WebInspector.SourceCodeLocation.prototype.__proto__ = WebInspector.Object.prototype;
463