Subject: Re: bug#70105: 30.0.50; Emacs should support EditorConfig out of the box

From: Stefan Monnier <[email-masked]>
Date: Tue, 18 Jun 2024 19:08:30 -0400

> I'm still working on the actual metadata of that branch, as well as
> etc/NEWS and doc, but in the mean time maybe you could look at the code to
> see if you have any objections there.

OK, I have a first cut at the doc done.
So, here's what I'm proposing, presented as a single diff.
Some commit messages still need to be improved (and I just noticed that
the files' copyright lines also need to be fixed), but other than that,
I think it's about ready.

Comments?  Objections?

        Stefan
diff --git a/doc/emacs/custom.texi b/doc/emacs/custom.texi
index 6bf4cbe00df..5287a90bb71 100644
--- a/doc/emacs/custom.texi
+++ b/doc/emacs/custom.texi
@@ -1550,6 +1550,41 @@ Directory 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 @code{.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 @code{.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 @code{.editorconfig}
+files whenever a file is visited, just as it does for @code{.dir-locals.el}.
+
+When both @code{.editorconfig} and @code{.dir-locals.el} files are
+encountered, the corresponding settings are combined, and in case there
+is overlap, the settings coming from the nearest file take precedence.
+
+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
diff --git a/etc/NEWS b/etc/NEWS
index b2fdbc4a88f..06742d24afe 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1964,6 +1964,14 @@ The following new XML schemas are now supported:
 
 * New Modes and Packages in Emacs 30.1
 
+** New package EditorConfig.
+This package provides support for the EditorConfig standard that
+is an editor-neutral way to provide directory local settings.
+It is enabled via a new global minor mode 'editorconfig-mode'
+which makes Emacs obey the '.editorconfig' files.
+And the package also comes with 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'
diff --git a/lisp/editorconfig-conf-mode.el b/lisp/editorconfig-conf-mode.el
new file mode 100644
index 00000000000..2b4ddd4410f
--- /dev/null
+++ b/lisp/editorconfig-conf-mode.el
@@ -0,0 +1,95 @@
+;;; editorconfig-conf-mode.el --- Major mode for editing .editorconfig files  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2024 EditorConfig Team
+
+;; Author: EditorConfig Team <[email-masked]>
+
+;; See
+;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors
+;; or the CONTRIBUTORS file for the list of contributors.
+
+;; This file is part of EditorConfig Emacs Plugin.
+
+;; EditorConfig Emacs Plugin 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.
+
+;; EditorConfig Emacs Plugin 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
+;; EditorConfig Emacs Plugin. 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
diff --git a/lisp/editorconfig-core-handle.el b/lisp/editorconfig-core-handle.el
new file mode 100644
index 00000000000..868d622af94
--- /dev/null
+++ b/lisp/editorconfig-core-handle.el
@@ -0,0 +1,223 @@
+;;; editorconfig-core-handle.el --- Handle Class for EditorConfig File  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2024 EditorConfig Team
+
+;; Author: EditorConfig Team <[email-masked]>
+
+;; See
+;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors
+;; or the CONTRIBUTORS file for the list of contributors.
+
+;; This file is part of EditorConfig Emacs Plugin.
+
+;; EditorConfig Emacs Plugin 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.
+
+;; EditorConfig Emacs Plugin 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
+;; EditorConfig Emacs Plugin. 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)))
+
+(defsubst editorconfig-core-handle--string-trim (str)
+  "Remove leading and trailing whitespaces from STR."
+  (replace-regexp-in-string "[[:space:]]+\\'"
+                            ""
+                            (replace-regexp-in-string "\\`[[:space:]]+"
+                                                      ""
+                                                      str)))
+
+(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
diff --git a/lisp/editorconfig-core.el b/lisp/editorconfig-core.el
new file mode 100644
index 00000000000..908d5db6f7e
--- /dev/null
+++ b/lisp/editorconfig-core.el
@@ -0,0 +1,156 @@
+;;; editorconfig-core.el --- EditorConfig Core library in Emacs Lisp  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2024 EditorConfig Team
+
+;; Author: EditorConfig Team <[email-masked]>
+
+;; See
+;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors
+;; or the CONTRIBUTORS file for the list of contributors.
+
+;; This file is part of EditorConfig Emacs Plugin.
+
+;; EditorConfig Emacs Plugin 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.
+
+;; EditorConfig Emacs Plugin 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
+;; EditorConfig Emacs Plugin. 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
diff --git a/lisp/editorconfig-fnmatch.el b/lisp/editorconfig-fnmatch.el
new file mode 100644
index 00000000000..74d1795ee75
--- /dev/null
+++ b/lisp/editorconfig-fnmatch.el
@@ -0,0 +1,292 @@
+;;; editorconfig-fnmatch.el --- Glob pattern matching in Emacs lisp  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2024 EditorConfig Team
+
+;; Author: EditorConfig Team <[email-masked]>
+
+;; See
+;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors
+;; or the CONTRIBUTORS file for the list of contributors.
+
+;; This file is part of EditorConfig Emacs Plugin.
+
+;; EditorConfig Emacs Plugin 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.
+
+;; EditorConfig Emacs Plugin 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
+;; EditorConfig Emacs Plugin. 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
diff --git a/lisp/editorconfig-tools.el b/lisp/editorconfig-tools.el
new file mode 100644
index 00000000000..4738a6c4d94
--- /dev/null
+++ b/lisp/editorconfig-tools.el
@@ -0,0 +1,119 @@
+;;; editorconfig-tools.el --- Editorconfig tools   -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2024 EditorConfig Team
+
+;; Author: EditorConfig Team <[email-masked]>
+
+;; See
+;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors
+;; or the CONTRIBUTORS file for the list of contributors.
+
+;; This file is part of EditorConfig Emacs Plugin.
+
+;; EditorConfig Emacs Plugin 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.
+
+;; EditorConfig Emacs Plugin 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
+;; EditorConfig Emacs Plugin. 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
diff --git a/lisp/editorconfig.el b/lisp/editorconfig.el
new file mode 100644
index 00000000000..a7e34434258
--- /dev/null
+++ b/lisp/editorconfig.el
@@ -0,0 +1,711 @@
+;;; editorconfig.el --- EditorConfig Plugin  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2024 EditorConfig Team
+
+;; Author: EditorConfig Team <[email-masked]>
+;; 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 the CONTRIBUTORS file for the list of contributors.
+
+;; This file is part of EditorConfig Emacs Plugin.
+
+;; EditorConfig Emacs Plugin 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.
+
+;; EditorConfig Emacs Plugin 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
+;; EditorConfig Emacs Plugin. 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)
+    (bash-ts-mode sh-basic-offset
+                  sh-indentation)
+    (bpftrace-mode c-basic-offset)
+    (c++-ts-mode c-basic-offset
+                 c-ts-mode-indent-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)
+    (csharp-mode c-basic-offset)
+    (csharp-ts-mode c-basic-offset
+                    csharp-ts-mode-indent-offset)
+    (emacs-lisp-mode . editorconfig--get-indentation-lisp-mode)
+    (ess-mode ess-indent-offset)
+    (feature-mode feature-indent-offset
+                  feature-indent-level)
+    (gdscript-mode gdscript-indent-offset)
+    (go-ts-mode go-ts-mode-indent-offset)
+    (hcl-mode hcl-indent-level)
+    (html-ts-mode html-ts-mode-indent-offset)
+    (java-ts-mode c-basic-offset
+                  java-ts-mode-indent-offset)
+    (js-mode js-indent-level)
+    (jsonian-mode jsonian-default-indentation)
+    (latex-mode . editorconfig--get-indentation-latex-mode)
+    (lisp-mode . editorconfig--get-indentation-lisp-mode)
+    (matlab-mode matlab-indent-level)
+    (octave-mode octave-block-offset)
+    ;; 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)
+    (ps-mode ps-mode-tab)
+    (ruby-mode ruby-indent-level)
+    (rust-ts-mode rust-indent-offset
+                  rust-ts-mode-indent-offset)
+    (scss-mode css-indent-offset)
+    (sgml-mode sgml-basic-offset)
+    (sh-mode sh-indentation)
+    (svelte-mode svelte-basic-offset)
+    (tcl-mode tcl-indent-level
+              tcl-continued-indent-level)
+    (toml-ts-mode toml-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)
+    (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-markup-indent-offset . ,size)
+    (web-mode-css-indent-offset . ,size)
+    (web-mode-code-indent-offset . ,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 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: Actually, we should loop over the "editorconfig-core-handles"
+      ;; since each one comes from a different directory.
+      (when alist
+        (cons
+         ;; FIXME: This should be the dir where we found the
+         ;; `.editorconfig' file.
+         (file-name-directory 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
+
+;; Local Variables:
+;; sentence-end-double-space: t
+;; End: