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