-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcmdlist.el
1212 lines (1097 loc) · 60.6 KB
/
cmdlist.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; cmdlist.el --- Automated latex command maintenance
;; Copyright © 2020-2022 Joseph Helfer
;; Author: Joseph Helfer
;; URL: https://github.com/jojhelfer/cmdlist.el
;; Version: 1.2.2
;; Package-Requires: ((emacs "25.1"))
;; This file is NOT part of GNU Emacs.
;; This file 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, or (at your option)
;; any later version.
;; This file 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.
;; For a full copy of the GNU General Public License
;; see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; The idea is to maintain a global file with all the latex commands
;; you like to use. This package then provides tools to maintain that
;; file and automatically update your latex files based on it.
;; There is now also functionality which automatically adds
;; "usepackage" statements based on a global file listing which
;; packages provide which commands.
;; There are several convenience functions, most of which are listed
;; in the example keybindings below. The most import one is
;; cmdlist-conditional-update-latex-buffer. See the documentation of
;; those functions for more info, as well as the customization
;; variables below under "Variables".
;; Also see README.md (presently available at
;; https://github.com/jojhelfer/cmdlist.el) for more information.
;; My keybindings:
;; (evil-define-key 'normal LaTeX-mode-map
;; (kbd "SPC d c") 'cmdlist-update-save-and-compile
;; (kbd "SPC g u") 'cmdlist-conditional-update-buffer
;; (kbd "SPC g c") 'cmdlist-generate-and-add-cmd
;; (kbd "SPC g a") 'cmdlist-add-cmd-to-file
;; (kbd "SPC g r") 'cmdlist-generate-mathrm
;; (kbd "SPC g b") 'cmdlist-generate-mathbf
;; (kbd "SPC g B") 'cmdlist-generate-mathbb
;; (kbd "SPC g C") 'cmdlist-generate-mathcal
;; (kbd "SPC g F") 'cmdlist-generate-mathfrak
;; (kbd "SPC g o") 'cmdlist-generate-operatorname
;; (kbd "SPC g p") 'cmdlist-generate-package
;; (kbd "SPC g z") 'cmdlist-delete-unused-newcmds
;; (kbd "SPC g f") 'cmdlist-open-cmdlist-file)
;; TODO Look into just parsing the whole file, hopefully with some pre-existing tool. In particular, look into AucTeX's TeX-auto-file, TeX-auto-save, etc. We should also use TeX-find-macro-start and TeX-find-macro-end (and TeX-current-macro, etc.)
;; TODO Add commands to clean unused packages
;; TODO Try to guess command provider by looking through package/class files
;; TODO Ignore commands used in comments
;; TODO Be careful about commands (and packages) which are the empty string
;; TODO Add \usetikzlibrary support
;; TODO Add option to (via elisp) allow presence of certain commands to trigger insertion of arbitrary code in preamble
(require 'cl-lib)
(require 'seq)
;;;;;;;;;;;;;;;
;; Variables ;;
;;;;;;;;;;;;;;;
(defvar cmdlist-base-directory (file-name-concat user-emacs-directory "cmdlist")
"Default directory in which files with command definitions should be stored")
(defvar cmdlist-files `(,(file-name-concat cmdlist-base-directory "latex-commands.sty"))
"List of files containing `\\\(re\)newcommand' entries to be used by `cmdlist.el'. The first entry is also used to store new commands.")
(defvar cmdlist-heading "% Commands"
"Heading under which commands are inserted into the current buffer.")
(defvar cmdlist-visit-after-adding nil
"If non-nil, visit cmdlist file after adding new command, unless value is `ask', in which case ask first.")
(defvar cmdlist-also-add-to-buffer nil
"If non-nil, also add command to buffer when adding to global command list, unless value is `ask', in which case ask first.")
(defvar cmdlist-add-to-file-default nil
"If non-nil, add new commands to cmdlist file by default (i.e., without prefix argument) rather than to current buffer.")
(defvar cmdlist-braces-around-cmd-name nil
"If non-nil, put braces around the command name (as in `\newcommand{\foo}' when adding new commands.")
(defvar cmdlist-theorem-file (file-name-concat cmdlist-base-directory "latex-theorems.sty")
"File containing `\\newtheorem' entries to be used by `cmdlist'.")
(defvar cmdlist-theorem-heading "% Theorems"
"Heading which `cmdlist-conditional-update-buffer' looks for in deciding whether to update theorems.")
(defvar cmdlist-default-shared-counter "defn"
"Default shared counter for `newtheorem's.")
(defvar cmdlist-default-parent-counter "section"
"Default parent counter for `newtheorem's (if `cmdlist-default-shared-counter' is `nil').")
(defvar cmdlist-package-file (file-name-concat cmdlist-base-directory "latex-packages.sty")
"File each of whose lines contains a `\\usepackage' or `\\documentclass' command followed by a comment with a comma-separated list of commands and environment it provides.")
(defvar cmdlist-package-heading "% Packages"
"Heading under which packages are inserted into the current buffer.")
(defvar cmdlist-builtin-file (file-name-concat cmdlist-base-directory "latex-builtins")
"File containing a list of built-in latex commands and environments, one per line.")
(defvar cmdlist-cmd-defining-cmds
(list "let" "def" "newtheorem*" "foreach"
(list "newif"
(lambda (cmd)
;; Remove starting backslash if its there
(when (string-prefix-p "\\" cmd) (setq cmd (substring cmd 1)))
(append (list cmd)
(when (>= (length cmd) 2)
(let ((cmdnoif (substring cmd 2)))
(list cmd (concat cmdnoif "true") (concat cmdnoif "false"))))))))
"List of commands (besides `\(re\)newcommand' and `newtheorem') which define a new command (with a backslash). Each entry can also be a list (cmd fun) where cmd is the name of the command, and fun takes its argument and returns a list of commands it defines.")
(defvar cmdlist-ignore-at-symbol t
"If non-nil, `cmdlist-package-update-latex-buffer' will ignore commands containing `@'. (Note that, in any case, `@' is always treated as part of a command name, which will be incorrect if not within `\\makeatletter' region.)")
(defvar cmdlist-newcommands-to-ignore ()
"List of commands that should not be taken from `cmdlist-files' by `cmdlist-update-latex-buffer'. This can be used, for example, to prevent a \"renewcommand\" of a builtin command from getting inserted.")
(defvar cmdlist-test-minimal-file "/tmp/minimal.tex"
"Path of file created by `cmdlist-test-command-in-minimal-file' (which is called during `cmdlist-package-update-latex-buffer' when you come across an unrecognized command and select \"test it in a minimal LaTeX file\").")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Constants and internal variables ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defconst cmdlist--newcmd-regex "\\\\\\(new\\|renew\\|provide\\)command"
"Regex matching the basic command-defining commands (including the initial backslash) -- these are the only ones that are permitted in `cmdlist-files'.")
(defconst cmdlist--newthm-regex "\\\\newtheorem\\*?"
"Regex matching the theorem-defining commands (including the initial backslash) -- these are the ones that are permitted in `cmdlist-theorem-file'.")
(defvar-local cmdlist--local-ignored-cmds ()
"List of commands that `cmdlist-package-update-latex-buffer' should not ask about even if they are not currently defined in the current file or provided by a package. If equal to `t' (instead of a list), ignore *all* commands.")
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; General utility functions ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun cmdlist--list-of-things (things &optional fmt)
"Concatenates the strings in THINGS, separated by newlines with a number at the beginning of each line.
FMT specifies how the number should be formatted (default \"[%d]\")."
(unless fmt
(setq fmt "[%d] "))
(let ((lines (cl-mapcar (lambda (x n) (concat (format fmt n) x)) things (number-sequence 1 (length things)))))
(mapconcat 'identity lines "\n")))
(defun cmdlist--prompt-for-number (prompt &optional min max)
"Keep displaying PROMPT and asking the user for input until the input is an integer (between MIN and MAX, if provided), then return it as an integer."
(let ((response "")
(res 0))
(while (or (and min (< res min))
(and max (> res max))
(not (equal response (format "%d" res))))
(setq response (read-from-minibuffer prompt))
(setq res (string-to-number response)))
res))
(defun cmdlist--forward-brexp ()
"Move forward across one matching curly bracket expression."
(interactive)
(let ((started nil))
(while (and (search-forward "{" nil nil)
(if (eq (char-before (1- (point))) ?\\) t (setq started t) nil)))
(when started
(let ((count 1))
(while (> count 0)
(re-search-forward "[{}]")
(unless (eq (char-before (1- (point))) ?\\)
(setq count (+ count
(if (eq (char-before) ?\{) 1 -1)))))))))
(defmacro cmdlist--save-everything (&rest body)
"Save mark-and-excursion, restriction, and match-data."
(declare (indent 0) (debug t))
`(save-mark-and-excursion
(save-restriction
(save-match-data
,@body))))
(defmacro cmdlist--save-everything-widen (&rest body)
"Save mark-and-excursion, restriction, and match-data, and then widen before executing BODY."
(declare (indent 0) (debug t))
`(save-mark-and-excursion
(save-restriction
(save-match-data
(widen)
,@body))))
(defun cmdlist--read-lines (file)
"Read lines of file into a list and return it."
(with-temp-buffer
(insert-file-contents file)
(split-string (buffer-string) "\n" t)))
(defun cmdlist--file-exists-p (file)
"Call `file-exists-p' (and return its value) and print an error message if it returns `nil'. FILE can also be a list whose car is a filename, or a symbol whose `symbol-value' is the name of a file (or a list whose `car' is the filename), in which case mention the symbol name in the error message as well."
(let ((symb (when (symbolp file) file)))
(when symb
(setq file (symbol-value file)))
(when (listp file)
(setq file (car file)))
(or (file-exists-p file)
(progn
(message
(concat
"The file `" file "' "
(when symb (concat "(the value of the variable `" (symbol-name symb) "') "))
"does not exist."))
nil))))
;;;;;;;;;;;;;;;;;;;;;;
;; Parsing commands ;;
;;;;;;;;;;;;;;;;;;;;;;
(defun cmdlist--shloop-latex-arg ()
"Move past the latex argument (bracket expression, command name consisting of characters `[A-Za-z@]', or (possibly escaped) single character) starting after point, and return it."
;; Move past any spaces and comments
(while
(cond ((eq (char-after) ? ) (forward-char) t))
(cond ((eq (char-after) ?%) (forward-line) t)))
(let ((start (point)))
(pcase (char-after)
(?\{ (cmdlist--forward-brexp))
(?\\
(forward-char)
(if (string-match-p "[A-Za-z@]" (string (char-after)))
(re-search-forward "[A-Za-z@]+")
(forward-char)))
(_ (forward-char)))
(buffer-substring-no-properties start (point))))
(defun cmdlist--shloop-optional-latex-arg ()
"Move past the optional latex argument starting after point, and return it (not including the square brackets). This assumes the char after point is `['. The end of the optional argument is by definition the first unescaped closing square bracket which is not inside of an (unescaped) curly brace expression."
(forward-char)
(let ((start (point)))
(while (not (eq (char-after) ?\]))
(pcase (char-after)
(?\{ (cmdlist--forward-brexp))
(?\\
(forward-char 2))
(_ (forward-char))))
(forward-char)
(buffer-substring-no-properties start (1- (point)))))
(defun cmdlist--latex-cmd-under-point ()
"If point is on a latex command, return its name, else nil"
(let ((start (point)))
(cmdlist--save-everything
(when (< (point) (point-max))
(forward-char))
(when (search-backward "\\" nil t)
(let ((cmd (cmdlist--shloop-latex-arg)))
(when (and (<= start (point))
(string-match-p "[a-ZA-Z@]" (substring cmd 1 2)))
(substring cmd 1)))))))
(defun cmdlist--search-backward-incl (string &optional as-regex)
"Goto beginning of first instance of STRING occurring before or around point. Return nil if STRING was not found. If AS-REGEX is non-nill, treat STRING as a regex."
(let ((backward-fun (if as-regex #'re-search-backward #'search-backward))
(forward-fun (if as-regex #'re-search-forward #'search-forward))
(start (point))
(place)
(mdata))
(save-mark-and-excursion
(if (funcall backward-fun string nil t)
(progn (setq mdata (match-data))
(setq place (match-beginning 0))
(forward-char))
(goto-char (point-min)))
(while (and (funcall forward-fun string nil t)
(<= (match-beginning 0) start))
(setq mdata (match-data))
(setq place (match-beginning 0))
(goto-char place)
(forward-char)))
(when place
(set-match-data mdata)
(goto-char place))))
(defun cmdlist--re-search-backward-incl (regex)
"Goto beginning of first instance of REGEX occurring before or around point. Return nil if STRING was not found."
(cmdlist--search-backward-incl regex t))
(defun cmdlist--surrounding-newcmd (&optional regex)
"If point is not inside of a latex `\\\(re\)newcommand' (or given REGEX, e.g., `\\newtheorem'), return nil. Otherwise, return the text of the whole command. If the command definition is immediately followed by whitespace and then a comment, include that as well. This function will fail on nested command definitions."
(unless regex (setq regex cmdlist--newcmd-regex))
(let ((start (point))
(beg))
(cmdlist--save-everything
(when (cmdlist--re-search-backward-incl regex)
(setq beg (point))
(search-forward-regexp regex)
(cmdlist--shloop-latex-arg)
;; Allow any number of optional arguments
(while (eq (char-after) ?\[)
(cmdlist--shloop-optional-latex-arg))
(cmdlist--shloop-latex-arg)
;; Ditto
(while (eq (char-after) ?\[)
(cmdlist--shloop-optional-latex-arg))
(when (>= (point) start)
(when (string-match-p "^ *%" (buffer-substring (point) (line-end-position)))
(end-of-line))
(buffer-substring-no-properties beg (point)))))))
(defun cmdlist--scan-for-latex-cmds (&optional ignore-newcmds)
"Return all names of latex commands in current buffer."
(let (cmds last-newcmd-pos)
(cmdlist--save-everything-widen
(when ignore-newcmds
(goto-char (point-max))
(if (not (re-search-backward cmdlist--newcmd-regex nil t))
(setq ignore-newcmds nil)
(goto-char (match-end 0))
(when (eq (char-after) ?\{)
(cmdlist--forward-brexp))
(cmdlist--forward-brexp)
(setq last-newcmd-pos (point))))
(goto-char (point-min))
(while (search-forward "\\" nil t)
;; Make sure it's not a double backslash
(unless (and
(or (eq (char-after) ?\\)
;; We were using `looking-back' here, but it turns out to be extremely slow
;; (its documentation says as much).
;; We were also using looking-at above, which seems to not be slow,
;; but is still unnecessarily regexpful
(eq (char-after (- (point) 2)) ?\\))
;; Or rather, make sure it's not an even number of backslashes
(let ((beg (progn (while (eq (char-before) ?\\) (backward-char)) (point)))
(end (progn (while (eq (char-after) ?\\) (forward-char)) (point))))
(cl-evenp (- end beg))))
(let ((cmd (cmdlist--latex-cmd-under-point)))
(unless (or (not cmd)
(equal cmd "")
(and ignore-newcmds
(< (point) last-newcmd-pos)
(let ((snc (cmdlist--surrounding-newcmd)))
(and snc
(equal cmd (cmdlist--newcmd-name snc))))))
(push cmd cmds)))))
(reverse (delete-dups cmds)))))
(defun cmdlist--newcmd-name (cmd &optional regex)
"Return the name of the given `\\\(re\)newcommmand' (or given REGEX, e.g., `\\newtheorem')."
(unless regex (setq regex cmdlist--newcmd-regex))
(cmdlist--save-everything
(with-temp-buffer
(insert cmd)
(goto-char (point-min))
(re-search-forward (concat regex "[^*@a-zA-Z]"))
;; Take back the "look-ahead" in the regex
(backward-char)
(car (split-string (cmdlist--shloop-latex-arg) nil nil "[{}]*\\\\?")))))
(defun cmdlist--scan-for-newcmds (&optional regex)
"Return a list of all \\newcommands (or given REGEX, e.g., `\\newtheorem') in the current buffer"
(unless regex (setq regex cmdlist--newcmd-regex))
(let ((res))
(cmdlist--save-everything-widen
(goto-char (point-min))
(while (re-search-forward (concat regex "[^*@a-zA-Z]") nil t)
;; Take back the "look-ahead" in the regex
(backward-char)
(push (cmdlist--surrounding-newcmd regex) res)
(cmdlist--forward-brexp)))
(reverse res)))
(defun cmdlist--scan-file-for-newcmds (file &optional regex)
"Return a list of all \\newcommmands (or given REGEX, e.g., `\\newtheorem') in the given file"
(unless regex (setq regex cmdlist--newcmd-regex))
(save-mark-and-excursion
(with-temp-buffer
(insert-file-contents file)
(cmdlist--scan-for-newcmds regex))))
(defun cmdlist--assemble-newcmd (name defn &optional numargs opt)
"Return the `\\newcommand' defining NAME with definition DEFN. If NUMARGS or OPT are provided, they are the number of arguments and the default optional argument, respectively."
(concat
"\\newcommand"
(when cmdlist-braces-around-cmd-name "{")
"\\"
name
(when cmdlist-braces-around-cmd-name "}")
(when (and numargs (> numargs 0)) (format "[%d]" numargs))
(when opt (format "[%s]" opt))
"{" defn "}"))
(defun cmdlist--num-args-in-defn (defn)
"Return the greatest argument number in the latex command definition DEFN. Only works for definitions with at most 9 arguments."
(let ((maxnum 0))
(cmdlist--save-everything
(with-temp-buffer
(insert defn)
(goto-char (point-min))
(while (search-forward "#" nil t)
(let ((newnum (string-to-number (char-to-string (char-after)))))
(when (> newnum maxnum)
(setq maxnum newnum))))))
maxnum))
(defun cmdlist--sort-newcmds (reverse beg end)
"Variant of `sort-lines' (using the amazingly flexible `sort-subr') which keeps each (possibly multi-line) `\\newcommand' together, along with any lines before it up to the previous `\\newcommand'. Also ignores case."
(interactive "P\nr")
(cmdlist--save-everything
(narrow-to-region beg end)
(goto-char (point-min))
(let ;; To make `end-of-line' and etc. to ignore fields.
((inhibit-field-text-motion t)
(sort-fold-case t))
(sort-subr reverse 'forward-line
(lambda ()
(cmdlist--re-search-backward-incl cmdlist--newcmd-regex)
(goto-char (match-end 0))
(when (eq (char-after) ?\{) (cmdlist--forward-brexp))
(cmdlist--forward-brexp)
(end-of-line))
(lambda () (re-search-forward (concat cmdlist--newcmd-regex "{?")) nil)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Finding commands in a list of \newcommands ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun cmdlist--get-cmdlist-matches (cmdlist name)
"Return all elements of CMDLIST which are `\\\(re\)newcommand's defining the command NAME."
(let ((case-fold-search nil))
(seq-filter
(lambda (cmd)
(string-match-p (concat cmdlist--newcmd-regex "\\({\\\\" name "}\\|\\\\" name "\\)[{\\[]") cmd))
cmdlist)))
(defun cmdlist--select-cmds-from-cmdlist (cmdlist names exceptions)
"For each element of NAMES which is not in EXCEPTION, get all elements of CMDLIST which are `\\newcommand's defining this element, and return them in a list. Each time there are multiple matches, the user is queried for a choice."
(let ((choices))
(dolist (name names)
(unless (member name exceptions)
(let* ((cmds (cmdlist--get-cmdlist-matches cmdlist name))
(cmd (cmdlist--singleton-or-prompt
cmds
(concat "Multiple entries for " name ". Choose from above: "))))
(when cmd
(push cmd choices)))))
choices))
(defun cmdlist--replace-prompt (cmdlist entry)
"If the command defined in ENTRY is not in CMDLIST, return `t'. Otherwise, display all matches and ask the user to proceed. The user may select an entry to replace, which is then returned, cancel, in which case `nil' is return, or choose to add a new entry, in which `t' is returned."
(let* ((name (cmdlist--newcmd-name entry))
(matches (cmdlist--get-cmdlist-matches cmdlist name))
(many (< 1(length matches))))
(if (not matches) t
(let ((choice (car (read-multiple-choice
(concat "\"" name "\" already defined. Previous definition"
(when many "s")
":\n"
(cmdlist--list-of-things matches " ")
"\n\nWhat would you like to do?")
`((?q "quit")
(?r ,(concat "replace " (when many "an ") "existing definition"))
(?a "add a new definition"))))))
(pcase choice
(?q nil)
(?a t)
(?r (cmdlist--singleton-or-prompt matches "Which definition to replace? ")))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Modifying buffer and adding new commands ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun cmdlist--stick-at-top (heading text &optional sortfun)
"Stick TEXT (or each string in TEXT) after the first occurrence of HEADING, then sort it using SORTFUN if non-nil.
If HEADING does not occur, first insert HEADING before \\begin{document}"
(let ((starting-point))
(cmdlist--save-everything-widen
(goto-char (point-min))
(unless (search-forward heading nil t)
(search-forward "\\begin{document}")
(beginning-of-line)
(insert (concat heading "\n\n"))
(forward-line -2))
(forward-line)
(setq starting-point (point))
(forward-line -1)
(search-forward "\n\n" nil t)
(forward-line -1)
(when (not (listp text))
(setq text (list text)))
(dolist (tx text)
(insert (concat tx "\n")))
(when sortfun
(funcall sortfun nil starting-point (point))))))
(defun cmdlist--add-to-cmdlist-file (newcmd &optional file)
"Add NEWCMD to FILE (default: the first entry in `cmdlist-files') and then sort the lines.
If the command being defined in NEWCMD is already in FILE, ask for confirmation."
(unless file (setq file (car cmdlist-files)))
(if (and (file-readable-p file) (file-writable-p file))
(let ((cmdlist (cmdlist--scan-file-for-newcmds file)))
(let ((action (cmdlist--replace-prompt cmdlist newcmd)))
(if action
(cmdlist--save-everything
(let ((fibuf (find-file-noselect file)))
(with-current-buffer fibuf
(unless (eq t action)
(goto-char (point-min))
(search-forward (concat action "\n"))
(delete-region (match-beginning 0) (match-end 0)))
(goto-char (point-min))
(insert newcmd)
(insert "\n")
(cmdlist--sort-newcmds nil (point-min) (point-max))
(save-buffer))
(if (not cmdlist-visit-after-adding)
(kill-buffer fibuf)
(unless (and (eq cmdlist-visit-after-adding 'ask)
(not (y-or-n-p
(format "New entry\n %s\nadded to file %s\nVisit? "
newcmd file))))
(switch-to-buffer-other-window fibuf)
(search-forward newcmd)
(beginning-of-line))))
(message "New entry\n %s\nadded to file %s" newcmd file))
(message "Operation cancelled."))))
(message "File %s does not exist or not readable/writable." file)))
(defun cmdlist--add-newcmd (newcmd &optional heading file)
"Add the given new command to the top of the file under HEADING (default: `cmdlist-heading'), asking for confirmation if already defined.
If FILE is given, instead add it to FILE. If FILE is `t', it is set to the first element of `cmdlist-files'."
(if file
(progn
(unless (stringp file) (setq file (car cmdlist-files)))
(cmdlist--add-to-cmdlist-file newcmd file))
(unless heading (setq heading cmdlist-heading))
(let ((action (cmdlist--replace-prompt (cmdlist--scan-for-newcmds) newcmd)))
(if (not action)
(message "Operation cancelled")
(unless (eq t action)
(cmdlist--save-everything-widen
(goto-char (point-min))
(search-forward (concat action "\n"))
(delete-region (match-beginning 0) (match-end 0))))
(cmdlist--stick-at-top heading newcmd 'cmdlist--sort-newcmds)
(message "New entry\n %s\nadded under %s ." newcmd heading)))))
(defun cmdlist--generate-newcmd ()
"Generate a `\\newcommand' and return it. The name and definition of the new macro are queried for.
The number of arguments is guessed by how many are used in the command. If there is at least one, query for an optional argument.
If there is a macro under the current point, the default name is
this macro."
(let* ((name (cmdlist--latex-cmd-under-point))
(defn)
(numargs)
(optional))
(if (not name)
(setq name ""))
(setq name (read-from-minibuffer "Name of new command? \\" name))
(setq defn (read-from-minibuffer (concat "Definition of command \\" name " ? ")))
(setq numargs (cmdlist--num-args-in-defn defn))
(when (and (> numargs 0)
(y-or-n-p "Optional argument? "))
(setq optional
(read-from-minibuffer (format "Default optional argument for \\%s[%d]? "
name numargs))))
(cmdlist--assemble-newcmd name defn numargs optional)))
(defun cmdlist-update-latex-buffer (&optional heading files prefix)
"Add to current buffer under HEADING all commands defined in FILES which are present in the current file and not already defined. If PREFIX is non-nil, it is prepended to `\\newcommand'.
By default HEADING is `cmdlist-heading' and FILES are the files in the variable `cmdlist-files'."
(interactive)
(when (cmdlist--file-exists-p (or files 'cmdlist-files))
(unless heading (setq heading cmdlist-heading))
(unless files (setq files cmdlist-files))
(unless prefix (setq prefix ""))
(let* ((cmdlist (apply 'append (mapcar 'cmdlist--scan-file-for-newcmds files)))
(cmds (cmdlist--scan-for-latex-cmds))
(exceptions (append (mapcar 'cmdlist--newcmd-name (cmdlist--scan-for-newcmds)) (cmdlist--scan-for-other-defined-cmds) cmdlist-newcommands-to-ignore))
(newcmds (cmdlist--select-cmds-from-cmdlist cmdlist cmds exceptions)))
(if newcmds
(progn
(setq newcmds (mapcar (lambda (x) (concat prefix x)) newcmds))
(cmdlist--stick-at-top heading newcmds 'cmdlist--sort-newcmds)
(message "Added %d new commands:\n%s" (length newcmds) (cmdlist--list-of-things newcmds)))
(message "No commands added.")))))
(defun add-headers (headers)
"Add each string in the list HEADERS to the top of the document under the `documentclass' line. Each string is inserted preceded by `% ' and separated by a blank line. Do nothing and return `nil' if the document class line is not found, else return `t'."
(cmdlist--save-everything-widen
(goto-char (point-min))
(when (string-match-p "^\\\\documentclass{"
(buffer-substring-no-properties (point) (line-end-position)))
(forward-line)
(dolist (h headers)
(beginning-of-line)
(insert "\n% " h "\n")))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Convenience functions ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun cmdlist-add-cmd-to-file ()
"Add the `\\newcommand' definition under point to the first entry of `cmdlist-files'."
(interactive)
(when (cmdlist--file-exists-p 'cmdlist-files)
(let ((newcmd (cmdlist--surrounding-newcmd)))
(when newcmd
(cmdlist--add-newcmd (cmdlist--surrounding-newcmd) nil t)))))
(defun cmdlist-generate-and-add-cmd (tofile)
"Generate a `\\newcommand' with `cmdlist--generate-newcmd' and add it to current file under `cmdlist-heading'. With or without prefix argument (depending on `cmdlist-add-to-file-default'), add to first entry of `cmdlist-files' instead."
(interactive "P")
(when cmdlist-add-to-file-default
(setq tofile (not tofile)))
(when (or (not tofile) (cmdlist--file-exists-p 'cmdlist-files))
(let* ((newcmd (cmdlist--generate-newcmd))
(tobuf (and tofile
cmdlist-also-add-to-buffer
(if (eq cmdlist-also-add-to-buffer 'ask)
(y-or-n-p (format "Also add\n %s\nto buffer?" newcmd)) t))))
(cmdlist--add-newcmd newcmd nil (when tofile t))
(when tobuf
(cmdlist--add-newcmd newcmd nil nil)))))
(defun cmdlist--generate-and-add-fontletter (font &optional heading file)
"Generate a ``\\newcommand'' of the form `\\newcomand\\name{\\FONT{letter}}' and add it to the current buffer or to FILE with `cmdlist--add-newcmd' (hence FILE can be `t').
The name and letter are queried for, and by default are both the latex macro under point."
(when (or (not file)
(cmdlist--file-exists-p (if (eq file t) 'cmdlist-files file)))
(let* ((name (read-from-minibuffer
(format "Name of %s command? \\" font) (cmdlist--latex-cmd-under-point)))
(letter (read-from-minibuffer "Text? " name)))
(cmdlist--add-newcmd (cmdlist--assemble-newcmd name (format "\\%s{%s}" font letter)) heading file))))
(defun cmdlist-generate-mathbb (tofile)
"Run `cmdlist-generate-and-add-fontletter' with `mathbb'. Prefix argument puts it in the cmdlist file."
(interactive "P")
(cmdlist--generate-and-add-fontletter "mathbb" nil (when tofile t)))
(defun cmdlist-generate-mathbf (tofile)
"Run `cmdlist-generate-and-add-fontletter' with `mathbf'. Prefix argument puts it in the cmdlist file."
(interactive "P")
(cmdlist--generate-and-add-fontletter "mathbf" nil (when tofile t)))
(defun cmdlist-generate-mathrm (tofile)
"Run `cmdlist-generate-and-add-fontletter' with `mathrm'. Prefix argument puts it in the cmdlist file."
(interactive "P")
(cmdlist--generate-and-add-fontletter "mathrm" nil (when tofile t)))
(defun cmdlist-generate-mathcal (tofile)
"Run `cmdlist-generate-and-add-fontletter' with `mathcal'. Prefix argument puts it in the cmdlist file."
(interactive "P")
(cmdlist--generate-and-add-fontletter "mathcal" nil (when tofile t)))
(defun cmdlist-generate-mathfrak (tofile)
"Run `cmdlist-generate-and-add-fontletter' with `mathfrak'. Prefix argument puts it in the cmdlist file."
(interactive "P")
(cmdlist--generate-and-add-fontletter "mathfrak" nil (when tofile t)))
(defun cmdlist-generate-operatorname (tofile)
"Run `cmdlist-generate-and-add-fontletter' with `operatorname'. Prefix argument puts it in the cmdlist file."
(interactive "P")
(cmdlist--generate-and-add-fontletter "operatorname" nil (when tofile t)))
(defun cmdlist-generate-package ()
"Ask for a package name and stick it under `% Packages'"
(interactive)
(let ((name (read-from-minibuffer "Package name? ")))
(cmdlist--stick-at-top "% Packages" (concat "\\usepackage{" name "}"))))
(defun cmdlist--get-unused-newcmds (&optional whole-buffer regex)
"Return a list of `\\newcommand's in this buffer which are (apparently) not being used. If whole-buffer is nil, restrict to the commands under `cmdlist-heading'."
(let* ((cmds (cmdlist--scan-for-latex-cmds t))
(newcmds
(if whole-buffer
(cmdlist--scan-for-newcmds)
(cmdlist--save-everything-widen
(goto-char (point-min))
(search-forward cmdlist-heading)
(let ((st (point)))
(search-forward "\n\n")
(narrow-to-region st (point))
(cmdlist--scan-for-newcmds)))))
(unuseds (seq-filter
(lambda (x) (not (member (cmdlist--newcmd-name x) cmds)))
newcmds)))
unuseds))
(defun cmdlist-show-unused-newcmds ()
"Display `\\newcommand's in this buffer which are (apparently) not being used."
(interactive)
(let ((unuseds (append (cmdlist--get-unused-newcmds) (cmdlist--get-unused-newthms))))
(if unuseds
(message "Seemingly unused commands:\n\n%s" (mapconcat #'identity unuseds "\n"))
(message "No unused commands found"))))
(defun cmdlist-delete-unused-newcmds ()
"Delete (seemingly) unused `\\\(re\)newcommand's and `\\\\newtheorem's in this buffer, after prompting."
(interactive)
(let ((unuseds (append (cmdlist--get-unused-newcmds) (cmdlist--get-unused-newthms))))
(if unuseds
(when (y-or-n-p (format "Seemingly unused commands:\n%s\nDelete? " (cmdlist--list-of-things unuseds)))
(dolist (x unuseds)
(cmdlist--save-everything-widen
(goto-char (point-min))
(let ((case-fold-search nil))
(search-forward x))
; instead of kill-line, to avoid changing kill-ring
(delete-region (progn (beginning-of-line) (point))
(1+ (progn (end-of-line) (point)))))))
(message "No unused commands found"))))
(defun cmdlist-open-cmdlist-file ()
"`(find-file-other-window (car cmdlist-files))'"
(interactive)
(when (cmdlist--file-exists-p 'cmdlist-files)
(let ((cmd (cmdlist--latex-cmd-under-point)))
(find-file-other-window (car cmdlist-files))
(setq cmd (completing-read "Goto command: "
(mapcar 'cmdlist--newcmd-name (cmdlist--scan-for-newcmds))
nil nil nil nil cmd))
(when cmd
(let ((cmd-pos))
(cmdlist--save-everything
(goto-char (point-min))
(when (re-search-forward (concat cmdlist--newcmd-regex "{?\\\\" cmd "[}{\\[]" cmd) nil t)
(setq cmd-pos (point))))
(when cmd-pos
(goto-char cmd-pos)))))))
(defun cmdlist-conditional-update-buffer ()
"If `cmdlist-heading' is present in the buffer, run `cmdlist-update-latex-buffer'. If furthermore either of `cmdlist-theorem-heading' or `cmdlist-package-heading' is present, run `cmdlist-newthm-update-latex-buffer' or `cmdlist-package-update-latex-buffer', respectively."
(interactive)
(let ((has-heading nil)
(has-thm-heading nil)
(has-pkg-heading nil))
(cmdlist--save-everything-widen
(goto-char (point-min))
(setq has-heading (re-search-forward (concat "^" cmdlist-heading "$") nil t))
(goto-char (point-min))
(setq has-thm-heading (re-search-forward (concat "^" cmdlist-theorem-heading "$") nil t))
(goto-char (point-min))
(setq has-pkg-heading (re-search-forward (concat "^" cmdlist-package-heading "$") nil t)))
(when has-heading
(cmdlist-update-latex-buffer)
(when has-thm-heading
(cmdlist-newthm-update-latex-buffer))
(when has-pkg-heading
(cmdlist-package-update-latex-buffer)))))
(defun cmdlist-update-save-and-compile ()
"Save buffer, run `cmdlist-conditional-update-buffer', and then compile with `Tex-command'."
(interactive)
(cmdlist-conditional-update-buffer)
(save-buffer)
(TeX-command "LaTeX" 'TeX-master-file nil))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Environments and theorems ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun cmdlist--scan-for-latex-envs ()
"Return all names of latex environments in current buffer."
(let (envs)
(cmdlist--save-everything-widen
(goto-char (point-min))
(while (search-forward "\\begin{" nil t)
(backward-char)
(push (car (split-string (cmdlist--shloop-latex-arg) nil nil "[{}]")) envs)))
(reverse (delete-dups envs))))
(defun cmdlist--stick-thm-at-top (text &optional style shared-counter parent-counter)
"Run cmdlist--stick-at-top with TEXT and with heading set to `\\theoremstyle{STYLE}' (by default, STYLE is read from a comment at the end of text, which is removed, and is otherwise `plain'), and with no sorting. If SHARED-COUNTER is non-nil and is different from the theorem being added, add it as an optional argument after the first argument. If SHARED-COUNTER is nil or equal to the theorem being added, if PARENT-COUNTER is provided, add it as an optional argument after the second argument. If both SHARED-COUNTER and PARENT-COUNTER are nil, they are set to `cmdlist-default-shared-counter' and `cmdlist-default-parent-counter', respectively."
(unless (or shared-counter parent-counter)
(setq shared-counter cmdlist-default-shared-counter)
(setq parent-counter cmdlist-default-parent-counter))
(with-temp-buffer
(insert text)
;; Get style if present. Else default to `plain'.
(goto-char (point-min))
(if (re-search-forward "%\\(.*\\)" nil t)
(progn (unless style (setq style (match-string-no-properties 1)))
(replace-match ""))
(unless style (setq style "plain")))
;; If shared-counter or parent-counter is non-nil, remove hard-coded counter
(goto-char (point-min))
(when (and (or shared-counter parent-counter)
(re-search-forward "\\[[^\\]*\\]" nil t))
(replace-match ""))
(goto-char (point-min))
(let ((shared-is-current (and shared-counter (search-forward (concat "{" shared-counter "}") nil t))))
(goto-char (point-min))
(when (search-forward "}")
(if (and shared-counter (not shared-is-current))
(insert "[" shared-counter "]")
(when (and parent-counter (search-forward "}"))
(insert "[" parent-counter "]")))))
(setq text (buffer-substring-no-properties (point-min) (point-max))))
(cmdlist--stick-at-top (concat "\\theoremstyle{" style "}") text))
(defun cmdlist--get-unused-newthms ()
"Return a list of `\\newtheorem's in this buffer which are (apparently) not being used."
;; This was originally adapted from `cmdlist--get-unused-newcmds' but now seems different enough that there isn't anything obvious to factor out.
(let* ((envs (append (cmdlist--scan-for-latex-envs) (cmdlist--get-newtheorem-dependencies)))
(newthms (cmdlist--scan-for-newcmds cmdlist--newthm-regex))
(unuseds (seq-filter
(lambda (x) (not (member (cmdlist--newcmd-name x cmdlist--newthm-regex) envs)))
newthms)))
unuseds))
(defun cmdlist--get-newtheorem-dependencies ()
"Get a list of all the optional arguments in newtheorem commands appearing after the first (bracketed) argument."
(let (res)
(cmdlist--save-everything-widen
(goto-char (point-min))
(while (re-search-forward (concat cmdlist--newthm-regex "{[a-zA-Z]*}\\[\\([a-zA-Z]*\\)\\]") nil t)
(add-to-list 'res (match-string-no-properties 1)))
res)))
(defun cmdlist-newthm-update-latex-buffer (&optional file)
"Add to current buffer all ``\\newtheorem's defined in FILE (using `cmdlist--stick-thm-at-top') which are present in the current file and not already defined. By default FILE is `cmdlist-theorem-file'. Return a list of the new theorems added."
;; This was originally adapted from `cmdlist-update-latex-buffer' but now seems different enough that there isn't anything obvious to factor out.
(interactive)
(when (cmdlist--file-exists-p (or file 'cmdlist-theorem-file))
(unless file (setq file cmdlist-theorem-file))
(let* ((thmlist (cmdlist--scan-file-for-newcmds file cmdlist--newthm-regex))
(envs (append (cmdlist--scan-for-latex-envs) (cmdlist--get-newtheorem-dependencies)))
(exceptions (mapcar #'(lambda (x) (cmdlist--newcmd-name x cmdlist--newthm-regex)) (cmdlist--scan-for-newcmds cmdlist--newthm-regex)))
(newthms
(seq-filter
(lambda (thm)
(let ((thmname (cmdlist--newcmd-name thm cmdlist--newthm-regex)))
(and (member thmname envs)
(not (member thmname exceptions)))))
thmlist)))
(if (not newthms)
(message "No theorems added.")
(dolist (thm newthms)
(cmdlist--stick-thm-at-top thm))
;; If we have introduced any new dependencies, run it again
(when (cl-some
(lambda (dep)
(and (member dep thmlist)
(not (member dep envs))
(not (member dep exceptions))))
(cmdlist--get-newtheorem-dependencies))
(setq newthms (append newthms (cmdlist-newthm-update-latex-buffer))))
(message "Added %d new theorem:\n%s" (length newthms) (cmdlist--list-of-things newthms))
newthms))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Packages and built-ins ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun cmdlist--package-and-class-sort ()
"Sort lines in buffer ignoring initial `\\usepackage' or `\\documentclass'."
(cmdlist--save-everything
(goto-char (point-min))
(let ((sort-fold-case t))
(sort-subr nil 'forward-line 'end-of-line
(lambda ()
(save-match-data
(and (re-search-forward
"^\\\\\\(usepackage\\|documentclass\\)" nil t) nil)))))))
(defun cmdlist--scan-package-file (packages &optional recurse file pkgs-instead)
"Return a list of all comma-separated entries appearing after `%'s in FILE (default is `cmdlist-package-file') in lines defining packages in PACKAGES. If RECURSE is non-nil, also recursively include entries appearing in packages appearing after the second `%' in some line. If PKGS-INSTEAD is non-nil, return the entries after the second `%' instead of the first."
(unless file (setq file cmdlist-package-file))
;; If we're recursing, add all packages.
(when recurse
(let ((new-pkgs packages))
(while new-pkgs
(let ((newer-pkgs ()))
(dolist (p (cmdlist--scan-package-file new-pkgs nil file t))
(when (not (member p packages))
(push p packages)
(push p newer-pkgs)))
(setq new-pkgs newer-pkgs)))))
;; Make sure we're sorted
;; Warning! (sort) is destructive! (It returns the sorted list, but mangles the original one!)
(setq packages (seq-sort 'string< packages))
(let ((result ()))
(with-temp-buffer
(insert-file-contents file)
;; Make sure we're sorted here too
(cmdlist--package-and-class-sort)
(dolist (p packages)
(goto-char (point-min))
(when (and (re-search-forward (concat "^\\\\\\(usepackage\\|documentclass\\){"
(regexp-quote p) "}") nil t)
(search-forward "%" (line-end-position) t))
(setq result
(append result
(split-string
(or (nth (if pkgs-instead 2 1)
(split-string (thing-at-point 'line t) "%" nil (string ?\n)))
"") "," t)))))
(cl-remove-duplicates result))))
(defun cmdlist--match-in-package-file (name &optional recurse file)
"Return a list of all lines (minus final `%.*' in FILE (default `cmdlist-package-file)) which start with `\\usepackage' and include NAME in a comma-separated list after a `%'. If RECURSE is non-nil, also recursively return any lines which have a package after their second `%' which provides NAME."
(unless file (setq file cmdlist-package-file))
(with-temp-buffer
(insert-file-contents file)
(let ((res) ;; result
(names (list name)) ;; List of all names we're looking for
(pkgs-for-rec ()) ;; List of all packages we are recursively looking for
(is-pkg nil) ;; Whether we're currently looking for a package.
(case-fold-search nil)) ;; Unfortunate that this isn't the default
(while (setq name (pop names))
(while (re-search-forward
(concat "^\\(\\\\usepackage{\\([^}]*\\)}\\)%"
(when is-pkg "[^%]*%")
"\\([^%]*,\\)?"
(regexp-quote name)
"\\([,%]\\|\n\\)")
nil t)
(push (match-string 1) res)
(when (and recurse (not (member (match-string 2) pkgs-for-rec)))
(push (match-string 2) names)
(push (match-string 2) pkgs-for-rec)))
(setq is-pkg t))
res)))
(defun cmdlist--scan-for-defined-envs ()
"Return a list of all environments defined by `\\\(re\)newenvironment's in the current buffer."
(let ((res))
(cmdlist--save-everything-widen
(goto-char (point-min))
(while (re-search-forward "\\\\r?e?newenvironment" nil t)
(push (car (split-string (cmdlist--shloop-latex-arg) nil nil "[{}]")) res)))
(reverse res)))
(defun cmdlist--scan-for-packages ()
"Return a list of all packages `\\usepackage'd in current buffer."
(let ((res))
(cmdlist--save-everything-widen
(goto-char (point-min))
(while (re-search-forward "\\\\\\(usepackage\\|RequirePackage\\)" nil t)
(while (not (eq (char-after) ?{)) (forward-sexp))
(push (car (split-string (cmdlist--shloop-latex-arg) nil nil "[{}]")) res)))
(reverse res)))
(defun cmdlist--get-document-class ()
"Return a singleton list with the document class of this document or `nil'."
(cmdlist--save-everything-widen
(goto-char (point-min))
(when (search-forward "\\documentclass")
(while (not (eq (char-after) ?{)) (forward-sexp))
;; list car is just to make it clearer what's happening
(list (car (split-string (cmdlist--shloop-latex-arg) nil nil "[{}]"))))))
(defun cmdlist--singleton-or-prompt (l prompt)
"L should be a list of strings. If L is a singleton, return it, otherwise prompt with PROMPT for an element to choose. If L is empty, return nil."
(when l
(if (= (length l) 1)
(car l)
(setq prompt (concat (cmdlist--list-of-things l) "\n" prompt))
(nth
(- (cmdlist--prompt-for-number prompt 1 (length l)) 1)
l))))
(defun cmdlist--choose-and-add-package-or-class (cmd &optional package-file class pkgchoice)
"Prompt for a package (or if CLASS, document class) from PACKAGE-FILE (default `cmdlist-package-file') and add cmd to it. Create a new package or class if necessary. Then sort the package file. Return a newline-terminated message explaining what happened. If PKGCHOICE is non-nil, use that as chosen package or document class instead of prompting."
(unless package-file (setq package-file cmdlist-package-file))
(let* ((definer (if class "\\documentclass" "\\usepackage"))
(pkg-or-class-list
(mapcar (lambda (x) (cmdlist--newcmd-name x definer))
(seq-filter
(lambda (x) (string-prefix-p definer x))
(cmdlist--read-lines package-file))))
(pkg
(or pkgchoice
(completing-read
(concat "Choose a " (if class "document class" "package") ": ")
pkg-or-class-list))))
(with-temp-file package-file
(when (file-exists-p package-file)
(insert-file-contents package-file))
(if (member pkg pkg-or-class-list)
;; We are adding to an existing line.
(progn (goto-char (point-min))