** CSS mode
---
-*** Support for completing attribute values, at-rules, bang-rules, and
-HTML tags using the 'completion-at-point' command.
+*** Support for completing attribute values, at-rules, bang-rules,
+HTML tags, classes and IDs using the 'completion-at-point' command.
+Completion candidates for HTML classes and IDs are retrieved from open
+HTML mode buffers.
+++
** Emacs now supports character name escape sequences in character and
;; - electric ; and }
;; - filling code with auto-fill-mode
;; - fix font-lock errors with multi-line selectors
-;; - support completion of user-defined classes names and IDs
;;; Code:
"Non-nil if nested selectors are allowed in the current mode.")
(make-variable-buffer-local 'css--nested-selectors-allowed)
-;; TODO: Currently only supports completion of HTML tags. By looking
-;; at open HTML mode buffers we should be able to provide completion
-;; of user-defined classes and IDs too.
+(defvar css-class-list-function #'ignore
+ "Called to provide completions of class names.
+This can be bound by buffers that are able to suggest class name
+completions, such as HTML mode buffers.")
+
+(defvar css-id-list-function #'ignore
+ "Called to provide completions of IDs.
+This can be bound by buffers that are able to suggest ID
+completions, such as HTML mode buffers.")
+
+(defun css--foreign-completions (extractor)
+ "Return a list of completions provided by other buffers.
+EXTRACTOR should be the name of a function that may be defined in
+one or more buffers. In each of the buffers where EXTRACTOR is
+defined, EXTRACTOR is called and the results are accumulated into
+a list of completions."
+ (delete-dups
+ (seq-mapcat
+ (lambda (buf)
+ (with-current-buffer buf
+ (funcall (symbol-value extractor))))
+ (buffer-list))))
+
(defun css--complete-selector ()
"Complete part of a CSS selector at point."
(when (or (= (nth 0 (syntax-ppss)) 0) css--nested-selectors-allowed)
- (save-excursion
- (let ((end (point)))
+ (let ((end (point)))
+ (save-excursion
(skip-chars-backward "-[:alnum:]")
- (list (point) end css--html-tags)))))
+ (let ((start-char (char-before)))
+ (list
+ (point) end
+ (completion-table-dynamic
+ (lambda (_)
+ (cond
+ ((eq start-char ?.)
+ (css--foreign-completions 'css-class-list-function))
+ ((eq start-char ?#)
+ (css--foreign-completions 'css-id-list-function))
+ (t css--html-tags))))))))))
(defun css-completion-at-point ()
"Complete current symbol at point.
;;; Code:
+(require 'dom)
+(require 'seq)
+(require 'subr-x)
(eval-when-compile
(require 'skeleton)
(require 'cl-lib))
nil t)
(match-string-no-properties 1))))
+(defvar html--buffer-classes-cache nil
+ "Cache for `html-current-buffer-classes'.
+When set, this should be a cons cell where the CAR is the
+buffer's tick counter (as produced by `buffer-modified-tick'),
+and the CDR is the list of class names found in the buffer.")
+(make-variable-buffer-local 'html--buffer-classes-cache)
+
+(defvar html--buffer-ids-cache nil
+ "Cache for `html-current-buffer-ids'.
+When set, this should be a cons cell where the CAR is the
+buffer's tick counter (as produced by `buffer-modified-tick'),
+and the CDR is the list of class names found in the buffer.")
+(make-variable-buffer-local 'html--buffer-ids-cache)
+
+(defun html-current-buffer-classes ()
+ "Return a list of class names used in the current buffer.
+The result is cached in `html--buffer-classes-cache'."
+ (let ((tick (buffer-modified-tick)))
+ (if (eq (car html--buffer-classes-cache) tick)
+ (cdr html--buffer-classes-cache)
+ (let* ((dom (libxml-parse-html-region (point-min) (point-max)))
+ (classes
+ (seq-mapcat
+ (lambda (el)
+ (when-let (class-list
+ (cdr (assq 'class (dom-attributes el))))
+ (split-string class-list)))
+ (dom-by-class dom ""))))
+ (setq-local html--buffer-classes-cache (cons tick classes))
+ classes))))
+
+(defun html-current-buffer-ids ()
+ "Return a list of IDs used in the current buffer.
+The result is cached in `html--buffer-ids-cache'."
+ (let ((tick (buffer-modified-tick)))
+ (if (eq (car html--buffer-ids-cache) tick)
+ (cdr html--buffer-ids-cache)
+ (let* ((dom
+ (libxml-parse-html-region (point-min) (point-max)))
+ (ids
+ (seq-mapcat
+ (lambda (el)
+ (when-let (id-list
+ (cdr (assq 'id (dom-attributes el))))
+ (split-string id-list)))
+ (dom-by-id dom ""))))
+ (setq-local html--buffer-ids-cache (cons tick ids))
+ ids))))
+
\f
;;;###autoload
(define-derived-mode html-mode sgml-mode '(sgml-xml-mode "XHTML" "HTML")
(setq-local add-log-current-defun-function #'html-current-defun-name)
(setq-local sentence-end-base "[.?!][]\"'”)}]*\\(<[^>]*>\\)*")
+ (when (fboundp 'libxml-parse-html-region)
+ (defvar css-class-list-function)
+ (setq-local css-class-list-function #'html-current-buffer-classes)
+ (defvar css-id-list-function)
+ (setq-local css-id-list-function #'html-current-buffer-ids))
+
(setq imenu-create-index-function 'html-imenu-index)
(setq-local sgml-empty-tags