dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

editorconfig.el (41858B)


      1 ;;; editorconfig.el --- EditorConfig Emacs Plugin  -*- lexical-binding: t -*-
      2 
      3 ;; Copyright (C) 2011-2023 EditorConfig Team
      4 
      5 ;; Author: EditorConfig Team <editorconfig@googlegroups.com>
      6 ;; Version: 0.10.1
      7 ;; URL: https://github.com/editorconfig/editorconfig-emacs#readme
      8 ;; Package-Requires: ((emacs "26.1") (nadvice "0.3"))
      9 ;; Keywords: convenience editorconfig
     10 
     11 ;; See
     12 ;; https://github.com/editorconfig/editorconfig-emacs/graphs/contributors
     13 ;; or the CONTRIBUTORS file for the list of contributors.
     14 
     15 ;; This file is part of EditorConfig Emacs Plugin.
     16 
     17 ;; EditorConfig Emacs Plugin is free software: you can redistribute it and/or
     18 ;; modify it under the terms of the GNU General Public License as published by
     19 ;; the Free Software Foundation, either version 3 of the License, or (at your
     20 ;; option) any later version.
     21 
     22 ;; EditorConfig Emacs Plugin is distributed in the hope that it will be useful,
     23 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     24 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
     25 ;; Public License for more details.
     26 
     27 ;; You should have received a copy of the GNU General Public License along with
     28 ;; EditorConfig Emacs Plugin. If not, see <https://www.gnu.org/licenses/>.
     29 
     30 ;;; Commentary:
     31 
     32 ;; EditorConfig helps developers define and maintain consistent
     33 ;; coding styles between different editors and IDEs.
     34 
     35 ;; The EditorConfig project consists of a file format for defining
     36 ;; coding styles and a collection of text editor plugins that enable
     37 ;; editors to read the file format and adhere to defined styles.
     38 ;; EditorConfig files are easily readable and they work nicely with
     39 ;; version control systems.
     40 
     41 ;;; Code:
     42 
     43 (require 'cl-lib)
     44 (require 'pcase)
     45 
     46 (require 'nadvice)
     47 
     48 (eval-when-compile
     49   (require 'rx)
     50   (require 'subr-x)
     51   (defvar tex-indent-basic)
     52   (defvar tex-indent-item)
     53   (defvar tex-indent-arg)
     54   (defvar evil-shift-width))
     55 
     56 (require 'editorconfig-core)
     57 
     58 (defgroup editorconfig nil
     59   "EditorConfig Emacs Plugin.
     60 
     61 EditorConfig helps developers define and maintain consistent
     62 coding styles between different editors and IDEs."
     63   :tag "EditorConfig"
     64   :prefix "editorconfig-"
     65   :group 'tools)
     66 
     67 (define-obsolete-variable-alias
     68   'edconf-exec-path
     69   'editorconfig-exec-path
     70   "0.5")
     71 (defcustom editorconfig-exec-path
     72   "editorconfig"
     73   "Path to EditorConfig executable.
     74 
     75 Used by `editorconfig--execute-editorconfig-exec'."
     76   :type 'string
     77   :group 'editorconfig)
     78 
     79 (define-obsolete-variable-alias
     80   'edconf-get-properties-function
     81   'editorconfig-get-properties-function
     82   "0.5")
     83 (defcustom editorconfig-get-properties-function
     84   'editorconfig-core-get-properties-hash
     85   "A function which gets EditorConfig properties for specified file.
     86 
     87 This function will be called with one argument, full path of the target file,
     88 and should return a hash object containing properties, or nil if any core
     89 program is not available.  Keys of this hash should be symbols of properties,
     90 and values should be strings of their values.
     91 
     92 
     93 For example, if you always want to use built-in core library instead
     94 of any EditorConfig executable to get properties, add following to
     95 your init.el:
     96 
     97   (set-variable \\='editorconfig-get-properties-function
     98                 #\\='editorconfig-core-get-properties-hash)
     99 
    100 Possible known values are:
    101 
    102 * `editorconfig-core-get-properties-hash' (default)
    103   * Always use built-in Emacs-Lisp implementation to get properties
    104 * `editorconfig-get-properties'
    105   * Use `editorconfig-get-properties-from-exec' when
    106     `editorconfig-exec-path' executable is found, otherwise
    107     use `editorconfig-core-get-properties-hash'
    108 * `editorconfig-get-properties-from-exec'
    109   * Get properties by executing EditorConfig executable"
    110   :type 'function
    111   :group 'editorconfig)
    112 
    113 (defcustom editorconfig-mode-lighter " EditorConfig"
    114   "Command `editorconfig-mode' lighter string."
    115   :type 'string
    116   :group 'editorconfig)
    117 
    118 (define-obsolete-variable-alias
    119   'edconf-custom-hooks
    120   'editorconfig-after-apply-functions
    121   "0.5")
    122 (define-obsolete-variable-alias
    123   'editorconfig-custom-hooks
    124   'editorconfig-after-apply-functions
    125   "0.7.14")
    126 (defcustom editorconfig-after-apply-functions ()
    127   "A list of functions after loading common EditorConfig settings.
    128 
    129 Each element in this list is a hook function.  This hook function
    130 takes one parameter, which is a property hash table.  The value
    131 of properties can be obtained through gethash function.
    132 
    133 The hook does not have to be coding style related; you can add
    134 whatever functionality you want.  For example, the following is
    135 an example to add a new property emacs_linum to decide whether to
    136 show line numbers on the left:
    137 
    138   (add-hook \\='editorconfig-after-apply-functions
    139     \\='(lambda (props)
    140        (let ((show-line-num (gethash \\='emacs_linum props)))
    141          (cond ((equal show-line-num \"true\") (linum-mode 1))
    142            ((equal show-line-num \"false\") (linum-mode 0))))))
    143 
    144 This hook will be run even when there are no matching sections in
    145 \".editorconfig\", or no \".editorconfig\" file was found at all."
    146   :type 'hook
    147   :group 'editorconfig)
    148 
    149 (defcustom editorconfig-hack-properties-functions ()
    150   "A list of function to alter property values before applying them.
    151 
    152 These functions will be run after loading \".editorconfig\" files and before
    153 applying them to current buffer, so that you can alter some properties from
    154 \".editorconfig\" before they take effect.
    155 \(Since 2021/08/30 (v0.9.0): Buffer coding-systems are set before running
    156 this functions, so this variable cannot be used to change coding-systems.)
    157 
    158 For example, Makefiles always use tab characters for indentation: you can
    159 overwrite \"indent_style\" property when current `major-mode' is a
    160 `makefile-mode' with following code:
    161 
    162   (add-hook \\='editorconfig-hack-properties-functions
    163             \\='(lambda (props)
    164                (when (derived-mode-p \\='makefile-mode)
    165                  (puthash \\='indent_style \"tab\" props))))
    166 
    167 This hook will be run even when there are no matching sections in
    168 \".editorconfig\", or no \".editorconfig\" file was found at all."
    169   :type 'hook
    170   :group 'editorconfig)
    171 (make-obsolete-variable 'editorconfig-hack-properties-functions
    172                         "Using `editorconfig-after-apply-functions' instead is recommended,
    173     because since 2021/08/30 (v0.9.0) this variable cannot support all properties:
    174     charset values will be referenced before running this hook."
    175                         "v0.9.0")
    176 
    177 (define-obsolete-variable-alias
    178   'edconf-indentation-alist
    179   'editorconfig-indentation-alist
    180   "0.5")
    181 (defcustom editorconfig-indentation-alist
    182   ;; For contributors: Sort modes in alphabetical order
    183   '((apache-mode apache-indent-level)
    184     (awk-mode c-basic-offset)
    185     (bpftrace-mode c-basic-offset)
    186     (c++-mode c-basic-offset)
    187     (c++-ts-mode c-basic-offset
    188                  c-ts-mode-indent-offset)
    189     (c-mode c-basic-offset)
    190     (c-ts-mode c-basic-offset
    191                c-ts-mode-indent-offset)
    192     (cmake-mode cmake-tab-width)
    193     (cmake-ts-mode cmake-tab-width
    194                    cmake-ts-mode-indent-offset)
    195     (coffee-mode coffee-tab-width)
    196     (cperl-mode cperl-indent-level)
    197     (crystal-mode crystal-indent-level)
    198     (csharp-mode c-basic-offset)
    199     (csharp-ts-mode c-basic-offset
    200                     csharp-ts-mode-indent-offset)
    201     (css-mode css-indent-offset)
    202     (css-ts-mode css-indent-offset)
    203     (d-mode c-basic-offset)
    204     (emacs-lisp-mode lisp-indent-offset)
    205     (enh-ruby-mode enh-ruby-indent-level)
    206     (erlang-mode erlang-indent-level)
    207     (ess-mode ess-indent-offset)
    208     (f90-mode f90-associate-indent
    209               f90-continuation-indent
    210               f90-critical-indent
    211               f90-do-indent
    212               f90-if-indent
    213               f90-program-indent
    214               f90-type-indent)
    215     (feature-mode feature-indent-offset
    216                   feature-indent-level)
    217     (fsharp-mode fsharp-continuation-offset
    218                  fsharp-indent-level
    219                  fsharp-indent-offset)
    220     (gdscript-mode gdscript-indent-offset)
    221     (groovy-mode groovy-indent-offset)
    222     (go-ts-mode go-ts-mode-indent-offset)
    223     (haskell-mode haskell-indent-spaces
    224                   haskell-indent-offset
    225                   haskell-indentation-layout-offset
    226                   haskell-indentation-left-offset
    227                   haskell-indentation-starter-offset
    228                   haskell-indentation-where-post-offset
    229                   haskell-indentation-where-pre-offset
    230                   shm-indent-spaces)
    231     (haxor-mode haxor-tab-width)
    232     (html-ts-mode html-ts-mode-indent-offset)
    233     (idl-mode c-basic-offset)
    234     (jade-mode jade-tab-width)
    235     (java-mode c-basic-offset)
    236     (java-ts-mode c-basic-offset
    237                   java-ts-mode-indent-offset)
    238     (js-mode js-indent-level)
    239     (js-ts-mode js-indent-level)
    240     (js-jsx-mode js-indent-level sgml-basic-offset)
    241     (js2-mode js2-basic-offset)
    242     (js2-jsx-mode js2-basic-offset sgml-basic-offset)
    243     (js3-mode js3-indent-level)
    244     (json-mode js-indent-level)
    245     (json-ts-mode json-ts-mode-indent-offset)
    246     (julia-mode julia-indent-offset)
    247     (kotlin-mode kotlin-tab-width)
    248     (latex-mode . editorconfig-set-indentation-latex-mode)
    249     (lisp-mode lisp-indent-offset)
    250     (livescript-mode livescript-tab-width)
    251     (lua-mode lua-indent-level)
    252     (matlab-mode matlab-indent-level)
    253     (meson-mode meson-indent-basic)
    254     (mips-mode mips-tab-width)
    255     (mustache-mode mustache-basic-offset)
    256     (nasm-mode nasm-basic-offset)
    257     (nginx-mode nginx-indent-level)
    258     (nxml-mode nxml-child-indent (nxml-attribute-indent . 2))
    259     (objc-mode c-basic-offset)
    260     (octave-mode octave-block-offset)
    261     (perl-mode perl-indent-level)
    262     ;; No need to change `php-mode-coding-style' value for php-mode
    263     ;; since we run editorconfig later than it resets `c-basic-offset'.
    264     ;; See https://github.com/editorconfig/editorconfig-emacs/issues/116
    265     ;; for details.
    266     (php-mode c-basic-offset)
    267     (pike-mode c-basic-offset)
    268     (ps-mode ps-mode-tab)
    269     (pug-mode pug-tab-width)
    270     (puppet-mode puppet-indent-level)
    271     (python-mode . editorconfig-set-indentation-python-mode)
    272     (python-ts-mode . editorconfig-set-indentation-python-mode)
    273     (rjsx-mode js-indent-level sgml-basic-offset)
    274     (ruby-mode ruby-indent-level)
    275     (ruby-ts-mode ruby-indent-level)
    276     (rust-mode rust-indent-offset)
    277     (rust-ts-mode rust-indent-offset
    278                   rust-ts-mode-indent-offset)
    279     (rustic-mode rustic-indent-offset)
    280     (scala-mode scala-indent:step)
    281     (scss-mode css-indent-offset)
    282     (sgml-mode sgml-basic-offset)
    283     (sh-mode sh-basic-offset sh-indentation)
    284     (bash-ts-mode sh-basic-offset sh-indentation)
    285     (slim-mode slim-indent-offset)
    286     (sml-mode sml-indent-level)
    287     (tcl-mode tcl-indent-level
    288               tcl-continued-indent-level)
    289     (terra-mode terra-indent-level)
    290     (toml-ts-mode toml-ts-mode-indent-offset)
    291     (typescript-mode typescript-indent-level)
    292     (typescript-ts-base-mode typescript-ts-mode-indent-offset)
    293     (verilog-mode verilog-indent-level
    294                   verilog-indent-level-behavioral
    295                   verilog-indent-level-declaration
    296                   verilog-indent-level-module
    297                   verilog-cexp-indent
    298                   verilog-case-indent)
    299     (web-mode (web-mode-indent-style . (lambda (size) 2))
    300               web-mode-attr-indent-offset
    301               web-mode-attr-value-indent-offset
    302               web-mode-code-indent-offset
    303               web-mode-css-indent-offset
    304               web-mode-markup-indent-offset
    305               web-mode-sql-indent-offset
    306               web-mode-block-padding
    307               web-mode-script-padding
    308               web-mode-style-padding)
    309     (yaml-mode yaml-indent-offset)
    310     (yaml-ts-mode yaml-indent-offset))
    311   "Alist of indentation setting methods by modes.
    312 
    313 Each element looks like (MODE . FUNCTION) or (MODE . INDENT-SPEC-LIST).
    314 
    315 If FUNCTION is provided, it will be called when setting the
    316 indentation.  The indent size will be passed.
    317 
    318 If INDENT-SPEC-LIST is provided, each element of it must have one of the
    319 following forms:
    320 
    321  1. VARIABLE
    322     It means (VARIABLE . 1).
    323 
    324  2. (VARIABLE . SPEC)
    325     Setting VARIABLE according to the type of SPEC:
    326 
    327       - Integer
    328         The value is (* SPEC INDENT-SIZE);
    329 
    330       - Function
    331         The value is (funcall SPEC INDENT-SIZE);
    332 
    333       - Any other type.
    334         The value is SPEC.
    335 
    336 NOTE: Only the **buffer local** value of VARIABLE will be set."
    337   :type '(alist :key-type symbol :value-type sexp)
    338   :risky t
    339   :group 'editorconfig)
    340 
    341 (defcustom editorconfig-exclude-modes ()
    342   "Modes in which `editorconfig-mode-apply' will not run."
    343   :type '(repeat (symbol :tag "Major Mode"))
    344   :group 'editorconfig)
    345 
    346 (defcustom editorconfig-exclude-regexps ()
    347   "List of regexp for buffer filenames `editorconfig-mode-apply' will not run.
    348 
    349 When variable `buffer-file-name' matches any of the regexps, then
    350 `editorconfig-mode-apply' will not do its work."
    351   :type '(repeat string)
    352   :group 'editorconfig)
    353 (with-eval-after-load 'recentf
    354   (add-to-list 'editorconfig-exclude-regexps
    355                (rx-to-string '(seq string-start
    356                                    (eval (file-truename (expand-file-name recentf-save-file))))
    357                              t)))
    358 
    359 (defcustom editorconfig-trim-whitespaces-mode nil
    360   "Buffer local minor-mode to use to trim trailing whitespaces.
    361 
    362 If set, enable that mode when `trim_trailing_whitespace` is set to true.
    363 Otherwise, use `delete-trailing-whitespace'."
    364   :type 'symbol
    365   :group 'editorconfig)
    366 
    367 (defvar editorconfig-properties-hash nil
    368   "Hash object of EditorConfig properties that was enabled for current buffer.
    369 Set by `editorconfig-apply' and nil if that is not invoked in
    370 current buffer yet.")
    371 (make-variable-buffer-local 'editorconfig-properties-hash)
    372 (put 'editorconfig-properties-hash
    373      'permanent-local
    374      t)
    375 
    376 (defvar editorconfig-lisp-use-default-indent nil
    377   "Selectively ignore the value of indent_size for Lisp files.
    378 Prevents `lisp-indent-offset' from being set selectively.
    379 
    380 nil - `lisp-indent-offset' is always set normally.
    381 t   - `lisp-indent-offset' is never set normally
    382        (always use default indent for lisps).
    383 number - `lisp-indent-offset' is not set only if indent_size is
    384          equal to this number.  For example, if this is set to 2,
    385          `lisp-indent-offset' will not be set only if indent_size is 2.")
    386 
    387 (define-error 'editorconfig-error
    388               "Error thrown from editorconfig lib")
    389 
    390 (defun editorconfig-error (&rest args)
    391   "Signal an `editorconfig-error'.
    392 Make a message by passing ARGS to `format-message'."
    393   (signal 'editorconfig-error (list (apply #'format-message args))))
    394 
    395 (defun editorconfig--disabled-for-filename (filename)
    396   "Return non-nil when EditorConfig is disabled for FILENAME."
    397   (cl-assert (stringp filename))
    398   (cl-loop for regexp in editorconfig-exclude-regexps
    399            if (string-match regexp filename) return t
    400            finally return nil))
    401 
    402 (defun editorconfig--disabled-for-majormode (majormode)
    403   "Return non-nil when Editorconfig is disabled for MAJORMODE."
    404   (cl-assert majormode)
    405   (or (provided-mode-derived-p majormode 'special-mode)
    406       ;; Some special modes (like `archive-mode') are not derived from
    407       ;; `special-mode'
    408       (eq (get majormode 'mode-class) 'special)
    409       (memq majormode
    410             editorconfig-exclude-modes)))
    411 
    412 (defun editorconfig-string-integer-p (string)
    413   "Return non-nil if STRING represents integer."
    414   (and (stringp string)
    415        (string-match-p "\\`[0-9]+\\'" string)))
    416 
    417 (defun editorconfig-set-indentation-python-mode (size)
    418   "Set `python-mode' indent size to SIZE."
    419   (when (boundp 'python-indent-offset)
    420     (setq-local python-indent-offset size))
    421   ;; For https://launchpad.net/python-mode
    422   (when (boundp 'py-indent-offset)
    423     (setq-local py-indent-offset size)))
    424 
    425 (defun editorconfig-set-indentation-latex-mode (size)
    426   "Set `latex-mode' indent size to SIZE."
    427   (setq-local tex-indent-basic size)
    428   (setq-local tex-indent-item size)
    429   (setq-local tex-indent-arg (* 2 size))
    430   ;; For AUCTeX
    431   (when (boundp 'TeX-brace-indent-level)
    432     (setq-local TeX-brace-indent-level size))
    433   (when (boundp 'LaTeX-indent-level)
    434     (setq-local LaTeX-indent-level size))
    435   (when (boundp 'LaTeX-item-indent)
    436     (setq-local LaTeX-item-indent (- size))))
    437 
    438 (defun editorconfig--should-set (size symbol)
    439   "Determines if editorconfig should set SYMBOL using SIZE."
    440   (if (eq symbol 'lisp-indent-offset)
    441       (cond
    442        ((null editorconfig-lisp-use-default-indent)  t)
    443        ((eql t editorconfig-lisp-use-default-indent) nil)
    444        ((numberp editorconfig-lisp-use-default-indent)
    445         (not (eql size editorconfig-lisp-use-default-indent)))
    446        (t t))
    447     t))
    448 
    449 (defun editorconfig-set-indentation (style &optional size tab_width)
    450   "Set indentation type from STYLE, SIZE and TAB_WIDTH."
    451   (if (editorconfig-string-integer-p size)
    452       (setq size (string-to-number size))
    453     (unless (equal size "tab") (setq size nil)))
    454   (cond (tab_width
    455          (setq tab-width (string-to-number tab_width)))
    456         ((numberp size)
    457          (setq tab-width size)))
    458   (when (equal size "tab")
    459     (setq size tab-width))
    460   (cond ((equal style "space")
    461          (setq indent-tabs-mode nil))
    462         ((equal style "tab")
    463          (setq indent-tabs-mode t)))
    464   (when size
    465     (when (featurep 'evil)
    466       (setq-local evil-shift-width size))
    467     (let ((parent major-mode)
    468           entry)
    469       ;; Find the closet parent mode of `major-mode' in
    470       ;; `editorconfig-indentation-alist'.
    471       (while (and (not (setq entry (assoc parent editorconfig-indentation-alist)))
    472                   (setq parent (get parent 'derived-mode-parent))))
    473       (when entry
    474         (let ((fn-or-list (cdr entry)))
    475           (cond ((functionp fn-or-list) (funcall fn-or-list size))
    476                 ((listp fn-or-list)
    477                  (dolist (elem fn-or-list)
    478                    (cond ((and (symbolp elem)
    479                                (editorconfig--should-set size elem))
    480                           (set (make-local-variable elem) size))
    481                          ((and (consp elem)
    482                                (editorconfig--should-set size (car elem)))
    483                           (let ((spec (cdr elem)))
    484                             (set (make-local-variable (car elem))
    485                                  (cond ((functionp spec) (funcall spec size))
    486                                        ((integerp spec) (* spec size))
    487                                        (t spec))))))))))))))
    488 
    489 (defvar-local editorconfig--apply-coding-system-currently nil
    490   "Used internally.")
    491 (put 'editorconfig--apply-coding-system-currently
    492      'permanent-local
    493      t)
    494 
    495 (defun editorconfig-merge-coding-systems (end-of-line charset)
    496   "Return merged coding system symbol of END-OF-LINE and CHARSET."
    497   (let ((eol (cond
    498               ((equal end-of-line "lf") 'undecided-unix)
    499               ((equal end-of-line "cr") 'undecided-mac)
    500               ((equal end-of-line "crlf") 'undecided-dos)
    501               (t 'undecided)))
    502         (cs (cond
    503              ((equal charset "latin1") 'iso-latin-1)
    504              ((equal charset "utf-8") 'utf-8)
    505              ((equal charset "utf-8-bom") 'utf-8-with-signature)
    506              ((equal charset "utf-16be") 'utf-16be-with-signature)
    507              ((equal charset "utf-16le") 'utf-16le-with-signature)
    508              (t 'undecided))))
    509     (merge-coding-systems cs eol)))
    510 
    511 (cl-defun editorconfig-set-coding-system-revert (end-of-line charset)
    512   "Set buffer coding system by END-OF-LINE and CHARSET.
    513 
    514 This function will revert buffer when the coding-system has been changed."
    515   ;; `editorconfig--advice-find-file-noselect' does not use this function
    516   (let ((coding-system (editorconfig-merge-coding-systems end-of-line
    517                                                           charset)))
    518     (display-warning '(editorconfig editorconfig-set-coding-system-revert)
    519                      (format "editorconfig-set-coding-system-revert: buffer-file-name: %S | buffer-file-coding-system: %S | coding-system: %S | apply-currently: %S"
    520                              buffer-file-name
    521                              buffer-file-coding-system
    522                              coding-system
    523                              editorconfig--apply-coding-system-currently)
    524                      :debug)
    525     (when (eq coding-system 'undecided)
    526       (cl-return-from editorconfig-set-coding-system-revert))
    527     (when (and buffer-file-coding-system
    528                (memq buffer-file-coding-system
    529                      (coding-system-aliases (merge-coding-systems coding-system
    530                                                                   buffer-file-coding-system))))
    531       (cl-return-from editorconfig-set-coding-system-revert))
    532     (unless (file-readable-p buffer-file-name)
    533       (set-buffer-file-coding-system coding-system)
    534       (cl-return-from editorconfig-set-coding-system-revert))
    535     (unless (memq coding-system
    536                   (coding-system-aliases editorconfig--apply-coding-system-currently))
    537       ;; Revert functions might call editorconfig-apply again
    538       (unwind-protect
    539           (progn
    540             (setq editorconfig--apply-coding-system-currently coding-system)
    541             ;; Revert without query if buffer is not modified
    542             (let ((revert-without-query '(".")))
    543               (revert-buffer-with-coding-system coding-system)))
    544         (setq editorconfig--apply-coding-system-currently nil)))))
    545 
    546 (defun editorconfig-set-trailing-nl (final-newline)
    547   "Set up requiring final newline by FINAL-NEWLINE.
    548 
    549 This function will set `require-final-newline' and `mode-require-final-newline'
    550 to non-nil when FINAL-NEWLINE is true."
    551   (pcase final-newline
    552     ("true"
    553      ;; keep prefs around how/when the nl is added, if set - otherwise add on save
    554      (setq-local require-final-newline      (or require-final-newline t))
    555      (setq-local mode-require-final-newline (or mode-require-final-newline t)))
    556     ("false"
    557      ;; FIXME: Add functionality for actually REMOVING any trailing newlines here!
    558      ;;        (rather than just making sure we don't automagically ADD a new one)
    559      (setq-local require-final-newline      nil)
    560      (setq-local mode-require-final-newline nil))))
    561 
    562 (defun editorconfig-set-trailing-ws (trim-trailing-ws)
    563   "Set up trimming of trailing whitespace at end of lines by TRIM-TRAILING-WS."
    564   (make-local-variable 'write-file-functions) ;; just current buffer
    565   (when (and (equal trim-trailing-ws "true")
    566              (not buffer-read-only))
    567     ;; when true we push delete-trailing-whitespace (emacs > 21)
    568     ;; to write-file-functions
    569     (if editorconfig-trim-whitespaces-mode
    570         (funcall editorconfig-trim-whitespaces-mode 1)
    571       (add-to-list 'write-file-functions 'delete-trailing-whitespace)))
    572   (when (or (equal trim-trailing-ws "false")
    573             buffer-read-only)
    574     ;; when false we remove every delete-trailing-whitespace
    575     ;; from write-file-functions
    576     (when editorconfig-trim-whitespaces-mode
    577       (funcall editorconfig-trim-whitespaces-mode 0))
    578     (setq write-file-functions
    579           (remove 'delete-trailing-whitespace write-file-functions))))
    580 
    581 (defun editorconfig-set-line-length (length)
    582   "Set the max line length (`fill-column') to LENGTH."
    583   (when (and (editorconfig-string-integer-p length)
    584              (> (string-to-number length) 0))
    585     (setq fill-column (string-to-number length))))
    586 
    587 
    588 (defun editorconfig--execute-editorconfig-exec (filename)
    589   "Execute EditorConfig core with FILENAME and return output."
    590   (if filename
    591       (with-temp-buffer
    592         (let ((remote (file-remote-p filename))
    593               (remote-localname (file-remote-p filename
    594                                                'localname)))
    595           (display-warning '(editorconfig editorconfig--execute-editorconfig-exec)
    596                            (format "editorconfig--execute-editorconfig-exec: filename: %S | remote: %S | remote-localname: %S"
    597                                    filename
    598                                    remote
    599                                    remote-localname)
    600                            :debug)
    601           (if remote
    602               (progn
    603                 (cd (concat remote "/"))
    604                 (setq filename remote-localname))
    605             (cd "/")))
    606         (display-warning '(editorconfig editorconfig--execute-editorconfig-exec)
    607                          (format "editorconfig--execute-editorconfig-exec: default-directory: %S | filename: %S"
    608                                  default-directory
    609                                  filename
    610                                  )
    611                          :debug)
    612         (if (eq 0
    613                 (process-file editorconfig-exec-path nil t nil filename))
    614             (buffer-string)
    615           (editorconfig-error (buffer-string))))
    616     ""))
    617 
    618 (defun editorconfig--parse-properties (props-string)
    619   "Create properties hash table from PROPS-STRING."
    620   (let ((props-list (split-string props-string "\n"))
    621         (properties (make-hash-table)))
    622     (dolist (prop props-list properties)
    623       (let ((key-val (split-string prop " *= *")))
    624         (when (> (length key-val) 1)
    625           (let ((key (intern (car key-val)))
    626                 (val (mapconcat 'identity (cdr key-val) "")))
    627             (puthash key val properties)))))))
    628 
    629 (defun editorconfig-get-properties-from-exec (filename)
    630   "Get EditorConfig properties of file FILENAME.
    631 
    632 This function uses value of `editorconfig-exec-path' to get properties."
    633   (if (executable-find editorconfig-exec-path)
    634       (editorconfig--parse-properties (editorconfig--execute-editorconfig-exec filename))
    635     (editorconfig-error "Unable to find editorconfig executable")))
    636 
    637 (defun editorconfig-get-properties (filename)
    638   "Get EditorConfig properties for file FILENAME.
    639 
    640 It calls `editorconfig-get-properties-from-exec' if
    641 `editorconfig-exec-path' is found, otherwise
    642 `editorconfig-core-get-properties-hash'."
    643   (if (and (executable-find editorconfig-exec-path)
    644            (not (file-remote-p filename)))
    645       (editorconfig-get-properties-from-exec filename)
    646     (require 'editorconfig-core)
    647     (editorconfig-core-get-properties-hash filename)))
    648 
    649 (defun editorconfig-call-get-properties-function (filename)
    650   "Call `editorconfig-get-properties-function' with FILENAME and return result.
    651 
    652 This function also removes `unset' properties and calls
    653 `editorconfig-hack-properties-functions'."
    654   (unless (functionp editorconfig-get-properties-function)
    655     (editorconfig-error "Invalid editorconfig-get-properties-function value"))
    656   (if (stringp filename)
    657       (setq filename (expand-file-name filename))
    658     (editorconfig-error "Invalid argument: %S" filename))
    659   (let ((props nil))
    660     (condition-case err
    661         (setq props (funcall editorconfig-get-properties-function
    662                              filename))
    663       (error
    664        (editorconfig-error "Error from editorconfig-get-properties-function: %S"
    665                            err)))
    666     (cl-loop for k being the hash-keys of props using (hash-values v)
    667              when (equal v "unset") do (remhash k props))
    668     props))
    669 
    670 (defun editorconfig-set-local-variables (props)
    671   "Set buffer variables according to EditorConfig PROPS."
    672   (editorconfig-set-indentation (gethash 'indent_style props)
    673                                 (gethash 'indent_size props)
    674                                 (gethash 'tab_width props))
    675   (editorconfig-set-trailing-nl (gethash 'insert_final_newline props))
    676   (editorconfig-set-trailing-ws (gethash 'trim_trailing_whitespace props))
    677   (editorconfig-set-line-length (gethash 'max_line_length props)))
    678 
    679 ;;;###autoload
    680 (defun editorconfig-apply ()
    681   "Get and apply EditorConfig properties to current buffer.
    682 
    683 This function does not respect the values of `editorconfig-exclude-modes' and
    684 `editorconfig-exclude-regexps' and always applies available properties.
    685 Use `editorconfig-mode-apply' instead to make use of these variables."
    686   (interactive)
    687   (when buffer-file-name
    688     (condition-case err
    689         (progn
    690           (let ((props (editorconfig-call-get-properties-function buffer-file-name)))
    691             (condition-case err
    692                 (run-hook-with-args 'editorconfig-hack-properties-functions props)
    693               (error
    694                (display-warning '(editorconfig editorconfig-hack-properties-functions)
    695                                 (format "Error while running editorconfig-hack-properties-functions, abort running hook: %S"
    696                                         err)
    697                                 :warning)))
    698             (setq editorconfig-properties-hash props)
    699             (editorconfig-set-local-variables props)
    700             (editorconfig-set-coding-system-revert
    701              (gethash 'end_of_line props)
    702              (gethash 'charset props))
    703             (condition-case err
    704                 (run-hook-with-args 'editorconfig-after-apply-functions props)
    705               (error
    706                (display-warning '(editorconfig editorconfig-after-apply-functions)
    707                                 (format "Error while running editorconfig-after-apply-functions, abort running hook: %S"
    708                                         err)
    709                                 :warning)))))
    710       (error
    711        (display-warning '(editorconfig editorconfig-apply)
    712                         (format "Error in editorconfig-apply, styles will not be applied: %S" err)
    713                         :error)))))
    714 
    715 (defun editorconfig-mode-apply ()
    716   "Get and apply EditorConfig properties to current buffer.
    717 
    718 This function does nothing when the major mode is listed in
    719 `editorconfig-exclude-modes', or variable `buffer-file-name' matches
    720 any of regexps in `editorconfig-exclude-regexps'."
    721   (interactive)
    722   (when (and major-mode
    723              (not (editorconfig--disabled-for-majormode major-mode))
    724              buffer-file-name
    725              (not (editorconfig--disabled-for-filename buffer-file-name)))
    726     (editorconfig-apply)))
    727 
    728 (defun editorconfig-major-mode-hook ()
    729   "Function to run when `major-mode' has been changed.
    730 
    731 This functions does not reload .editorconfig file, just sets local variables
    732 again.  Changing major mode can reset these variables.
    733 
    734 This function also executes `editorconfig-after-apply-functions' functions."
    735   (display-warning '(editorconfig editorconfig-major-mode-hook)
    736                    (format "editorconfig-major-mode-hook: editorconfig-mode: %S, major-mode: %S, -properties-hash: %S"
    737                            (and (boundp 'editorconfig-mode)
    738                                 editorconfig-mode)
    739                            major-mode
    740                            editorconfig-properties-hash)
    741                    :debug)
    742   (when (and (bound-and-true-p editorconfig-mode)
    743              editorconfig-properties-hash)
    744     (editorconfig-set-local-variables editorconfig-properties-hash)
    745     (condition-case err
    746         (run-hook-with-args 'editorconfig-after-apply-functions editorconfig-properties-hash)
    747       (error
    748        (display-warning '(editorconfig editorconfig-major-mode-hook)
    749                         (format "Error while running `editorconfig-after-apply-functions': %S"
    750                                 err))))))
    751 
    752 (defvar editorconfig--cons-filename-codingsystem nil
    753   "Used interally.
    754 
    755 `editorconfig--advice-find-file-noselect' will set this variable, and
    756 `editorconfig--advice-insert-file-contents' will use this variable to set
    757 `coding-system-for-read' value.")
    758 
    759 (defun editorconfig--advice-insert-file-contents (f filename &rest args)
    760   "Set `coding-system-for-read'.
    761 
    762 This function should be added as an advice function to `insert-file-contents'.
    763 F is that function, and FILENAME and ARGS are arguments passed to F."
    764   ;; This function uses `editorconfig--cons-filename-codingsystem' to decide what coding-system
    765   ;; should be used, which will be set by `editorconfig--advice-find-file-noselect'.
    766   (display-warning '(editorconfig editorconfig--advice-insert-file-contents)
    767                    (format "editorconfig--advice-insert-file-contents: filename: %S args: %S codingsystem: %S bufferfilename: %S"
    768                            filename args
    769                            editorconfig--cons-filename-codingsystem
    770                            buffer-file-name)
    771                    :debug)
    772   (if (and (stringp filename)
    773            (stringp (car editorconfig--cons-filename-codingsystem))
    774            (string= (expand-file-name filename)
    775                     (car editorconfig--cons-filename-codingsystem))
    776            (cdr editorconfig--cons-filename-codingsystem)
    777            (not (eq (cdr editorconfig--cons-filename-codingsystem)
    778                     'undecided)))
    779       (let ((coding-system-for-read (cdr editorconfig--cons-filename-codingsystem))
    780             ;; (coding-system-for-read 'undecided)
    781             )
    782         (apply f filename args))
    783     (apply f filename args)))
    784 
    785 (defun editorconfig--advice-find-file-noselect (f filename &rest args)
    786   "Get EditorConfig properties and apply them to buffer to be visited.
    787 
    788 This function should be added as an advice function to `find-file-noselect'.
    789 F is that function, and FILENAME and ARGS are arguments passed to F."
    790   (let ((props nil)
    791         (coding-system nil)
    792         (ret nil))
    793     (condition-case err
    794         (when (and (stringp filename)
    795                    (not (editorconfig--disabled-for-filename filename)))
    796           (setq props (editorconfig-call-get-properties-function filename))
    797           (setq coding-system
    798                 (editorconfig-merge-coding-systems (gethash 'end_of_line props)
    799                                                    (gethash 'charset props))))
    800       (error
    801        (display-warning '(editorconfig editorconfig--advice-find-file-noselect)
    802                         (format "Failed to get properties, styles will not be applied: %S"
    803                                 err)
    804                         :warning)))
    805 
    806     (let ((editorconfig--cons-filename-codingsystem (cons (expand-file-name filename)
    807                                                           coding-system)))
    808       (setq ret (apply f filename args)))
    809 
    810     (condition-case err
    811         (with-current-buffer ret
    812           (when (and props
    813                      ;; filename has already been checked
    814                      (not (editorconfig--disabled-for-majormode major-mode)))
    815             (when (and (file-remote-p filename)
    816                        (not (local-variable-p 'buffer-file-coding-system))
    817                        (not (file-exists-p filename))
    818                        coding-system
    819                        (not (eq coding-system
    820                                 'undecided)))
    821               ;; When file path indicates it is a remote file and it actually
    822               ;; does not exists, `buffer-file-coding-system' will not be set.
    823               ;; (Seems `insert-file-contents' will not be called)
    824               ;; For that case, explicitly set this value so that saving will be done
    825               ;; with expected coding system.
    826               (set-buffer-file-coding-system coding-system))
    827 
    828             ;; NOTE: When using editorconfig-2-mode, hack-properties-functions cannot affect coding-system value,
    829             ;; because it has to be set before initializing buffers.
    830             (condition-case err
    831                 (run-hook-with-args 'editorconfig-hack-properties-functions props)
    832               (error
    833                (display-warning '(editorconfig editorconfig-hack-properties-functions)
    834                                 (format "Error while running editorconfig-hack-properties-functions, abort running hook: %S"
    835                                         err)
    836                                 :warning)))
    837             (setq editorconfig-properties-hash props)
    838             ;; When initializing buffer, `editorconfig-major-mode-hook'
    839             ;; will be called before setting `editorconfig-properties-hash', so
    840             ;; execute this explicitly here.
    841             (editorconfig-set-local-variables props)
    842 
    843             (condition-case err
    844                 (run-hook-with-args 'editorconfig-after-apply-functions props)
    845               (error
    846                (display-warning '(editorconfig editorconfig--advice-find-file-noselect)
    847                                 (format "Error while running `editorconfig-after-apply-functions': %S"
    848                                         err))))))
    849       (error
    850        (display-warning '(editorconfig editorconfig--advice-find-file-noselect)
    851                         (format "Error while setting variables from EditorConfig: %S" err))))
    852     ret))
    853 
    854 (defvar editorconfig--legacy-version nil
    855   "Use legacy version of editorconfig-mode.
    856 
    857 As of 2021/08/30, `editorconfig-mode' uses totally new implementation by
    858 default.  This flag disables this and go back to previous version.")
    859 
    860 ;;;###autoload
    861 (define-minor-mode editorconfig-mode
    862   "Toggle EditorConfig feature.
    863 
    864 To disable EditorConfig in some buffers, modify
    865 `editorconfig-exclude-modes' or `editorconfig-exclude-regexps'."
    866   :global t
    867   :lighter editorconfig-mode-lighter
    868   (if (not editorconfig--legacy-version)
    869       (let ((modehooks '(prog-mode-hook
    870                          text-mode-hook
    871                          read-only-mode-hook
    872                          ;; Some modes call `kill-all-local-variables' in their init
    873                          ;; code, which clears some values set by editorconfig.
    874                          ;; For those modes, editorconfig-apply need to be called
    875                          ;; explicitly through their hooks.
    876                          rpm-spec-mode-hook)))
    877         (if editorconfig-mode
    878             (progn
    879               (advice-add 'find-file-noselect :around 'editorconfig--advice-find-file-noselect)
    880               (advice-add 'insert-file-contents :around 'editorconfig--advice-insert-file-contents)
    881               (dolist (hook modehooks)
    882                 (add-hook hook
    883                           'editorconfig-major-mode-hook
    884                           t)))
    885           (advice-remove 'find-file-noselect 'editorconfig--advice-find-file-noselect)
    886           (advice-remove 'insert-file-contents 'editorconfig--advice-insert-file-contents)
    887           (dolist (hook modehooks)
    888             (remove-hook hook 'editorconfig-major-mode-hook))))
    889 
    890     ;; editorconfig--legacy-version is enabled
    891     ;; See https://github.com/editorconfig/editorconfig-emacs/issues/141 for why
    892     ;; not `after-change-major-mode-hook'
    893     (dolist (hook '(change-major-mode-after-body-hook
    894                     read-only-mode-hook
    895                     ;; Some modes call `kill-all-local-variables' in their init
    896                     ;; code, which clears some values set by editorconfig.
    897                     ;; For those modes, editorconfig-apply need to be called
    898                     ;; explicitly through their hooks.
    899                     rpm-spec-mode-hook))
    900       (if editorconfig-mode
    901           (add-hook hook 'editorconfig-mode-apply)
    902         (remove-hook hook 'editorconfig-mode-apply)))))
    903 
    904 
    905 ;; Tools
    906 ;; Some useful commands for users, not required for EditorConfig to work
    907 
    908 ;;;###autoload
    909 (defun editorconfig-find-current-editorconfig ()
    910   "Find the closest .editorconfig file for current file."
    911   (interactive)
    912   (eval-and-compile (require 'editorconfig-core))
    913   (when-let* ((file (editorconfig-core-get-nearest-editorconfig
    914                     default-directory)))
    915     (find-file file)))
    916 
    917 ;;;###autoload
    918 (defun editorconfig-display-current-properties ()
    919   "Display EditorConfig properties extracted for current buffer."
    920   (interactive)
    921   (if editorconfig-properties-hash
    922       (let ((buf (get-buffer-create "*EditorConfig Properties*"))
    923             (file buffer-file-name)
    924             (props editorconfig-properties-hash))
    925         (with-current-buffer buf
    926           (erase-buffer)
    927           (insert (format "# EditorConfig for %s\n" file))
    928           (maphash (lambda (k v)
    929                      (insert (format "%S = %s\n" k v)))
    930                    props))
    931         (display-buffer buf))
    932     (message "Properties are not applied to current buffer yet.")
    933     nil))
    934 ;;;###autoload
    935 (defalias 'describe-editorconfig-properties
    936   'editorconfig-display-current-properties)
    937 
    938 ;;;###autoload
    939 (defun editorconfig-format-buffer()
    940   "Format buffer according to .editorconfig indent_style and indent_width."
    941   (interactive)
    942   (when (string= (gethash 'indent_style editorconfig-properties-hash) "tab")
    943     (tabify (point-min) (point-max)))
    944   (when (string= (gethash 'indent_style editorconfig-properties-hash) "space")
    945     (untabify (point-min) (point-max)))
    946   (indent-region (point-min) (point-max)))
    947 
    948 
    949 
    950 ;; (defconst editorconfig--version
    951 ;;   (eval-when-compile
    952 ;;     (require 'lisp-mnt)
    953 ;;     (declare-function lm-version "lisp-mnt" nil)
    954 ;;     (lm-version))
    955 ;;   "EditorConfig version.")
    956 
    957 (declare-function find-library-name "find-func" (library))
    958 (declare-function lm-version "lisp-mnt" nil)
    959 
    960 ;;;###autoload
    961 (defun editorconfig-version (&optional show-version)
    962   "Get EditorConfig version as string.
    963 
    964 If called interactively or if SHOW-VERSION is non-nil, show the
    965 version in the echo area and the messages buffer."
    966   (interactive (list t))
    967   (let* ((version (with-temp-buffer
    968                     (require 'find-func)
    969                     (insert-file-contents (find-library-name "editorconfig"))
    970                     (require 'lisp-mnt)
    971                     (lm-version)))
    972          (pkg (and (eval-and-compile (require 'package nil t))
    973                    (cadr (assq 'editorconfig
    974                                package-alist))))
    975          (pkg-version (and pkg
    976                            (package-version-join (package-desc-version pkg))))
    977          (version-full (if (and pkg-version
    978                                 (not (string= version pkg-version)))
    979                            (concat version "-" pkg-version)
    980                          version)))
    981     (when show-version
    982       (message "EditorConfig Emacs v%s" version-full))
    983     version-full))
    984 
    985 (provide 'editorconfig)
    986 ;;; editorconfig.el ends here
    987 
    988 ;; Local Variables:
    989 ;; sentence-end-double-space: t
    990 ;; End: