Merge remote-tracking branch 'origin/scratch/emacs-editorconfig'

* doc/emacs/custom.texi (EditorConfig support): New node.
* lisp/editorconfig-conf-mode.el, lisp/editorconfig-core-handle.el,
* lisp/editorconfig-core.el, lisp/editorconfig-fnmatch.el,
* lisp/editorconfig-tools.el, lisp/editorconfig.el: New files.
This commit is contained in:
Stefan Monnier 2024-06-21 10:07:09 -04:00
commit 45f6cfb89e
8 changed files with 1724 additions and 0 deletions

View file

@ -1550,6 +1550,44 @@ variables are handled in the same way as unsafe file-local variables
do not visit a file directly but perform work within a directory, such
as Dired buffers (@pxref{Dired}).
@node EditorConfig support
@subsubsection Per-Directory Variables via EditorConfig
@cindex EditorConfig support
The EditorConfig standard is an alternative to the @file{.dir-locals.el}
files, which can control only a very small number of variables, but
has the advantage of being editor-neutral. Those settings are stored in
files named @file{.editorconfig}.
If you want Emacs to obey those settings, you need to enable
the @code{editorconfig-mode} minor mode. This is usually all that is
needed: when the mode is activated, Emacs will look for @file{.editorconfig}
files whenever a file is visited, just as it does for @file{.dir-locals.el}.
When both @file{.editorconfig} and @file{.dir-locals.el} files are
found, their settings are combined, and in case of a conflict, the
setting coming from the closest file takes precedence.
If they are equally close, @file{.dir-locals.el} takes precedence.
In terms of security, those settings are subject to the same checks
as those coming from @file{.dir-locals.el} (and also honor
@code{enable-local-variables}).
The @code{indent_size} setting of the EditorConfig standard does not
correspond to a fixed variable in Emacs, but instead needs to set
different variables depending on the major mode. Ideally all major
modes should set the corresponding @code{editorconfig-indent-size-vars},
but if you use a major mode in which @code{indent_size} does not take
effect because the major mode does not yet support it, you can customize
the @code{editorconfig-indentation-alist} variable to tell Emacs which
variables need to be set in that major mode.
Similarly, there are several different ways to trim whitespace at
the end of lines. When the EditorConfig @code{trim_trailing_whitespace}
setting is used, by default @code{editorconfig-mode} simply calls
@code{delete-trailing-whitespace} every time you save your file.
If you prefer some other behavior, You can customize
@code{editorconfig-trim-whitespaces-mode} to the minor mode of
your preference, such as @code{ws-butler-mode}.
@node Connection Variables
@subsection Per-Connection Local Variables
@cindex local variables, for all remote connections

View file

@ -1984,6 +1984,14 @@ to give to "ping".
* New Modes and Packages in Emacs 30.1
** New package EditorConfig.
This package provides support for the EditorConfig standard,
an editor-neutral way to provide directory local (project-wide) settings.
It is enabled via a new global minor mode 'editorconfig-mode'
which makes Emacs obey the '.editorconfig' files.
There is also a new major mode 'editorconfig-conf-mode'
to edit those configuration files.
+++
** New package Track-Changes.
This library is a layer of abstraction above 'before-change-functions'

View file

