8068dcc189be892f3b28250651d2ad3fee95f251
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / CSSStyleDeclarationTextEditor.js
1 /*
2  * Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
3  * Copyright (C) 2015 Tobias Reiss <tobi+webkit@basecode.de>
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
15  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
16  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
18  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
24  * THE POSSIBILITY OF SUCH DAMAGE.
25  */
26
27 WebInspector.CSSStyleDeclarationTextEditor = class CSSStyleDeclarationTextEditor extends WebInspector.Object
28 {
29     constructor(delegate, style, element)
30     {
31         super();
32
33         this._element = element || document.createElement("div");
34         this._element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.StyleClassName);
35         this._element.classList.add(WebInspector.SyntaxHighlightedStyleClassName);
36         this._element.addEventListener("mousedown", this._handleMouseDown.bind(this));
37         this._element.addEventListener("mouseup", this._handleMouseUp.bind(this));
38
39         this._mouseDownCursorPosition = null;
40
41         this._showsImplicitProperties = true;
42         this._alwaysShowPropertyNames = {};
43         this._filterResultPropertyNames = null;
44         this._sortProperties = false;
45
46         this._prefixWhitespace = "\n";
47         this._suffixWhitespace = "\n";
48         this._linePrefixWhitespace = "";
49
50         this._delegate = delegate || null;
51
52         this._codeMirror = CodeMirror(this.element, {
53             readOnly: true,
54             lineWrapping: true,
55             mode: "css-rule",
56             electricChars: false,
57             indentWithTabs: false,
58             indentUnit: 4,
59             smartIndent: false,
60             matchBrackets: true,
61             autoCloseBrackets: true
62         });
63
64         this._codeMirror.addKeyMap({
65             "Enter": this._handleEnterKey.bind(this),
66             "Shift-Enter": this._insertNewlineAfterCurrentLine.bind(this),
67             "Shift-Tab": this._handleShiftTabKey.bind(this),
68             "Tab": this._handleTabKey.bind(this)
69         });
70
71         this._completionController = new WebInspector.CodeMirrorCompletionController(this._codeMirror, this);
72         this._tokenTrackingController = new WebInspector.CodeMirrorTokenTrackingController(this._codeMirror, this);
73
74         this._completionController.noEndingSemicolon = true;
75
76         this._jumpToSymbolTrackingModeEnabled = false;
77         this._tokenTrackingController.classNameForHighlightedRange = WebInspector.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName;
78         this._tokenTrackingController.mouseOverDelayDuration = 0;
79         this._tokenTrackingController.mouseOutReleaseDelayDuration = 0;
80         this._tokenTrackingController.mode = WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens;
81
82         // Make sure CompletionController adds event listeners first.
83         // Otherwise we end up in race conditions during complete or delete-complete phases.
84         this._codeMirror.on("change", this._contentChanged.bind(this));
85         this._codeMirror.on("blur", this._editorBlured.bind(this));
86         this._codeMirror.on("beforeChange", this._handleBeforeChange.bind(this));
87
88         if (typeof this._delegate.cssStyleDeclarationTextEditorFocused === "function")
89             this._codeMirror.on("focus", this._editorFocused.bind(this));
90
91         this.style = style;
92     }
93
94     // Public
95
96     get element()
97     {
98         return this._element;
99     }
100
101     get delegate()
102     {
103         return this._delegate;
104     }
105
106     set delegate(delegate)
107     {
108         this._delegate = delegate || null;
109     }
110
111     get style()
112     {
113         return this._style;
114     }
115
116     set style(style)
117     {
118         if (this._style === style)
119             return;
120
121         if (this._style) {
122             this._style.removeEventListener(WebInspector.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);
123             if (this._style.ownerRule && this._style.ownerRule.sourceCodeLocation)
124                 WebInspector.notifications.removeEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateJumpToSymbolTrackingMode, this);
125         }
126
127         this._style = style || null;
128
129         if (this._style) {
130             this._style.addEventListener(WebInspector.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);
131             if (this._style.ownerRule && this._style.ownerRule.sourceCodeLocation)
132                 WebInspector.notifications.addEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._updateJumpToSymbolTrackingMode, this);
133         }
134
135         this._updateJumpToSymbolTrackingMode();
136
137         this._resetContent();
138     }
139
140     get focused()
141     {
142         return this._codeMirror.getWrapperElement().classList.contains("CodeMirror-focused");
143     }
144
145     get alwaysShowPropertyNames()
146     {
147         return Object.keys(this._alwaysShowPropertyNames);
148     }
149
150     set alwaysShowPropertyNames(alwaysShowPropertyNames)
151     {
152         this._alwaysShowPropertyNames = (alwaysShowPropertyNames || []).keySet();
153
154         this._resetContent();
155     }
156
157     get showsImplicitProperties()
158     {
159         return this._showsImplicitProperties;
160     }
161
162     set showsImplicitProperties(showsImplicitProperties)
163     {
164         if (this._showsImplicitProperties === showsImplicitProperties)
165             return;
166
167         this._showsImplicitProperties = showsImplicitProperties;
168
169         this._resetContent();
170     }
171
172     get sortProperties()
173     {
174         return this._sortProperties;
175     }
176
177     set sortProperties(sortProperties)
178     {
179         if (this._sortProperties === sortProperties)
180             return;
181
182         this._sortProperties = sortProperties;
183
184         this._resetContent();
185     }
186
187     focus()
188     {
189         this._codeMirror.focus();
190     }
191
192     refresh()
193     {
194         this._resetContent();
195     }
196
197     updateLayout(force)
198     {
199         this._codeMirror.refresh();
200     }
201
202     highlightProperty(property)
203     {
204         function propertiesMatch(cssProperty)
205         {
206             if (cssProperty.enabled && !cssProperty.overridden) {
207                 if (cssProperty.canonicalName === property.canonicalName || hasMatchingLonghandProperty(cssProperty))
208                     return true;
209             }
210
211             return false;
212         }
213
214         function hasMatchingLonghandProperty(cssProperty)
215         {
216             var cssProperties = cssProperty.relatedLonghandProperties;
217
218             if (!cssProperties.length)
219                 return false;
220
221             for (var property of cssProperties) {
222                 if (propertiesMatch(property))
223                     return true;
224             }
225
226             return false;
227         }
228
229         for (var cssProperty of this.style.properties) {
230             if (propertiesMatch(cssProperty)) {
231                 var selection = cssProperty.__propertyTextMarker.find();
232                 this._codeMirror.setSelection(selection.from, selection.to);
233                 this.focus();
234
235                 return true;
236             }
237         }
238
239         return false;
240     }
241
242     clearSelection()
243     {
244         this._codeMirror.setCursor({line: 0, ch: 0});
245     }
246
247     findMatchingProperties(needle)
248     {
249         if (!needle) {
250             this.resetFilteredProperties();
251             return false;
252         }
253
254         var propertiesList = this._style.visibleProperties.length ? this._style.visibleProperties : this._style.properties;
255         var matchingProperties = [];
256
257         for (var property of propertiesList)
258             matchingProperties.push(property.text.includes(needle));
259
260         if (!matchingProperties.includes(true)) {
261             this.resetFilteredProperties();
262             return false;
263         }
264
265         for (var i = 0; i < matchingProperties.length; ++i) {
266             var property = propertiesList[i];
267
268             if (matchingProperties[i])
269                 property.__filterResultClassName = WebInspector.CSSStyleDetailsSidebarPanel.FilterMatchSectionClassName;
270             else
271                 property.__filterResultClassName = WebInspector.CSSStyleDetailsSidebarPanel.NoFilterMatchInPropertyClassName;
272
273             this._updateTextMarkerForPropertyIfNeeded(property);
274         }
275
276         return true;
277     }
278
279     resetFilteredProperties()
280     {
281         var propertiesList = this._style.visibleProperties.length ? this._style.visibleProperties : this._style.properties;
282
283         for (var property of propertiesList) {
284             if (property.__filterResultClassName) {
285                 property.__filterResultClassName = null;
286                 this._updateTextMarkerForPropertyIfNeeded(property)
287             }
288         }
289     }
290
291     removeNonMatchingProperties(needle)
292     {
293         this._filterResultPropertyNames = null;
294
295         if (!needle) {
296             this._resetContent();
297             return false;
298         }
299
300         var matchingPropertyNames = [];
301
302         for (var property of this._style.properties) {
303             var indexesOfNeedle = property.text.getMatchingIndexes(needle);
304
305             if (indexesOfNeedle.length) {
306                 matchingPropertyNames.push(property.name);
307                 property.__filterResultClassName = WebInspector.CSSStyleDetailsSidebarPanel.FilterMatchSectionClassName;
308                 property.__filterResultNeedlePosition = {start: indexesOfNeedle, length: needle.length};
309             }
310         }
311
312         this._filterResultPropertyNames = matchingPropertyNames.length ? matchingPropertyNames.keySet() : {};
313
314         this._resetContent();
315
316         return matchingPropertyNames.length > 0;
317     }
318
319     uncommentAllProperties()
320     {
321         function uncommentProperties(properties)
322         {
323             if (!properties.length)
324                 return false;
325
326             for (var property of properties) {
327                 if (property._commentRange) {
328                     this._uncommentRange(property._commentRange);
329                     property._commentRange = null;
330                 }
331             }
332
333             return true;
334         }
335
336         return uncommentProperties.call(this, this._style.pendingProperties) || uncommentProperties.call(this, this._style.properties);
337     }
338
339     commentAllProperties()
340     {
341         if (!this._style.properties.length)
342             return false;
343
344         for (var property of this._style.properties) {
345             if (property.__propertyTextMarker)
346                 this._commentProperty(property);
347         }
348
349         return true;
350     }
351
352     selectFirstProperty()
353     {
354         var line = this._codeMirror.getLine(0);
355         var trimmedLine = line.trimRight();
356
357         if (!line || !trimmedLine.trimLeft().length)
358             this.clearSelection();
359
360         var index = line.indexOf(":");
361         var cursor = {line: 0, ch: 0};
362
363         this._codeMirror.setSelection(cursor, {line: 0, ch: index < 0 || this._textAtCursorIsComment(this._codeMirror, cursor) ? trimmedLine.length : index});
364     }
365
366     selectLastProperty()
367     {
368         var line = this._codeMirror.lineCount() - 1;
369         var lineText = this._codeMirror.getLine(line);
370         var trimmedLine = lineText.trimRight();
371
372         var lastAnchor;
373         var lastHead;
374
375         if (this._textAtCursorIsComment(this._codeMirror, {line, ch: line.length})) {
376             lastAnchor = 0;
377             lastHead = line.length;
378         } else {
379             var colon = /(?::\s*)/.exec(lineText);
380             lastAnchor = colon ? colon.index + colon[0].length : 0;
381             lastHead = trimmedLine.length - trimmedLine.endsWith(";");
382         }
383
384         this._codeMirror.setSelection({line, ch: lastAnchor}, {line, ch: lastHead});
385     }
386
387     // Protected
388
389     didDismissPopover(popover)
390     {
391         if (popover === this._colorPickerPopover)
392             this._colorPickerPopover = null;
393         if (popover === this._cubicBezierEditorPopover)
394             this._cubicBezierEditorPopover = null;
395     }
396
397     completionControllerCompletionsHidden(completionController)
398     {
399         var styleText = this._style.text;
400         var currentText = this._formattedContent();
401
402         // If the style text and the current editor text differ then we need to commit.
403         // Otherwise we can just update the properties that got skipped because a completion
404         // was pending the last time _propertiesChanged was called.
405         if (styleText !== currentText)
406             this._commitChanges();
407         else
408             this._propertiesChanged();
409     }
410
411     // Private
412
413     _textAtCursorIsComment(codeMirror, cursor)
414     {
415         var token = codeMirror.getTokenTypeAt(cursor);
416         return token && token.includes("comment");
417     }
418
419     _highlightNextNameOrValue(codeMirror, cursor, text)
420     {
421         var nextAnchor;
422         var nextHead;
423
424         if (this._textAtCursorIsComment(codeMirror, cursor)) {
425             nextAnchor = 0;
426             nextHead = text.length;
427         } else {
428             var colonIndex = text.indexOf(":");
429             var substringIndex = colonIndex >= 0 && cursor.ch >= colonIndex ? colonIndex : 0;
430
431             var regExp = /(?:[^:;\s]\s*)+/g;
432             regExp.lastIndex = substringIndex;
433             var match = regExp.exec(text);
434
435             nextAnchor = match.index;
436             nextHead = nextAnchor + match[0].length;
437         }
438
439         codeMirror.setSelection({line: cursor.line, ch: nextAnchor}, {line: cursor.line, ch: nextHead});
440     }
441
442     _handleMouseDown(event)
443     {
444         if (this._codeMirror.options.readOnly)
445             return;
446
447         let cursor = this._codeMirror.coordsChar({left: event.x, top: event.y});
448         let line = this._codeMirror.getLine(cursor.line);
449         let trimmedLine = line.trimRight();
450         if (!trimmedLine.trimLeft().length || cursor.ch !== trimmedLine.length)
451             return;
452
453         this._mouseDownCursorPosition = cursor;
454     }
455
456     _handleMouseUp(event)
457     {
458         if (this._codeMirror.options.readOnly || !this._mouseDownCursorPosition)
459             return;
460
461         let cursor = this._codeMirror.coordsChar({left: event.x, top: event.y});
462         if (this._mouseDownCursorPosition.line === cursor.line && this._mouseDownCursorPosition.ch === cursor.ch) {
463             let nextLine = this._codeMirror.getLine(cursor.line + 1);
464             if (cursor.line < this._codeMirror.lineCount() - 1 && (!nextLine || !nextLine.trim().length)) {
465                 this._codeMirror.setCursor({line: cursor.line + 1, ch: 0});
466             } else {
467                 let line = this._codeMirror.getLine(cursor.line);
468                 let replacement = "\n";
469                 if (!line.trimRight().endsWith(";") && !this._textAtCursorIsComment(this._codeMirror, cursor))
470                     replacement = ";" + replacement;
471
472                 this._codeMirror.replaceRange(replacement, cursor);
473             }
474         }
475
476         this._mouseDownCursorPosition = null;
477     }
478
479     _handleBeforeChange(codeMirror, change)
480     {
481         if (change.origin !== "+delete" || this._completionController.isShowingCompletions())
482             return CodeMirror.Pass;
483
484         if (!change.to.line && !change.to.ch) {
485             if (codeMirror.lineCount() === 1)
486                 return CodeMirror.Pass;
487
488             var line = codeMirror.getLine(change.to.line);
489             if (line && line.trim().length)
490                 return CodeMirror.Pass;
491
492             codeMirror.execCommand("deleteLine");
493             return;
494         }
495
496         var marks = codeMirror.findMarksAt(change.to);
497         if (!marks.length)
498             return CodeMirror.Pass;
499
500         for (var mark of marks)
501             mark.clear();
502     }
503
504     _handleEnterKey(codeMirror)
505     {
506         var cursor = codeMirror.getCursor();
507         var line = codeMirror.getLine(cursor.line);
508         var trimmedLine = line.trimRight();
509         var hasEndingSemicolon = trimmedLine.endsWith(";");
510
511         if (!trimmedLine.trimLeft().length)
512             return CodeMirror.Pass;
513
514         if (hasEndingSemicolon && cursor.ch === trimmedLine.length - 1)
515             ++cursor.ch;
516
517         if (cursor.ch === trimmedLine.length) {
518             var replacement = "\n";
519
520             if (!hasEndingSemicolon && !this._textAtCursorIsComment(this._codeMirror, cursor))
521                 replacement = ";" + replacement;
522
523             this._codeMirror.replaceRange(replacement, cursor);
524             return;
525         }
526
527         return CodeMirror.Pass;
528     }
529
530     _insertNewlineAfterCurrentLine(codeMirror)
531     {
532         var cursor = codeMirror.getCursor();
533         var line = codeMirror.getLine(cursor.line);
534         var trimmedLine = line.trimRight();
535
536         cursor.ch = trimmedLine.length;
537
538         if (cursor.ch) {
539             var replacement = "\n";
540
541             if (!trimmedLine.endsWith(";") && !this._textAtCursorIsComment(this._codeMirror, cursor))
542                 replacement = ";" + replacement;
543
544             this._codeMirror.replaceRange(replacement, cursor);
545             return;
546         }
547
548         return CodeMirror.Pass;
549     }
550
551     _handleShiftTabKey(codeMirror)
552     {
553         function switchRule()
554         {
555             if (this._delegate && typeof this._delegate.cssStyleDeclarationTextEditorSwitchRule === "function") {
556                 this._delegate.cssStyleDeclarationTextEditorSwitchRule(true);
557                 return;
558             }
559
560             return CodeMirror.Pass;
561         }
562
563         var cursor = codeMirror.getCursor();
564         var line = codeMirror.getLine(cursor.line);
565         var previousLine = codeMirror.getLine(cursor.line - 1);
566
567         if (!line && !previousLine && !cursor.line)
568             return switchRule.call(this);
569
570         var trimmedPreviousLine = previousLine ? previousLine.trimRight() : "";
571         var previousAnchor;
572         var previousHead;
573         var isComment = this._textAtCursorIsComment(codeMirror, cursor);
574
575         if (cursor.ch === line.indexOf(":") || line.indexOf(":") < 0 || isComment) {
576             if (previousLine) {
577                 --cursor.line;
578
579                 if (this._textAtCursorIsComment(codeMirror, cursor)) {
580                     previousAnchor = 0;
581                     previousHead = trimmedPreviousLine.length;
582                 } else {
583                     var colon = /(?::\s*)/.exec(previousLine);
584                     previousAnchor = colon ? colon.index + colon[0].length : 0;
585                     previousHead = trimmedPreviousLine.length - trimmedPreviousLine.endsWith(";");
586                 }
587                 
588                 codeMirror.setSelection({line: cursor.line, ch: previousAnchor}, {line: cursor.line, ch: previousHead});
589                 return;
590             }
591
592             if (cursor.line) {
593                 codeMirror.setCursor(cursor.line - 1, 0);
594                 return;
595             }
596
597             return switchRule.call(this);
598         }
599
600         if (isComment) {
601             previousAnchor = 0;
602             previousHead = line.length;
603         } else {
604             var match = /(?:[^:;\s]\s*)+/.exec(line);
605             previousAnchor = match.index;
606             previousHead = previousAnchor + match[0].length;
607         }
608
609         codeMirror.setSelection({line: cursor.line, ch: previousAnchor}, {line: cursor.line, ch: previousHead});
610     }
611
612     _handleTabKey(codeMirror)
613     {
614         function switchRule() {
615             if (this._delegate && typeof this._delegate.cssStyleDeclarationTextEditorSwitchRule === "function") {
616                 this._delegate.cssStyleDeclarationTextEditorSwitchRule();
617                 return;
618             }
619
620             return CodeMirror.Pass;
621         }
622
623         var cursor = codeMirror.getCursor();
624         var line = codeMirror.getLine(cursor.line);
625         var trimmedLine = line.trimRight();
626         var lastLine = cursor.line === codeMirror.lineCount() - 1;
627         var nextLine = codeMirror.getLine(cursor.line + 1);
628         var trimmedNextLine = nextLine ? nextLine.trimRight() : "";
629
630         if (!trimmedLine.trimLeft().length) {
631             if (lastLine)
632                 return switchRule.call(this);
633
634             if (!trimmedNextLine.trimLeft().length) {
635                 codeMirror.setCursor(cursor.line + 1, 0);
636                 return;
637             }
638
639             ++cursor.line;
640             this._highlightNextNameOrValue(codeMirror, cursor, nextLine);
641             return;
642         }
643
644         if (trimmedLine.endsWith(":")) {
645             codeMirror.setCursor(cursor.line, line.length);
646             this._completionController._completeAtCurrentPosition(true);
647             return;
648         }
649
650         var hasEndingSemicolon = trimmedLine.endsWith(";");
651
652         if (cursor.ch >= line.trimRight().length - hasEndingSemicolon) {
653             this._completionController.completeAtCurrentPositionIfNeeded().then(function(result) {
654                 if (result !== WebInspector.CodeMirrorCompletionController.UpdatePromise.NoCompletionsFound)
655                     return;
656
657                 var replacement = "";
658
659                 if (!hasEndingSemicolon && !this._textAtCursorIsComment(codeMirror, cursor))
660                     replacement += ";";
661
662                 if (lastLine)
663                     replacement += "\n";
664
665                 if (replacement.length)
666                     codeMirror.replaceRange(replacement, {line: cursor.line, ch: trimmedLine.length});
667
668                 if (!nextLine) {
669                     codeMirror.setCursor(cursor.line + 1, 0);
670                     return;
671                 }
672
673                 this._highlightNextNameOrValue(codeMirror, {line: cursor.line + 1, ch: 0}, nextLine);
674             }.bind(this));
675
676             return;
677         }
678
679         this._highlightNextNameOrValue(codeMirror, cursor, line);
680     }
681
682     _clearRemoveEditingLineClassesTimeout()
683     {
684         if (!this._removeEditingLineClassesTimeout)
685             return;
686
687         clearTimeout(this._removeEditingLineClassesTimeout);
688         delete this._removeEditingLineClassesTimeout;
689     }
690
691     _removeEditingLineClasses()
692     {
693         this._clearRemoveEditingLineClassesTimeout();
694
695         function removeEditingLineClasses()
696         {
697             var lineCount = this._codeMirror.lineCount();
698             for (var i = 0; i < lineCount; ++i)
699                 this._codeMirror.removeLineClass(i, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);
700         }
701
702         this._codeMirror.operation(removeEditingLineClasses.bind(this));
703     }
704
705     _removeEditingLineClassesSoon()
706     {
707         if (this._removeEditingLineClassesTimeout)
708             return;
709         this._removeEditingLineClassesTimeout = setTimeout(this._removeEditingLineClasses.bind(this), WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay);
710     }
711
712     _formattedContent()
713     {
714         // Start with the prefix whitespace we stripped.
715         var content = this._prefixWhitespace;
716
717         // Get each line and add the line prefix whitespace and newlines.
718         var lineCount = this._codeMirror.lineCount();
719         for (var i = 0; i < lineCount; ++i) {
720             var lineContent = this._codeMirror.getLine(i);
721             content += this._linePrefixWhitespace + lineContent;
722             if (i !== lineCount - 1)
723                 content += "\n";
724         }
725
726         // Add the suffix whitespace we stripped.
727         content += this._suffixWhitespace;
728
729         // This regular expression replacement removes extra newlines
730         // in between properties while preserving leading whitespace
731         return content.replace(/\s*\n\s*\n(\s*)/g, "\n$1");
732     }
733
734     _commitChanges()
735     {
736         if (this._commitChangesTimeout) {
737             clearTimeout(this._commitChangesTimeout);
738             delete this._commitChangesTimeout;
739         }
740
741         this._style.text = this._formattedContent();
742     }
743
744     _editorBlured(codeMirror)
745     {
746         // Clicking a suggestion causes the editor to blur. We don't want to reset content in this case.
747         if (this._completionController.isHandlingClickEvent())
748             return;
749
750         // Reset the content on blur since we stop accepting external changes while the the editor is focused.
751         // This causes us to pick up any change that was suppressed while the editor was focused.
752         this._resetContent();
753         this.dispatchEventToListeners(WebInspector.CSSStyleDeclarationTextEditor.Event.Blurred);
754     }
755
756     _editorFocused(codeMirror)
757     {
758         if (typeof this._delegate.cssStyleDeclarationTextEditorFocused === "function")
759             this._delegate.cssStyleDeclarationTextEditorFocused();
760     }
761
762     _contentChanged(codeMirror, change)
763     {
764         // Return early if the style isn't editable. This still can be called when readOnly is set because
765         // clicking on a color swatch modifies the text.
766         if (!this._style || !this._style.editable || this._ignoreCodeMirrorContentDidChangeEvent)
767             return;
768
769         this._markLinesWithCheckboxPlaceholder();
770
771         this._clearRemoveEditingLineClassesTimeout();
772         this._codeMirror.addLineClass(change.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);
773
774         // When the change is a completion change, create color swatches now since the changes
775         // will not go through _propertiesChanged until completionControllerCompletionsHidden happens.
776         // This way any auto completed colors get swatches right away.
777         if (this._completionController.isCompletionChange(change)) {
778             this._createColorSwatches(false, change.from.line);
779             this._createBezierEditors(false, change.from.line);
780         }
781
782         // Use a short delay for user input to coalesce more changes before committing. Other actions like
783         // undo, redo and paste are atomic and work better with a zero delay. CodeMirror identifies changes that
784         // get coalesced in the undo stack with a "+" prefix on the origin. Use that to set the delay for our coalescing.
785         var delay = change.origin && change.origin.charAt(0) === "+" ? WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay : 0;
786
787         // Reset the timeout so rapid changes coalesce after a short delay.
788         if (this._commitChangesTimeout)
789             clearTimeout(this._commitChangesTimeout);
790         this._commitChangesTimeout = setTimeout(this._commitChanges.bind(this), delay);
791
792         this.dispatchEventToListeners(WebInspector.CSSStyleDeclarationTextEditor.Event.ContentChanged);
793     }
794
795     _updateTextMarkers(nonatomic)
796     {
797         function update()
798         {
799             this._clearTextMarkers(true);
800
801             this._iterateOverProperties(true, function(property) {
802                 var styleTextRange = property.styleDeclarationTextRange;
803                 console.assert(styleTextRange);
804                 if (!styleTextRange)
805                     return;
806
807                 var from = {line: styleTextRange.startLine, ch: styleTextRange.startColumn};
808                 var to = {line: styleTextRange.endLine, ch: styleTextRange.endColumn};
809
810                 // Adjust the line position for the missing prefix line.
811                 if (this._prefixWhitespace) {
812                     --from.line;
813                     --to.line;
814                 }
815
816                 // Adjust the column for the stripped line prefix whitespace.
817                 from.ch -= this._linePrefixWhitespace.length;
818                 to.ch -= this._linePrefixWhitespace.length;
819
820                 this._createTextMarkerForPropertyIfNeeded(from, to, property);
821             });
822
823             if (!this._codeMirror.getOption("readOnly")) {
824                 // Look for comments that look like properties and add checkboxes in front of them.
825                 this._codeMirror.eachLine(function(lineHandler) {
826                     this._createCommentedCheckboxMarker(lineHandler);
827                 }.bind(this));
828             }
829
830             // Look for colors and make swatches.
831             this._createColorSwatches(true);
832             this._createBezierEditors(true);
833
834             this._markLinesWithCheckboxPlaceholder();
835         }
836
837         if (nonatomic)
838             update.call(this);
839         else
840             this._codeMirror.operation(update.bind(this));
841     }
842
843     _createCommentedCheckboxMarker(lineHandle)
844     {
845         var lineNumber = lineHandle.lineNo();
846
847         // Since lineNumber can be 0, it is also necessary to check if it is a number before returning.
848         if (!lineNumber && isNaN(lineNumber))
849             return;
850
851         // Matches a comment like: /* -webkit-foo: bar; */
852         var commentedPropertyRegex = /\/\*\s*[-\w]+\s*:\s*[^;]+;?\s*\*\//g;
853
854         var match = commentedPropertyRegex.exec(lineHandle.text);
855         if (!match)
856             return;
857
858         while (match) {
859             var checkboxElement = document.createElement("input");
860             checkboxElement.type = "checkbox";
861             checkboxElement.checked = false;
862             checkboxElement.addEventListener("change", this._propertyCommentCheckboxChanged.bind(this));
863
864             var from = {line: lineNumber, ch: match.index};
865             var to = {line: lineNumber, ch: match.index + match[0].length};
866
867             var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement);
868             checkboxMarker.__propertyCheckbox = true;
869
870             var commentTextMarker = this._codeMirror.markText(from, to);
871
872             checkboxElement.__commentTextMarker = commentTextMarker;
873
874             match = commentedPropertyRegex.exec(lineHandle.text);
875         }
876     }
877
878     _createColorSwatches(nonatomic, lineNumber)
879     {
880         function update()
881         {
882             var range = typeof lineNumber === "number" ? new WebInspector.TextRange(lineNumber, 0, lineNumber + 1, 0) : null;
883
884             // Look for color strings and add swatches in front of them.
885             createCodeMirrorColorTextMarkers(this._codeMirror, range, function(marker, color, colorString) {
886                 var swatchElement = document.createElement("span");
887                 swatchElement.title = WebInspector.UIString("Click to select a color. Shift-click to switch color formats.");
888                 swatchElement.className = WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName;
889                 swatchElement.addEventListener("click", this._colorSwatchClicked.bind(this));
890
891                 var swatchInnerElement = document.createElement("span");
892                 swatchInnerElement.style.backgroundColor = colorString;
893                 swatchElement.appendChild(swatchInnerElement);
894
895                 var codeMirrorTextMarker = marker.codeMirrorTextMarker;
896                 this._codeMirror.setUniqueBookmark(codeMirrorTextMarker.find().from, swatchElement);
897
898                 swatchInnerElement.__colorTextMarker = codeMirrorTextMarker;
899                 swatchInnerElement.__color = color;
900             }.bind(this));
901         }
902
903         if (nonatomic)
904             update.call(this);
905         else
906             this._codeMirror.operation(update.bind(this));
907     }
908
909     _createBezierEditors(nonatomic, lineNumber)
910     {
911         function update()
912         {
913             var range = typeof lineNumber === "number" ? new WebInspector.TextRange(lineNumber, 0, lineNumber + 1, 0) : null;
914
915             // Look for cubic-bezier and timing functions and add cubic-bezier icons in front of them.
916             createCodeMirrorCubicBezierTextMarkers(this._codeMirror, range, function(marker, cubicBezier) {
917                 var bezierMarker = document.createElement("span");
918                 bezierMarker.title = WebInspector.UIString("Click to open a cubic-bezier editor");
919                 bezierMarker.className = WebInspector.CSSStyleDeclarationTextEditor.BezierEditorClassName;
920                 bezierMarker.addEventListener("click", this._cubicBezierMarkerClicked.bind(this));
921
922                 var codeMirrorTextMarker = marker.codeMirrorTextMarker;
923                 this._codeMirror.setUniqueBookmark(codeMirrorTextMarker.find().from, bezierMarker);
924
925                 bezierMarker.__textMarker = codeMirrorTextMarker;
926                 bezierMarker.__bezier = cubicBezier;
927             }.bind(this));
928         }
929
930         if (nonatomic)
931             update.call(this);
932         else
933             this._codeMirror.operation(update.bind(this));
934     }
935
936     _updateTextMarkerForPropertyIfNeeded(property)
937     {
938         var textMarker = property.__propertyTextMarker;
939         console.assert(textMarker);
940         if (!textMarker)
941             return;
942
943         var range = textMarker.find();
944         console.assert(range);
945         if (!range)
946             return;
947
948         this._createTextMarkerForPropertyIfNeeded(range.from, range.to, property);
949     }
950
951     _createTextMarkerForPropertyIfNeeded(from, to, property)
952     {
953         if (!this._codeMirror.getOption("readOnly")) {
954             // Create a new checkbox element and marker.
955
956             console.assert(property.enabled);
957
958             var checkboxElement = document.createElement("input");
959             checkboxElement.type = "checkbox";
960             checkboxElement.checked = true;
961             checkboxElement.addEventListener("change", this._propertyCheckboxChanged.bind(this));
962             checkboxElement.__cssProperty = property;
963
964             var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement);
965             checkboxMarker.__propertyCheckbox = true;
966         } else if (this._delegate.cssStyleDeclarationTextEditorShouldAddPropertyGoToArrows
967                 && !property.implicit && typeof this._delegate.cssStyleDeclarationTextEditorShowProperty === "function") {
968
969             let arrowElement = WebInspector.createGoToArrowButton();
970             arrowElement.title = WebInspector.UIString("Option-click to show source");
971
972             let delegate = this._delegate;
973             arrowElement.addEventListener("click", function(event) {
974                 delegate.cssStyleDeclarationTextEditorShowProperty(property, event.altKey);
975             });
976
977             this._codeMirror.setUniqueBookmark(to, arrowElement);
978         }
979
980         function duplicatePropertyExistsBelow(cssProperty)
981         {
982             var propertyFound = false;
983
984             for (var property of this._style.properties) {
985                 if (property === cssProperty)
986                     propertyFound = true;
987                 else if (property.name === cssProperty.name && propertyFound)
988                     return true;
989             }
990
991             return false;
992         }
993
994         var propertyNameIsValid = false;
995         if (WebInspector.CSSCompletions.cssNameCompletions)
996             propertyNameIsValid = WebInspector.CSSCompletions.cssNameCompletions.isValidPropertyName(property.name);
997
998         var classNames = ["css-style-declaration-property"];
999
1000         if (property.overridden)
1001             classNames.push("overridden");
1002
1003         if (property.implicit)
1004             classNames.push("implicit");
1005
1006         if (this._style.inherited && !property.inherited)
1007             classNames.push("not-inherited");
1008
1009         if (!property.valid && property.hasOtherVendorNameOrKeyword())
1010             classNames.push("other-vendor");
1011         else if (!property.valid && (!propertyNameIsValid || duplicatePropertyExistsBelow.call(this, property)))
1012             classNames.push("invalid");
1013
1014         if (!property.enabled)
1015             classNames.push("disabled");
1016
1017         if (property.__filterResultClassName && !property.__filterResultNeedlePosition)
1018             classNames.push(property.__filterResultClassName);
1019
1020         var classNamesString = classNames.join(" ");
1021
1022         // If there is already a text marker and it's in the same document, then try to avoid recreating it.
1023         // FIXME: If there are multiple CSSStyleDeclarationTextEditors for the same style then this will cause
1024         // both editors to fight and always recreate their text markers. This isn't really common.
1025         if (property.__propertyTextMarker && property.__propertyTextMarker.doc.cm === this._codeMirror && property.__propertyTextMarker.find()) {
1026             // If the class name is the same then we don't need to make a new marker.
1027             if (property.__propertyTextMarker.className === classNamesString)
1028                 return;
1029
1030             property.__propertyTextMarker.clear();
1031         }
1032
1033         var propertyTextMarker = this._codeMirror.markText(from, to, {className: classNamesString});
1034
1035         propertyTextMarker.__cssProperty = property;
1036         property.__propertyTextMarker = propertyTextMarker;
1037
1038         property.addEventListener(WebInspector.CSSProperty.Event.OverriddenStatusChanged, this._propertyOverriddenStatusChanged, this);
1039
1040         this._removeCheckboxPlaceholder(from.line);
1041
1042         if (property.__filterResultClassName && property.__filterResultNeedlePosition) {
1043             for (var needlePosition of property.__filterResultNeedlePosition.start) {
1044                 var start = {line: from.line, ch: needlePosition};
1045                 var end = {line: to.line, ch: start.ch + property.__filterResultNeedlePosition.length};
1046
1047                 this._codeMirror.markText(start, end, {className: property.__filterResultClassName});
1048             }
1049         }
1050
1051         if (property.hasOtherVendorNameOrKeyword() || property.text.trim().endsWith(":"))
1052             return;
1053
1054         var propertyHasUnnecessaryPrefix = property.name.startsWith("-webkit-") && WebInspector.CSSCompletions.cssNameCompletions.isValidPropertyName(property.canonicalName);
1055
1056         function generateInvalidMarker(options)
1057         {
1058             var invalidMarker = document.createElement("button");
1059             invalidMarker.className = "invalid-warning-marker";
1060             invalidMarker.title = options.title;
1061
1062             if (typeof options.correction === "string") {
1063                 // Allow for blank strings
1064                 invalidMarker.classList.add("clickable");
1065                 invalidMarker.addEventListener("click", function() {
1066                     this._codeMirror.replaceRange(options.correction, from, to);
1067
1068                     if (options.autocomplete) {
1069                         this._codeMirror.setCursor(to);
1070                         this.focus();
1071                         this._completionController._completeAtCurrentPosition(true);
1072                     }
1073                 }.bind(this));
1074             }
1075
1076             this._codeMirror.setBookmark(options.position, invalidMarker);
1077         }
1078
1079         function instancesOfProperty(propertyName)
1080         {
1081             var count = 0;
1082
1083             for (var property of this._style.properties) {
1084                 if (property.name === propertyName)
1085                     ++count;
1086             }
1087
1088             return count;
1089         }
1090
1091         // Number of times this property name is listed in the rule.
1092         var instances = instancesOfProperty.call(this, property.name);
1093         var invalidMarkerInfo;
1094
1095         if (propertyHasUnnecessaryPrefix && !instancesOfProperty.call(this, property.canonicalName)) {
1096             // This property has a prefix and is valid without the prefix and the rule containing this property does not have the unprefixed version of the property.
1097             generateInvalidMarker.call(this, {
1098                 position: from,
1099                 title: WebInspector.UIString("The 'webkit' prefix is not necessary.\nClick to insert a duplicate without the prefix."),
1100                 correction: property.text + "\n" + property.text.replace("-webkit-", ""),
1101                 autocomplete: false
1102             });
1103         } else if (instances > 1) {
1104             invalidMarkerInfo = {
1105                 position: from,
1106                 title: WebInspector.UIString("Duplicate property '%s'.\nClick to delete this property.").format(property.name),
1107                 correction: "",
1108                 autocomplete: false
1109             };
1110         }
1111
1112         if (property.valid) {
1113             if (invalidMarkerInfo)
1114                 generateInvalidMarker.call(this, invalidMarkerInfo);
1115
1116             return;
1117         }
1118
1119         if (propertyNameIsValid) {
1120             // The property's name is valid but its value is not (either it is not supported for this property or there is no value).
1121             var semicolon = /:\s*/.exec(property.text);
1122             var start = {line: from.line, ch: semicolon.index + semicolon[0].length};
1123             var end = {line: to.line, ch: start.ch + property.value.length};
1124
1125             this._codeMirror.markText(start, end, {className: "invalid"});
1126
1127             if (/^(?:\d+)$/.test(property.value)) {
1128                 invalidMarkerInfo = {
1129                     position: start,
1130                     title: WebInspector.UIString("The value '%s' needs units.\nClick to add 'px' to the value.").format(property.value),
1131                     correction: property.name + ": " + property.value + "px;",
1132                     autocomplete: false
1133                 };
1134             } else {
1135                 var valueReplacement = property.value.length ? WebInspector.UIString("The value '%s' is not supported for this property.\nClick to delete and open autocomplete.").format(property.value) : WebInspector.UIString("This property needs a value.\nClick to open autocomplete.");
1136
1137                 invalidMarkerInfo = {
1138                     position: start,
1139                     title: valueReplacement,
1140                     correction: property.name + ": ",
1141                     autocomplete: true
1142                 };
1143             }
1144         } else if (!instancesOfProperty.call(this, "-webkit-" + property.name) && WebInspector.CSSCompletions.cssNameCompletions.propertyRequiresWebkitPrefix(property.name)) {
1145             // The property is valid and exists in the rule while its prefixed version does not.
1146             invalidMarkerInfo = {
1147                 position: from,
1148                 title: WebInspector.UIString("The 'webkit' prefix is needed for this property.\nClick to insert a duplicate with the prefix."),
1149                 correction: "-webkit-" + property.text + "\n" + property.text,
1150                 autocomplete: false
1151             };
1152         } else if (!propertyHasUnnecessaryPrefix && !WebInspector.CSSCompletions.cssNameCompletions.isValidPropertyName("-webkit-" + property.name)) {
1153             // The property either has no prefix and is invalid with a prefix or is invalid without a prefix.
1154             var closestPropertyName = WebInspector.CSSCompletions.cssNameCompletions.getClosestPropertyName(property.name);
1155
1156             if (closestPropertyName) {
1157                 // The property name has less than 3 other properties that have the same Levenshtein distance.
1158                 invalidMarkerInfo = {
1159                     position: from,
1160                     title: WebInspector.UIString("Did you mean '%s'?\nClick to replace.").format(closestPropertyName),
1161                     correction: property.text.replace(property.name, closestPropertyName),
1162                     autocomplete: true
1163                 };
1164             } else if (property.name.startsWith("-webkit-") && (closestPropertyName = WebInspector.CSSCompletions.cssNameCompletions.getClosestPropertyName(property.canonicalName))) {
1165                 // The unprefixed property name has less than 3 other properties that have the same Levenshtein distance.
1166                 invalidMarkerInfo = {
1167                     position: from,
1168                     title: WebInspector.UIString("Did you mean '%s'?\nClick to replace.").format("-webkit-" + closestPropertyName),
1169                     correction: property.text.replace(property.canonicalName, closestPropertyName),
1170                     autocomplete: true
1171                 };
1172             } else {
1173                 // The property name is so vague or nonsensical that there are more than 3 other properties that have the same Levenshtein value.
1174                 invalidMarkerInfo = {
1175                     position: from,
1176                     title: WebInspector.UIString("The property '%s' is not supported.").format(property.name),
1177                     correction: false,
1178                     autocomplete: false
1179                 };
1180             }
1181         }
1182
1183         if (!invalidMarkerInfo)
1184             return;
1185
1186         generateInvalidMarker.call(this, invalidMarkerInfo);
1187     }
1188
1189     _clearTextMarkers(nonatomic, all)
1190     {
1191         function clear()
1192         {
1193             var markers = this._codeMirror.getAllMarks();
1194             for (var i = 0; i < markers.length; ++i) {
1195                 var textMarker = markers[i];
1196
1197                 if (!all && textMarker.__checkboxPlaceholder) {
1198                     var position = textMarker.find();
1199
1200                     // Only keep checkbox placeholders if they are in the first column.
1201                     if (position && !position.ch)
1202                         continue;
1203                 }
1204
1205                 if (textMarker.__cssProperty) {
1206                     textMarker.__cssProperty.removeEventListener(null, null, this);
1207
1208                     delete textMarker.__cssProperty.__propertyTextMarker;
1209                     delete textMarker.__cssProperty;
1210                 }
1211
1212                 textMarker.clear();
1213             }
1214         }
1215
1216         if (nonatomic)
1217             clear.call(this);
1218         else
1219             this._codeMirror.operation(clear.bind(this));
1220     }
1221
1222     _iterateOverProperties(onlyVisibleProperties, callback)
1223     {
1224         var properties = onlyVisibleProperties ? this._style.visibleProperties : this._style.properties;
1225
1226         if (this._filterResultPropertyNames) {
1227             properties = properties.filter(function(property) {
1228                 return (!property.implicit || this._showsImplicitProperties) && property.name in this._filterResultPropertyNames;
1229             }, this);
1230
1231             if (this._sortProperties)
1232                 properties.sort(function(a, b) { return a.name.localeCompare(b.name); });
1233         } else if (!onlyVisibleProperties) {
1234             // Filter based on options only when all properties are used.
1235             properties = properties.filter(function(property) {
1236                 return !property.implicit || this._showsImplicitProperties || property.canonicalName in this._alwaysShowPropertyNames;
1237             }, this);
1238
1239             if (this._sortProperties)
1240                 properties.sort(function(a, b) { return a.name.localeCompare(b.name); });
1241         }
1242
1243         for (var i = 0; i < properties.length; ++i) {
1244             if (callback.call(this, properties[i], i === properties.length - 1))
1245                 break;
1246         }
1247     }
1248
1249     _propertyCheckboxChanged(event)
1250     {
1251         var property = event.target.__cssProperty;
1252         console.assert(property);
1253         if (!property)
1254             return;
1255
1256         this._commentProperty(property);
1257     }
1258
1259     _commentProperty(property)
1260     {
1261         var textMarker = property.__propertyTextMarker;
1262         console.assert(textMarker);
1263         if (!textMarker)
1264             return;
1265
1266         // Check if the property has been removed already, like from double-clicking
1267         // the checkbox and calling this event listener multiple times.
1268         var range = textMarker.find();
1269         if (!range)
1270             return;
1271
1272         property._commentRange = range;
1273         property._commentRange.to.ch += 6; // Number of characters added by comments.
1274
1275         var text = this._codeMirror.getRange(range.from, range.to);
1276
1277         function update()
1278         {
1279             // Replace the text with a commented version.
1280             this._codeMirror.replaceRange("/* " + text + " */", range.from, range.to);
1281
1282             // Update the line for any color swatches or cubic-beziers that got removed.
1283             this._createColorSwatches(true, range.from.line);
1284             this._createBezierEditors(true, range.from.line);
1285         }
1286
1287         this._codeMirror.operation(update.bind(this));
1288     }
1289
1290     _propertyCommentCheckboxChanged(event)
1291     {
1292         var commentTextMarker = event.target.__commentTextMarker;
1293         console.assert(commentTextMarker);
1294         if (!commentTextMarker)
1295             return;
1296
1297         // Check if the comment has been removed already, like from double-clicking
1298         // the checkbox and calling event listener multiple times.
1299         var range = commentTextMarker.find();
1300         if (!range)
1301             return;
1302
1303         this._uncommentRange(range);
1304     }
1305
1306     _uncommentRange(range)
1307     {
1308         var text = this._codeMirror.getRange(range.from, range.to);
1309
1310         // Remove the comment prefix and suffix.
1311         text = text.replace(/^\/\*\s*/, "").replace(/\s*\*\/$/, "");
1312
1313         // Add a semicolon if there isn't one already.
1314         if (text.length && text.charAt(text.length - 1) !== ";")
1315             text += ";";
1316
1317         function update()
1318         {
1319             this._codeMirror.addLineClass(range.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);
1320             this._codeMirror.replaceRange(text, range.from, range.to);
1321
1322             // Update the line for any color swatches or cubic-beziers that got removed.
1323             this._createColorSwatches(true, range.from.line);
1324             this._createBezierEditors(true, range.from.line);
1325         }
1326
1327         this._codeMirror.operation(update.bind(this));
1328     }
1329
1330     _colorSwatchClicked(event)
1331     {
1332         if (this._colorPickerPopover)
1333             return;
1334
1335         var swatch = event.target;
1336
1337         var color = swatch.__color;
1338         console.assert(color);
1339         if (!color)
1340             return;
1341
1342         var colorTextMarker = swatch.__colorTextMarker;
1343         console.assert(colorTextMarker);
1344         if (!colorTextMarker)
1345             return;
1346
1347         var range = colorTextMarker.find();
1348         console.assert(range);
1349         if (!range)
1350             return;
1351
1352         function updateCodeMirror(newColorText)
1353         {
1354             function update()
1355             {
1356                 // The original text marker might have been cleared by a style update,
1357                 // in this case we need to find the new color text marker so we know
1358                 // the right range for the new style color text.
1359                 if (!colorTextMarker || !colorTextMarker.find()) {
1360                     colorTextMarker = null;
1361
1362                     var marks = this._codeMirror.findMarksAt(range.from);
1363                     if (!marks.length)
1364                         return;
1365
1366                     for (var i = 0; i < marks.length; ++i) {
1367                         var mark = marks[i];
1368                         if (WebInspector.TextMarker.textMarkerForCodeMirrorTextMarker(mark).type !== WebInspector.TextMarker.Type.Color)
1369                             continue;
1370                         colorTextMarker = mark;
1371                         break;
1372                     }
1373                 }
1374
1375                 if (!colorTextMarker)
1376                     return;
1377
1378                 // Sometimes we still might find a stale text marker with findMarksAt.
1379                 var newRange = colorTextMarker.find();
1380                 if (!newRange)
1381                     return;
1382
1383                 range = newRange;
1384
1385                 colorTextMarker.clear();
1386
1387                 this._codeMirror.replaceRange(newColorText, range.from, range.to);
1388
1389                 // The color's text format could have changed, so we need to update the "range"
1390                 // variable to anticipate a different "range.to" property.
1391                 range.to.ch = range.from.ch + newColorText.length;
1392
1393                 colorTextMarker = this._codeMirror.markText(range.from, range.to);
1394
1395                 swatch.__colorTextMarker = colorTextMarker;
1396             }
1397
1398             this._codeMirror.operation(update.bind(this));
1399         }
1400
1401         if (event.shiftKey || this._codeMirror.getOption("readOnly")) {
1402             var nextFormat = color.nextFormat();
1403             console.assert(nextFormat);
1404             if (!nextFormat)
1405                 return;
1406             color.format = nextFormat;
1407
1408             var newColorText = color.toString();
1409
1410             // Ignore the change so we don't commit the format change. However, any future user
1411             // edits will commit the color format.
1412             this._ignoreCodeMirrorContentDidChangeEvent = true;
1413             updateCodeMirror.call(this, newColorText);
1414             delete this._ignoreCodeMirrorContentDidChangeEvent;
1415         } else {
1416             this._colorPickerPopover = new WebInspector.Popover(this);
1417
1418             var colorPicker = new WebInspector.ColorPicker;
1419
1420             colorPicker.addEventListener(WebInspector.ColorPicker.Event.ColorChanged, function(event) {
1421                 updateCodeMirror.call(this, event.data.color.toString());
1422             }.bind(this));
1423
1424             var bounds = WebInspector.Rect.rectFromClientRect(swatch.getBoundingClientRect());
1425
1426             this._colorPickerPopover.content = colorPicker.element;
1427             this._colorPickerPopover.present(bounds.pad(2), [WebInspector.RectEdge.MIN_X]);
1428
1429             colorPicker.color = color;
1430         }
1431     }
1432
1433     _cubicBezierMarkerClicked(event)
1434     {
1435         if (this._cubicBezierEditorPopover)
1436             return;
1437
1438         var bezierMarker = event.target;
1439
1440         var bezier = bezierMarker.__bezier;
1441         console.assert(bezier);
1442         if (!bezier)
1443             return;
1444
1445         var bezierTextMarker = bezierMarker.__textMarker;
1446         console.assert(bezierTextMarker);
1447         if (!bezierTextMarker)
1448             return;
1449
1450         var range = bezierTextMarker.find();
1451         console.assert(range);
1452         if (!range)
1453             return;
1454
1455         function updateCodeMirror(newCubicBezierText)
1456         {
1457             function update()
1458             {
1459                 // The original text marker might have been cleared by a style update,
1460                 // in this case we need to find the new bezier text marker so we know
1461                 // the right range for the new style bezier text.
1462                 if (!bezierTextMarker || !bezierTextMarker.find()) {
1463                     bezierTextMarker = null;
1464
1465                     var marks = this._codeMirror.findMarksAt(range.from);
1466                     if (!marks.length)
1467                         return;
1468
1469                     for (var i = 0; i < marks.length; ++i) {
1470                         var mark = marks[i];
1471                         if (WebInspector.TextMarker.textMarkerForCodeMirrorTextMarker(mark).type !== WebInspector.TextMarker.Type.CubicBezier)
1472                             continue;
1473                         bezierTextMarker = mark;
1474                         break;
1475                     }
1476                 }
1477
1478                 if (!bezierTextMarker)
1479                     return;
1480
1481                 // Sometimes we still might find a stale text marker with findMarksAt.
1482                 var newRange = bezierTextMarker.find();
1483                 if (!newRange)
1484                     return;
1485
1486                 range = newRange;
1487
1488                 bezierTextMarker.clear();
1489
1490                 this._codeMirror.replaceRange(newCubicBezierText, range.from, range.to);
1491
1492                 // The bezier's text format could have changed, so we need to update the "range"
1493                 // variable to anticipate a different "range.to" property.
1494                 range.to.ch = range.from.ch + newCubicBezierText.length;
1495
1496                 bezierTextMarker = this._codeMirror.markText(range.from, range.to);
1497
1498                 bezierMarker.__textMarker = bezierTextMarker;
1499             }
1500
1501             this._codeMirror.operation(update.bind(this));
1502         }
1503
1504         this._cubicBezierEditorPopover = new WebInspector.Popover(this);
1505
1506         var bezierEditor = new WebInspector.BezierEditor;
1507
1508         bezierEditor.addEventListener(WebInspector.BezierEditor.Event.BezierChanged, function(event) {
1509             updateCodeMirror.call(this, event.data.bezier.toString());
1510         }.bind(this));
1511
1512         var bounds = WebInspector.Rect.rectFromClientRect(bezierMarker.getBoundingClientRect());
1513
1514         this._cubicBezierEditorPopover.content = bezierEditor.element;
1515         this._cubicBezierEditorPopover.present(bounds.pad(2), [WebInspector.RectEdge.MIN_X]);
1516
1517         bezierEditor.bezier = bezier;
1518     }
1519
1520     _propertyOverriddenStatusChanged(event)
1521     {
1522         this._updateTextMarkerForPropertyIfNeeded(event.target);
1523     }
1524
1525     _propertiesChanged(event)
1526     {
1527         // Don't try to update the document while completions are showing. Doing so will clear
1528         // the completion hint and prevent further interaction with the completion.
1529         if (this._completionController.isShowingCompletions())
1530             return;
1531
1532         // Reset the content if the text is different and we are not focused.
1533         if (!this.focused && (!this._style.text || this._style.text !== this._formattedContent())) {
1534             this._resetContent();
1535             return;
1536         }
1537
1538         this._removeEditingLineClassesSoon();
1539
1540         this._updateTextMarkers();
1541     }
1542
1543     _markLinesWithCheckboxPlaceholder()
1544     {
1545         if (this._codeMirror.getOption("readOnly"))
1546             return;
1547
1548         var linesWithPropertyCheckboxes = {};
1549         var linesWithCheckboxPlaceholders = {};
1550
1551         var markers = this._codeMirror.getAllMarks();
1552         for (var i = 0; i < markers.length; ++i) {
1553             var textMarker = markers[i];
1554             if (textMarker.__propertyCheckbox) {
1555                 var position = textMarker.find();
1556                 if (position)
1557                     linesWithPropertyCheckboxes[position.line] = true;
1558             } else if (textMarker.__checkboxPlaceholder) {
1559                 var position = textMarker.find();
1560                 if (position)
1561                     linesWithCheckboxPlaceholders[position.line] = true;
1562             }
1563         }
1564
1565         var lineCount = this._codeMirror.lineCount();
1566
1567         for (var i = 0; i < lineCount; ++i) {
1568             if (i in linesWithPropertyCheckboxes || i in linesWithCheckboxPlaceholders)
1569                 continue;
1570
1571             var position = {line: i, ch: 0};
1572
1573             var placeholderElement = document.createElement("div");
1574             placeholderElement.className = WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName;
1575
1576             var placeholderMark = this._codeMirror.setUniqueBookmark(position, placeholderElement);
1577             placeholderMark.__checkboxPlaceholder = true;
1578         }
1579     }
1580
1581     _removeCheckboxPlaceholder(lineNumber)
1582     {
1583         var marks = this._codeMirror.findMarksAt({line: lineNumber, ch: 0});
1584         for (var i = 0; i < marks.length; ++i) {
1585             var mark = marks[i];
1586             if (!mark.__checkboxPlaceholder)
1587                 continue;
1588
1589             mark.clear();
1590             return;
1591         }
1592     }
1593
1594     _formattedContentFromEditor()
1595     {
1596         var mapping = {original: [0], formatted: [0]};
1597         // FIXME: <rdar://problem/10593948> Provide a way to change the tab width in the Web Inspector
1598         var indentString = "    ";
1599         var builder = new WebInspector.FormatterContentBuilder(mapping, [], [], 0, 0, indentString);
1600         var formatter = new WebInspector.Formatter(this._codeMirror, builder);
1601         var start = {line: 0, ch: 0};
1602         var end = {line: this._codeMirror.lineCount() - 1};
1603         formatter.format(start, end);
1604
1605         return builder.formattedContent.trim();
1606     }
1607
1608     _resetContent()
1609     {
1610         if (this._commitChangesTimeout) {
1611             clearTimeout(this._commitChangesTimeout);
1612             this._commitChangesTimeout = null;
1613         }
1614
1615         this._removeEditingLineClasses();
1616
1617         // Only allow editing if we have a style, it is editable and we have text range in the stylesheet.
1618         const readOnly = !this._style || !this._style.editable || !this._style.styleSheetTextRange;
1619         this._codeMirror.setOption("readOnly", readOnly);
1620
1621         if (readOnly) {
1622             this.element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName);
1623             this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties"));
1624         } else {
1625             this.element.classList.remove(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName);
1626             this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties \u2014 Click to Edit"));
1627         }
1628
1629         if (!this._style) {
1630             this._ignoreCodeMirrorContentDidChangeEvent = true;
1631
1632             this._clearTextMarkers(false, true);
1633             this._codeMirror.setValue("");
1634             this._codeMirror.clearHistory();
1635             this._codeMirror.markClean();
1636
1637             this._ignoreCodeMirrorContentDidChangeEvent = false;
1638             return;
1639         }
1640
1641         function update()
1642         {
1643             // Remember the cursor position/selection.
1644             let isEditorReadOnly = this._codeMirror.getOption("readOnly");
1645             let styleText = this._style.text;
1646             let trimmedStyleText = styleText.trim();
1647
1648             // We only need to format non-empty styles, but prepare checkbox placeholders
1649             // in any case because that will indent the cursor when the User starts typing.
1650             if (!trimmedStyleText && !isEditorReadOnly) {
1651                 this._markLinesWithCheckboxPlaceholder();
1652                 return;
1653             }
1654
1655             // Generate formatted content for readonly editors by iterating properties.
1656             if (isEditorReadOnly) {
1657                 this._codeMirror.setValue("");
1658                 let lineNumber = 0;
1659                 this._iterateOverProperties(false, function(property) {
1660                     let from = {line: lineNumber, ch: 0};
1661                     let to = {line: lineNumber};
1662                     // Readonly properties are pretty printed by `synthesizedText` and not the Formatter.
1663                     this._codeMirror.replaceRange((lineNumber ? "\n" : "") + property.synthesizedText, from);
1664                     this._createTextMarkerForPropertyIfNeeded(from, to, property);
1665                     lineNumber++;
1666                 });
1667                 return;
1668             }
1669
1670             let selectionAnchor = this._codeMirror.getCursor("anchor");
1671             let selectionHead = this._codeMirror.getCursor("head");
1672             let whitespaceRegex = /\s+/g;
1673
1674             // FIXME: <rdar://problem/10593948> Provide a way to change the tab width in the Web Inspector
1675             this._linePrefixWhitespace = "    ";
1676             let styleTextPrefixWhitespace = styleText.match(/^\s*/);
1677
1678             // If there is a match and the style text contains a newline, attempt to pull out the prefix whitespace
1679             // in front of the first line of CSS to use for every line.  If  there is no newline, we want to avoid
1680             // adding multiple spaces to a single line CSS rule and instead format it on multiple lines.
1681             if (styleTextPrefixWhitespace && trimmedStyleText.includes("\n")) {
1682                 let linePrefixWhitespaceMatch = styleTextPrefixWhitespace[0].match(/[^\S\n]+$/);
1683                 if (linePrefixWhitespaceMatch)
1684                     this._linePrefixWhitespace = linePrefixWhitespaceMatch[0];
1685             }
1686
1687             // Set non-optimized, valid and invalid styles in preparation for the Formatter.
1688             this._codeMirror.setValue(trimmedStyleText);
1689
1690             // Now the Formatter pretty prints the styles.
1691             this._codeMirror.setValue(this._formattedContentFromEditor());
1692
1693             // We need to workaround the fact that...
1694             // 1) `this._style.properties` only holds valid CSSProperty instances but not
1695             // comments and invalid properties like `color;`.
1696             // 2) `_createTextMarkerForPropertyIfNeeded` relies on CSSProperty instances.
1697             let cssPropertiesMap = new Map();
1698             this._iterateOverProperties(false, function(cssProperty) {
1699                 cssProperty.__refreshedAfterBlur = false;
1700
1701                 let propertyTextSansWhitespace = cssProperty.text.replace(whitespaceRegex, "");
1702                 let existingProperties = cssPropertiesMap.get(propertyTextSansWhitespace) || [];
1703                 existingProperties.push(cssProperty);
1704
1705                 cssPropertiesMap.set(propertyTextSansWhitespace, existingProperties);
1706             });
1707
1708             // Go through the Editor line by line and create TextMarker when a
1709             // CSSProperty instance for that property exists. If not, then don't create a TextMarker.
1710             this._codeMirror.eachLine(function(lineHandler) {
1711                 let lineNumber = lineHandler.lineNo();
1712                 let lineContentSansWhitespace = lineHandler.text.replace(whitespaceRegex, "");
1713                 let properties = cssPropertiesMap.get(lineContentSansWhitespace);
1714                 if (!properties) {
1715                     this._createCommentedCheckboxMarker(lineHandler);
1716                     return;
1717                 }
1718
1719                 for (let property of properties) {
1720                     if (property.__refreshedAfterBlur)
1721                         continue;
1722
1723                     let from = {line: lineNumber, ch: 0};
1724                     let to = {line: lineNumber};
1725                     this._createTextMarkerForPropertyIfNeeded(from, to, property);
1726                     property.__refreshedAfterBlur = true;
1727                     break;
1728                 }
1729             }.bind(this));
1730
1731             // Look for colors and make swatches.
1732             this._createColorSwatches(true);
1733             this._createBezierEditors(true);
1734
1735             // Restore the cursor position/selection.
1736             this._codeMirror.setSelection(selectionAnchor, selectionHead);
1737
1738             // Reset undo history since undo past the reset is wrong when the content was empty before
1739             // or the content was representing a previous style object.
1740             this._codeMirror.clearHistory();
1741
1742             // Mark the editor as clean (unedited state).
1743             this._codeMirror.markClean();
1744
1745             this._markLinesWithCheckboxPlaceholder();
1746         }
1747
1748         // This needs to be done first and as a separate operation to avoid an exception in CodeMirror.
1749         this._clearTextMarkers(false, true);
1750
1751         this._ignoreCodeMirrorContentDidChangeEvent = true;
1752         this._codeMirror.operation(update.bind(this));
1753         this._ignoreCodeMirrorContentDidChangeEvent = false;
1754     }
1755
1756     _updateJumpToSymbolTrackingMode()
1757     {
1758         var oldJumpToSymbolTrackingModeEnabled = this._jumpToSymbolTrackingModeEnabled;
1759
1760         if (!this._style || !this._style.ownerRule || !this._style.ownerRule.sourceCodeLocation)
1761             this._jumpToSymbolTrackingModeEnabled = false;
1762         else
1763             this._jumpToSymbolTrackingModeEnabled = WebInspector.modifierKeys.altKey && !WebInspector.modifierKeys.metaKey && !WebInspector.modifierKeys.shiftKey;
1764
1765         if (oldJumpToSymbolTrackingModeEnabled !== this._jumpToSymbolTrackingModeEnabled) {
1766             if (this._jumpToSymbolTrackingModeEnabled) {
1767                 this._tokenTrackingController.highlightLastHoveredRange();
1768                 this._tokenTrackingController.enabled = !this._codeMirror.getOption("readOnly");
1769             } else {
1770                 this._tokenTrackingController.removeHighlightedRange();
1771                 this._tokenTrackingController.enabled = false;
1772             }
1773         }
1774     }
1775
1776     tokenTrackingControllerHighlightedRangeWasClicked(tokenTrackingController)
1777     {
1778         console.assert(this._style.ownerRule.sourceCodeLocation);
1779         if (!this._style.ownerRule.sourceCodeLocation)
1780             return;
1781
1782         // Special case command clicking url(...) links.
1783         var token = this._tokenTrackingController.candidate.hoveredToken;
1784         if (/\blink\b/.test(token.type)) {
1785             var url = token.string;
1786             var baseURL = this._style.ownerRule.sourceCodeLocation.sourceCode.url;
1787             WebInspector.openURL(absoluteURL(url, baseURL));
1788             return;
1789         }
1790
1791         // Jump to the rule if we can't find a property.
1792         // Find a better source code location from the property that was clicked.
1793         var sourceCodeLocation = this._style.ownerRule.sourceCodeLocation;
1794         var marks = this._codeMirror.findMarksAt(this._tokenTrackingController.candidate.hoveredTokenRange.start);
1795         for (var i = 0; i < marks.length; ++i) {
1796             var mark = marks[i];
1797             var property = mark.__cssProperty;
1798             if (property) {
1799                 var sourceCode = sourceCodeLocation.sourceCode;
1800                 var styleSheetTextRange = property.styleSheetTextRange;
1801                 sourceCodeLocation = sourceCode.createSourceCodeLocation(styleSheetTextRange.startLine, styleSheetTextRange.startColumn);
1802             }
1803         }
1804
1805         WebInspector.showSourceCodeLocation(sourceCodeLocation);
1806     }
1807
1808     tokenTrackingControllerNewHighlightCandidate(tokenTrackingController, candidate)
1809     {
1810         this._tokenTrackingController.highlightRange(candidate.hoveredTokenRange);
1811     }
1812 };
1813
1814 WebInspector.CSSStyleDeclarationTextEditor.Event = {
1815     ContentChanged: "css-style-declaration-text-editor-content-changed",
1816     Blurred: "css-style-declaration-text-editor-blurred"
1817 };
1818
1819 WebInspector.CSSStyleDeclarationTextEditor.StyleClassName = "css-style-text-editor";
1820 WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName = "read-only";
1821 WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName = "color-swatch";
1822 WebInspector.CSSStyleDeclarationTextEditor.BezierEditorClassName = "cubic-bezier-marker";
1823 WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName = "checkbox-placeholder";
1824 WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName = "editing-line";
1825 WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay = 250;
1826 WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay = 2000;