diff --git a/.gitignore b/.gitignore index 50dec9aff..701f69faf 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ build/* /systems/ /result + +TODO diff --git a/extensions/lisp-mode/repl.lisp b/extensions/lisp-mode/repl.lisp index aec89927f..a2d906bb8 100644 --- a/extensions/lisp-mode/repl.lisp +++ b/extensions/lisp-mode/repl.lisp @@ -101,10 +101,10 @@ :callback (lambda (&rest args) (declare (ignore args)) (copy-down-to-repl 'pathname - (lem/directory-mode::get-pathname (current-point)))))) + (lem/directory-mode/internal::get-pathname (current-point)))))) (defun repl-compute-context-menu-items () - (if (lem/directory-mode::get-pathname (current-point)) + (if (lem/directory-mode/internal::get-pathname (current-point)) (list (context-menu-copy-down-pathname-to-repl)) (remove nil @@ -612,8 +612,8 @@ (define-repl-shortcut ls () (insert-character (current-point) #\newline) - (lem/directory-mode::insert-directories-and-files (current-point) - (buffer-directory (current-buffer))) + (lem/directory-mode/internal::insert-directories-and-files (current-point) + (buffer-directory (current-buffer))) (lem/listener-mode:refresh-prompt (current-buffer))) (define-repl-shortcut pwd () diff --git a/lem.asd b/lem.asd index 868539ad2..816080cd3 100644 --- a/lem.asd +++ b/lem.asd @@ -195,7 +195,15 @@ (:file "link") (:file "thingatp") (:file "gtags") - (:file "directory-mode") + (:module "directory-mode" + :serial t + :components ((:file "file") + (:file "attributes") + (:file "mode") + (:file "internal") + (:file "commands") + (:file "keybinds") + (:file "main"))) (:file "abbrev") (:file "rectangle") (:file "auto-save") diff --git a/src/ext/directory-mode.lisp b/src/ext/directory-mode.lisp deleted file mode 100644 index 4d6efc433..000000000 --- a/src/ext/directory-mode.lisp +++ /dev/null @@ -1,693 +0,0 @@ -(defpackage :lem/directory-mode - (:use :cl :lem) - #+sbcl - (:lock t) - (:export - :*default-sort-method*)) -(in-package :lem/directory-mode) - -(deftype sort-method () - '(member :pathname :mtime :size)) - -(declaim (type (sort-method) *default-sort-method*)) -(defvar *default-sort-method* :pathname - "Default method to sort files when opening a directory. - - A keyword, one of :pathname (sort by file name), :mtime (last modification time) and :size.") - -(define-attribute current-directory-attribute - (t :bold t)) - -(define-attribute file-size-attribute - (t)) - -(define-attribute file-date-attribute - (t)) - -(define-attribute file-attribute - (t)) - -(define-attribute directory-attribute - (t :foreground :base0D :bold t)) - -(define-attribute link-attribute - (t :foreground :base0B :bold t)) - -(define-major-mode directory-mode () - (:name "Directory" - :keymap *directory-mode-keymap*) - (setf (variable-value 'highlight-line :buffer (current-buffer)) nil)) - -(define-key *global-keymap* "C-x C-j" 'find-file-directory) - -(define-key *directory-mode-keymap* "q" 'quit-active-window) -(define-key *directory-mode-keymap* "M-q" 'quit-active-window) -(define-key *directory-mode-keymap* "g" 'directory-mode-update-buffer) -(define-key *directory-mode-keymap* "^" 'directory-mode-up-directory) -(define-key *directory-mode-keymap* "Return" 'directory-mode-find-file) -(define-key *directory-mode-keymap* "Space" 'directory-mode-read-file) -(define-key *directory-mode-keymap* "o" 'directory-mode-find-file-next-window) -(define-key *directory-mode-keymap* "n" 'directory-mode-next-line) -(define-key *directory-mode-keymap* "p" 'directory-mode-previous-line) -(define-key *directory-mode-keymap* "M-}" 'directory-mode-next-mark) -(define-key *directory-mode-keymap* "M-{" 'directory-mode-previous-mark) -(define-key *directory-mode-keymap* "m" 'directory-mode-mark-and-next-line) -(define-key *directory-mode-keymap* "u" 'directory-mode-unmark-and-next-line) -(define-key *directory-mode-keymap* "U" 'directory-mode-unmark-and-previous-line) -(define-key *directory-mode-keymap* "t" 'directory-mode-toggle-marks) -(define-key *directory-mode-keymap* "* !" 'directory-mode-unmark-all) -(define-key *directory-mode-keymap* "* %" 'directory-mode-mark-regexp) -(define-key *directory-mode-keymap* "* /" 'directory-mode-mark-directories) -(define-key *directory-mode-keymap* "* @" 'directory-mode-mark-links) -(define-key *directory-mode-keymap* "* C-n" 'directory-mode-next-mark) -(define-key *directory-mode-keymap* "* C-p" 'directory-mode-previous-mark) -(define-key *directory-mode-keymap* "Q" 'directory-mode-query-replace) -(define-key *directory-mode-keymap* "D" 'directory-mode-delete-files) -(define-key *directory-mode-keymap* "C" 'directory-mode-copy-files) -(define-key *directory-mode-keymap* "R" 'directory-mode-rename-files) -(define-key *directory-mode-keymap* "r" 'directory-mode-rename-file) -(define-key *directory-mode-keymap* "s" 'directory-mode-sort-files) -(define-key *directory-mode-keymap* "+" 'make-directory) -(define-key *directory-mode-keymap* "C-k" 'directory-mode-kill-lines) - -(defun run-command (command) - (when (consp command) - (setf command (mapcar #'princ-to-string command))) - (let ((error-string - (with-output-to-string (error-output) - (uiop:run-program command - :ignore-error-status t - :error-output error-output)))) - (when (string/= error-string "") - (editor-error "~A" error-string)))) - -(defun remove-line-overlay-when-buffer-change (point arg) - (declare (ignore arg)) - (alexandria:when-let (ov (buffer-value (point-buffer point) 'line-overlay)) - (setf (buffer-value (point-buffer point) 'line-overlay) nil) - (delete-overlay ov))) - -(defun update-line (point) - (with-point ((start point) - (end point)) - (back-to-indentation start) - (line-end end) - (let ((ov (buffer-value point 'line-overlay))) - (cond (ov - (move-point (overlay-start ov) start) - (move-point (overlay-end ov) end)) - (t - (add-hook (variable-value 'before-change-functions :buffer (point-buffer point)) - 'remove-line-overlay-when-buffer-change) - (setf ov (make-overlay start end 'region)) - (setf (buffer-value point 'line-overlay) ov)))))) - -(defun move-to-start-line (point) - (move-to-line point 3)) - -(defun get-line-property (p key) - (with-point ((p p)) - (text-property-at (line-start p) key))) - -(defun get-pathname (point) - (get-line-property point 'pathname)) - -(defun get-name (point) - (get-line-property point 'name)) - -(defun get-mark (p) - (with-point ((p p)) - (eql #\* (character-at (line-start p) 1)))) - -(defun set-mark (p mark) - (with-buffer-read-only (point-buffer p) nil - (let ((*inhibit-read-only* t)) - (with-point ((p p)) - (let ((pathname (get-line-property p 'pathname))) - (when (and pathname (not (uiop:pathname-equal - pathname - (uiop:pathname-parent-directory-pathname - (buffer-directory (point-buffer p)))))) - (character-offset (line-start p) 1) - (delete-character p 1) - (insert-character p (if mark #\* #\space)))))))) - -(defun iter-marks (p function) - (with-point ((p p)) - (move-to-start-line p) - (loop - (funcall function p) - (unless (line-offset p 1) (return))))) - - -(defun marked-lines (p) - (with-point ((p p)) - (let ((points '())) - (iter-marks p - (lambda (p) - (when (get-mark p) - (push (copy-point p :temporary) points)))) - (nreverse points)))) - -(defun marked-files (p) - (mapcar 'get-pathname (marked-lines p))) - -(defun filter-marks (p function) - (iter-marks p - (lambda (p) - (set-mark p (funcall function p))))) - -(defun current-file (p) - (alexandria:when-let (pathname (get-pathname p)) - pathname)) - -(defun selected-files (p) - (or (marked-files p) - (uiop:ensure-list (current-file p)))) - -(defun process-current-line-pathname (function) - (alexandria:when-let (pathname (get-pathname (current-point))) - (funcall function pathname))) - -(defun symbolic-link-p (pathname) - (not (uiop:pathname-equal pathname (probe-file pathname)))) - -(defun get-file-attribute (pathname) - (cond ((symbolic-link-p pathname) - 'link-attribute) - ((uiop:directory-pathname-p pathname) - 'directory-attribute) - (t - 'file-attribute))) - -(defun human-readable-file-size (size) - (loop :for sign :in '(#\Y #\Z #\E #\P #\T #\G #\M #\k) - :for val := (expt 1024 8) :then (/ val 1024) - :do (when (>= size val) - (return (format nil "~D~C" (1+ (floor size val)) sign))) - :finally (return (princ-to-string size)))) - -(defun insert-icon (point pathname) - (cond ((uiop:directory-pathname-p pathname) - (insert-string point (icon-string "folder"))) - ((let ((string (icon-string-by-ext (pathname-type pathname)))) - (when string - (insert-string point string) - t))) - (t - (let ((string (icon-string-by-ext "txt"))) - (when string - (insert-string point string) - t))))) - -(defun insert-pathname (point pathname directory &optional content) - (let ((file-size (handler-case (file-size pathname) - (error () - (return-from insert-pathname))))) - (with-point ((start point)) - (let ((name (or content (namestring (enough-namestring pathname directory))))) - (insert-string point " " 'pathname pathname 'name name) - (insert-string point - (format nil " ~5@A " - (if file-size (human-readable-file-size file-size) "")) - :attribute 'file-size-attribute) - (multiple-value-bind (second minute hour day month year week) - (let ((date (file-write-date pathname))) - (if date - (decode-universal-time date) - (values 0 0 0 0 0 0 nil))) - (insert-string point - (format nil "~4,'0D/~2,'0D/~2,'0D ~2,'0D:~2,'0D:~2,'0D ~A " - year month day hour minute second - (if week (aref #("Mon" "Tue" "Wed" "Thr" "Fri" "Sat" "Sun") week) - " ")) - :attribute 'file-date-attribute)) - (unless (string= name "..") - (insert-icon point name)) - (insert-string point - name - :attribute (get-file-attribute pathname) - :file pathname) - (when (symbolic-link-p pathname) - (insert-string point (format nil " -> ~A" (probe-file pathname)))) - (back-to-indentation start) - (lem/button:apply-button-between-points - start point - (lambda () - (lem/button:with-context () - (directory-mode-find-file)))) - (insert-character point #\newline) - (put-text-property start point :read-only t))))) - -(defun insert-directories-and-files (point - directory - &key (sort-method *default-sort-method*) - (without-parent-directory t)) - (unless without-parent-directory - (alexandria:when-let (pathname (probe-file (merge-pathnames "../" directory))) - (insert-pathname point pathname directory ".."))) - (dolist (pathname (list-directory directory :sort-method sort-method)) - (insert-pathname point pathname directory))) - -(defun update (buffer &key (sort-method *default-sort-method*)) - "Update this directory buffer content." - (with-buffer-read-only buffer nil - (let ((*inhibit-read-only* t)) - (let* ((directory (buffer-directory buffer)) - (p (buffer-point buffer)) - (line-number (line-number-at-point p))) - (erase-buffer buffer) - (buffer-start p) - (insert-string p (format nil "~A~2%" directory) :attribute 'current-directory-attribute) - (insert-directories-and-files p directory - :sort-method sort-method - :without-parent-directory nil) - (move-to-line p line-number))))) - -(defun update-all () - (dolist (buffer (buffer-list)) - (when (eq 'directory-mode (buffer-major-mode buffer)) - (update buffer)))) - -(defun create-directory-buffer (name filename) - (let ((buffer (make-buffer name :enable-undo-p nil :read-only-p t))) - (change-buffer-mode buffer 'directory-mode) - (setf (buffer-directory buffer) filename) - (update buffer) - (move-to-start-line (buffer-point buffer)) - buffer)) - -(defun directory-buffer (filename) - (setf filename (uiop:directory-exists-p - (expand-file-name (namestring filename) - (buffer-directory)))) - (let* ((name (pathname-directory-last-name filename)) - (buffer (get-buffer name))) - (cond ((and buffer (not (uiop:pathname-equal filename (buffer-directory buffer)))) - (create-directory-buffer (unique-buffer-name name) filename)) - ((null buffer) - (create-directory-buffer name filename)) - (t - buffer)))) - -(defun delete-file* (file) - #+windows - (if (uiop:directory-pathname-p file) - (sb-ext:delete-directory file :recursive t) - (delete-file file)) - #-windows - (if (and (not (string= (namestring file) - (namestring (uiop:resolve-symlinks file))))) - (and (prompt-for-y-or-n-p - (format nil "It appears that ~a is a symlink, delete it?" file)) - (run-command `("unlink" ,(string-right-trim - (string - (uiop:directory-separator-for-host)) - (namestring file))))) - (run-command `("rm" "-fr" ,file)))) - -(defun subdirectory-p (to-pathname from-pathname) - (let ((to-dir (pathname-directory to-pathname)) - (from-dir (pathname-directory from-pathname))) - (assert (eq :absolute (car to-dir))) - (assert (eq :absolute (car from-dir))) - (and (<= (length from-dir) - (length to-dir)) - (loop - :for from-elt :in (cdr from-dir) - :for to-elt :in (cdr to-dir) - :when (not (equal from-elt to-elt)) - :do (return nil) - :finally (return t))))) - -(defun pathname-directory-last-name (pathname) - (enough-namestring pathname (uiop:pathname-parent-directory-pathname pathname))) - -(defvar *rename-p* nil) - -(defun copy-directory (src dst) - (setf dst (uiop:ensure-directory-pathname dst)) - (let ((dst (if (probe-file dst) - (merge-pathnames (pathname-directory-last-name src) - dst) - dst))) - (when (subdirectory-p dst src) - (editor-error "Cannot copy `~A' into its subdirectory `~A'" src dst)) - ;(format t "mkdir ~A~%" dst) - (let ((dst (ensure-directories-exist dst))) - (dolist (file (list-directory src)) - (copy-file file dst))) - (when *rename-p* (uiop:delete-empty-directory src)))) - -(defun copy-file (src dst) - (if (uiop:directory-pathname-p src) - (copy-directory src dst) - (let ((probe-dst (probe-file dst))) - (cond ((and probe-dst (uiop:directory-pathname-p probe-dst)) - (copy-file src (merge-pathnames (file-namestring src) probe-dst))) - (t - ;(format t "copy ~A -> ~A~%" src dst) - (if *rename-p* - (rename-file src dst) - (uiop:copy-file src dst))))))) - -(defun copy-file* (src dst) - #+windows - (copy-file src dst) - #-windows - (run-command `("cp" "-R" ,src ,dst))) - -(defun rename-file* (src dst) - #+windows - (let ((*rename-p* t)) - (copy-file src dst)) - #-windows - (run-command `("mv" ,src ,dst))) - -(defun copy-or-rename-file (src dst) - #+windows - (copy-file src dst) - #-windows - (if *rename-p* - (run-command `("mv" ,src ,dst)) - (run-command `("cp" "-R" ,src ,dst)))) - -(defun check-copy-files (src-files dst) - (let ((n (length src-files))) - (cond ((or (and (< 1 n) (uiop:file-exists-p dst)) - (and (= 1 n) (uiop:directory-pathname-p (first src-files)) - (uiop:file-exists-p dst))) - (editor-error "Target must be a directory")) - ((and (< 1 n) (not (uiop:directory-exists-p dst))) - (editor-error "No such file or directory"))))) - -(defun copy-files (src-files dst-file) - (check-copy-files src-files dst-file) - (dolist (file src-files) - (copy-or-rename-file file dst-file))) - -(defun rename-files (src-files dst-file) - (let ((*rename-p* t)) - (dolist (file src-files) - (copy-or-rename-file file dst-file)))) - -(define-command directory-mode-update-buffer () () - (update (current-buffer))) - -(define-command directory-mode-up-directory () () - (let ((dir (buffer-directory))) - (switch-to-buffer - (directory-buffer (uiop:pathname-parent-directory-pathname (buffer-directory)))) - (search-filename-and-recenter - (concatenate - 'string - (car - (reverse - (split-sequence:split-sequence - (uiop:directory-separator-for-host) - dir - :remove-empty-subseqs t))) - (string (uiop:directory-separator-for-host)))))) - -(define-command directory-mode-find-file () () - (process-current-line-pathname 'find-file)) - -(define-command directory-mode-read-file () () - (process-current-line-pathname 'read-file)) - -(define-command directory-mode-find-file-next-window () () - (process-current-line-pathname (lambda (pathname) - (let ((buffer (execute-find-file *find-file-executor* - (get-file-mode pathname) - pathname))) - (switch-to-window - (pop-to-buffer buffer)))))) - -(define-command directory-mode-next-line (p) (:universal) - (line-offset (current-point) p)) - -(define-command directory-mode-previous-line (p) (:universal) - (line-offset (current-point) (- p))) - -(define-command directory-mode-mark-and-next-line () () - (set-mark (current-point) t) - (directory-mode-next-line 1)) - -(define-command directory-mode-unmark-and-next-line () () - (set-mark (current-point) nil) - (directory-mode-next-line 1)) - -(define-command directory-mode-unmark-and-previous-line () () - (directory-mode-previous-line 1) - (set-mark (current-point) nil)) - -(define-command directory-mode-toggle-marks () () - (filter-marks (current-point) - (lambda (p) (not (get-mark p))))) - -(define-command directory-mode-unmark-all () () - (filter-marks (current-point) (constantly nil))) - -(define-command directory-mode-mark-regexp (regex &optional arg) ((:string "Regex: ") :universal-nil) - "Mark all files matching the regular expression REGEX. -With prefix argument ARG, unmark all those files." - (let ((scanner (ppcre:create-scanner regex))) - (filter-marks (current-point) - (lambda (p) - (if (ppcre:scan scanner (get-name p)) - (not arg) - (get-mark p)))))) - -(define-command directory-mode-mark-directories (&optional arg) (:universal-nil) - "Mark all directories in the current buffer except '..'. -With prefix argument ARG, unmark all those directories." - (filter-marks (current-point) - (lambda (p) - (line-start p) - (move-to-file-position p) - (if (eq 'directory-attribute (text-property-at p :attribute)) - (not arg) - (get-mark p))))) - -(define-command directory-mode-mark-links (&optional arg) (:universal-nil) - "Mark all symbolic links in the current buffer. -With prefix argument ARG, unmark all those links." - (filter-marks (current-point) - (lambda (p) - (line-start p) - (move-to-file-position p) - (if (eq 'link-attribute (text-property-at p :attribute)) - (not arg) - (get-mark p))))) - -(define-command directory-mode-mark-suffix (suffix &optional arg) ((:string "Suffix: ") :universal-nil) - "Mark all files with the given SUFFIX. -The provided SUFFIX is a string, and not a file extenion, meaning every file with -a name ending in SUFFIX will be marked. -With prefix argument ARG, unmark all those files." - (filter-marks (current-point) - (lambda (p) - (let ((name (get-name p))) - ;; Use < so exact matches are not marked - (if (and (< (length suffix) (length name)) - (string= name suffix :start1 (- (length name) (length suffix)))) - (not arg) - (get-mark p)))))) - -(define-command directory-mode-mark-extension (extension &optional arg) ((:string "Extension: ") :universal-nil) - "Mark all files with the given EXTENSION. -A '.' is prepended to the EXTENSION if not present. -With prefix argument ARG, unmark all those files." - ;; Support empty extension, which will mark all files ending with a '.'. - (when (or (= 0 (length extension)) - (char/= (aref extension 0) #\.)) - (setf extension (concatenate 'string "." extension))) - (directory-mode-mark-suffix extension arg)) - -(define-command directory-mode-next-mark (n) (:universal) - "Move to the next Nth marked entry." - (cond ((= 0 n) - nil) - ((< n 0) - (directory-mode-previous-mark (- n))) - (t (let* ((all-marks (delete-if (lambda (p) - (point< p (current-point))) - (marked-lines (current-point)))) - (result (nth (- n 1) all-marks))) - (if result - (progn - (move-point (current-point) result) - (move-to-file-position (current-point))) - (editor-error "No next mark")))))) - -(define-command directory-mode-previous-mark (n) (:universal) - "Move to the previous Nth marked entry." - (cond ((= 0 n) nil) - ((< n 0) (directory-mode-next-mark (- n))) - (t (with-point ((point (current-point))) - (line-start point) - (let* ((all-marks (delete-if (lambda (p) - (point>= p point)) - (marked-lines point))) - (result (last all-marks n))) - (if (and result - (= n (length result))) - (progn - (move-point (current-point) (car result)) - (move-to-file-position (current-point))) - (editor-error "No previous mark"))))))) - -(defun query-replace-marked-files (query-function) - (destructuring-bind (before after) - (lem/isearch:read-query-replace-args) - (dolist (file (marked-files (current-point))) - (find-file file) - (buffer-start (current-point)) - (funcall query-function before after)))) - -(define-command directory-mode-query-replace () () - (query-replace-marked-files 'lem/isearch:query-replace)) - -(define-command directory-mode-query-replace-regexp () () - (query-replace-marked-files 'lem/isearch:query-replace-regexp)) - -(define-command directory-mode-query-replace-symbol () () - (query-replace-marked-files 'lem/isearch:query-replace-symbol)) - -(define-command directory-mode-delete-files () () - (let ((files (selected-files (current-point)))) - (when (prompt-for-y-or-n-p (format nil "Really delete files~%~{- ~A~%~}" files)) - (dolist (file files) - (delete-file* file)) - (update-all)))) - -(defun get-dest-directory () - (dolist (window (window-list) (buffer-directory)) - (when (and (not (eq window (current-window))) - (eq 'directory-mode (buffer-major-mode (window-buffer window)))) - (return (buffer-directory (window-buffer window)))))) - -(define-command directory-mode-copy-files () () - (let ((dst-file (prompt-for-file "Destination Filename: " :directory (get-dest-directory))) - (files (selected-files (current-point)))) - (copy-files files dst-file)) - (update-all)) - -(define-command directory-mode-rename-files () () - (let ((dst-file (prompt-for-file "Destination Filename: " :directory (get-dest-directory)))) - (rename-files (selected-files (current-point)) dst-file)) - (update-all)) - -(defun move-to-file-position (point) - (with-point ((limit point)) - (line-end limit) - (next-single-property-change point :file limit))) - -(defun replace-file-name (point string) - (when (alexandria:emptyp string) (setf string " ")) - (line-start point) - (move-to-file-position point) - (character-at point 1) - (let ((file (text-property-at point :file)) - (*inhibit-read-only* t)) - (with-point ((end point)) - (line-end end) - (delete-between-points point end) - (insert-string point string :file file)))) - -(defun prompt-for-rename-file (point) - (let ((file (current-file point))) - (save-excursion - (move-point (current-point) point) - (prompt-for-string - "" - :initial-value (if file (file-namestring file) "") - :test-function (lambda (string) - (not (alexandria:emptyp string))) - :gravity :cursor - :use-border nil)))) - -(define-command directory-mode-rename-file () () - (with-point ((point (current-point) :right-inserting)) - (move-to-file-position point) - (alexandria:when-let (source-file (text-property-at point :file)) - (replace-file-name point "") - (unwind-protect - (let* ((new-file (merge-pathnames (prompt-for-rename-file point) - (buffer-directory (current-buffer))))) - (when (probe-file new-file) - (editor-error "The filename already exists.")) - (rename-file* source-file new-file)) - (directory-mode-update-buffer))))) - -(defun search-filename-and-recenter (filename) - "Search `filename` in this files listing, recenter the window on it" - (move-to-beginning-of-buffer) - (search-forward (current-point) filename) - (window-recenter (current-window)) - (character-offset (current-point) (* -1 (length filename)))) - -(define-command directory-mode-sort-files () () - "Sort files: by name, by last modification time, then by size. - - Each new directory buffer first uses the default sort method (`lem/directory-mode:*default-sort-method*')" - (let ((path (get-pathname (current-point)))) - (cond - ;; mtime -> size - ((eql (buffer-value (current-buffer) :sort-method) :mtime) - (message "Sorting by size") - (setf (buffer-value (current-buffer) :sort-method) :size) - (update (current-buffer) :sort-method :size)) - ;; size -> pathname - ((eql (buffer-value (current-buffer) :sort-method) :size) - (message "Sorting by name") - (setf (buffer-value (current-buffer) :sort-method) :pathname) - (update (current-buffer) :sort-method :pathname)) - (t - ;; At first call, the buffer's sort-method is not set. - (message "Sorting by last modification time") - (setf (buffer-value (current-buffer) :sort-method) :mtime) - (update (current-buffer) :sort-method :mtime))) - - ;; Follow file name. - (when (and path (str:non-blank-string-p (file-namestring path))) - (search-filename-and-recenter (file-namestring path))))) - -(define-command make-directory (filename) ((:new-file "Make directory: ")) - (setf filename (uiop:ensure-directory-pathname filename)) - (ensure-directories-exist filename) - (update-all)) - -(define-command find-file-directory () () - "Open this file's directory and place point on the filename." - (let ((fullpath (buffer-filename))) - (cond - ((mode-active-p (current-buffer) 'directory-mode) - (directory-mode-up-directory)) - ((null fullpath) - (message "No file at point")) - (t - (switch-to-buffer - (find-file-buffer (lem-core/commands/file::directory-for-file-or-lose (buffer-directory)))) - (let ((filename (file-namestring fullpath))) - (search-filename-and-recenter (file-namestring filename))))))) - -(define-command directory-mode-kill-lines () () - "Delete the marked lines from the directory-mode buffer. -This does not delete the marked entries, but only remove them from the buffer." - (with-buffer-read-only (current-buffer) nil - (let ((*inhibit-read-only* t) - (marked-lines (marked-lines (current-point)))) - (save-excursion - ;; Reverse the lines so killing is done from the end of the buffer - (loop :for marked-line :in (nreverse marked-lines) - :do (move-point (current-point) marked-line) - (kill-whole-line)))))) - -(setf *find-directory-function* 'directory-buffer) - -(defmethod execute :after ((mode directory-mode) command argument) - (when (mode-active-p (current-buffer) 'directory-mode) - (update-line (current-point)))) - - diff --git a/src/ext/directory-mode/attributes.lisp b/src/ext/directory-mode/attributes.lisp new file mode 100644 index 000000000..c69511bc2 --- /dev/null +++ b/src/ext/directory-mode/attributes.lisp @@ -0,0 +1,30 @@ +(uiop:define-package :lem/directory-mode/attributes + (:use :cl :lem) + (:export :current-directory-attribute + :file-size-attribute + :file-date-attribute + :file-attribute + :directory-attribute + :link-attribute)) +(in-package :lem/directory-mode/attributes) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (sb-ext:lock-package :lem/directory-mode/attributes)) + +(define-attribute current-directory-attribute + (t :bold t)) + +(define-attribute file-size-attribute + (t)) + +(define-attribute file-date-attribute + (t)) + +(define-attribute file-attribute + (t)) + +(define-attribute directory-attribute + (t :foreground :base0D :bold t)) + +(define-attribute link-attribute + (t :foreground :base0B :bold t)) diff --git a/src/ext/directory-mode/commands.lisp b/src/ext/directory-mode/commands.lisp new file mode 100644 index 000000000..94d77053e --- /dev/null +++ b/src/ext/directory-mode/commands.lisp @@ -0,0 +1,338 @@ +(uiop:define-package :lem/directory-mode/commands + (:use :cl + :lem + :lem/directory-mode/internal + :lem/directory-mode/attributes) + (:import-from :lem/directory-mode/mode + :directory-mode) + (:import-from :lem/directory-mode/file + :delete-file* + :rename-file* + :copy-files + :rename-files) + (:export :directory-mode-update-buffer + :directory-mode-up-directory + :directory-mode-find-file + :directory-mode-read-file + :directory-mode-find-file-next-window + :directory-mode-next-line + :directory-mode-previous-line + :directory-mode-mark-and-next-line + :directory-mode-unmark-and-next-line + :directory-mode-unmark-and-previous-line + :directory-mode-toggle-marks + :directory-mode-unmark-all + :directory-mode-mark-regexp + :directory-mode-mark-directories + :directory-mode-mark-links + :directory-mode-mark-suffix + :directory-mode-mark-extension + :directory-mode-next-mark + :directory-mode-previous-mark + :directory-mode-query-replace + :directory-mode-query-replace-regexp + :directory-mode-query-replace-symbol + :directory-mode-delete-files + :directory-mode-copy-files + :directory-mode-rename-files + :directory-mode-rename-file + :directory-mode-sort-files + :make-directory + :find-file-directory + :directory-mode-kill-lines)) +(in-package :lem/directory-mode/commands) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (sb-ext:lock-package :lem/directory-mode/commands)) + +(defun search-filename-and-recenter (filename) + "Search `filename` in this files listing, recenter the window on it" + (move-to-beginning-of-buffer) + (search-forward (current-point) filename) + (window-recenter (current-window)) + (character-offset (current-point) (* -1 (length filename)))) + +(define-command directory-mode-update-buffer () () + (update-buffer (current-buffer))) + +(define-command directory-mode-up-directory () () + (let ((dir (buffer-directory))) + (switch-to-buffer + (directory-buffer (uiop:pathname-parent-directory-pathname (buffer-directory)))) + (search-filename-and-recenter + (concatenate + 'string + (car + (reverse + (split-sequence:split-sequence + (uiop:directory-separator-for-host) + dir + :remove-empty-subseqs t))) + (string (uiop:directory-separator-for-host)))))) + +(define-command directory-mode-find-file () () + (open-selected-file :read-only nil :next-window nil)) + +(define-command directory-mode-read-file () () + (open-selected-file :read-only t :next-window nil)) + +(define-command directory-mode-find-file-next-window () () + (open-selected-file :read-only nil :next-window t)) + +(define-command directory-mode-next-line (p) (:universal) + (line-offset (current-point) p)) + +(define-command directory-mode-previous-line (p) (:universal) + (line-offset (current-point) (- p))) + +(define-command directory-mode-mark-and-next-line () () + (set-mark (current-point) t) + (directory-mode-next-line 1)) + +(define-command directory-mode-unmark-and-next-line () () + (set-mark (current-point) nil) + (directory-mode-next-line 1)) + +(define-command directory-mode-unmark-and-previous-line () () + (directory-mode-previous-line 1) + (set-mark (current-point) nil)) + +(define-command directory-mode-toggle-marks () () + (filter-marks (current-point) + (lambda (p) (not (get-mark p))))) + +(define-command directory-mode-unmark-all () () + (filter-marks (current-point) (constantly nil))) + +(define-command directory-mode-mark-regexp (regex &optional arg) ((:string "Regex: ") :universal-nil) + "Mark all files matching the regular expression REGEX. +With prefix argument ARG, unmark all those files." + (let ((scanner (ppcre:create-scanner regex))) + (filter-marks (current-point) + (lambda (p) + (if (ppcre:scan scanner (get-name p)) + (not arg) + (get-mark p)))))) + +(define-command directory-mode-mark-directories (&optional arg) (:universal-nil) + "Mark all directories in the current buffer except '..'. +With prefix argument ARG, unmark all those directories." + (filter-marks (current-point) + (lambda (p) + (line-start p) + (move-to-file-position p) + (if (eq 'directory-attribute (text-property-at p :attribute)) + (not arg) + (get-mark p))))) + +(define-command directory-mode-mark-links (&optional arg) (:universal-nil) + "Mark all symbolic links in the current buffer. +With prefix argument ARG, unmark all those links." + (filter-marks (current-point) + (lambda (p) + (line-start p) + (move-to-file-position p) + (if (eq 'link-attribute (text-property-at p :attribute)) + (not arg) + (get-mark p))))) + +(define-command directory-mode-mark-suffix (suffix &optional arg) ((:string "Suffix: ") :universal-nil) + "Mark all files with the given SUFFIX. +The provided SUFFIX is a string, and not a file extenion, meaning every file with +a name ending in SUFFIX will be marked. +With prefix argument ARG, unmark all those files." + (filter-marks (current-point) + (lambda (p) + (let ((name (get-name p))) + ;; Use < so exact matches are not marked + (if (and (< (length suffix) (length name)) + (string= name suffix :start1 (- (length name) (length suffix)))) + (not arg) + (get-mark p)))))) + +(define-command directory-mode-mark-extension (extension &optional arg) ((:string "Extension: ") :universal-nil) + "Mark all files with the given EXTENSION. +A '.' is prepended to the EXTENSION if not present. +With prefix argument ARG, unmark all those files." + ;; Support empty extension, which will mark all files ending with a '.'. + (when (or (= 0 (length extension)) + (char/= (aref extension 0) #\.)) + (setf extension (concatenate 'string "." extension))) + (directory-mode-mark-suffix extension arg)) + +(define-command directory-mode-next-mark (n) (:universal) + "Move to the next Nth marked entry." + (cond ((= 0 n) + nil) + ((< n 0) + (directory-mode-previous-mark (- n))) + (t (let* ((all-marks (delete-if (lambda (p) + (point< p (current-point))) + (marked-lines (current-point)))) + (result (nth (- n 1) all-marks))) + (if result + (progn + (move-point (current-point) result) + (move-to-file-position (current-point))) + (editor-error "No next mark")))))) + +(define-command directory-mode-previous-mark (n) (:universal) + "Move to the previous Nth marked entry." + (cond ((= 0 n) nil) + ((< n 0) (directory-mode-next-mark (- n))) + (t (with-point ((point (current-point))) + (line-start point) + (let* ((all-marks (delete-if (lambda (p) + (point>= p point)) + (marked-lines point))) + (result (last all-marks n))) + (if (and result + (= n (length result))) + (progn + (move-point (current-point) (car result)) + (move-to-file-position (current-point))) + (editor-error "No previous mark"))))))) + +(defun query-replace-marked-files (query-function) + (destructuring-bind (before after) + (lem/isearch:read-query-replace-args) + (dolist (file (marked-files (current-point))) + (find-file file) + (buffer-start (current-point)) + (funcall query-function before after)))) + +(define-command directory-mode-query-replace () () + (query-replace-marked-files 'lem/isearch:query-replace)) + +(define-command directory-mode-query-replace-regexp () () + (query-replace-marked-files 'lem/isearch:query-replace-regexp)) + +(define-command directory-mode-query-replace-symbol () () + (query-replace-marked-files 'lem/isearch:query-replace-symbol)) + +(define-command directory-mode-delete-files () () + (let ((files (selected-files (current-point)))) + (when (prompt-for-y-or-n-p (format nil "Really delete files~%~{- ~A~%~}" files)) + (dolist (file files) + (delete-file* file)) + (update-all-buffers)))) + +(defun get-dest-directory () + (dolist (window (window-list) (buffer-directory)) + (when (and (not (eq window (current-window))) + (eq 'directory-mode (buffer-major-mode (window-buffer window)))) + (return (buffer-directory (window-buffer window)))))) + +(define-command directory-mode-copy-files () () + (let ((dst-file (prompt-for-file "Destination Filename: " :directory (get-dest-directory))) + (files (selected-files (current-point)))) + (copy-files files dst-file)) + (update-all-buffers)) + +(define-command directory-mode-rename-files () () + (let ((dst-file (prompt-for-file "Destination Filename: " :directory (get-dest-directory)))) + (rename-files (selected-files (current-point)) dst-file)) + (update-all-buffers)) + +(defun move-to-file-position (point) + (with-point ((limit point)) + (line-end limit) + (next-single-property-change point :file limit))) + +(defun replace-file-name (point string) + (when (alexandria:emptyp string) (setf string " ")) + (line-start point) + (move-to-file-position point) + (character-at point 1) + (let ((file (text-property-at point :file)) + (*inhibit-read-only* t)) + (with-point ((end point)) + (line-end end) + (delete-between-points point end) + (insert-string point string :file file)))) + +(defun prompt-for-rename-file (point) + (let ((file (current-file point))) + (save-excursion + (move-point (current-point) point) + (prompt-for-string + "" + :initial-value (if file (file-namestring file) "") + :test-function (lambda (string) + (not (alexandria:emptyp string))) + :gravity :cursor + :use-border nil)))) + +(define-command directory-mode-rename-file () () + (with-point ((point (current-point) :right-inserting)) + (move-to-file-position point) + (alexandria:when-let (source-file (text-property-at point :file)) + (replace-file-name point "") + (unwind-protect + (let* ((new-file (merge-pathnames (prompt-for-rename-file point) + (buffer-directory (current-buffer))))) + (when (probe-file new-file) + (editor-error "The filename already exists.")) + (rename-file* source-file new-file)) + (directory-mode-update-buffer))))) + +(define-command directory-mode-sort-files () () + "Sort files: by name, by last modification time, then by size. + + Each new directory buffer first uses the default sort method (`lem/directory-mode:*default-sort-method*')" + (let ((path (get-pathname (current-point)))) + (cond + ;; mtime -> size + ((eql (buffer-value (current-buffer) :sort-method) :mtime) + (message "Sorting by size") + (setf (buffer-value (current-buffer) :sort-method) :size) + (update-buffer (current-buffer) :sort-method :size)) + ;; size -> pathname + ((eql (buffer-value (current-buffer) :sort-method) :size) + (message "Sorting by name") + (setf (buffer-value (current-buffer) :sort-method) :pathname) + (update-buffer (current-buffer) :sort-method :pathname)) + (t + ;; At first call, the buffer's sort-method is not set. + (message "Sorting by last modification time") + (setf (buffer-value (current-buffer) :sort-method) :mtime) + (update-buffer (current-buffer) :sort-method :mtime))) + + ;; Follow file name. + (when (and path (str:non-blank-string-p (file-namestring path))) + (search-filename-and-recenter (file-namestring path))))) + +(define-command make-directory (filename) ((:new-file "Make directory: ")) + (setf filename (uiop:ensure-directory-pathname filename)) + (ensure-directories-exist filename) + (update-all-buffers)) + +(define-command find-file-directory () () + "Open this file's directory and place point on the filename." + (let ((fullpath (buffer-filename))) + (cond + ((mode-active-p (current-buffer) 'directory-mode) + (directory-mode-up-directory)) + ((null fullpath) + (message "No file at point")) + (t + (switch-to-buffer + (find-file-buffer (lem-core/commands/file::directory-for-file-or-lose (buffer-directory)))) + (let ((filename (file-namestring fullpath))) + (search-filename-and-recenter (file-namestring filename))))))) + +(define-command directory-mode-kill-lines () () + "Delete the marked lines from the directory-mode buffer. +This does not delete the marked entries, but only remove them from the buffer." + (with-buffer-read-only (current-buffer) nil + (let ((*inhibit-read-only* t) + (marked-lines (marked-lines (current-point)))) + (save-excursion + ;; Reverse the lines so killing is done from the end of the buffer + (loop :for marked-line :in (nreverse marked-lines) + :do (move-point (current-point) marked-line) + (kill-whole-line)))))) + +(defmethod execute :after ((mode directory-mode) command argument) + (when (mode-active-p (current-buffer) 'directory-mode) + (update-highlight-line-overlay (current-point)))) diff --git a/src/ext/directory-mode/file.lisp b/src/ext/directory-mode/file.lisp new file mode 100644 index 000000000..b82ed7c65 --- /dev/null +++ b/src/ext/directory-mode/file.lisp @@ -0,0 +1,130 @@ +(uiop:define-package :lem/directory-mode/file + (:use :cl + :lem) + (:export + :delete-file* + :rename-file* + :copy-files + :rename-files + :symbolic-link-p + :pathname-directory-last-name)) +(in-package :lem/directory-mode/file) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (sb-ext:lock-package :lem/directory-mode/file)) + +(defvar *rename-p* nil) + +(defun delete-file* (file) + #+windows + (if (uiop:directory-pathname-p file) + (sb-ext:delete-directory file :recursive t) + (delete-file file)) + #-windows + (if (and (not (string= (namestring file) + (namestring (uiop:resolve-symlinks file))))) + (and (prompt-for-y-or-n-p + (format nil "It appears that ~a is a symlink, delete it?" file)) + (run-command `("unlink" ,(string-right-trim + (string + (uiop:directory-separator-for-host)) + (namestring file))))) + (run-command `("rm" "-fr" ,file)))) + +(defun subdirectory-p (to-pathname from-pathname) + (let ((to-dir (pathname-directory to-pathname)) + (from-dir (pathname-directory from-pathname))) + (assert (eq :absolute (car to-dir))) + (assert (eq :absolute (car from-dir))) + (and (<= (length from-dir) + (length to-dir)) + (loop + :for from-elt :in (cdr from-dir) + :for to-elt :in (cdr to-dir) + :when (not (equal from-elt to-elt)) + :do (return nil) + :finally (return t))))) + +(defun copy-directory (src dst) + (setf dst (uiop:ensure-directory-pathname dst)) + (let ((dst (if (probe-file dst) + (merge-pathnames (pathname-directory-last-name src) + dst) + dst))) + (when (subdirectory-p dst src) + (editor-error "Cannot copy `~A' into its subdirectory `~A'" src dst)) + ;(format t "mkdir ~A~%" dst) + (let ((dst (ensure-directories-exist dst))) + (dolist (file (list-directory src)) + (copy-file file dst))) + (when *rename-p* (uiop:delete-empty-directory src)))) + +(defun copy-file (src dst) + (if (uiop:directory-pathname-p src) + (copy-directory src dst) + (let ((probe-dst (probe-file dst))) + (cond ((and probe-dst (uiop:directory-pathname-p probe-dst)) + (copy-file src (merge-pathnames (file-namestring src) probe-dst))) + (t + ;(format t "copy ~A -> ~A~%" src dst) + (if *rename-p* + (rename-file src dst) + (uiop:copy-file src dst))))))) + +(defun copy-file* (src dst) + #+windows + (copy-file src dst) + #-windows + (run-command `("cp" "-R" ,src ,dst))) + +(defun rename-file* (src dst) + #+windows + (let ((*rename-p* t)) + (copy-file src dst)) + #-windows + (run-command `("mv" ,src ,dst))) + +(defun copy-or-rename-file (src dst) + #+windows + (copy-file src dst) + #-windows + (if *rename-p* + (run-command `("mv" ,src ,dst)) + (run-command `("cp" "-R" ,src ,dst)))) + +(defun check-copy-files (src-files dst) + (let ((n (length src-files))) + (cond ((or (and (< 1 n) (uiop:file-exists-p dst)) + (and (= 1 n) (uiop:directory-pathname-p (first src-files)) + (uiop:file-exists-p dst))) + (editor-error "Target must be a directory")) + ((and (< 1 n) (not (uiop:directory-exists-p dst))) + (editor-error "No such file or directory"))))) + +(defun copy-files (src-files dst-file) + (check-copy-files src-files dst-file) + (dolist (file src-files) + (copy-or-rename-file file dst-file))) + +(defun rename-files (src-files dst-file) + (let ((*rename-p* t)) + (dolist (file src-files) + (copy-or-rename-file file dst-file)))) + +(defun symbolic-link-p (pathname) + (not (uiop:pathname-equal pathname (probe-file pathname)))) + +(defun pathname-directory-last-name (pathname) + (enough-namestring pathname (uiop:pathname-parent-directory-pathname pathname))) + +;;; internal functions +(defun run-command (command) + (when (consp command) + (setf command (mapcar #'princ-to-string command))) + (let ((error-string + (with-output-to-string (error-output) + (uiop:run-program command + :ignore-error-status t + :error-output error-output)))) + (when (string/= error-string "") + (lem:editor-error "~A" error-string)))) diff --git a/src/ext/directory-mode/internal.lisp b/src/ext/directory-mode/internal.lisp new file mode 100644 index 000000000..b752bd88b --- /dev/null +++ b/src/ext/directory-mode/internal.lisp @@ -0,0 +1,292 @@ +(uiop:define-package :lem/directory-mode/internal + (:use :cl + :lem) + (:import-from :lem/directory-mode/file + :symbolic-link-p + :pathname-directory-last-name) + (:import-from :lem/directory-mode/attributes + :current-directory-attribute + :file-size-attribute + :file-date-attribute + :file-attribute + :directory-attribute + :link-attribute) + (:import-from :lem/directory-mode/mode + :directory-mode) + (:export :sort-method + :*default-sort-method* + :update-buffer + :directory-buffer + :open-selected-file + :set-mark + :get-mark + :filter-marks + :get-name + :marked-lines + :marked-files + :get-pathname + :current-file + :selected-files + :update-all-buffers + :update-highlight-line-overlay)) +(in-package :lem/directory-mode/internal) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (sb-ext:lock-package :lem/directory-mode/internal)) + +(deftype sort-method () + '(member :pathname :mtime :size)) + +(declaim (type (sort-method) *default-sort-method*)) +(defvar *default-sort-method* :pathname + "Default method to sort files when opening a directory. + + A keyword, one of :pathname (sort by file name), :mtime (last modification time) and :size.") + +(defun move-to-start-line (point) + (move-to-line point 3)) + +(defun insert-spaces-with-property (point pathname name) + (insert-string point + " " + 'pathname pathname + 'name name)) + +(defun get-line-property (p key) + (with-point ((p p)) + (text-property-at (line-start p) key))) + +(defun get-pathname (point) + (get-line-property point 'pathname)) + +(defun get-name (point) + (get-line-property point 'name)) + +(defun get-mark (p) + (with-point ((p p)) + (eql #\* (character-at (line-start p) 1)))) + +(defun set-mark (p mark) + (with-buffer-read-only (point-buffer p) nil + (let ((*inhibit-read-only* t)) + (with-point ((p p)) + (let ((pathname (get-line-property p 'pathname))) + (when (and pathname (not (uiop:pathname-equal + pathname + (uiop:pathname-parent-directory-pathname + (buffer-directory (point-buffer p)))))) + (character-offset (line-start p) 1) + (delete-character p 1) + (insert-character p (if mark #\* #\space)))))))) + +(defun iter-marks (p function) + (with-point ((p p)) + (move-to-start-line p) + (loop + (funcall function p) + (unless (line-offset p 1) (return))))) + +(defun marked-lines (p) + (with-point ((p p)) + (let ((points '())) + (iter-marks p + (lambda (p) + (when (get-mark p) + (push (copy-point p :temporary) points)))) + (nreverse points)))) + +(defun marked-files (p) + (mapcar 'get-pathname (marked-lines p))) + +(defun filter-marks (p function) + (iter-marks p + (lambda (p) + (set-mark p (funcall function p))))) + +(defun current-file (p) + (alexandria:when-let (pathname (get-pathname p)) + pathname)) + +(defun selected-files (p) + (or (marked-files p) + (uiop:ensure-list (current-file p)))) + +(defun process-current-line-pathname (function) + (alexandria:when-let (pathname (get-pathname (current-point))) + (funcall function pathname))) + +(defun open-selected-file (&key read-only next-window) + (if read-only + (process-current-line-pathname (if next-window 'read-file-next-window 'read-file)) + (process-current-line-pathname (if next-window 'find-file-next-window 'find-file)))) + +(defstruct item + directory + pathname + content) + +(defun item-name (item) + (or (item-content item) + (namestring (enough-namestring (item-pathname item) + (item-directory item))))) + +(defun human-readable-file-size (size) + (loop :for sign :in '(#\Y #\Z #\E #\P #\T #\G #\M #\k) + :for val := (expt 1024 8) :then (/ val 1024) + :do (when (>= size val) + (return (format nil "~D~C" (1+ (floor size val)) sign))) + :finally (return (princ-to-string size)))) + +(defun insert-file-size (point item) + (let ((pathname (item-pathname item))) + (let ((file-size (handler-case (file-size pathname) + (error () + (return-from insert-file-size))))) + + (insert-string point + (format nil " ~5@A " + (if file-size (human-readable-file-size file-size) "")) + :attribute 'file-size-attribute)))) + +(defun insert-file-write-date (point item) + (let ((pathname (item-pathname item))) + (multiple-value-bind (second minute hour day month year week) + (let ((date (file-write-date pathname))) + (if date + (decode-universal-time date) + (values 0 0 0 0 0 0 nil))) + (insert-string point + (format nil "~4,'0D/~2,'0D/~2,'0D ~2,'0D:~2,'0D:~2,'0D ~A " + year month day hour minute second + (if week (aref #("Mon" "Tue" "Wed" "Thr" "Fri" "Sat" "Sun") week) + " ")) + :attribute 'file-date-attribute)))) + +(defun insert-icon (point pathname) + (cond ((uiop:directory-pathname-p pathname) + (insert-string point (icon-string "folder"))) + ((let ((string (icon-string-by-ext (pathname-type pathname)))) + (when string + (insert-string point string) + t))) + (t + (let ((string (icon-string-by-ext "txt"))) + (when string + (insert-string point string) + t))))) + +(defun get-file-attribute (pathname) + (cond ((symbolic-link-p pathname) + 'link-attribute) + ((uiop:directory-pathname-p pathname) + 'directory-attribute) + (t + 'file-attribute))) + +(defun insert-file-name (point item) + (let ((name (item-name item)) + (pathname (item-pathname item))) + (unless (string= name "..") + (insert-icon point name)) + (insert-string point + name + :attribute (get-file-attribute pathname) + :file pathname) + (when (symbolic-link-p pathname) + (insert-string point (format nil " -> ~A" (probe-file pathname)))))) + +(defparameter *file-entry-inserters* + (list #'insert-file-size + #'insert-file-write-date + #'insert-file-name)) + +(defun insert-item (point item) + (dolist (inserter *file-entry-inserters*) + (funcall inserter point item))) + +(defun insert-pathname (point item) + (let ((pathname (item-pathname item))) + (with-point ((start point)) + (insert-spaces-with-property point pathname (item-name item)) + (insert-item point item) + (back-to-indentation start) + (lem/button:apply-button-between-points + start + point + (lambda () + (lem/button:with-context () + (open-selected-file :read-only nil :next-window nil)))) + (insert-character point #\newline) + (put-text-property start point :read-only t)))) + +(defun insert-directories-and-files (point + directory + &key (sort-method *default-sort-method*) + (without-parent-directory t)) + (unless without-parent-directory + (alexandria:when-let (pathname (probe-file (merge-pathnames "../" directory))) + (insert-pathname point (make-item :directory directory :pathname pathname :content "..")))) + (dolist (pathname (list-directory directory :sort-method sort-method)) + (insert-pathname point (make-item :directory directory :pathname pathname)))) + +(defun update-buffer (buffer &key (sort-method *default-sort-method*)) + "Update this directory buffer content." + (with-buffer-read-only buffer nil + (let ((*inhibit-read-only* t)) + (let* ((directory (buffer-directory buffer)) + (p (buffer-point buffer)) + (line-number (line-number-at-point p))) + (erase-buffer buffer) + (buffer-start p) + (insert-string p (format nil "~A~2%" directory) :attribute 'current-directory-attribute) + (insert-directories-and-files p directory + :sort-method sort-method + :without-parent-directory nil) + (move-to-line p line-number))))) + +(defun create-directory-buffer (name filename) + (let ((buffer (make-buffer name :enable-undo-p nil :read-only-p t))) + (change-buffer-mode buffer 'directory-mode) + (setf (buffer-directory buffer) filename) + (update-buffer buffer) + (move-to-start-line (buffer-point buffer)) + buffer)) + +(defun directory-buffer (filename) + (setf filename (uiop:directory-exists-p + (expand-file-name (namestring filename) + (buffer-directory)))) + (let* ((name (pathname-directory-last-name filename)) + (buffer (get-buffer name))) + (cond ((and buffer (not (uiop:pathname-equal filename (buffer-directory buffer)))) + (create-directory-buffer (unique-buffer-name name) filename)) + ((null buffer) + (create-directory-buffer name filename)) + (t + buffer)))) + +(defun update-all-buffers () + (dolist (buffer (buffer-list)) + (when (eq 'directory-mode (buffer-major-mode buffer)) + (update-buffer buffer)))) + +(defun remove-line-overlay-when-buffer-change (point arg) + (declare (ignore arg)) + (alexandria:when-let (ov (buffer-value (point-buffer point) 'line-overlay)) + (setf (buffer-value (point-buffer point) 'line-overlay) nil) + (delete-overlay ov))) + +(defun update-highlight-line-overlay (point) + (with-point ((start point) + (end point)) + (back-to-indentation start) + (line-end end) + (let ((ov (buffer-value point 'line-overlay))) + (cond (ov + (move-point (overlay-start ov) start) + (move-point (overlay-end ov) end)) + (t + (add-hook (variable-value 'before-change-functions :buffer (point-buffer point)) + 'remove-line-overlay-when-buffer-change) + (setf ov (make-overlay start end 'region)) + (setf (buffer-value point 'line-overlay) ov)))))) diff --git a/src/ext/directory-mode/keybinds.lisp b/src/ext/directory-mode/keybinds.lisp new file mode 100644 index 000000000..17018effa --- /dev/null +++ b/src/ext/directory-mode/keybinds.lisp @@ -0,0 +1,42 @@ +(uiop:define-package :lem/directory-mode/keybinds + (:use :cl + :lem + :lem/directory-mode/commands) + (:import-from :lem/directory-mode/mode + :*directory-mode-keymap*)) +(in-package :lem/directory-mode/keybinds) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (sb-ext:lock-package :lem/directory-mode/keybinds)) + +(define-key *global-keymap* "C-x C-j" 'find-file-directory) + +(define-key *directory-mode-keymap* "q" 'quit-active-window) +(define-key *directory-mode-keymap* "M-q" 'quit-active-window) +(define-key *directory-mode-keymap* "g" 'directory-mode-update-buffer) +(define-key *directory-mode-keymap* "^" 'directory-mode-up-directory) +(define-key *directory-mode-keymap* "Return" 'directory-mode-find-file) +(define-key *directory-mode-keymap* "Space" 'directory-mode-read-file) +(define-key *directory-mode-keymap* "o" 'directory-mode-find-file-next-window) +(define-key *directory-mode-keymap* "n" 'directory-mode-next-line) +(define-key *directory-mode-keymap* "p" 'directory-mode-previous-line) +(define-key *directory-mode-keymap* "M-}" 'directory-mode-next-mark) +(define-key *directory-mode-keymap* "M-{" 'directory-mode-previous-mark) +(define-key *directory-mode-keymap* "m" 'directory-mode-mark-and-next-line) +(define-key *directory-mode-keymap* "u" 'directory-mode-unmark-and-next-line) +(define-key *directory-mode-keymap* "U" 'directory-mode-unmark-and-previous-line) +(define-key *directory-mode-keymap* "t" 'directory-mode-toggle-marks) +(define-key *directory-mode-keymap* "* !" 'directory-mode-unmark-all) +(define-key *directory-mode-keymap* "* %" 'directory-mode-mark-regexp) +(define-key *directory-mode-keymap* "* /" 'directory-mode-mark-directories) +(define-key *directory-mode-keymap* "* @" 'directory-mode-mark-links) +(define-key *directory-mode-keymap* "* C-n" 'directory-mode-next-mark) +(define-key *directory-mode-keymap* "* C-p" 'directory-mode-previous-mark) +(define-key *directory-mode-keymap* "Q" 'directory-mode-query-replace) +(define-key *directory-mode-keymap* "D" 'directory-mode-delete-files) +(define-key *directory-mode-keymap* "C" 'directory-mode-copy-files) +(define-key *directory-mode-keymap* "R" 'directory-mode-rename-files) +(define-key *directory-mode-keymap* "r" 'directory-mode-rename-file) +(define-key *directory-mode-keymap* "s" 'directory-mode-sort-files) +(define-key *directory-mode-keymap* "+" 'make-directory) +(define-key *directory-mode-keymap* "C-k" 'directory-mode-kill-lines) diff --git a/src/ext/directory-mode/main.lisp b/src/ext/directory-mode/main.lisp new file mode 100644 index 000000000..a5976a085 --- /dev/null +++ b/src/ext/directory-mode/main.lisp @@ -0,0 +1,14 @@ +(uiop:define-package :lem/directory-mode + (:use :cl) + (:use-reexport :lem/directory-mode/commands) + (:use-reexport :lem/directory-mode/mode) + (:import-from :lem/directory-mode/internal + :*default-sort-method*) + (:export :*default-sort-method* + :*directory-mode-keymap*)) +(in-package :lem/directory-mode) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (sb-ext:lock-package :lem/directory-mode)) + +(setf lem:*find-directory-function* 'lem/directory-mode/internal:directory-buffer) diff --git a/src/ext/directory-mode/mode.lisp b/src/ext/directory-mode/mode.lisp new file mode 100644 index 000000000..4274f82cd --- /dev/null +++ b/src/ext/directory-mode/mode.lisp @@ -0,0 +1,13 @@ +(uiop:define-package :lem/directory-mode/mode + (:use :cl :lem) + (:export :directory-mode + :*directory-mode-keymap*)) +(in-package :lem/directory-mode/mode) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (sb-ext:lock-package :lem/directory-mode/mode)) + +(define-major-mode directory-mode () + (:name "Directory" + :keymap *directory-mode-keymap*) + (setf (variable-value 'highlight-line :buffer (current-buffer)) nil)) diff --git a/src/ext/filer.lisp b/src/ext/filer.lisp index 268e0253f..2e7c13c78 100644 --- a/src/ext/filer.lisp +++ b/src/ext/filer.lisp @@ -89,10 +89,10 @@ (defun insert-item (point item) (with-point ((start point)) (back-to-indentation start) - (lem/directory-mode::insert-icon point (item-pathname item)) + (lem/directory-mode/internal::insert-icon point (item-pathname item)) (insert-string point (item-content item) - :attribute (lem/directory-mode::get-file-attribute (item-pathname item))) + :attribute (lem/directory-mode/internal::get-file-attribute (item-pathname item))) (put-text-property start point :item item) (lem-core::set-clickable start point