From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: Stefan Monnier via "Bug reports for GNU Emacs, the Swiss army knife of text editors" Newsgroups: gmane.emacs.bugs Subject: bug#70105: 30.0.50; Emacs should support EditorConfig out of the box Date: Tue, 18 Jun 2024 19:08:30 -0400 Message-ID: References: <867chibhqi.fsf@gnu.org> <86frtnd2e6.fsf@gnu.org> Reply-To: Stefan Monnier Mime-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="17898"; mail-complaints-to="usenet@ciao.gmane.io" User-Agent: Gnus/5.13 (Gnus v5.13) Cc: jcs090218@gmail.com, bjorn.bidar@thaodan.de, 8slashes+git@gmail.com, 70105@debbugs.gnu.org To: Eli Zaretskii Original-X-From: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Wed Jun 19 01:09:25 2024 Return-path: Envelope-to: geb-bug-gnu-emacs@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1sJhx5-0004Mr-Ou for geb-bug-gnu-emacs@m.gmane-mx.org; Wed, 19 Jun 2024 01:09:24 +0200 Original-Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1sJhwk-0005h5-9e; Tue, 18 Jun 2024 19:09:02 -0400 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1sJhwi-0005gx-V9 for bug-gnu-emacs@gnu.org; Tue, 18 Jun 2024 19:09:00 -0400 Original-Received: from debbugs.gnu.org ([2001:470:142:5::43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1sJhwi-0004hP-I6 for bug-gnu-emacs@gnu.org; Tue, 18 Jun 2024 19:09:00 -0400 Original-Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1sJhwj-0004pn-Um for bug-gnu-emacs@gnu.org; Tue, 18 Jun 2024 19:09:01 -0400 X-Loop: help-debbugs@gnu.org Resent-From: Stefan Monnier Original-Sender: "Debbugs-submit" Resent-CC: bug-gnu-emacs@gnu.org Resent-Date: Tue, 18 Jun 2024 23:09:01 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 70105 X-GNU-PR-Package: emacs Original-Received: via spool by 70105-submit@debbugs.gnu.org id=B70105.171875213518570 (code B ref 70105); Tue, 18 Jun 2024 23:09:01 +0000 Original-Received: (at 70105) by debbugs.gnu.org; 18 Jun 2024 23:08:55 +0000 Original-Received: from localhost ([127.0.0.1]:54907 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1sJhwZ-0004pP-Te for submit@debbugs.gnu.org; Tue, 18 Jun 2024 19:08:54 -0400 Original-Received: from mailscanner.iro.umontreal.ca ([132.204.25.50]:12097) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1sJhwV-0004p9-8k for 70105@debbugs.gnu.org; Tue, 18 Jun 2024 19:08:50 -0400 Original-Received: from pmg3.iro.umontreal.ca (localhost [127.0.0.1]) by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id D68F1444505; Tue, 18 Jun 2024 19:08:38 -0400 (EDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=iro.umontreal.ca; s=mail; t=1718752111; bh=hmKudAKOKIUG2fbjIRmHMAhbH2DUlDUoTHWWOlOoHTI=; h=From:To:Cc:Subject:In-Reply-To:References:Date:From; b=Sn0wqhfmLcTJFqsYB6QoyPy5eeoJWNzPzuDNz3U50oKyaqQop+92tprADKhWSPa9E +eZCxfJ7MtD0Eh6AOOc2KUinmTwKs/5CPpDNapnKnz7NeRv//eQ/qo08g4qPMRYSLp mFBsMndQbDTkWAMD++tqVNm6WGuALvEJ3z91MG9rKF1hwym+LjFqHMGioJkiw9jJOm pT2N4eU/8imy9YTZCshp/wAsbjv3NEJ3HvC66EeJGad/ndXj3G+cAa/MnmzX7c6TZD gEDb+0Bz+5RRQqT6SjrodYRBkGqMmYn+YvWDUD8ODEAqc9kWqOTxsH29nqQtsKafJ0 Jxa+3FeRqKPQA== Original-Received: from mail01.iro.umontreal.ca (unknown [172.31.2.1]) by pmg3.iro.umontreal.ca (Proxmox) with ESMTP id 17B66444509; Tue, 18 Jun 2024 19:08:31 -0400 (EDT) Original-Received: from lechazo (lechon.iro.umontreal.ca [132.204.27.242]) by mail01.iro.umontreal.ca (Postfix) with ESMTPSA id E0FE4120708; Tue, 18 Jun 2024 19:08:30 -0400 (EDT) In-Reply-To: (Stefan Monnier's message of "Tue, 18 Jun 2024 02:01:51 -0400") X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: bug-gnu-emacs@gnu.org List-Id: "Bug reports for GNU Emacs, the Swiss army knife of text editors" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Original-Sender: bug-gnu-emacs-bounces+geb-bug-gnu-emacs=m.gmane-mx.org@gnu.org Xref: news.gmane.io gmane.emacs.bugs:287461 Archived-At: --=-=-= Content-Type: text/plain > 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 --=-=-= Content-Type: text/x-diff; charset=utf-8 Content-Disposition: inline; filename=editorconfig.patch Content-Transfer-Encoding: quoted-printable 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}). =20 +@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{.editorconfi= g} +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 =20 +** 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 fil= es -*- lexical-binding: t -*- + +;; Copyright (C) 2011-2024 EditorConfig Team + +;; Author: EditorConfig Team + +;; 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 publishe= d by +;; the Free Software Foundation, either version 3 of the License, or (at y= our +;; option) any later version. + +;; EditorConfig Emacs Plugin is distributed in the hope that it will be us= eful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Gener= al +;; 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 . + +;;; 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[EditorCon= fig]" + "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]*[=3D:]" 1 font-lock-variable-name-face)= ))) + + ;; Highlight all key values + (dolist (key-value key-value-list) + (push `(,(format "[=3D:][ \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]*[=3D:]" 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-m= ode)) + +(provide 'editorconfig-conf-mode) +;;; editorconfig-conf-mode.el ends here diff --git a/lisp/editorconfig-core-handle.el b/lisp/editorconfig-core-hand= le.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 + +;; 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 publishe= d by +;; the Free Software Foundation, either version 3 of the License, or (at y= our +;; option) any later version. + +;; EditorConfig Emacs Plugin is distributed in the hope that it will be us= eful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Gener= al +;; 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 . + +;;; 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' instan= ce.") + +(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 =3D (editorconfig-core-handle-section-get-propert= ies + 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-ge= t-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 "\\([^=3D: \t]+\\)[ \t]*[=3D:][ \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-posit= ion) + (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 + +;; 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 publishe= d by +;; the Free Software Foundation, either version 3 of the License, or (at y= our +;; option) any later version. + +;; EditorConfig Emacs Plugin is distributed in the hope that it will be us= eful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Gener= al +;; 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 . + +;;; 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 ini= t.el: + +;; (setq editorconfig-get-properties-function +;; 'editorconfig-core-get-properties-hash) + + +;; Functions + +;; editorconfig-core-get-properties-hash (&optional file confname confvers= ion) + +;; 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=3D 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 preced= ence." + (maphash (lambda (key value) (puthash key value into)) update) + into) + +(defun editorconfig-core-get-properties-hash (&optional file confname conf= version) + "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 re= turns +hash object instead." + (setq file + (expand-file-name (or file + buffer-file-name + (error "FILE is not given and `buffer-file-n= ame' 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 f= ile) + confname)) + (editorconfig-core--hash-merge result + (editorconfig-core-handle-get-propert= ies-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_newl= ine + 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=3D v-indent-style "tab") + ;; ;; If VERSION < 0.9.0, indent_size should have no defau= lt value + ;; (version<=3D "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=3D 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=3D 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 -*- l= exical-binding: t -*- + +;; Copyright (C) 2011-2024 EditorConfig Team + +;; Author: EditorConfig Team + +;; 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 publishe= d by +;; the Free Software Foundation, either version 3 of the License, or (at y= our +;; option) any later version. + +;; EditorConfig Emacs Plugin is distributed in the hope that it will be us= eful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Gener= al +;; 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 . + +;;; Commentary: + +;; editorconfig-fnmatch.el provides a fnmatch implementation with a few +;; extensions. +;; The main usage of this library is glob pattern matching for EditorConfi= g, 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 matc= hings +;; 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/editor= config/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 matchin= gs 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 tw= ice." + (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 cac= hed +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 (=3D (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) + (=3D (aref pattern pos) ?*)) + (push ".*" result) + (push "[^/]*" result))) + + (?? + (push "[^/]" result)) + + (?\[ + (if in-brackets + (push "\\[" result) + (if (=3D (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 (=3D (aref pattern pos) ?\])) + (not has-slash)) + (if (and (=3D (aref pattern pos) ?/) + (not (=3D (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 (=3D (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--numer= ic-range-regexp + pattern-sub)) + (if num-range + (let ((number-start (string-to-number (match-string 1 + p= attern-sub))) + (number-end (string-to-number (match-string 2 + pat= tern-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 patter= n-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 (<=3D (+ index 3) (length pattern)) + (string=3D (substring pattern index (+ index 3)) "**/"= )) + (progn + (push "\\(?:/\\|/.*/\\)" result) + (setq index (+ index 3))) + (push "/" result))) + + (t + (unless (=3D current-char ?\\) + (push (regexp-quote (char-to-string current-char)) result)))) + + (if (=3D 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 + +;; 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 publishe= d by +;; the Free Software Foundation, either version 3 of the License, or (at y= our +;; option) any later version. + +;; EditorConfig Emacs Plugin is distributed in the hope that it will be us= eful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Gener= al +;; 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 . + +;;; 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-f= ile-name))) + (condition-case err + (run-hook-with-args 'editorconfig-hack-properties-function= s props) + (error + (display-warning '(editorconfig editorconfig-hack-propertie= s-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 pr= ops) + (error + (display-warning '(editorconfig editorconfig-after-apply-fu= nctions) + (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 =3D %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 +;; 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 publishe= d by +;; the Free Software Foundation, either version 3 of the License, or (at y= our +;; option) any later version. + +;; EditorConfig Emacs Plugin is distributed in the hope that it will be us= eful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Gener= al +;; 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 . + +;;; 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 fun= ction +;; that function should not set the vars but should instead *return* the= m. +;; - 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 \\=3D'editorconfig-after-apply-functions + \\=3D'(lambda (props) + (let ((show-line-num (gethash \\=3D'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 befo= re +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 \\=3D'editorconfig-hack-properties-functions + \\=3D'(lambda (props) + (when (derived-mode-p \\=3D'makefile-mode) + (puthash \\=3D'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 buf= fer. +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 SI= ZE +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 `editorconfi= g-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-va= rs)))))) + +(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: buffe= r-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-s= ystem + buffer-f= ile-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-syste= m) + ;; 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 sa= fe. +(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 r= esult. + +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-ha= sh: %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 equiva= lent. +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) settin= gs +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 variabl= es +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-mod= e: %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 editorconf= ig-properties-hash) + (error + (display-warning '(editorconfig editorconfig-major-mode-hook) + (format "Error while running `editorconfig-after-a= pply-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-nose= lect) + (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-function= s props) + (error + (display-warning '(editorconfig editorconfig-hack-propertie= s-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 pr= ops) + (error + (display-warning '(editorconfig editorconfig--advice-find-f= ile-noselect) + (format "Error while running `editorconfig= -after-apply-functions': %S" + err)))))) + (error + (display-warning '(editorconfig editorconfig--advice-find-file-nose= lect) + (format "Error while setting variables from Editor= Config: %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=E2=89=A530 + (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-syste= m)) + (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-n= ame)) + (alist (editorconfig--get-local-variables props))) + ;; FIXME: Actually, we should loop over the "editorconfig-core-handl= es" + ;; 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=E2=89=A530 + (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 th= eir init + ;; code, which clears some values set by editorconf= ig. + ;; For those modes, editorconfig-apply need to be c= alled + ;; 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-fil= e-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" (libr= ary)) + (insert-file-contents (find-library-name "editorconfi= g")) + (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=3D 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: --=-=-=--