Web Inspector: Tabbing over CSS properties prepended by * doesn't move the highlighte...
[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         let cursor = codeMirror.getCursor();
564         let line = codeMirror.getLine(cursor.line);
565         let previousLine = codeMirror.getLine(cursor.line - 1);
566
567         if (!line && !previousLine && !cursor.line)
568             return switchRule.call(this);
569
570         let trimmedPreviousLine = previousLine ? previousLine.trimRight() : "";
571         let previousAnchor = 0;
572         let previousHead = line.length;
573         let isComment = this._textAtCursorIsComment(codeMirror, cursor);
574
575         if (cursor.ch === line.indexOf(":") || line.indexOf(":") < 0 || isComment) {
576             if (previousLine) {
577                 --cursor.line;
578                 previousHead = trimmedPreviousLine.length;
579
580                 if (!this._textAtCursorIsComment(codeMirror, cursor)) {
581                     let colon = /(?::\s*)/.exec(previousLine);
582                     previousAnchor = colon ? colon.index + colon[0].length : 0;
583                     if (trimmedPreviousLine.includes(";"))
584                         previousHead = trimmedPreviousLine.lastIndexOf(";");
585                 }
586                 
587                 codeMirror.setSelection({line: cursor.line, ch: previousAnchor}, {line: cursor.line, ch: previousHead});
588                 return;
589             }
590
591             if (cursor.line) {
592                 codeMirror.setCursor(cursor.line - 1, 0);
593                 return;
594             }
595
596             return switchRule.call(this);
597         }
598
599         if (!isComment) {
600             let match = /(?:[^:;\s]\s*)+/.exec(line);
601             previousAnchor = match.index;
602             previousHead = previousAnchor + match[0].length;
603         }
604
605         codeMirror.setSelection({line: cursor.line, ch: previousAnchor}, {line: cursor.line, ch: previousHead});
606     }
607
608     _handleTabKey(codeMirror)
609     {
610         function switchRule() {
611             if (this._delegate && typeof this._delegate.cssStyleDeclarationTextEditorSwitchRule === "function") {
612                 this._delegate.cssStyleDeclarationTextEditorSwitchRule();
613                 return;
614             }
615
616             return CodeMirror.Pass;
617         }
618
619         let cursor = codeMirror.getCursor();
620         let line = codeMirror.getLine(cursor.line);
621         let trimmedLine = line.trimRight();
622         let lastLine = cursor.line === codeMirror.lineCount() - 1;
623         let nextLine = codeMirror.getLine(cursor.line + 1);
624         let trimmedNextLine = nextLine ? nextLine.trimRight() : "";
625
626         if (!trimmedLine.trimLeft().length) {
627             if (lastLine)
628                 return switchRule.call(this);
629
630             if (!trimmedNextLine.trimLeft().length) {
631                 codeMirror.setCursor(cursor.line + 1, 0);
632                 return;
633             }
634
635             ++cursor.line;
636             this._highlightNextNameOrValue(codeMirror, cursor, nextLine);
637             return;
638         }
639
640         if (trimmedLine.endsWith(":")) {
641             codeMirror.setCursor(cursor.line, line.length);
642             this._completionController._completeAtCurrentPosition(true);
643             return;
644         }
645
646         let hasEndingSemicolon = trimmedLine.endsWith(";");
647         let pastLastSemicolon = line.includes(";") && cursor.ch >= line.lastIndexOf(";");
648
649         if (cursor.ch >= line.trimRight().length - hasEndingSemicolon || pastLastSemicolon) {
650             this._completionController.completeAtCurrentPositionIfNeeded().then(function(result) {
651                 if (result !== WebInspector.CodeMirrorCompletionController.UpdatePromise.NoCompletionsFound)
652                     return;
653
654                 let replacement = "";
655
656                 if (!hasEndingSemicolon && !pastLastSemicolon && !this._textAtCursorIsComment(codeMirror, cursor))
657                     replacement += ";";
658
659                 if (lastLine)
660                     replacement += "\n";
661
662                 if (replacement.length)
663                     codeMirror.replaceRange(replacement, {line: cursor.line, ch: trimmedLine.length});
664
665                 if (!nextLine) {
666                     codeMirror.setCursor(cursor.line + 1, 0);
667                     return;
668                 }
669
670                 this._highlightNextNameOrValue(codeMirror, {line: cursor.line + 1, ch: 0}, nextLine);
671             }.bind(this));
672
673             return;
674         }
675
676         this._highlightNextNameOrValue(codeMirror, cursor, line);
677     }
678
679     _clearRemoveEditingLineClassesTimeout()
680     {
681         if (!this._removeEditingLineClassesTimeout)
682             return;
683
684         clearTimeout(this._removeEditingLineClassesTimeout);
685         delete this._removeEditingLineClassesTimeout;
686     }
687
688     _removeEditingLineClasses()
689     {
690         this._clearRemoveEditingLineClassesTimeout();
691
692         function removeEditingLineClasses()
693         {
694             var lineCount = this._codeMirror.lineCount();
695             for (var i = 0; i < lineCount; ++i)
696                 this._codeMirror.removeLineClass(i, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);
697         }
698
699         this._codeMirror.operation(removeEditingLineClasses.bind(this));
700     }
701
702     _removeEditingLineClassesSoon()
703     {
704         if (this._removeEditingLineClassesTimeout)
705             return;
706         this._removeEditingLineClassesTimeout = setTimeout(this._removeEditingLineClasses.bind(this), WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay);
707     }
708
709     _formattedContent()
710     {
711         // Start with the prefix whitespace we stripped.
712         var content = this._prefixWhitespace;
713
714         // Get each line and add the line prefix whitespace and newlines.
715         var lineCount = this._codeMirror.lineCount();
716         for (var i = 0; i < lineCount; ++i) {
717             var lineContent = this._codeMirror.getLine(i);
718             content += this._linePrefixWhitespace + lineContent;
719             if (i !== lineCount - 1)
720                 content += "\n";
721         }
722
723         // Add the suffix whitespace we stripped.
724         content += this._suffixWhitespace;
725
726         // This regular expression replacement removes extra newlines
727         // in between properties while preserving leading whitespace
728         return content.replace(/\s*\n\s*\n(\s*)/g, "\n$1");
729     }
730
731     _commitChanges()
732     {
733         if (this._commitChangesTimeout) {
734             clearTimeout(this._commitChangesTimeout);
735             delete this._commitChangesTimeout;
736         }
737
738         this._style.text = this._formattedContent();
739     }
740
741     _editorBlured(codeMirror)
742     {
743         // Clicking a suggestion causes the editor to blur. We don't want to reset content in this case.
744         if (this._completionController.isHandlingClickEvent())
745             return;
746
747         // Reset the content on blur since we stop accepting external changes while the the editor is focused.
748         // This causes us to pick up any change that was suppressed while the editor was focused.
749         this._resetContent();
750         this.dispatchEventToListeners(WebInspector.CSSStyleDeclarationTextEditor.Event.Blurred);
751     }
752
753     _editorFocused(codeMirror)
754     {
755         if (typeof this._delegate.cssStyleDeclarationTextEditorFocused === "function")
756             this._delegate.cssStyleDeclarationTextEditorFocused();
757     }
758
759     _contentChanged(codeMirror, change)
760     {
761         // Return early if the style isn't editable. This still can be called when readOnly is set because
762         // clicking on a color swatch modifies the text.
763         if (!this._style || !this._style.editable || this._ignoreCodeMirrorContentDidChangeEvent)
764             return;
765
766         this._markLinesWithCheckboxPlaceholder();
767
768         this._clearRemoveEditingLineClassesTimeout();
769         this._codeMirror.addLineClass(change.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);
770
771         // When the change is a completion change, create color swatches now since the changes
772         // will not go through _propertiesChanged until completionControllerCompletionsHidden happens.
773         // This way any auto completed colors get swatches right away.
774         if (this._completionController.isCompletionChange(change)) {
775             this._createColorSwatches(false, change.from.line);
776             this._createBezierEditors(false, change.from.line);
777         }
778
779         // Use a short delay for user input to coalesce more changes before committing. Other actions like
780         // undo, redo and paste are atomic and work better with a zero delay. CodeMirror identifies changes that
781         // get coalesced in the undo stack with a "+" prefix on the origin. Use that to set the delay for our coalescing.
782         var delay = change.origin && change.origin.charAt(0) === "+" ? WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay : 0;
783
784         // Reset the timeout so rapid changes coalesce after a short delay.
785         if (this._commitChangesTimeout)
786             clearTimeout(this._commitChangesTimeout);
787         this._commitChangesTimeout = setTimeout(this._commitChanges.bind(this), delay);
788
789         this.dispatchEventToListeners(WebInspector.CSSStyleDeclarationTextEditor.Event.ContentChanged);
790     }
791
792     _updateTextMarkers(nonatomic)
793     {
794         function update()
795         {
796             this._clearTextMarkers(true);
797
798             this._iterateOverProperties(true, function(property) {
799                 var styleTextRange = property.styleDeclarationTextRange;
800                 console.assert(styleTextRange);
801                 if (!styleTextRange)
802                     return;
803
804                 var from = {line: styleTextRange.startLine, ch: styleTextRange.startColumn};
805                 var to = {line: styleTextRange.endLine, ch: styleTextRange.endColumn};
806
807                 // Adjust the line position for the missing prefix line.
808                 if (this._prefixWhitespace) {
809                     --from.line;
810                     --to.line;
811                 }
812
813                 // Adjust the column for the stripped line prefix whitespace.
814                 from.ch -= this._linePrefixWhitespace.length;
815                 to.ch -= this._linePrefixWhitespace.length;
816
817                 this._createTextMarkerForPropertyIfNeeded(from, to, property);
818             });
819
820             if (!this._codeMirror.getOption("readOnly")) {
821                 // Look for comments that look like properties and add checkboxes in front of them.
822                 this._codeMirror.eachLine(function(lineHandler) {
823                     this._createCommentedCheckboxMarker(lineHandler);
824                 }.bind(this));
825             }
826
827             // Look for colors and make swatches.
828             this._createColorSwatches(true);
829             this._createBezierEditors(true);
830
831             this._markLinesWithCheckboxPlaceholder();
832         }
833
834         if (nonatomic)
835             update.call(this);
836         else
837             this._codeMirror.operation(update.bind(this));
838     }
839
840     _createCommentedCheckboxMarker(lineHandle)
841     {
842         var lineNumber = lineHandle.lineNo();
843
844         // Since lineNumber can be 0, it is also necessary to check if it is a number before returning.
845         if (!lineNumber && isNaN(lineNumber))
846             return;
847
848         // Matches a comment like: /* -webkit-foo: bar; */
849         var commentedPropertyRegex = /\/\*\s*[-\w]+\s*:\s*[^;]+;?\s*\*\//g;
850
851         var match = commentedPropertyRegex.exec(lineHandle.text);
852         if (!match)
853             return;
854
855         while (match) {
856             var checkboxElement = document.createElement("input");
857             checkboxElement.type = "checkbox";
858             checkboxElement.checked = false;
859             checkboxElement.addEventListener("change", this._propertyCommentCheckboxChanged.bind(this));
860
861             var from = {line: lineNumber, ch: match.index};
862             var to = {line: lineNumber, ch: match.index + match[0].length};
863
864             var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement);
865             checkboxMarker.__propertyCheckbox = true;
866
867             var commentTextMarker = this._codeMirror.markText(from, to);
868
869             checkboxElement.__commentTextMarker = commentTextMarker;
870
871             match = commentedPropertyRegex.exec(lineHandle.text);
872         }
873     }
874
875     _createColorSwatches(nonatomic, lineNumber)
876     {
877         function update()
878         {
879             var range = typeof lineNumber === "number" ? new WebInspector.TextRange(lineNumber, 0, lineNumber + 1, 0) : null;
880
881             // Look for color strings and add swatches in front of them.
882             createCodeMirrorColorTextMarkers(this._codeMirror, range, function(marker, color, colorString) {
883                 var swatchElement = document.createElement("span");
884                 swatchElement.title = WebInspector.UIString("Click to select a color. Shift-click to switch color formats.");
885                 swatchElement.className = WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName;
886                 swatchElement.addEventListener("click", this._colorSwatchClicked.bind(this));
887
888                 var swatchInnerElement = document.createElement("span");
889                 swatchInnerElement.style.backgroundColor = colorString;
890                 swatchElement.appendChild(swatchInnerElement);
891
892                 var codeMirrorTextMarker = marker.codeMirrorTextMarker;
893                 this._codeMirror.setUniqueBookmark(codeMirrorTextMarker.find().from, swatchElement);
894
895                 swatchInnerElement.__colorTextMarker = codeMirrorTextMarker;
896                 swatchInnerElement.__color = color;
897             }.bind(this));
898         }
899
900         if (nonatomic)
901             update.call(this);
902         else
903             this._codeMirror.operation(update.bind(this));
904     }
905
906     _createBezierEditors(nonatomic, lineNumber)
907     {
908         function update()
909         {
910             var range = typeof lineNumber === "number" ? new WebInspector.TextRange(lineNumber, 0, lineNumber + 1, 0) : null;
911
912             // Look for cubic-bezier and timing functions and add cubic-bezier icons in front of them.
913             createCodeMirrorCubicBezierTextMarkers(this._codeMirror, range, function(marker, cubicBezier) {
914                 var bezierMarker = document.createElement("span");
915                 bezierMarker.title = WebInspector.UIString("Click to open a cubic-bezier editor");
916                 bezierMarker.className = WebInspector.CSSStyleDeclarationTextEditor.BezierEditorClassName;
917                 bezierMarker.addEventListener("click", this._cubicBezierMarkerClicked.bind(this));
918
919                 var codeMirrorTextMarker = marker.codeMirrorTextMarker;
920                 this._codeMirror.setUniqueBookmark(codeMirrorTextMarker.find().from, bezierMarker);
921
922                 bezierMarker.__textMarker = codeMirrorTextMarker;
923                 bezierMarker.__bezier = cubicBezier;
924             }.bind(this));
925         }
926
927         if (nonatomic)
928             update.call(this);
929         else
930             this._codeMirror.operation(update.bind(this));
931     }
932
933     _updateTextMarkerForPropertyIfNeeded(property)
934     {
935         var textMarker = property.__propertyTextMarker;
936         console.assert(textMarker);
937         if (!textMarker)
938             return;
939
940         var range = textMarker.find();
941         console.assert(range);
942         if (!range)
943             return;
944
945         this._createTextMarkerForPropertyIfNeeded(range.from, range.to, property);
946     }
947
948     _createTextMarkerForPropertyIfNeeded(from, to, property)
949     {
950         if (!this._codeMirror.getOption("readOnly")) {
951             // Create a new checkbox element and marker.
952
953             console.assert(property.enabled);
954
955             var checkboxElement = document.createElement("input");
956             checkboxElement.type = "checkbox";
957             checkboxElement.checked = true;
958             checkboxElement.addEventListener("change", this._propertyCheckboxChanged.bind(this));
959             checkboxElement.__cssProperty = property;
960
961             var checkboxMarker = this._codeMirror.setUniqueBookmark(from, checkboxElement);
962             checkboxMarker.__propertyCheckbox = true;
963         } else if (this._delegate.cssStyleDeclarationTextEditorShouldAddPropertyGoToArrows
964                 && !property.implicit && typeof this._delegate.cssStyleDeclarationTextEditorShowProperty === "function") {
965
966             let arrowElement = WebInspector.createGoToArrowButton();
967             arrowElement.title = WebInspector.UIString("Option-click to show source");
968
969             let delegate = this._delegate;
970             arrowElement.addEventListener("click", function(event) {
971                 delegate.cssStyleDeclarationTextEditorShowProperty(property, event.altKey);
972             });
973
974             this._codeMirror.setUniqueBookmark(to, arrowElement);
975         }
976
977         function duplicatePropertyExistsBelow(cssProperty)
978         {
979             var propertyFound = false;
980
981             for (var property of this._style.properties) {
982                 if (property === cssProperty)
983                     propertyFound = true;
984                 else if (property.name === cssProperty.name && propertyFound)
985                     return true;
986             }
987
988             return false;
989         }
990
991         var propertyNameIsValid = false;
992         if (WebInspector.CSSCompletions.cssNameCompletions)
993             propertyNameIsValid = WebInspector.CSSCompletions.cssNameCompletions.isValidPropertyName(property.name);
994
995         var classNames = ["css-style-declaration-property"];
996
997         if (property.overridden)
998             classNames.push("overridden");
999
1000         if (property.implicit)
1001             classNames.push("implicit");
1002
1003         if (this._style.inherited && !property.inherited)
1004             classNames.push("not-inherited");
1005
1006         if (!property.valid && property.hasOtherVendorNameOrKeyword())
1007             classNames.push("other-vendor");
1008         else if (!property.valid && (!propertyNameIsValid || duplicatePropertyExistsBelow.call(this, property)))
1009             classNames.push("invalid");
1010
1011         if (!property.enabled)
1012             classNames.push("disabled");
1013
1014         if (property.__filterResultClassName && !property.__filterResultNeedlePosition)
1015             classNames.push(property.__filterResultClassName);
1016
1017         var classNamesString = classNames.join(" ");
1018
1019         // If there is already a text marker and it's in the same document, then try to avoid recreating it.
1020         // FIXME: If there are multiple CSSStyleDeclarationTextEditors for the same style then this will cause
1021         // both editors to fight and always recreate their text markers. This isn't really common.
1022         if (property.__propertyTextMarker && property.__propertyTextMarker.doc.cm === this._codeMirror && property.__propertyTextMarker.find()) {
1023             // If the class name is the same then we don't need to make a new marker.
1024             if (property.__propertyTextMarker.className === classNamesString)
1025                 return;
1026
1027             property.__propertyTextMarker.clear();
1028         }
1029
1030         var propertyTextMarker = this._codeMirror.markText(from, to, {className: classNamesString});
1031
1032         propertyTextMarker.__cssProperty = property;
1033         property.__propertyTextMarker = propertyTextMarker;
1034
1035         property.addEventListener(WebInspector.CSSProperty.Event.OverriddenStatusChanged, this._propertyOverriddenStatusChanged, this);
1036
1037         this._removeCheckboxPlaceholder(from.line);
1038
1039         if (property.__filterResultClassName && property.__filterResultNeedlePosition) {
1040             for (var needlePosition of property.__filterResultNeedlePosition.start) {
1041                 var start = {line: from.line, ch: needlePosition};
1042                 var end = {line: to.line, ch: start.ch + property.__filterResultNeedlePosition.length};
1043
1044                 this._codeMirror.markText(start, end, {className: property.__filterResultClassName});
1045             }
1046         }
1047
1048         if (property.hasOtherVendorNameOrKeyword() || property.text.trim().endsWith(":"))
1049             return;
1050
1051         var propertyHasUnnecessaryPrefix = property.name.startsWith("-webkit-") && WebInspector.CSSCompletions.cssNameCompletions.isValidPropertyName(property.canonicalName);
1052
1053         function generateInvalidMarker(options)
1054         {
1055             var invalidMarker = document.createElement("button");
1056             invalidMarker.className = "invalid-warning-marker";
1057             invalidMarker.title = options.title;
1058
1059             if (typeof options.correction === "string") {
1060                 // Allow for blank strings
1061                 invalidMarker.classList.add("clickable");
1062                 invalidMarker.addEventListener("click", function() {
1063                     this._codeMirror.replaceRange(options.correction, from, to);
1064
1065                     if (options.autocomplete) {
1066                         this._codeMirror.setCursor(to);
1067                         this.focus();
1068                         this._completionController._completeAtCurrentPosition(true);
1069                     }
1070                 }.bind(this));
1071             }
1072
1073             this._codeMirror.setBookmark(options.position, invalidMarker);
1074         }
1075
1076         function instancesOfProperty(propertyName)
1077         {
1078             var count = 0;
1079
1080             for (var property of this._style.properties) {
1081                 if (property.name === propertyName)
1082                     ++count;
1083             }
1084
1085             return count;
1086         }
1087
1088         // Number of times this property name is listed in the rule.
1089         var instances = instancesOfProperty.call(this, property.name);
1090         var invalidMarkerInfo;
1091
1092         if (propertyHasUnnecessaryPrefix && !instancesOfProperty.call(this, property.canonicalName)) {
1093             // 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.
1094             generateInvalidMarker.call(this, {
1095                 position: from,
1096                 title: WebInspector.UIString("The 'webkit' prefix is not necessary.\nClick to insert a duplicate without the prefix."),
1097                 correction: property.text + "\n" + property.text.replace("-webkit-", ""),
1098                 autocomplete: false
1099             });
1100         } else if (instances > 1) {
1101             invalidMarkerInfo = {
1102                 position: from,
1103                 title: WebInspector.UIString("Duplicate property '%s'.\nClick to delete this property.").format(property.name),
1104                 correction: "",
1105                 autocomplete: false
1106             };
1107         }
1108
1109         if (property.valid) {
1110             if (invalidMarkerInfo)
1111                 generateInvalidMarker.call(this, invalidMarkerInfo);
1112
1113             return;
1114         }
1115
1116         if (propertyNameIsValid) {
1117             // The property's name is valid but its value is not (either it is not supported for this property or there is no value).
1118             var semicolon = /:\s*/.exec(property.text);
1119             var start = {line: from.line, ch: semicolon.index + semicolon[0].length};
1120             var end = {line: to.line, ch: start.ch + property.value.length};
1121
1122             this._codeMirror.markText(start, end, {className: "invalid"});
1123
1124             if (/^(?:\d+)$/.test(property.value)) {
1125                 invalidMarkerInfo = {
1126                     position: start,
1127                     title: WebInspector.UIString("The value '%s' needs units.\nClick to add 'px' to the value.").format(property.value),
1128                     correction: property.name + ": " + property.value + "px;",
1129                     autocomplete: false
1130                 };
1131             } else {
1132                 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.");
1133
1134                 invalidMarkerInfo = {
1135                     position: start,
1136                     title: valueReplacement,
1137                     correction: property.name + ": ",
1138                     autocomplete: true
1139                 };
1140             }
1141         } else if (!instancesOfProperty.call(this, "-webkit-" + property.name) && WebInspector.CSSCompletions.cssNameCompletions.propertyRequiresWebkitPrefix(property.name)) {
1142             // The property is valid and exists in the rule while its prefixed version does not.
1143             invalidMarkerInfo = {
1144                 position: from,
1145                 title: WebInspector.UIString("The 'webkit' prefix is needed for this property.\nClick to insert a duplicate with the prefix."),
1146                 correction: "-webkit-" + property.text + "\n" + property.text,
1147                 autocomplete: false
1148             };
1149         } else if (!propertyHasUnnecessaryPrefix && !WebInspector.CSSCompletions.cssNameCompletions.isValidPropertyName("-webkit-" + property.name)) {
1150             // The property either has no prefix and is invalid with a prefix or is invalid without a prefix.
1151             var closestPropertyName = WebInspector.CSSCompletions.cssNameCompletions.getClosestPropertyName(property.name);
1152
1153             if (closestPropertyName) {
1154                 // The property name has less than 3 other properties that have the same Levenshtein distance.
1155                 invalidMarkerInfo = {
1156                     position: from,
1157                     title: WebInspector.UIString("Did you mean '%s'?\nClick to replace.").format(closestPropertyName),
1158                     correction: property.text.replace(property.name, closestPropertyName),
1159                     autocomplete: true
1160                 };
1161             } else if (property.name.startsWith("-webkit-") && (closestPropertyName = WebInspector.CSSCompletions.cssNameCompletions.getClosestPropertyName(property.canonicalName))) {
1162                 // The unprefixed property name has less than 3 other properties that have the same Levenshtein distance.
1163                 invalidMarkerInfo = {
1164                     position: from,
1165                     title: WebInspector.UIString("Did you mean '%s'?\nClick to replace.").format("-webkit-" + closestPropertyName),
1166                     correction: property.text.replace(property.canonicalName, closestPropertyName),
1167                     autocomplete: true
1168                 };
1169             } else {
1170                 // The property name is so vague or nonsensical that there are more than 3 other properties that have the same Levenshtein value.
1171                 invalidMarkerInfo = {
1172                     position: from,
1173                     title: WebInspector.UIString("The property '%s' is not supported.").format(property.name),
1174                     correction: false,
1175                     autocomplete: false
1176                 };
1177             }
1178         }
1179
1180         if (!invalidMarkerInfo)
1181             return;
1182
1183         generateInvalidMarker.call(this, invalidMarkerInfo);
1184     }
1185
1186     _clearTextMarkers(nonatomic, all)
1187     {
1188         function clear()
1189         {
1190             var markers = this._codeMirror.getAllMarks();
1191             for (var i = 0; i < markers.length; ++i) {
1192                 var textMarker = markers[i];
1193
1194                 if (!all && textMarker.__checkboxPlaceholder) {
1195                     var position = textMarker.find();
1196
1197                     // Only keep checkbox placeholders if they are in the first column.
1198                     if (position && !position.ch)
1199                         continue;
1200                 }
1201
1202                 if (textMarker.__cssProperty) {
1203                     textMarker.__cssProperty.removeEventListener(null, null, this);
1204
1205                     delete textMarker.__cssProperty.__propertyTextMarker;
1206                     delete textMarker.__cssProperty;
1207                 }
1208
1209                 textMarker.clear();
1210             }
1211         }
1212
1213         if (nonatomic)
1214             clear.call(this);
1215         else
1216             this._codeMirror.operation(clear.bind(this));
1217     }
1218
1219     _iterateOverProperties(onlyVisibleProperties, callback)
1220     {
1221         var properties = onlyVisibleProperties ? this._style.visibleProperties : this._style.properties;
1222
1223         if (this._filterResultPropertyNames) {
1224             properties = properties.filter(function(property) {
1225                 return (!property.implicit || this._showsImplicitProperties) && property.name in this._filterResultPropertyNames;
1226             }, this);
1227
1228             if (this._sortProperties)
1229                 properties.sort(function(a, b) { return a.name.localeCompare(b.name); });
1230         } else if (!onlyVisibleProperties) {
1231             // Filter based on options only when all properties are used.
1232             properties = properties.filter(function(property) {
1233                 return !property.implicit || this._showsImplicitProperties || property.canonicalName in this._alwaysShowPropertyNames;
1234             }, this);
1235
1236             if (this._sortProperties)
1237                 properties.sort(function(a, b) { return a.name.localeCompare(b.name); });
1238         }
1239
1240         for (var i = 0; i < properties.length; ++i) {
1241             if (callback.call(this, properties[i], i === properties.length - 1))
1242                 break;
1243         }
1244     }
1245
1246     _propertyCheckboxChanged(event)
1247     {
1248         var property = event.target.__cssProperty;
1249         console.assert(property);
1250         if (!property)
1251             return;
1252
1253         this._commentProperty(property);
1254     }
1255
1256     _commentProperty(property)
1257     {
1258         var textMarker = property.__propertyTextMarker;
1259         console.assert(textMarker);
1260         if (!textMarker)
1261             return;
1262
1263         // Check if the property has been removed already, like from double-clicking
1264         // the checkbox and calling this event listener multiple times.
1265         var range = textMarker.find();
1266         if (!range)
1267             return;
1268
1269         property._commentRange = range;
1270         property._commentRange.to.ch += 6; // Number of characters added by comments.
1271
1272         var text = this._codeMirror.getRange(range.from, range.to);
1273
1274         function update()
1275         {
1276             // Replace the text with a commented version.
1277             this._codeMirror.replaceRange("/* " + text + " */", range.from, range.to);
1278
1279             // Update the line for any color swatches or cubic-beziers that got removed.
1280             this._createColorSwatches(true, range.from.line);
1281             this._createBezierEditors(true, range.from.line);
1282         }
1283
1284         this._codeMirror.operation(update.bind(this));
1285     }
1286
1287     _propertyCommentCheckboxChanged(event)
1288     {
1289         var commentTextMarker = event.target.__commentTextMarker;
1290         console.assert(commentTextMarker);
1291         if (!commentTextMarker)
1292             return;
1293
1294         // Check if the comment has been removed already, like from double-clicking
1295         // the checkbox and calling event listener multiple times.
1296         var range = commentTextMarker.find();
1297         if (!range)
1298             return;
1299
1300         this._uncommentRange(range);
1301     }
1302
1303     _uncommentRange(range)
1304     {
1305         var text = this._codeMirror.getRange(range.from, range.to);
1306
1307         // Remove the comment prefix and suffix.
1308         text = text.replace(/^\/\*\s*/, "").replace(/\s*\*\/$/, "");
1309
1310         // Add a semicolon if there isn't one already.
1311         if (text.length && text.charAt(text.length - 1) !== ";")
1312             text += ";";
1313
1314         function update()
1315         {
1316             this._codeMirror.addLineClass(range.from.line, "wrap", WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName);
1317             this._codeMirror.replaceRange(text, range.from, range.to);
1318
1319             // Update the line for any color swatches or cubic-beziers that got removed.
1320             this._createColorSwatches(true, range.from.line);
1321             this._createBezierEditors(true, range.from.line);
1322         }
1323
1324         this._codeMirror.operation(update.bind(this));
1325     }
1326
1327     _colorSwatchClicked(event)
1328     {
1329         if (this._colorPickerPopover)
1330             return;
1331
1332         var swatch = event.target;
1333
1334         var color = swatch.__color;
1335         console.assert(color);
1336         if (!color)
1337             return;
1338
1339         var colorTextMarker = swatch.__colorTextMarker;
1340         console.assert(colorTextMarker);
1341         if (!colorTextMarker)
1342             return;
1343
1344         var range = colorTextMarker.find();
1345         console.assert(range);
1346         if (!range)
1347             return;
1348
1349         function updateCodeMirror(newColorText)
1350         {
1351             function update()
1352             {
1353                 // The original text marker might have been cleared by a style update,
1354                 // in this case we need to find the new color text marker so we know
1355                 // the right range for the new style color text.
1356                 if (!colorTextMarker || !colorTextMarker.find()) {
1357                     colorTextMarker = null;
1358
1359                     var marks = this._codeMirror.findMarksAt(range.from);
1360                     if (!marks.length)
1361                         return;
1362
1363                     for (var i = 0; i < marks.length; ++i) {
1364                         var mark = marks[i];
1365                         if (WebInspector.TextMarker.textMarkerForCodeMirrorTextMarker(mark).type !== WebInspector.TextMarker.Type.Color)
1366                             continue;
1367                         colorTextMarker = mark;
1368                         break;
1369                     }
1370                 }
1371
1372                 if (!colorTextMarker)
1373                     return;
1374
1375                 // Sometimes we still might find a stale text marker with findMarksAt.
1376                 var newRange = colorTextMarker.find();
1377                 if (!newRange)
1378                     return;
1379
1380                 range = newRange;
1381
1382                 colorTextMarker.clear();
1383
1384                 this._codeMirror.replaceRange(newColorText, range.from, range.to);
1385
1386                 // The color's text format could have changed, so we need to update the "range"
1387                 // variable to anticipate a different "range.to" property.
1388                 range.to.ch = range.from.ch + newColorText.length;
1389
1390                 colorTextMarker = this._codeMirror.markText(range.from, range.to);
1391
1392                 swatch.__colorTextMarker = colorTextMarker;
1393             }
1394
1395             this._codeMirror.operation(update.bind(this));
1396         }
1397
1398         if (event.shiftKey || this._codeMirror.getOption("readOnly")) {
1399             var nextFormat = color.nextFormat();
1400             console.assert(nextFormat);
1401             if (!nextFormat)
1402                 return;
1403             color.format = nextFormat;
1404
1405             var newColorText = color.toString();
1406
1407             // Ignore the change so we don't commit the format change. However, any future user
1408             // edits will commit the color format.
1409             this._ignoreCodeMirrorContentDidChangeEvent = true;
1410             updateCodeMirror.call(this, newColorText);
1411             delete this._ignoreCodeMirrorContentDidChangeEvent;
1412         } else {
1413             this._colorPickerPopover = new WebInspector.Popover(this);
1414
1415             var colorPicker = new WebInspector.ColorPicker;
1416
1417             colorPicker.addEventListener(WebInspector.ColorPicker.Event.ColorChanged, function(event) {
1418                 updateCodeMirror.call(this, event.data.color.toString());
1419             }.bind(this));
1420
1421             var bounds = WebInspector.Rect.rectFromClientRect(swatch.getBoundingClientRect());
1422
1423             this._colorPickerPopover.content = colorPicker.element;
1424             this._colorPickerPopover.present(bounds.pad(2), [WebInspector.RectEdge.MIN_X]);
1425
1426             colorPicker.color = color;
1427         }
1428     }
1429
1430     _cubicBezierMarkerClicked(event)
1431     {
1432         if (this._cubicBezierEditorPopover)
1433             return;
1434
1435         var bezierMarker = event.target;
1436
1437         var bezier = bezierMarker.__bezier;
1438         console.assert(bezier);
1439         if (!bezier)
1440             return;
1441
1442         var bezierTextMarker = bezierMarker.__textMarker;
1443         console.assert(bezierTextMarker);
1444         if (!bezierTextMarker)
1445             return;
1446
1447         var range = bezierTextMarker.find();
1448         console.assert(range);
1449         if (!range)
1450             return;
1451
1452         function updateCodeMirror(newCubicBezierText)
1453         {
1454             function update()
1455             {
1456                 // The original text marker might have been cleared by a style update,
1457                 // in this case we need to find the new bezier text marker so we know
1458                 // the right range for the new style bezier text.
1459                 if (!bezierTextMarker || !bezierTextMarker.find()) {
1460                     bezierTextMarker = null;
1461
1462                     var marks = this._codeMirror.findMarksAt(range.from);
1463                     if (!marks.length)
1464                         return;
1465
1466                     for (var i = 0; i < marks.length; ++i) {
1467                         var mark = marks[i];
1468                         if (WebInspector.TextMarker.textMarkerForCodeMirrorTextMarker(mark).type !== WebInspector.TextMarker.Type.CubicBezier)
1469                             continue;
1470                         bezierTextMarker = mark;
1471                         break;
1472                     }
1473                 }
1474
1475                 if (!bezierTextMarker)
1476                     return;
1477
1478                 // Sometimes we still might find a stale text marker with findMarksAt.
1479                 var newRange = bezierTextMarker.find();
1480                 if (!newRange)
1481                     return;
1482
1483                 range = newRange;
1484
1485                 bezierTextMarker.clear();
1486
1487                 this._codeMirror.replaceRange(newCubicBezierText, range.from, range.to);
1488
1489                 // The bezier's text format could have changed, so we need to update the "range"
1490                 // variable to anticipate a different "range.to" property.
1491                 range.to.ch = range.from.ch + newCubicBezierText.length;
1492
1493                 bezierTextMarker = this._codeMirror.markText(range.from, range.to);
1494
1495                 bezierMarker.__textMarker = bezierTextMarker;
1496             }
1497
1498             this._codeMirror.operation(update.bind(this));
1499         }
1500
1501         this._cubicBezierEditorPopover = new WebInspector.Popover(this);
1502
1503         var bezierEditor = new WebInspector.BezierEditor;
1504
1505         bezierEditor.addEventListener(WebInspector.BezierEditor.Event.BezierChanged, function(event) {
1506             updateCodeMirror.call(this, event.data.bezier.toString());
1507         }.bind(this));
1508
1509         var bounds = WebInspector.Rect.rectFromClientRect(bezierMarker.getBoundingClientRect());
1510
1511         this._cubicBezierEditorPopover.content = bezierEditor.element;
1512         this._cubicBezierEditorPopover.present(bounds.pad(2), [WebInspector.RectEdge.MIN_X]);
1513
1514         bezierEditor.bezier = bezier;
1515     }
1516
1517     _propertyOverriddenStatusChanged(event)
1518     {
1519         this._updateTextMarkerForPropertyIfNeeded(event.target);
1520     }
1521
1522     _propertiesChanged(event)
1523     {
1524         // Don't try to update the document while completions are showing. Doing so will clear
1525         // the completion hint and prevent further interaction with the completion.
1526         if (this._completionController.isShowingCompletions())
1527             return;
1528
1529         // Reset the content if the text is different and we are not focused.
1530         if (!this.focused && (!this._style.text || this._style.text !== this._formattedContent())) {
1531             this._resetContent();
1532             return;
1533         }
1534
1535         this._removeEditingLineClassesSoon();
1536
1537         this._updateTextMarkers();
1538     }
1539
1540     _markLinesWithCheckboxPlaceholder()
1541     {
1542         if (this._codeMirror.getOption("readOnly"))
1543             return;
1544
1545         var linesWithPropertyCheckboxes = {};
1546         var linesWithCheckboxPlaceholders = {};
1547
1548         var markers = this._codeMirror.getAllMarks();
1549         for (var i = 0; i < markers.length; ++i) {
1550             var textMarker = markers[i];
1551             if (textMarker.__propertyCheckbox) {
1552                 var position = textMarker.find();
1553                 if (position)
1554                     linesWithPropertyCheckboxes[position.line] = true;
1555             } else if (textMarker.__checkboxPlaceholder) {
1556                 var position = textMarker.find();
1557                 if (position)
1558                     linesWithCheckboxPlaceholders[position.line] = true;
1559             }
1560         }
1561
1562         var lineCount = this._codeMirror.lineCount();
1563
1564         for (var i = 0; i < lineCount; ++i) {
1565             if (i in linesWithPropertyCheckboxes || i in linesWithCheckboxPlaceholders)
1566                 continue;
1567
1568             var position = {line: i, ch: 0};
1569
1570             var placeholderElement = document.createElement("div");
1571             placeholderElement.className = WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName;
1572
1573             var placeholderMark = this._codeMirror.setUniqueBookmark(position, placeholderElement);
1574             placeholderMark.__checkboxPlaceholder = true;
1575         }
1576     }
1577
1578     _removeCheckboxPlaceholder(lineNumber)
1579     {
1580         var marks = this._codeMirror.findMarksAt({line: lineNumber, ch: 0});
1581         for (var i = 0; i < marks.length; ++i) {
1582             var mark = marks[i];
1583             if (!mark.__checkboxPlaceholder)
1584                 continue;
1585
1586             mark.clear();
1587             return;
1588         }
1589     }
1590
1591     _formattedContentFromEditor()
1592     {
1593         var mapping = {original: [0], formatted: [0]};
1594         // FIXME: <rdar://problem/10593948> Provide a way to change the tab width in the Web Inspector
1595         var indentString = "    ";
1596         var builder = new WebInspector.FormatterContentBuilder(mapping, [], [], 0, 0, indentString);
1597         var formatter = new WebInspector.Formatter(this._codeMirror, builder);
1598         var start = {line: 0, ch: 0};
1599         var end = {line: this._codeMirror.lineCount() - 1};
1600         formatter.format(start, end);
1601
1602         return builder.formattedContent.trim();
1603     }
1604
1605     _resetContent()
1606     {
1607         if (this._commitChangesTimeout) {
1608             clearTimeout(this._commitChangesTimeout);
1609             this._commitChangesTimeout = null;
1610         }
1611
1612         this._removeEditingLineClasses();
1613
1614         // Only allow editing if we have a style, it is editable and we have text range in the stylesheet.
1615         const readOnly = !this._style || !this._style.editable || !this._style.styleSheetTextRange;
1616         this._codeMirror.setOption("readOnly", readOnly);
1617
1618         if (readOnly) {
1619             this.element.classList.add(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName);
1620             this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties"));
1621         } else {
1622             this.element.classList.remove(WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName);
1623             this._codeMirror.setOption("placeholder", WebInspector.UIString("No Properties \u2014 Click to Edit"));
1624         }
1625
1626         if (!this._style) {
1627             this._ignoreCodeMirrorContentDidChangeEvent = true;
1628
1629             this._clearTextMarkers(false, true);
1630             this._codeMirror.setValue("");
1631             this._codeMirror.clearHistory();
1632             this._codeMirror.markClean();
1633
1634             this._ignoreCodeMirrorContentDidChangeEvent = false;
1635             return;
1636         }
1637
1638         function update()
1639         {
1640             // Remember the cursor position/selection.
1641             let isEditorReadOnly = this._codeMirror.getOption("readOnly");
1642             let styleText = this._style.text;
1643             let trimmedStyleText = styleText.trim();
1644
1645             // We only need to format non-empty styles, but prepare checkbox placeholders
1646             // in any case because that will indent the cursor when the User starts typing.
1647             if (!trimmedStyleText && !isEditorReadOnly) {
1648                 this._markLinesWithCheckboxPlaceholder();
1649                 return;
1650             }
1651
1652             // Generate formatted content for readonly editors by iterating properties.
1653             if (isEditorReadOnly) {
1654                 this._codeMirror.setValue("");
1655                 let lineNumber = 0;
1656                 this._iterateOverProperties(false, function(property) {
1657                     let from = {line: lineNumber, ch: 0};
1658                     let to = {line: lineNumber};
1659                     // Readonly properties are pretty printed by `synthesizedText` and not the Formatter.
1660                     this._codeMirror.replaceRange((lineNumber ? "\n" : "") + property.synthesizedText, from);
1661                     this._createTextMarkerForPropertyIfNeeded(from, to, property);
1662                     lineNumber++;
1663                 });
1664                 return;
1665             }
1666
1667             let selectionAnchor = this._codeMirror.getCursor("anchor");
1668             let selectionHead = this._codeMirror.getCursor("head");
1669             let whitespaceRegex = /\s+/g;
1670
1671             // FIXME: <rdar://problem/10593948> Provide a way to change the tab width in the Web Inspector
1672             this._linePrefixWhitespace = "    ";
1673             let styleTextPrefixWhitespace = styleText.match(/^\s*/);
1674
1675             // If there is a match and the style text contains a newline, attempt to pull out the prefix whitespace
1676             // in front of the first line of CSS to use for every line.  If  there is no newline, we want to avoid
1677             // adding multiple spaces to a single line CSS rule and instead format it on multiple lines.
1678             if (styleTextPrefixWhitespace && trimmedStyleText.includes("\n")) {
1679                 let linePrefixWhitespaceMatch = styleTextPrefixWhitespace[0].match(/[^\S\n]+$/);
1680                 if (linePrefixWhitespaceMatch)
1681                     this._linePrefixWhitespace = linePrefixWhitespaceMatch[0];
1682             }
1683
1684             // Set non-optimized, valid and invalid styles in preparation for the Formatter.
1685             this._codeMirror.setValue(trimmedStyleText);
1686
1687             // Now the Formatter pretty prints the styles.
1688             this._codeMirror.setValue(this._formattedContentFromEditor());
1689
1690             // We need to workaround the fact that...
1691             // 1) `this._style.properties` only holds valid CSSProperty instances but not
1692             // comments and invalid properties like `color;`.
1693             // 2) `_createTextMarkerForPropertyIfNeeded` relies on CSSProperty instances.
1694             let cssPropertiesMap = new Map();
1695             this._iterateOverProperties(false, function(cssProperty) {
1696                 cssProperty.__refreshedAfterBlur = false;
1697
1698                 let propertyTextSansWhitespace = cssProperty.text.replace(whitespaceRegex, "");
1699                 let existingProperties = cssPropertiesMap.get(propertyTextSansWhitespace) || [];
1700                 existingProperties.push(cssProperty);
1701
1702                 cssPropertiesMap.set(propertyTextSansWhitespace, existingProperties);
1703             });
1704
1705             // Go through the Editor line by line and create TextMarker when a
1706             // CSSProperty instance for that property exists. If not, then don't create a TextMarker.
1707             this._codeMirror.eachLine(function(lineHandler) {
1708                 let lineNumber = lineHandler.lineNo();
1709                 let lineContentSansWhitespace = lineHandler.text.replace(whitespaceRegex, "");
1710                 let properties = cssPropertiesMap.get(lineContentSansWhitespace);
1711                 if (!properties) {
1712                     this._createCommentedCheckboxMarker(lineHandler);
1713                     return;
1714                 }
1715
1716                 for (let property of properties) {
1717                     if (property.__refreshedAfterBlur)
1718                         continue;
1719
1720                     let from = {line: lineNumber, ch: 0};
1721                     let to = {line: lineNumber};
1722                     this._createTextMarkerForPropertyIfNeeded(from, to, property);
1723                     property.__refreshedAfterBlur = true;
1724                     break;
1725                 }
1726             }.bind(this));
1727
1728             // Look for colors and make swatches.
1729             this._createColorSwatches(true);
1730             this._createBezierEditors(true);
1731
1732             // Restore the cursor position/selection.
1733             this._codeMirror.setSelection(selectionAnchor, selectionHead);
1734
1735             // Reset undo history since undo past the reset is wrong when the content was empty before
1736             // or the content was representing a previous style object.
1737             this._codeMirror.clearHistory();
1738
1739             // Mark the editor as clean (unedited state).
1740             this._codeMirror.markClean();
1741
1742             this._markLinesWithCheckboxPlaceholder();
1743         }
1744
1745         // This needs to be done first and as a separate operation to avoid an exception in CodeMirror.
1746         this._clearTextMarkers(false, true);
1747
1748         this._ignoreCodeMirrorContentDidChangeEvent = true;
1749         this._codeMirror.operation(update.bind(this));
1750         this._ignoreCodeMirrorContentDidChangeEvent = false;
1751     }
1752
1753     _updateJumpToSymbolTrackingMode()
1754     {
1755         var oldJumpToSymbolTrackingModeEnabled = this._jumpToSymbolTrackingModeEnabled;
1756
1757         if (!this._style || !this._style.ownerRule || !this._style.ownerRule.sourceCodeLocation)
1758             this._jumpToSymbolTrackingModeEnabled = false;
1759         else
1760             this._jumpToSymbolTrackingModeEnabled = WebInspector.modifierKeys.altKey && !WebInspector.modifierKeys.metaKey && !WebInspector.modifierKeys.shiftKey;
1761
1762         if (oldJumpToSymbolTrackingModeEnabled !== this._jumpToSymbolTrackingModeEnabled) {
1763             if (this._jumpToSymbolTrackingModeEnabled) {
1764                 this._tokenTrackingController.highlightLastHoveredRange();
1765                 this._tokenTrackingController.enabled = !this._codeMirror.getOption("readOnly");
1766             } else {
1767                 this._tokenTrackingController.removeHighlightedRange();
1768                 this._tokenTrackingController.enabled = false;
1769             }
1770         }
1771     }
1772
1773     tokenTrackingControllerHighlightedRangeWasClicked(tokenTrackingController)
1774     {
1775         console.assert(this._style.ownerRule.sourceCodeLocation);
1776         if (!this._style.ownerRule.sourceCodeLocation)
1777             return;
1778
1779         // Special case command clicking url(...) links.
1780         var token = this._tokenTrackingController.candidate.hoveredToken;
1781         if (/\blink\b/.test(token.type)) {
1782             var url = token.string;
1783             var baseURL = this._style.ownerRule.sourceCodeLocation.sourceCode.url;
1784             WebInspector.openURL(absoluteURL(url, baseURL));
1785             return;
1786         }
1787
1788         // Jump to the rule if we can't find a property.
1789         // Find a better source code location from the property that was clicked.
1790         var sourceCodeLocation = this._style.ownerRule.sourceCodeLocation;
1791         var marks = this._codeMirror.findMarksAt(this._tokenTrackingController.candidate.hoveredTokenRange.start);
1792         for (var i = 0; i < marks.length; ++i) {
1793             var mark = marks[i];
1794             var property = mark.__cssProperty;
1795             if (property) {
1796                 var sourceCode = sourceCodeLocation.sourceCode;
1797                 var styleSheetTextRange = property.styleSheetTextRange;
1798                 sourceCodeLocation = sourceCode.createSourceCodeLocation(styleSheetTextRange.startLine, styleSheetTextRange.startColumn);
1799             }
1800         }
1801
1802         WebInspector.showSourceCodeLocation(sourceCodeLocation);
1803     }
1804
1805     tokenTrackingControllerNewHighlightCandidate(tokenTrackingController, candidate)
1806     {
1807         this._tokenTrackingController.highlightRange(candidate.hoveredTokenRange);
1808     }
1809 };
1810
1811 WebInspector.CSSStyleDeclarationTextEditor.Event = {
1812     ContentChanged: "css-style-declaration-text-editor-content-changed",
1813     Blurred: "css-style-declaration-text-editor-blurred"
1814 };
1815
1816 WebInspector.CSSStyleDeclarationTextEditor.StyleClassName = "css-style-text-editor";
1817 WebInspector.CSSStyleDeclarationTextEditor.ReadOnlyStyleClassName = "read-only";
1818 WebInspector.CSSStyleDeclarationTextEditor.ColorSwatchElementStyleClassName = "color-swatch";
1819 WebInspector.CSSStyleDeclarationTextEditor.BezierEditorClassName = "cubic-bezier-marker";
1820 WebInspector.CSSStyleDeclarationTextEditor.CheckboxPlaceholderElementStyleClassName = "checkbox-placeholder";
1821 WebInspector.CSSStyleDeclarationTextEditor.EditingLineStyleClassName = "editing-line";
1822 WebInspector.CSSStyleDeclarationTextEditor.CommitCoalesceDelay = 250;
1823 WebInspector.CSSStyleDeclarationTextEditor.RemoveEditingLineClassesDelay = 2000;