branch: elpa/javelin
commit b08af48e1c3c8637ca26b60c3ef2a910f703977c
Author: Damian Barabonkov <[email protected]>
Commit: Damian Barabonkov <[email protected]>

    feat: Migrate harpoon storage to JSON format
---
 harpoon.el | 308 +++++++++++++++++++++++++++++++++++++++++--------------------
 1 file changed, 208 insertions(+), 100 deletions(-)

diff --git a/harpoon.el b/harpoon.el
index 1fa67af1885..8139f2f25d7 100644
--- a/harpoon.el
+++ b/harpoon.el
@@ -1,6 +1,6 @@
 ;;; harpoon.el --- Bookmarks on steroids    -*- lexical-binding: t; -*-
 
-;; Copyright (C) 2022  Otávio Schwanck
+;; Copyright (C) 2022  Otávio Schwanck, 2025 Damian Barabonkov
 
 ;; Author: Otávio Schwanck <[email protected]>
 ;; Keywords: tools languages
@@ -43,10 +43,6 @@
   "Return the default project package."
   (if (featurep 'projectile) 'projectile 'project))
 
-(defvar harpoon-mode-map
-  (let ((map (make-sparse-keymap)))
-    (define-key map (kbd "<return>") #'harpoon-find-file) map))
-
 (defgroup harpoon nil
   "Organize bookmarks by project and branch."
   :group 'tools)
@@ -70,12 +66,6 @@
 (defvar harpoon-cache '()
   "Cache for harpoon.")
 
-(defvar harpoon--current-project-path nil
-  "Current project path on harpoon.  Its only transactional.")
-
-(defvar harpoon--project-path nil
-  "Current project name on harpoon.  Its only transactional.")
-
 (defvar harpoon-cache-loaded nil
   "Cache for harpoon.")
 
@@ -134,7 +124,71 @@
 
 (defun harpoon--file-name ()
   "File name for harpoon on current project."
-  (concat harpoon-cache-file (harpoon--cache-key)))
+  (concat harpoon-cache-file (harpoon--cache-key) ".json"))
+
+(defun harpoon--read-json ()
+  "Read and parse the harpoon JSON file.
+Returns a list of alists with `harpoon_number' and `filepath' keys."
+  (if (file-exists-p (harpoon--file-name))
+      (condition-case nil
+          (json-parse-string (f-read (harpoon--file-name) 'utf-8)
+                             :object-type 'alist
+                             :array-type 'list)
+        (error '()))
+    '()))
+
+(defun harpoon--write-json (data)
+  "Write DATA to the harpoon JSON file.
+DATA should be a list of alists with `harpoon_number' and `filepath' keys."
+  (harpoon--create-directory)
+  (f-write-text (json-serialize data) 'utf-8 (harpoon--file-name)))
+
+(defun harpoon--get-filepath-by-number (harpoon-number)
+  "Get the filepath for a given HARPOON-NUMBER.
+Returns nil if not found."
+  (let ((data (harpoon--read-json)))
+    (alist-get 'filepath
+               (seq-find (lambda (item)
+                           (= (alist-get 'harpoon_number item) harpoon-number))
+                         data))))
+
+(defun harpoon--set-filepath-by-number (harpoon-number filepath)
+  "Set FILEPATH for a given HARPOON-NUMBER.
+Updates existing entry or adds a new one."
+  (let* ((data (harpoon--read-json))
+         (existing (seq-find (lambda (item)
+                               (= (alist-get 'harpoon_number item) 
harpoon-number))
+                             data)))
+    (if existing
+        ;; Update existing entry
+        (setf (alist-get 'filepath existing) filepath)
+      ;; Add new entry
+      (push `((harpoon_number . ,harpoon-number) (filepath . ,filepath)) data))
+    (harpoon--write-json data)))
+
+(defun harpoon--remove-by-number (harpoon-number)
+  "Remove entry with HARPOON-NUMBER from the harpoon list."
+  (let ((data (harpoon--read-json)))
+    (harpoon--write-json
+     (seq-remove (lambda (item)
+                   (= (alist-get 'harpoon_number item) harpoon-number))
+                 data))))
+
+(defun harpoon--get-all-filepaths ()
+  "Get all filepaths from harpoon, sorted by harpoon_number.
+Returns a list of filepaths."
+  (let ((data (harpoon--read-json)))
+    (mapcar (lambda (item) (alist-get 'filepath item))
+            (seq-sort (lambda (a b)
+                        (< (alist-get 'harpoon_number a)
+                           (alist-get 'harpoon_number b)))
+                      data))))
+
+(defun harpoon--next-available-number ()
+  "Get the next available harpoon number."
+  (let* ((data (harpoon--read-json))
+         (numbers (mapcar (lambda (item) (alist-get 'harpoon_number item)) 
data)))
+    (if numbers (1+ (apply #'max numbers)) 1)))
 
 (defun harpoon--buffer-file-name ()
   "Parse harpoon file name."
@@ -145,32 +199,26 @@
   (s-replace-regexp "/" "---" string))
 
 ;;;###autoload
-(defun harpoon-go-to (line-number)
-  "Go to specific file on harpoon (by line order). LINE-NUMBER: Line to go."
+(defun harpoon-go-to (harpoon-number)
+  "Go to specific file on harpoon by HARPOON-NUMBER."
   (require 'project)
-
-  (let* ((file-name (s-replace-regexp "\n" ""
-                                      (with-temp-buffer
-                                        (insert-file-contents-literally
-                                         (if (eq major-mode 'harpoon-mode)
-                                             (file-truename (buffer-file-name))
-                                           (harpoon--file-name)))
-                                        (goto-char (point-min))
-                                        (forward-line (- line-number 1))
-                                        (buffer-substring-no-properties 
(line-beginning-position) (line-end-position)))))
-         (full-file-name (if (and (fboundp 'project-root) 
(harpoon--has-project)) (concat (or harpoon--project-path 
(harpoon-project-root-function)) file-name) file-name)))
-    (if (file-exists-p full-file-name)
-        (find-file full-file-name)
-      (message (concat full-file-name " not found.")))))
-
-(defun harpoon--delete (line-number)
-  "Delete an item on harpoon. LINE-NUMBER: Line of item to delete."
-  (harpoon-toggle-file)
-  (goto-char (point-min)) (forward-line (- line-number 1))
-  (kill-whole-line)
-  (save-buffer)
-  (kill-buffer)
-  (harpoon-delete-item))
+  (let* ((file-name (harpoon--get-filepath-by-number harpoon-number))
+         (full-file-name (when file-name
+                           (if (and (fboundp 'project-root) 
(harpoon--has-project))
+                               (concat (or harpoon--project-path 
(harpoon-project-root-function)) file-name)
+                             file-name))))
+    (cond
+     ((null file-name)
+      (message "No file harpooned to position %d" harpoon-number))
+     ((file-exists-p full-file-name)
+      (find-file full-file-name))
+     (t
+      (message "%s not found." full-file-name)))))
+
+(defun harpoon--delete (harpoon-number)
+  "Delete an item on harpoon. HARPOON-NUMBER: Position to delete."
+  (harpoon--remove-by-number harpoon-number)
+  (message "Deleted harpoon position %d" harpoon-number))
 
 
 ;;;###autoload
@@ -281,38 +329,116 @@
   (interactive)
   (harpoon-go-to 9))
 
+(defun harpoon-assign-to (harpoon-number)
+  "Assign the current buffer to a specific position in harpoon.
+HARPOON-NUMBER: The position (1-9) to assign the current file to."
+  (require 'project)
+  (let ((file-to-add (harpoon--buffer-file-name)))
+    (harpoon--set-filepath-by-number harpoon-number file-to-add)
+    (message "Assigned %s to harpoon position %d" file-to-add harpoon-number)))
+
+;;;###autoload
+(defun harpoon-assign-to-1 ()
+  "Assign current buffer to position 1 on harpoon."
+  (interactive)
+  (harpoon-assign-to 1))
+
+;;;###autoload
+(defun harpoon-assign-to-2 ()
+  "Assign current buffer to position 2 on harpoon."
+  (interactive)
+  (harpoon-assign-to 2))
+
+;;;###autoload
+(defun harpoon-assign-to-3 ()
+  "Assign current buffer to position 3 on harpoon."
+  (interactive)
+  (harpoon-assign-to 3))
+
+;;;###autoload
+(defun harpoon-assign-to-4 ()
+  "Assign current buffer to position 4 on harpoon."
+  (interactive)
+  (harpoon-assign-to 4))
+
+;;;###autoload
+(defun harpoon-assign-to-5 ()
+  "Assign current buffer to position 5 on harpoon."
+  (interactive)
+  (harpoon-assign-to 5))
+
+;;;###autoload
+(defun harpoon-assign-to-6 ()
+  "Assign current buffer to position 6 on harpoon."
+  (interactive)
+  (harpoon-assign-to 6))
+
+;;;###autoload
+(defun harpoon-assign-to-7 ()
+  "Assign current buffer to position 7 on harpoon."
+  (interactive)
+  (harpoon-assign-to 7))
+
+;;;###autoload
+(defun harpoon-assign-to-8 ()
+  "Assign current buffer to position 8 on harpoon."
+  (interactive)
+  (harpoon-assign-to 8))
+
+;;;###autoload
+(defun harpoon-assign-to-9 ()
+  "Assign current buffer to position 9 on harpoon."
+  (interactive)
+  (harpoon-assign-to 9))
+
 ;;;###autoload
 (defun harpoon-go-to-next ()
   "Go to the next file in harpoon."
   (interactive)
-  (let* ((files (delete "" (split-string (harpoon--get-file-text) "\n")))
+  (let* ((data (harpoon--read-json))
+         (sorted-data (seq-sort (lambda (a b)
+                                  (< (alist-get 'harpoon_number a)
+                                     (alist-get 'harpoon_number b)))
+                                data))
+         (files (mapcar (lambda (item) (alist-get 'filepath item)) 
sorted-data))
          (current-file (harpoon--buffer-file-name))
          (current-index (or (cl-position current-file files :test 'string=) 
-1))
-         (next-index (mod (+ current-index 1) (length files))))
-    (harpoon-go-to (1+ next-index))))
+         (next-index (mod (+ current-index 1) (length files)))
+         (next-item (nth next-index sorted-data)))
+    (when next-item
+      (harpoon-go-to (alist-get 'harpoon_number next-item)))))
 
 ;;;###autoload
 (defun harpoon-go-to-prev ()
   "Go to the previous file in harpoon."
   (interactive)
-  (let* ((files (delete "" (split-string (harpoon--get-file-text) "\n")))
+  (let* ((data (harpoon--read-json))
+         (sorted-data (seq-sort (lambda (a b)
+                                  (< (alist-get 'harpoon_number a)
+                                     (alist-get 'harpoon_number b)))
+                                data))
+         (files (mapcar (lambda (item) (alist-get 'filepath item)) 
sorted-data))
          (current-file (harpoon--buffer-file-name))
          (current-index (or (cl-position current-file files :test 'string=) 
-1))
-         (prev-index (mod (+ current-index (length files) -1) (length files))))
-    (harpoon-go-to (1+ prev-index))))
+         (prev-index (mod (+ current-index (length files) -1) (length files)))
+         (prev-item (nth prev-index sorted-data)))
+    (when prev-item
+      (harpoon-go-to (alist-get 'harpoon_number prev-item)))))
 
 ;;;###autoload
 (defun harpoon-add-file ()
   "Add current file to harpoon."
   (interactive)
-  (harpoon--create-directory)
-  (let ((harpoon-current-file-text
-         (harpoon--get-file-text)))
-    (if (string-match-p (harpoon--buffer-file-name) harpoon-current-file-text)
+  (let* ((file-to-add (harpoon--buffer-file-name))
+         (data (harpoon--read-json))
+         (existing (seq-find (lambda (item)
+                               (string= (alist-get 'filepath item) 
file-to-add))
+                             data)))
+    (if existing
         (message "This file is already on harpoon.")
-      (progn
-        (f-write-text (concat harpoon-current-file-text 
(harpoon--buffer-file-name) "\n") 'utf-8 (harpoon--file-name))
-        (message "File added to harpoon.")))))
+      (let ((next-num (harpoon--next-available-number)))
+        (harpoon--set-filepath-by-number next-num file-to-add)
+        (message "File added to harpoon at position %d." next-num)))))
 
 ;;;###autoload
 (defun harpoon-quick-menu-hydra ()
@@ -342,14 +468,19 @@
 
 (defun harpoon--hydra-candidates (method)
   "Candidates for hydra. METHOD = Method to execute on harpoon item."
-  (let ((line-number 0)
-        (full-candidates (seq-take (delete "" (split-string 
(harpoon--get-file-text) "\n")) 9)))
+  (let* ((data (harpoon--read-json))
+         (sorted-data (seq-sort (lambda (a b)
+                                  (< (alist-get 'harpoon_number a)
+                                     (alist-get 'harpoon_number b)))
+                                data))
+         (full-candidates (seq-take sorted-data 9)))
     (mapcar (lambda (item)
-              (setq line-number (+ 1 line-number))
-              (list (format "%s" line-number)
-                    (intern (concat method (format "%s" line-number)))
-                    (harpoon--format-item-name item)
-                    :column (if (< line-number 6) "1-5" "6-9")))
+              (let ((num (alist-get 'harpoon_number item))
+                    (filepath (alist-get 'filepath item)))
+                (list (format "%s" num)
+                      (intern (concat method (format "%s" num)))
+                      (harpoon--format-item-name filepath)
+                      :column (if (< num 6) "1-5" "6-9"))))
             full-candidates)))
 
 (defun harpoon--format-item-name (item)
@@ -364,7 +495,7 @@ FULL-CANDIDATES:  Candidates to be edited."
 ITEM = Full item.  SPLITTED-ITEM = Item splitted.
 FULL-CANDIDATES = All candidates to look."
   (let ((file-base-name (nth (- (length splitted-item) 1) splitted-item))
-        (candidates (seq-take (delete "" (split-string 
(harpoon--get-file-text) "\n")) 9)))
+        (candidates (seq-take (harpoon--get-all-filepaths) 9)))
     (if (member file-base-name (mapcar (lambda (x)
                                          (nth (- (length (split-string x "/")) 
1) (split-string x "/")))
                                        (delete item candidates)))
@@ -396,24 +527,20 @@ Select items to delete:
   (when (fboundp 'harpoon-delete-hydra/body) (harpoon-delete-hydra/body)))
 
 
-(defun harpoon--get-file-text ()
-  "Get text inside harpoon file."
-  (if (file-exists-p (harpoon--file-name))
-      (f-read (harpoon--file-name) 'utf-8) ""))
-
 (defun harpoon--package-name ()
   "Return harpoon package name."
   "harpoon")
 
+;;;###autoload
 ;;;###autoload
 (defun harpoon-toggle-file ()
-  "Open harpoon file."
+  "Open harpoon JSON file for viewing/editing."
   (interactive)
-  (unless (eq major-mode 'harpoon-mode)
-    (harpoon--create-directory)
-    (setq harpoon--current-project-path (when (harpoon--has-project) 
(harpoon-project-root-function)))
-    (find-file (harpoon--file-name) '(:dedicated t))
-    (harpoon-mode)))
+  (harpoon--create-directory)
+  ;; Ensure file exists with empty array if it doesn't
+  (unless (file-exists-p (harpoon--file-name))
+    (harpoon--write-json '()))
+  (find-file (harpoon--file-name)))
 
 ;;;###autoload
 (defun harpoon-toggle-quick-menu ()
@@ -430,44 +557,25 @@ Select items to delete:
 
 (defun harpoon--fix-quick-menu-items ()
   "Fix harpoon quick menu items."
-  (if (harpoon--has-project)
-      (completing-read "Harpoon to file: " (harpoon--add-numbers-to-quick-menu 
(delete "" (split-string (harpoon--get-file-text) "\n"))))
-    (completing-read "Harpoon to file: " (harpoon--add-numbers-to-quick-menu 
(delete "" (split-string (harpoon--get-file-text) "\n"))))))
-
-(defun harpoon--add-numbers-to-quick-menu (files)
-  "Add numbers to files.  FILES = Files to add the numbers."
-  (let ((line-number 0))
-    (mapcar (lambda (line) (setq line-number (+ 1 line-number)) (concat 
(format "%s" line-number) " - " line)) files)))
-
-(define-derived-mode harpoon-mode nil "Harpoon"
-  "Mode for harpoon."
-  (setq-local require-final-newline mode-require-final-newline)
-  (setq-local harpoon--project-path harpoon--current-project-path)
-  (setq harpoon--current-project-path nil)
-  (display-line-numbers-mode t))
+  (let* ((data (harpoon--read-json))
+         (sorted-data (seq-sort (lambda (a b)
+                                  (< (alist-get 'harpoon_number a)
+                                     (alist-get 'harpoon_number b)))
+                                data))
+         (items (mapcar (lambda (item)
+                          (format "%d - %s"
+                                  (alist-get 'harpoon_number item)
+                                  (alist-get 'filepath item)))
+                        sorted-data)))
+    (completing-read "Harpoon to file: " items)))
 
 ;;;###autoload
 (defun harpoon-clear ()
   "Clear harpoon files."
   (interactive)
   (when (yes-or-no-p "Do you really want to clear harpoon file? ")
-    (if (eq major-mode 'harpoon-mode)
-        (progn (f-write "" 'utf-8 (file-truename (buffer-file-name)))
-               (kill-buffer))
-      (f-write "" 'utf-8 (harpoon--file-name)))
+    (harpoon--write-json '())
     (message "Harpoon cleaned.")))
 
-;;;###autoload
-(defun harpoon-find-file ()
-  "Visit file on `harpoon-mode'."
-  (interactive)
-  (let* ((line (buffer-substring-no-properties (point-at-bol) (point-at-eol)))
-         (path (concat harpoon--project-path line)))
-    (if (file-exists-p path)
-        (progn (save-buffer)
-               (kill-buffer)
-               (find-file path))
-      (message "File not found."))))
-
 (provide 'harpoon)
 ;;; harpoon.el ends here

Reply via email to