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