Skip to content

Commit d5a703a

Browse files
authored
Merge pull request #2103 from lem-project/feat/filer-directory-sync
feat(filer): auto-sync to current buffer's directory
2 parents eae687b + 6000cb9 commit d5a703a

File tree

3 files changed

+204
-3
lines changed

3 files changed

+204
-3
lines changed

lem-tests.asd

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
(:file "completion")
7474
(:file "command-line-arguments")
7575
(:file "window")
76-
(:file "legit"))
76+
(:file "legit")
77+
(:file "filer"))
7778
:perform (test-op (o c)
7879
(symbol-call :rove :run c)))

src/ext/filer.lisp

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
(defpackage :lem/filer
2-
(:use :cl :lem))
2+
(:use :cl :lem)
3+
(:export :subdirectory-p
4+
:expand-to-directory
5+
:expand-directory-item
6+
:find-child-directory-item
7+
:create-directory-item
8+
:directory-item
9+
:directory-item-open-p
10+
:directory-item-children
11+
:file-item
12+
:item-pathname))
313
(in-package :lem/filer)
414

515
(define-attribute triangle-attribute
616
(t :bold t :foreground :base0D))
717

18+
(define-attribute current-file-attribute
19+
(t :bold t :background :base01))
20+
821
(define-major-mode filer-mode ()
922
(:name "Filer"
1023
:keymap *filer-mode-keymap*)
@@ -155,11 +168,112 @@
155168
(defun filer-active-p ()
156169
(and (lem-core::frame-leftside-window (current-frame))
157170
(eq 'filer-mode
158-
(buffer-major-mode
171+
(buffer-major-mode
159172
(window-buffer
160173
(lem-core::frame-leftside-window
161174
(current-frame)))))))
162175

176+
(defun filer-buffer ()
177+
"Return the Filer buffer if it exists, or nil."
178+
(alexandria:when-let ((window (frame-leftside-window (current-frame))))
179+
(let ((buffer (window-buffer window)))
180+
(when (eq 'filer-mode (buffer-major-mode buffer))
181+
buffer))))
182+
183+
(defun filer-current-directory ()
184+
"Return the root directory of the current Filer, or nil if Filer is not active."
185+
(when (filer-active-p)
186+
(alexandria:when-let ((buffer (filer-buffer)))
187+
(alexandria:when-let ((root (root-item buffer)))
188+
(item-pathname root)))))
189+
190+
(defun subdirectory-p (child parent)
191+
"Return T if CHILD is a subdirectory of PARENT."
192+
(let ((child-str (namestring child))
193+
(parent-str (namestring parent)))
194+
(and (> (length child-str) (length parent-str))
195+
(string= parent-str child-str :end2 (length parent-str)))))
196+
197+
(defun find-child-directory-item (parent-item target-dir)
198+
"Find a child directory-item in PARENT-ITEM that is on the path to TARGET-DIR."
199+
(loop :for child :in (directory-item-children parent-item)
200+
:when (and (typep child 'directory-item)
201+
(let ((child-path (item-pathname child)))
202+
(or (uiop:pathname-equal child-path target-dir)
203+
(subdirectory-p target-dir child-path))))
204+
:return child))
205+
206+
(defun expand-directory-item (item)
207+
"Expand a directory-item if not already expanded."
208+
(unless (directory-item-open-p item)
209+
(setf (directory-item-open-p item) t)
210+
(setf (directory-item-children item)
211+
(create-directory-children (item-pathname item)))))
212+
213+
(defun expand-to-directory (root-item target-dir)
214+
"Expand the tree from ROOT-ITEM to TARGET-DIR.
215+
Returns T if expansion was performed, NIL otherwise."
216+
(let ((root-path (item-pathname root-item)))
217+
(when (or (uiop:pathname-equal root-path target-dir)
218+
(subdirectory-p target-dir root-path))
219+
(expand-directory-item root-item)
220+
(loop :for current-item := root-item
221+
:then next-item
222+
:for next-item := (find-child-directory-item current-item target-dir)
223+
:while next-item
224+
:do (expand-directory-item next-item)
225+
:finally (return t)))))
226+
227+
(defun current-file-overlay (buffer)
228+
"Get the current file highlight overlay for BUFFER."
229+
(buffer-value buffer 'current-file-overlay))
230+
231+
(defun (setf current-file-overlay) (overlay buffer)
232+
"Set the current file highlight overlay for BUFFER."
233+
(setf (buffer-value buffer 'current-file-overlay) overlay))
234+
235+
(defun clear-current-file-highlight (filer-buf)
236+
"Clear the current file highlight in FILER-BUF."
237+
(alexandria:when-let ((overlay (current-file-overlay filer-buf)))
238+
(delete-overlay overlay)
239+
(setf (current-file-overlay filer-buf) nil)))
240+
241+
(defun highlight-file-in-filer (filer-buf file-path)
242+
"Highlight FILE-PATH in FILER-BUF and move point to it."
243+
(clear-current-file-highlight filer-buf)
244+
(with-point ((point (buffer-point filer-buf)))
245+
(buffer-start point)
246+
(loop :while (not (end-buffer-p point))
247+
:do (back-to-indentation point)
248+
(alexandria:when-let ((item (text-property-at point :item)))
249+
(when (and (typep item 'file-item)
250+
(uiop:pathname-equal (item-pathname item) file-path))
251+
(with-point ((start point)
252+
(end point))
253+
(line-start start)
254+
(line-end end)
255+
(let ((overlay (make-overlay start end 'current-file-attribute)))
256+
(setf (current-file-overlay filer-buf) overlay)))
257+
(return)))
258+
(unless (line-offset point 1)
259+
(return)))))
260+
261+
(defun sync-filer-to-buffer-directory (buffer)
262+
"Sync the Filer to BUFFER's directory if Filer is active.
263+
Expands the tree to show the buffer's directory and highlights the current file."
264+
(when (and (filer-active-p)
265+
(not (eq 'filer-mode (buffer-major-mode buffer)))
266+
(not (not-switchable-buffer-p buffer)))
267+
(alexandria:when-let ((buffer-dir (ignore-errors (probe-file (buffer-directory buffer)))))
268+
(alexandria:when-let ((filer-buf (filer-buffer)))
269+
(alexandria:when-let ((root (root-item filer-buf)))
270+
(when (expand-to-directory root buffer-dir)
271+
(render filer-buf root))
272+
(alexandria:when-let ((file-path (buffer-filename buffer)))
273+
(highlight-file-in-filer filer-buf file-path)))))))
274+
275+
(add-hook *switch-to-buffer-hook* 'sync-filer-to-buffer-directory)
276+
163277
(defun deactive-filer ()
164278
(when (eq (current-window) (lem-core::frame-leftside-window (current-frame)))
165279
(next-window))

tests/filer.lisp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
(defpackage :lem-tests/filer
2+
(:use :cl :rove :lem :lem/filer))
3+
(in-package :lem-tests/filer)
4+
5+
;;; Test subdirectory-p
6+
(deftest subdirectory-p-test
7+
(testing "subdirectory-p returns T for valid subdirectories"
8+
(ok (subdirectory-p
9+
#P"/home/user/project/src/"
10+
#P"/home/user/project/"))
11+
(ok (subdirectory-p
12+
#P"/home/user/project/src/lib/"
13+
#P"/home/user/project/"))
14+
(ok (subdirectory-p
15+
#P"/a/b/c/"
16+
#P"/a/")))
17+
18+
(testing "subdirectory-p returns NIL for non-subdirectories"
19+
(ng (subdirectory-p
20+
#P"/home/user/project/"
21+
#P"/home/user/project/"))
22+
(ng (subdirectory-p
23+
#P"/home/user/other/"
24+
#P"/home/user/project/"))
25+
(ng (subdirectory-p
26+
#P"/home/"
27+
#P"/home/user/project/"))))
28+
29+
;;; Test create-directory-item
30+
(deftest create-directory-item-test
31+
(testing "create-directory-item creates closed directory by default"
32+
(let ((item (create-directory-item #P"/tmp/")))
33+
(ok (typep item 'directory-item))
34+
(ng (directory-item-open-p item))
35+
(ok (null (directory-item-children item)))))
36+
37+
(testing "create-directory-item with :open t creates open directory with children"
38+
(let ((item (create-directory-item #P"/tmp/" :open t)))
39+
(ok (typep item 'directory-item))
40+
(ok (directory-item-open-p item)))))
41+
42+
;;; Test expand-directory-item
43+
(deftest expand-directory-item-test
44+
(testing "expand-directory-item opens a closed directory"
45+
(let ((item (create-directory-item #P"/tmp/")))
46+
(ng (directory-item-open-p item))
47+
(expand-directory-item item)
48+
(ok (directory-item-open-p item))))
49+
50+
(testing "expand-directory-item does nothing if already open"
51+
(let ((item (create-directory-item #P"/tmp/" :open t)))
52+
(ok (directory-item-open-p item))
53+
(expand-directory-item item)
54+
(ok (directory-item-open-p item)))))
55+
56+
;;; Test find-child-directory-item
57+
(deftest find-child-directory-item-test
58+
(testing "find-child-directory-item finds matching child"
59+
(let ((root (create-directory-item #P"/tmp/" :open t)))
60+
;; Create a test subdirectory structure
61+
(let ((child (find-child-directory-item
62+
root
63+
(item-pathname root))))
64+
;; Should return nil if target is the same as root
65+
(ok (null child)))))
66+
67+
(testing "find-child-directory-item returns nil for non-matching path"
68+
(let ((root (create-directory-item #P"/tmp/" :open t)))
69+
(ok (null (find-child-directory-item
70+
root
71+
#P"/nonexistent/path/"))))))
72+
73+
;;; Test expand-to-directory
74+
(deftest expand-to-directory-test
75+
(testing "expand-to-directory returns T for same directory"
76+
(let ((root (create-directory-item #P"/tmp/")))
77+
(ok (expand-to-directory root #P"/tmp/"))))
78+
79+
(testing "expand-to-directory returns NIL for unrelated directory"
80+
(let ((root (create-directory-item #P"/tmp/")))
81+
(ng (expand-to-directory root #P"/var/")))))
82+
83+
;;; Note: Tests for filer-active-p, filer-buffer, filer-current-directory,
84+
;;; and highlight-file-in-filer are not included here because they require
85+
;;; a full editor environment with *implementation* bound.
86+
;;; These functions are tested manually through interactive use.

0 commit comments

Comments
 (0)