-
Notifications
You must be signed in to change notification settings - Fork 1
/
evedel.el
2763 lines (2549 loc) · 138 KB
/
evedel.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
;;; evedel.el --- Instructed LLM programmer/assistant -*- lexical-binding: t; -*-
;; Copyright (C) 2024 daedsidog
;; Author: daedsidog <[email protected]>
;; Version: 0.4.16
;; Keywords: convenience, tools
;; Package-Requires: ((emacs "29.1") (gptel "0.9.0"))
;; URL: https://github.com/daedsidog/evedel
;; 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/>.
;; This file is NOT part of GNU Emacs.
;;; Commentary:
;; Evedel is a gptel extension aimed at managing and processing in-code
;; instructions. It provides functionality to define, manipulate, and process
;; directives and references within a buffer. Key features include creating,
;; deleting, and updating instructional overlays, as well as processing
;; responses from LLMs for directives.
;;; Code:
(require 'cl-lib)
(require 'ediff)
(require 'gptel)
(defgroup evedel nil
"Customization group for Evedel."
:group 'tools)
(defcustom e-reference-color "yellow"
"Color to be used as a tint for reference overlays."
:type 'string)
(defcustom e-directive-color "orange"
"Color to be used as a tint for directive overlays."
:type 'string)
(defcustom e-directive-processing-color "cyan"
"Color to be used as a tint for directives being processed by the model."
:type 'string)
(defcustom e-directive-success-color "green"
"Color to be used as a tint for directives successfully processed by the model."
:type 'string)
(defcustom e-directive-fail-color "red"
"Color to be used as a tint for directives the model could not process."
:type 'string)
(defcustom e-highlighted-instruction-color "cyan"
"Color for currently highlighted instructions."
:type 'string)
(defcustom e-instruction-bg-tint-intensity 0.075
"Default intensity for background tinting of instructions."
:type 'float)
(defcustom e-instruction-label-tint-intensity 0.2
"Default intensity for label tinting of instructions."
:type 'float)
(defcustom e-highlighted-instruction-tint-intensity 0.2
"Default intensity for tinting of highlighted instructions."
:type 'float)
(defcustom e-subinstruction-tint-coefficient 0.4
"Coeffecient multiplied by by tint intensities.
Only applicable to the subinstructions. Makes it possible to have more a
more finely-tuned control over how tinting looks.
Does not affect the label colors, just the backgrounds."
:type 'float)
(defcustom e-empty-tag-query-matches-all t
"Determines behavior of directives without a tag search query.
If set to t, directives without a specific tag search query will use all
available references. Alternatively, if this is set to nil, directives without
a search query will not use any references."
:type 'boolean)
(defcustom e-always-match-untagged-references t
"Controls inclusion of untagged references in directive prompts.
When set to t, untagged references are always incorporated into directive
references, ensuring comprehensive coverage. Conversely, when set to nil,
untagged references are ignored, unless `evedel-empty-tag-query-matches-all'
is set to t.
A reference is considered untagged when it has no direct tags. References can
inherit tags from ancestor references and still be considered untagged."
:type 'boolean)
(defcustom e-patch-outdated-instructions t
"Automatically patch instructions when the save file is outdated if non-nil."
:type 'boolean
:group 'evedel)
(defcustom e-descriptive-mode-roles
'((emacs-lisp-mode . "an Emacs Lisp programmer")
(js-mode . "a JavaScript programmer")
(c-mode . "a C programmer")
(c++-mode . "a C++ programmer")
(lisp-mode . "a Common Lisp programmer")
(web-mode . "a web developer"))
"Assciation list between major modes and model roles.
Answers the question \"who is the model?\""
:type 'list)
(defvar e--instructions ()
"Association list mapping buffers or files to lists of instruction overlays.")
(defvar e--default-instruction-priority -99)
(defvar e--highlighted-instruction nil)
(defvar e--inhibit-file-restoration nil
"If t, `evedel--restore-file-instructions' becomes inert.
This is sometimes necessary to prevent various hooks from interfering with the
instruction restoration process.")
(defvar e--id-counter 0)
(defvar e--id-usage-map (make-hash-table))
(defvar e--retired-ids ())
(defmacro e--foreach-instruction (binding &rest body)
"Iterate over `evedel--instructions' with BINDING as the binding.
Executes BODY inside an existing `cl-loop' form, which means that the macro is
expecting for BODY to be written in the `cl-loop' DSL.
BINDING can either be a symbol to bind the instruction to, or a
list where the `car' is the symbol binding and the `cadr' is a buffer.
If the buffer inside BINDING is non-nil, only iterate over the instructions
that are located inside that buffer.
The purpose of this macro is to be able to iterate over instructions
while also making sure that the iterated instructions are valid, i.e.
have an associated buffer to the overlay.
This macro is the preferred way to iterate over instructions, as it
handles all the internal bookkeeping and cleanup."
(declare (indent 1))
;; "bof" stands for "buffer or file".
(cl-with-gensyms (cons bof specific-buffer)
(let ((instr (if (listp binding) (car binding) binding)))
`(cl-labels ((trashp (instr)
(and (null (overlay-buffer instr))
(not (overlay-get instr 'e-marked-for-deletion))))
(clean-alist-entry (cons)
(mapc (lambda (instr) (e--delete-instruction instr (car cons)))
(cl-remove-if-not #'trashp (cdr cons)))
(let ((instrs (cl-remove-if #'trashp (cdr cons))))
(setf (cdr cons) instrs))))
(let ((,specific-buffer ,(if (listp binding) (cadr binding) nil)))
(if (not ,specific-buffer)
(cl-loop for ,cons in e--instructions
do (let ((,bof (car ,cons)))
(if (stringp ,bof) ; bof is a file, restore it.
(e--restore-file-instructions ,bof)
(clean-alist-entry ,cons)))) ; bof is a buffer, clean it.
(when-let ((cons (assoc ,specific-buffer e--instructions)))
(clean-alist-entry cons)))
;; Remove empty cons cells from the alist.
(setq e--instructions (cl-remove-if (lambda (cons)
(null (cdr cons)))
e--instructions))
;; The instructions alist should now be cleaned of deleted instructions.
(cl-loop for ,instr
in (if ,specific-buffer
(alist-get ,specific-buffer e--instructions)
(flatten-tree
(cl-remove nil
(mapcar (lambda (plist-or-instrs)
(if (plist-get plist-or-instrs :instructions)
nil ; Plist
plist-or-instrs))
(mapcar #'cdr e--instructions)))))
,@body))))))
;;;###autoload
(defun e-version (&optional here message)
"Return the current version of Evedel.
Interactively, or when MESSAGE is non-nil, show it in echo area. With prefix
argument, or when HERE is non-nil, insert it at point."
(interactive (list (or current-prefix-arg 'interactive)))
(let ((version "v0.4.16"))
(cond
((or message (called-interactively-p 'any)) (message "Evedel %s" version))
(here (insert (format "Evedel %s" version)))
(t version))))
;;;###autoload
(defun e-save-instructions (path)
"Save instructions overlays to a file PATH specified by the user.
Instructions are only saved if they are associated with a buffer that has an
associated file on disk. In other words, instructions in ethereal buffers are
not saved."
(interactive (list (read-file-name "Save instruction list to file: ")))
(let ((file-alist ())
(saved-instruction-count 0))
(cl-loop for cons in e--instructions
if (bufferp (car cons))
do (let ((buffer (car cons)))
(when-let ((buffer-file-name (buffer-file-name buffer)))
(let ((file (file-relative-name buffer-file-name
(file-name-directory path))))
(when-let ((instrs (e--stashed-buffer-instructions buffer)))
(let ((original-content
(with-current-buffer buffer
(buffer-substring-no-properties (point-min) (point-max)))))
(push (cons file
(list :original-content original-content
:instructions instrs))
file-alist))
(cl-incf saved-instruction-count (length instrs))))))
else do
(push cons file-alist)
(cl-incf saved-instruction-count (length (plist-get (cdr cons) :instructions))))
(if (not (zerop saved-instruction-count))
(with-temp-file path
(let ((save-file ()))
(setf save-file (plist-put save-file :version (e-version)))
(setf save-file
(plist-put save-file :ids (list :id-counter e--id-counter
:used-ids (hash-table-keys e--id-usage-map)
:retired-ids e--retired-ids)))
(setf save-file (plist-put save-file :files file-alist))
(prin1 save-file (current-buffer)))
(let ((file-count (length file-alist)))
(message "Wrote %d Evedel instruction%s from %d file%s to %s"
saved-instruction-count
(if (= 1 saved-instruction-count) "" "s")
file-count
(if (= 1 file-count) "" "s")
path)))
(when (called-interactively-p 'any)
(message "No Evedel instructions to save")))))
;;;###autoload
(defun e-load-instructions (path)
"Load instruction overlays from a file specified by PATH."
(interactive (list (read-file-name "Instruction list file: ")))
(when (and (e--instructions)
(called-interactively-p 'any))
(unless (y-or-n-p "Discard existing Evedel instructions? ")
(user-error "Aborted")))
(let* ((save-file (e--patch-save-file (with-temp-buffer
(insert-file-contents path)
(read (current-buffer)))))
(file-alist (plist-get save-file :files))
(id-counter-plist (plist-get save-file :ids)))
(unless (listp file-alist)
(user-error "Malformed Evedel instruction list"))
(e-delete-all-instructions)
(cl-destructuring-bind (&key id-counter used-ids retired-ids) id-counter-plist
(let ((hm (make-hash-table)))
(cl-loop for used-id in used-ids
do (puthash used-id t hm))
(setq e--id-counter id-counter
e--id-usage-map hm
e--retired-ids retired-ids)))
(setq e--instructions file-alist)
(cl-loop for cons in e--instructions
do (when (stringp (car cons))
(setf (car cons)
;; We want to turn the relative paths of the save file to be absolute paths
;; that we will be able to handle.
(expand-file-name (car cons) (file-name-parent-directory path)))))
(let ((total-restored 0)
(total-kia 0)
(total (cl-reduce #'+
(mapcar #'length
(mapcar (lambda (plist)
(plist-get plist :instructions))
(mapcar #'cdr e--instructions))))))
(cl-loop for (file . _) in e--instructions
do (progn
(cl-multiple-value-bind (restored kia) (e--restore-file-instructions file t)
(cl-incf total-restored restored)
(cl-incf total-kia kia))))
(when (called-interactively-p 'any)
(message "Restored %d out of %d instructions from %s%s"
total-restored
total
(expand-file-name path)
(if (not (zerop total-kia))
(format ", with %d lost to patching" total-kia)
""))))))
;;;###autoload
(defun e-instruction-count ()
"Return the number of instructions currently loaded instructions.
If called interactively, it messages the number of instructions and buffers."
(interactive)
(let ((count 0)
(buffer-hash (make-hash-table :test 'eq)))
(e--foreach-instruction instr count instr into instr-count
do (puthash (overlay-buffer instr) t buffer-hash)
finally (setf count instr-count))
(let ((buffers (hash-table-count buffer-hash)))
(when (called-interactively-p 'interactive)
(if (= count 0)
(message "No Evedel instructions currently loaded")
(message "Evedel is showing %d instruction%s from %d buffer%s"
count (if (/= count 1) "s" "")
buffers (if (/= buffers 1) "s" ""))))
count)))
;;;###autoload
(defun e-create-reference ()
"Create a reference instruction within the selected region.
If a region is selected but partially covers an existing reference, then the
command will resize the reference in the following manner:
- If the mark is located INSIDE the reference (i.e., the point is located
OUTSIDE the reference) then the reference will be expanded to the point.
- If the mark is located OUTSIDE the reference (i.e., the point is located
INSIDE the reference) then the reference will be shrunk to the point."
(interactive)
(e--create-instruction 'reference))
;;;###autoload
(defun e-create-directive ()
"Create a directive instruction within the selected region.
If a region is selected but partially covers an existing directive, then the
command will resize the directive in the following manner:
- If the mark is located INSIDE the directive (i.e., the point is located
OUTSIDE the directive) then the directive will be expanded to the point.
- If the mark is located OUTSIDE the directive (i.e., the point is located
INSIDE the directive) then the directive will be shrunk to the point."
(interactive)
(e--create-instruction 'directive))
(defun e-link-instructions (from-list to-list)
"Link instructions with ids in FROM-LIST to those in TO-LIST.
When invoked interactively, prompts user for two lists of instruction ids."
(interactive
(let ((completion-table (mapcar #'number-to-string (hash-table-keys e--id-usage-map))))
(list (mapcar #'string-to-number
(completing-read-multiple "Select instruction ids to link: "
completion-table nil t))
(mapcar #'string-to-number
(completing-read-multiple "Select instruction ids to link to: "
completion-table nil t)))))
(cl-labels
((update-links (instr-id num-key update-id)
(let* ((instr (e--instruction-with-id instr-id))
(links (overlay-get instr 'e-links))
(ids (plist-get links num-key)))
(unless (member update-id ids)
(setq ids (cons update-id ids))
(overlay-put instr 'e-links (plist-put links num-key ids))
t))))
(let ((new-link-count 0)
(involved-instrs (make-hash-table)))
(dolist (from-id from-list)
(when-let ((from-instr (e--instruction-with-id from-id)))
(dolist (to-id to-list)
(when (/= from-id to-id)
(when-let ((to-instr (e--instruction-with-id to-id)))
(when (and (update-links from-id :to to-id)
(update-links to-id :from from-id))
(puthash from-instr t involved-instrs)
(puthash to-instr t involved-instrs)
(cl-incf new-link-count)))))))
(cl-loop for instr being the hash-keys of involved-instrs
do (e--update-instruction-overlay instr))
(when (called-interactively-p 'interactive)
(message "Created %d instruction link%s"
new-link-count
(if (= new-link-count 1) "" "s"))))))
(defun e-unlink-instructions (from-list to-list)
"Unlink instructions with ids in FROM-LIST from those in TO-LIST.
When invoked interactively, prompts user for two lists of instruction ids."
(interactive
(let ((completion-table (mapcar #'number-to-string (hash-table-keys e--id-usage-map))))
(list (mapcar #'string-to-number
(completing-read-multiple "Select instruction ids to unlink: "
completion-table nil t))
(mapcar #'string-to-number
(completing-read-multiple "Select instruction ids to unlink from: "
completion-table nil t)))))
(cl-labels
((remove-links (instr-id num-key remove-id)
(let* ((instr (e--instruction-with-id instr-id))
(links (overlay-get instr 'e-links))
(ids (plist-get links num-key)))
(when (member remove-id ids)
(setq ids (remove remove-id ids))
(overlay-put instr 'e-links (plist-put links num-key ids))
t))))
(let ((removed-link-count 0)
(involved-instrs (make-hash-table)))
(dolist (from-id from-list)
(when-let ((from-instr (e--instruction-with-id from-id)))
(dolist (to-id to-list)
(when-let ((to-instr (e--instruction-with-id to-id)))
(when (and (remove-links from-id :to to-id)
(remove-links to-id :from from-id))
(puthash from-instr t involved-instrs)
(puthash to-instr t involved-instrs)
(cl-incf removed-link-count))))))
(cl-loop for instr being the hash-keys of involved-instrs
do (when (buffer-live-p (overlay-buffer instr))
(e--update-instruction-overlay instr)))
(when (called-interactively-p 'interactive)
(message "Removed %d instruction link%s"
removed-link-count
(if (= removed-link-count 1) "" "s"))))))
(defun e-cycle-instructions-at-point (point)
"Cycle through instructions at POINT, highlighting them.
This command allows for cycling through overlapping instructions at a
point in the buffer and allows one to have better accuracy when instructions
overlap to the point where no other reasonable option is available."
(interactive "d")
(let ((instructions-at-point (e--instructions-at point))
(original-highlighted-instruction e--highlighted-instruction))
(cond
((null instructions-at-point)
(setq e--highlighted-instruction nil)
(when (called-interactively-p 'any)
(message "No instructions at point")))
((or (null e--highlighted-instruction)
(not (memq e--highlighted-instruction instructions-at-point)))
(setq e--highlighted-instruction nil)
(setq e--highlighted-instruction (e--highest-priority-instruction instructions-at-point)))
(t
(if-let ((parent (e--parent-instruction e--highlighted-instruction)))
(setq e--highlighted-instruction parent)
(setq e--highlighted-instruction nil))))
(when e--highlighted-instruction
(e--update-instruction-overlay e--highlighted-instruction))
(when original-highlighted-instruction
(e--update-instruction-overlay original-highlighted-instruction))
e--highlighted-instruction))
(defun e-modify-directive ()
"Modify the directive under the point."
(interactive)
(when-let ((directive (e--highest-priority-instruction (e--instructions-at (point) 'directive)
t)))
(when (eq (overlay-get directive 'e-directive-status) 'processing)
(overlay-put directive 'e-directive-status nil))
(let ((topmost-directive (e--topmost-instruction directive 'directive)))
(when (eq (overlay-get topmost-directive 'e-directive-status) 'failed)
(setf (overlay-get topmost-directive 'e-directive-status) nil)
(e--update-instruction-overlay topmost-directive t)))
(e--read-directive directive)))
(defun e-modify-reference-commentary ()
"Modify the reference commentary under the point."
(interactive)
(when-let ((reference (e--highest-priority-instruction (e--instructions-at (point) 'reference)
t)))
(e--read-commentary reference)))
(defun e-process-directives ()
"Send directives to model via gptel.
If a region is selected, send all directives within the region.
If a region is not selected and there is a directive under the point, send it."
(interactive)
(let ((count 0))
(cl-labels ((execute (directive)
(unless (e--being-processed-p directive)
(if (e--directive-empty-p directive)
;; There is no point in sending an empty directive to gptel.
(e--process-directive-llm-response "The directive is empty!"
(list :context directive))
(gptel-request (e--directive-llm-prompt directive)
:system (e--directive-llm-system-message directive)
:dry-run nil
:stream nil
:in-place nil
:callback #'e--process-directive-llm-response
:context directive)
(overlay-put directive 'e-directive-status 'processing)
(e--update-instruction-overlay directive t)
(setq count (1+ count))))))
(if (region-active-p)
(when-let ((toplevel-directives
(cl-remove-duplicates
(mapcar (lambda (instr)
(e--topmost-instruction instr 'directive))
(e--instructions-in (region-beginning)
(region-end)
'directive)))))
(dolist (directive toplevel-directives)
(execute directive)))
(if-let ((directive (e--topmost-instruction (e--highest-priority-instruction
(e--instructions-at (point) 'directive)
t)
'directive)))
(execute directive)
(when-let ((toplevel-directives (cl-remove-duplicates
(mapcar (lambda (instr)
(e--topmost-instruction instr 'directive))
(e--instructions-in (point-min)
(point-max)
'directive)))))
(dolist (dir toplevel-directives)
(execute dir)))))
(if (> count 0)
(message "Sent %d directive%s to gptel for processing"
count
(if (> count 1) "s" ""))
(message "No directives sent to gptel")))))
(defun e-delete-instructions ()
"Delete instruction(s) either at point or within the selected region.
Display a message to the user showing how many instructions were deleted.
Throw a user error if no instructions to delete were found."
(interactive)
(let ((deleted-count 0))
(if (use-region-p)
(let ((start (region-beginning))
(end (region-end)))
(dolist (overlay (e--wholly-contained-instructions (current-buffer) start end))
(when (overlay-get overlay 'e-instruction)
(e--delete-instruction overlay)
(setq deleted-count (1+ deleted-count))))
(when (> deleted-count 0)
(deactivate-mark))
(unless (> deleted-count 0)
(user-error "No instructions to delete within the selected region")))
(let ((overlay (e--delete-instruction-at (point))))
(when overlay
(setq deleted-count 1))
(unless overlay
(user-error "No instruction to delete at point"))))
(when (> deleted-count 0)
(message "Deleted %d instruction%s" deleted-count (if (> deleted-count 1) "s" "")))))
(defun e-delete-all-instructions ()
"Delete all Evedel instructions across all buffers."
(interactive)
(let ((instr-count (length (e--instructions))))
(when (and (called-interactively-p 'any)
(zerop instr-count))
(user-error "No instructions to delete"))
(when (and (called-interactively-p 'any)
instr-count
(not (y-or-n-p "Are you sure you want to delete all instructions?")))
(user-error "Aborted")))
(let ((buffer-count 0)
(deleted-instr-count 0))
(e--foreach-instruction instr
with buffer-hash = (make-hash-table)
unless (gethash (overlay-buffer instr) buffer-hash)
do (progn
(puthash (overlay-buffer instr) t buffer-hash)
(cl-incf buffer-count))
do (progn
(e--delete-instruction instr)
(cl-incf deleted-instr-count)))
(when (not (zerop deleted-instr-count))
(message "Deleted %d Evedel instruction%s in %d buffer%s"
deleted-instr-count
(if (= 1 deleted-instr-count) "" "s")
buffer-count
(if (= 1 buffer-count) "" "s"))))
(setq e--instructions nil)
(e--reset-id-counter))
(defun e-convert-instructions ()
"Convert instructions between reference and directive type.
If a region is selected, convert all instructions within the region. If no
region is selected, convert only the highest priority instruction at point.
Bodyless directives cannot be converted to references. Attempting to do so
will throw a user error."
(interactive)
(let* ((instructions (if (use-region-p)
(e--instructions-in (region-beginning)
(region-end))
(cl-remove-if #'null
(list (e--highest-priority-instruction
(e--instructions-at (point))
t)))))
(num-instructions (length instructions))
(converted-directives-to-references 0)
(converted-references-to-directives 0))
(if (= num-instructions 0)
(user-error "No instructions to convert")
(dolist (instr instructions)
(cond
((e--directivep instr)
(unless (e--bodyless-instruction-p instr)
(overlay-put instr 'e-instruction-type 'reference)
(setq converted-directives-to-references (1+ converted-directives-to-references))))
((e--referencep instr)
(overlay-put instr 'e-instruction-type 'directive)
(setq converted-references-to-directives (1+ converted-references-to-directives)))
(t
(user-error "Unknown instruction type")))
(e--update-instruction-overlay instr t))
(let ((msg "Converted %d instruction%s")
(conversion-msgs
(delq nil
(list (when (> converted-directives-to-references 0)
(format "%d directive%s to reference%s"
converted-directives-to-references
(if (= converted-directives-to-references 1) "" "s")
(if (= converted-directives-to-references 1) "" "s")))
(when (> converted-references-to-directives 0)
(format "%d reference%s to directive%s"
converted-references-to-directives
(if (= converted-references-to-directives 1) "" "s")
(if (= converted-references-to-directives 1) "" "s")))))))
(message (concat
msg (if conversion-msgs
(concat ": " (mapconcat #'identity conversion-msgs " and "))
""))
num-instructions
(if (> num-instructions 1) "s" ""))
(when (region-active-p)
(deactivate-mark))))))
(defun e-next-instruction ()
"Cycle through instructions in the forward direction."
(interactive)
(unless (e--cycle-instruction nil 'next)
(e--print-instruction-not-found 'next nil)))
(defun e-previous-instruction ()
"Cycle through instructions in the backward direction."
(interactive)
(unless (e--cycle-instruction nil 'previous)
(e--print-instruction-not-found 'previous nil)))
(defun e-next-reference ()
"Cycle through references in the forward direction."
(interactive)
(unless (e--cycle-instruction 'reference 'next)
(e--print-instruction-not-found 'next 'reference)))
(defun e-previous-reference ()
"Cycle through references in the backward direction."
(interactive)
(unless (e--cycle-instruction 'reference 'previous)
(e--print-instruction-not-found 'previous 'reference)))
(defun e-next-directive ()
"Cycle through directives in the forward direction."
(interactive)
(unless (e--cycle-instruction 'directive 'next)
(e--print-instruction-not-found 'next 'directive)))
(defun e-previous-directive ()
"Cycle through directives in the backward direction."
(interactive)
(unless (e--cycle-instruction 'directive 'previous)
(e--print-instruction-not-found 'previous 'directive)))
(defun e-preview-directive-prompt ()
"Preview directive prompt at the current point.
This command is useful to see what is actually being sent to the model."
(interactive)
(let ((directive (e--topmost-instruction (car (e--instructions-at (point) 'directive))
'directive)))
(let ((request-string (e--directive-llm-prompt directive)))
(let ((bufname "*evedel-directive-preview*"))
(with-temp-buffer-window bufname
'((display-buffer-reuse-window
display-buffer-same-window))
nil
(princ (format "<!-- SYSTEM: %s -->"
(replace-regexp-in-string "\n"
"\n# "
(e--directive-llm-system-message directive))))
(princ "\n\n")
(princ request-string)
(with-current-buffer bufname
(when (fboundp 'markdown-mode)
(markdown-mode))
(read-only-mode 1)
(visual-line-mode 1)
(display-line-numbers-mode 1)
(let ((local-map (make-sparse-keymap)))
(set-keymap-parent local-map (current-local-map))
(define-key local-map (kbd "q") 'quit-window)
(use-local-map local-map))))))))
(defun e-modify-directive-tag-query ()
"Prompt minibuffer to enter a tag search query for a directive.
The directive in question is either the directive under the curent point.
A tag query is an _infix_ expression, containing symbol atoms and the operator
symbols: `and', `or', `not'. If no operator is present between two expressions,
then an implicit `and' operator is assumed.
Examples:
(signature and function and doc)
(not dog or not cat)
(cat or dog or (sheep and black))
((cat and dog) or (dog and goose))"
(interactive)
(if-let ((directive (e--topmost-instruction
(e--highest-priority-instruction (e--instructions-at (point)) t)
'directive)))
(e--read-directive-tag-query directive)
(user-error "No directive at point")))
(defun e-directive-undo (&optional arg)
"Undo the last change of the directive history at point.
If ARG is nonzero, traverse the directive history backwards; otherwise, forwards."
(interactive "P")
(let ((directive (e--highest-priority-instruction
(e--instructions-at (point) 'directive))))
(if directive
(e--directive-next-history directive (not (null arg)))
(user-error "No directive found at point"))))
(defun e-add-tags (&optional reference)
"Add tags to the reference under the point.
Adds specificly to REFERENCE if it is non-nil."
(interactive)
(let* ((instructions (e--instructions-at (point) 'reference))
(instr (or reference (e--highest-priority-instruction instructions t))))
(if instr
(let* ((existing-tags (e--available-tags))
(input (completing-read-multiple "Add tags (or leave empty): "
existing-tags nil nil))
(new-tags (mapcar #'intern input)))
(let ((added (e--add-tags instr new-tags)))
(message "%d tag%s added" added (if (= added 1) "" "s"))))
(user-error "No reference at point"))))
(defun e-remove-tags ()
"Remove tags from the reference under the point."
(interactive)
(let* ((instructions (e--instructions-at (point) 'reference))
(instr (e--highest-priority-instruction instructions t)))
(if instr
(let ((tags-list (e--reference-tags instr)))
(if (null tags-list)
(user-error "Reference has no tags of its own to remove")
;; Prompt the user to remove tags.
(let* ((input (completing-read-multiple "Remove tags: " tags-list nil t))
(tags-to-remove (mapcar #'intern input)))
(let ((removed (e--remove-tags instr tags-to-remove)))
(message "%d tag%s removed" removed (if (= removed 1) "" "s"))))))
(user-error "No reference at point"))))
(declare-function e--instruction-with-id "evedel.el")
(let ((map (make-hash-table)))
(cl-defun e--instruction-with-id (target-id)
"Return the instruction with the given integer TARGET-ID.
Returns nil if no instruction with the spcific id was found."
(when-let ((instr (gethash target-id map)))
(when (buffer-live-p instr)
(cl-return-from e--instruction-with-id instr)))
(setq map (make-hash-table))
(e--foreach-instruction instr
do (puthash (e--instruction-id instr) instr map))
(gethash target-id map)))
(cl-defun e--patch-save-file (save-file)
"Return a patched SAVE-FILE that matches the current version."
(let ((save-file-version (plist-get save-file :version))
(new-save-file ()))
(when (string= save-file-version (e-version))
(cl-return-from e--patch-save-file save-file))
(cl-labels ((recreate-instr-ids (files-alist)
(let ((e--id-counter 0)
(e--id-usage-map (make-hash-table))
(e--retired-ids ()))
(cl-loop for (_ . file-plist) in files-alist
do (let ((instr-plists (plist-get file-plist :instructions)))
(cl-loop for instr-plist in instr-plists
do (let ((ov-props (plist-get instr-plist :properties)))
(with-temp-buffer
(let ((ov (make-overlay 1 1)))
(mapc (lambda (prop)
(overlay-put ov
prop
(plist-get ov-props prop)))
ov-props)
(overlay-put ov 'e-id (e--create-id))
(plist-put instr-plist
:properties
(overlay-properties ov))))))))
(cl-values files-alist e--id-counter e--id-usage-map e--retired-ids)))
(recreate-id-counter (files-alist)
(cl-multiple-value-bind (files-alist id-counter id-usage-map retired-ids)
(recreate-instr-ids files-alist)
(cl-values
(list :id-counter id-counter
:used-ids (hash-table-keys id-usage-map)
:retired-ids retired-ids)
files-alist))))
;; There is no save file version available. This means we are using a save file whose version
;; is v0.4.7 or older. Only v0.4.7 is newer support backward save compatibility.
;;
;; This branch updates version v0.4.7 to the latest version by adding ids to the existing
;; instructions, and changing the save file to include the latest version number and the id
;; counter.
(if (null save-file-version)
(condition-case err
(cl-multiple-value-bind (ids-plist files-alist) (recreate-id-counter save-file)
(setq new-save-file (plist-put new-save-file :ids ids-plist))
(setq new-save-file (plist-put new-save-file :files files-alist)))
(error
(error "Error patching a versionless save file.
Save file backward compatibility was added in v0.4.7. If the save file is older than that, then \
unfortunately it is no longer supported. If the save file is from v0.4.7 or newer, then this is a \
bug that you should report.
The error: %s" err)))
(pcase save-file-version
("v0.4.9"
;; v0.4.9 had a problem where overlays which were deleted extrajudicially did not retire
;; their id number, causing the id to be used perpetually. This patch cleans the used id
;; list.
(cl-multiple-value-bind (ids-plist files-alist)
(recreate-id-counter (plist-get save-file :files))
(setq new-save-file (plist-put new-save-file :ids ids-plist))
(setq new-save-file (plist-put new-save-file :files files-alist))))
;; Save file is a newer version, but needs no patching. We would still like to display
;; a message indicating that the file underwent a patching procedure.
(_ (setq new-save-file save-file))))
(if new-save-file
(progn
(message "Patched loaded save file to version %s" (e-version))
(setq new-save-file (plist-put new-save-file :version (e-version)))
new-save-file)
save-file))))
(defun e--instruction-id (instruction)
(overlay-get instruction 'e-id))
(defun e--stashed-buffer-instructions (buffer)
(e--foreach-instruction (instr buffer)
collect (list :overlay-start (overlay-start instr)
:overlay-end (overlay-end instr)
:properties (overlay-properties instr))))
(defun e--stash-buffer (buffer &optional file-contents)
(let ((instrs (e--stashed-buffer-instructions buffer)))
(when instrs
(with-current-buffer buffer
(let ((original-content (or file-contents (buffer-substring-no-properties (point-min)
(point-max)))))
(setf (alist-get buffer e--instructions)
(list :original-content original-content
:instructions instrs)
(car (assoc buffer e--instructions))
(buffer-file-name buffer))
(mapc #'delete-overlay (e--instructions-in (point-min) (point-max))))))))
(defun e--read-directive-tag-query (directive)
"Prompt user to enter a directive tag query text via minibuffer for DIRECTIVE."
(let ((original-tag-query (overlay-get directive 'e-directive-infix-tag-query-string))
(timer nil)
(minibuffer-message))
(minibuffer-with-setup-hook
(lambda ()
(add-hook 'minibuffer-exit-hook
(lambda ()
(when timer
(cancel-timer timer)))
nil t)
(add-hook 'after-change-functions
(lambda (_beg _end _len)
(when timer
(cancel-timer timer))
(setq timer
(run-with-timer
0.5
nil
(lambda ()
(condition-case err
(let ((query (read (concat "("
(minibuffer-contents)
")"))))
(let ((refs (e--filter-references
(e--tag-query-prefix-from-infix query)))
(total-count 0)
(buffer-count 0))
(cl-loop with bufhash = (make-hash-table)
for ref in refs
do (progn
(cl-incf total-count)
(puthash (overlay-buffer ref) t bufhash))
finally (setq buffer-count
(hash-table-count bufhash)))
(setq minibuffer-message
(format "%d hit%s in %d buffer%s"
total-count
(if (= total-count 1) "" "s")
buffer-count
(if (= buffer-count 1) "" "s")))))
(error
(setq minibuffer-message
(error-message-string err))))
(when minibuffer-message
(set-minibuffer-message minibuffer-message))))))
nil t))
(condition-case err
(let ((tag-query (read-from-minibuffer "Directive tag query: "
(substring-no-properties
(or original-tag-query "")))))
(let ((parsed-prefix-tag-query
(e--tag-query-prefix-from-infix (read (concat "(" tag-query ")")))))
(overlay-put directive 'e-directive-prefix-tag-query parsed-prefix-tag-query)
(if (string-empty-p tag-query)
(overlay-put directive 'e-directive-infix-tag-query-string nil)
(overlay-put directive
'e-directive-infix-tag-query-string
;; Since Emacs doesn't have negative-lookaheads, we have to make due
;; by first applying the face we want the symbols to be, and then
;; applying the default face on everything we don't want to match.
(e--apply-face-to-match "\\b\\(?:(*not\\|or\\|and\\)\\b\\|(\\|)"
(e--apply-face-to-match
"\\(:?.+\\)"
tag-query
'font-lock-constant-face)
nil))
(overlay-put directive 'e-directive-status nil)))
(e--update-instruction-overlay directive t))
(error
(message (error-message-string err)))))))
(defun e--print-instruction-not-found (direction type)
"Print a not found message for the given DIRECTION and TYPE."
(let ((type-string (pcase type
('directive "directive")
('reference "reference")
(_ "instruction"))))
(message "No %s %s found"
(if (eq direction 'next) "next" "previous")
type-string)))
(cl-defun e--reference-matches-query-p (reference query)
"Return t only if REFERENCE matches the tag QUERY."
(unless reference
(cl-return-from e--reference-matches-query-p nil))
(let ((atoms (cl-remove-duplicates (cl-remove-if (lambda (elm)
(member elm '(not or and nil)))
(flatten-tree query)))))
(if (and (null atoms) e-empty-tag-query-matches-all)
t
(let ((tags (e--reference-tags reference t))
(direct-tags (e--reference-tags reference nil))
(instr-id (lambda (tag) (let ((tagname (symbol-name tag)))
(when (string-match "^id:\\([1-9][0-9]*\\)$" tagname)
(string-to-number (match-string 1 tagname)))))))
(if (and (null direct-tags) e-always-match-untagged-references)
t
(let ((atom-bindings (mapcar (lambda (atom)
(pcase atom
('is:bufferlevel
(e--instruction-bufferlevel-p reference))
('is:subreference
(e--parent-instruction reference 'reference))
('is:tagless
(null tags))
('is:directly-tagless
(null (e--reference-tags reference nil)))
('is:with-commentary
(not (string-empty-p (e--commentary-text reference))))
(_ (if-let ((id (funcall instr-id atom)))
(= id (e--instruction-id reference))
(member atom tags)))))
atoms)))