Skip to content

Commit 378cb30

Browse files
authored
Merge pull request #2006 from mychris/feature/bookmark-extension
Adds a new bookmark package.
2 parents 256df65 + 9550dfc commit 378cb30

File tree

3 files changed

+300
-1
lines changed

3 files changed

+300
-1
lines changed

extensions/bookmark/bookmark.lisp

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
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)))))
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
(defsystem "lem-bookmark"
2+
:depends-on ("lem/core")
3+
:serial t
4+
:components ((:file "bookmark")))

lem.asd

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,8 @@
288288
"lem-legit"
289289
"lem-dashboard"
290290
"lem-copilot"
291-
"lem-claude-code"))
291+
"lem-claude-code"
292+
"lem-bookmark"))
292293

293294
(defsystem "lem"
294295
:version "2.3.0"

0 commit comments

Comments
 (0)