Subject: bug#13425: new css-mode indenter

From: E Sabof <[email-masked]>
Date: Tue, 15 Jan 2013 19:32:43 +0000

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."
>
>