Subject: Re: bug#13425: new css-mode indenter

From: E Sabof <[email-masked]>
Date: Thu, 17 Jan 2013 00:17:34 +0000

I've rewritten the indenter - this time I've added a parser for statements
within curly brackets, so there is a lot less hackery. I've also added some
more tests cases.

Evgeni

New        css-mode.css
diff --git a/css-mode.css b/css-mode.css
new file mode 100644
index 0000000..f101971
--- /dev/null
+++ b/css-mode.css
@@ -0,0 +1,230 @@
+#top_menu .button.active,
+#top_menu .button.active:active {
+    background-image: url('images/header_button_active.png');
+    cursor: default;
+    color: #999;
+    text-shadow: 1px 1px 2px #111;
+    /* Comment */
+}
+
+#top_menu .button.active,
+#top_menu .button.active:active
+{
+    /* Comment */
+    background-image: url('images/header_button_active.png');
+    cursor: default;
+    color: #999;
+    text-shadow: 1px 1px 2px #111;
+    /* Multiline
+       comment1 */
+    /* Multiline
+     * comment2 */
+}
+/* Multiline
+   comment1 */
+/* Multiline
+ * comment2 */
+#glass {
+    z-index: 2;
+    position: absolute;
+    top: -112px;
+    left: 0;
+    right: 0;
+    margin: 0 0 0 0;
+    margin:
+        0 0 0 0;
+    text-shadow:
+        1px 1px #FDE31D,
+        -1px -1px #B36300;
+    height: 140px;
+    background-image: url('images/honey_blurred2.png');
+    background-image: url(
+        'images/honey_blurred2.png');
+    background-image:
+        #fff,
+        url('images/honey_blurred2.png'),
+        url('images/honey_blurred2.png');
+    background-image:
+        #fff,
+        /* #fff, */
+        url('images/honey_blurred2.png');
+    background-image: #fff,
+        url('images/honey_blurred2.png');
+    -webkit-mask-image:
+        -webkit-gradient(
+            linear,
+            left top,
+            /* left bottom, */
+            left bottom,
+            color-stop(
+                1,
+                rgba(0,0,0,0.2)
+            ),
+            color-stop(1,
+
+                rgba(0,0,0,0.2)
+            ),
+            /* comment */
+            color-stop(0.7, rgba(0,0,0,.0)),
+            color-stop
+            (
+                0.7, rgba(0,0,0,.0)
+
+            )
+            /* comment*/ );
+    background-repeat: repeat-x;
+    background-position: center top;
+    background-attachment: fixed; }
+
+#glass {
+    margin:
+        /*     0 0 0 0; */
+        /* text-shadow: */
+        1px 1px #FDE31D,
+        -1px -1px #B36300;
+    height: 140px;
+    background-image: url('images/honey_blurred2.png');
+    /* background-image: url( */
+    /*     'images/honey_blurred2.png'); */
+
+    background-image:
+        #fff,
+        url('images/honey_blurred2.png'),
+        url('images/honey_blurred2.png');
+    background-image:
+        #fff,
+        /* #fff, */
+        /*     url('images/honey_blurred2.png'); */
+        /* background-image: #fff, */
+        url('images/honey_blurred2.png');
+    -webkit-mask-image:
+        -webkit-gradient(
+            linear,
+            left top,
+            /* left bottom, */
+            left bottom,
+            color-stop(
+                /* 1, */
+                /* rgba(0,0,0,0.2) */
+            ),
+            color-stop(1,
+                /* com */
+                rgba(0,0,0,0.2)
+
+            ),
+            /* comment */
+            color-stop(0.7, rgba(0,0,0,.0)),
+            color-stop
+            (
+
+                0.7, rgba(0,0,0,.0)
+            )
+            /* comment*/);
+    -webkit-mask-image:
+        /* -webkit- */gradient(
+            linear,
+            left /* top */,
+            /* left  */bottom,
+            left bottom,
+            color-stop(
+                /* 1, */
+                /* rgba(0,0,0,0.2) */
+            ),
+            color-stop(1,
+                /* com */
+                rgba(0,0,0,0.2)
+
+            ),
+            /* comment */
+            color-stop(0.7/* , rgba(0,0,0,.0) */
+                , rgba(0,0,0,.0) ),
+            color-stop
+            (
+
+                0.7, rgba(0,0,0,.0)
+            )
+            /* comment*/);
+    color: black,
+        /* red */,
+        blue;
+    /* -webkit-mask-image: */
+    background:
+        -webkit-gradient(
+            /* com */
+    );
+    -webkit-mask-image: -webkit-gradient(
+        /* Forgivable? Better? */
+    ),
+        -webkit-mask-image(
+            /* Back on track */
+        );
+    background-attachment: fixed
+    /* sdfsdf */
+}
+
+p:nth-child(2) {
+    margin:
+        0
+        0
+        0
+        0;
+    margin: 0 0
+        0
+        0;
+
+    /* comment */
+
+    text-shadow:1px 1px #FDE31D, /* no space */
+        -1px -1px #B36300;
+}
+
+p:nth-child
+(
+    2
+)
+{
+    height: 2px
+}
+
+#field_message {
+    width: 100%;
+    display: block;
+    -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
+    -moz-box-sizing: border-box;    /* Firefox, other Gecko */
+    box-sizing: border-box;         /* Opera/IE 8+ */
+    /* resize: vertical; */
+    resize: none;
+    height: 160px
+}
+
+
+some {
+
+}
+
+/* SASS */
+
+.some {
+    /* resize: vertical; */
+    resize: none;
+
+    height: 160px;
+    .other,
+    .another,
+    &:a,
+    #id {
+        /* resize: vertical; */
+        resize: none;
+
+        height: 160px;
+    }
+    /* Comment */
+    resize: none;
+    height: 160px;
+    /* Comment */
+}
+
+
+/* Local Variables: */
+/* css-debug: t */
+/* End: */
\ No newline at end of file
Modified   css-mode.el
diff --git a/css-mode.el b/css-mode.el
index 1abe9a8..9f3a786 100644
--- a/css-mode.el
+++ b/css-mode.el
@@ -31,12 +31,95 @@
 ;; - completion
 ;; - fix font-lock errors with multi-line selectors

