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