• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2009 Google 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 are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31WebInspector.SourceFrame = function(parentElement, addBreakpointDelegate, removeBreakpointDelegate)
32{
33    this._parentElement = parentElement;
34
35    this._textModel = new WebInspector.TextEditorModel();
36    this._textModel.replaceTabsWithSpaces = true;
37
38    this._messages = [];
39    this._rowMessages = {};
40    this._messageBubbles = {};
41    this.breakpoints = [];
42    this._shortcuts = {};
43
44    this._loaded = false;
45
46    this._addBreakpointDelegate = addBreakpointDelegate;
47    this._removeBreakpointDelegate = removeBreakpointDelegate;
48}
49
50WebInspector.SourceFrame.prototype = {
51
52    set visible(visible)
53    {
54        this._visible = visible;
55        this._createViewerIfNeeded();
56    },
57
58    get executionLine()
59    {
60        return this._executionLine;
61    },
62
63    set executionLine(x)
64    {
65        if (this._executionLine === x)
66            return;
67
68        var previousLine = this._executionLine;
69        this._executionLine = x;
70
71        if (this._textViewer)
72            this._updateExecutionLine(previousLine);
73    },
74
75    revealLine: function(lineNumber)
76    {
77        if (this._textViewer)
78            this._textViewer.revealLine(lineNumber - 1, 0);
79        else
80            this._lineNumberToReveal = lineNumber;
81    },
82
83    addBreakpoint: function(breakpoint)
84    {
85        this.breakpoints.push(breakpoint);
86        breakpoint.addEventListener("enabled", this._breakpointChanged, this);
87        breakpoint.addEventListener("disabled", this._breakpointChanged, this);
88        breakpoint.addEventListener("condition-changed", this._breakpointChanged, this);
89        if (this._textViewer)
90            this._addBreakpointToSource(breakpoint);
91    },
92
93    removeBreakpoint: function(breakpoint)
94    {
95        this.breakpoints.remove(breakpoint);
96        breakpoint.removeEventListener("enabled", null, this);
97        breakpoint.removeEventListener("disabled", null, this);
98        breakpoint.removeEventListener("condition-changed", null, this);
99        if (this._textViewer)
100            this._removeBreakpointFromSource(breakpoint);
101    },
102
103    addMessage: function(msg)
104    {
105        // Don't add the message if there is no message or valid line or if the msg isn't an error or warning.
106        if (!msg.message || msg.line <= 0 || !msg.isErrorOrWarning())
107            return;
108        this._messages.push(msg)
109        if (this._textViewer)
110            this._addMessageToSource(msg);
111    },
112
113    clearMessages: function()
114    {
115        for (var line in this._messageBubbles) {
116            var bubble = this._messageBubbles[line];
117            bubble.parentNode.removeChild(bubble);
118        }
119
120        this._messages = [];
121        this._rowMessages = {};
122        this._messageBubbles = {};
123        if (this._textViewer)
124            this._textViewer.resize();
125    },
126
127    sizeToFitContentHeight: function()
128    {
129        if (this._textViewer)
130            this._textViewer.revalidateDecorationsAndPaint();
131    },
132
133    setContent: function(mimeType, content, url)
134    {
135        this._loaded = true;
136        this._textModel.setText(null, content);
137        this._mimeType = mimeType;
138        this._url = url;
139        this._createViewerIfNeeded();
140    },
141
142    highlightLine: function(line)
143    {
144        if (this._textViewer)
145            this._textViewer.highlightLine(line - 1);
146        else
147            this._lineToHighlight = line;
148    },
149
150    _createViewerIfNeeded: function()
151    {
152        if (!this._visible || !this._loaded || this._textViewer)
153            return;
154
155        this._textViewer = new WebInspector.TextViewer(this._textModel, WebInspector.platform, this._url);
156        var element = this._textViewer.element;
157        element.addEventListener("keydown", this._keyDown.bind(this), true);
158        element.addEventListener("contextmenu", this._contextMenu.bind(this), true);
159        element.addEventListener("mousedown", this._mouseDown.bind(this), true);
160        this._parentElement.appendChild(element);
161
162        this._needsProgramCounterImage = true;
163        this._needsBreakpointImages = true;
164
165        this._textViewer.beginUpdates();
166
167        this._textViewer.mimeType = this._mimeType;
168        this._addExistingMessagesToSource();
169        this._addExistingBreakpointsToSource();
170        this._updateExecutionLine();
171        this._textViewer.resize();
172
173        if (this._lineNumberToReveal) {
174            this.revealLine(this._lineNumberToReveal);
175            delete this._lineNumberToReveal;
176        }
177
178        if (this._pendingMarkRange) {
179            var range = this._pendingMarkRange;
180            this.markAndRevealRange(range);
181            delete this._pendingMarkRange;
182        }
183
184        if (this._lineToHighlight) {
185            this.highlightLine(this._lineToHighlight);
186            delete this._lineToHighlight;
187        }
188        this._textViewer.endUpdates();
189    },
190
191    findSearchMatches: function(query)
192    {
193        var ranges = [];
194
195        // First do case-insensitive search.
196        var regex = "";
197        for (var i = 0; i < query.length; ++i) {
198            var char = query.charAt(i);
199            if (char === "]")
200                char = "\\]";
201            regex += "[" + char + "]";
202        }
203        var regexObject = new RegExp(regex, "i");
204        this._collectRegexMatches(regexObject, ranges);
205
206        // Then try regex search if user knows the / / hint.
207        try {
208            if (/^\/.*\/$/.test(query))
209                this._collectRegexMatches(new RegExp(query.substring(1, query.length - 1)), ranges);
210        } catch (e) {
211            // Silent catch.
212        }
213        return ranges;
214    },
215
216    _collectRegexMatches: function(regexObject, ranges)
217    {
218        for (var i = 0; i < this._textModel.linesCount; ++i) {
219            var line = this._textModel.line(i);
220            var offset = 0;
221            do {
222                var match = regexObject.exec(line);
223                if (match) {
224                    ranges.push(new WebInspector.TextRange(i, offset + match.index, i, offset + match.index + match[0].length));
225                    offset += match.index + 1;
226                    line = line.substring(match.index + 1);
227                }
228            } while (match)
229        }
230        return ranges;
231    },
232
233    markAndRevealRange: function(range)
234    {
235        if (this._textViewer)
236            this._textViewer.markAndRevealRange(range);
237        else
238            this._pendingMarkRange = range;
239    },
240
241    clearMarkedRange: function()
242    {
243        if (this._textViewer) {
244            this._textViewer.markAndRevealRange(null);
245        } else
246            delete this._pendingMarkRange;
247    },
248
249    _incrementMessageRepeatCount: function(msg, repeatDelta)
250    {
251        if (!msg._resourceMessageLineElement)
252            return;
253
254        if (!msg._resourceMessageRepeatCountElement) {
255            var repeatedElement = document.createElement("span");
256            msg._resourceMessageLineElement.appendChild(repeatedElement);
257            msg._resourceMessageRepeatCountElement = repeatedElement;
258        }
259
260        msg.repeatCount += repeatDelta;
261        msg._resourceMessageRepeatCountElement.textContent = WebInspector.UIString(" (repeated %d times)", msg.repeatCount);
262    },
263
264    _breakpointChanged: function(event)
265    {
266        var breakpoint = event.target;
267        var lineNumber = breakpoint.line - 1;
268        if (lineNumber >= this._textModel.linesCount)
269            return;
270
271        if (breakpoint.enabled)
272            this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
273        else
274            this._textViewer.addDecoration(lineNumber, "webkit-breakpoint-disabled");
275
276        if (breakpoint.condition)
277            this._textViewer.addDecoration(lineNumber, "webkit-breakpoint-conditional");
278        else
279            this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
280    },
281
282    _updateExecutionLine: function(previousLine)
283    {
284        if (previousLine) {
285            if (previousLine - 1 < this._textModel.linesCount)
286                this._textViewer.removeDecoration(previousLine - 1, "webkit-execution-line");
287        }
288
289        if (!this._executionLine)
290            return;
291
292        this._drawProgramCounterImageIfNeeded();
293
294        if (this._executionLine < this._textModel.linesCount)
295            this._textViewer.addDecoration(this._executionLine - 1, "webkit-execution-line");
296    },
297
298    _addExistingMessagesToSource: function()
299    {
300        var length = this._messages.length;
301        for (var i = 0; i < length; ++i)
302            this._addMessageToSource(this._messages[i]);
303    },
304
305    _addMessageToSource: function(msg)
306    {
307        if (msg.line >= this._textModel.linesCount)
308            return;
309
310        var messageBubbleElement = this._messageBubbles[msg.line];
311        if (!messageBubbleElement || messageBubbleElement.nodeType !== Node.ELEMENT_NODE || !messageBubbleElement.hasStyleClass("webkit-html-message-bubble")) {
312            messageBubbleElement = document.createElement("div");
313            messageBubbleElement.className = "webkit-html-message-bubble";
314            this._messageBubbles[msg.line] = messageBubbleElement;
315            this._textViewer.addDecoration(msg.line - 1, messageBubbleElement);
316        }
317
318        var rowMessages = this._rowMessages[msg.line];
319        if (!rowMessages) {
320            rowMessages = [];
321            this._rowMessages[msg.line] = rowMessages;
322        }
323
324        for (var i = 0; i < rowMessages.length; ++i) {
325            if (rowMessages[i].isEqual(msg, true)) {
326                this._incrementMessageRepeatCount(rowMessages[i], msg.repeatDelta);
327                return;
328            }
329        }
330
331        rowMessages.push(msg);
332
333        var imageURL;
334        switch (msg.level) {
335            case WebInspector.ConsoleMessage.MessageLevel.Error:
336                messageBubbleElement.addStyleClass("webkit-html-error-message");
337                imageURL = "Images/errorIcon.png";
338                break;
339            case WebInspector.ConsoleMessage.MessageLevel.Warning:
340                messageBubbleElement.addStyleClass("webkit-html-warning-message");
341                imageURL = "Images/warningIcon.png";
342                break;
343        }
344
345        var messageLineElement = document.createElement("div");
346        messageLineElement.className = "webkit-html-message-line";
347        messageBubbleElement.appendChild(messageLineElement);
348
349        // Create the image element in the Inspector's document so we can use relative image URLs.
350        var image = document.createElement("img");
351        image.src = imageURL;
352        image.className = "webkit-html-message-icon";
353        messageLineElement.appendChild(image);
354        messageLineElement.appendChild(document.createTextNode(msg.message));
355
356        msg._resourceMessageLineElement = messageLineElement;
357    },
358
359    _addExistingBreakpointsToSource: function()
360    {
361        for (var i = 0; i < this.breakpoints.length; ++i)
362            this._addBreakpointToSource(this.breakpoints[i]);
363    },
364
365    _addBreakpointToSource: function(breakpoint)
366    {
367        var lineNumber = breakpoint.line - 1;
368        if (lineNumber >= this._textModel.linesCount)
369            return;
370
371        this._textModel.setAttribute(lineNumber, "breakpoint", breakpoint);
372        breakpoint.sourceText = this._textModel.line(breakpoint.line - 1);
373        this._drawBreakpointImagesIfNeeded();
374
375        this._textViewer.beginUpdates();
376        this._textViewer.addDecoration(lineNumber, "webkit-breakpoint");
377        if (!breakpoint.enabled)
378            this._textViewer.addDecoration(lineNumber, "webkit-breakpoint-disabled");
379        if (breakpoint.condition)
380            this._textViewer.addDecoration(lineNumber, "webkit-breakpoint-conditional");
381        this._textViewer.endUpdates();
382    },
383
384    _removeBreakpointFromSource: function(breakpoint)
385    {
386        var lineNumber = breakpoint.line - 1;
387        this._textViewer.beginUpdates();
388        this._textModel.removeAttribute(lineNumber, "breakpoint");
389        this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint");
390        this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-disabled");
391        this._textViewer.removeDecoration(lineNumber, "webkit-breakpoint-conditional");
392        this._textViewer.endUpdates();
393    },
394
395    _contextMenu: function(event)
396    {
397        if (event.target.className !== "webkit-line-number")
398            return;
399        var row = event.target.parentElement;
400
401        var lineNumber = row.lineNumber;
402        var contextMenu = new WebInspector.ContextMenu();
403
404        var breakpoint = this._textModel.getAttribute(lineNumber, "breakpoint");
405        if (!breakpoint) {
406            // This row doesn't have a breakpoint: We want to show Add Breakpoint and Add and Edit Breakpoint.
407            contextMenu.appendItem(WebInspector.UIString("Add Breakpoint"), this._addBreakpointDelegate.bind(this, lineNumber + 1));
408
409            function addConditionalBreakpoint()
410            {
411                this._addBreakpointDelegate(lineNumber + 1);
412                var breakpoint = this._textModel.getAttribute(lineNumber, "breakpoint");
413                if (breakpoint)
414                    this._editBreakpointCondition(breakpoint);
415            }
416
417            contextMenu.appendItem(WebInspector.UIString("Add Conditional Breakpoint..."), addConditionalBreakpoint.bind(this));
418        } else {
419            // This row has a breakpoint, we want to show edit and remove breakpoint, and either disable or enable.
420            contextMenu.appendItem(WebInspector.UIString("Remove Breakpoint"), WebInspector.panels.scripts.removeBreakpoint.bind(WebInspector.panels.scripts, breakpoint));
421            contextMenu.appendItem(WebInspector.UIString("Edit Breakpoint..."), this._editBreakpointCondition.bind(this, breakpoint));
422            if (breakpoint.enabled)
423                contextMenu.appendItem(WebInspector.UIString("Disable Breakpoint"), function() { breakpoint.enabled = false; });
424            else
425                contextMenu.appendItem(WebInspector.UIString("Enable Breakpoint"), function() { breakpoint.enabled = true; });
426        }
427        contextMenu.show(event);
428    },
429
430    _mouseDown: function(event)
431    {
432        if (event.button != 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
433            return;
434        if (event.target.className !== "webkit-line-number")
435            return;
436        var row = event.target.parentElement;
437
438        var lineNumber = row.lineNumber;
439
440        var breakpoint = this._textModel.getAttribute(lineNumber, "breakpoint");
441        if (breakpoint)
442            this._removeBreakpointDelegate(breakpoint);
443        else if (this._addBreakpointDelegate)
444            this._addBreakpointDelegate(lineNumber + 1);
445        event.preventDefault();
446    },
447
448    _editBreakpointCondition: function(breakpoint)
449    {
450        this._showBreakpointConditionPopup(breakpoint.line);
451
452        function committed(element, newText)
453        {
454            breakpoint.condition = newText;
455            dismissed.call(this);
456        }
457
458        function dismissed()
459        {
460            if (this._conditionElement)
461                this._textViewer.removeDecoration(breakpoint.line - 1, this._conditionElement);
462            delete this._conditionEditorElement;
463            delete this._conditionElement;
464        }
465
466        var dismissedHandler = dismissed.bind(this);
467        this._conditionEditorElement.addEventListener("blur", dismissedHandler, false);
468
469        WebInspector.startEditing(this._conditionEditorElement, committed.bind(this), dismissedHandler);
470        this._conditionEditorElement.value = breakpoint.condition;
471        this._conditionEditorElement.select();
472    },
473
474    _showBreakpointConditionPopup: function(lineNumber)
475    {
476        this._conditionElement = this._createConditionElement(lineNumber);
477        this._textViewer.addDecoration(lineNumber - 1, this._conditionElement);
478    },
479
480    _createConditionElement: function(lineNumber)
481    {
482        var conditionElement = document.createElement("div");
483        conditionElement.className = "source-frame-breakpoint-condition";
484
485        var labelElement = document.createElement("label");
486        labelElement.className = "source-frame-breakpoint-message";
487        labelElement.htmlFor = "source-frame-breakpoint-condition";
488        labelElement.appendChild(document.createTextNode(WebInspector.UIString("The breakpoint on line %d will stop only if this expression is true:", lineNumber)));
489        conditionElement.appendChild(labelElement);
490
491        var editorElement = document.createElement("input");
492        editorElement.id = "source-frame-breakpoint-condition";
493        editorElement.className = "monospace";
494        editorElement.type = "text"
495        conditionElement.appendChild(editorElement);
496        this._conditionEditorElement = editorElement;
497
498        return conditionElement;
499    },
500
501    _keyDown: function(event)
502    {
503        var shortcut = WebInspector.KeyboardShortcut.makeKeyFromEvent(event);
504        var handler = this._shortcuts[shortcut];
505        if (handler) {
506            handler(event);
507            event.preventDefault();
508        } else
509            WebInspector.documentKeyDown(event);
510    },
511
512    _evalSelectionInCallFrame: function(event)
513    {
514        if (!WebInspector.panels.scripts || !WebInspector.panels.scripts.paused)
515            return;
516
517        var selection = this.element.contentWindow.getSelection();
518        if (!selection.rangeCount)
519            return;
520
521        var expression = selection.getRangeAt(0).toString().trimWhitespace();
522        WebInspector.panels.scripts.evaluateInSelectedCallFrame(expression, false, "console", function(result, exception) {
523            WebInspector.showConsole();
524            var commandMessage = new WebInspector.ConsoleCommand(expression);
525            WebInspector.console.addMessage(commandMessage);
526            WebInspector.console.addMessage(new WebInspector.ConsoleCommandResult(result, exception, commandMessage));
527        });
528    },
529
530    resize: function()
531    {
532        if (this._textViewer)
533            this._textViewer.resize();
534    },
535
536    _drawProgramCounterInContext: function(ctx, glow)
537    {
538        if (glow)
539            ctx.save();
540
541        ctx.beginPath();
542        ctx.moveTo(17, 2);
543        ctx.lineTo(19, 2);
544        ctx.lineTo(19, 0);
545        ctx.lineTo(21, 0);
546        ctx.lineTo(26, 5.5);
547        ctx.lineTo(21, 11);
548        ctx.lineTo(19, 11);
549        ctx.lineTo(19, 9);
550        ctx.lineTo(17, 9);
551        ctx.closePath();
552        ctx.fillStyle = "rgb(142, 5, 4)";
553
554        if (glow) {
555            ctx.shadowBlur = 4;
556            ctx.shadowColor = "rgb(255, 255, 255)";
557            ctx.shadowOffsetX = -1;
558            ctx.shadowOffsetY = 0;
559        }
560
561        ctx.fill();
562        ctx.fill(); // Fill twice to get a good shadow and darker anti-aliased pixels.
563
564        if (glow)
565            ctx.restore();
566    },
567
568    _drawProgramCounterImageIfNeeded: function()
569    {
570        if (!this._needsProgramCounterImage)
571            return;
572
573        var ctx = document.getCSSCanvasContext("2d", "program-counter", 26, 11);
574        ctx.clearRect(0, 0, 26, 11);
575        this._drawProgramCounterInContext(ctx, true);
576
577        delete this._needsProgramCounterImage;
578    },
579
580    _drawBreakpointImagesIfNeeded: function(conditional)
581    {
582        if (!this._needsBreakpointImages)
583            return;
584
585        function drawBreakpoint(ctx, disabled, conditional)
586        {
587            ctx.beginPath();
588            ctx.moveTo(0, 2);
589            ctx.lineTo(2, 0);
590            ctx.lineTo(21, 0);
591            ctx.lineTo(26, 5.5);
592            ctx.lineTo(21, 11);
593            ctx.lineTo(2, 11);
594            ctx.lineTo(0, 9);
595            ctx.closePath();
596            ctx.fillStyle = conditional ? "rgb(217, 142, 1)" : "rgb(1, 142, 217)";
597            ctx.strokeStyle = conditional ? "rgb(205, 103, 0)" : "rgb(0, 103, 205)";
598            ctx.lineWidth = 3;
599            ctx.fill();
600            ctx.save();
601            ctx.clip();
602            ctx.stroke();
603            ctx.restore();
604
605            if (!disabled)
606                return;
607
608            ctx.save();
609            ctx.globalCompositeOperation = "destination-out";
610            ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
611            ctx.fillRect(0, 0, 26, 11);
612            ctx.restore();
613        }
614
615
616        // Unconditional breakpoints.
617
618        var ctx = document.getCSSCanvasContext("2d", "breakpoint", 26, 11);
619        ctx.clearRect(0, 0, 26, 11);
620        drawBreakpoint(ctx);
621
622        var ctx = document.getCSSCanvasContext("2d", "breakpoint-program-counter", 26, 11);
623        ctx.clearRect(0, 0, 26, 11);
624        drawBreakpoint(ctx);
625        ctx.clearRect(20, 0, 6, 11);
626        this._drawProgramCounterInContext(ctx, true);
627
628        var ctx = document.getCSSCanvasContext("2d", "breakpoint-disabled", 26, 11);
629        ctx.clearRect(0, 0, 26, 11);
630        drawBreakpoint(ctx, true);
631
632        var ctx = document.getCSSCanvasContext("2d", "breakpoint-disabled-program-counter", 26, 11);
633        ctx.clearRect(0, 0, 26, 11);
634        drawBreakpoint(ctx, true);
635        ctx.clearRect(20, 0, 6, 11);
636        this._drawProgramCounterInContext(ctx, true);
637
638
639        // Conditional breakpoints.
640
641        var ctx = document.getCSSCanvasContext("2d", "breakpoint-conditional", 26, 11);
642        ctx.clearRect(0, 0, 26, 11);
643        drawBreakpoint(ctx, false, true);
644
645        var ctx = document.getCSSCanvasContext("2d", "breakpoint-conditional-program-counter", 26, 11);
646        ctx.clearRect(0, 0, 26, 11);
647        drawBreakpoint(ctx, false, true);
648        ctx.clearRect(20, 0, 6, 11);
649        this._drawProgramCounterInContext(ctx, true);
650
651        var ctx = document.getCSSCanvasContext("2d", "breakpoint-disabled-conditional", 26, 11);
652        ctx.clearRect(0, 0, 26, 11);
653        drawBreakpoint(ctx, true, true);
654
655        var ctx = document.getCSSCanvasContext("2d", "breakpoint-disabled-conditional-program-counter", 26, 11);
656        ctx.clearRect(0, 0, 26, 11);
657        drawBreakpoint(ctx, true, true);
658        ctx.clearRect(20, 0, 6, 11);
659        this._drawProgramCounterInContext(ctx, true);
660
661        delete this._needsBreakpointImages;
662    }
663}
664
665WebInspector.SourceFrame.prototype.__proto__ = WebInspector.Object.prototype;
666