diff options
| -rw-r--r-- | llm-tools-hl-test.el | 180 | ||||
| -rw-r--r-- | llm-tools-hl.el | 168 |
2 files changed, 334 insertions, 14 deletions
diff --git a/llm-tools-hl-test.el b/llm-tools-hl-test.el index 4bc0fa0..85777c8 100644 --- a/llm-tools-hl-test.el +++ b/llm-tools-hl-test.el | |||
| @@ -2,7 +2,7 @@ | |||
| 2 | (require 'llm-tools-hl) | 2 | (require 'llm-tools-hl) |
| 3 | 3 | ||
| 4 | (ert-deftest llm-tools--hl-file-read-test () | 4 | (ert-deftest llm-tools--hl-file-read-test () |
| 5 | "Test that 'llm-tools--hl-file-read' produces correct hashline format." | 5 | "Test that 'llm-tools--hl-file-read' produces correct hashline format." |
| 6 | (let* ((content "\ | 6 | (let* ((content "\ |
| 7 | const TITLE = \"Mr\"; | 7 | const TITLE = \"Mr\"; |
| 8 | export function greet(name) { | 8 | export function greet(name) { |
| @@ -23,3 +23,181 @@ export function greet(name) { | |||
| 23 | (unwind-protect | 23 | (unwind-protect |
| 24 | (should (equal actual expected)) | 24 | (should (equal actual expected)) |
| 25 | (delete-file file)))) | 25 | (delete-file file)))) |
| 26 | |||
| 27 | (ert-deftest llm-tools--hl-anchor-to-line-num-test () | ||
| 28 | (should (= (llm-tools--hl-anchor-to-line-num "5ff") 5)) | ||
| 29 | (should (= (llm-tools--hl-anchor-to-line-num "123ab") 123)) | ||
| 30 | (should (= (llm-tools--hl-anchor-to-line-num "1vx") 1))) | ||
| 31 | |||
| 32 | (ert-deftest llm-tools--hl-parse-section-single-replace () | ||
| 33 | (let ((section "= 1vx..1vx\n~const TITLE = \"Mrs\";")) | ||
| 34 | (should (= (length (llm-tools--hl-parse-section section)) 1)) | ||
| 35 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 36 | (should (eq (hl-op-type op) 'replace)) | ||
| 37 | (should (equal (hl-op-range op) '("1vx" "1vx"))) | ||
| 38 | (should (equal (hl-op-payload op) '("const TITLE = \"Mrs\";")))))) | ||
| 39 | |||
| 40 | (ert-deftest llm-tools--hl-parse-section-multi-line-payload () | ||
| 41 | (let ((section "= 3gv..6be\n~\treturn [\n~\t\t\"Mrs\",\n~\t\tname?.trim() || \"guest\",\n~\t].join(\" \");")) | ||
| 42 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 43 | (should (= (length (hl-op-payload op)) 4))))) | ||
| 44 | |||
| 45 | (ert-deftest llm-tools--hl-parse-section-blank-replace () | ||
| 46 | "Replace with no payload should produce nil payload, not empty list." | ||
| 47 | (let ((section "= 5ff..5ff")) | ||
| 48 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 49 | (should (eq (hl-op-type op) 'replace)) | ||
| 50 | (should (null (hl-op-payload op)))))) | ||
| 51 | |||
| 52 | (ert-deftest llm-tools--hl-parse-section-insert-after () | ||
| 53 | (let ((section "+ 4ei\n~\t\t\"Dr\",")) | ||
| 54 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 55 | (should (eq (hl-op-type op) 'insert-after)) | ||
| 56 | (should (string= (hl-op-anchor op) "4ei")) | ||
| 57 | (should (equal (hl-op-payload op) '("\t\t\"Dr\",")))))) | ||
| 58 | |||
| 59 | (ert-deftest llm-tools--hl-parse-section-insert-before () | ||
| 60 | (let ((section "< 5ff\n~\t\t\"Dr\",")) | ||
| 61 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 62 | (should (eq (hl-op-type op) 'insert-before)) | ||
| 63 | (should (string= (hl-op-anchor op) "5ff"))))) | ||
| 64 | |||
| 65 | (ert-deftest llm-tools--hl-parse-section-delete () | ||
| 66 | (let ((section "- 5ff..5ff")) | ||
| 67 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 68 | (should (eq (hl-op-type op) 'delete)) | ||
| 69 | (should (equal (hl-op-range op) '("5ff" "5ff")))))) | ||
| 70 | |||
| 71 | (ert-deftest llm-tools--hl-parse-section-skip-comments-and-blanks () | ||
| 72 | (let ((section "# comment\n\n+ 4ei\n~line")) | ||
| 73 | (should (= (length (llm-tools--hl-parse-section section)) 1)))) | ||
| 74 | |||
| 75 | (ert-deftest llm-tools--hl-parse-section-multiple-ops () | ||
| 76 | "Two separate insert ops in one section." | ||
| 77 | (let ((section "+ 4ei\n~first\n< 5ff\n~second")) | ||
| 78 | (let ((ops (llm-tools--hl-parse-section section))) | ||
| 79 | (should (= (length ops) 2)) | ||
| 80 | (should (eq (hl-op-type (car ops)) 'insert-after)) | ||
| 81 | (should (eq (hl-op-type (cadr ops)) 'insert-before))))) | ||
| 82 | |||
| 83 | (ert-deftest llm-tools--hl-apply-replace-single-line () | ||
| 84 | (let* ((content "line1\nline2\nline3\n") | ||
| 85 | (file (make-temp-file nil nil nil content)) | ||
| 86 | (patch (format "@@ test\n= 2%s..2%s\n~REPLACED" (elt 1 (llm-tools--hl-hash (string-lines content)))))) | ||
| 87 | (unwind-protect | ||
| 88 | (progn | ||
| 89 | (llm-tools--hl-apply patch) | ||
| 90 | (should (string= (llm-tools--hl-file-read file) | ||
| 91 | (string-join | ||
| 92 | (list (format "1%s|line1" (llm-tools--hl-hash "line1")) | ||
| 93 | (format "2%s|REPLACED" (llm-tools--hl-hash "REPLACED")) | ||
| 94 | (format "3%s|line3" (llm-tools--hl-hash "line3"))) | ||
| 95 | "\n")))) | ||
| 96 | (delete-file file)))) | ||
| 97 | |||
| 98 | (ert-deftest llm-tools--hl-apply-insert-after () | ||
| 99 | (let* ((content "A\nB\nC\n") | ||
| 100 | (file (make-temp-file nil nil nil content)) | ||
| 101 | (anchor (format "+ %s" (llm-tools--hl-file-line-hash file 1))) | ||
| 102 | (patch (concat "@@ test\n" anchor "\n~NEW"))) | ||
| 103 | (unwind-protect | ||
| 104 | (progn | ||
| 105 | (llm-tools--hl-apply patch) | ||
| 106 | (let ((lines (llm-tools--hl-file-lines file))) | ||
| 107 | (should (equal lines '("A" "NEW" "B" "C"))))) | ||
| 108 | (delete-file file)))) | ||
| 109 | |||
| 110 | (ert-deftest llm-tools--hl-apply-insert-before () | ||
| 111 | (let* ((content "A\nB\nC\n") | ||
| 112 | (file (make-temp-file nil nil nil content)) | ||
| 113 | (anchor (format "< %s" (llm-tools--hl-file-line-hash file 3))) | ||
| 114 | (patch (concat "@@ test\n" anchor "\n~NEW"))) | ||
| 115 | (unwind-protect | ||
| 116 | (progn | ||
| 117 | (llm-tools--hl-apply patch) | ||
| 118 | (let ((lines (llm-tools--hl-file-lines file))) | ||
| 119 | (should (equal lines '("A" "B" "NEW" "C"))))) | ||
| 120 | (delete-file file)))) | ||
| 121 | |||
| 122 | (ert-deftest llm-tools--hl-apply-append-to-eof () | ||
| 123 | (let* ((content "A\nB\n") | ||
| 124 | (file (make-temp-file nil nil nil content)) | ||
| 125 | (patch "@@ test\n+ EOF\n~APPENDED")) | ||
| 126 | (unwind-protect | ||
| 127 | (progn | ||
| 128 | (llm-tools--hl-apply patch) | ||
| 129 | (let ((lines (llm-tools--hl-file-lines file))) | ||
| 130 | (should (equal lines '("A" "B" "APPENDED"))))) | ||
| 131 | (delete-file file)))) | ||
| 132 | |||
| 133 | (ert-deftest llm-tools--hl-apply-delete () | ||
| 134 | (let* ((content "A\nB\nC\n") | ||
| 135 | (file (make-temp-file nil nil nil content)) | ||
| 136 | (anchor (llm-tools--hl-file-line-hash file 2)) | ||
| 137 | (patch (format "@@ test\n- 2%s..2%s" anchor anchor))) | ||
| 138 | (unwind-protect | ||
| 139 | (progn | ||
| 140 | (llm-tools--hl-apply patch) | ||
| 141 | (let ((lines (llm-tools--hl-file-lines file))) | ||
| 142 | (should (equal lines '("A" "C"))))) | ||
| 143 | (delete-file file)))) | ||
| 144 | |||
| 145 | (ert-deftest llm-tools--hl-apply-blank-line () | ||
| 146 | "Replace with no payload produces a blank line." | ||
| 147 | (let* ((content "A\nB\nC\n") | ||
| 148 | (file (make-temp-file nil nil nil content)) | ||
| 149 | (anchor (llm-tools--hl-file-line-hash file 2)) | ||
| 150 | (patch (format "@@ test\n= 2%s..2%s" anchor anchor))) | ||
| 151 | (unwind-protect | ||
| 152 | (progn | ||
| 153 | (llm-tools--hl-apply patch) | ||
| 154 | (let ((lines (llm-tools--hl-file-lines file))) | ||
| 155 | (should (equal lines '("A" "" "C"))))) | ||
| 156 | (delete-file file)))) | ||
| 157 | |||
| 158 | (ert-deftest llm-tools--hl-apply-offset-tracking () | ||
| 159 | "Multiple ops in one file: verify offset adjusts subsequent line numbers." | ||
| 160 | (let* ((content "A\nB\nC\nD\nE\n") | ||
| 161 | (file (make-temp-file nil nil nil content)) | ||
| 162 | (a1 (llm-tools--hl-file-line-hash file 1)) | ||
| 163 | (a3 (llm-tools--hl-file-line-hash file 3)) | ||
| 164 | ;; Insert 2 lines after line 1, then delete original line 3 | ||
| 165 | (patch (format "@@ test\n+ 1%s\n~X\n~Y\n- 3%s..3%s" a1 a3 a3))) | ||
| 166 | (unwind-protect | ||
| 167 | (progn | ||
| 168 | (llm-tools--hl-apply patch) | ||
| 169 | ;; After insert: A X Y B C D E (offset = +2) | ||
| 170 | ;; Original line 3 (C) is now at index 5 (1-based). Delete it. | ||
| 171 | ;; Result: A X Y B D E | ||
| 172 | (let ((lines (llm-tools--hl-file-lines file))) | ||
| 173 | (should (equal lines '("A" "X" "Y" "B" "D" "E"))))) | ||
| 174 | (delete-file file)))) | ||
| 175 | |||
| 176 | (ert-deftest llm-tools--hl-apply-multi-file () | ||
| 177 | "Two files in one patch both get modified." | ||
| 178 | (let* ((file1 (make-temp-file nil nil nil "A\nB\n")) | ||
| 179 | (file2 (make-temp-file nil nil nil "X\nY\n")) | ||
| 180 | (a1 (llm-tools--hl-file-line-hash file1 1)) | ||
| 181 | (a2 (llm-tools--hl-file-line-hash file2 2)) | ||
| 182 | (patch (format "@@ %s\n+ 1%s\n~NEW1\n@@ %s\n= 2%s..2%s\n~NEW2" | ||
| 183 | file1 a1 file2 a2 a2))) | ||
| 184 | (unwind-protect | ||
| 185 | (progn | ||
| 186 | (llm-tools--hl-apply patch) | ||
| 187 | (should (equal (llm-tools--hl-file-lines file1) '("A" "NEW1" "B"))) | ||
| 188 | (should (equal (llm-tools--hl-file-lines file2) '("X" "NEW2")))) | ||
| 189 | (delete-file file1) | ||
| 190 | (delete-file file2)))) | ||
| 191 | |||
| 192 | (ert-deftest llm-tools--hl-apply-multi-line-payload () | ||
| 193 | "Replace with many payload lines." | ||
| 194 | (let* ((content "A\nB\n") | ||
| 195 | (file (make-temp-file nil nil nil content)) | ||
| 196 | (a1 (llm-tools--hl-file-line-hash file 1)) | ||
| 197 | (patch (format "@@ test\n= 1%s..1%s\n~ONE\n~TWO\n~THREE" a1 a1))) | ||
| 198 | (unwind-protect | ||
| 199 | (progn | ||
| 200 | (llm-tools--hl-apply patch) | ||
| 201 | (let ((lines (llm-tools--hl-file-lines file))) | ||
| 202 | (should (equal lines '("ONE" "TWO" "THREE" "B"))))) | ||
| 203 | (delete-file file)))) | ||
diff --git a/llm-tools-hl.el b/llm-tools-hl.el index b4ca40a..808b837 100644 --- a/llm-tools-hl.el +++ b/llm-tools-hl.el | |||
| @@ -97,7 +97,7 @@ Each element is a line number and two-character hash, like `1vx'." | |||
| 97 | (defun llm-tools--hl-file-line-hash (file n) | 97 | (defun llm-tools--hl-file-line-hash (file n) |
| 98 | "Return the hash of the Nth line in FILE." | 98 | "Return the hash of the Nth line in FILE." |
| 99 | (let ((lines (llm-tools--hl-file-lines file))) | 99 | (let ((lines (llm-tools--hl-file-lines file))) |
| 100 | (llm-tools--hl-hash (elt lines (1- n))))) | 100 | (llm-tools--hl-hash (elt lines (1- n))))) |
| 101 | 101 | ||
| 102 | ;;;; Diff Parsing | 102 | ;;;; Diff Parsing |
| 103 | (defun llm-tools--split-patch-sections (patch) | 103 | (defun llm-tools--split-patch-sections (patch) |
| @@ -126,7 +126,7 @@ ANCHORS is a sorted, deduplicated list of all line anchors referenced by | |||
| 126 | the operations in that file's sections. Each anchor is a string like | 126 | the operations in that file's sections. Each anchor is a string like |
| 127 | \"5ff\" or a list of two strings from a range like | 127 | \"5ff\" or a list of two strings from a range like |
| 128 | (\"3gv\" \"6be\")." | 128 | (\"3gv\" \"6be\")." |
| 129 | (let ((sections (split-patch-sections patch))) | 129 | (let ((sections (llm-tools--split-patch-sections patch))) |
| 130 | (delete-dups | 130 | (delete-dups |
| 131 | (mapcar (lambda (patch) | 131 | (mapcar (lambda (patch) |
| 132 | (cons (car patch) | 132 | (cons (car patch) |
| @@ -151,12 +151,17 @@ line in the on-disk file." | |||
| 151 | (let ((patch-anchors (llm-tools--hl-get-anchors patch))) | 151 | (let ((patch-anchors (llm-tools--hl-get-anchors patch))) |
| 152 | (mapcar (lambda (section) | 152 | (mapcar (lambda (section) |
| 153 | (let* ((file (car section)) | 153 | (let* ((file (car section)) |
| 154 | (anchors (delete "EOF" (cdr section))) | 154 | (anchors (delete "BOF" (delete "EOF" (cdr section)))) |
| 155 | (hl (llm-tools--hl-file-read-hashes file))) | 155 | (hl (llm-tools--hl-file-read-hashes file))) |
| 156 | ;; cl-subsetp uses eql by default which compares by identity and not content | 156 | ;; cl-subsetp uses eql by default which compares by identity and not content |
| 157 | (cons file (cl-subsetp anchors hl :test #'equal)))) | 157 | (cons file (cl-subsetp anchors hl :test #'equal)))) |
| 158 | patch-anchors))) | 158 | patch-anchors))) |
| 159 | 159 | ||
| 160 | ;; TODO this should also validate the operators (e.g. any invalid | ||
| 161 | ;; op-chars, payload given before all other operations, no payload | ||
| 162 | ;; after +/< or payload given to -, or anchors without line | ||
| 163 | ;; number). it should return a string saying it's a malformed patch | ||
| 164 | ;; and what went wrong. | ||
| 160 | (defun llm-tools--hl-verify (patch) | 165 | (defun llm-tools--hl-verify (patch) |
| 161 | "Verify if the files in PATCH need to be re-read. | 166 | "Verify if the files in PATCH need to be re-read. |
| 162 | Returns a string message that states which files need to be re-read. | 167 | Returns a string message that states which files need to be re-read. |
| @@ -171,21 +176,158 @@ Empty string means no files need to be re-read." | |||
| 171 | (llm-tools--hl-verify-anchors patch)))) | 176 | (llm-tools--hl-verify-anchors patch)))) |
| 172 | "\n")) | 177 | "\n")) |
| 173 | 178 | ||
| 179 | (cl-defstruct hl-op | ||
| 180 | (type nil) ; 'insert-after 'insert-before 'delete 'replace | ||
| 181 | (anchor nil) ; string like "4ei" or "EOF" for +/-/< | ||
| 182 | (range nil) ; (start . end) strings for - and = | ||
| 183 | (payload nil)) ; list of strings (nil is no payload) | ||
| 184 | |||
| 185 | (defun llm-tools--hl-anchor-to-line-num (anchor) | ||
| 186 | "Extract the line number from ANCHOR like \"5ff\"." | ||
| 187 | (string-to-number (substring anchor 0 (- (length anchor) 2)))) | ||
| 188 | |||
| 189 | (defun llm-tools--hl-parse-op-line (line) | ||
| 190 | "Parse a single operation LINE into a `hl-op` struct. | ||
| 191 | LINE should start with `+`, `<`, `-`, or `=`." | ||
| 192 | (pcase (substring line 0 1) | ||
| 193 | ("+" | ||
| 194 | (make-hl-op :type 'insert-after | ||
| 195 | :anchor (substring line 2))) | ||
| 196 | ("<" | ||
| 197 | (make-hl-op :type 'insert-before | ||
| 198 | :anchor (substring line 2))) | ||
| 199 | ("-" | ||
| 200 | (make-hl-op :type 'delete | ||
| 201 | :range (split-string (substring line 2) "\\.\\."))) | ||
| 202 | ("=" | ||
| 203 | (make-hl-op :type 'replace | ||
| 204 | :range (split-string (substring line 2) "\\.\\."))))) | ||
| 205 | |||
| 174 | (defun llm-tools--hl-parse-section (section) | 206 | (defun llm-tools--hl-parse-section (section) |
| 175 | "Parses operation and optional payload from SECTION." | 207 | "Parse SECTION (body after `@@ PATH`) into a list of `hl-op` structs. |
| 176 | ) | 208 | Skips comment lines (starting with `#`) and blank lines." |
| 209 | (let (ops op) | ||
| 210 | (dolist (line (string-lines section)) | ||
| 211 | (when (string-match "\\S-" line) | ||
| 212 | (let ((op-char (substring line 0 1))) | ||
| 213 | (pcase op-char | ||
| 214 | ("~" | ||
| 215 | (when op | ||
| 216 | (setf (hl-op-payload op) | ||
| 217 | (append (hl-op-payload op) | ||
| 218 | (list (substring line 1)))))) | ||
| 219 | ("#" nil) | ||
| 220 | (_ | ||
| 221 | (when op (push op ops)) | ||
| 222 | (setq op (llm-tools--hl-parse-op-line line)))))) | ||
| 223 | (when op (push op ops)) | ||
| 224 | (nreverse ops)))) | ||
| 225 | |||
| 226 | (defun llm-tools--hl-group-by-file (sections) | ||
| 227 | "Group SECTIONS by file path. | ||
| 228 | Returns an alist of (FILENAME . SECTION-LIST) where SECTION-LIST | ||
| 229 | contains all (PATH . REST) cons cells from SECTIONS that target | ||
| 230 | FILENAME." | ||
| 231 | (let (grouped) | ||
| 232 | (dolist (sec sections) | ||
| 233 | (let ((existing (assoc (car sec) grouped))) | ||
| 234 | (if existing | ||
| 235 | (push sec (cdr existing)) | ||
| 236 | (push (cons (car sec) (list sec)) grouped)))) | ||
| 237 | grouped)) | ||
| 238 | |||
| 239 | (defun llm-tools--hl-apply-one-op (op content offset) | ||
| 240 | "Apply a single OP to CONTENT (list of strings) at the given OFFSET. | ||
| 241 | Returns a cons cell (NEW-CONTENT . NEW-OFFSET)." | ||
| 242 | (pcase (hl-op-type op) | ||
| 243 | ('insert-after | ||
| 244 | (let* ((anchor (hl-op-anchor op)) | ||
| 245 | (payload (hl-op-payload op)) | ||
| 246 | (line-num (if (string= anchor "EOF") | ||
| 247 | (length content) | ||
| 248 | (+ (llm-tools--hl-anchor-to-line-num anchor) | ||
| 249 | offset))) | ||
| 250 | (idx (1- line-num))) | ||
| 251 | (cons (append (cl-subseq content 0 (1+ idx)) | ||
| 252 | payload | ||
| 253 | (cl-subseq content (1+ idx))) | ||
| 254 | (+ offset (length payload))))) | ||
| 255 | ('insert-before | ||
| 256 | (let* ((anchor (hl-op-anchor op)) | ||
| 257 | (payload (hl-op-payload op)) | ||
| 258 | (line-num (if (string= anchor "BOF") | ||
| 259 | 1 | ||
| 260 | (+ (llm-tools--hl-anchor-to-line-num anchor) | ||
| 261 | offset))) | ||
| 262 | (idx (1- line-num))) | ||
| 263 | (cons (append (cl-subseq content 0 idx) | ||
| 264 | payload | ||
| 265 | (cl-subseq content idx)) | ||
| 266 | (+ offset (length payload))))) | ||
| 267 | ('delete | ||
| 268 | (let* ((range (hl-op-range op)) | ||
| 269 | (a (+ (llm-tools--hl-anchor-to-line-num (car range)) offset)) | ||
| 270 | (b (+ (llm-tools--hl-anchor-to-line-num (cadr range)) offset))) | ||
| 271 | (cons (append (cl-subseq content 0 (1- a)) | ||
| 272 | (cl-subseq content b)) | ||
| 273 | (- offset (- b a 1))))) | ||
| 274 | ('replace | ||
| 275 | (let* ((range (hl-op-range op)) | ||
| 276 | (a (+ (llm-tools--hl-anchor-to-line-num (car range)) offset)) | ||
| 277 | (b (+ (llm-tools--hl-anchor-to-line-num (cadr range)) offset)) | ||
| 278 | (payload (or (hl-op-payload op) | ||
| 279 | '("")))) | ||
| 280 | (cons (append (cl-subseq content 0 (1- a)) | ||
| 281 | payload | ||
| 282 | (cl-subseq content b)) | ||
| 283 | (+ offset (- (length payload) (- b a 1)))))))) | ||
| 284 | |||
| 285 | (defun llm-tools--hl-apply-ops (ops content) | ||
| 286 | "Apply a list of OPS to CONTENT (list of strings). | ||
| 287 | Returns the modified list of strings." | ||
| 288 | (let ((offset 0)) | ||
| 289 | (dolist (op ops content) | ||
| 290 | (let ((result (llm-tools--hl-apply-one-op op content offset))) | ||
| 291 | (setq content (car result)) | ||
| 292 | (setq offset (cdr result)))))) | ||
| 177 | 293 | ||
| 178 | (defun llm-tools--hl-apply (patch) | 294 | (defun llm-tools--hl-apply (patch) |
| 179 | "TODO rewrite docstring. Applies a patch." | 295 | "Apply all operations in PATCH to their respective files. |
| 180 | ;; when performing each operation, I need to make sure that the line | 296 | Operations within a file are applied sequentially, with line numbers |
| 181 | ;; numbers of subsequent operations in same file also get updated. | 297 | adjusted for prior insertions/deletions via a running offset." |
| 182 | (let ((sections (split-patch-sections patch))) | 298 | (dolist (file-group (llm-tools--hl-group-by-file |
| 183 | (llm-tools--hl-parse sections))) | 299 | (llm-tools--split-patch-sections patch))) |
| 300 | (let* ((file (car file-group)) | ||
| 301 | (ops (mapcan (lambda (sec) | ||
| 302 | (llm-tools--hl-parse-section (cdr sec))) | ||
| 303 | (cdr file-group))) | ||
| 304 | (content (llm-tools--hl-apply-ops | ||
| 305 | ops | ||
| 306 | (llm-tools--hl-file-lines file)))) | ||
| 307 | (with-temp-buffer | ||
| 308 | (insert (string-join content "\n")) | ||
| 309 | (write-region (point-min) (point-max) file))))) | ||
| 184 | 310 | ||
| 185 | (defun llm-tools--hl-edit (patch) | 311 | (defun llm-tools--hl-edit (patch) |
| 186 | "TODO write better docstring. First verifies if any files in PATCH needs | 312 | "Apply a hashline PATCH to the files it references. |
| 187 | to be re-read. Then, applies the changes in PATCH. If any files need to | 313 | |
| 188 | be re-read, the edit operation is cancelled for all files." | 314 | PATCH is a string containing one or more sections, each starting with |
| 315 | `@@ FILEPATH' followed by operations: | ||
| 316 | + ANCHOR Insert lines after ANCHOR (or `EOF' to append) | ||
| 317 | < ANCHOR Insert lines before ANCHOR (or `BOF' to prepend) | ||
| 318 | - A..B Delete lines from A to B | ||
| 319 | = A..B Replace lines from A to B with the following payload | ||
| 320 | ~TEXT Payload line for the preceding operation | ||
| 321 | |||
| 322 | ANCHOR is a hashline string like `5ff' (line number + 2-char hash) or | ||
| 323 | `BOF' / `EOF'. Lines are written directly to disk. | ||
| 324 | |||
| 325 | Before applying, verifies that every anchor in PATCH still matches the | ||
| 326 | current file contents. If any file has drifted, the entire operation is | ||
| 327 | aborted and an error message is returned identifying the affected files. | ||
| 328 | |||
| 329 | Returns a success message if all operations applied, or an error string | ||
| 330 | if verification failed." | ||
| 189 | (let ((msg (llm-tools--hl-verify patch))) | 331 | (let ((msg (llm-tools--hl-verify patch))) |
| 190 | (if (string= msg "") | 332 | (if (string= msg "") |
| 191 | (progn | 333 | (progn |
