-
Notifications
You must be signed in to change notification settings - Fork 1
/
latex-table-wizard.el
2102 lines (1803 loc) · 85 KB
/
latex-table-wizard.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
;;; latex-table-wizard.el --- Magic editing of LaTeX tables -*- lexical-binding: t; -*-
;; Copyright (C) 2022, 2023 Free Software Foundation, Inc.
;; Author: Enrico Flor <[email protected]>
;; Maintainer: Enrico Flor <[email protected]>
;; URL: https://github.com/enricoflor/latex-table-wizard
;; Version: 1.5.4
;; Keywords: convenience
;; Package-Requires: ((emacs "27.1") (auctex "12.1") (transient "0.3.7"))
;; SPDX-License-Identifier: GPL-3.0-or-later
;; This program 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.
;;
;; This program 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 this program. If not, see
;; <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This package provides you with commands to smartly navigate and
;; edit large and complex LaTeX table-like environments with a
;; transient.el-based interface. Table-like environments are portions
;; of text delimited by a pair of matching "\begin" and "\end" macros
;; that organize output text into aligned colums.
;; The entry point of the package is
;; M-x latex-table-wizard
;; while point is inside of a table(-like) environment. From there, you
;; can do several things such as:
;; + navigate "logically" (that is, move by cells);
;; + insert or kill rows or column;
;; + move arbitrary cells or groups of cells around;
;; + align the table in different ways (however alignment is not
;; needed for the functionalities above).
;; Standard LaTeX2e table environments are supported out of the box,
;; but you can define additional ones. The entry point for
;; customization is
;; M-x latex-table-wizard-customize
;; The keybinding set by default in the transient prefix are inspired
;; to some extent by Emacs defaults. If you want to change these
;; keybindings you should change the value of the variable
;; latex-table-wizard-transient-keys.
;; By default, the syntax this package expects is the one of standards
;; LaTeX tabular environments, whereby "&" separates columns and "\\"
;; separates rows. Additional, or different, types of table-like
;; environments (with their own syntax separators) can be added by the
;; user. This is done by adding mappings to
;; latex-table-wizard-new-environments-alist. Suppose I want to
;; define a new table like environment whose name is "mytable", whose
;; column and row separators are strings like "\COL" and "\ROW", and
;; the LaTeX macro to add a horizontal line is "\myhline{}":
;; \begin{mytable}
;; ...
;; \end{mytable}
;; For latex-table-wizard to handle this table, just add the following
;; cons cell to latex-table-wizard-new-environments-alist:
;; '("mytable" . (:col '("\\COL")
;; :row '("\\ROW")
;; :lines '("myhline")))
;; Each value is a list of strings to allow for more than one macro to
;; have the same function.
;; See the Info page for a complete overview of the package.
;;; Code:
;;; Dependencies
(require 'tex)
(require 'latex)
(require 'seq)
(eval-when-compile (require 'rx))
(require 'regexp-opt)
(eval-when-compile (require 'subr-x))
(require 'transient)
(defgroup latex-table-wizard nil
"LaTeX table wizard configuration options."
:prefix "latex-table-wizard-"
:group 'convenience)
;;; Regular expressions and configuration options
(defcustom latex-table-wizard-allow-detached-args nil
"If t, allow arguments of macros to be detached in parsing.
This means that if non-nil, this package will parse argument
groups (strings in brackets or in braces) as arguments of the
macro even if they are separated by whitespace, one line break,
and comments. This conforms to how LaTeX interprets them.
However, doing this may cause some troubles if you happen to have
a string in braces at the start of the first
cell (position (0,0)): this is because if there is no blank line
between that cell and the table opening \\='\\begin\\=' macro
with its arguments, that string which should be in the first cell
may end up being parsed as an additional argument to the
\\='\\begin\\=' macro.
You avoid this danger if you set this variable to nil, but then
you should never have whitespace between the macro and its
arguments and between the arguments themselves."
:type 'boolean)
(defcustom latex-table-wizard-warn-about-detached-args t
"If t, warn about suspect cases of non-allowed detached arguments.
The warning will be echoed in the echo area any time that, while
parsing the table, cases in which a LaTeX macro and its
arguments, or two arguments of the same LaTeX macro might be
separated from its arguments by whitespace or comment are found.
Since the parser doesn't quite know what string preceded by an
unescaped backslash is a valid LaTeX macro and whether it accepts
what number of arguments, false positives are likely to be found.
If `latex-table-wizard-allow-detached-args' is non-nil, detached
arguments are allowed and so no warning will ever be issued
regardless of the value of this variable."
:type 'boolean
:link '(variable-link latex-table-wizard-allow-detached-args))
(defcustom latex-table-wizard-column-delimiters '("&")
"List of strings that are column delimiters if unescaped."
:type '(repeat string))
(defcustom latex-table-wizard-row-delimiters '("\\\\")
"List of strings that are row delimiters if unescaped."
:type '(repeat string))
(defcustom latex-table-wizard-hline-macros '("cline"
"vline"
"midrule"
"hline"
"toprule"
"bottomrule")
"Name of macros that draw horizontal lines.
Each member of this list is a string that would be between the
\"\\\" and the arguments."
:type '(repeat string))
(defcustom latex-table-wizard-new-environments-alist nil
"Alist mapping environment names to property lists.
The environment name is a string, for example \"foo\" for an
environment like
\\begin{foo}
...
\\end{foo}
The cdr of each mapping is a property list with three keys:
:col
:row
:lines
The values for :col and :row are two lists of strings.
The value for :lines is a list of strings just like is the case
for `latex-table-wizard-hline-macros', each of which is the name
of a macro that inserts some horizontal line. For a macro
\"\\foo{}\", use string \"foo\"."
:type '(alist :key-type (string :tag "Name of the environment:")
:value-type (plist :key-type symbol
:options (:col :row :lines)
:value-type (repeat string)))
:link '(variable-link latex-table-wizard-hline-macros))
;; Every time latex-table-wizard--parse-table is evaluated, the values
;; of the variables below are set:
(defvar latex-table-wizard--current-col-delims nil)
(defvar latex-table-wizard--current-row-delims nil)
(defvar latex-table-wizard--current-hline-macros nil)
(defun latex-table-wizard--set-current-values ()
"Set temporary values that specify the syntax of the environment.
If the current environment is one that is mapped to something in
`latex-table-wizard-new-environments', set the values accordingly."
(let* ((values (cdr (assoc (LaTeX-current-environment)
latex-table-wizard-new-environments-alist)))
(col (plist-get values :col))
(row (plist-get values :row))
(lines (plist-get values :lines)))
(if col
(setq latex-table-wizard--current-col-delims col)
(setq latex-table-wizard--current-col-delims
latex-table-wizard-column-delimiters))
(if row
(setq latex-table-wizard--current-row-delims row)
(setq latex-table-wizard--current-row-delims
latex-table-wizard-row-delimiters))
(if lines
(setq latex-table-wizard--current-hline-macros lines)
(setq latex-table-wizard--current-hline-macros
latex-table-wizard-hline-macros))))
(defvar latex-table-wizard-after-table-modified-hook nil
"Hook ran after table has been modified.
This hook is ran only after certain `latex-table-wizard'
interactive commands are called.")
;;; Parsing
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; The central data structure is the CELL, which is a plist with ;;
;; four keys: ;;
;; ;;
;; + :column (column number, starting from 0 as the leftmost column) ;;
;; + :row (row number, starting from 0 as the top row) ;;
;; + :start (marker, beginning of inside of the cell) ;;
;; + :end (marker, end of inside of the cell) ;;
;; ;;
;; A parse of a table is a list of all its cells represented as such ;;
;; plists. ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; this rx expression matches what can separate different arguments of
;; a (La)TeX macro: whitespace and comments. If
;; latex-table-wizard-allow-detached-args is nil, this rx
;; expression will effectively never be used.
(defconst latex-table-wizard--blank-detach-arg-re
(rx (seq (* space)
(? (seq "%" (* not-newline)))
(? "\n")
(* (seq (* space) "%" (* not-newline) "\n"))
(* space)))
"Regexp matching what can separate a macro from its arguments.")
(defvar latex-table-wizard--detached nil)
(defun latex-table-wizard--warn-detached ()
"Warn the user if suspected detached macros are found in table.
A macro is detached if there is any blank string separating the
macro from its arguments or one argument from the next.
Don't do anything if
`latex-table-wizard-allow-detached-args' is non-nil,
because it means that the user is aware of this and is taking the
measures needed for the parser not to be confused."
(unless latex-table-wizard-allow-detached-args
(let ((message-log-max 0))
(message (concat "Warning: suspect detached macro found.\n"
"If the table isn't parsed correctly, "
"try not to separate arguments from macro,\n"
"or set latex-table-wizard-allow-detached-args"
"to t.")))))
(defun latex-table-wizard--macro-at-point (&optional pos bound detached-args)
"Return data about LaTeX macro at point or at POS, if any.
If POS is nil, check whether point is currently on a LaTeX macro,
otherwise check if buffer position or marker POS is on one.
If BOUND is nil, look only as far back as
`latex-table-wizard--table-begin' (if that is nil too, it
defaults to the minimum available position in current buffer),
otherwise stop at BOUND (a buffer position or marker).
Return value is a list that start with the buffer positions of
begin and end of the macro, and then the strings corresponding to
the name and each of its arguments.
This function knows nothing about the signature of the macro, so
it's greedy (it keeps eating up arguments at it finds them, but
it does not skip over an empty line).
If DETACHED-ARGS is non-nil, allow for arguments of the macro to
be separated by whitespace and one line break.
Should be rather costly, but robust."
(declare (side-effect-free t))
(save-match-data
(let ((limit (or bound latex-table-wizard--table-begin (point-min)))
(skip-some (lambda ()
(when detached-args
(skip-chars-forward " \t"))
(while (looking-at-p "%.*")
(goto-char (line-beginning-position 2)))
(when detached-args
(skip-chars-forward "\n" (line-end-position 2))
(skip-chars-forward " \t"))))
(start (or pos (point)))
guess b return e intermediate)
(save-excursion
(goto-char start)
(when (and (not (TeX-escaped-p (1- (point))))
(looking-back "[\]}]" (line-beginning-position)))
(forward-char -1))
(setq guess (ignore-errors (LaTeX-what-macro limit)))
(cond ((and (looking-back "\\\\[[:alpha:]]*" (line-beginning-position))
(not (TeX-escaped-p (match-beginning 0))))
(goto-char (match-beginning 0)))
((and (not guess)
(looking-at-p "\\\\")
(not (TeX-escaped-p)))
nil)
((not guess)
(TeX-search-unescaped "\\\\[[:alpha:]]" 'backward t nil t))
((eq (nth 1 guess) 'env)
(TeX-search-unescaped "\\begin" 'backward nil nil t))
((eq (nth 1 guess) 'mac)
(TeX-search-unescaped (concat "\\" (nth 0 guess))
'backward nil nil t))
(t
(TeX-search-unescaped (concat "\\begin{" (nth 0 guess))
'backward nil nil t)))
(setq b (point)
intermediate (point))
(when (looking-at "\\\\[^\[{\s]+")
(goto-char (match-end 0)))
(push (buffer-substring-no-properties intermediate (point))
return)
(funcall skip-some)
(while (looking-at-p "[\[{]")
(setq intermediate (point))
(forward-sexp 1)
(push (buffer-substring-no-properties intermediate (point))
return)
(funcall skip-some))
(skip-chars-backward " \t\n")
(setq e (point))
(unless (>= start e)
(cons b (cons e (nreverse (mapcar #'string-trim return)))))))))
(defun latex-table-wizard--goto-end-of-macro (&optional pos names re)
"If looking at unescaped macro named NAME, go to its end.
If POS is non-nil, it is a marker or buffer position, and it is
the position from which the macro whould be searched. If nil, it
defaults to the current value of `point'.
If NAMES is nil, skip any LaTeX macro that point is looking at.
Otherwise, it is a list of strings, and this funcation will only
skip those macros whose name (without the backslash) is in NAME.
If RE is non-nil, it is a regular expression this function will
skip every macro whose name is matched by it (ignoring the value
passed as NAMES)
Call `latex-table-wizard--warn-detached' if the macro is
separated from its arguments, or any two successive arguments are
separated from each other."
(when-let* ((macro
(latex-table-wizard--macro-at-point
pos nil latex-table-wizard-allow-detached-args))
(mname (string-trim-left (nth 2 macro) "\\\\")))
(when (or (and (not names) (not re))
(member mname names)
(when re (string-match re mname)))
(goto-char (nth 1 macro)))))
(defun latex-table-wizard--get-out ()
"If point is on an environment delimiting macro, move out.
If it is on an \\='end\\=' macro, move to its end, otherwise to
its beginning."
(latex-table-wizard--set-current-values)
(when-let* ((macro (latex-table-wizard--macro-at-point))
(name (string-trim-left "\\\\" (nth 2 macro))))
(cond ((equal name "begin")
(goto-char (nth 0 macro)))
((equal name "end")
(goto-char (nth 1 macro)))
(t nil))))
(defun latex-table-wizard--skip-stuff (&optional bound)
"Skip comments, blank space and hline macros.
Hline macros are LaTeX macros whose name is a string in
`latex-table-wizard--current-hline-macros'.
BOUND is a buffer position or marker: this function will not skip
beyond that point. If it is nil, it defaults to the value of
`latex-table-wizard--table-end' (if that is nil too, it is the
maximum available position in current buffer)."
(let ((lim (or bound
latex-table-wizard--table-end
(save-excursion
(goto-char (point-max))
(point-marker))))
done new-start-of-line)
(catch 'stop
(while (and (not done) (<= (point) lim))
(skip-syntax-forward " ")
(let ((temp-pos (point)))
(when (looking-at "\n\\|%")
(forward-line)
(setq new-start-of-line (point))
(when (looking-at (concat
"[[:space:]]*"
(string-join
latex-table-wizard--current-col-delims
"\\|")))
(throw 'stop nil)))
(ignore-errors
(latex-table-wizard--goto-end-of-macro
nil latex-table-wizard--current-hline-macros))
(when (looking-at "\n\\|%")
(forward-line)
(setq new-start-of-line (point)))
(when (= (point) temp-pos)
;; we haven't moved since trying to skip whitespace, we
;; are done here.
(setq done t)))))
(when new-start-of-line (goto-char new-start-of-line))))
(defun latex-table-wizard--get-cell-boundaries (col-re row-re beginning limit)
"Return boundaries of current cell (where point is).
What is returned is a list of the form
(B E EOR)
where B and E are markers (beginning and end of the cell), and
EOR is t iff this cell is the rightmost cell in the current row,
nil otherwise.
COL-RE and ROW-RE are regular expressions matching column and row
delimiters respectively.
BEGINNING is a buffer position that is assumed to be where the
topmost point a cell left boundary can be.
LIMIT is a buffer position at which the parsing stops."
(declare (side-effect-free t))
(save-match-data
(let ((beg (point-marker))
end end-of-row)
(latex-table-wizard--skip-stuff limit)
(unless (string-blank-p (buffer-substring-no-properties beg (point)))
(setq beg (point-marker)))
(while (and (< (point) limit) (not end))
(let ((macro (latex-table-wizard--macro-at-point
nil beginning latex-table-wizard-allow-detached-args)))
(cond ((looking-at-p "[[:space:]]+%")
(TeX-comment-forward 1))
((TeX-escaped-p)
;; whatever we are looking at is escaped so we just go
;; one step forward
(forward-char 1))
((looking-at col-re)
;; a column delimiter: bingo
(setq end (point-marker))
(goto-char (match-end 0)))
((looking-at row-re)
;; a row delimiter: bingo
(let ((after-del (save-excursion (goto-char (match-end 0))
(point-marker)))
(end-of-previous-cell
(progn (goto-char (match-beginning 0))
(point-marker))))
(goto-char after-del)
(setq end end-of-previous-cell
end-of-row t)
(latex-table-wizard--skip-stuff limit)))
((looking-at "\\$\\|{")
(unless (ignore-errors (forward-sexp))
(forward-char 1)))
((looking-at "\\\\(\\|\\\\\\[")
(TeX-search-unescaped "\\\\)\\|\\\\\\]" 'forward t nil t))
((looking-at "[[:space:]]*\\\\\\(begin[\[{]\\)")
(goto-char (match-beginning 1))
(LaTeX-find-matching-end))
(macro
(goto-char (nth 1 macro)))
(t (forward-char 1)))))
(list beg end end-of-row))))
(defsubst latex-table-wizard--get-env-ends (table)
"Return leftmost and rightmost positions in TABLE.
TABLE is a list of cell plists. The return type is a cons
cell (B . E) with B and E being markers.
Note that if TABLE is the list of all cells (i.e. the return
value of `latex-table-wizard--parse-table'), B and E will not
necessarily correspond to `latex-table-wizard--table-begin' and
`latex-table-wizard--table-end'. These value should be equal
only if there are no hline macros at the beginning or at the end
of the table (which are part of the table environment but not of
any cell)."
(declare (pure t) (side-effect-free t))
`(,(apply #'min (mapcar (lambda (x) (plist-get x :start)) table))
.
,(apply #'max (mapcar (lambda (x) (plist-get x :end)) table))))
(defvar-local latex-table-wizard--parse nil
"Data from a parsed table environment.
The value of this variable is a list of the form
(H P)
where H is a hash string and P is a list of plists (that is, of
cell objects). H is the sha256 of the corresponding buffer
substring and P is the parse of the the environment.")
(defvar-local latex-table-wizard--table-begin nil
"Marker corresponding to the beginning of the inside of the table.
The value of this variable is set by
`latex-table-wizard--parse-table'.
Note that this is not the left boundary of the top left cell: the
value of this variable is a position preceding any hline macro
that is inside of the table environment but not considered part
of a cell. If you need that value instead you need to get the
car of the result of applying `latex-table-wizard--get-env-ends'
to the list of all cells.")
(defvar-local latex-table-wizard--table-end nil
"Marker corresponding to the end of the inside of the table.
The value of this variable is set by
`latex-table-wizard--parse-table'.
Note that this is not the right boundary of the bottom right
cell: the value of this variable is a position preceding any
hline macro that is inside of the table environment but not
considered part of a cell. If you need that value instead you
need to get the cdr of the result of applying
`latex-table-wizard--get-env-ends' to the list of all cells.")
(defun latex-table-wizard--parse-table ()
"Parse table(-like) environment point is in.
Return a list of plists, each of which is a cells and has the
form
(:column C :row R :start S :end E).
Each value is an integer, S and E are markers.
If point is inside the table but between two cells, relocate it
to the one that precedes point."
(setq latex-table-wizard--detached nil)
(let* ((bl-rx (if latex-table-wizard-allow-detached-args
latex-table-wizard--blank-detach-arg-re
""))
(cells-list '())
(col 0)
(row 0)
(env-beg (save-excursion
(LaTeX-find-matching-begin)
(latex-table-wizard--goto-end-of-macro (1+ (point)))
(ignore-errors
(latex-table-wizard--goto-end-of-macro
nil latex-table-wizard--current-hline-macros))
(point-marker)))
(env-end (save-excursion
(LaTeX-find-matching-end)
(if-let ((end-macro
(latex-table-wizard--macro-at-point
(1- (point))
latex-table-wizard-allow-detached-args)))
(goto-char (car end-macro))
(TeX-search-unescaped (concat "\\\\end" bl-rx "[{\[]")
'backward t env-beg t))
(re-search-backward "[^[:space:]]" nil t)
(while (TeX-in-comment)
(TeX-search-unescaped "%" 'backward t env-beg t)
(re-search-backward "[^[:space:]]" nil t))
(unless (eolp) (forward-char 1))
(point-marker)))
(hash (secure-hash 'sha256
(buffer-substring-no-properties env-beg
env-end))))
(save-excursion (goto-char env-beg)
(latex-table-wizard--set-current-values))
(let ((col-re (regexp-opt latex-table-wizard--current-col-delims))
(row-re (regexp-opt latex-table-wizard--current-row-delims)))
(if (and (ignore-errors (<= latex-table-wizard--table-begin
(point-marker)
latex-table-wizard--table-end))
(equal env-beg latex-table-wizard--table-begin)
(equal env-end latex-table-wizard--table-end)
(equal hash (car latex-table-wizard--parse)))
(nth 1 latex-table-wizard--parse)
(setq latex-table-wizard--table-begin env-beg
latex-table-wizard--table-end env-end)
(save-excursion
(goto-char env-beg)
;; we need to make some space between the end of of the \begin
;; macro and the start of the (0,0) cell
(if (looking-at-p "[[:space:]]")
(forward-char 1)
(insert " "))
(TeX-comment-forward 1)
(while (looking-at-p "[[:space:]]*%")
(TeX-comment-forward 1))
(skip-syntax-backward " ")
(while (< (point) env-end)
(let ((data (latex-table-wizard--get-cell-boundaries
col-re row-re env-beg env-end)))
(push (list :column col
:row row
:start (nth 0 data)
:end (if (nth 1 data) (nth 1 data) env-end))
cells-list)
(if (nth 2 data) ; this was the last cell in the row
(setq row (1+ row)
col 0)
(setq col (1+ col)))
;; if we just hit the end of a row and the next thing coming
;; is another row delimiter, skip that one (because you are
;; not in a cell)
(while (and (nth 2 data)
(save-excursion
(skip-syntax-forward " ")
(looking-at-p row-re)))
(re-search-forward row-re nil t)))))
(setq latex-table-wizard--parse (list hash cells-list))
(when latex-table-wizard--detached
(latex-table-wizard--warn-detached))
cells-list))))
(defun latex-table-wizard--get-cell-pos (table prop-val1
&optional prop-val2)
"Return the cell plist from TABLE at specific position.
The position is given by PROP-VAL1 and PROP-VAL2, each of which
is a cons cell of the form (P . V), where P is either
\\=':column\\=' or \\=':row\\=' and V is the corresponding value.
If prop-val2 is nil, it is assumed that TABLE is a list of cells
that only differ for the property in the car of PROP-VAL1 (in
other words, that TABLE is either a column or a row)"
(declare (pure t) (side-effect-free t))
(catch 'cell
(if prop-val2
(dolist (x table)
(when (and (= (cdr prop-val1) (plist-get x (car prop-val1)))
(= (cdr prop-val2) (plist-get x (car prop-val2))))
(throw 'cell x)))
(dolist (x table)
(when (= (cdr prop-val1) (plist-get x (car prop-val1)))
(throw 'cell x))))))
(defun latex-table-wizard--sort (table same-line dir)
"Return a sorted table, column or row given TABLE.
TABLE is a list of cells (a list of plists) that includes
the cell point is in.
If SAME-LINE is non-nil, return sorted current column (if DIR is
either \\='next\\=' or \\='previous\\=') or current row (if
DIR is either \\='forward\\=' or \\='backward\\=').
If SAME-LINE is nil, return sorted table, so that given a table
like this:
A & B & C \\
D & E & F
if DIR is either \\='forward\\=' or \\='backward\\=', A follows
F, C precedes D and so on; and if DIR is either \\='next\\=' or
\\='previous\\=', A follows F, D precedes B and so on."
(declare (pure t) (side-effect-free t))
(let* ((vert (memq dir '(next previous)))
(prop (if vert :row :column))
(thing (if vert
(latex-table-wizard--get-thing 'column table)
(latex-table-wizard--get-thing 'row table)))
(copy-table (copy-sequence table)))
(if (not same-line)
(sort copy-table (lambda (x y)
(let ((rows (list (plist-get x :row)
(plist-get y :row)))
(cols (list (plist-get x :column)
(plist-get y :column))))
(cond ((and vert (apply #'= cols))
(apply #'< rows))
(vert
(apply #'< cols))
((apply #'= rows)
(apply #'< cols))
(t
(apply #'< rows))))))
(sort thing (lambda (x y) (< (plist-get x prop)
(plist-get y prop)))))))
;;; Moving around
(defun latex-table-wizard--get-landing-index (now steps max-index
&optional min-index)
"Move across indices of a sequence.
NOW is the index from which to start the movement.
STEPS, an integer, specifies how many steps to move forward or
backwards from index NOW (depending on whether it is a positive
or negative integer).
MAX-INDEX is the index at which the movement restarts from
MIN-INDEX (which if not specified defaults to 0)."
(declare (pure t) (side-effect-free t))
(let* ((zero-index (or min-index 0))
(floor (min zero-index max-index))
(ceiling (max zero-index max-index))
(count 0))
(while (< count (abs steps))
(let ((new (if (>= steps 0) (1+ now) (1- now))))
(cond ((> new ceiling) (setq now floor
count (1+ count)))
((< new floor) (setq now ceiling
count (1+ count)))
(t (setq now new
count (1+ count))))))
now))
(defun latex-table-wizard--get-other-cell (dir same-line count table curr)
"Return cell plist from TABLE.
The cell that is returned is the one found moving COUNT cells
from current cell CURR in direction DIR (either \\='forward\\=',
\\='backward\\=', \\='next\\=' or \\='previous\\=').
If SAME-LINE is non-nil, loop over the current row (if DIR is
\\='forward\\=' or \\='backward\\='), or column (if DIR is
\\='next\\=' or \\='previous\\='). Otherwise continue search for
cell in a different row or column if no cell is left in the
current DIR."
(declare (pure t) (side-effect-free t))
(let* ((steps (or count 1))
(sorted (latex-table-wizard--sort table same-line dir))
(cell-num (1- (length sorted)))
(now (let ((ind 0)
(col (plist-get curr :column))
(row (plist-get curr :row)))
(catch 'stop
(dolist (i sorted)
(when (and (= (plist-get i :column) col)
(= (plist-get i :row) row))
(throw 'stop t))
(setq ind (1+ ind))))
ind))
(land (if (memq dir '(next forward))
(latex-table-wizard--get-landing-index
now steps 0 cell-num)
(latex-table-wizard--get-landing-index
now (- 0 steps) 0 cell-num))))
(nth land sorted)))
(defun latex-table-wizard--remove-overlays (&optional table beg end)
"Remove table internal overlays.
These are the overlays that have a non-nil value for the name
property \\='table-inside-ol\\='.
Optional arguments BEG and END are passed, they are buffer
positions or markers indicating beginning and end of the table.
Optional arguments TABLE is a list of cell plists: if its not
given, a value is retrieved with
`latex-table-wizard--parse-table'."
(if beg
(remove-overlays beg end 'tabl-inside-ol t)
(let* ((tab (or table (latex-table-wizard--parse-table)))
(lims (latex-table-wizard--get-env-ends tab)))
(remove-overlays (car lims) (cdr lims) 'tabl-inside-ol t))))
(defun latex-table-wizard--hl-cells (list-of-cells)
"Highlight cells in LIST-OF-CELLS with an overlay.
The overlay has a non-nil value for the name property
\\='table-inside-ol\\='."
(unless latex-table-wizard-no-highlight
(let ((ols '()))
(dolist (x list-of-cells)
(push (make-overlay (plist-get x :start)
(plist-get x :end))
ols))
(dolist (x ols)
(overlay-put x 'tabl-inside-ol t)
(overlay-put x 'face 'latex-table-wizard-highlight)))))
(defvar-local latex-table-wizard--selection nil
"Current selection, a list of cell objects.")
(defun latex-table-wizard--locate-point (pos cells)
"Return cell from CELLS in which position POS is in.
CELLS is a list of cell plists.
POS is a buffer position or a marker.
If POS is not in a cell in CELLS, it means it's between two
cells: return the closest one after having moved point to its
beginning."
(declare (pure t))
(let* ((table (or cells (latex-table-wizard--parse-table)))
(ends (latex-table-wizard--get-env-ends table)))
(cond ((< pos (car ends))
(latex-table-wizard--get-cell-pos table
'(:column . 0) '(:row . 0)))
((> pos (cdr ends))
(car (seq-filter
(lambda (x) (= (plist-get x :end) (cdr ends)))
table)))
(t
(let (candidate)
(catch 'found
(dolist (c table)
(when (<= (plist-get c :start) pos (plist-get c :end))
(setq candidate c)
(throw 'found t))))
(if candidate
candidate
(let* ((end-pos
(thread-last
table
(seq-filter (lambda (x) (< (plist-get x :end) pos)))
(mapcar (lambda (x) (plist-get x :end)))
(apply #'max)))
(final (seq-find (lambda (x) (= end-pos
(plist-get x :end)))
table)))
(goto-char (plist-get final :start))
final)))))))
(defun latex-table-wizard--get-thing (thing &optional table)
"Return THING point is in.
THING can be either \\='cell\\=', \\='column\\=' or \\='row\\='.
TABLE is a list of cell plists. If it is nil, use the value of
`latex-table-wizard--parse-table'.
If THING is \\='cell\\=', return one plist, else return a list of
plists."
(declare (side-effect-free t))
(let* ((pos (point))
(cells-list (or table (latex-table-wizard--parse-table)))
(curr (latex-table-wizard--locate-point pos cells-list)))
(if (eq thing 'cell)
curr
(let* ((prop (if (eq thing 'row) :row :column))
(other-prop (if (eq thing 'row) :column :row))
(curr-value (plist-get curr prop)))
(sort (seq-filter (lambda (x) (= curr-value (plist-get x prop)))
cells-list)
(lambda (x y) (> (plist-get x other-prop)
(plist-get y other-prop))))))))
(defsubst latex-table-wizard--shift (dir cell table)
"Given a CELL and a list of cells TABLE, return one of TABLE.
The cell returned is the one whose coordinates correspond to
having CELL shifted in direction DIR (whose value is either
\\='next\\=', \\='previous\\=', \\='forward\\=' or
\\='backward\\='). If no such cell is found in TABLE, return
nil."
(declare (pure t) (side-effect-free t))
(let (target ; a cons cell of (column . row)
output)
(cond ((eq dir 'next)
(setq target (cons (plist-get cell :column)
(1+ (plist-get cell :row)))))
((eq dir 'previous)
(setq target (cons (plist-get cell :column)
(1- (plist-get cell :row)))))
((eq dir 'forward)
(setq target (cons (1+ (plist-get cell :column))
(plist-get cell :row))))
((eq dir 'backward)
(setq target (cons (1- (plist-get cell :column))
(plist-get cell :row)))))
(catch 'found
(dolist (c table)
(when (and (= (plist-get c :column) (car target))
(= (plist-get c :row) (cdr target)))
(setq output c)
(throw 'found t))))
output))
(defun latex-table-wizard--jump (dir &optional absolute
count same-line nocycle)
"Move point to the beginning of a cell in the table.
DIR is either \\='next\\=', \\='previous\\=', \\='forward\\=' or
\\='backward\\=' and determines the direction of motion. This
function assumes being evaluated with point inside of a
tabular-like environment.
With ABSOLUTE being t, move to the last or first cell in the row
or column (depending on the value of DIR) point is currently in.
COUNT is a positive integer that determines how many steps in
direction DIR to take.
If SAME-LINE is non-nil, never leave current column or row.
If NOCYCLE is non-nil, do not move and return nil in case the
jump would move point to a different column (if DIR is either
\\='forward\\=' or \\='backward\\=') or to a different row (if
DIR is either \\='next\\=', \\='previous\\=')."
(when (latex-table-wizard--in-tabular-env-p)
(latex-table-wizard--setup)
(with-silent-modifications
(let* ((message-log-max 0)
(cells (latex-table-wizard--parse-table))
(curr (latex-table-wizard--get-thing 'cell cells))
(target (if (not absolute)
(latex-table-wizard--get-other-cell
dir same-line count cells curr)
(let ((sorted (latex-table-wizard--sort cells t dir)))
(if (memq dir '(previous backward))
(car sorted)
(car (last sorted))))))
(stop (and nocycle (not (latex-table-wizard--shift dir curr cells)))))
(latex-table-wizard--remove-overlays cells)
(unless stop
(goto-char (plist-get target :start))
(latex-table-wizard--hl-cells (list target))
(latex-table-wizard--hl-cells latex-table-wizard--selection)
(message "Col X Row (%d,%d)"
(plist-get target :column)
(plist-get target :row)))))))
;;; Swapping functions
(defun latex-table-wizard--swap-cells (x y)
"Swap the content of two cells X and Y."
(save-excursion
(let ((x-string (concat
" "
(string-trim
(buffer-substring (plist-get x :start)
(plist-get x :end)))
" "))
(y-string (concat
" "
(string-trim
(buffer-substring (plist-get y :start)
(plist-get y :end)))
" ")))
(goto-char (plist-get x :end))
(delete-region (plist-get x :start) (plist-get x :end))
(insert y-string)
(just-one-space)
(goto-char (plist-get y :end))
(delete-region (plist-get y :start) (plist-get y :end))
(insert x-string)
(just-one-space))))
(defun latex-table-wizard--type-of-selection (sel)
"Return type of list of cells SEL.
Non-nil values that are returned are is either \\='cell\\=' (if
SEL only contains one cell), \\='column\\=' or \\='row\\='.
If SEL is a list of more than one cell such that not all the
cells have the same value for either :column or :row, it means
that this selection is neither a column or a row, and nil is
returned."
(declare (pure t))
(cond ((= 1 (length sel)) 'cell)
((not sel) (user-error "Empty selection"))
((apply #'= (mapcar (lambda (x) (plist-get x :column)) sel)) 'column)
((apply #'= (mapcar (lambda (x) (plist-get x :row)) sel)) 'row)
(t nil)))
(defun latex-table-wizard--swap-line (type line1 line2)
"Swap columns or rows LINE1 and LINE2.
TYPE is either \\='column\\=' or \\='row\\='."