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