Web Inspector: Styles sidebar editing with incomplete property looks poor in UI
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 8 May 2015 18:37:49 +0000 (18:37 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 8 May 2015 18:37:49 +0000 (18:37 +0000)
https://bugs.webkit.org/show_bug.cgi?id=141692

Patch by Tobias Reiss <tobi+webkit@basecode.de> on 2015-05-08
Reviewed by Timothy Hatcher.

Add "css-rule" Formatter that breaks CSS declarations into multiple lines,
keeps comments and invalid styles and adds whitespace.

* Tools/PrettyPrinting/css-rule-tests/*.css: Added.
Add test cases.

* Tools/PrettyPrinting/index.html:
Enable Test setup to be able to run "css-rule" Formatter tests.

* UserInterface/Controllers/Formatter.js:
(Formatter.prototype._handleToken):
* UserInterface/Controllers/FormatterContentBuilder.js:
(FormatterContentBuilder.prototype.removeLastNewline):
(FormatterContentBuilder.prototype.removeLastWhitespace):
(FormatterContentBuilder.prototype._popFormattedContent):
(FormatterContentBuilder.prototype._popNewLine): Deleted.
* UserInterface/Views/CSSStyleDeclarationTextEditor.js:
(WebInspector.CSSStyleDeclarationTextEditor.prototype._formattedContentFromEditor):
(WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent.update.set this):
(WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent.update.get this):
(WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent.update):
(WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent):
(WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent.update.countNewLineCharacters): Deleted.
(WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent.update.else): Deleted.
* UserInterface/Views/CodeMirrorFormatters.js:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@184000 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Tools/PrettyPrinting/index.html
Source/WebInspectorUI/UserInterface/Controllers/Formatter.js
Source/WebInspectorUI/UserInterface/Controllers/FormatterContentBuilder.js
Source/WebInspectorUI/UserInterface/Views/CSSStyleDeclarationTextEditor.js
Source/WebInspectorUI/UserInterface/Views/CodeMirrorFormatters.js

index c76a6b4f6218ad3b5081342d566869b1c3312141..ba388f9c192230a310cfca6ac282a94d29718a60 100644 (file)
@@ -1,3 +1,36 @@
+2015-05-08  Tobias Reiss  <tobi+webkit@basecode.de>
+
+        Web Inspector: Styles sidebar editing with incomplete property looks poor in UI
+        https://bugs.webkit.org/show_bug.cgi?id=141692
+
+        Reviewed by Timothy Hatcher.
+
+        Add "css-rule" Formatter that breaks CSS declarations into multiple lines,
+        keeps comments and invalid styles and adds whitespace.
+
+        * Tools/PrettyPrinting/css-rule-tests/*.css: Added.
+        Add test cases.
+
+        * Tools/PrettyPrinting/index.html:
+        Enable Test setup to be able to run "css-rule" Formatter tests.
+
+        * UserInterface/Controllers/Formatter.js:
+        (Formatter.prototype._handleToken):
+        * UserInterface/Controllers/FormatterContentBuilder.js:
+        (FormatterContentBuilder.prototype.removeLastNewline):
+        (FormatterContentBuilder.prototype.removeLastWhitespace):
+        (FormatterContentBuilder.prototype._popFormattedContent):
+        (FormatterContentBuilder.prototype._popNewLine): Deleted.
+        * UserInterface/Views/CSSStyleDeclarationTextEditor.js:
+        (WebInspector.CSSStyleDeclarationTextEditor.prototype._formattedContentFromEditor):
+        (WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent.update.set this):
+        (WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent.update.get this):
+        (WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent.update):
+        (WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent):
+        (WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent.update.countNewLineCharacters): Deleted.
+        (WebInspector.CSSStyleDeclarationTextEditor.prototype._resetContent.update.else): Deleted.
+        * UserInterface/Views/CodeMirrorFormatters.js:
+
 2015-05-07  Joseph Pecoraro  <pecoraro@apple.com>
 
         Web Inspector: Expanding Object with only __proto__ looks poor should have a label
 2015-05-07  Joseph Pecoraro  <pecoraro@apple.com>
 
         Web Inspector: Expanding Object with only __proto__ looks poor should have a label
index 273ac1530f3bb505282b582b4605af366d1c6cc4..e3bfc93274e799762d245391beaa3530f3458be7 100644 (file)
@@ -8,6 +8,8 @@
     <script src="../../UserInterface/External/CodeMirror/codemirror.js"></script>
     <script src="../../UserInterface/External/CodeMirror/javascript.js"></script>
     <script src="../../UserInterface/External/CodeMirror/css.js"></script>
     <script src="../../UserInterface/External/CodeMirror/codemirror.js"></script>
     <script src="../../UserInterface/External/CodeMirror/javascript.js"></script>
     <script src="../../UserInterface/External/CodeMirror/css.js"></script>
+    <script src="../../UserInterface/Base/WebInspector.js"></script>
+    <script src="../../UserInterface/Views/CodeMirrorAdditions.js"></script>
     <script src="../../UserInterface/Controllers/Formatter.js"></script>
     <script src="FormatterDebug.js"></script>
     <script src="../../UserInterface/Controllers/FormatterContentBuilder.js"></script>
     <script src="../../UserInterface/Controllers/Formatter.js"></script>
     <script src="FormatterDebug.js"></script>
     <script src="../../UserInterface/Controllers/FormatterContentBuilder.js"></script>
@@ -21,6 +23,7 @@
     <select id="mode">
         <option selected value="text/javascript">JavaScript</option>
         <option value="text/css">CSS</option>
     <select id="mode">
         <option selected value="text/javascript">JavaScript</option>
         <option value="text/css">CSS</option>
+        <option value="css-rule">CSS-Rule</option>
     </select>
     <button id="populate">Populate</button>
     <button id="run-tests">Run Tests</button>
     </select>
     <button id="populate">Populate</button>
     <button id="run-tests">Run Tests</button>
 
         if (modePicker.value === "text/javascript")
             runJavaScriptTests(completedCallback);
 
         if (modePicker.value === "text/javascript")
             runJavaScriptTests(completedCallback);
+        else if (modePicker.value === "css-rule")
+            runCssRuleTests(completedCallback);
         else
             runCSSTests(completedCallback);
     }
         else
             runCSSTests(completedCallback);
     }
             "js-tests/switch-case-default.js",
         ]);
     }
             "js-tests/switch-case-default.js",
         ]);
     }
+    function runCssRuleTests(callback) {
+        _runTests(callback, [
+            "css-rule-tests/invalid-property-is-not-removed.css",
+            "css-rule-tests/remove-whitespace-before-colon.css",
+            "css-rule-tests/remove-whitespace-before-semicolon.css",
+            "css-rule-tests/remove-whitespace-before-property.css",
+            "css-rule-tests/remove-whitespace-before-prefixed-property.css",
+            "css-rule-tests/remove-whitespace-before-invalid-property.css",
+            "css-rule-tests/remove-whitespace-before-comment.css",
+            "css-rule-tests/split-comment-followed-by-property.css",
+            "css-rule-tests/split-comment-followed-by-prefixed-property.css",
+            "css-rule-tests/split-comment-followed-by-invalid-property.css",
+            "css-rule-tests/split-comment-followed-by-comment.css",
+            "css-rule-tests/split-property-followed-by-property.css",
+            "css-rule-tests/split-property-followed-by-prefixed-property.css",
+            "css-rule-tests/split-property-followed-by-invalid-property.css",
+            "css-rule-tests/split-property-followed-by-comment.css",
+            "css-rule-tests/split-invalid-property-followed-by-property.css",
+            "css-rule-tests/split-invalid-property-followed-by-prefixed-property.css",
+            "css-rule-tests/split-invalid-property-followed-by-invalid-property.css",
+            "css-rule-tests/split-invalid-property-followed-by-comment.css",
+            "css-rule-tests/split-property-without-semicolon-followed-by-comment-and-property.css",
+            "css-rule-tests/add-whitespace-after-colon.css",
+            "css-rule-tests/add-whitespace-after-comma.css",
+            "css-rule-tests/do-not-append-semicolon.css",
+            "css-rule-tests/keep-prefixed-value.css",
+        ]);
+    }
     function runCSSTests(callback) {
         _runTests(callback, [
             "css-tests/basic.css",
     function runCSSTests(callback) {
         _runTests(callback, [
             "css-tests/basic.css",
index bfa998eff82db7345f3cdba8307250e10c8e57ab..c09c5565699213629aa432bbae6ea9c8694c5e2f 100644 (file)
@@ -89,7 +89,7 @@ class Formatter
         if (startOfNewLine)
             this._builder.appendNewline();
 
         if (startOfNewLine)
             this._builder.appendNewline();
 
-        // Whitespace. Collapse to a single space.
+        // Whitespace. Remove all spaces or collapse to a single space.
         if (isWhiteSpace) {
             this._builder.appendSpace();
             return;
         if (isWhiteSpace) {
             this._builder.appendSpace();
             return;
@@ -101,6 +101,10 @@ class Formatter
         if (mode.modifyStateForTokenPre)
             mode.modifyStateForTokenPre(this._lastToken, this._lastContent, token, state, content, isComment);
 
         if (mode.modifyStateForTokenPre)
             mode.modifyStateForTokenPre(this._lastToken, this._lastContent, token, state, content, isComment);
 
+        // Should we remove the last whitespace?
+        if (this._builder.lastTokenWasWhitespace && mode.removeLastWhitespace(this._lastToken, this._lastContent, token, state, content, isComment))
+            this._builder.removeLastWhitespace();
+
         // Should we remove the last newline?
         if (this._builder.lastTokenWasNewline && mode.removeLastNewline(this._lastToken, this._lastContent, token, state, content, isComment, firstTokenOnLine))
             this._builder.removeLastNewline();
         // Should we remove the last newline?
         if (this._builder.lastTokenWasNewline && mode.removeLastNewline(this._lastToken, this._lastContent, token, state, content, isComment, firstTokenOnLine))
             this._builder.removeLastNewline();
index 8291c34a4606f0a802da064a83ec3ba131488c6b..4d741eeda1fc6d17440428f244556a1d92995382 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2013 Apple Inc. All rights reserved.
 /*
  * Copyright (C) 2013 Apple Inc. All rights reserved.
+ * Copyright (C) 2015 Tobias Reiss <tobi+webkit@basecode.de>
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -137,13 +138,27 @@ class FormatterContentBuilder
         console.assert(this.lastTokenWasNewline);
         console.assert(this._formattedContent.lastValue === "\n");
         if (this.lastTokenWasNewline) {
         console.assert(this.lastTokenWasNewline);
         console.assert(this._formattedContent.lastValue === "\n");
         if (this.lastTokenWasNewline) {
-            this._popNewLine();
+            this._popFormattedContent();
+            this._formattedLineEndings.pop();
             this._startOfLine = false;
             this.lastTokenWasNewline = false;
             this.lastTokenWasWhitespace = false;
         }
     }
 
             this._startOfLine = false;
             this.lastTokenWasNewline = false;
             this.lastTokenWasWhitespace = false;
         }
     }
 
+    removeLastWhitespace()
+    {
+        console.assert(this.lastTokenWasWhitespace);
+        console.assert(this._formattedContent.lastValue === " ");
+        if (this.lastTokenWasWhitespace) {
+            this._popFormattedContent();
+            // No need to worry about `_startOfLine` and `lastTokenWasNewline`
+            // because `appendSpace` takes care of not adding whitespace
+            // to the beginning of a line.
+            this.lastTokenWasWhitespace = false;
+        }
+    }
+
     indent()
     {
         ++this._indent;
     indent()
     {
         ++this._indent;
@@ -170,11 +185,10 @@ class FormatterContentBuilder
 
     // Private
 
 
     // Private
 
-    _popNewLine()
+    _popFormattedContent()
     {
         var removed = this._formattedContent.pop();
         this._formattedContentLength -= removed.length;
     {
         var removed = this._formattedContent.pop();
         this._formattedContentLength -= removed.length;
-        this._formattedLineEndings.pop();
     }
 
     _append(str)
     }
 
     _append(str)
index d3e2b6d00b06b28a86658e1601ad9451ded1b47d..b9b183ea212227e05d14771cf5af942efc338db0 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2013 Apple Inc. All rights reserved.
 /*
  * Copyright (C) 2013 Apple Inc. All rights reserved.
+ * Copyright (C) 2015 Tobias Reiss <tobi+webkit@basecode.de>
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -776,6 +777,20 @@ WebInspector.CSSStyleDeclarationTextEditor = class CSSStyleDeclarationTextEditor
         }
     }
 
         }
     }
 
+    _formattedContentFromEditor()
+    {
+        var mapping = {original: [0], formatted: [0]};
+        // FIXME: <rdar://problem/10593948> Provide a way to change the tab width in the Web Inspector
+        var indentString = "    ";
+        var builder = new FormatterContentBuilder(mapping, [], [], 0, 0, indentString);
+        var formatter = new Formatter(this._codeMirror, builder);
+        var start = {line: 0, ch: 0};
+        var end = {line: this._codeMirror.lineCount() - 1};
+        formatter.format(start, end);
+
+        return builder.formattedContent.trim();
+    }
+
     _resetContent()
     {
         if (this._commitChangesTimeout) {
     _resetContent()
     {
         if (this._commitChangesTimeout) {
@@ -816,114 +831,62 @@ WebInspector.CSSStyleDeclarationTextEditor = class CSSStyleDeclarationTextEditor
             // Remember the cursor position/selection.
             var selectionAnchor = this._codeMirror.getCursor("anchor");
             var selectionHead = this._codeMirror.getCursor("head");
             // Remember the cursor position/selection.
             var selectionAnchor = this._codeMirror.getCursor("anchor");
             var selectionHead = this._codeMirror.getCursor("head");
-
-            function countNewLineCharacters(text)
-            {
-                var matches = text.match(/\n/g);
-                return matches ? matches.length : 0;
+            var isEditorReadOnly = this._codeMirror.getOption("readOnly");
+            var styleText = this._style.text.trim();
+            var findWhitespace = /\s+/g;
+
+            // Only format non-empty styles. Keep in mind that styleText is always empty
+            // for "readOnly" Editors. But prepare Checkbox placeholders in any case.
+            // Because that will indent the cursor when the User starts typing.
+            if (!styleText && !isEditorReadOnly) {
+                this._markLinesWithCheckboxPlaceholder();
+                return;
             }
 
             }
 
-            var styleText = this._style.text;
-
-            // Pretty print the content if there are more properties than there are lines.
-            // This could be an option exposed to the user; however, it is almost always
-            // desired in this case.
-
-            if (styleText && this._style.visibleProperties.length <= countNewLineCharacters(styleText.trim()) + 1) {
-                // This style has formatted text content, so use it for a high-fidelity experience.
-
-                var prefixWhitespaceMatch = styleText.match(/^[ \t]*\n/);
-                this._prefixWhitespace = prefixWhitespaceMatch ? prefixWhitespaceMatch[0] : "";
-
-                var suffixWhitespaceMatch = styleText.match(/\n[ \t]*$/);
-                this._suffixWhitespace = suffixWhitespaceMatch ? suffixWhitespaceMatch[0] : "";
-
-                this._codeMirror.setValue(styleText);
-
-                if (this._prefixWhitespace)
-                    this._codeMirror.replaceRange("", {line: 0, ch: 0}, {line: 1, ch: 0});
-
-                if (this._suffixWhitespace) {
-                    var lineCount = this._codeMirror.lineCount();
-                    this._codeMirror.replaceRange("", {line: lineCount - 2}, {line: lineCount - 1});
-                }
-
-                this._linePrefixWhitespace = "";
-
-                var linesToStrip = [];
-
-                // Remember the whitespace so it can be restored on commit.
-                var lineCount = this._codeMirror.lineCount();
-                for (var i = 0; i < lineCount; ++i) {
-                    var lineContent = this._codeMirror.getLine(i);
-                    var prefixWhitespaceMatch = lineContent.match(/^\s+/);
-
-                    // If there is no prefix whitespace (except for empty lines) then the prefix
-                    // whitespace of all other lines will be retained as is. Update markers and return.
-                    if (!prefixWhitespaceMatch) {
-                        if (!lineContent)
-                            continue;
-                        this._linePrefixWhitespace = "";
-                        this._updateTextMarkers(true);
-                        return;
-                    }
-
-                    linesToStrip.push(i);
-
-                    // Only remember the shortest whitespace so we don't loose any of the
-                    // original author's whitespace if their indentation lengths differed.
-                    // Using the shortest also makes the adjustment work in _updateTextMarkers.
-
-                    // FIXME: This messes up if there is a mix of spaces and tabs. A tab
-                    // is treated the same as a space when prefix whitespace is omitted,
-                    // so if the shortest prefixed whitespace is, say, two tab characters,
-                    // lines that begin with four spaces will only have a two space indent.
-                    if (!this._linePrefixWhitespace || prefixWhitespaceMatch[0].length < this._linePrefixWhitespace.length)
-                        this._linePrefixWhitespace = prefixWhitespaceMatch[0];
-                }
-
-                // Strip the whitespace from the beginning of each line.
-                for (var i = 0; i < linesToStrip.length; ++i) {
-                    var lineNumber = linesToStrip[i];
-                    var from = {line: lineNumber, ch: 0};
-                    var to = {line: lineNumber, ch: this._linePrefixWhitespace.length};
-                    this._codeMirror.replaceRange("", from, to);
-                }
-
-                // Update all the text markers.
-                this._updateTextMarkers(true);
-            } else {
-                // This style does not have text content or it is minified, so we want to synthesize the text content.
-
-                this._prefixWhitespace = "";
-                this._suffixWhitespace = "";
-                this._linePrefixWhitespace = "";
-
-                this._codeMirror.setValue("");
+            // Set non-optimized, valid and invalid styles in preparation for the Formatter.
+            // Set empty string in case of readonly styles.
+            this._codeMirror.setValue(styleText);
 
 
+            if (isEditorReadOnly) {
                 var lineNumber = 0;
                 var lineNumber = 0;
-
-                // Iterate only visible properties if we have original style text. That way we known we only synthesize
-                // what was originaly in the style text.
-                this._iterateOverProperties(styleText ? true : false, function(property) {
-                    // Some property text can have line breaks, so consider that in the ranges below.
-                    var propertyText = property.synthesizedText;
-                    var propertyLineCount = countNewLineCharacters(propertyText);
-
+                this._iterateOverProperties(false, function(property) {
                     var from = {line: lineNumber, ch: 0};
                     var from = {line: lineNumber, ch: 0};
-                    var to = {line: lineNumber + propertyLineCount};
-
-                    this._codeMirror.replaceRange((lineNumber ? "\n" : "") + propertyText, from);
+                    var to = {line: lineNumber};
+                    // Readonly properties are pretty printed by `synthesizedText` and not the Formatter.
+                    this._codeMirror.replaceRange((lineNumber ? "\n" : "") + property.synthesizedText, from);
                     this._createTextMarkerForPropertyIfNeeded(from, to, property);
                     this._createTextMarkerForPropertyIfNeeded(from, to, property);
-
-                    lineNumber += propertyLineCount + 1;
+                    lineNumber++;
                 });
 
                 });
 
-                // Look for colors and make swatches.
-                this._createColorSwatches(true);
+                return;
             }
 
             }
 
-            this._markLinesWithCheckboxPlaceholder();
+            // Now the Formatter pretty prints the styles.
+            this._codeMirror.setValue(this._formattedContentFromEditor());
+
+            // We need to workaround the fact that...
+            // 1) `this._style.properties` only holds valid CSSProperty instances but not
+            // comments and invalid properties like `color;`.
+            // 2) `_createTextMarkerForPropertyIfNeeded` relies on CSSProperty instances.
+            var cssPropertiesMap = new Map();
+            this._iterateOverProperties(false, function(cssProperty) {
+                cssPropertiesMap.set(cssProperty.text.replace(findWhitespace, ""), cssProperty);
+            });
+
+            // Go through the Editor line by line and create TextMarker when a
+            // CSSProperty instance for that property exists. If not, then don't create a TextMarker.
+            this._codeMirror.eachLine(function(lineHandler) {
+                var lineNumber = lineHandler.lineNo();
+                var lineContentSansWhitespace = lineHandler.text.replace(findWhitespace, "");
+                if (cssPropertiesMap.has(lineContentSansWhitespace)) {
+                    var from = {line: lineNumber, ch: 0};
+                    var to = {line: lineNumber};
+                    this._createTextMarkerForPropertyIfNeeded(from, to, cssPropertiesMap.get(lineContentSansWhitespace));
+                }
+            }.bind(this));
+
+            // Look for colors and make swatches.
+            this._createColorSwatches(true);
 
             // Restore the cursor position/selection.
             this._codeMirror.setSelection(selectionAnchor, selectionHead);
 
             // Restore the cursor position/selection.
             this._codeMirror.setSelection(selectionAnchor, selectionHead);
@@ -934,6 +897,8 @@ WebInspector.CSSStyleDeclarationTextEditor = class CSSStyleDeclarationTextEditor
 
             // Mark the editor as clean (unedited state).
             this._codeMirror.markClean();
 
             // Mark the editor as clean (unedited state).
             this._codeMirror.markClean();
+
+            this._markLinesWithCheckboxPlaceholder();
         }
 
         // This needs to be done first and as a separate operation to avoid an exception in CodeMirror.
         }
 
         // This needs to be done first and as a separate operation to avoid an exception in CodeMirror.
index 4f5cfb3e60e9a61727fa23073ae96f50efeff23f..902f112524d91e18020c6425fb205afa7914b893 100644 (file)
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2013 Apple Inc. All rights reserved.
 /*
  * Copyright (C) 2013 Apple Inc. All rights reserved.
+ * Copyright (C) 2015 Tobias Reiss <tobi+webkit@basecode.de>
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -106,6 +107,11 @@ CodeMirror.extendMode("javascript", {
         return 0;
     },
 
         return 0;
     },
 
+    removeLastWhitespace: function(lastToken, lastContent, token, state, content, isComment)
+    {
+        return false;
+    },
+
     removeLastNewline: function(lastToken, lastContent, token, state, content, isComment, firstTokenOnLine)
     {
         if (!token) {
     removeLastNewline: function(lastToken, lastContent, token, state, content, isComment, firstTokenOnLine)
     {
         if (!token) {
@@ -374,6 +380,11 @@ CodeMirror.extendMode("css", {
         return 0;
     },
 
         return 0;
     },
 
+    removeLastWhitespace: function(lastToken, lastContent, token, state, content, isComment)
+    {
+        return false;
+    },
+
     removeLastNewline: function(lastToken, lastContent, token, state, content, isComment, firstTokenOnLine)
     {
         if (isComment) { // Comment after semicolon.
     removeLastNewline: function(lastToken, lastContent, token, state, content, isComment, firstTokenOnLine)
     {
         if (isComment) { // Comment after semicolon.
@@ -419,3 +430,83 @@ CodeMirror.extendMode("css", {
             state._cssPrettyPrint.lineLength = 0;
     }
 });
             state._cssPrettyPrint.lineLength = 0;
     }
 });
+
+CodeMirror.extendMode("css-rule", {
+    shouldHaveSpaceBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
+    {
+        return lastContent === ":" && !lastToken;
+    },
+
+    shouldHaveSpaceAfterLastToken: function(lastToken, lastContent, token, state, content, isComment)
+    {
+        return lastContent === "," && !lastToken;
+    },
+
+    newlinesAfterToken: function(lastToken, lastContent, token, state, content, isComment)
+    {
+        return 0;
+    },
+
+    removeLastWhitespace: function(lastToken, lastContent, token, state, content, isComment)
+    {
+        // Remove whitespace before a comment which moves the comment to the beginning of the line.
+        if (isComment)
+            return true;
+
+        // A semicolon indicates the end of line. So remove whitespace before next line.
+        if (!lastToken)
+            return lastContent === ";";
+
+        // Remove whitespace before semicolon. Like `prop: value ;`.
+        // Remove whitespace before colon. Like `prop : value;`.
+        if (!token)
+            return content === ";" || content === ":";
+
+        // A comment is supposed to be in its own line. So remove whitespace before next line.
+        if (/\bcomment\b/.test(lastToken))
+            return true;
+
+        return false;
+    },
+
+    removeLastNewline: function(lastToken, lastContent, token, state, content, isComment, firstTokenOnLine)
+    {
+        return false;
+    },
+
+    indentAfterToken: function(lastToken, lastContent, token, state, content, isComment)
+    {
+        return false;
+    },
+
+    newlineBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
+    {
+        // Add new line before comments.
+        if (isComment)
+            return true;
+
+        // Add new line before a prefixed property like `-webkit-animation`.
+        if (state.state === "block")
+            return /\bmeta\b/.test(token);
+
+        // Add new line after comment
+        if (/\bcomment\b/.test(lastToken))
+            return true;
+
+        // Add new line before a regular property like `display`.
+        if (/\bproperty\b/.test(token))
+            return !(/\bmeta\b/.test(lastToken));
+
+        return false;
+    },
+
+    indentBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
+    {
+        return false;
+    },
+
+    dedentsBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
+    {
+        return 0;
+    }
+});