+;;; TODO:
+;;  extra empty line indentation
+
 ;;; Code:

+(require 'cl-lib)
+
 (defgroup css nil
   "Cascading Style Sheets (CSS) editing mode."
   :group 'languages)

+;; Debugging
+
+(defvar css-debug-overlays nil)
+
+(defun css-debug-overlay (beginning end color)
+  (let ((overlay (make-overlay beginning end)))
+    (overlay-put overlay 'face `(:background ,color))
+    (push overlay css-debug-overlays)))
+
+(defvar css-debug nil)
+
+(defun css-debug-msg (name)
+  (when css-debug
+    (message "%s" name)))
+
+(defun css-debug-goto-root-declaration ()
+  (let* (( ppss (syntax-ppss))
+         ( depth (nth 0  ppss)))
+    (when (nth 3 ppss)
+      (goto-char (nth 8 ppss)))
+    (while (and (cl-plusp depth)
+                (not (equal (char-after) ?\{ )))
+      (up-list -1)
+      (cl-decf depth))))
+
+(defun css-debug-parser-current ()
+  (interactive)
+  (mapc 'delete-overlay css-debug-overlays)
+  (let* (( point
+           (save-excursion
+             (back-to-indentation)
+             (point)))
+         ( parsed
+           (save-excursion
+             (css-debug-goto-root-declaration)
+             (css-parse-curly)))
+         ( relevant
+           (css-pc-get-relevant parsed point)))
+    (css-debug-overlay
+     (car relevant)
+     (cadr relevant)
+     "DarkRed")
+    nil))
+
+(defun css-debug-highight-parsed (parsed)
+  (mapc 'delete-overlay css-debug-overlays)
+  (cl-mapc (lambda (item)
+             (css-debug-overlay
+              (car item)
+              (cadr item)
+              (case (caddr item)
+                ('nested-selector  "DarkGreen")
+                ('comment          "SaddleBrown")
+                ( t                "DarkRed"))))
+           parsed))
+
+(defun css-debug-parser-all ()
+  (interactive)
+  (let* (( parsed
+           (save-excursion
+             (css-debug-goto-root-declaration)
+             (css-parse-curly))))
+    (css-debug-highight-parsed
+     parsed)
+    nil))
+
+(defun css-debug-parser-inside ()
+  (interactive)
+  (let* (( parsed
+           (save-excursion
+             (css-debug-goto-root-declaration)
+             (css-parse-curly)))
+         ( inside
+           (css-pc-inside-statement
+            parsed (point))))
+    (message "P %s" inside)))
+
+;; EOF Debugging

 (defun css-extract-keyword-list (res)
   (with-temp-buffer
@@ -92,7 +175,6 @@
        (t nil)))
     elems))

-
 (defun css-extract-props-and-vals ()
   (with-temp-buffer
     (url-insert-file-contents "http://www.w3.org/TR/CSS21/propidx.html")
@@ -122,7 +204,7 @@

 (defconst css-pseudo-ids
   '("active" "after" "before" "first" "first-child" "first-letter"
"first-line"
-    "focus" "hover" "lang" "left" "link" "right" "visited")
+    "focus" "hover" "lang" "last-child" "left" "link" "right" "visited")
   "Identifiers for pseudo-elements and pseudo-classes.")

 (defconst css-at-ids
@@ -264,8 +346,8 @@
   '(css-font-lock-keywords nil t))

 ;;;###autoload
-(define-derived-mode css-mode fundamental-mode "CSS"
-  "Major mode to edit Cascading Style Sheets."
+(define-derived-mode css-mode fundamental-mode
+  "CSS" "Major mode to edit Cascading Style Sheets."
   (setq-local font-lock-defaults css-font-lock-defaults)
   (setq-local comment-start "/*")
   (setq-local comment-start-skip "/\\*+[ \t]*")
@@ -276,6 +358,8 @@
   (setq-local indent-line-function 'css-indent-line)
   (setq-local fill-paragraph-function 'css-fill-paragraph)
   (setq-local add-log-current-defun-function #'css-current-defun-name)
+  (setq-local beginning-of-defun-function 'css-beginning-of-defun)
+  (setq-local end-of-defun-function 'css-end-of-defun)
   (when css-electric-keys
     (let ((fc (make-char-table 'auto-fill-chars)))
       (set-char-table-parent fc auto-fill-chars)
@@ -394,102 +478,188 @@
                           ;; FIXME: We should also skip punctuation.
                           (not (memq (char-after) '(?\; ?\})))))))))))

-(defun css-indent-calculate-virtual ()
-  (if (or (save-excursion (skip-chars-backward " \t") (bolp))
-          (if (looking-at "\\s(")
-              (save-excursion
-                (forward-char 1) (skip-chars-forward " \t")
-                (not (or (eolp) (looking-at comment-start-skip))))))
-      (current-column)
-    (css-indent-calculate)))
-
-(defcustom css-indent-offset 4
-  "Basic size of one indentation step."
-  :version "22.2"
-  :type 'integer
-  :group 'css)
+(defun css-beginning-of-defun (&optional arg)
+  (unless arg (setq arg 1))
+  (when (progn
+          ;; What for?
+          (unless (zerop (current-column))
+            (end-of-line))
+          (re-search-backward "^[^\n ].+{[ ]?$" nil t arg))
+    (while (save-excursion
+             (and (zerop (forward-line -1))
+                  (string-match-p
+                   "^[^}[:space:]/]"
+                   (buffer-substring
+                    (line-beginning-position)
+                    (line-end-position)))))
+      (forward-line -1))))
+
+(defun css-end-of-defun (&optional arg)
+  (interactive)
+  (unless arg (setq arg 1))
+  (ignore-errors
+    (when (cl-plusp (car (syntax-ppss)))
+      (css-beginning-of-defun))
+    (progn
+      (search-forward "{" nil t arg)
+      (backward-char)
+      (forward-sexp)
+      (ignore-errors
+        (forward-char)))
+    t))
+
+(defun css-go-up ()
+  (let* (( ppss (syntax-ppss)))
+    (when (or (nth 3 ppss) (nth 4 ppss))
+      (goto-char (nth 8 ppss)))
+    (when (cl-plusp (nth 0 ppss))
+      (up-list -1))))
+
+(defmacro css-while-point-moving (&rest rest)
+  (let ((old-point (cl-gensym)))
+    `(let (,old-point)
+       (while (not (equal (point) ,old-point))
+         (setq ,old-point (point))
+         ,@rest))))
+
+(defun css-parse-curly ()
+  (let (( start (point))
+        ( indentation (current-indentation))
+        ( end (save-excursion
+                (forward-sexp)
+                (point)))
+        point result)
+    (forward-char)
+    (cl-loop named main-loop
+             do
+             (skip-chars-forward "\n\t " end)
+             (when (>= (point) (1- end))
+               (cl-return-from main-loop))
+             (setq point (point))
+             (if (forward-comment 1)
+                 (push (list point (point) 'comment) result)
+                 (progn
+                   (cl-loop (unless (re-search-forward ";\\|{\\|}" end t)
+                              (cl-return-from main-loop))
+                            (unless (nth 4 (syntax-ppss))
+                              (cl-return)))
+                   (cond ( (equal (char-before) ?\{ )
+                           (backward-char)
+                           (forward-sexp)
+                           (push (list point (point) 'nested-selector)
result))
+                         ( (equal (char-before) ?\} )
+                           (backward-char)
+                           (css-while-point-moving
+                            (skip-chars-backward "\n\t " start)
+                            (forward-comment -1))
+                           (push (list point (point) 'statement) result))
+                         ( t (push (list point (point) 'statement)
result))))))
+    (nreverse result)))
+
+(defun css-pc-get-relevant (parsed point)
+  (car (reverse (cl-remove-if (apply-partially '< point)
+                              parsed :key 'car))))
+
+(defun css-pc-inside-statement (parsed point)
+  (cl-some (lambda (item)
+           (and (<= (car item) point)
+                (<= point (cadr item))))
+         parsed))

 (defun css-indent-calculate ()
-  (let ((ppss (syntax-ppss))
-        pos)
-    (with-syntax-table css-navigation-syntax-table
-      (save-excursion
-        (cond
-         ;; Inside a string.
-         ((nth 3 ppss) 'noindent)
-         ;; Inside a comment.
-         ((nth 4 ppss)
-          (setq pos (point))
-          (forward-line -1)
-          (skip-chars-forward " \t")
-          (if (>= (nth 8 ppss) (point))
-              (progn
-                (goto-char (nth 8 ppss))
-                (if (eq (char-after pos) ?*)
-                    (forward-char 1)
-                  (if (not (looking-at comment-start-skip))
-                      (error "Internal css-mode error")
-                    (goto-char (match-end 0))))
-                (current-column))
-            (if (and (eq (char-after pos) ?*) (eq (char-after) ?*))
-                (current-column)
-              ;; 'noindent
-              (current-column)
-              )))
-         ;; In normal code.
-         (t
-          (or
-           (when (looking-at "\\s)")
-             (forward-char 1)
-             (backward-sexp 1)
-             (css-indent-calculate-virtual))
-           (when (looking-at comment-start-skip)
-             (forward-comment (point-max))
-             (css-indent-calculate))
-           (when (save-excursion (forward-comment (- (point-max)))
-                                 (setq pos (point))
-                                 (eq (char-syntax (preceding-char)) ?\())
-             (goto-char (1- pos))
-             (if (not (looking-at "\\s([ \t]*"))
-                 (error "Internal css-mode error")
-               (if (or (memq (char-after (match-end 0)) '(?\n nil))
-                       (save-excursion (goto-char (match-end 0))
-                                       (looking-at comment-start-skip)))
-                   (+ (css-indent-calculate-virtual) css-indent-offset)
-                 (progn (goto-char (match-end 0)) (current-column)))))
-           (progn
-             (css-backward-sexp 1)
-             (if (looking-at "\\s(")
-                 (css-indent-calculate)
-               (css-indent-calculate-virtual))))))))))
-
+  (save-match-data
+    (condition-case error
+        (with-syntax-table css-navigation-syntax-table
+          (back-to-indentation)
+          (let* ((point (point))
+                 (ppss (syntax-ppss))
+                 ( css-curly-parsed
+                   (save-excursion
+                     (css-go-up)
+                     (when (equal (char-after) ?{ )
+                       (css-parse-curly))))
+                 css-parsed-relevant
+                 (block-ending-line
+                  (member (char-after
+                           (save-excursion
+                             (back-to-indentation)
+                             (point)))
+                          '( ?\} ?\) ) )))
+            (cond ( (nth 4 ppss)
+                    ;; Inside a multiline comment
+                    (css-debug-msg "MC")
+                    (save-excursion
+                      (forward-line -1)
+                      (skip-chars-forward " \t")
+                      (if (>= (nth 8 ppss) (point))
+                          (progn
+                            (goto-char (nth 8 ppss))
+                            (if (eq (char-after point) ?*)
+                                (forward-char 1)
+                                (if (not (looking-at comment-start-skip))
+                                   (error "Internal css-mode error")
+                                   (goto-char (match-end 0))))
+                           (current-column))
+                         (current-column))))
+                 ( ;; If "outside" indent to 0
+                  (zerop (nth 0 ppss))
+                  (css-debug-msg "ZERO")
+                  0)
+                 ( ;; inside curly brackets
+                  (and css-curly-parsed
+                       (not block-ending-line)
+                       (setq css-parsed-relevant
+                             (css-pc-get-relevant
+                              css-curly-parsed point))
+                       (not (eq (nth 2 css-parsed-relevant)
+                                'nested-selector)))
+                  (css-debug-msg "C")
+                  (+ css-indent-offset
+                     (save-excursion
+                       (css-go-up)
+                       (current-indentation))
+                     (if (and (not (equal (line-number-at-pos
+                                           (car css-parsed-relevant))
+                                          (line-number-at-pos)))
+                              (css-pc-inside-statement
+                               css-curly-parsed point))
+                         css-indent-offset 0)))
+                 ( ;; Inside parentheses, closing brackets
+                  t
+                  (css-debug-msg "P")
+                  (+ (save-excursion
+                       (css-go-up)
+                       (current-indentation))
+                     (if block-ending-line
+                         0 css-indent-offset))))))
+      (error ;; My best error-less guess
+       (css-debug-msg "Err")
+       (* (car (syntax-ppss))
+          css-indent-offset)))))

 (defun css-indent-line ()
   "Indent current line according to CSS indentation rules."
   (interactive)
-  (let* ((savep (point))
-         (forward-sexp-function nil)
- (indent (condition-case nil
-     (save-excursion
-       (forward-line 0)
-       (skip-chars-forward " \t")
-       (if (>= (point) savep) (setq savep nil))
-       (css-indent-calculate))
-   (error nil))))
-    (if (not (numberp indent)) 'noindent
-      (if savep
-          (save-excursion (indent-line-to indent))
-        (indent-line-to indent)))))
+  (save-excursion
+    (indent-line-to (css-indent-calculate)))
+  (when (< (current-column) (current-indentation))
+    (back-to-indentation)))
+
+(defcustom css-indent-offset 4
+  "Basic size of one indentation step."
+  :version "22.2"
+  :type 'integer
+  :group 'css)

 (defun css-current-defun-name ()
   "Return the name of the CSS section at point, or nil."
   (save-excursion
     (let ((max (max (point-min) (- (point) 1600))))  ; approx 20 lines back
       (when (search-backward "{" max t)
- (skip-chars-backward " \t\r\n")
- (beginning-of-line)
- (if (looking-at "^[ \t]*\\([^{\r\n]*[^ {\t\r\n]\\)")
-    (match-string-no-properties 1))))))
+        (skip-chars-backward " \t\r\n")
+        (beginning-of-line)
+        (if (looking-at "^[ \t]*\\([^{\r\n]*[^ {\t\r\n]\\)")
+            (match-string-no-properties 1))))))

 (provide 'css-mode)
 ;;; css-mode.el ends here
\ No newline at end of file

On Tue, Jan 15, 2013 at 7:32 PM, E Sabof <[email-masked]> wrote:

> Here is a patch, including a more advanced test case. In the end there is
> a "failing" section - although I haven't seen anyone breaking at spaces.
>
> There is a varaiable called css-colon - the default is ":". If set to ":
> ", it will also indent statements like this in SASS (a CSS pre-processor)
>
> a { &:hover, &:active { background: green; } }
>
> However, it will no longer correctly indent statements like this:
>
> background:#000, /* no space */ url('image.png');
>
> If I manage to fix the "breaking at spaces" case, I'll send another patch.
>
> Evgeni
>
>  New css-mode.css diff --git a/css-mode.css b/css-mode.css new file mode
> 100644 index 0000000..40a732f --- /dev/null +++ b/css-mode.css @@ -0,0
> +1,86 @@ +#top_menu .button.active, +#top_menu .button.active:active { +
> background-image: url('images/header_button_active.png'); + cursor:
> default; + color: #999; + text-shadow: 1px 1px 2px #111; + /* Comment */ +}
> +#top_menu .button.active, +#top_menu .button.active:active +{ + /* Comment
> */ + background-image: url('images/header_button_active.png'); + cursor:
> default; + color: #999; + text-shadow: 1px 1px 2px #111; + /* Multiline +
> comment1 */ + /* Multiline + * comment2 */ +} +/* Multiline + comment1 */
> +/* Multiline + * comment2 */ +#glass { + z-index: 2; + position: absolute;
> + top: -112px; + left: 0; + right: 0; + margin: 0 0 0 0; + margin: + 0 0 0
> 0; + text-shadow: + 1px 1px #FDE31D, + -1px -1px #B36300; + height: 140px;
> + background-image: url('images/honey_blurred2.png'); + background-image:
> url( + 'images/honey_blurred2.png'); + background-image: + #fff, +
> url('images/honey_blurred2.png'), + url('images/honey_blurred2.png'); +
> background-image: + #fff, + /* #fff, */ + url('images/honey_blurred2.png');
> + background-image: #fff, + url('images/honey_blurred2.png'); +
> -webkit-mask-image: + -webkit-gradient( + linear, + left top, + /* left
> bottom, */ + left bottom, + color-stop( + 1, + rgba(0,0,0,0.2) + ), +
> color-stop(1, + rgba(0,0,0,0.2) + ), + /* comment */ + color-stop(0.7,
> rgba(0,0,0,.0)), + color-stop + ( + 0.7, rgba(0,0,0,.0) + ) + /* comment*/
> ); + background-repeat: repeat-x; + background-position: center top; +
> background-attachment: fixed; } + +#failing { + margin: 0 + 0 + 0 + 0; +
> margin: + 0 + 0 + 0 + 0; +} \ No newline at end of file Modified
> css-mode.el diff --git a/css-mode.el b/css-mode.el index 1abe9a8..44682e1
> 100644 --- a/css-mode.el +++ b/css-mode.el @@ -33,6 +33,8 @@ ;;; Code:
> +(require 'cl-lib) + (defgroup css nil "Cascading Style Sheets (CSS)
> editing mode." :group 'languages) @@ -92,7 +94,6 @@ (t nil))) elems)) -
> (defun css-extract-props-and-vals () (with-temp-buffer
> (url-insert-file-contents "http://www.w3.org/TR/CSS21/propidx.html") @@
> -122,7 +123,7 @@ (defconst css-pseudo-ids '("active" "after" "before"
> "first" "first-child" "first-letter" "first-line" - "focus" "hover" "lang"
> "left" "link" "right" "visited") + "focus" "hover" "lang" "last-child"
> "left" "link" "right" "visited") "Identifiers for pseudo-elements and
> pseudo-classes.") (defconst css-at-ids @@ -264,8 +265,8 @@
> '(css-font-lock-keywords nil t)) ;;;###autoload -(define-derived-mode
> css-mode fundamental-mode "CSS" - "Major mode to edit Cascading Style
> Sheets." +(define-derived-mode css-mode fundamental-mode + "CSS" "Major
> mode to edit Cascading Style Sheets." (setq-local font-lock-defaults
> css-font-lock-defaults) (setq-local comment-start "/*") (setq-local
> comment-start-skip "/\\*+[ \t]*") @@ -276,6 +277,8 @@ (setq-local
> indent-line-function 'css-indent-line) (setq-local fill-paragraph-function
> 'css-fill-paragraph) (setq-local add-log-current-defun-function
> #'css-current-defun-name) + (setq-local beginning-of-defun-function
> 'css-beginning-of-defun) + (setq-local end-of-defun-function
> 'css-end-of-defun) (when css-electric-keys (let ((fc (make-char-table
> 'auto-fill-chars))) (set-char-table-parent fc auto-fill-chars) @@ -394,102
> +397,178 @@ ;; FIXME: We should also skip punctuation. (not (memq
> (char-after) '(?\; ?\}))))))))))) -(defun css-indent-calculate-virtual () -
> (if (or (save-excursion (skip-chars-backward " \t") (bolp)) - (if
> (looking-at "\\s(") - (save-excursion - (forward-char 1)
> (skip-chars-forward " \t") - (not (or (eolp) (looking-at
> comment-start-skip)))))) - (current-column) - (css-indent-calculate)))
> +(defun css-comment-line-p () + (interactive) + (cond ( (save-excursion +
> (back-to-indentation) + (looking-at "/\\*")) + 1 ) + ( (nth 4
> (syntax-ppss)) + t))) + +(defun css-beginning-of-defun (&optional arg) +
> (unless arg (setq arg 1)) + (when (progn + ;; What for? + (unless (zerop
> (current-column)) + (end-of-line)) + (re-search-backward "^[^\n ].+{[ ]?$"
> nil t arg)) + (while (save-excursion + (and (zerop (forward-line -1)) +
> (string-match-p + "^[^}[:space:]/]" + (buffer-substring +
> (line-beginning-position) + (line-end-position))))) + (forward-line -1))))
> + +(defun css-end-of-defun (&optional arg) + (interactive) + (unless arg
> (setq arg 1)) + (ignore-errors + (when (cl-plusp (first (syntax-ppss))) +
> (css-beginning-of-defun)) + (progn + (search-forward "{" nil t arg) +
> (backward-char) + (forward-sexp) + (ignore-errors + (forward-char))) + t))
> + +;; To make writing derived modes easier. Ex. SASS also supports // type
> comments +(defvar css-comment-line-p-function 'css-comment-line-p + "Should
> return 1 if at the beginning of a comment, t if inside.") + +(defun
> css--goto-prev-struct-line () + (while (and (zerop (forward-line -1)) +
> (funcall css-comment-line-p-function)))) + +(defvar css-debug nil) +
> +(defun css-indent-debug-msg (name) + (when css-debug + (message "%s"
> name))) + +(defun css-visible-end-of-line () + (save-excursion +
> (end-of-line) + (skip-syntax-backward + " " (line-beginning-position)) +
> (point))) -(defcustom css-indent-offset 4 - "Basic size of one indentation
> step." - :version "22.2" - :type 'integer - :group 'css) +(defun
> css-indentation-end-pos () + (save-excursion + (back-to-indentation) +
> (point))) -(defun css-indent-calculate () - (let ((ppss (syntax-ppss)) -
> pos) - (with-syntax-table css-navigation-syntax-table - (save-excursion -
> (cond - ;; Inside a string. - ((nth 3 ppss) 'noindent) - ;; Inside a
> comment. - ((nth 4 ppss) - (setq pos (point)) - (forward-line -1) -
> (skip-chars-forward " \t") - (if (>= (nth 8 ppss) (point)) - (progn -
> (goto-char (nth 8 ppss)) - (if (eq (char-after pos) ?*) - (forward-char 1)
> - (if (not (looking-at comment-start-skip)) - (error "Internal css-mode
> error") - (goto-char (match-end 0)))) - (current-column)) - (if (and (eq
> (char-after pos) ?*) (eq (char-after) ?*)) - (current-column) - ;;
> 'noindent - (current-column) - ))) - ;; In normal code. - (t - (or - (when
> (looking-at "\\s)") - (forward-char 1) - (backward-sexp 1) -
> (css-indent-calculate-virtual)) - (when (looking-at comment-start-skip) -
> (forward-comment (point-max)) - (css-indent-calculate)) - (when
> (save-excursion (forward-comment (- (point-max))) - (setq pos (point)) -
> (eq (char-syntax (preceding-char)) ?\()) - (goto-char (1- pos)) - (if (not
> (looking-at "\\s([ \t]*")) - (error "Internal css-mode error") - (if (or
> (memq (char-after (match-end 0)) '(?\n nil)) - (save-excursion (goto-char
> (match-end 0)) - (looking-at comment-start-skip))) - (+
> (css-indent-calculate-virtual) css-indent-offset) - (progn (goto-char
> (match-end 0)) (current-column))))) - (progn - (css-backward-sexp 1) - (if
> (looking-at "\\s(") - (css-indent-calculate) -
> (css-indent-calculate-virtual)))))))))) +(defun
> css-current-character-indentation () + "Like (current-indentation), but
> counts tabs as single characters." + (save-excursion +
> (back-to-indentation) + (- (point) (line-beginning-position)))) + +(defvar
> css-colon ":" + "A dervied mode for SASS or LESS might want to set this to
> +\": \", to make nested pseudo-classes work.") +(defun css-indent-calculate
> () + ;; If I go to the beginning of line, MC stops working +
> (back-to-indentation) + (with-syntax-table css-navigation-syntax-table +
> (let* (psl-indent + psl-last-char + psl-first-char + ( psl +
> (save-excursion + (css--goto-prev-struct-line) + (setq psl-indent
> (current-indentation)) + (setq psl-last-char (char-before
> (css-visible-end-of-line))) + (setq psl-first-char (char-after
> (css-indentation-end-pos))) + (buffer-substring + (line-beginning-position)
> + (line-end-position)))) + ( psl-closing-brackets + (+ (cl-count ?} psl) +
> (cl-count ?\) psl))) + ( psl-open-brackets (+ (cl-count ?{ psl) (cl-count
> ?\( psl))) + ( psl-has-colon (cl-plusp (cl-count ?: psl))) + (ppss
> (syntax-ppss)) + previous-comment-indent + previous-line-was-comment + pos)
> + (cond ( ;; Inside a multiline comment + ( eq (funcall
> css-comment-line-p-function) t) + (css-indent-debug-msg "MC") +
> (save-excursion + (nth 4 ppss) + (setq pos (point)) + (forward-line -1) +
> (skip-chars-forward " \t") + (if (>= (nth 8 ppss) (point)) + (progn +
> (goto-char (nth 8 ppss)) + (if (eq (char-after pos) ?*) + (forward-char 1)
> + (if (not (looking-at comment-start-skip)) + (error "Internal css-mode
> error") + (goto-char (match-end 0)))) + (current-column)) +
> (current-column)))) + ( ;; If "outside" indent to 0 + (zerop (nth 0 ppss))
> + (css-indent-debug-msg "ZERO") + 0) + ( ;; Not-first member of comma
> ending lines + (and (not (cl-search css-colon psl)) + (equal psl-last-char
> ?\, ) + (= psl-open-brackets psl-closing-brackets)) + (css-indent-debug-msg
> "MCB") + psl-indent) + ( ;; Line after beginning of comma block + (and
> (member psl-last-char '( ?: ?\, ) ) + (= psl-open-brackets
> psl-closing-brackets)) + (css-indent-debug-msg "LABOC") + (+ psl-indent
> css-indent-offset)) + ( ;; Default, based on nesting level + t +
> (css-indent-debug-msg "LAST") + (let (( parent-indent + (save-excursion +
> (backward-up-list) + (css-current-character-indentation))) + (
> block-ending-line + (member (char-after (css-indentation-end-pos)) + '( ?\}
> ?\) ) ))) + (+ parent-indent + (* (+ (if block-ending-line -1 0) + 1) +
> css-indent-offset)))) + )))) (defun css-indent-line () "Indent current line
> according to CSS indentation rules." (interactive) - (let* ((savep (point))
> - (forward-sexp-function nil) - (indent (condition-case nil -
> (save-excursion - (forward-line 0) - (skip-chars-forward " \t") - (if (>=
> (point) savep) (setq savep nil)) - (css-indent-calculate)) - (error nil))))
> - (if (not (numberp indent)) 'noindent - (if savep - (save-excursion
> (indent-line-to indent)) - (indent-line-to indent))))) + (save-excursion +
> (indent-line-to (css-indent-calculate))) + (when (< (current-column)
> (current-indentation)) + (back-to-indentation))) + +(defcustom
> css-indent-offset 4 + "Basic size of one indentation step." + :version
> "22.2" + :type 'integer + :group 'css) (defun css-current-defun-name ()
> "Return the name of the CSS section at point, or nil." (save-excursion (let
> ((max (max (point-min) (- (point) 1600)))) ; approx 20 lines back (when
> (search-backward "{" max t) - (skip-chars-backward " \t\r\n") -
> (beginning-of-line) - (if (looking-at "^[ \t]*\\([^{\r\n]*[^ {\t\r\n]\\)")
> - (match-string-no-properties 1)))))) + (skip-chars-backward " \t\r\n") +
> (beginning-of-line) + (if (looking-at "^[ \t]*\\([^{\r\n]*[^ {\t\r\n]\\)")
> + (match-string-no-properties 1)))))) (provide 'css-mode) ;;; css-mode.el
> ends here \ No newline at end of file
>
>
> On Tue, Jan 15, 2013 at 5:00 AM, Stefan Monnier <[email-masked]>wrote:
>
>> > I've written a new indenter for CSS, which fixes issues with mulitline
>> > statements following a colon, and comments:
>>
>> Interesting, I don't use CSS myself very much, but I've toyed with an
>> SMIE indenter for css-mode (see below).
>>
>> > body {
>> >     color: #333;
>> >     font: 15px "Helvetica Neue",
>> >         Helvetica,
>> >         Arial,
>> >         "Nimbus Sans L",
>> >         sans-serif;
>> >     font-weight: 300;
>> >     line-height: 1.625;
>> >     a { /* It also handles SCSS */
>> >         font: 15px "Helvetica Neue",
>> >             Helvetica,
>> >             Arial,
>> >             "Nimbus Sans L",
>> >             sans-serif;
>> >         /* Some comment */
>> >     }
>> >     /* Some comment at the end of a block */
>> > }
>>
>> I've tried it on the above test case and the SMIE code seems to handle
>> it OK (tho differently), with the main exception being the
>> comment-at-end-of-block, which is probably a general problem which
>> should be fixed in smie.el (probably in smie-indent-comment).
>>
>> But I have basically never used/tested my code, so...
>>
>> > I wondered whether this functionality could be integrated into Emacs.
>> > https://github.com/sabof/es-css-mode
>> > https://github.com/sabof/es-lib (it takes some functions from it)
>>
>> If you could provide a patch against css-mode.el, that would be more
>> convenient.  Also it would be good to add a good set of test cases (in
>> the form of a new file test/indent/css-mode.css).
>>
>>
>>         Stefan
>>
>>
>> === modified file 'lisp/textmodes/css-mode.el'
>> --- lisp/textmodes/css-mode.el  2013-01-02 16:13:04 +0000
>> +++ lisp/textmodes/css-mode.el  2013-01-15 04:53:03 +0000
>> @@ -263,6 +263,22 @@
>>  (defvar css-font-lock-defaults
>>    '(css-font-lock-keywords nil t))
>>
>> +(defcustom css-indent-offset 4
>> +  "Basic size of one indentation step."
>> +  :version "22.2"
>> +  :type 'integer)
>> +
>> +(defconst css-smie-grammar
>> +  (smie-prec2->grammar
>> +   (smie-precs->prec2 '((assoc ";") (left ":") (assoc ",")))))
>> +
>> +(defun css-smie-rules (kind token)
>> +  (pcase (cons kind token)
>> +    (`(:elem . basic) css-indent-offset)
>> +    (`(:elem . arg) 0)
>> +    (`(:before . "{") (if (smie-rule-hanging-p)
>> +                         (smie-rule-parent 0)))))
>> +
>>  ;;;###autoload
>>  (define-derived-mode css-mode fundamental-mode "CSS"
>>    "Major mode to edit Cascading Style Sheets."
>> @@ -271,11 +287,11 @@
>>    (setq-local comment-start-skip "/\\*+[ \t]*")
>>    (setq-local comment-end "*/")
>>    (setq-local comment-end-skip "[ \t]*\\*+/")
>> -  (setq-local forward-sexp-function 'css-forward-sexp)
>>    (setq-local parse-sexp-ignore-comments t)
>>    (setq-local indent-line-function 'css-indent-line)
>>    (setq-local fill-paragraph-function 'css-fill-paragraph)
>>    (setq-local add-log-current-defun-function #'css-current-defun-name)
>> +  (smie-setup css-smie-grammar #'css-smie-rules)
>>    (when css-electric-keys
>>      (let ((fc (make-char-table 'auto-fill-chars)))
>>        (set-char-table-parent fc auto-fill-chars)
>> @@ -355,131 +371,6 @@
>>              ;; Don't use the default filling code.
>>              t)))))))
>>
>> -;;; Navigation and indentation.
>> -
>> -(defconst css-navigation-syntax-table
>> -  (let ((st (make-syntax-table css-mode-syntax-table)))
>> -    (map-char-table (lambda (c v)
>> -                      ;; Turn punctuation (code = 1) into symbol (code =
>> 1).
>> -                      (if (eq (car-safe v) 1)
>> -                          (set-char-table-range st c (cons 3 (cdr v)))))
>> -                    st)
>> -    st))
>> -
>> -(defun css-backward-sexp (n)
>> -  (let ((forward-sexp-function nil))
>> -    (if (< n 0) (css-forward-sexp (- n))
>> -      (while (> n 0)
>> -        (setq n (1- n))
>> -        (forward-comment (- (point-max)))
>> -        (if (not (eq (char-before) ?\;))
>> -            (backward-sexp 1)
>> -          (while (progn (backward-sexp 1)
>> -                        (save-excursion
>> -                          (forward-comment (- (point-max)))
>> -                          ;; FIXME: We should also skip punctuation.
>> -                          (not (or (bobp) (memq (char-before) '(?\;
>> ?\{))))))))))))
>> -
>> -(defun css-forward-sexp (n)
>> -  (let ((forward-sexp-function nil))
>> -    (if (< n 0) (css-backward-sexp (- n))
>> -      (while (> n 0)
>> -        (setq n (1- n))
>> -        (forward-comment (point-max))
>> -        (if (not (eq (char-after) ?\;))
>> -            (forward-sexp 1)
>> -          (while (progn (forward-sexp 1)
>> -                        (save-excursion
>> -                          (forward-comment (point-max))
>> -                          ;; FIXME: We should also skip punctuation.
>> -                          (not (memq (char-after) '(?\; ?\})))))))))))
>> -
>> -(defun css-indent-calculate-virtual ()
>> -  (if (or (save-excursion (skip-chars-backward " \t") (bolp))
>> -          (if (looking-at "\\s(")
>> -              (save-excursion
>> -                (forward-char 1) (skip-chars-forward " \t")
>> -                (not (or (eolp) (looking-at comment-start-skip))))))
>> -      (current-column)
>> -    (css-indent-calculate)))
>> -
>> -(defcustom css-indent-offset 4
>> -  "Basic size of one indentation step."
>> -  :version "22.2"
>> -  :type 'integer
>> -  :group 'css)
>> -
>> -(defun css-indent-calculate ()
>> -  (let ((ppss (syntax-ppss))
>> -        pos)
>> -    (with-syntax-table css-navigation-syntax-table
>> -      (save-excursion
>> -        (cond
>> -         ;; Inside a string.
>> -         ((nth 3 ppss) 'noindent)
>> -         ;; Inside a comment.
>> -         ((nth 4 ppss)
>> -          (setq pos (point))
>> -          (forward-line -1)
>> -          (skip-chars-forward " \t")
>> -          (if (>= (nth 8 ppss) (point))
>> -              (progn
>> -                (goto-char (nth 8 ppss))
>> -                (if (eq (char-after pos) ?*)
>> -                    (forward-char 1)
>> -                  (if (not (looking-at comment-start-skip))
>> -                      (error "Internal css-mode error")
>> -                    (goto-char (match-end 0))))
>> -                (current-column))
>> -            (if (and (eq (char-after pos) ?*) (eq (char-after) ?*))
>> -                (current-column)
>> -              ;; 'noindent
>> -              (current-column)
>> -              )))
>> -         ;; In normal code.
>> -         (t
>> -          (or
>> -           (when (looking-at "\\s)")
>> -             (forward-char 1)
>> -             (backward-sexp 1)
>> -             (css-indent-calculate-virtual))
>> -           (when (looking-at comment-start-skip)
>> -             (forward-comment (point-max))
>> -             (css-indent-calculate))
>> -           (when (save-excursion (forward-comment (- (point-max)))
>> -                                 (setq pos (point))
>> -                                 (eq (char-syntax (preceding-char)) ?\())
>> -             (goto-char (1- pos))
>> -             (if (not (looking-at "\\s([ \t]*"))
>> -                 (error "Internal css-mode error")
>> -               (if (or (memq (char-after (match-end 0)) '(?\n nil))
>> -                       (save-excursion (goto-char (match-end 0))
>> -                                       (looking-at comment-start-skip)))
>> -                   (+ (css-indent-calculate-virtual) css-indent-offset)
>> -                 (progn (goto-char (match-end 0)) (current-column)))))
>> -           (progn
>> -             (css-backward-sexp 1)
>> -             (if (looking-at "\\s(")
>> -                 (css-indent-calculate)
>> -               (css-indent-calculate-virtual))))))))))
>> -
>> -
>> -(defun css-indent-line ()
>> -  "Indent current line according to CSS indentation rules."
>> -  (interactive)
>> -  (let* ((savep (point))
>> -         (forward-sexp-function nil)
>> -        (indent (condition-case nil
>> -                    (save-excursion
>> -                      (forward-line 0)
>> -                      (skip-chars-forward " \t")
>> -                      (if (>= (point) savep) (setq savep nil))
>> -                      (css-indent-calculate))
>> -                  (error nil))))
>> -    (if (not (numberp indent)) 'noindent
>> -      (if savep
>> -          (save-excursion (indent-line-to indent))
>> -        (indent-line-to indent)))))
>>
>>  (defun css-current-defun-name ()
>>    "Return the name of the CSS section at point, or nil."
>>
>>
>