From: Stefan Monnier via "Bug reports for GNU Emacs, the Swiss army knife of text editors" <bug-gnu-emacs@gnu.org>
To: Eli Zaretskii <eliz@gnu.org>
Cc: jcs090218@gmail.com, bjorn.bidar@thaodan.de,
8slashes+git@gmail.com, 70105@debbugs.gnu.org
Subject: bug#70105: 30.0.50; Emacs should support EditorConfig out of the box
Date: Tue, 18 Jun 2024 19:08:30 -0400 [thread overview]
Message-ID: <jwvtthp97nj.fsf-monnier+emacs@gnu.org> (raw)
In-Reply-To: <jwv8qz2dcco.fsf-monnier+emacs@gnu.org> (Stefan Monnier's message of "Tue, 18 Jun 2024 02:01:51 -0400")
[-- Attachment #1: Type: text/plain, Size: 498 bytes --]
> 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
[-- Attachment #2: editorconfig.patch --]
[-- Type: text/x-diff, Size: 70887 bytes --]
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:
\f
* 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 <editorconfig@googlegroups.com>
+
+;; 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 <editorconfig@googlegroups.com>
+
+;; 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 <editorconfig@googlegroups.com>
+
+;; 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 <editorconfig@googlegroups.com>
+
+;; 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 <editorconfig@googlegroups.com>
+
+;; 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 <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 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:
next prev parent reply other threads:[~2024-06-18 23:08 UTC|newest]
Thread overview: 34+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-03-31 13:44 bug#70105: 30.0.50; Emacs should support EditorConfig out of the box Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-03-31 14:25 ` Eli Zaretskii
2024-03-31 20:40 ` Björn Bidar via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-03-31 22:26 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-06 23:51 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-08 12:50 ` Eli Zaretskii
2024-06-09 4:21 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-18 6:01 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-18 6:21 ` Ihor Radchenko
2024-06-18 13:17 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-18 16:21 ` Ihor Radchenko
2024-06-18 19:37 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-18 19:55 ` Ihor Radchenko
2024-06-18 20:07 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-18 9:10 ` Björn Bidar via Bug reports for GNU Emacs, the Swiss army knife of text editors
[not found] ` <87v826hb13.fsf@>
2024-06-18 12:56 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-18 19:26 ` Stefan Kangas
2024-06-18 19:47 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-18 23:08 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors [this message]
2024-06-19 5:48 ` Rudolf Schlatte
2024-06-19 6:01 ` Stefan Kangas
2024-06-19 8:18 ` Michael Albinus via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-19 15:18 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-19 15:18 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-19 16:52 ` Stefan Kangas
2024-06-19 17:26 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-19 19:31 ` Stefan Kangas
2024-06-19 19:56 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-19 20:51 ` Stefan Kangas
2024-06-21 14:19 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-19 15:52 ` Ihor Radchenko
2024-06-19 15:57 ` Eli Zaretskii
2024-06-20 16:33 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2024-06-09 11:49 ` Stefan Kangas
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://www.gnu.org/software/emacs/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=jwvtthp97nj.fsf-monnier+emacs@gnu.org \
--to=bug-gnu-emacs@gnu.org \
--cc=70105@debbugs.gnu.org \
--cc=8slashes+git@gmail.com \
--cc=bjorn.bidar@thaodan.de \
--cc=eliz@gnu.org \
--cc=jcs090218@gmail.com \
--cc=monnier@iro.umontreal.ca \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://git.savannah.gnu.org/cgit/emacs.git
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).