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.CSSStyleDeclarationTextEditor = function(delegate, style, element)
27{
28    WebInspector.Object.call(this);
29
30    this._element = element || document.createElement("div");
31    this._element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.StyleClassName);
32    this._element.classList.add(WebInspector.SyntaxHighlightedStyleClassName);
33
34    this._showsImplicitProperties = true;
35    this._alwaysShowPropertyNames = {};
36    this._sortProperties = false;
37
38    this._prefixWhitespace = "";
39    this._suffixWhitespace = "";
40    this._linePrefixWhitespace = "";
41
42    this._delegate = delegate || null;
43
44    this._codeMirror = CodeMirror(this.element, {
45        readOnly: true,
46        lineWrapping: true,
47        mode: "css-rule",
48        electricChars: false,
49        indentWithTabs: true,
50        indentUnit: 4,
51        smartIndent: false,
52        matchBrackets: true,
53        autoCloseBrackets: true
54    });
55
56    this._codeMirror.on("change", this._contentChanged.bind(this));
57    this._codeMirror.on("blur", this._editorBlured.bind(this));
58
59    this._completionController = new WebInspector.CodeMirrorCompletionController(this._codeMirror, this);
60    this._tokenTrackingController = new WebInspector.CodeMirrorTokenTrackingController(this._codeMirror, this);
61
62    this._jumpToSymbolTrackingModeEnabled = false;
63    this._tokenTrackingController.classNameForHighlightedRange = WebInspector.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName;
64    this._tokenTrackingController.mouseOverDelayDuration = 0;
65    this._tokenTrackingController.mouseOutReleaseDelayDuration = 0;
66    this._tokenTrackingController.mode = WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens;
67
68    this.style = style;
69};
70
71WebInspector.Object.addConstructorFunctions(WebInspector.CSSStyleDeclarationTextEditor);
72
73WebInspector.CSSStyleDeclarationTextEditor.StyleClassName = "css-style-text-editor";
74WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName = "read-only";
75WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName = "color-swatch";
76WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName = "checkbox-placeholder";
77WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName = "editing-line";
78WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay = 250;
79WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay = 2000;
80
81WebInspector.CSSStyleDeclarationTextEditor.prototype = {
82    constructor: WebInspector.CSSStyleDeclarationTextEditor,
83
84    // Public
85
86    get element()
87    {
88        return this._element;
89    },
90
91    get delegate()
92    {
93        return this._delegate;
94    },
95
96    set delegate(delegate)
97    {
98        this._delegate = delegate || null;
99    },
100
101    get style()
102    {
103        return this._style;
104    },
105
106    set style(style)
107    {
108        if (this._style === style)
109            return;
110
111        if (this._style) {
112            this._style.removeEventListener(WebInspector.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);
113            if (this._style.ownerRule && this._style.ownerRule.sourceCodeLocation)
114                WebInspector.notifications.removeEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateJumpToSymbolTrackingMode, this);
115        }
116
117        this._style = style || null;
118
119        if (this._style) {
120            this._style.addEventListener(WebInspector.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);
121            if (this._style.ownerRule && this._style.ownerRule.sourceCodeLocation)
122                WebInspector.notifications.addEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateJumpToSymbolTrackingMode, this);
123        }
124
125        this._updateJumpToSymbolTrackingMode();
126
127        this._resetContent();
128    },
129
130    get focused()
131    {
132        return this._codeMirror.getWrapperElement().classList.contains("CodeMirror-focused");
133    },
134
135    get alwaysShowPropertyNames()
136    {
137        return Object.keys(this._alwaysShowPropertyNames);
138    },
139
140    set alwaysShowPropertyNames(alwaysShowPropertyNames)
141    {
142        this._alwaysShowPropertyNames = (alwaysShowPropertyNames || []).keySet();
143
144        this._resetContent();
145    },
146
147    get showsImplicitProperties()
148    {
149        return this._showsImplicitProperties;
150    },
151
152    set showsImplicitProperties(showsImplicitProperties)
153    {
154        if (this._showsImplicitProperties === showsImplicitProperties)
155            return;
156
157        this._showsImplicitProperties = showsImplicitProperties;
158
159        this._resetContent();
160    },
161
162    get sortProperties()
163    {
164        return this._sortProperties;
165    },
166
167    set sortProperties(sortProperties)
168    {
169        if (this._sortProperties === sortProperties)
170            return;
171
172        this._sortProperties = sortProperties;
173
174        this._resetContent();
175    },
176
177    focus: function()
178    {
179        this._codeMirror.focus();
180    },
181
182    refresh: function()
183    {
184        this._resetContent();
185    },
186
187    updateLayout: function(force)
188    {
189        this._codeMirror.refresh();
190    },
191
192    // Protected
193
194    didDismissPopover: function(popover)
195    {
196        if (popover === this._colorPickerPopover)
197            delete this._colorPickerPopover;
198    },
199
200    completionControllerCompletionsHidden: function(completionController)
201    {
202        var styleText = this._style.text;
203        var currentText = this._formattedContent();
204
205        // If the style text and the current editor text differ then we need to commit.
206        // Otherwise we can just update the properties that got skipped because a completion
207        // was pending the last time _propertiesChanged was called.
208        if (styleText !== currentText)
209            this._commitChanges();
210        else
211            this._propertiesChanged();
212    },
213
214    // Private
215
216    _clearRemoveEditingLineClassesTimeout: function()
217    {
218        if (!this._removeEditingLineClassesTimeout)
219            return;
220
221        clearTimeout(this._removeEditingLineClassesTimeout);
222        delete this._removeEditingLineClassesTimeout;
223    },
224
225    _removeEditingLineClasses: function()
226    {
227        this._clearRemoveEditingLineClassesTimeout();
228
229        function removeEditingLineClasses()
230        {
231            var lineCount = this._codeMirror.lineCount();
232            for (var i = 0; i < lineCount; ++i)
233                this._codeMirror.removeLineClass(i, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);
234        }
235
236        this._codeMirror.operation(removeEditingLineClasses.bind(this));
237    },
238
239    _removeEditingLineClassesSoon: function()
240    {
241        if (this._removeEditingLineClassesTimeout)
242            return;
243        this._removeEditingLineClassesTimeout = setTimeout(this._removeEditingLineClasses.bind(this), WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay);
244    },
245
246    _formattedContent: function()
247    {
248        // Start with the prefix whitespace we stripped.
249        var content = this._prefixWhitespace;
250
251        // Get each line and add the line prefix whitespace and newlines.
252        var lineCount = this._codeMirror.lineCount();
253        for (var i = 0; i < lineCount; ++i) {
254            var lineContent = this._codeMirror.getLine(i);
255            content += this._linePrefixWhitespace + lineContent;
256            if (i !== lineCount - 1)
257                content += "\n";
258        }
259
260        // Add the suffix whitespace we stripped.
261        content += this._suffixWhitespace;
262
263        return content;
264    },
265
266    _commitChanges: function()
267    {
268        if (this._commitChangesTimeout) {
269            clearTimeout(this._commitChangesTimeout);
270            delete this._commitChangesTimeout;
271        }
272
273        this._style.text = this._formattedContent();
274    },
275
276    _editorBlured: function(codeMirror)
277    {
278        // Clicking a suggestion causes the editor to blur. We don't want to reset content in this case.
279        if (this._completionController.isHandlingClickEvent())
280            return;
281
282        // Reset the content on blur since we stop accepting external changes while the the editor is focused.
283        // This causes us to pick up any change that was suppressed while the editor was focused.
284        this._resetContent();
285    },
286
287    _contentChanged: function(codeMirror, change)
288    {
289        // Return early if the style isn't editable. This still can be called when readOnly is set because
290        // clicking on a color swatch modifies the text.
291        if (!this._style || !this._style.editable || this._ignoreCodeMirrorContentDidChangeEvent)
292            return;
293
294        this._markLinesWithCheckboxPlaceholder();
295
296        this._clearRemoveEditingLineClassesTimeout();
297        this._codeMirror.addLineClass(change.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);
298
299        // When the change is a completion change, create color swatches now since the changes
300        // will not go through _propertiesChanged until completionControllerCompletionsHidden happens.
301        // This way any auto completed colors get swatches right away.
302        if (this._completionController.isCompletionChange(change))
303            this._createColorSwatches(false, change.from.line);
304
305        // Use a short delay for user input to coalesce more changes before committing. Other actions like
306        // undo, redo and paste are atomic and work better with a zero delay. CodeMirror identifies changes that
307        // get coalesced in the undo stack with a "+" prefix on the origin. Use that to set the delay for our coalescing.
308        const delay = change.origin && change.origin.charAt(0) === "+" ? WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay : 0;
309
310        // Reset the timeout so rapid changes coalesce after a short delay.
311        if (this._commitChangesTimeout)
312            clearTimeout(this._commitChangesTimeout);
313        this._commitChangesTimeout = setTimeout(this._commitChanges.bind(this), delay);
314    },
315
316    _updateTextMarkers: function(nonatomic)
317    {
318        function update()
319        {
320            this._clearTextMarkers(true);
321
322            var styleText = this._style.text;
323
324            this._iterateOverProperties(true, function(property) {
325                var styleTextRange = property.styleDeclarationTextRange;
326                console.assert(styleTextRange);
327                if (!styleTextRange)
328                    return;
329
330                var from = {line: styleTextRange.startLine, ch: styleTextRange.startColumn};
331                var to = {line: styleTextRange.endLine, ch: styleTextRange.endColumn};
332
333                // Adjust the line position for the missing prefix line.
334                if (this._prefixWhitespace) {
335                    --from.line;
336                    --to.line;
337                }
338
339                // Adjust the column for the stripped line prefix whitespace.
340                from.ch -= this._linePrefixWhitespace.length;
341                to.ch -= this._linePrefixWhitespace.length;
342
343                this._createTextMarkerForPropertyIfNeeded(from, to, property);
344            });
345
346            if (!this._codeMirror.getOption("readOnly")) {
347                // Matches a comment like: /* -webkit-foo: bar; */
348                const commentedPropertyRegex = /\/\*\s*[-\w]+\s*:\s*[^;]+;?\s*\*\//g;
349
350                // Look for comments that look like properties and add checkboxes in front of them.
351                var lineCount = this._codeMirror.lineCount();
352                for (var i = 0; i < lineCount; ++i) {
353                    var lineContent = this._codeMirror.getLine(i);
354
355                    var match = commentedPropertyRegex.exec(lineContent);
356                    while (match) {
357                        var checkboxElement = document.createElement("input");
358                        checkboxElement.type = "checkbox";
359                        checkboxElement.checked = false;
360                        checkboxElement.addEventListener("change", this._propertyCommentCheckboxChanged.bind(this));
361
362                        var from = {line: i, ch: match.index};
363                        var to = {line: i, ch: match.index + match[0].length};
364
365                        var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement);
366                        checkboxMarker.__propertyCheckbox = true;
367
368                        var commentTextMarker = this._codeMirror.markText(from, to);
369
370                        checkboxElement.__commentTextMarker = commentTextMarker;
371
372                        match = commentedPropertyRegex.exec(lineContent);
373                    }
374                }
375            }
376
377            // Look for colors and make swatches.
378            this._createColorSwatches(true);
379
380            this._markLinesWithCheckboxPlaceholder();
381        }
382
383        if (nonatomic)
384            update.call(this);
385        else
386            this._codeMirror.operation(update.bind(this));
387    },
388
389    _createColorSwatches: function(nonatomic, lineNumber)
390    {
391        function update()
392        {
393            var range = typeof lineNumber === "number" ? new WebInspector.TextRange(lineNumber, 0, lineNumber + 1, 0) : null;
394
395            // Look for color strings and add swatches in front of them.
396            this._codeMirror.createColorMarkers(range, function(marker, color, colorString) {
397                var swatchElement = document.createElement("span");
398                swatchElement.title = WebInspector.UIString("Click to open a colorpicker. Shift-click to change color format.");
399                swatchElement.className = WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName;
400                swatchElement.addEventListener("click", this._colorSwatchClicked.bind(this));
401
402                var swatchInnerElement = document.createElement("span");
403                swatchInnerElement.style.backgroundColor = colorString;
404                swatchElement.appendChild(swatchInnerElement);
405
406                var codeMirrorTextMarker = marker.codeMirrorTextMarker;
407                var swatchMarker = this._codeMirror.setUniqueBookmark(codeMirrorTextMarker.find().from, swatchElement);
408
409                swatchInnerElement.__colorTextMarker = codeMirrorTextMarker;
410                swatchInnerElement.__color = color;
411            }.bind(this));
412        }
413
414        if (nonatomic)
415            update.call(this);
416        else
417            this._codeMirror.operation(update.bind(this));
418    },
419
420    _updateTextMarkerForPropertyIfNeeded: function(property)
421    {
422        var textMarker = property.__propertyTextMarker;
423        console.assert(textMarker);
424        if (!textMarker)
425            return;
426
427        var range = textMarker.find();
428        console.assert(range);
429        if (!range)
430            return;
431
432        this._createTextMarkerForPropertyIfNeeded(range.from, range.to, property);
433    },
434
435    _createTextMarkerForPropertyIfNeeded: function(from, to, property)
436    {
437        if (!this._codeMirror.getOption("readOnly")) {
438            // Create a new checkbox element and marker.
439
440            console.assert(property.enabled);
441
442            var checkboxElement = document.createElement("input");
443            checkboxElement.type = "checkbox";
444            checkboxElement.checked = true;
445            checkboxElement.addEventListener("change", this._propertyCheckboxChanged.bind(this));
446            checkboxElement.__cssProperty = property;
447
448            var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement);
449            checkboxMarker.__propertyCheckbox = true;
450        }
451
452        var classNames = ["css-style-declaration-property"];
453
454        if (property.overridden)
455            classNames.push("overridden");
456
457        if (property.implicit)
458            classNames.push("implicit");
459
460        if (this._style.inherited && !property.inherited)
461            classNames.push("not-inherited");
462
463        if (!property.valid && property.hasOtherVendorNameOrKeyword())
464            classNames.push("other-vendor");
465        else if (!property.valid)
466            classNames.push("invalid");
467
468        if (!property.enabled)
469            classNames.push("disabled");
470
471        var classNamesString = classNames.join(" ");
472
473        // If there is already a text marker and it's in the same document, then try to avoid recreating it.
474        // FIXME: If there are multiple CSSStyleDeclarationTextEditors for the same style then this will cause
475        // both editors to fight and always recreate their text markers. This isn't really common.
476        if (property.__propertyTextMarker && property.__propertyTextMarker.doc.cm === this._codeMirror && property.__propertyTextMarker.find()) {
477            // If the class name is the same then we don't need to make a new marker.
478            if (property.__propertyTextMarker.className === classNamesString)
479                return;
480
481            property.__propertyTextMarker.clear();
482        }
483
484        var propertyTextMarker = this._codeMirror.markText(from, to, {className: classNamesString});
485
486        propertyTextMarker.__cssProperty = property;
487        property.__propertyTextMarker = propertyTextMarker;
488
489        property.addEventListener(WebInspector.CSSProperty.Event.OverriddenStatusChanged, this._propertyOverriddenStatusChanged, this);
490
491        this._removeCheckboxPlaceholder(from.line);
492    },
493
494    _clearTextMarkers: function(nonatomic, all)
495    {
496        function clear()
497        {
498            var markers = this._codeMirror.getAllMarks();
499            for (var i = 0; i < markers.length; ++i) {
500                var textMarker = markers[i];
501
502                if (!all && textMarker.__checkboxPlaceholder) {
503                    var position = textMarker.find();
504
505                    // Only keep checkbox placeholders if they are in the first column.
506                    if (position && !position.ch)
507                        continue;
508                }
509
510                if (textMarker.__cssProperty) {
511                    textMarker.__cssProperty.removeEventListener(null, null, this);
512
513                    delete textMarker.__cssProperty.__propertyTextMarker;
514                    delete textMarker.__cssProperty;
515                }
516
517                textMarker.clear();
518            }
519        }
520
521        if (nonatomic)
522            clear.call(this);
523        else
524            this._codeMirror.operation(clear.bind(this));
525    },
526
527    _iterateOverProperties: function(onlyVisibleProperties, callback)
528    {
529        var properties = onlyVisibleProperties ? this._style.visibleProperties : this._style.properties;
530
531        if (!onlyVisibleProperties) {
532            // Filter based on options only when all properties are used.
533            properties = properties.filter((function(property) {
534                return !property.implicit || this._showsImplicitProperties || property.canonicalName in this._alwaysShowPropertyNames;
535            }).bind(this));
536
537            if (this._sortProperties)
538                properties.sort(function(a, b) { return a.name.localeCompare(b.name) });
539        }
540
541        for (var i = 0; i < properties.length; ++i) {
542            if (callback.call(this, properties[i], i === properties.length - 1))
543                break;
544        }
545    },
546
547    _propertyCheckboxChanged: function(event)
548    {
549        var property = event.target.__cssProperty;
550        console.assert(property);
551        if (!property)
552            return;
553
554        var textMarker = property.__propertyTextMarker;
555        console.assert(textMarker);
556        if (!textMarker)
557            return;
558
559        // Check if the property has been removed already, like from double-clicking
560        // the checkbox and calling this event listener multiple times.
561        var range = textMarker.find();
562        if (!range)
563            return;
564
565        var text = this._codeMirror.getRange(range.from, range.to);
566
567        function update()
568        {
569            // Replace the text with a commented version.
570            this._codeMirror.replaceRange("/* " + text + " */", range.from, range.to);
571
572            // Update the line for any color swatches that got removed.
573            this._createColorSwatches(true, range.from.line);
574        }
575
576        this._codeMirror.operation(update.bind(this));
577    },
578
579    _propertyCommentCheckboxChanged: function(event)
580    {
581        var commentTextMarker = event.target.__commentTextMarker;
582        console.assert(commentTextMarker);
583        if (!commentTextMarker)
584            return;
585
586        // Check if the comment has been removed already, like from double-clicking
587        // the checkbox and calling event listener multiple times.
588        var range = commentTextMarker.find();
589        if (!range)
590            return;
591
592        var text = this._codeMirror.getRange(range.from, range.to);
593
594        // Remove the comment prefix and suffix.
595        text = text.replace(/^\/\*\s*/, "").replace(/\s*\*\/$/, "");
596
597        // Add a semicolon if there isn't one already.
598        if (text.length && text.charAt(text.length - 1) !== ";")
599            text += ";";
600
601        function update()
602        {
603            this._codeMirror.addLineClass(range.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);
604            this._codeMirror.replaceRange(text, range.from, range.to);
605
606            // Update the line for any color swatches that got removed.
607            this._createColorSwatches(true, range.from.line);
608        }
609
610        this._codeMirror.operation(update.bind(this));
611    },
612
613    _colorSwatchClicked: function(event)
614    {
615        if (this._colorPickerPopover)
616            return;
617
618        var swatch = event.target;
619
620        var color = swatch.__color;
621        console.assert(color);
622        if (!color)
623            return;
624
625        var colorTextMarker = swatch.__colorTextMarker;
626        console.assert(colorTextMarker);
627        if (!colorTextMarker)
628            return;
629
630        var range = colorTextMarker.find();
631        console.assert(range);
632        if (!range)
633            return;
634
635        function updateCodeMirror(newColorText)
636        {
637            function update()
638            {
639                // The original text marker might have been cleared by a style update,
640                // in this case we need to find the new color text marker so we know
641                // the right range for the new style color text.
642                if (!colorTextMarker || !colorTextMarker.find()) {
643                    colorTextMarker = null;
644
645                    var marks = this._codeMirror.findMarksAt(range.from);
646                    if (!marks.length)
647                        return;
648
649                    for (var i = 0; i < marks.length; ++i) {
650                        var mark = marks[i];
651                        if (WebInspector.TextMarker.textMarkerForCodeMirrorTextMarker(mark).type !== WebInspector.TextMarker.Type.Color)
652                            continue;
653                        colorTextMarker = mark;
654                        break;
655                    }
656                }
657
658                if (!colorTextMarker)
659                    return;
660
661                // Sometimes we still might find a stale text marker with findMarksAt.
662                var newRange = colorTextMarker.find();
663                if (!newRange)
664                    return;
665
666                range = newRange;
667
668                colorTextMarker.clear();
669
670                this._codeMirror.replaceRange(newColorText, range.from, range.to);
671
672                // The color's text format could have changed, so we need to update the "range"
673                // variable to anticipate a different "range.to" property.
674                range.to.ch = range.from.ch + newColorText.length;
675
676                colorTextMarker = this._codeMirror.markText(range.from, range.to);
677
678                swatch.__colorTextMarker = colorTextMarker;
679            }
680
681            this._codeMirror.operation(update.bind(this));
682        }
683
684        if (event.shiftKey || this._codeMirror.getOption("readOnly")) {
685            var nextFormat = color.nextFormat();
686            console.assert(nextFormat);
687            if (!nextFormat)
688                return;
689            color.format = nextFormat;
690
691            var newColorText = color.toString();
692
693            // Ignore the change so we don't commit the format change. However, any future user
694            // edits will commit the color format.
695            this._ignoreCodeMirrorContentDidChangeEvent = true;
696            updateCodeMirror.call(this, newColorText);
697            delete this._ignoreCodeMirrorContentDidChangeEvent;
698        } else {
699            this._colorPickerPopover = new WebInspector.Popover(this);
700
701            var colorPicker = new WebInspector.ColorPicker;
702
703            colorPicker.addEventListener(WebInspector.ColorPicker.Event.ColorChanged, function(event) {
704                updateCodeMirror.call(this, event.data.color.toString());
705            }.bind(this));
706
707            var bounds = WebInspector.Rect.rectFromClientRect(swatch.getBoundingClientRect());
708
709            this._colorPickerPopover.content = colorPicker.element;
710            this._colorPickerPopover.present(bounds.pad(2), [WebInspector.RectEdge.MIN_X]);
711
712            colorPicker.color = color;
713        }
714    },
715
716    _propertyOverriddenStatusChanged: function(event)
717    {
718        this._updateTextMarkerForPropertyIfNeeded(event.target);
719    },
720
721    _propertiesChanged: function(event)
722    {
723        // Don't try to update the document while completions are showing. Doing so will clear
724        // the completion hint and prevent further interaction with the completion.
725        if (this._completionController.isShowingCompletions())
726            return;
727
728        // Reset the content if the text is different and we are not focused.
729        if (!this.focused && this._style.text !== this._formattedContent()) {
730            this._resetContent();
731            return;
732        }
733
734        this._removeEditingLineClassesSoon();
735
736        this._updateTextMarkers();
737    },
738
739    _markLinesWithCheckboxPlaceholder: function()
740    {
741        if (this._codeMirror.getOption("readOnly"))
742            return;
743
744        var linesWithPropertyCheckboxes = {};
745        var linesWithCheckboxPlaceholders = {};
746
747        var markers = this._codeMirror.getAllMarks();
748        for (var i = 0; i < markers.length; ++i) {
749            var textMarker = markers[i];
750            if (textMarker.__propertyCheckbox) {
751                var position = textMarker.find();
752                if (position)
753                    linesWithPropertyCheckboxes[position.line] = true;
754            } else if (textMarker.__checkboxPlaceholder) {
755                var position = textMarker.find();
756                if (position)
757                    linesWithCheckboxPlaceholders[position.line] = true;
758            }
759        }
760
761        var lineCount = this._codeMirror.lineCount();
762
763        for (var i = 0; i < lineCount; ++i) {
764            if (i in linesWithPropertyCheckboxes || i in linesWithCheckboxPlaceholders)
765                continue;
766
767            var position = {line: i, ch: 0};
768
769            var placeholderElement = document.createElement("div");
770            placeholderElement.className = WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName;
771
772            var placeholderMark = this._codeMirror.setUniqueBookmark(position, placeholderElement);
773            placeholderMark.__checkboxPlaceholder = true;
774        }
775    },
776
777    _removeCheckboxPlaceholder: function(lineNumber)
778    {
779        var marks = this._codeMirror.findMarksAt({line: lineNumber, ch: 0});
780        for (var i = 0; i < marks.length; ++i) {
781            var mark = marks[i];
782            if (!mark.__checkboxPlaceholder)
783                continue;
784
785            mark.clear();
786            return;
787        }
788    },
789
790    _resetContent: function()
791    {
792        if (this._commitChangesTimeout) {
793            clearTimeout(this._commitChangesTimeout);
794            delete this._commitChangesTimeout;
795        }
796
797        this._removeEditingLineClasses();
798
799        // Only allow editing if we have a style, it is editable and we have text range in the stylesheet.
800        var readOnly = !this._style || !this._style.editable || !this._style.styleSheetTextRange;
801        this._codeMirror.setOption("readOnly", readOnly);
802
803        if (readOnly) {
804            this.element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName);
805            this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties"));
806        } else {
807            this.element.classList.remove(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName);
808            this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties \u2014 Click to Edit"));
809        }
810
811        if (!this._style) {
812            this._ignoreCodeMirrorContentDidChangeEvent = true;
813
814            this._clearTextMarkers(false, true);
815
816            this._codeMirror.setValue("");
817            this._codeMirror.clearHistory();
818            this._codeMirror.markClean();
819
820            delete this._ignoreCodeMirrorContentDidChangeEvent;
821
822            return;
823        }
824
825        function update()
826        {
827            // Remember the cursor position/selection.
828            var selectionAnchor = this._codeMirror.getCursor("anchor");
829            var selectionHead = this._codeMirror.getCursor("head");
830
831            function countNewLineCharacters(text)
832            {
833                var matches = text.match(/\n/g);
834                return matches ? matches.length : 0;
835            }
836
837            var styleText = this._style.text;
838
839            // Pretty print the content if there are more properties than there are lines.
840            // This could be an option exposed to the user; however, it is almost always
841            // desired in this case.
842
843            if (styleText && this._style.visibleProperties.length <= countNewLineCharacters(styleText.trim()) + 1) {
844                // This style has formatted text content, so use it for a high-fidelity experience.
845
846                var prefixWhitespaceMatch = styleText.match(/^[ \t]*\n/);
847                this._prefixWhitespace = prefixWhitespaceMatch ? prefixWhitespaceMatch[0] : "";
848
849                var suffixWhitespaceMatch = styleText.match(/\n[ \t]*$/);
850                this._suffixWhitespace = suffixWhitespaceMatch ? suffixWhitespaceMatch[0] : "";
851
852                this._codeMirror.setValue(styleText);
853
854                if (this._prefixWhitespace)
855                    this._codeMirror.replaceRange("", {line: 0, ch: 0}, {line: 1, ch: 0});
856
857                if (this._suffixWhitespace) {
858                    var lineCount = this._codeMirror.lineCount();
859                    this._codeMirror.replaceRange("", {line: lineCount - 2}, {line: lineCount - 1});
860                }
861
862                this._linePrefixWhitespace = "";
863
864                var linesToStrip = [];
865
866                // Remember the whitespace so it can be restored on commit.
867                var lineCount = this._codeMirror.lineCount();
868                for (var i = 0; i < lineCount; ++i) {
869                    var lineContent = this._codeMirror.getLine(i);
870                    var prefixWhitespaceMatch = lineContent.match(/^\s+/);
871
872                    // If there is no prefix whitespace (except for empty lines) then the prefix
873                    // whitespace of all other lines will be retained as is. Update markers and return.
874                    if (!prefixWhitespaceMatch) {
875                        if (!lineContent)
876                            continue;
877                        this._linePrefixWhitespace = "";
878                        this._updateTextMarkers(true);
879                        return;
880                    }
881
882                    linesToStrip.push(i);
883
884                    // Only remember the shortest whitespace so we don't loose any of the
885                    // original author's whitespace if their indentation lengths differed.
886                    // Using the shortest also makes the adjustment work in _updateTextMarkers.
887
888                    // FIXME: This messes up if there is a mix of spaces and tabs. A tab
889                    // is treated the same as a space when prefix whitespace is omitted,
890                    // so if the shortest prefixed whitespace is, say, two tab characters,
891                    // lines that begin with four spaces will only have a two space indent.
892                    if (!this._linePrefixWhitespace || prefixWhitespaceMatch[0].length < this._linePrefixWhitespace.length)
893                        this._linePrefixWhitespace = prefixWhitespaceMatch[0];
894                }
895
896                // Strip the whitespace from the beginning of each line.
897                for (var i = 0; i < linesToStrip.length; ++i) {
898                    var lineNumber = linesToStrip[i];
899                    var from = {line: lineNumber, ch: 0};
900                    var to = {line: lineNumber, ch: this._linePrefixWhitespace.length};
901                    this._codeMirror.replaceRange("", from, to);
902                }
903
904                // Update all the text markers.
905                this._updateTextMarkers(true);
906            } else {
907                // This style does not have text content or it is minified, so we want to synthesize the text content.
908
909                this._prefixWhitespace = "";
910                this._suffixWhitespace = "";
911                this._linePrefixWhitespace = "";
912
913                this._codeMirror.setValue("");
914
915                var lineNumber = 0;
916
917                // Iterate only visible properties if we have original style text. That way we known we only synthesize
918                // what was originaly in the style text.
919                this._iterateOverProperties(styleText ? true : false, function(property) {
920                    // Some property text can have line breaks, so consider that in the ranges below.
921                    var propertyText = property.synthesizedText;
922                    var propertyLineCount = countNewLineCharacters(propertyText);
923
924                    var from = {line: lineNumber, ch: 0};
925                    var to = {line: lineNumber + propertyLineCount};
926
927                    this._codeMirror.replaceRange((lineNumber ? "\n" : "") + propertyText, from);
928                    this._createTextMarkerForPropertyIfNeeded(from, to, property);
929
930                    lineNumber += propertyLineCount + 1;
931                });
932
933                // Look for colors and make swatches.
934                this._createColorSwatches(true);
935            }
936
937            this._markLinesWithCheckboxPlaceholder();
938
939            // Restore the cursor position/selection.
940            this._codeMirror.setSelection(selectionAnchor, selectionHead);
941
942            // Reset undo history since undo past the reset is wrong when the content was empty before
943            // or the content was representing a previous style object.
944            this._codeMirror.clearHistory();
945
946            // Mark the editor as clean (unedited state).
947            this._codeMirror.markClean();
948        }
949
950        // This needs to be done first and as a separate operation to avoid an exception in CodeMirror.
951        this._clearTextMarkers(false, true);
952
953        this._ignoreCodeMirrorContentDidChangeEvent = true;
954        this._codeMirror.operation(update.bind(this));
955        delete this._ignoreCodeMirrorContentDidChangeEvent;
956    },
957
958    _updateJumpToSymbolTrackingMode: function()
959    {
960        var oldJumpToSymbolTrackingModeEnabled = this._jumpToSymbolTrackingModeEnabled;
961
962        if (!this._style || !this._style.ownerRule || !this._style.ownerRule.sourceCodeLocation)
963            this._jumpToSymbolTrackingModeEnabled = false;
964        else
965            this._jumpToSymbolTrackingModeEnabled = WebInspector.modifierKeys.altKey && !WebInspector.modifierKeys.metaKey && !WebInspector.modifierKeys.shiftKey;
966
967        if (oldJumpToSymbolTrackingModeEnabled !== this._jumpToSymbolTrackingModeEnabled) {
968            if (this._jumpToSymbolTrackingModeEnabled) {
969                this._tokenTrackingController.highlightLastHoveredRange();
970                this._tokenTrackingController.enabled = !this._codeMirror.getOption("readOnly");
971            } else {
972                this._tokenTrackingController.removeHighlightedRange();
973                this._tokenTrackingController.enabled = false;
974            }
975        }
976    },
977
978    tokenTrackingControllerHighlightedRangeWasClicked: function(tokenTrackingController)
979    {
980        console.assert(this._style.ownerRule.sourceCodeLocation);
981        if (!this._style.ownerRule.sourceCodeLocation)
982            return;
983
984        // Special case command clicking url(...) links.
985        var token = this._tokenTrackingController.candidate.hoveredToken;
986        if (/\blink\b/.test(token.type)) {
987            var url = token.string;
988            var baseURL = this._style.ownerRule.sourceCodeLocation.sourceCode.url;
989            WebInspector.openURL(absoluteURL(url, baseURL));
990            return;
991        }
992
993        // Jump to the rule if we can't find a property.
994        // Find a better source code location from the property that was clicked.
995        var sourceCodeLocation = this._style.ownerRule.sourceCodeLocation;
996        var marks = this._codeMirror.findMarksAt(this._tokenTrackingController.candidate.hoveredTokenRange.start);
997        for (var i = 0; i < marks.length; ++i) {
998            var mark = marks[i];
999            var property = mark.__cssProperty;
1000            if (property) {
1001                var sourceCode = sourceCodeLocation.sourceCode;
1002                var styleSheetTextRange = property.styleSheetTextRange;
1003                sourceCodeLocation = sourceCode.createSourceCodeLocation(styleSheetTextRange.startLine, styleSheetTextRange.startColumn);
1004            }
1005        }
1006
1007        WebInspector.resourceSidebarPanel.showSourceCodeLocation(sourceCodeLocation);
1008    },
1009
1010    tokenTrackingControllerNewHighlightCandidate: function(tokenTrackingController, candidate)
1011    {
1012        this._tokenTrackingController.highlightRange(candidate.hoveredTokenRange);
1013    }
1014};
1015
1016WebInspector.CSSStyleDeclarationTextEditor.prototype.__proto__ = WebInspector.Object.prototype;
1017