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