Compare commits

...

53 commits

Author SHA1 Message Date
Dmitry Gutov
d4875e1235 Merge branch 'master' into scratch/etags-regen 2022-07-11 14:17:45 +03:00
Dmitry Gutov
9c00d6c3f6 Merge branch 'master' into scratch/etags-regen 2022-01-07 20:34:08 +02:00
Dmitry Gutov
ebcba77d4c Merge branch 'master' into scratch/etags-regen 2021-11-11 03:44:58 +03:00
Dmitry Gutov
ad50128a36 Merge branch 'master' into scratch/etags-regen 2021-09-24 00:26:05 +03:00
Dmitry Gutov
535c262606 Merge branch 'master' into scratch/etags-regen 2021-09-17 22:29:19 +03:00
Dmitry Gutov
d680a23e57 Merge branch 'master' into scratch/etags-regen 2021-09-12 03:31:15 +03:00
Dmitry Gutov
c409977cc2 Merge branch 'master' into scratch/etags-regen 2021-09-06 18:17:17 +03:00
Dmitry Gutov
8cb5af412e Merge branch 'master' into scratch/etags-regen 2021-08-17 04:10:21 +03:00
Dmitry Gutov
87ab04638e Merge branch 'master' into scratch/etags-regen 2021-08-08 03:58:12 +03:00
Dmitry Gutov
9cf8957ae1 Merge branch 'master' into scratch/etags-regen 2021-07-04 04:53:55 +03:00
Dmitry Gutov
7ee6e8f350 Merge branch 'master' into scratch/etags-regen 2021-04-29 21:21:01 +03:00
Dmitry Gutov
1db074f707 Merge branch 'master' into scratch/etags-regen 2021-03-30 03:47:12 +03:00
Dmitry Gutov
519e1499c2 Fix etags-regen-lang-regexp-alist's docstring 2021-02-19 16:40:44 +02:00
Dmitry Gutov
a8a59b9594 Merge branch 'master' into scratch/etags-regen 2021-02-10 15:31:59 +02:00
Dmitry Gutov
40c293b832 Merge branch 'master' into scratch/etags-regen 2021-02-09 01:22:40 +02:00
Dmitry Gutov
f4a1d47327 Brute force refresh implementation
Seems okay-ish in an Emacs checkout (~70ms to refresh), but in GDB
it's 200ms already.  Need smarter heuristics.
2021-02-08 04:11:32 +02:00
Dmitry Gutov
25b291580c Introduce project-files-filtered and use it
This cuts the time it takes to list all source files by ~2x in the
several real-world cases I've tested.
2021-02-08 02:07:29 +02:00
Dmitry Gutov
4f7b533473 etags-regen--all-files: Extract to a separate function 2021-01-31 04:04:06 +02:00
Dmitry Gutov
f520e5dcfa Merge branch 'master' into scratch/etags-regen 2021-01-31 03:12:16 +02:00
Dmitry Gutov
3098e4798d Merge branch 'master' into scratch/etags-regen 2021-01-30 04:31:59 +02:00
Dmitry Gutov
44f19c7f28 Merge branch 'master' into scratch/etags-regen 2021-01-22 04:51:26 +02:00
Dmitry Gutov
1daad1784f Merge branch 'master' into scratch/etags-regen 2021-01-19 03:26:37 +02:00
Dmitry Gutov
8d00e2f20b Merge branch 'master' into scratch/etags-regen 2021-01-16 21:43:33 +02:00
Dmitry Gutov
153a549b0d etags-regen--tags-generate: Always ignore Emacs lock files 2021-01-16 05:17:34 +02:00
Dmitry Gutov
b7e29962a8 New feature: etags-regen-ignores 2021-01-14 15:31:34 +02:00
Dmitry Gutov
7ef4c7c1ae etags-regen--update-file: Do call set-buffer-modified-p anyway 2021-01-13 02:50:02 +02:00
Dmitry Gutov
a9969d0e57 etags-regen--update-file: Speed up dramatically
^ turned out to be a very expensive anchor.  It was the difference
between a "real" regexp search and a literal search, and the latter's
much faster.  And since we're searching literally, might as well use
search-forward.
2021-01-13 02:42:35 +02:00
Dmitry Gutov
d81f30a98b etags-regen--update-file: Don't synchronize updated tags to disk 2021-01-13 02:31:37 +02:00
Dmitry Gutov
3585f8ba71 etags-regen-file-extensions: Extract user option from code 2021-01-11 05:07:55 +02:00
Dmitry Gutov
30f8dba7ab ; Check that buffer is non-nil 2021-01-11 03:13:22 +02:00
Dmitry Gutov
d1dd227df2 etags-regen--tags-cleanup: Also kill the visiting buffer 2021-01-11 02:27:26 +02:00
Dmitry Gutov
834c52de8f Merge branch 'master' into scratch/etags-regen 2021-01-11 00:47:17 +02:00
Dmitry Gutov
9a1fc1e512 Remove dead reference 2021-01-07 05:15:45 +02:00
Dmitry Gutov
c0a5291e4d Merge branch 'master' into scratch/etags-regen 2021-01-07 01:21:12 +02:00
Dmitry Gutov
4df82d18af Avoid unsafe value prompts when the package's not loaded 2021-01-04 03:22:26 +02:00
Dmitry Gutov
146b0e7bad Add a clarification 2021-01-04 02:15:59 +02:00
Dmitry Gutov
527087263f Undo the etags.el changes 2021-01-04 02:05:45 +02:00
Dmitry Gutov
46c1f2f447 Solve (hopefully?) the local-variable-satefy issue 2021-01-04 02:04:30 +02:00
Dmitry Gutov
aa4eddb24e Ensure errors buffer name is more consistent 2021-01-03 23:49:00 +02:00
Dmitry Gutov
6f26f60563 Bookkeeping 2021-01-03 22:19:27 +02:00
Dmitry Gutov
798c90ba1b Support etags-regen-program-options in --update-file
Also switch to shell-command for consistency, to avoid problems with
quoting in regexps.
2021-01-03 20:09:15 +02:00
Dmitry Gutov
8ce70ebb3e Use `silent' for appending, too 2021-01-03 04:55:18 +02:00
Dmitry Gutov
d6285de058 New defcustom: etags-regen-program-options 2021-01-03 04:54:50 +02:00
Dmitry Gutov
d9e3afed09 Make etags-regen-program a user option 2021-01-03 04:13:25 +02:00
Dmitry Gutov
570f132500 etags.c: Implement the -L flag 2021-01-03 04:08:58 +02:00
Dmitry Gutov
4d0886e528 Move to a separate file and minor mode 2021-01-03 03:57:25 +02:00
Dmitry Gutov
ab5bf3c058 Merge branch 'master' into scratch/etags-regen 2021-01-03 00:46:33 +02:00
Dmitry Gutov
94437f9d26 Avoid the 'unsaved buffer' prompt at quit 2020-12-14 05:35:59 +02:00
Dmitry Gutov
2350411d41 Speed up tags file update 2020-12-12 02:14:57 +02:00
Dmitry Gutov
4b431fd803 Add some notes 2020-12-10 22:19:15 +02:00
Dmitry Gutov
94374c30be Add a note 2020-12-10 04:42:32 +02:00
Dmitry Gutov
1da73b4b65 Better feedback 2020-12-09 05:12:36 +02:00
Dmitry Gutov
64d7ae811d etags auto-generation and incremental updates WIP 2020-12-09 00:59:16 +02:00
4 changed files with 419 additions and 52 deletions

View file

@ -4,7 +4,14 @@
((nil . ((tab-width . 8)
(sentence-end-double-space . t)
(fill-column . 70)
(emacs-lisp-docstring-fill-column . 65)
(bug-reference-url-format . "https://debbugs.gnu.org/%s")
(etags-regen-lang-regexp-alist
.
((("c" "objc") .
("/[ \t]*DEFVAR_[A-Z_ \t(]+\"\\([^\"]+\\)\"/\\1/"
"/[ \t]*DEFVAR_[A-Z_ \t(]+\"[^\"]+\",[ \t]\\([A-Za-z0-9_]+\\)/\\1/"))))
(etags-regen-ignores . ("test/manual/etags/"))
(emacs-lisp-docstring-fill-column . 65)
(bug-reference-url-format . "https://debbugs.gnu.org/%s")))
(c-mode . ((c-file-style . "GNU")
(c-noise-macro-names . ("INLINE" "ATTRIBUTE_NO_SANITIZE_UNDEFINED" "UNINIT" "CALLBACK" "ALIGN_STACK"))

View file

@ -1146,7 +1146,7 @@ main (int argc, char **argv)
/* When the optstring begins with a '-' getopt_long does not rearrange the
non-options arguments to be at the end, but leaves them alone. */
optstring = concat ("-ac:Cf:Il:o:Qr:RSVhH",
(CTAGS) ? "BxdtTuvw" : "Di:",
(CTAGS) ? "BxdtTuvw" : "Di:L:",
"");
while ((opt = getopt_long (argc, argv, optstring, longopts, NULL)) != EOF)
@ -1158,6 +1158,7 @@ main (int argc, char **argv)
break;
case 1:
case 'L':
/* This means that a file name has been seen. Record it. */
argbuffer[current_arg].arg_type = at_filename;
argbuffer[current_arg].what = optarg;

View file

@ -0,0 +1,330 @@
;;; etags-regen.el --- Auto-(re)regenerating tags -*- lexical-binding: t -*-
;; Copyright (C) 2021 Free Software Foundation, Inc.
;; Author: Dmitry Gutov <dgutov@yandex.ru>
;; Keywords: tools
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Simple tags generation with automatic invalidation.
;;; Code:
(defgroup etags-regen nil
"Auto-(re)generating tags."
:group 'tools)
(defvar etags-regen--tags-file nil)
(defvar etags-regen--tags-root nil)
(defvar etags-regen--tags-mtime nil)
(defvar etags-regen--new-file nil)
(declare-function project-root "project")
(declare-function project-files "project")
(defcustom etags-regen-program (executable-find "etags")
"Name of the etags executable."
;; Always having our 'etags' here would be easier, but we can't
;; always rely on it being installed. So it might be ctags's etags.
:type 'file)
(defcustom etags-regen-program-options nil
"List of additional options to pass to the etags program."
:type '(repeat string))
(defcustom etags-regen-lang-regexp-alist nil
"Mapping of languages to additional regexps for tags.
Each language should be one of the recognized by etags, see
'etags --help'. Each tag regexp should be a string in the format
as documented for the '--regex' arguments.
We support only Emacs's etags program with this option."
:type '(repeat
(cons
:tag "Languages group"
(repeat (string :tag "Language name"))
(repeat (string :tag "Tag Regexp")))))
;;;###autoload
(put 'etags-regen-lang-regexp-alist 'safe-local-variable
(lambda (value)
(and (listp value)
(seq-every-p
(lambda (group)
(and (consp group)
(listp (car group))
(listp (cdr group))
(seq-every-p
(lambda (lang)
(and (stringp lang)
(string-match-p "\\`[a-z*+]+\\'" lang)))
(car group))
(seq-every-p #'stringp (cdr group))))
value))))
;; XXX: We have to list all extensions: etags falls back to Fortran.
;; http://lists.gnu.org/archive/html/emacs-devel/2018-01/msg00323.html
(defcustom etags-regen-file-extensions
'("rb" "js" "py" "pl" "el" "c" "cpp" "cc" "h" "hh" "hpp"
"java" "go" "cl" "lisp" "prolog" "php" "erl" "hrl"
"F" "f" "f90" "for" "cs" "a" "asm" "ads" "adb" "ada")
"Code file extensions.
File extensions to generate the tags for."
:type '(repeat (string :tag "File extension")))
;;;###autoload
(put 'etags-regen-file-extensions 'safe-local-variable
(lambda (value)
(and (listp value)
(seq-every-p
(lambda (ext)
(and (stringp ext)
(string-match-p "\\`[a-zA-Z0-9]+\\'" ext)))
value))))
(defcustom etags-regen-ignores nil
"Additional ignore rules, in the format of `project-ignores'."
:type '(repeat
(string :tag "Glob to ignore")))
;;;###autoload
(put 'etags-regen-ignores 'safe-local-variable
(lambda (value)
(and (listp value)
(seq-every-p #'stringp value))))
(defvar etags-regen--errors-buffer-name "*etags-regen-tags-errors*")
(defun etags-regen--all-mtimes (proj)
(let ((files (etags-regen--all-files proj))
(mtimes (make-hash-table :test 'equal)))
(with-temp-buffer
(mapc (lambda (f)
(insert f "\0"))
files)
(shell-command-on-region
(point-min) (point-max) "xargs -0 stat -c \"%Y\""
nil t etags-regen--errors-buffer-name t)
(goto-char (point-min))
(while (not (eobp))
(puthash (pop files)
(string-to-number (buffer-substring (point) (line-end-position)))
mtimes)
(forward-line 1)))
mtimes))
(defun etags-regen--refresh ()
(save-excursion
(let* ((tags-file-buf (get-file-buffer etags-regen--tags-file))
(proj (project-current))
(tags-mtime etags-regen--tags-mtime)
(all-mtimes (etags-regen--all-mtimes proj))
added-files
changed-files
removed-files)
(set-buffer tags-file-buf)
(dolist (file (tags-table-files))
(let ((mtime (gethash file all-mtimes)))
(cond
((null mtime)
(push file removed-files))
((> mtime tags-mtime)
(push file changed-files)
(remhash file all-mtimes))
(t
(remhash file all-mtimes)))))
(maphash
(lambda (key _value)
(push key added-files))
all-mtimes)
(when (> (+ (length added-files)
(length changed-files)
(length removed-files))
100)
(message "etags-regen: Too many changes, falling back to full rescan")
(etags-regen--tags-cleanup))
(dolist (file (nconc removed-files changed-files))
(etags-regen--remove-tag file))
(when (or changed-files added-files)
(apply #'etags-regen--append-tags
(nconc changed-files added-files))))))
(defun etags-regen--maybe-generate ()
(let (proj)
(when etags-regen--tags-root
(if (file-in-directory-p default-directory
etags-regen--tags-root)
(etags-regen--refresh)
(etags-regen--tags-cleanup)))
(when (and (not (or tags-file-name
tags-table-list))
(setq proj (project-current)))
(message "Generating new tags table...")
(let ((start (time-to-seconds)))
(etags-regen--tags-generate proj)
(message "...done (%.2f s)" (- (time-to-seconds) start)))
;; Invalidate the scanned tags after any change is written to disk.
(add-hook 'after-save-hook #'etags-regen--update-file)
(add-hook 'before-save-hook #'etags-regen--mark-as-new)
(visit-tags-table etags-regen--tags-file))))
(defun etags-regen--all-files (proj)
(let* ((root (project-root proj))
(default-directory root)
(files (project-files-filtered
proj
;; FIXME: Extensions in upper case.
(mapcar (lambda (ext) (format "*.%s" ext))
etags-regen-file-extensions)
nil
'(".#*"))))
files))
(defun etags-regen--tags-generate (proj)
(require 'dired)
(let* ((root (project-root proj))
(default-directory root)
(files (etags-regen--all-files proj))
(tags-file (make-temp-file "emacs-regen-tags-"))
;; ctags's etags requires '-L -' for stdin input.
;; It looks half-broken here (indexes only some of the input files),
;; but better-maintained versions of it exist (like universal-ctags).
(command (format "%s %s -L - -o %s"
etags-regen-program
(mapconcat #'identity (etags-regen--build-program-options) " ")
tags-file)))
(setq etags-regen--tags-file tags-file
etags-regen--tags-root root
etags-regen--tags-mtime (time-to-seconds))
(with-temp-buffer
(mapc (lambda (f)
(insert f "\n"))
files)
(shell-command-on-region (point-min) (point-max) command
nil nil etags-regen--errors-buffer-name t))))
(defun etags-regen--build-program-options ()
(nconc
(mapcan
(lambda (group)
(mapcan
(lambda (lang)
(mapcar (lambda (regexp)
(concat "--regex="
(shell-quote-argument
(format "{%s}%s" lang regexp))))
(cdr group)))
(car group)))
etags-regen-lang-regexp-alist)
etags-regen-program-options))
(defun etags-regen--update-file ()
;; TODO: Maybe only do this when Emacs is idle for a bit.
(let ((file-name buffer-file-name)
(tags-file-buf (get-file-buffer etags-regen--tags-file))
pr should-scan)
(save-excursion
(when tags-file-buf
(cond
((and etags-regen--new-file
(kill-local-variable 'etags-regen--new-file)
(setq pr (project-current))
(equal (project-root pr) etags-regen--tags-root)
(member file-name (project-files pr)))
(set-buffer tags-file-buf)
(setq should-scan t))
((progn (set-buffer tags-file-buf)
(etags-regen--remove-tag file-name))
(setq should-scan t))))
(when should-scan
(etags-regen--append-tags file-name)
))))
(defun etags-regen--remove-tag (file-name)
(goto-char (point-min))
(when (search-forward (format "\f\n%s," file-name) nil t)
(let ((start (match-beginning 0)))
(search-forward "\f\n" nil 'move)
(let ((inhibit-read-only t)
(save-silently t))
(delete-region start
(if (eobp)
(point)
(- (point) 2)))))
t))
(defun etags-regen--append-tags (&rest file-names)
(goto-char (point-max))
(let ((options (etags-regen--build-program-options))
(inhibit-read-only t))
(setq etags-regen--tags-mtime (time-to-seconds))
;; FIXME: call-process is significantly faster, though.
;; Like 10ms vs 20ms here.
(shell-command
(format "%s %s %s -o -"
etags-regen-program (mapconcat #'identity options " ")
(mapconcat #'identity file-names " "))
t etags-regen--errors-buffer-name))
;; We don't want Emacs to ask us to save the buffer when exiting.
(set-buffer-modified-p nil)
;; FIXME: Is there a better way to do this?
;; Completion table is the only remaining place where the
;; update is not incremental.
(setq-default tags-completion-table nil))
(defun etags-regen--mark-as-new ()
(unless buffer-file-number
(setq-local etags-regen--new-file t)))
(defun etags-regen--tags-cleanup ()
(when etags-regen--tags-file
;; TODO: Maybe keep the generated files around, after we learn to
;; update them for the whole project quickly, so opening one
;; created in a previous session makes sense.
(delete-file etags-regen--tags-file)
(let ((buffer (get-file-buffer etags-regen--tags-file)))
(and buffer
(kill-buffer buffer)))
(setq tags-file-name nil
tags-table-list nil
etags-regen--tags-file nil
etags-regen--tags-root nil
etags-regen--tags-mtime nil))
(remove-hook 'after-save-hook #'etags-regen--update-file)
(remove-hook 'before-save-hook #'etags-regen--mark-as-new))
;;;###autoload
(define-minor-mode etags-regen-mode
"Generate tags automatically."
:global t
(if etags-regen-mode
(progn
(advice-add 'etags--xref-backend :before
#'etags-regen--maybe-generate)
(advice-add 'tags-completion-at-point-function :before
#'etags-regen--maybe-generate))
(advice-remove 'etags--xref-backend #'etags-regen--maybe-generate)
(advice-remove 'tags-completion-at-point-function #'etags-regen--maybe-generate)
(etags-regen--tags-cleanup)))
(provide 'etags-regen)
;;; etags-regen.el ends here

View file

@ -288,11 +288,33 @@ The default implementation uses `find-program'. PROJECT is used
to find the list of ignores for each directory."
(mapcan
(lambda (dir)
(project--files-in-directory dir
(project--dir-ignores project dir)))
(project-files-filtered project nil dir))
(or dirs
(list (project-root project)))))
;; XXX: Or INCLUDE-FILES and EXCLUDE-FILES?
;; TODO: Add tests.
(cl-defgeneric project-files-filtered ( project &optional files dir
extra-ignores no-project-ignores)
"Return a list of files FILES in directory DIR in PROJECT.
FILES must a list of file name glob patterns, nil meaning to list
any files. DIR must be an absolute name or nil, in which case it
defaults to the project root. EXTRA-IGNORES are ignore entries
to use together with the list of ignores already configured for
the project. But if NO-PROJECT-IGNORES is non-nil, only
EXTRA-IGNORES should be applied.
The default implementation uses `find-program'."
(unless dir (setq dir (project-root project)))
(project--files-in-directory
dir
(append
(unless no-project-ignores
(project--dir-ignores project dir))
extra-ignores)
(and files
(mapconcat #'identity files " "))))
(defun project--files-in-directory (dir ignores &optional files)
(require 'find-dired)
(require 'xref)
@ -486,31 +508,54 @@ backend implementation of `project-external-roots'.")
(funcall project-vc-external-roots-function)))
(list (project-root project))))
(cl-defmethod project-files ((project (head vc)) &optional dirs)
(mapcan
(lambda (dir)
(let ((ignores (project--value-in-dir 'project-vc-ignores dir))
backend)
(if (and (file-equal-p dir (nth 2 project))
(setq backend (cadr project))
(cond
((eq backend 'Hg))
((and (eq backend 'Git)
(or
(not ignores)
(version<= "1.9" (vc-git--program-version)))))))
(project--vc-list-files dir backend ignores)
(project--files-in-directory
dir
(project--dir-ignores project dir)))))
(or dirs
(list (project-root project)))))
;; TODO: Add tests.
(cl-defmethod project-files-filtered ( (project (head vc)) &optional files dir
extra-ignores no-project-ignores)
(unless dir (setq dir (project-root project)))
(let ((ignores (append (unless no-project-ignores
(project--value-in-dir 'project-vc-ignores dir))
extra-ignores))
(backend (vc-responsible-backend dir)))
(if (cond
((eq backend 'Hg))
((and (eq backend 'Git)
(or
(not ignores)
(version<= "1.9" (vc-git--program-version))))))
(project--vc-list-files dir backend ignores files no-project-ignores)
(project--files-in-directory
dir
(project--dir-ignores project dir)
(and files
(mapconcat #'identity files " "))))))
(declare-function vc-git--program-version "vc-git")
(declare-function vc-git--run-command-string "vc-git")
(declare-function vc-hg-command "vc-hg")
(defun project--vc-list-files (dir backend extra-ignores)
(defun project--vc-git-ignore-to-spec (i)
(format
":(exclude,glob,top)%s"
(if (string-match "\\*\\*" i)
;; Looks like pathspec glob
;; format already.
i
(if (string-match "\\./" i)
;; ./abc -> abc
(setq i (substring i 2))
;; abc -> **/abc
(setq i (concat "**/" i))
;; FIXME: '**/abc' should also
;; match a directory with that
;; name, but doesn't (git 2.25.1).
;; Maybe we should replace
;; such entries with two.
(if (string-match "/\\'" i)
;; abc/ -> abc/**
(setq i (concat i "**"))))
i)))
(defun project--vc-list-files (dir backend extra-ignores &optional names no-gitignore)
(defvar vc-git-use-literal-pathspecs)
(pcase backend
(`Git
@ -518,35 +563,18 @@ backend implementation of `project-external-roots'.")
(args '("-z"))
(vc-git-use-literal-pathspecs nil)
files)
;; Include unregistered.
(setq args (append args
'("-c" "--exclude-standard")
(and project-vc-include-untracked '("-o"))))
(when extra-ignores
'("-c")
(and project-vc-include-untracked '("-o"))
(unless no-gitignore '("--exclude-standard"))))
(when (or files extra-ignores)
(setq args (append args
(cons "--"
(mapcar
(lambda (i)
(format
":(exclude,glob,top)%s"
(if (string-match "\\*\\*" i)
;; Looks like pathspec glob
;; format already.
i
(if (string-match "\\./" i)
;; ./abc -> abc
(setq i (substring i 2))
;; abc -> **/abc
(setq i (concat "**/" i))
;; FIXME: '**/abc' should also
;; match a directory with that
;; name, but doesn't (git 2.25.1).
;; Maybe we should replace
;; such entries with two.
(if (string-match "/\\'" i)
;; abc/ -> abc/**
(setq i (concat i "**"))))
i)))
extra-ignores)))))
'("--")
names
(mapcar
#'project--vc-git-ignore-to-spec
extra-ignores))))
(setq files
(mapcar
(lambda (file) (concat default-directory file))
@ -563,7 +591,8 @@ backend implementation of `project-external-roots'.")
(project--vc-list-files
(concat default-directory module)
backend
extra-ignores)))
extra-ignores
names)))
submodules)))
(setq files
(apply #'nconc files sub-files))))