-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathlentic.el
1344 lines (1169 loc) · 50.8 KB
/
lentic.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
;;; lentic.el --- One buffer as a view of another -*- lexical-binding: t -*-
;;; Header:
;; This file is not part of Emacs
;; Author: Phillip Lord <[email protected]>
;; Maintainer: Phillip Lord <[email protected]>
;; Version: 0.12
;; Package-Requires: ((emacs "25") (m-buffer "0.13") (dash "2.5.0"))
;; The contents of this file are subject to the GPL License, Version 3.0.
;; Copyright (C) 2014-2024 Free Software Foundation, Inc.
;; 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 <http://www.gnu.org/licenses/>.
;;; Commentary:
;; `lentic' enables /lenticular text/: simultaneous editing and viewing of the
;; same (or closely related) text in two or more buffers, potentially in
;; different modes. Lenticular text is named after lenticular printing, which
;; produce images which change depending on the angle at which they are
;; viewed.
;; Sometimes, it would be nice to edit a file in two ways at once. For
;; instance, you might have a source file in a computational language with
;; richly marked documentation. As Emacs is a modal editor, it would be nice
;; to edit this file both in a mode for the computational language and for the
;; marked up documentation.
;; One solution to this is to use a single-mode which supports both types of
;; editing. The problem with this is that it is fundamentally difficult to
;; support two types of editing at the same time; more over, you need a new
;; mode for each combination. Another solution is to use one of the
;; multiple-mode tools which are available. The problem with this is that they
;; generally need some support from the modes in question. And, again, the
;; difficulty is supporting both forms of editing in the same environment. A
;; final problem is that it is not just the editing environment that needs to
;; be adapted; the programmatic environment needs to be untroubled by the
;; documentation, and the documentation environment untroubled by the program
;; code.
;; Lenticular text provides an alternative solution. Two lentic buffers, by
;; default, the share content but are otherwise independent. Therefore,
;; you can have two buffers open, each showing the content in different modes;
;; to switch modes, you simply switch buffers. The content, location of point,
;; and view are shared.
;; Moreover, lentic buffers can also perform a bi-directional transformation
;; between the two. If this is done, then the two can have different but
;; related text. This also solves the problem of integration with a
;; tool-chain; each lentic buffer can be associated with a different file and
;; a different syntax. For example, this file is, itself, lenticular text. It
;; can be viewed either as Emacs-Lisp or in Org-Mode. In Emacs-Lisp mode, this
;; text is commented out, in org-mode it is not.
;; In fact, although the default behaviour of lentic appears to keep the same
;; text in each buffer, even it uses this bi-directional transformation
;; capability; while the text is shared, the text properties are not. This is
;; a behaviour which differs between lentic buffers and indirect buffers. The
;; lentic buffers can therefore be in different modes without fighting each
;; other to set the text properties.
;; It is possible to configure the transformation for any two buffers in a
;; extensible way. Mostly I have concentrated on mode-specific operation,
;; but, for instance, I have also used this ability on a per-project basis
;; controlling, for instance, the location of the lentic-file.
;;; Usage:
;; lentic can be installed from GNU ELPA/Marmalade then add
;; (global-lentic-mode 1)
;; to your init file.
;; The main user entry points are accessible through the lentic edit menu, or
;; through `global-lentic-mode' which adds keybindings to create and manipulate
;; new lentic buffers. See `lentic-mode' commentary for more information.
;; By default, the lentic buffer created contains exactly the same contents as
;; the original buffer, but is otherwise separate; it can have a different major
;; modes, different syntax highlighting, invisible regions and even different
;; narrowing. Saving one buffer will save the other; killing the lentic buffer
;; does not affect the original, but killing the original also kills the lentic.
;; While this is somewhat useful, more generally a buffer will be configured to
;; produce a particular transformation. This can control many features of the
;; lentic, including the file name, major mode and an arbitrary transformation
;; between the two. Configuration is considered next.
;;; Configuration:
;; lentic buffers are configurable in a large number of ways. It is possible
;; to control the nature of the transformation, the default buffer name that a
;; lentic buffer takes, and the file location (or not) of the lentic buffer.
;; Lentic now supports any number of lentic buffers, in relatively arbitrary
;; geometries, although this requires additional support from the
;; configuration objects.
;; Configuration of a buffer happens in one of two places. First,
;; `lentic-init' is run when a lentic buffer is first created. This function
;; should return the configuration object, and is mostly designed for use as a
;; file-local or dir-local variable. This object is stored in the `lentic-config'
;; and all subsequent operation happens through this.
;; There are now a number of different configurations, which can be used for
;; general-purposes use as well as an extension points for subclass
;; configurations. The two most general configurations are:
;; - default: this copies all text exactly, but does not transfer
;; text-properties (which is the behaviour of indirect buffers). It is
;; possible to configure the default file or mode on a per-object basis.
;; - chunk: this is designed for programmatic syntaxes where chunks of code are
;; demarcated by start and end tags, and everything else is commented by
;; line-start comments. Comments are added or removed between the two buffers.
;; The second of these is extended in lentic-org.el to provide the
;; configuration for this file: there is a normal emacs-lisp file in one buffer
;; and an org-mode version in another. Other programmatic and documentation modes
;; are supported in other files.
;;; Status:
;; This is a beta release, but is now nearly feature complete. The core lentic
;; libraries should hopefully be fairly stable now, however, there is the
;; possibility that it will behave badly and may result in data loss. Please
;; use with care on files with backups.
;; Previous releases of this package were called "linked-buffer". I changed
;; this because I wanted a name for the general idea of text with two
;; visualisations; "linked text" doesn't work because it is sounds like
;; hyperlinked text.
;; Although it is still too early to guarantee, I hope that the current
;; configuration scheme will remain fixed, and subclass extensions should
;; require little change for the future.
;;; Code:
;; #+BEGIN_SRC emacs-lisp
(require 'eieio)
(require 'm-buffer)
(require 'm-buffer-at)
(require 'dash)
(defvar lentic-doc "lenticular.org")
(defvar lentic-doc-html-files '("lenticular.css"))
;; #+end_src
;; ** State
;; This section defines all of the variables that the basic state for lentic
;; is stored in. We deliberately have as few of these as possible, as this
;; makes re-initializing the state during development as straight-forward as
;; possible.
;; We start with `lentic-init' which provides the ability to define some default
;; configuration for a buffer. These are just functions which return
;; `lentic-configuration' objects. This is a slight step of indirection but is
;; essentially there to allow the use of file- or dir-local variables to define
;; the default behaviour for a given buffer. All the values have to be defined by
;; the user as safe, so we do not want too many different values.
;; #+begin_src emacs-lisp
(defvar lentic-init nil
"Function that initializes lentics for this buffer.
This should be one or a list of functions that each return a
`lentic-configuration' object.")
(make-variable-buffer-local 'lentic-init)
;; #+end_src
;; The `lentic-config' variable stores all of the configuration objects for each
;; lentic-buffer of this-buffer. Each lentic-buffer should have one configuration
;; object and is this configuration object that controls the behaviour and
;; updating of that lentic. As lentics are bi-directional, the `lentic-config'
;; variable should be -- for each lentic-configuration object in this-buffer
;; pointing to that-buffer there should be one in that-buffer pointing to
;; this-buffer. This variable has to `permanent-local' otherwise a new mode (or
;; typing `normal-mode') would break everything.
;; #+begin_src emacs-lisp
(defvar lentic-config nil
"Configuration for lentic.
This is a list of objects of the class `lentic-configuration'
lentic-configuration', which defines the way in which the text in
the different buffers is kept synchronized. This configuration is
resilient to changes of mode in the current buffer.")
(make-variable-buffer-local 'lentic-config)
(put 'lentic-config 'permanent-local t)
(defvar lentic-counter 0)
(defun lentic-config-name (buffer)
"Given BUFFER, return a name for the configuration object."
(format "lentic \"%s:%s\"" buffer (setq lentic-counter (+ 1 lentic-counter))))
;;;###autoload
(defvar lentic-init-functions nil
"All functions that can be used as `lentic-init' function.")
;; #+end_src
;; ** Base Configuration
;; This section defines the base class and generic methods for all
;; lentic-configuration objects. Most of the properties of this class define the
;; behaviour of the lentic-buffer -- in other words they are configuration.
;; However, there are a few properties which store state about the last
;; before-change event that occured which are used to percolate the changes
;; correctly. This is a handy place to store these, but are not really
;; end-user properties.
;; #+begin_src emacs-lisp
(defclass lentic-configuration ()
((this-buffer
:initarg :this-buffer
:documentation
"The this-buffer for this configuration. This should be the
current-buffer when this configuration is present in `lentic-config'." )
(that-buffer
:initarg :that-buffer
:documentation
"The that-buffer for this configuration. The that-buffer (if
live) should a lentic-configuration object for this-buffer in
its `lentic-config'." )
(creator
:initarg :creator ;; FIXME: Not used.
:initform nil
:documentation
"Non-nil if this lentic-configuration was used to create a
lentic view. This is used to determine the behaviour when the
buffer is killed: killing the creator kills all views, but killing
a view does not kill the creator.")
(delete-on-exit
:initarg :delete-on-exit
:initform nil
:documentation
"Non-nil if the file associated with this should be deleted on exit.")
(singleton ;; FIXME: Not used?
:initarg :singleton
:initform nil
:documentation
"Non-nil if only one lentic (and therefore object) of this type
can exist for a given buffer.")
(sync-point
:initarg :sync-point
:initform t
:documentation
"Non-nil if changes to the location of point in this-buffer
should be percolated into that-buffer.")
(last-change-start
:initarg :last-change-start ;; FIXME: Not used.
:initform nil
:documentation
"The location of the start of the last before-change event.
This should only be set by lentic.")
(last-change-start-converted
:initarg :last-change-start-converted ;; FIXME: Not used.
:initform nil
:documentation
"The location of the start of the last before-change event,
converted into the equivalent location in that-buffer. This
should only be set by lentic.")
(last-change-stop
:initarg :last-change-stop ;; FIXME: Not used.
:initform nil
:documentation
"The location of the stop of the last before-change event.
This should only be set by lentic." )
(last-change-stop-converted
:initarg :last-change-stop-converted ;; FIXME: Not used.
:initform nil
"The location of the stop of the last before-change event,
converted into the equivalent location in that-buffer. This
should only be set by lentic."))
"Configuration object for lentic which defines the behavior of
the lentic buffer.")
;; #+end_src
;; We define a set of generic methods. I am not entirely sure what the purpose of
;; generic methods are and whether I need them or not; I think it's just a place
;; to put the documentation.
;; #+begin_src emacs-lisp
(cl-defgeneric lentic-create (conf)
"Create the lentic for this configuration.
Given a `lentic-configuration' object, create the lentic
appropriate for that configurationuration. It is the callers
responsibility to check that buffer has not already been
created.")
(cl-defgeneric lentic-convert (conf location)
"Convert LOCATION in this-buffer to an equivalent location in
that-buffer. LOCATION is a numeric location, rather than a
marker. By equivalent, we mean the same semantic location as
determined by the transformation between the buffers. It is
possible that a given LOCATION could map to more than one
location in the lentic buffer.")
(cl-defgeneric lentic-clone (conf)
"Updates that-buffer to reflect the contents in this-buffer.
Updates at least the region that has been given between start and
stop in the this-buffer, into the region start-converted and
stop-converted in that-buffer.
Returns a list of the start location in that-buffer of the
change, the stop location in that-buffer of the change and the
length-before in that buffer of the region changed before the
change, if and only if the changes are exactly that suggested by
the START, STOP, _LENGTH-BEFORE, START-CONVERTED and
STOP-CONVERTED. Otherwise, this should return nil.")
;; #+end_src
;; We need an invert method because we can create the configuration object for a
;; this-buffer without actually creating that-buffer. This may happen at any
;; point in the future. So, the configuration object needs to be able to return
;; it's own inverse. This can be a configuration object of the same class which
;; is normal when the lentic transformation is symmetrical, or a different class
;; which is normal when the lentic transformation is asymmetrical.
;; #+begin_src emacs-lisp
(cl-defgeneric lentic-invert (conf)
"Return a new configuration object for the lentic buffer.
This method is called at the time that the lentic is created. It
is the callers responsibility to ensure that this is only called
at creation time and not subsequently. The invert function should
only return the configuration object and NOT create the lentic
buffer.")
;; #+end_src
;; `lentic-coexist?' has been created to cope with the case when a buffer has two
;; or more default views. We may wish to re-initialize all the default lentic
;; views. However, this is going to be problematic if some are already there --
;; we will end up with two many. In general, configurations which have been
;; created as a result of calls to the `lentic-init' functions should return
;; false here if there is another call to the same function. Lentic buffers which
;; are being used as a persistent view should generally return true here so that
;; as many can be created as required.
;; #+begin_src emacs-lisp
(cl-defgeneric lentic-coexist? (this-conf that-conf)
"Return non-nil if THIS-CONF and co-exist with THAT-CONF.
By co-exist this means that both configurations are valid for a
given buffer at the same time. A nil return indicates that there
should only be one of these two for a given buffer.")
;; #+end_src
;; I've implemented `lentic-this' and `lentic-that' as methods although I think I
;; have only over-ridden the implementation once in lentic-delayed which has
;; since been deleted anyway.
;; #+begin_src emacs-lisp
(cl-defmethod lentic-this ((conf lentic-configuration))
"Returns this-buffer for this configuration object.
In most cases, this is likely to be the `current-buffer' but
this should not be relied on."
(oref conf this-buffer))
(cl-defmethod lentic-that ((conf lentic-configuration))
"Returns the that-buffer for this configuration object.
This may return nil if there is not that-buffer, probably because
it has not been created."
(and (slot-boundp conf 'that-buffer)
(oref conf that-buffer)))
(cl-defmethod lentic-ensure-that ((conf lentic-configuration))
"Get the lentic for this configuration
or create it if it does not exist."
(or (lentic-that conf)
(lentic-create conf)))
;; #+end_src
;; This part of the user interface is not ideal at the moment. I need something
;; which allows me to see all the currently active lentic-buffers, but I am far
;; from convinced that the mode-line is the best place, since the mode-line gets
;; overly full for most users.
;; As a second problem, supporting mode-line display directly in the
;; configuration object seems right, and breaks the encapsulation between
;; lentic.el and lentic-mode.el. Probably this needs to be replaced by some sort
;; of status keyword return value.
;; #+begin_src emacs-lisp
(cl-defmethod lentic-mode-line-string ((conf lentic-configuration))
"Returns a mode-line string for this configuration object."
(when (slot-boundp conf 'that-buffer)
(let ((that (oref conf that-buffer)))
(if
(and that
(buffer-live-p that))
"on"
""))))
;; #+end_src
;; ** Default Configuration
;; This is the default implementation of a lentic configuration. It provides an
;; identity transformation at that string level -- the two buffers will (should!)
;; have identical `buffer-string' at all times. Or, more strictly, identical
;; without properties, so identical ~(buffer-substring-no-properties (point-min)
;; (point-max))~, which is not nearly so snappy.
;; We add two more properties to this class -- perhaps these should be pushed upwards.
;; #+begin_src emacs-lisp
(defclass lentic-default-configuration (lentic-configuration)
((lentic-file
:initform nil
:initarg :lentic-file
:documentation
"The name of the file that will be associated with that lentic buffer.")
(lentic-mode
:initform nil
:initarg :lentic-mode ;; FIXME: Not used.
:documentation
"The mode for that lentic buffer."))
"Configuration which maintains two lentics with the same contents.")
;; #+end_src
;; We add in a string transformation function here. There has no actual
;; function within lentic per se, but it is used in lentic-dev as something we
;; can advice. This avoids bulking up the code in lentic, while still allows
;; me to affect the transformation during development of new transforms.
;; #+begin_src emacs-lisp
(defun lentic-insertion-string-transform (string)
"Transform the STRING that is about to be inserted.
This function is not meant to do anything. It's useful to
advice."
string)
;; #+end_src
;; The default methods should be self-explanatory!
;; #+begin_src emacs-lisp
(cl-defmethod lentic-create ((conf lentic-default-configuration))
"Create an new lentic buffer. This creates the new buffer sets
the mode to the same as the main buffer or which ever is
specified in the configuration. The current contents of the main
buffer are copied."
;; make sure the world is ready for lentic buffers
(lentic-ensure-hooks)
;; create lentic
(let* ((this-buffer
(lentic-this conf))
(that-buffer
(generate-new-buffer
(format "*lentic: %s*"
(buffer-name
this-buffer))))
(sec-file (oref conf lentic-file))
(sec-mode
(or
;; the specified normal mode
(oref conf lentic-mode)
;; if we have a file try normal mode
(if sec-file
'normal-mode
;; otherwise the same mode as the main file
major-mode))))
(oset conf creator t)
;; make sure this-buffer knows about that-buffer
(oset conf that-buffer that-buffer)
;; init that-buffer with mode, file and config
;; the mode must be init'd after adding content in case there are any
;; file-local variables need to be evaled
;; insert the contents
(lentic-update-contents conf)
(with-current-buffer that-buffer
(when sec-mode
(funcall sec-mode))
(when sec-file
(set-visited-file-name sec-file))
(setq lentic-config
(list (lentic-invert conf))))
that-buffer))
(defun lentic--file-equal-p (f1 f2)
(let ((a1 (file-attributes f1))
(a2 (file-attributes f2)))
(and a1 (equal a1 a2))))
(cl-defmethod lentic-coexist? ((this-conf lentic-default-configuration)
that-conf)
"By default, we can have multiple lentic buffers with the same
configuration, unless specifically disallowed, or unless it has
the same associated file as pre-existing buffer (which is going
to break!)."
(and
(not (oref this-conf singleton))
(not
(and (oref this-conf lentic-file)
(oref that-conf lentic-file)
(lentic--file-equal-p
(oref this-conf lentic-file)
(oref that-conf lentic-file))))))
(cl-defmethod lentic-invert ((conf lentic-default-configuration))
"By default, return a clone of the existing object, but switch
the this and that buffers around. "
(clone
conf
:this-buffer (lentic-that conf)
:that-buffer (lentic-this conf)
:sync-point (oref conf sync-point)))
(cl-defmethod lentic-convert ((_conf lentic-default-configuration)
location)
"The two buffers should be identical, so we just return the
same location."
location)
(cl-defmethod lentic-clone ((conf lentic-configuration)
&optional start stop _length-before
start-converted stop-converted)
"The default clone method cuts out the before region and pastes
in the new."
(let ((this-b (lentic-this conf))
(that-b (lentic-that conf)))
(with-current-buffer this-b
;;(lentic-log "this-b (point,start,stop)(%s,%s,%s)" (point) start stop)
(save-window-excursion
(save-restriction
(widen)
(let* ((start (or start (point-min)))
(stop (or stop (point-max))))
(with-current-buffer that-b
(save-restriction
(widen)
;; get the start location that we converted before the change.
;; lentic-convert is not reliable now, because the two
;; buffers do not share state until we have percolated it
(let ((converted-start
(max (point-min)
(or start-converted
(point-min))))
(converted-stop
(min (point-max)
(or stop-converted
(point-max)))))
(delete-region converted-start
converted-stop)
(save-excursion
(goto-char converted-start)
;; so this insertion is happening at the wrong place in block
;; comment -- in fact, it's happening one too early
(insert
(with-current-buffer this-b
;; want to see where it goes
;; hence the property
(lentic-insertion-string-transform
(buffer-substring-no-properties
start stop))))
(list converted-start
(+ converted-start (- stop start))
(- converted-stop converted-start))))))))))))
;;;###autoload
(defun lentic-default-init ()
"Default init function.
see `lentic-init' for details."
(lentic-default-configuration
:this-buffer (current-buffer)))
(add-to-list 'lentic-init-functions #'lentic-default-init)
;; #+end_src
;; ** Basic Operation
;; In this section, we define some utility functions and the hooks we need into
;; the core Emacs operations.
;; *** Utility
;; We start with some utility macros. These deal with the fact that a buffer can
;; have a lentic or not, and that even if it does that lentic does not need to be
;; live. This happens for instance if a lentic buffer is deleted -- the buffer
;; object will still be live (because the configuration object hangs on to it).
;; At some point, the hook system needs to clean this up by detecting the
;; buffer-kill and removing the configuration objection.
;; #+begin_src emacs-lisp
(defmacro lentic-when-lentic (&rest body)
"Evaluate BODY when the `current-buffer' has a lentic buffer."
(declare (debug t))
`(when (and
lentic-config
(-any?
(lambda (conf)
(-when-let
(buf (lentic-that conf))
(buffer-live-p buf)))
lentic-config))
,@body))
(defmacro lentic-when-buffer (buffer &rest body)
"When BUFFER is a live buffer eval BODY."
(declare (debug t)
(indent 1))
`(when (buffer-live-p ,buffer)
,@body))
(defmacro lentic-when-with-current-buffer (buffer &rest body)
"When BUFFER is a live buffer eval BODY with BUFFER current."
(declare (debug t)
(indent 1))
`(lentic-when-buffer ,buffer
(with-current-buffer ,buffer
,@body)))
(defmacro lentic-with-lentic-buffer (buffer &rest body)
"With BUFFER as current, eval BODY when BUFFER has a lentic."
(declare (debug t)
(indent 1))
`(lentic-when-with-current-buffer ,buffer
(when lentic-config
,@body)))
(defvar lentic-condition-case-disabled
noninteractive
"If non-nil throw exceptions from errors.
By default this is set to the value of noninteractive, so that
Emacs crashes with backtraces in batch." )
(defmacro lentic-condition-case-unless-disabled (var bodyform &rest handlers)
"Like `condition-case' but can be disabled like `condition-case-unless-debug'."
(declare (debug condition-case) (indent 2))
`(if lentic-condition-case-disabled
,bodyform
(condition-case-unless-debug ,var
,bodyform
,@handlers)))
(defmacro lentic-widen (conf &rest body)
"Widen both buffers in CONF, then evaluate BODY."
(declare (debug t)
(indent 1))
`(with-current-buffer
(lentic-that ,conf)
(save-restriction
(widen)
(with-current-buffer
(lentic-this ,conf)
(save-restriction
(widen)
,@body)))))
;; #+end_src
;; Recurse down the lentic tree to all lentic views.
;; #+begin_src emacs-lisp
(defun lentic-each (buffer fn &optional seen-buffer)
"Starting at BUFFER, call FN on every lentic-buffer.
FN should take a single argument which is the buffer.
SEEN-BUFFER is a list of buffers to ignore."
(lentic-with-lentic-buffer buffer
(setq seen-buffer (cons buffer seen-buffer))
(-map
(lambda (conf)
(let ((that
(lentic-that conf)))
(when (and (not (-contains? seen-buffer that))
(buffer-live-p that))
(funcall fn that)
(lentic-each that fn seen-buffer))))
lentic-config)))
(defun lentic-garbage-collect-config ()
"Remove non-live configs in current-buffer."
(setq lentic-config
(--filter
(buffer-live-p
(lentic-that it))
lentic-config)))
;; #+end_src
;; *** Initialisation
;; #+begin_src emacs-lisp
(defun lentic-ensure-init ()
"Ensure that the `lentic-init' has been run."
(lentic-garbage-collect-config)
(setq lentic-config
;; and attach to lentic-config
(-concat
lentic-config
;; return only those that can co-exist
(-filter
(lambda (this-conf)
(-all?
(lambda (that-conf)
(lentic-coexist? this-conf that-conf))
lentic-config))
(-map
(lambda (init)
;; instantiate a new conf object (but do not create the buffer)
(funcall init))
(if (not lentic-init)
'(lentic-default-init)
(-list lentic-init)))))))
(defun lentic-init-all-create ()
"Create all lentics fo the current buffer."
(lentic-ensure-init)
(-map
(lambda (conf)
(if (and
(slot-boundp conf 'that-buffer)
(buffer-live-p
(lentic-that conf)))
(lentic-that conf)
(lentic-create conf)))
(-list lentic-config)))
;; #+end_src
;; *** Hook System
;; The lentic hook system is relatively involved, unfortunately, and will
;; probably become more so. In so far as possible, though, all of the complexity
;; should be here, using the methods provided in the lentic-configuration object.
;; The complexity of the hook system and the fact that it is hooked deeply into
;; the core of Emacs can make it quite hard to debug. There are a number of
;; features put in place to help deal with this. These are:
;; - A logging system
;; - An emergency detection system.
;; - Two part hooks
;; Start by enabling hooks!
;; #+begin_src emacs-lisp
(defun lentic-ensure-hooks ()
"Ensures that the hooks that this mode requires are in place."
(add-hook 'post-command-hook
#'lentic-post-command-hook)
;; FIXME: Do we really need these hook functions to affect *all* buffers?
(add-hook 'after-change-functions
#'lentic-after-change-function)
(add-hook 'before-change-functions
#'lentic-before-change-function)
(add-hook 'after-save-hook
#'lentic-after-save-hook)
(add-hook 'kill-buffer-hook
#'lentic-kill-buffer-hook)
(add-hook 'kill-emacs-hook
#'lentic-kill-emacs-hook))
;; #+end_src
;; The logging system which allows post-mortem analysis of what lentic has done.
;; Originally, my plan was to leave logging in place so aid analysis of bug
;; reports, but this requires so much logging that it the log buffer becomes
;; impossible to analyse.
;; #+begin_src emacs-lisp
(defvar lentic-log nil)
(defmacro lentic-log (&rest rest)
"Log REST."
`(when lentic-log
(lentic-when-lentic
(let ((msg
(concat
(format ,@rest)
"\n")))
(princ msg #'external-debugging-output)))))
;; #+end_src
;; An emergency detection system. Several of the hooks in use (post-command-hook,
;; and the before- and after-change-functions) automatically remove hook
;; functions which give errors. In development, this means that all errors are
;; silently ignored and, worse, lentic continues in an inconsistent state with
;; some hooks working and some not. Lentic catches all errors, therefore, and
;; then drops into an "lentic-emergency" state, where all lentic functionality is
;; disabled. This is still a dangerous state as changes do not percolate, but at
;; least it should be predictable. The emergency state can be changed with
;; `lentic-unemergency' and `lentic-emergency'.
;; #+begin_src emacs-lisp
(defvar lentic-emergency nil
"Iff non-nil halt all lentic activity.
This is not the same as disabling lentic mode. It stops all
lentic related activity in all buffers; this happens as a result
of an error condition. If lentic was to carry on in these
circumstances, serious data loss could occur. In normal use, this
variable will only be set as a result of a problem with the code;
it is not recoverable from a user perspective.
It is useful to toggle this state on during development. Once
enabled, buffers will not update automaticaly but only when
explicitly told to. This is much easier than try to debug errors
happening on the after-change-hooks. The
function `lentic-emergency' and `lentic-unemergency' functions
enable this.")
(defvar lentic-emergency-debug nil
"Iff non-nil, lentic will store change data, even
during a `lentic-emergency'.
Normally, `lentic-emergency' disables all activity, but this makes
testing incremental changes charge. With this variable set, lentic will
attempt to store enough change data to operate manually. This does require
running some lentic code (notably `lentic-convert'). This is low
risk code, but may still be buggy, and so setting this variable can cause
repeated errors.")
(defun lentic-emergency ()
"Stop lentic from working due to code problem."
(interactive)
(setq lentic-emergency t)
(lentic-update-all-display))
(defun lentic-unemergency ()
"Start lentic working after stop due to code problem."
(interactive)
(setq lentic-emergency nil)
(lentic-update-all-display))
(defun lentic-hook-fail (err hook)
"Give an informative message when we have to fail.
ERR is the error. HOOK is the hook type."
(message "lentic mode has failed on \"%s\" hook: %s "
hook (error-message-string err))
(lentic-emergency)
(with-output-to-temp-buffer "*lentic-fail*"
(princ "There has been an error in lentic-mode.\n")
(princ "The following is debugging information\n\n")
(princ (format "Hook: %s\n" hook))
(princ (error-message-string err)))
(select-window (get-buffer-window "*lentic-fail*")))
;; #+end_src
;; As a byproduct of the last, lentic also has two part hooks: the real hook
;; function which just handles errors and calls the second function which does
;; the work. This make it possible to call the second function interactively,
;; without catching errors (so that they can be debugged) or causing the
;; lentic-emergency state. There are some utility functions in lentic-dev for
;; running hooks which require arguments.
;; **** General Hook
;; Start by handling saving, killing and general connecting with the Emacs
;; behaviour.
;; #+begin_src emacs-lisp
(defun lentic-after-save-hook ()
"Error protected call to real after save hook."
(unless lentic-emergency
(lentic-condition-case-unless-disabled err
(lentic-after-save-hook-1)
(error
(lentic-hook-fail err "after-save-hook")))))
(defun lentic-after-save-hook-1 ()
"Respond to a save in the `current-buffer'.
This also saves every lentic which is file-associated."
(lentic-each
(current-buffer)
(lambda (buffer)
(with-current-buffer
buffer
(when (buffer-file-name)
(save-buffer))))))
(defvar lentic-kill-retain nil
"If non-nil retain files even if requested to delete on exit.")
(defun lentic-kill-buffer-hook ()
"Error protected call to real `kill-buffer-hook'."
(unless lentic-emergency
(lentic-condition-case-unless-disabled err
(lentic-kill-buffer-hook-1)
(error
(lentic-hook-fail err "kill-buffer-hook")))))
(defvar lentic--killing-p nil)
(defun lentic-kill-buffer-hook-1 ()
"Respond to any buffer being killed.
If this killed buffer is lentic and is `creator', then kill all
lentic-buffers recursively. If the buffer is `delete-on-exit',
then remove any associated file."
(lentic-when-lentic
(when
(and
(--any?
(oref it delete-on-exit)
lentic-config)
;; might not exist if we not saved yet!
(file-exists-p buffer-file-name)
;; if we are cloning in batch, we really do not want to kill
;; everything at the end
(not noninteractive)
;; or we have blocked this anyway
(not lentic-kill-retain))
(delete-file buffer-file-name))
;; if we were the creator buffer, blitz the lentics (which causes their
;; files to delete also).
;; FIXME: "-p" is for *p*redicates, not boolean values.
(defvar lentic-killing-p)
(let ((lentic-killing-p t))
(when
(and
(not lentic-killing-p)
(--any?
(oref it creator)
lentic-config))
(lentic-each
(current-buffer)
#'kill-buffer)))))
(defun lentic-kill-emacs-hook ()
"Error protected call to real `kill-emacs-hook'."
(unless lentic-emergency
(lentic-condition-case-unless-disabled err
(lentic-kill-emacs-hook-1)
(error
(lentic-hook-fail err "kill-emacs-hook")))))
(defun lentic-kill-emacs-hook-1 ()
"Respond to `kill-emacs-hook.
This removes any files associated with lentics which are
marked as :delete-on-exit."
(-map
(lambda (buffer)
(lentic-with-lentic-buffer
buffer
(-map
(lambda (conf)
(and
(oref conf delete-on-exit)
(file-exists-p buffer-file-name)
(not noninteractive)
(delete-file (buffer-file-name))))
lentic-config)))
(buffer-list)))
;; #+end_src
;; **** Change Hooks
;; Handling and percolating changes is the most complex part of lentic, made more
;; complex still by the decision to support multiple buffers (why did I do that
;; to myself?).
;; The `post-command-hook' just percolates location of point through all the
;; lentic buffers.
;; #+begin_src emacs-lisp
(defun lentic-post-command-hook ()
"Update point according to config, with error handling."
(unless lentic-emergency
(lentic-condition-case-unless-disabled err
(progn
;; we test for this later anyway, but this makes it easier to debug.
(when lentic-config
(lentic-post-command-hook-1 (current-buffer))))
(error
(lentic-hook-fail err "post-command-hook")))))
(defun lentic-post-command-hook-1 (buffer &optional seen-buffer)
"Update point in BUFFER according to config.
SEEN-BUFFER is a list of lentics that have already been updated."
(lentic-with-lentic-buffer
buffer
;; now we have seen this buffer don't look again
(setq seen-buffer (cons buffer seen-buffer))
;; for all configurations
(-map
(lambda (config)
(let ((that
(lentic-that config)))
;; check for the termination condition
(unless (-contains? seen-buffer that)
(lentic-when-buffer
that
;; then update and recurse