|
| 1 | +;;;; -*- mode: lisp; coding: utf-8 -*- |
| 2 | +;;;; Bookmark package for lem |
| 3 | +;;;; |
| 4 | + |
| 5 | +(defpackage :lem-bookmark |
| 6 | + (:use :cl :lem) |
| 7 | + (:export |
| 8 | + ;; customization variables |
| 9 | + :*file* |
| 10 | + :*keymap* |
| 11 | + ;; bookmark type |
| 12 | + :bookmark |
| 13 | + :bookmark-name |
| 14 | + :bookmark-filename |
| 15 | + :bookmark-position |
| 16 | + :bookmark-p |
| 17 | + ;; internal functions for commands |
| 18 | + :load-from-file |
| 19 | + :save-to-file |
| 20 | + ;; commands |
| 21 | + :bookmark-load |
| 22 | + :bookmark-save |
| 23 | + :bookmark-set |
| 24 | + :bookmark-set-no-position |
| 25 | + :bookmark-set-no-overwrite |
| 26 | + :bookmark-set-no-position-no-overwrite |
| 27 | + :bookmark-delete |
| 28 | + :bookmark-delete-all |
| 29 | + :bookmark-rename |
| 30 | + :bookmark-relocate |
| 31 | + :bookmark-jump)) |
| 32 | + |
| 33 | +(in-package :lem-bookmark) |
| 34 | + |
| 35 | +(setf (documentation *package* t) |
| 36 | + "Bookmarks for the lem editor. |
| 37 | +Bookmarks are paths to files or directories that make it easy to open them. Each |
| 38 | +bookmark has a name and possibly a position associated with it. |
| 39 | +
|
| 40 | +The command BOOKMARK-SET is used to create a new bookmark, which points to the file |
| 41 | +of the current buffer and the current cursor position. The name of the bookmark will be |
| 42 | +prompted for. If you wish to not associate a position with the bookmark, you can use |
| 43 | +BOOKMARK-SET-NO-POSITION. This might be useful, if another package is managing file |
| 44 | +positions for you. |
| 45 | +
|
| 46 | +To open a previously set bookmark, use BOOKMARK-JUMP. |
| 47 | +
|
| 48 | +The set bookmarks are not persisted automatically. The commands BOOKMARK-SAVE and |
| 49 | +BOOKMARK-LOAD are used to save and load the bookmarks from disk. The variable |
| 50 | +*FILE* configures from which file the bookmark information is read from/saved to. |
| 51 | +
|
| 52 | +The keymap *KEYMAP* has some pre-defined mappings for most of the available commands. |
| 53 | +
|
| 54 | +Use (DESCRIBE (FIND-PACKAGE \"LEM-BOOKMARK\")) to find all available commands.") |
| 55 | + |
| 56 | +(defvar *file* #P"bookmarks.lisp-expr" |
| 57 | + "File in which bookmarks are saved. |
| 58 | +If the file is a relative path, it is relative to LEM-HOME.") |
| 59 | + |
| 60 | +(defvar *keymap* |
| 61 | + (make-keymap :name "Bookmark keymap") |
| 62 | + "Keymap for bookmark related commands.") |
| 63 | + |
| 64 | +(defvar *bookmark-table* (make-hash-table :test #'equal)) |
| 65 | + |
| 66 | +(define-key *keymap* "x" 'bookmark-set) |
| 67 | +(define-key *keymap* "X" 'bookmark-set-no-overwrite) |
| 68 | +(define-key *keymap* "m" 'bookmark-set) |
| 69 | +(define-key *keymap* "M" 'bookmark-set-no-overwrite) |
| 70 | +(define-key *keymap* "j" 'bookmark-jump) |
| 71 | +(define-key *keymap* "g" 'bookmark-jump) |
| 72 | +(define-key *keymap* "l" 'bookmark-load) |
| 73 | +(define-key *keymap* "s" 'bookmark-save) |
| 74 | +(define-key *keymap* "d" 'bookmark-delete) |
| 75 | +(define-key *keymap* "D" 'bookmark-delete-all) |
| 76 | +(define-key *keymap* "r" 'bookmark-rename) |
| 77 | +(define-key *keymap* "R" 'bookmark-rename-no-overwrite) |
| 78 | +(define-key *keymap* "h" 'bookmark-relocate) |
| 79 | + |
| 80 | +(defstruct bookmark |
| 81 | + (name) |
| 82 | + (filename) |
| 83 | + (position)) |
| 84 | + |
| 85 | +(defun bookmark-deserialize (list) |
| 86 | + (let* ((bookmark-name (car list)) |
| 87 | + (bookmark-data (cdr list)) |
| 88 | + (entry (make-bookmark |
| 89 | + :name bookmark-name |
| 90 | + :filename (cdr (assoc :filename bookmark-data)) |
| 91 | + :position (cdr (assoc :position bookmark-data))))) |
| 92 | + entry)) |
| 93 | + |
| 94 | +(defun bookmark-serialize (entry) |
| 95 | + (remove-if (lambda (field) (and (consp field) (null (cdr field)))) |
| 96 | + (list (bookmark-name entry) |
| 97 | + (cons :filename (bookmark-filename entry)) |
| 98 | + (cons :position (bookmark-position entry))))) |
| 99 | + |
| 100 | +(defun %bookmark-insert (name buffer &key no-position) |
| 101 | + (let ((path (cond ((buffer-filename buffer) |
| 102 | + (buffer-filename buffer)) |
| 103 | + ((string= "Directory" (mode-name (buffer-major-mode buffer))) |
| 104 | + (buffer-directory buffer))))) |
| 105 | + (when path |
| 106 | + (setf (gethash name *bookmark-table*) |
| 107 | + (make-bookmark :name name |
| 108 | + :filename path |
| 109 | + :position (if no-position |
| 110 | + nil |
| 111 | + (position-at-point (buffer-point buffer))))) |
| 112 | + t))) |
| 113 | + |
| 114 | +(defun %bookmark-find (name) |
| 115 | + (gethash name *bookmark-table*)) |
| 116 | + |
| 117 | +(defun %bookmark-delete (entry) |
| 118 | + (remhash (bookmark-name entry) *bookmark-table*)) |
| 119 | + |
| 120 | +(defun %bookmark-update (entry &key (new-name nil new-name-p) |
| 121 | + (new-filename nil new-filename-p) |
| 122 | + (position nil new-position-p)) |
| 123 | + (when new-name-p |
| 124 | + (remhash (bookmark-name entry) *bookmark-table*) |
| 125 | + (setf (bookmark-name entry) new-name) |
| 126 | + (setf (gethash new-name *bookmark-table*) entry)) |
| 127 | + (when new-filename-p |
| 128 | + (setf (bookmark-filename entry) new-filename)) |
| 129 | + (when new-position-p |
| 130 | + (setf (bookmark-position entry) new-position))) |
| 131 | + |
| 132 | +(defun %bookmark-relocate (entry buffer &key no-position) |
| 133 | + (%bookmark-update |
| 134 | + entry |
| 135 | + :new-filename (buffer-filename buffer) |
| 136 | + :new-position (if no-position |
| 137 | + nil |
| 138 | + (position-at-point (buffer-point buffer))))) |
| 139 | + |
| 140 | +(defun %bookmark-apply-position (entry buffer) |
| 141 | + (when (bookmark-position entry) |
| 142 | + (move-to-position (buffer-point buffer) (bookmark-position entry)))) |
| 143 | + |
| 144 | +(defun prompt-for-bookmark (prompt) |
| 145 | + (let ((candidates (loop :for entry :being :the :hash-value :in *bookmark-table* |
| 146 | + :collect (lem/completion-mode:make-completion-item |
| 147 | + :detail (if (bookmark-position entry) |
| 148 | + (format nil "~a:~a" (bookmark-filename entry) (bookmark-position entry)) |
| 149 | + (format nil "~a" (bookmark-filename entry))) |
| 150 | + :label (bookmark-name entry))))) |
| 151 | + (prompt-for-string prompt |
| 152 | + :completion-function (lambda (x) (completion-strings x candidates :key #'lem/completion-mode:completion-item-label)) |
| 153 | + :test-function (lambda (x) (find x candidates :test #'string= :key #'lem/completion-mode:completion-item-label)) |
| 154 | + :history-symbol 'prompt-for-bookmark))) |
| 155 | + |
| 156 | +(defun load-from-file (file-path &optional (bookmark-table *bookmark-table*)) |
| 157 | + (with-open-file (input file-path :direction :input) |
| 158 | + (loop :for bookmark-line :in (read input) |
| 159 | + :do (let ((bookmark-entry (bookmark-deserialize bookmark-line))) |
| 160 | + (setf (gethash (bookmark-name bookmark-entry) bookmark-table) bookmark-entry))))) |
| 161 | + |
| 162 | +(define-command bookmark-load () () |
| 163 | + "Load bookmarks from the file specified in the *FILE*." |
| 164 | + (let* ((file *file*) |
| 165 | + (full-path (if (uiop:relative-pathname-p file) |
| 166 | + (uiop:merge-pathnames* file (lem-home)) |
| 167 | + file))) |
| 168 | + (handler-case (load-from-file full-path) |
| 169 | + (sb-int:simple-file-error (c) |
| 170 | + (editor-error "bookmark: ~a~&" c)))) |
| 171 | + nil) |
| 172 | + |
| 173 | +(defun save-to-file (file-path &optional (bookmark-table *bookmark-table*)) |
| 174 | + (with-open-file (output file-path |
| 175 | + :direction :output |
| 176 | + :if-exists :supersede |
| 177 | + :if-does-not-exist :create) |
| 178 | + (write (loop :for entry :being :the :hash-value :in bookmark-table |
| 179 | + :collect (bookmark-serialize entry)) |
| 180 | + :stream output) |
| 181 | + (write-line "" output)) |
| 182 | + nil) |
| 183 | + |
| 184 | +(define-command bookmark-save () () |
| 185 | + "Save bookmarks to *FILE*." |
| 186 | + (let* ((file *file*) |
| 187 | + (full-path (if (uiop:relative-pathname-p file) |
| 188 | + (uiop:merge-pathnames* file (lem-home)) |
| 189 | + file))) |
| 190 | + (handler-case (save-to-file full-path) |
| 191 | + (sb-int:simple-file-error (c) |
| 192 | + (editor-error "bookmark: ~a~&" c))))) |
| 193 | + |
| 194 | +(define-command bookmark-set (name) |
| 195 | + ((prompt-for-string "Boorkmark name: " :initial-value "")) |
| 196 | + "Set a new bookmark with NAME for the current buffer. |
| 197 | +If a bookmark with NAME already exists, it will be overwritten. |
| 198 | +
|
| 199 | +If called interactively, prompt for NAME." |
| 200 | + (unless (%bookmark-insert name (current-buffer)) |
| 201 | + (editor-error "bookmark: Buffer not visiting a file or directory~&"))) |
| 202 | + |
| 203 | +(define-command bookmark-set-no-position (name) |
| 204 | + ((prompt-for-string "Boorkmark name: " :initial-value "")) |
| 205 | + "Set a new bookmark with NAME for the current buffer without position. |
| 206 | +If a bookmark with NAME already exists, it will be overwritten. |
| 207 | +
|
| 208 | +If called interactively, prompt for NAME." |
| 209 | + (unless (%bookmark-insert name (current-buffer) :no-position t) |
| 210 | + (editor-error "bookmark: Buffer not visiting a file or directory~&"))) |
| 211 | + |
| 212 | +(define-command bookmark-set-no-overwrite (name) |
| 213 | + ((prompt-for-string "Boorkmark name: " :initial-value "")) |
| 214 | + "Set a new bookmark with NAME for the current buffer. |
| 215 | +If a bookmark with NAME already exists, it will be left unchanged. |
| 216 | +
|
| 217 | +If called interactively, prompt for NAME." |
| 218 | + (if (gethash name *bookmark-table*) |
| 219 | + (editor-error "bookmark: ~a: Bookmark already exists~&" name) |
| 220 | + (bookmark-set name))) |
| 221 | + |
| 222 | +(define-command bookmark-set-no-position-no-overwrite (name) |
| 223 | + ((prompt-for-string "Boorkmark name: " :initial-value "")) |
| 224 | + "Set a new bookmark with NAME for the current buffer without position. |
| 225 | +If a bookmark with NAME already exists, it will be left unchanged. |
| 226 | +
|
| 227 | +If called interactively, prompt for NAME." |
| 228 | + (if (gethash name *bookmark-table*) |
| 229 | + (editor-error "bookmark: ~a: Bookmark already exists~&" name) |
| 230 | + (bookmark-set-no-position name))) |
| 231 | + |
| 232 | +(define-command bookmark-delete (name) ((prompt-for-bookmark "Delete bookmark: ")) |
| 233 | + "Delete the bookmark with NAME. |
| 234 | +
|
| 235 | +If called interactively, prompt for NAME." |
| 236 | + (if (null (gethash name *bookmark-table*)) |
| 237 | + (editor-error "bookmark: ~a: Bookmark does not exist~&" name) |
| 238 | + (%bookmark-delete (gethash name *bookmark-table*)))) |
| 239 | + |
| 240 | +(define-command bookmark-delete-all () () |
| 241 | + "Delete all bookmarks." |
| 242 | + (if (<= (hash-table-count *bookmark-table*) 0) |
| 243 | + (editor-error "bookmark: No bookmarks available~&") |
| 244 | + (when (prompt-for-y-or-n-p (format nil "Do you really want to delete ~a bookmark~a?" |
| 245 | + (hash-table-count *bookmark-table*) |
| 246 | + (if (< 1 (hash-table-count *bookmark-table*)) "s" ""))) |
| 247 | + (setq *bookmark-table* (clrhash *bookmark-table*))))) |
| 248 | + |
| 249 | +(define-command bookmark-rename (old-name new-name) ((prompt-for-bookmark "Rename bookmark: ") |
| 250 | + (prompt-for-string "New bookmark name: ")) |
| 251 | + "Rename the bookmark with OLD-NAME to NEW-NAME. |
| 252 | +If a bookmark with NEW-NAME already exists, it will be overwritten. |
| 253 | +
|
| 254 | +If called interactively, prompt for OLD-NAME and NEW-NAME." |
| 255 | + (let ((entry (gethash old-name *bookmark-table*))) |
| 256 | + (if (null entry) |
| 257 | + (editor-error "bookmark: ~a: Bookmark does not exist~&" old-name) |
| 258 | + (%bookmark-update entry :new-name new-name)))) |
| 259 | + |
| 260 | +(define-command bookmark-rename-no-overwrite (old-name new-name) ((prompt-for-bookmark "Rename bookmark: ") |
| 261 | + (prompt-for-string "New bookmark name: ")) |
| 262 | + "Rename the bookmark with OLD-NAME to NEW-NAME. |
| 263 | +If a bookmark with NEW-NAME already exists, it will be left unchanged. |
| 264 | +
|
| 265 | +If called interactively, prompt for OLD-NAME and NEW-NAME." |
| 266 | + (let ((entry (gethash old-name *bookmark-table*))) |
| 267 | + (if (null entry) |
| 268 | + (editor-error "bookmark: ~a: Bookmark does not exist~&" old-name) |
| 269 | + (if (not (null (gethash new-name *bookmark-table*))) |
| 270 | + (editor-error "bookmark: ~a Bookmark already exists~&" new-name) |
| 271 | + (%bookmark-update entry :new-name new-name))))) |
| 272 | + |
| 273 | +(define-command bookmark-relocate (name) ((prompt-for-bookmark "Relocate bookmark: ")) |
| 274 | +"Relocate the bookmark NAME to the position and file of the current buffer. |
| 275 | +
|
| 276 | +If called interactively, prompt for NAME." |
| 277 | +(let ((entry (gethash name *bookmark-table*))) |
| 278 | + (if (null entry) |
| 279 | + (editor-error "bookmark: ~a: Bookmark does not exist~&" name) |
| 280 | + (let ((buffer (current-buffer))) |
| 281 | + (if (null (buffer-filename buffer)) |
| 282 | + (editor-error "bookmark: Buffer not visiting a file or directory~&") |
| 283 | + (%bookmark-relocate entry buffer)))))) |
| 284 | + |
| 285 | +(define-command bookmark-jump (name) ((prompt-for-bookmark "Jump to bookmark: ")) |
| 286 | +"Jump to the bookmark with NAME in the current window. |
| 287 | +If the bookmark is associated with a position, jump to it. |
| 288 | +
|
| 289 | +If called interactively, prompt for NAME." |
| 290 | +(let ((entry (gethash name *bookmark-table*))) |
| 291 | + (if (null entry) |
| 292 | + (editor-error "bookmark: ~a: Bookmark does not exist~&" name) |
| 293 | + (let ((buffer (find-file (bookmark-filename entry)))) |
| 294 | + (%bookmark-apply-position entry buffer))))) |
0 commit comments