@ -0,0 +1,96 @@
;;; editorconfig-conf-mode.el --- Major mode for editing .editorconfig files -*- lexical-binding: t -*-
;; Copyright (C) 2011-2024 Free Software Foundation, Inc.
;; Author: EditorConfig Team <editorconfig@googlegroups.com>
;; See
;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors or
;; https://github.com/editorconfig/editorconfig-emacs/blob/master/CONTRIBUTORS
;; for the list of contributors.
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
;; See the GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Major mode for editing .editorconfig files.
;;; Code:
(require 'conf-mode)
(defvar editorconfig-conf-mode-syntax-table
(let ((table (make-syntax-table conf-unix-mode-syntax-table)))
(modify-syntax-entry ?\; "<" table)
table)
"Syntax table in use in `editorconfig-conf-mode' buffers.")
(defvar editorconfig-conf-mode-abbrev-table nil
"Abbrev table in use in `editorconfig-conf-mode' buffers.")
(define-abbrev-table 'editorconfig-conf-mode-abbrev-table ())
;;;###autoload
(define-derived-mode editorconfig-conf-mode conf-unix-mode "Conf[EditorConfig]"
"Major mode for editing .editorconfig files."
(set-variable 'indent-line-function 'indent-relative)
(let ((key-property-list
'("charset"
"end_of_line"
"file_type_emacs"
"file_type_ext"
"indent_size"
"indent_style"
"insert_final_newline"
"max_line_length"
"root"
"tab_width"
"trim_trailing_whitespace"))
(key-value-list
'("unset"
"true"
"false"
"lf"
"cr"
"crlf"
"space"
"tab"
"latin1"
"utf-8"
"utf-8-bom"
"utf-16be"
"utf-16le"))
(font-lock-value
'(("^[ \t]*\\[\\(.+?\\)\\]" 1 font-lock-type-face)
("^[ \t]*\\(.+?\\)[ \t]*[=:]" 1 font-lock-variable-name-face))))
;; Highlight all key values
(dolist (key-value key-value-list)
(push `(,(format "[=:][ \t]*\\(%s\\)\\([ \t]\\|$\\)" key-value)
1 font-lock-constant-face)
font-lock-value))
;; Highlight all key properties
(dolist (key-property key-property-list)
(push `(,(format "^[ \t]*\\(%s\\)[ \t]*[=:]" key-property)
1 font-lock-builtin-face)
font-lock-value))
(conf-mode-initialize "#" font-lock-value)))
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.editorconfig\\'" . editorconfig-conf-mode))
(provide 'editorconfig-conf-mode)
;;; editorconfig-conf-mode.el ends here

View file

@ -0,0 +1,216 @@
;;; editorconfig-core-handle.el --- Handle Class for EditorConfig File -*- lexical-binding: t -*-
;; Copyright (C) 2011-2024 Free Software Foundation, Inc.
;; Author: EditorConfig Team <editorconfig@googlegroups.com>
;; See
;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors or
;; https://github.com/editorconfig/editorconfig-emacs/blob/master/CONTRIBUTORS
;; for the list of contributors.
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
;; See the GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Handle structures for EditorConfig config file. This library is used
;; internally from editorconfig-core.el .
;;; Code:
(require 'cl-lib)
(require 'editorconfig-fnmatch)
(defvar editorconfig-core-handle--cache-hash
(make-hash-table :test 'equal)
"Hash of EditorConfig filename and its `editorconfig-core-handle' instance.")
(cl-defstruct editorconfig-core-handle-section
"Structure representing one section in a .editorconfig file.
Slots:
`name'
String of section name (glob string).
`props'
Alist of properties: (KEY . VALUE)."
(name nil)
(props nil))
(defun editorconfig-core-handle-section-get-properties (section file)
"Return properties alist when SECTION name match FILE.
FILE should be a relative file name, relative to the directory where
the `.editorconfig' file which has SECTION lives.
If not match, return nil."
(when (editorconfig-core-handle--fnmatch-p
file (editorconfig-core-handle-section-name section))
(editorconfig-core-handle-section-props section)))
(cl-defstruct editorconfig-core-handle
"Structure representing an .editorconfig file.
Slots:
`top-props'
Alist of top properties like ((\"root\" . \"true\"))
`sections'
List of `editorconfig-core-handle-section' structure objects.
`mtime'
Last modified time of .editorconfig file.
`path'
Absolute path to .editorconfig file."
(top-props nil)
(sections nil)
(mtime nil)
(path nil))
(defun editorconfig-core-handle (conf)
"Return EditorConfig handle for CONF, which should be a file path.
If CONF does not exist return nil."
(when (file-readable-p conf)
(let ((cached (gethash conf editorconfig-core-handle--cache-hash))
(mtime (nth 5 (file-attributes conf))))
(if (and cached
(equal (editorconfig-core-handle-mtime cached) mtime))
cached
(let ((parsed (editorconfig-core-handle--parse-file conf)))
(puthash conf parsed editorconfig-core-handle--cache-hash))))))
(defun editorconfig-core-handle-root-p (handle)
"Return non-nil if HANDLE represent root EditorConfig file.
If HANDLE is nil return nil."
(when handle
(string-equal "true"
(downcase (or (cdr (assoc "root"
(editorconfig-core-handle-top-props handle)))
"")))))
(defun editorconfig-core-handle-get-properties (handle file)
"Return list of alist of properties from HANDLE for FILE.
The list returned will be ordered by the lines they appear.
If HANDLE is nil return nil."
(declare (obsolete editorconfig-core-handle-get-properties-hash "0.8.0"))
(when handle
(let* ((dir (file-name-directory (editorconfig-core-handle-path handle)))
(file (file-relative-name file dir)))
(cl-loop for section in (editorconfig-core-handle-sections handle)
for props = (editorconfig-core-handle-section-get-properties
section file)
when props collect (copy-alist props)))))
(defun editorconfig-core-handle-get-properties-hash (handle file)
"Return hash of properties from HANDLE for FILE.
If HANDLE is nil return nil."
(when handle
(let ((hash (make-hash-table))
(file (file-relative-name file (editorconfig-core-handle-path
handle))))
(dolist (section (editorconfig-core-handle-sections handle))
(cl-loop for (key . value) in (editorconfig-core-handle-section-get-properties section file)
do (puthash (intern key) value hash)))
hash)))
(defun editorconfig-core-handle--fnmatch-p (name pattern)
"Return non-nil if NAME match PATTERN.
If pattern has slash, pattern should be relative to DIR.
This function is a fnmatch with a few modification for EditorConfig usage."
(if (string-match-p "/" pattern)
(let ((pattern (replace-regexp-in-string "\\`/" "" pattern)))
(editorconfig-fnmatch-p name pattern))
(editorconfig-fnmatch-p (file-name-nondirectory name) pattern)))
(defun editorconfig-core-handle--parse-file (conf)
"Parse EditorConfig file CONF.
This function returns a `editorconfig-core-handle'.
If CONF is not found return nil."
(when (file-readable-p conf)
(with-temp-buffer
;; NOTE: Use this instead of insert-file-contents-literally to enable
;; code conversion
(insert-file-contents conf)
(goto-char (point-min))
(let ((sections ())
(top-props nil)
;; nil when pattern not appeared yet, "" when pattern is empty ("[]")
(pattern nil)
;; Alist of properties for current PATTERN
(props ())
;; Current line num
(current-line-number 1))
(while (not (eobp))
(skip-chars-forward " \t\f")
(cond
((looking-at "\\(?:[#;].*\\)?$")
nil)
;; Start of section
((looking-at "\\[\\(.*\\)\\][ \t]*$")
(let ((newpattern (match-string 1)))
(when pattern
(push (make-editorconfig-core-handle-section
:name pattern
:props (nreverse props))
sections))
(setq props nil)
(setq pattern newpattern)))
((looking-at "\\([^=: \t]+\\)[ \t]*[=:][ \t]*\\(.*?\\)[ \t]*$")
(let ((key (downcase (match-string 1)))
(value (match-string 2)))
(when (and (< (length key) 51)
(< (length value) 256))
(if pattern
(when (< (length pattern) 4097) ;;FIXME: 4097?
(push `(,key . ,value)
props))
(push `(,key . ,value)
top-props)))))
(t (error "Error while reading config file: %s:%d:\n %s\n"
conf current-line-number
(buffer-substring-no-properties (line-beginning-position)
(line-end-position)))))
(setq current-line-number (1+ current-line-number))
(goto-char (point-min))
(forward-line (1- current-line-number)))
(when pattern
(push (make-editorconfig-core-handle-section
:name pattern
:props (nreverse props))
sections))
(make-editorconfig-core-handle
:top-props (nreverse top-props)
:sections (nreverse sections)
:mtime (nth 5 (file-attributes conf))
:path conf)))))
(provide 'editorconfig-core-handle)
;;; editorconfig-core-handle.el ends here

157
lisp/editorconfig-core.el Normal file
View file

@ -0,0 +1,157 @@
;;; editorconfig-core.el --- EditorConfig Core library -*- lexical-binding: t -*-
;; Copyright (C) 2011-2024 Free Software Foundation, Inc.
;; Author: EditorConfig Team <editorconfig@googlegroups.com>
;; See
;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors or
;; https://github.com/editorconfig/editorconfig-emacs/blob/master/CONTRIBUTORS
;; for the list of contributors.
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This library is one implementation of EditorConfig Core, which parses
;; .editorconfig files and returns properties for given files.
;; This can be used in place of, for example, editorconfig-core-c.
;; Use from EditorConfig Emacs Plugin
;; Emacs plugin (v0.5 or later) can utilize this implementation.
;; By default, the plugin first search for any EditorConfig executable,
;; and fallback to this library if not found.
;; If you always want to use this library, add following lines to your init.el:
;; (setq editorconfig-get-properties-function
;; 'editorconfig-core-get-properties-hash)
;; Functions
;; editorconfig-core-get-properties-hash (&optional file confname confversion)
;; Get EditorConfig properties for FILE.
;; This function is almost same as `editorconfig-core-get-properties', but
;; returns hash object instead.
;;; Code:
(require 'cl-lib)
(require 'editorconfig-core-handle)
(eval-when-compile
(require 'subr-x))
(defun editorconfig-core--get-handles (dir confname &optional result)
"Get list of EditorConfig handlers for DIR from CONFNAME.
In the resulting list, the handle for root config file comes first, and the
nearest comes last.
The list may contains nil when no file was found for directories.
RESULT is used internally and normally should not be used."
(setq dir (expand-file-name dir))
(let ((handle (editorconfig-core-handle (concat (file-name-as-directory dir)
confname)))
(parent (file-name-directory (directory-file-name dir))))
(if (or (string= parent dir)
(and handle (editorconfig-core-handle-root-p handle)))
(cl-remove-if-not #'identity (cons handle result))
(editorconfig-core--get-handles parent
confname
(cons handle result)))))
(defun editorconfig-core-get-nearest-editorconfig (directory)
"Return path to .editorconfig file that is closest to DIRECTORY."
(when-let* ((handle (car (last
(editorconfig-core--get-handles directory
".editorconfig")))))
(editorconfig-core-handle-path handle)))
(defun editorconfig-core--hash-merge (into update)
"Merge two hashes INTO and UPDATE.
This is a destructive function, hash INTO will be modified.
When the same key exists in both two hashes, values of UPDATE takes precedence."
(maphash (lambda (key value) (puthash key value into)) update)
into)
(defun editorconfig-core-get-properties-hash (&optional file confname confversion)
"Get EditorConfig properties for FILE.
If FILE is not given, use currently visiting file.
Give CONFNAME for basename of config file other than .editorconfig.
If need to specify config format version, give CONFVERSION.
This function is almost same as `editorconfig-core-get-properties', but returns
hash object instead."
(setq file
(expand-file-name (or file
buffer-file-name
(error "FILE is not given and `buffer-file-name' is nil"))))
(setq confname (or confname ".editorconfig"))
(setq confversion (or confversion "0.12.0"))
(let ((result (make-hash-table)))
(dolist (handle (editorconfig-core--get-handles (file-name-directory file)
confname))
(editorconfig-core--hash-merge result
(editorconfig-core-handle-get-properties-hash handle
file)))
;; Downcase known boolean values
;; FIXME: Why not do that in `editorconfig-core-handle--parse-file'?
(dolist (key '( end_of_line indent_style indent_size insert_final_newline
trim_trailing_whitespace charset))
(when-let* ((val (gethash key result)))
(puthash key (downcase val) result)))
;; Add indent_size property
;; FIXME: Why? Which part of the spec requires that?
;;(let ((v-indent-size (gethash 'indent_size result))
;; (v-indent-style (gethash 'indent_style result)))
;; (when (and (not v-indent-size)
;; (string= v-indent-style "tab")
;; ;; If VERSION < 0.9.0, indent_size should have no default value
;; (version<= "0.9.0"
;; confversion))
;; (puthash 'indent_size
;; "tab"
;; result)))
;; Add tab_width property
;; FIXME: Why? Which part of the spec requires that?
;;(let ((v-indent-size (gethash 'indent_size result))
;; (v-tab-width (gethash 'tab_width result)))
;; (when (and v-indent-size
;; (not v-tab-width)
;; (not (string= v-indent-size "tab")))
;; (puthash 'tab_width v-indent-size result)))
;; Update indent-size property
;; FIXME: Why? Which part of the spec requires that?
;;(let ((v-indent-size (gethash 'indent_size result))
;; (v-tab-width (gethash 'tab_width result)))
;; (when (and v-indent-size
;; v-tab-width
;; (string= v-indent-size "tab"))
;; (puthash 'indent_size v-tab-width result)))
result))
(provide 'editorconfig-core)
;;; editorconfig-core.el ends here

View file

@ -0,0 +1,293 @@
;;; editorconfig-fnmatch.el --- Glob pattern matching -*- lexical-binding: t -*-
;; Copyright (C) 2011-2024 Free Software Foundation, Inc.
;; Author: EditorConfig Team <editorconfig@googlegroups.com>
;; See
;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors or
;; https://github.com/editorconfig/editorconfig-emacs/blob/master/CONTRIBUTORS
;; for the list of contributors.
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
;; See the GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; editorconfig-fnmatch.el provides a fnmatch implementation with a few
;; extensions.
;; The main usage of this library is glob pattern matching for EditorConfig, but
;; it can also act solely.
;; editorconfig-fnmatch-p (name pattern)
;; Test whether NAME match PATTERN.
;; PATTERN should be a shell glob pattern, and some zsh-like wildcard matchings
;; can be used:
;; * Matches any string of characters, except path separators (/)
;; ** Matches any string of characters
;; ? Matches any single character
;; [name] Matches any single character in name
;; [^name] Matches any single character not in name
;; {s1,s2,s3} Matches any of the strings given (separated by commas)
;; {min..max} Matches any number between min and max
;; This library is a port from editorconfig-core-py library.
;; https://github.com/editorconfig/editorconfig-core-py/blob/master/editorconfig/fnmatch.py
;;; Code:
(require 'cl-lib)
(defvar editorconfig-fnmatch--cache-hashtable
nil
"Cache of shell pattern and its translation.")
;; Clear cache on file reload
(setq editorconfig-fnmatch--cache-hashtable
(make-hash-table :test 'equal))
(defconst editorconfig-fnmatch--left-brace-regexp
"\\(^\\|[^\\]\\){"
"Regular expression for left brace ({).")
(defconst editorconfig-fnmatch--right-brace-regexp
"\\(^\\|[^\\]\\)}"
"Regular expression for right brace (}).")
(defconst editorconfig-fnmatch--numeric-range-regexp
"\\([+-]?[0-9]+\\)\\.\\.\\([+-]?[0-9]+\\)"
"Regular expression for numeric range (like {-3..+3}).")
(defun editorconfig-fnmatch--match-num (regexp string)
"Return how many times REGEXP is found in STRING."
(let ((num 0))
;; START arg does not work as expected in this case
(while (string-match regexp string)
(setq num (1+ num)
string (substring string (match-end 0))))
num))
(defun editorconfig-fnmatch-p (string pattern)
"Test whether STRING match PATTERN.
Matching ignores case if `case-fold-search' is non-nil.
PATTERN should be a shell glob pattern, and some zsh-like wildcard matchings can
be used:
* Matches any string of characters, except path separators (/)
** Matches any string of characters
? Matches any single character
[name] Matches any single character in name
[^name] Matches any single character not in name
{s1,s2,s3} Matches any of the strings given (separated by commas)
{min..max} Matches any number between min and max"
(string-match (editorconfig-fnmatch-translate pattern)
string))
;;(editorconfig-fnmatch-translate "{a,{-3..3}}.js")
;;(editorconfig-fnmatch-p "1.js" "{a,{-3..3}}.js")
(defun editorconfig-fnmatch-translate (pattern)
"Translate a shell PATTERN to a regular expression.
Translation result will be cached, so same translation will not be done twice."
(let ((cached (gethash pattern
editorconfig-fnmatch--cache-hashtable)))
(or cached
(puthash pattern
(editorconfig-fnmatch--do-translate pattern)
editorconfig-fnmatch--cache-hashtable))))
(defun editorconfig-fnmatch--do-translate (pattern &optional nested)
"Translate a shell PATTERN to a regular expression.
Set NESTED to t when this function is called from itself.
This function is called from `editorconfig-fnmatch-translate', when no cached
translation is found for PATTERN."
(let ((index 0)
(length (length pattern))
(brace-level 0)
(in-brackets nil)
;; List of strings of resulting regexp, in reverse order.
(result ())
(is-escaped nil)
(matching-braces (= (editorconfig-fnmatch--match-num
editorconfig-fnmatch--left-brace-regexp
pattern)
(editorconfig-fnmatch--match-num
editorconfig-fnmatch--right-brace-regexp
pattern)))
current-char
pos
has-slash
has-comma
num-range)
(while (< index length)
(if (and (not is-escaped)
(string-match "[^]\\*?[{},/\\-]+"
;;(string-match "[^]\\*?[{},/\\-]+" "?.a")
pattern
index)
(eq index (match-beginning 0)))
(progn
(push (regexp-quote (match-string 0 pattern)) result)
(setq index (match-end 0)
is-escaped nil))
(setq current-char (aref pattern index)
index (1+ index))
(cl-case current-char
(?*
(setq pos index)
(if (and (< pos length)
(= (aref pattern pos) ?*))
(push ".*" result)
(push "[^/]*" result)))
(??
(push "[^/]" result))
(?\[
(if in-brackets
(push "\\[" result)
(if (= (aref pattern index) ?/)
;; Slash after an half-open bracket
(progn
(push "\\[/" result)
(setq index (+ index 1)))
(setq pos index
has-slash nil)
(while (and (< pos length)
(not (= (aref pattern pos) ?\]))
(not has-slash))
(if (and (= (aref pattern pos) ?/)
(not (= (aref pattern (- pos 1)) ?\\)))
(setq has-slash t)
(setq pos (1+ pos))))
(if has-slash
(progn
(push (concat "\\["
(substring pattern
index
(1+ pos))
"\\]")
result)
(setq index (+ pos 2)))
(if (and (< index length)
(memq (aref pattern index)
'(?! ?^)))
(progn
(setq index (1+ index))
(push "[^" result))
(push "[" result))
(setq in-brackets t)))))
(?-
(if in-brackets
(push "-" result)
(push "\\-" result)))
(?\]
(push "]" result)
(setq in-brackets nil))
(?{
(setq pos index
has-comma nil)
(while (and (or (and (< pos length)
(not (= (aref pattern pos) ?})))
is-escaped)
(not has-comma))
(if (and (eq (aref pattern pos) ?,)
(not is-escaped))
(setq has-comma t)
(setq is-escaped (and (eq (aref pattern pos)
?\\)
(not is-escaped))
pos (1+ pos))))
(if (and (not has-comma)
(< pos length))
(let ((pattern-sub (substring pattern index pos)))
(setq num-range (string-match editorconfig-fnmatch--numeric-range-regexp
pattern-sub))
(if num-range
(let ((number-start (string-to-number (match-string 1
pattern-sub)))
(number-end (string-to-number (match-string 2
pattern-sub))))
(push (concat "\\(?:"
(mapconcat #'number-to-string
(cl-loop for i from number-start to number-end
collect i)
"\\|")
"\\)")
result))
(let ((inner (editorconfig-fnmatch--do-translate pattern-sub t)))
(push (format "{%s}" inner) result)))
(setq index (1+ pos)))
(if matching-braces
(progn
(push "\\(?:" result)
(setq brace-level (1+ brace-level)))
(push "{" result))))
(?,
(if (and (> brace-level 0)
(not is-escaped))
(push "\\|" result)
(push "\\," result)))
(?}
(if (and (> brace-level 0)
(not is-escaped))
(progn
(push "\\)" result)
(setq brace-level (- brace-level 1)))
(push "}" result)))
(?/
(if (and (<= (+ index 3) (length pattern))
(string= (substring pattern index (+ index 3)) "**/"))
(progn
(push "\\(?:/\\|/.*/\\)" result)
(setq index (+ index 3)))
(push "/" result)))
(t
(unless (= current-char ?\\)
(push (regexp-quote (char-to-string current-char)) result))))
(if (= current-char ?\\)
(progn (when is-escaped
(push "\\\\" result))
(setq is-escaped (not is-escaped)))
(setq is-escaped nil))))
(unless nested
(setq result `("\\'" ,@result "\\`")))
(apply #'concat (reverse result))))
(provide 'editorconfig-fnmatch)
;;; editorconfig-fnmatch.el ends here

120
lisp/editorconfig-tools.el Normal file
View file

@ -0,0 +1,120 @@
;;; editorconfig-tools.el --- Editorconfig tools -*- lexical-binding: t -*-
;; Copyright (C) 2011-2024 Free Software Foundation, Inc.
;; Author: EditorConfig Team <editorconfig@googlegroups.com>
;; See
;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors or
;; https://github.com/editorconfig/editorconfig-emacs/blob/master/CONTRIBUTORS
;; for the list of contributors.
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
;; See the GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Some utility commands for users, not used from editorconfig-mode.
;;; Code:
(require 'cl-lib)
(eval-when-compile
(require 'subr-x))
(require 'editorconfig)
;;;###autoload
(defun editorconfig-apply ()
"Get and apply EditorConfig properties to current buffer.
This function does not respect the values of `editorconfig-exclude-modes' and
`editorconfig-exclude-regexps' and always applies available properties.
Use `editorconfig-mode-apply' instead to make use of these variables."
(interactive)
(when buffer-file-name
(condition-case err
(progn
(let ((props (editorconfig-call-get-properties-function buffer-file-name)))
(condition-case err
(run-hook-with-args 'editorconfig-hack-properties-functions props)
(error
(display-warning '(editorconfig editorconfig-hack-properties-functions)
(format "Error while running editorconfig-hack-properties-functions, abort running hook: %S"
err)
:warning)))
(setq editorconfig-properties-hash props)
(editorconfig-set-local-variables props)
(editorconfig-set-coding-system-revert
(gethash 'end_of_line props)
(gethash 'charset props))
(condition-case err
(run-hook-with-args 'editorconfig-after-apply-functions props)
(error
(display-warning '(editorconfig editorconfig-after-apply-functions)
(format "Error while running editorconfig-after-apply-functions, abort running hook: %S"
err)
:warning)))))
(error
(display-warning '(editorconfig editorconfig-apply)
(format "Error in editorconfig-apply, styles will not be applied: %S" err)
:error)))))
(defun editorconfig-mode-apply ()
"Get and apply EditorConfig properties to current buffer.
This function does nothing when the major mode is listed in
`editorconfig-exclude-modes', or variable `buffer-file-name' matches
any of regexps in `editorconfig-exclude-regexps'."
(interactive)
(when (and major-mode buffer-file-name)
(editorconfig-apply)))
;;;###autoload
(defun editorconfig-find-current-editorconfig ()
"Find the closest .editorconfig file for current file."
(interactive)
(eval-and-compile (require 'editorconfig-core))
(when-let* ((file (editorconfig-core-get-nearest-editorconfig
default-directory)))
(find-file file)))
;;;###autoload
(defun editorconfig-display-current-properties ()
"Display EditorConfig properties extracted for current buffer."
(interactive)
(if editorconfig-properties-hash
(let ((buf (get-buffer-create "*EditorConfig Properties*"))
(file buffer-file-name)
(props editorconfig-properties-hash))
(with-current-buffer buf
(erase-buffer)
(insert (format "# EditorConfig for %s\n" file))
(maphash (lambda (k v)
(insert (format "%S = %s\n" k v)))
props))
(display-buffer buf))
(message "Properties are not applied to current buffer yet.")
nil))
;;;###autoload
(defalias 'describe-editorconfig-properties
#'editorconfig-display-current-properties)
(provide 'editorconfig-tools)
;;; editorconfig-tools.el ends here

796
lisp/editorconfig.el Normal file
View file

@ -0,0 +1,796 @@
;;; editorconfig.el --- EditorConfig Plugin -*- lexical-binding: t -*-
;; Copyright (C) 2011-2024 Free Software Foundation, Inc.
;; Author: EditorConfig Team <editorconfig@googlegroups.com>
;; Version: 0.11.0
;; URL: https://github.com/editorconfig/editorconfig-emacs#readme
;; Package-Requires: ((emacs "26.1"))
;; Keywords: convenience editorconfig
;; See
;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors or
;; https://github.com/editorconfig/editorconfig-emacs/blob/master/CONTRIBUTORS
;; for the list of contributors.
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
;; See the GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; EditorConfig helps developers define and maintain consistent
;; coding styles between different editors and IDEs.
;; The EditorConfig project consists of a file format for defining
;; coding styles and a collection of text editor plugins that enable
;; editors to read the file format and adhere to defined styles.
;; EditorConfig files are easily readable and they work nicely with
;; version control systems.
;;; News:
;; - In `editorconfig-indentation-alist', if a mode is associated to a function
;; that function should not set the vars but should instead *return* them.
;; - New var `editorconfig-indent-size-vars' for major modes to set.
;; - New hook `editorconfig-get-local-variables-functions' to support
;; additional settings.
;;; Code:
(require 'cl-lib)
(eval-when-compile (require 'subr-x))
(require 'editorconfig-core)
(defgroup editorconfig nil
"EditorConfig Emacs Plugin.
EditorConfig helps developers define and maintain consistent
coding styles between different editors and IDEs."
:tag "EditorConfig"
:prefix "editorconfig-"
:group 'tools)
(when (< emacs-major-version 30)
(define-obsolete-variable-alias
'edconf-custom-hooks
'editorconfig-after-apply-functions
"0.5")
(define-obsolete-variable-alias
'editorconfig-custom-hooks
'editorconfig-after-apply-functions
"0.7.14")
(defcustom editorconfig-after-apply-functions ()
"A list of functions after loading common EditorConfig settings.
Each element in this list is a hook function. This hook function
takes one parameter, which is a property hash table. The value
of properties can be obtained through gethash function.
The hook does not have to be coding style related; you can add
whatever functionality you want. For example, the following is
an example to add a new property emacs_linum to decide whether to
show line numbers on the left:
(add-hook \\='editorconfig-after-apply-functions
\\='(lambda (props)
(let ((show-line-num (gethash \\='emacs_linum props)))
(cond ((equal show-line-num \"true\") (linum-mode 1))
((equal show-line-num \"false\") (linum-mode 0))))))
This hook will be run even when there are no matching sections in
\".editorconfig\", or no \".editorconfig\" file was found at all."
:type 'hook))
(when (< emacs-major-version 30)
(defcustom editorconfig-hack-properties-functions ()
"A list of function to alter property values before applying them.
These functions will be run after loading \".editorconfig\" files and before
applying them to current buffer, so that you can alter some properties from
\".editorconfig\" before they take effect.
\(Since 2021/08/30 (v0.9.0): Buffer coding-systems are set before running
this functions, so this variable cannot be used to change coding-systems.)
For example, Makefiles always use tab characters for indentation: you can
overwrite \"indent_style\" property when current `major-mode' is a
`makefile-mode' with following code:
(add-hook \\='editorconfig-hack-properties-functions
\\='(lambda (props)
(when (derived-mode-p \\='makefile-mode)
(puthash \\='indent_style \"tab\" props))))
This hook will be run even when there are no matching sections in
\".editorconfig\", or no \".editorconfig\" file was found at all."
:type 'hook))
(make-obsolete-variable 'editorconfig-hack-properties-functions
'editorconfig-get-local-variables-functions
"2024")
(define-obsolete-variable-alias
'edconf-indentation-alist
'editorconfig-indentation-alist
"0.5")
(defcustom editorconfig-indentation-alist
;; For contributors: Sort modes in alphabetical order
'((apache-mode apache-indent-level)
(awk-mode c-basic-offset)
(bash-ts-mode sh-basic-offset)
(bpftrace-mode c-basic-offset)
(c++-mode c-basic-offset)
(c++-ts-mode c-basic-offset
c-ts-mode-indent-offset)
(c-mode c-basic-offset)
(c-ts-mode c-basic-offset
c-ts-mode-indent-offset)
(cmake-mode cmake-tab-width)
(cmake-ts-mode cmake-tab-width
cmake-ts-mode-indent-offset)
(coffee-mode coffee-tab-width)
(cperl-mode cperl-indent-level)
(crystal-mode crystal-indent-level)
(csharp-mode c-basic-offset)
(csharp-ts-mode c-basic-offset
csharp-ts-mode-indent-offset)
(css-mode css-indent-offset)
(css-ts-mode css-indent-offset)
(d-mode c-basic-offset)
(elixir-ts-mode elixir-ts-indent-offset)
(emacs-lisp-mode . editorconfig--get-indentation-lisp-mode)
(enh-ruby-mode enh-ruby-indent-level)
(erlang-mode erlang-indent-level)
(ess-mode ess-indent-offset)
(f90-mode f90-associate-indent
f90-continuation-indent
f90-critical-indent
f90-do-indent
f90-if-indent
f90-program-indent
f90-type-indent)
(feature-mode feature-indent-offset
feature-indent-level)
(fsharp-mode fsharp-continuation-offset
fsharp-indent-level
fsharp-indent-offset)
(gdscript-mode gdscript-indent-offset)
(go-ts-mode go-ts-mode-indent-offset)
(graphql-mode graphql-indent-level)
(groovy-mode groovy-indent-offset)
(haskell-mode haskell-indent-spaces
haskell-indent-offset
haskell-indentation-layout-offset
haskell-indentation-left-offset
haskell-indentation-starter-offset
haskell-indentation-where-post-offset
haskell-indentation-where-pre-offset
shm-indent-spaces)
(haxor-mode haxor-tab-width)
(hcl-mode hcl-indent-level)
(html-ts-mode html-ts-mode-indent-offset)
(idl-mode c-basic-offset)
(jade-mode jade-tab-width)
(java-mode c-basic-offset)
(java-ts-mode c-basic-offset
java-ts-mode-indent-offset)
(js-mode js-indent-level)
(js-ts-mode js-indent-level)
(js-jsx-mode js-indent-level sgml-basic-offset)
(js2-mode js2-basic-offset)
(js2-jsx-mode js2-basic-offset sgml-basic-offset)
(js3-mode js3-indent-level)
(json-mode js-indent-level)
(json-ts-mode json-ts-mode-indent-offset)
(jsonian-mode jsonian-default-indentation)
(julia-mode julia-indent-offset)
(kotlin-mode kotlin-tab-width)
(kotlin-ts-mode kotlin-ts-mode-indent-offset)
(latex-mode . editorconfig--get-indentation-latex-mode)
(lisp-mode . editorconfig--get-indentation-lisp-mode)
(livescript-mode livescript-tab-width)
(lua-mode lua-indent-level)
(lua-ts-mode lua-ts-indent-offset)
(magik-mode magik-indent-level)
(magik-ts-mode magik-indent-level)
(matlab-mode matlab-indent-level)
(meson-mode meson-indent-basic)
(mips-mode mips-tab-width)
(mustache-mode mustache-basic-offset)
(nasm-mode nasm-basic-offset)
(nginx-mode nginx-indent-level)
(nxml-mode nxml-child-indent (nxml-attribute-indent . 2))
(objc-mode c-basic-offset)
(octave-mode octave-block-offset)
(perl-mode perl-indent-level)
;; No need to change `php-mode-coding-style' value for php-mode
;; since we run editorconfig later than it resets `c-basic-offset'.
;; See https://github.com/editorconfig/editorconfig-emacs/issues/116
;; for details.
(php-mode c-basic-offset)
(php-ts-mode php-ts-mode-indent-offset)
(pike-mode c-basic-offset)
(protobuf-mode c-basic-offset)
(ps-mode ps-mode-tab)
(pug-mode pug-tab-width)
(puppet-mode puppet-indent-level)
(python-mode . editorconfig-set-indentation-python-mode)
(python-ts-mode . editorconfig-set-indentation-python-mode)
(rjsx-mode js-indent-level sgml-basic-offset)
(ruby-mode ruby-indent-level)
(ruby-ts-mode ruby-indent-level)
(rust-mode rust-indent-offset)
(rust-ts-mode rust-indent-offset
rust-ts-mode-indent-offset)
(rustic-mode rustic-indent-offset)
(scala-mode scala-indent:step)
(scss-mode css-indent-offset)
(sgml-mode sgml-basic-offset)
(sh-mode sh-basic-offset)
(slim-mode slim-indent-offset)
(sml-mode sml-indent-level)
(svelte-mode svelte-basic-offset)
(swift-mode swift-mode:basic-offset)
(terra-mode terra-indent-level)
(tcl-mode tcl-indent-level
tcl-continued-indent-level)
(toml-ts-mode toml-ts-mode-indent-offset)
(typescript-mode typescript-indent-level)
(typescript-ts-base-mode typescript-ts-mode-indent-offset)
(verilog-mode verilog-indent-level
verilog-indent-level-behavioral
verilog-indent-level-declaration
verilog-indent-level-module
verilog-cexp-indent
verilog-case-indent)
(web-mode . editorconfig--get-indentation-web-mode)
(yaml-mode yaml-indent-offset)
(yaml-ts-mode yaml-indent-offset)
(zig-mode zig-indent-offset)
)
"Alist of indentation setting methods by modes.
This is a fallback used for those modes which don't set
`editorconfig-indent-size-vars'.
Each element should look like (MODE . SETTING) where SETTING
should obey the same rules as `editorconfig-indent-size-vars'."
:type '(alist :key-type symbol
:value-type (choice function (repeat symbol)))
:risky t)
(defcustom editorconfig-trim-whitespaces-mode nil
"Buffer local minor-mode to use to trim trailing whitespaces.
If set, enable that mode when `trim_trailing_whitespace` is set to true.
Otherwise, use `delete-trailing-whitespace'."
:type 'symbol)
(defvar editorconfig-properties-hash nil
"Hash object of EditorConfig properties that was enabled for current buffer.
Set by `editorconfig-apply' and nil if that is not invoked in
current buffer yet.")
(make-variable-buffer-local 'editorconfig-properties-hash)
(put 'editorconfig-properties-hash
'permanent-local
t)
(defvar editorconfig-lisp-use-default-indent nil
"Selectively ignore the value of indent_size for Lisp files.
Prevents `lisp-indent-offset' from being set selectively.
nil - `lisp-indent-offset' is always set normally.
t - `lisp-indent-offset' is never set normally
(always use default indent for lisps).
number - `lisp-indent-offset' is not set only if indent_size is
equal to this number. For example, if this is set to 2,
`lisp-indent-offset' will not be set only if indent_size is 2.")
(define-error 'editorconfig-error
"Error thrown from editorconfig lib")
(defun editorconfig-error (&rest args)
"Signal an `editorconfig-error'.
Make a message by passing ARGS to `format-message'."
(signal 'editorconfig-error (list (apply #'format-message args))))
(defun editorconfig-string-integer-p (string)
"Return non-nil if STRING represents integer."
(and (stringp string)
(string-match-p "\\`[0-9]+\\'" string)))
(defun editorconfig--get-indentation-web-mode (size)
`((web-mode-indent-style . 2)
(web-mode-attr-indent-offset . ,size)
(web-mode-attr-value-indent-offset . ,size)
(web-mode-code-indent-offset . ,size)
(web-mode-css-indent-offset . ,size)
(web-mode-markup-indent-offset . ,size)
(web-mode-sql-indent-offset . ,size)
(web-mode-block-padding . ,size)
(web-mode-script-padding . ,size)
(web-mode-style-padding . ,size)))
(defun editorconfig--get-indentation-latex-mode (size)
"Vars to set `latex-mode' indent size to SIZE."
`((tex-indent-basic . ,size)
(tex-indent-item . ,size)
(tex-indent-arg . ,(* 2 size))
;; For AUCTeX
(TeX-brace-indent-level . ,size)
(LaTeX-indent-level . ,size)
(LaTeX-item-indent . ,(- size))))
(defun editorconfig--get-indentation-lisp-mode (size)
"Set indent size to SIZE for Lisp mode(s)."
(when (cond ((null editorconfig-lisp-use-default-indent) t)
((eql t editorconfig-lisp-use-default-indent) nil)
((numberp editorconfig-lisp-use-default-indent)
(not (eql size editorconfig-lisp-use-default-indent)))
(t t))
`((lisp-indent-offset . ,size))))
(cl-defun editorconfig--should-set (symbol)
"Determine if editorconfig should set SYMBOL."
(display-warning '(editorconfig editorconfig--should-set)
(format "symbol: %S"
symbol)
:debug)
(when (assq symbol file-local-variables-alist)
(cl-return-from editorconfig--should-set
nil))
(when (assq symbol dir-local-variables-alist)
(cl-return-from editorconfig--should-set
nil))
t)
(defvar editorconfig-indent-size-vars
#'editorconfig--default-indent-size-function
"Rule to use to set a given `indent_size'.
Can take the form of a function, in which case we call it with a single SIZE
argument (an integer) and it should return a list of (VAR . VAL) pairs.
Otherwise it can be a list of symbols (those which should be set to SIZE).
Major modes are expected to set this buffer-locally.")
(defun editorconfig--default-indent-size-function (size)
(let ((parents (if (fboundp 'derived-mode-all-parents) ;Emacs-30
(derived-mode-all-parents major-mode)
(let ((modes nil)
(mode major-mode))
(while mode
(push mode modes)
(setq mode (get mode 'derived-mode--parent)))
(nreverse modes))))
entry)
(let ((parents parents))
(while (and parents (not entry))
(setq entry (assq (pop parents) editorconfig-indentation-alist))))
(or
(when entry
(let ((rule (cdr entry)))
;; Filter out settings of unknown vars.
(delq nil
(mapcar (lambda (elem)
(let ((v (car elem)))
(cond
((not (symbolp v))
(message "Unsupported element in `editorconfig-indentation-alist': %S" elem))
((or (eq 'eval v) (boundp v)) elem))))
(if (functionp rule)
(funcall rule size)
(mapcar (lambda (elem) `(,elem . ,size)) rule))))))
;; Fallback, let's try and guess.
(let ((suffixes '("-indent-level" "-basic-offset" "-indent-offset"
"-block-offset"))
(guess ()))
(while (and parents (not guess))
(let* ((mode (pop parents))
(modename (symbol-name mode))
(name (substring modename 0
(string-match "-mode\\'" modename))))
(dolist (suffix suffixes)
(let ((sym (intern-soft (concat name suffix))))
(when (and sym (boundp sym))
(setq guess sym))))))
(when guess `((,guess . ,size))))
(and (local-variable-p 'smie-rules-function)
`((smie-indent-basic . ,size))))))
(defun editorconfig--get-indentation (props)
"Get indentation vars according to STYLE, SIZE, and TAB_WIDTH."
(let ((style (gethash 'indent_style props))
(size (gethash 'indent_size props))
(tab_width (gethash 'tab_width props)))
(when tab_width
(setq tab_width (string-to-number tab_width)))
(setq size
(cond ((editorconfig-string-integer-p size)
(string-to-number size))
((equal size "tab")
(or tab_width tab-width))
(t
nil)))
`(,@(if tab_width `((tab-width . ,tab_width)))
,@(pcase style
("space" `((indent-tabs-mode . nil)))
("tab" `((indent-tabs-mode . t))))
,@(when (and size (featurep 'evil))
`((evil-shift-width . ,size)))
,@(cond
((null size) nil)
((functionp editorconfig-indent-size-vars)
(funcall editorconfig-indent-size-vars size))
(t (mapcar (lambda (v) `(,v . ,size)) editorconfig-indent-size-vars))))))
(defvar-local editorconfig--apply-coding-system-currently nil
"Used internally.")
(put 'editorconfig--apply-coding-system-currently
'permanent-local
t)
(defun editorconfig-merge-coding-systems (end-of-line charset)
"Return merged coding system symbol of END-OF-LINE and CHARSET."
(let ((eol (cond
((equal end-of-line "lf") 'undecided-unix)
((equal end-of-line "cr") 'undecided-mac)
((equal end-of-line "crlf") 'undecided-dos)))
(cs (cond
((equal charset "latin1") 'iso-latin-1)
((equal charset "utf-8") 'utf-8)
((equal charset "utf-8-bom") 'utf-8-with-signature)
((equal charset "utf-16be") 'utf-16be-with-signature)
((equal charset "utf-16le") 'utf-16le-with-signature))))
(if (and eol cs)
(merge-coding-systems cs eol)
(or eol cs))))
(cl-defun editorconfig-set-coding-system-revert (end-of-line charset)
"Set buffer coding system by END-OF-LINE and CHARSET.
This function will revert buffer when the coding-system has been changed."
;; `editorconfig--advice-find-file-noselect' does not use this function
(let ((coding-system (editorconfig-merge-coding-systems end-of-line
charset)))
(display-warning '(editorconfig editorconfig-set-coding-system-revert)
(format "editorconfig-set-coding-system-revert: buffer-file-name: %S | buffer-file-coding-system: %S | coding-system: %S | apply-currently: %S"
buffer-file-name
buffer-file-coding-system
coding-system
editorconfig--apply-coding-system-currently)
:debug)
(when (memq coding-system '(nil undecided))
(cl-return-from editorconfig-set-coding-system-revert))
(when (and buffer-file-coding-system
(memq buffer-file-coding-system
(coding-system-aliases (merge-coding-systems coding-system
buffer-file-coding-system))))
(cl-return-from editorconfig-set-coding-system-revert))
(unless (file-readable-p buffer-file-name)
(set-buffer-file-coding-system coding-system)
(cl-return-from editorconfig-set-coding-system-revert))
(unless (memq coding-system
(coding-system-aliases editorconfig--apply-coding-system-currently))
;; Revert functions might call `editorconfig-apply' again
;; FIXME: I suspect `editorconfig--apply-coding-system-currently'
;; gymnastics is not needed now that we hook into `find-auto-coding'.
(unwind-protect
(progn
(setq editorconfig--apply-coding-system-currently coding-system)
;; Revert without query if buffer is not modified
(let ((revert-without-query '(".")))
(revert-buffer-with-coding-system coding-system)))
(setq editorconfig--apply-coding-system-currently nil)))))
(defun editorconfig--get-trailing-nl (props)
"Get the vars to require final newline according to PROPS."
(pcase (gethash 'insert_final_newline props)
("true"
;; Keep prefs around how/when the nl is added, if set.
`((require-final-newline
. ,(or require-final-newline mode-require-final-newline t))))
("false"
`((require-final-newline . nil)))))
(defun editorconfig--delete-trailing-whitespace ()
"Call `delete-trailing-whitespace' unless the buffer is read-only."
(unless buffer-read-only (delete-trailing-whitespace)))
;; Arrange for our (eval . (add-hook ...)) "local var" to be considered safe.
(defun editorconfig--add-hook-safe-p (exp)
(equal exp '(add-hook 'before-save-hook
#'editorconfig--delete-trailing-whitespace nil t)))
(let ((predicates (get 'add-hook 'safe-local-eval-function)))
(when (functionp predicates)
(setq predicates (list predicates)))
(unless (memq #'editorconfig--add-hook-safe-p predicates)
(put 'add-hook 'safe-local-eval-function #'editorconfig--add-hook-safe-p)))
(defun editorconfig--get-trailing-ws (props)
"Get vars to trim of trailing whitespace according to PROPS."
(pcase (gethash 'trim_trailing_whitespace props)
("true"
`((eval
. ,(if editorconfig-trim-whitespaces-mode
`(,editorconfig-trim-whitespaces-mode 1)
'(add-hook 'before-save-hook
#'editorconfig--delete-trailing-whitespace nil t)))))
("false"
;; Just do it right away rather than return a (VAR . VAL), which
;; would be probably more trouble than it's worth.
(when editorconfig-trim-whitespaces-mode
(funcall editorconfig-trim-whitespaces-mode 0))
(remove-hook 'before-save-hook
#'editorconfig--delete-trailing-whitespace t)
nil)))
(defun editorconfig--get-line-length (props)
"Get the max line length (`fill-column') to PROPS."
(let ((length (gethash 'max_line_length props)))
(when (and (editorconfig-string-integer-p length)
(> (string-to-number length) 0))
`((fill-column . ,(string-to-number length))))))
(defun editorconfig-call-get-properties-function (filename)
"Call `editorconfig-core-get-properties-hash' with FILENAME and return result.
This function also removes `unset' properties and calls
`editorconfig-hack-properties-functions'."
(if (stringp filename)
(setq filename (expand-file-name filename))
(editorconfig-error "Invalid argument: %S" filename))
(let ((props nil))
(condition-case-unless-debug err
(setq props (editorconfig-core-get-properties-hash filename))
(error
(editorconfig-error "Error from editorconfig-core-get-properties-hash: %S"
err)))
(cl-loop for k being the hash-keys of props using (hash-values v)
when (equal v "unset") do (remhash k props))
props))
(defvar editorconfig-get-local-variables-functions
'(editorconfig--get-indentation
editorconfig--get-trailing-nl
editorconfig--get-trailing-ws
editorconfig--get-line-length)
"Special hook run to convert EditorConfig settings to their Emacs equivalent.
Every function is called with one argument, a hash-table indexed by
EditorConfig settings represented as symbols and whose corresponding value
is represented as a string. It should return a list of (VAR . VAL) settings
where VAR is an ELisp variable and VAL is the value to which it should be set.")
(defun editorconfig--get-local-variables (props)
"Get variables settings according to EditorConfig PROPS."
(let ((alist ()))
(run-hook-wrapped 'editorconfig-get-local-variables-functions
(lambda (fun props)
(setq alist (append (funcall fun props) alist))
nil)
props)
alist))
(defun editorconfig-set-local-variables (props)
"Set buffer variables according to EditorConfig PROPS."
(pcase-dolist (`(,var . ,val) (editorconfig--get-local-variables props))
(if (eq 'eval var)
(eval val t)
(when (editorconfig--should-set var)
(set (make-local-variable var) val)))))
(defun editorconfig-major-mode-hook ()
"Function to run when `major-mode' has been changed.
This functions does not reload .editorconfig file, just sets local variables
again. Changing major mode can reset these variables.
This function also executes `editorconfig-after-apply-functions' functions."
(display-warning '(editorconfig editorconfig-major-mode-hook)
(format "editorconfig-major-mode-hook: editorconfig-mode: %S, major-mode: %S, -properties-hash: %S"
(and (boundp 'editorconfig-mode)
editorconfig-mode)
major-mode
editorconfig-properties-hash)
:debug)
(when (and (bound-and-true-p editorconfig-mode)
editorconfig-properties-hash)
(editorconfig-set-local-variables editorconfig-properties-hash)
(condition-case err
(run-hook-with-args 'editorconfig-after-apply-functions editorconfig-properties-hash)
(error
(display-warning '(editorconfig editorconfig-major-mode-hook)
(format "Error while running `editorconfig-after-apply-functions': %S"
err))))))
(defun editorconfig--advice-find-auto-coding (filename &rest _args)
"Consult `charset' setting of EditorConfig."
(let ((cs (dlet ((auto-coding-file-name filename))
(editorconfig--get-coding-system))))
(when cs (cons cs 'EditorConfig))))
(defun editorconfig--advice-find-file-noselect (f filename &rest args)
"Get EditorConfig properties and apply them to buffer to be visited.
This function should be added as an advice function to `find-file-noselect'.
F is that function, and FILENAME and ARGS are arguments passed to F."
(let ((props nil)
(ret nil))
(condition-case err
(when (stringp filename)
(setq props (editorconfig-call-get-properties-function filename)))
(error
(display-warning '(editorconfig editorconfig--advice-find-file-noselect)
(format "Failed to get properties, styles will not be applied: %S"
err)
:warning)))
(setq ret (apply f filename args))
(condition-case err
(with-current-buffer ret
(when props
;; NOTE: hack-properties-functions cannot affect coding-system value,
;; because it has to be set before initializing buffers.
(condition-case err
(run-hook-with-args 'editorconfig-hack-properties-functions props)
(error
(display-warning '(editorconfig editorconfig-hack-properties-functions)
(format "Error while running editorconfig-hack-properties-functions, abort running hook: %S"
err)
:warning)))
(setq editorconfig-properties-hash props)
;; When initializing buffer, `editorconfig-major-mode-hook'
;; will be called before setting `editorconfig-properties-hash', so
;; execute this explicitly here.
(editorconfig-set-local-variables props)
(condition-case err
(run-hook-with-args 'editorconfig-after-apply-functions props)
(error
(display-warning '(editorconfig editorconfig--advice-find-file-noselect)
(format "Error while running `editorconfig-after-apply-functions': %S"
err))))))
(error
(display-warning '(editorconfig editorconfig--advice-find-file-noselect)
(format "Error while setting variables from EditorConfig: %S" err))))
ret))
(defvar editorconfig--getting-coding-system nil)
(defun editorconfig--get-coding-system (&optional _size)
"Return the coding system to use according to EditorConfig.
Meant to be used on `auto-coding-functions'."
(defvar auto-coding-file-name) ;; Emacs≥30
(when (and (stringp auto-coding-file-name)
(file-name-absolute-p auto-coding-file-name)
;; Don't recurse infinitely.
(not (member auto-coding-file-name
editorconfig--getting-coding-system)))
(let* ((editorconfig--getting-coding-system
(cons auto-coding-file-name editorconfig--getting-coding-system))
(props (editorconfig-call-get-properties-function
auto-coding-file-name)))
(editorconfig-merge-coding-systems (gethash 'end_of_line props)
(gethash 'charset props)))))
(defun editorconfig--get-dir-local-variables ()
"Return the directory local variables specified via EditorConfig.
Meant to be used on `hack-dir-local-get-variables-functions'."
(when (stringp buffer-file-name)
(let* ((props (editorconfig-call-get-properties-function buffer-file-name))
(alist (editorconfig--get-local-variables props)))
;; FIXME: If there's `/foo/.editorconfig', `/foo/bar/.dir-locals.el',
;; and `/foo/bar/baz/.editorconfig', it would be nice to return two
;; pairs here, so that hack-dir-local can give different priorities
;; to the `/foo/.editorconfig' settings compared to those of
;; `/foo/bar/baz/.editorconfig', but we can't just convert the
;; settings from each file individually and let hack-dir-local merge
;; them because hack-dir-local doesn't have the notion of "unset",
;; and because the conversion of `indent_size' depends on `tab_width'.
(when alist
(cons
(file-name-directory (editorconfig-core-get-nearest-editorconfig
buffer-file-name))
alist)))))
;;;###autoload
(define-minor-mode editorconfig-mode
"Toggle EditorConfig feature."
:global t
(if (boundp 'hack-dir-local-get-variables-functions) ;Emacs≥30
(if editorconfig-mode
(progn
(add-hook 'hack-dir-local-get-variables-functions
;; Give them slightly lower precedence than settings from
;; `dir-locals.el'.
#'editorconfig--get-dir-local-variables t)
;; `auto-coding-functions' also exists in Emacs<30 but without
;; access to the file's name via `auto-coding-file-name'.
(add-hook 'auto-coding-functions
#'editorconfig--get-coding-system))
(remove-hook 'hack-dir-local-get-variables-functions
#'editorconfig--get-dir-local-variables)
(remove-hook 'auto-coding-functions
#'editorconfig--get-coding-system))
;; Emacs<30
(let ((modehooks '(prog-mode-hook
text-mode-hook
;; Some modes call `kill-all-local-variables' in their init
;; code, which clears some values set by editorconfig.
;; For those modes, editorconfig-apply need to be called
;; explicitly through their hooks.
rpm-spec-mode-hook)))
(if editorconfig-mode
(progn
(advice-add 'find-file-noselect :around #'editorconfig--advice-find-file-noselect)
(advice-add 'find-auto-coding :after-until
#'editorconfig--advice-find-auto-coding)
(dolist (hook modehooks)
(add-hook hook
#'editorconfig-major-mode-hook
t)))
(advice-remove 'find-file-noselect #'editorconfig--advice-find-file-noselect)
(advice-remove 'find-auto-coding
#'editorconfig--advice-find-auto-coding)
(dolist (hook modehooks)
(remove-hook hook #'editorconfig-major-mode-hook))))))
;; (defconst editorconfig--version
;; (eval-when-compile
;; (require 'lisp-mnt)
;; (declare-function lm-version "lisp-mnt" nil)
;; (lm-version))
;; "EditorConfig version.")
;; ;;;###autoload
;; (defun editorconfig-version (&optional show-version)
;; "Get EditorConfig version as string.
;;
;; If called interactively or if SHOW-VERSION is non-nil, show the
;; version in the echo area and the messages buffer."
;; (interactive (list t))
;; (let ((version-full
;; (if (fboundp 'package-get-version)
;; (package-get-version)
;; (let* ((version
;; (with-temp-buffer
;; (require 'find-func)
;; (declare-function find-library-name "find-func" (library))
;; (insert-file-contents (find-library-name "editorconfig"))
;; (require 'lisp-mnt)
;; (declare-function lm-version "lisp-mnt" nil)
;; (lm-version)))
;; (pkg (and (eval-and-compile (require 'package nil t))
;; (cadr (assq 'editorconfig
;; package-alist))))
;; (pkg-version (and pkg (package-version-join
;; (package-desc-version pkg)))))
;; (if (and pkg-version
;; (not (string= version pkg-version)))
;; (concat version "-" pkg-version)
;; version)))))
;; (when show-version
;; (message "EditorConfig Emacs v%s" version-full))
;; version-full))
(provide 'editorconfig)
;;; editorconfig.el ends here