]> code.delx.au - gnu-emacs/blobdiff - lisp/textmodes/css-mode.el
Add support for completion in `css-mode'
[gnu-emacs] / lisp / textmodes / css-mode.el
index 9c32604b4f41e5b98a1255813ce4a5cd2bb32dc0..555122b1b42091de3be2baada2632c1724454233 100644 (file)
@@ -1,8 +1,9 @@
-;;; css-mode.el --- Major mode to edit CSS files -*- lexical-binding: t -*-
+;;; css-mode.el --- Major mode to edit CSS files  -*- lexical-binding: t -*-
 
-;; Copyright (C) 2006-2013 Free Software Foundation, Inc.
+;; Copyright (C) 2006-2015 Free Software Foundation, Inc.
 
 ;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
+;; Maintainer: Simen Heggestøyl <simenheg@gmail.com>
 ;; Keywords: hypermedia
 
 ;; This file is part of GNU Emacs.
@@ -28,7 +29,7 @@
 
 ;; - electric ; and }
 ;; - filling code with auto-fill-mode
-;; - completion
+;; - attribute value completion
 ;; - fix font-lock errors with multi-line selectors
 
 ;;; Code:
 ;;    (media . "^ +\\* '\\([^ '\n]+\\)' media group")
 ;;    (property . "^ +\\* '\\([^ '\n]+\\)',")))
 
-(defconst css-pseudo-ids
-  '("active" "after" "before" "first" "first-child" "first-letter" "first-line"
-    "focus" "hover" "lang" "left" "link" "right" "visited")
-  "Identifiers for pseudo-elements and pseudo-classes.")
+(defconst css-pseudo-class-ids
+  '("active" "checked" "disabled" "empty" "enabled" "first"
+    "first-child" "first-of-type" "focus" "hover" "indeterminate" "lang"
+    "last-child" "last-of-type" "left" "link" "nth-child"
+    "nth-last-child" "nth-last-of-type" "nth-of-type" "only-child"
+    "only-of-type" "right" "root" "target" "visited")
+  "Identifiers for pseudo-classes.")
+
+(defconst css-pseudo-element-ids
+  '("after" "before" "first-letter" "first-line")
+  "Identifiers for pseudo-elements.")
 
 (defconst css-at-ids
   '("charset" "font-face" "import" "media" "page")
   (let ((st (make-syntax-table)))
     ;; C-style comments.
     (modify-syntax-entry ?/ ". 14" st)
-    (modify-syntax-entry ?* ". 23" st)
+    (modify-syntax-entry ?* ". 23b" st)
     ;; Strings.
     (modify-syntax-entry ?\" "\"" st)
     (modify-syntax-entry ?\' "\"" st)
   "\\\\\\(?:[^\000-\037\177]\\|[0-9a-fA-F]+[ \n\t\r\f]?\\)")
 (defconst css-nmchar-re (concat "\\(?:[-[:alnum:]]\\|" css-escapes-re "\\)"))
 (defconst css-nmstart-re (concat "\\(?:[[:alpha:]]\\|" css-escapes-re "\\)"))
-(defconst css-ident-re (concat css-nmstart-re css-nmchar-re "*"))
+(defconst css-ident-re ;; (concat css-nmstart-re css-nmchar-re "*")
+  ;; Apparently, "at rules" names can start with a dash, e.g. @-moz-keyframes.
+  (concat css-nmchar-re "+"))
 (defconst css-proprietary-nmstart-re ;; Vendor-specific properties.
   (concat "[-_]" (regexp-opt '("ms" "moz" "o" "khtml" "webkit")) "-"))
 (defconst css-name-re (concat css-nmchar-re "+"))
 
+(defconst scss--hash-re "#\\(?:{[$-_[:alnum:]]+}\\|[[:alnum:]]+\\)")
+
 (defface css-selector '((t :inherit font-lock-function-name-face))
   "Face to use for selectors."
   :group 'css)
 (defface css-proprietary-property '((t :inherit (css-property italic)))
   "Face to use for vendor-specific properties.")
 
-(defvar css-font-lock-keywords
-  `(("!\\s-*important" . font-lock-builtin-face)
+(defun css--font-lock-keywords (&optional sassy)
+  `((,(concat "!\\s-*"
+              (regexp-opt (append (if sassy '("global"))
+                                  '("important"))))
+     (0 font-lock-builtin-face))
     ;; Atrules keywords.  IDs not in css-at-ids are valid (ignored).
     ;; In fact the regexp should probably be
     ;; (,(concat "\\(@" css-ident-re "\\)\\([ \t\n][^;{]*\\)[;{]")
     ;;  (1 font-lock-builtin-face))
     ;; Since "An at-rule consists of everything up to and including the next
     ;; semicolon (;) or the next block, whichever comes first."
-    (,(concat "@" css-ident-re) . font-lock-builtin-face)
+    (,(concat "@" css-ident-re) (0 font-lock-builtin-face))
     ;; Selectors.
     ;; FIXME: attribute selectors don't work well because they may contain
     ;; strings which have already been highlighted as f-l-string-face and
     ;; thus prevent this highlighting from being applied (actually now that
-    ;; I use `append' this should work better).  But really the part of hte
+    ;; I use `keep' this should work better).  But really the part of the
     ;; selector between [...] should simply not be highlighted.
-    (,(concat "^\\([ \t]*[^@:{}\n][^:{}]+\\(?::" (regexp-opt css-pseudo-ids t)
-              "\\(?:([^)]+)\\)?[^:{\n]*\\)*\\)\\(?:\n[ \t]*\\)*{")
-     (1 'css-selector append))
+    (,(concat
+       "^[ \t]*\\("
+       (if (not sassy)
+           ;; We don't allow / as first char, so as not to
+           ;; take a comment as the beginning of a selector.
+           "[^@/:{} \t\n][^:{}]+"
+         ;; Same as for non-sassy except we do want to allow { and }
+         ;; chars in selectors in the case of #{$foo}
+         ;; variable interpolation!
+         (concat "\\(?:" scss--hash-re
+                 "\\|[^@/:{} \t\n#]\\)"
+                 "[^:{}#]*\\(?:" scss--hash-re "[^:{}#]*\\)*"))
+       ;; Even though pseudo-elements should be prefixed by ::, a
+       ;; single colon is accepted for backward compatibility.
+       "\\(?:\\(:" (regexp-opt (append css-pseudo-class-ids
+                                       css-pseudo-element-ids) t)
+       "\\|\\::" (regexp-opt css-pseudo-element-ids t) "\\)"
+       "\\(?:([^\)]+)\\)?"
+       (if (not sassy)
+           "[^:{}\n]*"
+         (concat "[^:{}\n#]*\\(?:" scss--hash-re "[^:{}\n#]*\\)*"))
+       "\\)*"
+       "\\)\\(?:\n[ \t]*\\)*{")
+     (1 'css-selector keep))
     ;; In the above rule, we allow the open-brace to be on some subsequent
     ;; line.  This will only work if we properly mark the intervening text
     ;; as being part of a multiline element (and even then, this only
               "\\)\\s-*:")
      (1 (if (match-end 2) 'css-proprietary-property 'css-property)))))
 
+(defvar css-font-lock-keywords (css--font-lock-keywords))
+
 (defvar css-font-lock-defaults
   '(css-font-lock-keywords nil t))
 
 (defun css-smie--forward-token ()
   (cond
    ((and (eq (char-before) ?\})
+         (scss-smie--not-interpolation-p)
          ;; FIXME: If the next char is not whitespace, what should we do?
          (or (memq (char-after) '(?\s ?\t ?\n))
              (looking-at comment-start-skip)))
     (forward-comment (- (point)))
     (cond
      ;; FIXME: If the next char is not whitespace, what should we do?
-     ((and (eq (char-before) ?\}) (> pos (point))) ";")
+     ((and (eq (char-before) ?\}) (scss-smie--not-interpolation-p)
+           (> pos (point))) ";")
      ((memq (char-before) '(?\; ?\, ?\:))
       (forward-char -1) (string (char-after)))
      (t (smie-default-backward-token)))))
   (pcase (cons kind token)
     (`(:elem . basic) css-indent-offset)
     (`(:elem . arg) 0)
-    (`(:before . "{") (if (smie-rule-hanging-p)
-                         (smie-rule-parent 0)))))
+    (`(:list-intro . ,(or `";" `"")) t) ;"" stands for BOB (bug#15467).
+    (`(:before . ,(or "{" "("))
+     (if (smie-rule-hanging-p) (smie-rule-parent 0)))))
+
+;;; Completion
+
+(defun css--complete-property ()
+  "Complete property at point."
+  (save-excursion
+    (let ((pos (point)))
+      (skip-chars-backward "-[:alnum:]")
+      (let ((start (point)))
+        (skip-chars-backward " \t\r\n")
+        (when (memq (char-before) '(?\{ ?\;))
+          (list start pos css-property-ids))))))
+
+(defun css--complete-pseudo-element-or-class ()
+  "Complete pseudo-element or pseudo-class at point."
+  (save-excursion
+    (let ((pos (point)))
+      (skip-chars-backward "-[:alnum:]")
+      (when (eq (char-before) ?\:)
+        (list (point) pos
+              (if (eq (char-before (- (point) 1)) ?\:)
+                  css-pseudo-element-ids
+                css-pseudo-class-ids))))))
+
+(defun css--complete-at-rule ()
+  "Complete at-rule (statement beginning with `@') at point."
+  (save-excursion
+    (let ((pos (point)))
+      (skip-chars-backward "-[:alnum:]")
+      (when (eq (char-before) ?\@)
+        (list (point) pos css-at-ids)))))
+
+(defun css-completion-at-point ()
+  "Complete current symbol at point.
+Currently supports completion of CSS properties, pseudo-elements,
+pesudo-classes, and at-rules."
+  (or (css--complete-property)
+      (css--complete-pseudo-element-or-class)
+      (css--complete-at-rule)))
 
 ;;;###autoload
 (define-derived-mode css-mode fundamental-mode "CSS"
   (setq-local comment-end "*/")
   (setq-local comment-end-skip "[ \t]*\\*+/")
   (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
               :forward-token #'css-smie--forward-token
               :backward-token #'css-smie--backward-token)
-  (when css-electric-keys
-    (let ((fc (make-char-table 'auto-fill-chars)))
-      (set-char-table-parent fc auto-fill-chars)
-      (dolist (c css-electric-keys)
-        (aset fc c 'indent-according-to-mode))
-      (setq-local auto-fill-chars fc))))
+  (setq-local electric-indent-chars
+              (append css-electric-keys electric-indent-chars))
+  (add-hook 'completion-at-point-functions
+            #'css-completion-at-point nil 'local))
 
 (defvar comment-continue)
 
               (cond
                ;; This is a false positive inside a string or comment.
                ((nth 8 (syntax-ppss)) nil)
+               ;; This is a false positive when encountering an
+               ;; interpolated variable (bug#19751).
+               ((eq (char-before (- (point) 1)) ?#) nil)
                ((eq (char-before) ?\})
                 (save-excursion
                   (forward-char -1)
                   (skip-chars-backward " \t")
-                  (unless (bolp) (newline))))
+                  (when (and (not (bolp))
+                             (scss-smie--not-interpolation-p))
+                    (newline))))
                (t
                 (while
                     (progn
        (if (looking-at "^[ \t]*\\([^{\r\n]*[^ {\t\r\n]\\)")
            (match-string-no-properties 1))))))
 
+;;; SCSS mode
+
+(defvar scss-mode-syntax-table
+  (let ((st (make-syntax-table css-mode-syntax-table)))
+    (modify-syntax-entry ?/ ". 124" st)
+    (modify-syntax-entry ?\n ">" st)
+    st))
+
+(defvar scss-font-lock-keywords
+  (append `((,(concat "$" css-ident-re) (0 font-lock-variable-name-face)))
+          (css--font-lock-keywords 'sassy)
+          `((,(concat "@mixin[ \t]+\\(" css-ident-re "\\)[ \t]*(")
+             (1 font-lock-function-name-face)))))
+
+(defun scss-smie--not-interpolation-p ()
+  (save-excursion
+    (forward-char -1)
+    (or (zerop (skip-chars-backward "-[:alnum:]"))
+        (not (looking-back "#{\\$" (- (point) 3))))))
+
+;;;###autoload (add-to-list 'auto-mode-alist '("\\.scss\\'" . scss-mode))
+;;;###autoload
+(define-derived-mode scss-mode css-mode "SCSS"
+  "Major mode to edit \"Sassy CSS\" files."
+  (setq-local comment-start "// ")
+  (setq-local comment-end "")
+  (setq-local comment-start-skip "/[*/]+[ \t]*")
+  (setq-local comment-end-skip "[ \t]*\\(?:\n\\|\\*+/\\)")
+  (setq-local font-lock-defaults '(scss-font-lock-keywords nil t)))
+
 (provide 'css-mode)
 ;;; css-mode.el ends here