;;; package-cl-inspect-mode.el --- -*- lexical-binding: t -*- ;; TODO: Name? cl-inspect? clinspect? clidget? eclidget? ;; Actually, make it independent of CL. ;; TODO: Better table abstraction? ;; Maybe https://github.com/kiwanami/emacs-ctable? ;; TODO: Add filters. ;; Can we add filters that ask for a column and a range depending on the type of data? (require 'cl-lib) (defvar clinspect-column-max-width 40) (defun clinspect--tabulated-list-sorter (entry index) "Return a sorter that smarter than `tabulated-list' default. If ENTRY element at INDEX is a number, sort by number, otherwise sort by string." (if (numberp (nth index entry)) (lambda (a b) (< (string-to-number (aref (cadr a) index)) (string-to-number (aref (cadr b) index)))) t)) (defun clinspect--tabulated-list-format (header entries) (apply #'vector (mapcar (lambda (index) (let ((column (nth index header))) (list column (min clinspect-column-max-width (max (length column) (apply #'max (mapcar (lambda (entry) (length (prin1-to-string (nth index entry)))) entries)))) (clinspect--tabulated-list-sorter (car entries) index)))) (number-sequence 0 (1- (length header)))))) (defun clinspect--vector->list (vector) (cl-loop for i from 0 below (length vector) collect (aref vector i))) (defun clinspect--test-filter (entry) (let ((path (aref (cadr entry) 0))) (string-match-p "agenda" path))) (defvar clinspect-filters (list #'identity)) ;; (setq clinspect-filters (list #'clinspect--test-filter)) (defun clinspect--filter-column (index predicate) (lambda (entry) (let ((value (aref (cadr entry) index))) (funcall predicate value)))) (defun clinspect--number-filter (index) (let ((min (read-from-minibuffer "Min value (empty means none): " nil nil :read nil "nil")) (max (read-from-minibuffer "Max value (empty means none): " nil nil :read nil "nil")) (number (string-to-number value))) (clinspect--filter-column index (lambda (value) (cond ((and min max) (<= min number max)) (min (<= min number )) (max (<= number max))))))) (defun clinspect-add-filter () (interactive) (let* ((column-names (mapcar #'car (clinspect--vector->list tabulated-list-format))) (column (completing-read "Column to filter by: " column-names)) (index (cl-position column column-names :test #'equal)) (sample (aref (cadr (car clinspect--tabulated-list-entries)) index)) (revert? t)) (cl-typecase (car (read-from-string sample)) (integer (add-to-list 'clinspect-filters (clinspect--number-filter index))) (t (message "Unsupported column values to infer filter.") (setq revert? nil))) (when revert? (tabulated-list-revert)))) (defun clinspect-reset-filters () (interactive) (setq clinspect-filters (list #'identity)) (tabulated-list-revert)) (defun clinspect--refresh () ;; TODO: Use -orfn / -andfn? (setq tabulated-list-entries (cl-loop for entry in clinspect--tabulated-list-entries when (cl-loop for filter in clinspect-filters always (funcall filter entry)) collect entry) ;; (clinspect-filter ;; clinspect--tabulated-list-entries) ) (tabulated-list-init-header)) (define-derived-mode clinspect-mode tabulated-list-mode "Clinspect" "Mode to inspect Common Lisp sequences." (add-hook 'tabulated-list-revert-hook 'clinspect--refresh nil t)) (defvar clinspect-buffer-name "clinspector") (defvar-local clinspect--tabulated-list-entries nil "Backup of `tabulated-list-entries', in case the latter gets filtered.") (defun clinspect (header data &optional name) "Inspect DATA. DATA is a list of things. HEADER is a list of strings, the column names." (switch-to-buffer (generate-new-buffer (format "*%s%s*" clinspect-buffer-name (if name (format "<%s>" name) "")))) (clinspect-mode) (setq clinspect--tabulated-list-entries (cl-loop for line in data for index from 1 upto (length header) collect (list index (apply #'vector (mapcar #'prin1-to-string line))))) (setq tabulated-list-format (clinspect--tabulated-list-format header data)) (tabulated-list-revert)) (provide 'clinspect)