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