662 lines
22 KiB
EmacsLisp
662 lines
22 KiB
EmacsLisp
;;; um.el --- An Ultralight database for Markdown composition. -*- lexical-binding: t -*-
|
|
|
|
;; by bth
|
|
;; Version: 0.1
|
|
|
|
;; Um is an ultralight database design and emacs toolkit for organizing writing
|
|
;; into larger compositions. It consists of two parts:
|
|
;;
|
|
;; 1. Elisp for functionality within emacs.
|
|
;; 2. Go for the commandline interface.
|
|
;;
|
|
;; This is somewhat like org-roam, except without any database dependency. (And
|
|
;; it assumes Markdown rather than org, which I don't care for.)
|
|
;;
|
|
;; This depends on a few simple ideas:
|
|
;;
|
|
;; 1. A numbered filename specification which serves as unique id. The
|
|
;; filesystem is the database.
|
|
;;
|
|
;; 2. A file header consisting of the title, date, and optional tags. These tags
|
|
;; can be used by the CLI to construct expressive queries.
|
|
;;
|
|
;; 3. Using a "root" project defined by `um-root-glob', where source files
|
|
;; should first be composed and where we can assume a file exists if not
|
|
;; elsewhere.
|
|
;;
|
|
;; 4. Using the built-in `project' package and the CLI to organize compositions
|
|
;; built from these source files.
|
|
;;
|
|
;; features provided:
|
|
;; `um-mode': for *.um files, which are lists of content files produced by "um tag".
|
|
;; `um-minor-mode': for *.md files, which are the content files.
|
|
;; `um-find-file-at-point' via `find-file': open a file under point in the
|
|
;; current project, falling back to a source directory.
|
|
;; `um-target-file-at-point-advice' via `embark-dwim': open file under point in all
|
|
;; known projects, falling back to a source directory.
|
|
;; `um-tag-xref': find files with some tag
|
|
;; `um-tag-dwim': insert or delete a tag in dired and other contexts.
|
|
;; `um-grep': grep limited to a filelist obtained dynamically.
|
|
|
|
(require 'project)
|
|
(require 'dired)
|
|
(require 'grep)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; defcustom
|
|
|
|
(defgroup um nil
|
|
"(u)ltralight (m)arkdown zettelkasten."
|
|
:link '(url-link :tag "Website" "https://git.bth.moi/bth/um")
|
|
:group 'files
|
|
:prefix "um-")
|
|
|
|
(defcustom um-root-glob ".*/writing/journal"
|
|
"Primary glob for `um-root-path.' This allows various mountpoints."
|
|
:type '(string)
|
|
:group 'um
|
|
)
|
|
|
|
(defcustom um-date-separator "-"
|
|
"Seperator used in date strings, used by `um--date-format' and `um--date-re'."
|
|
:type '(string)
|
|
:group 'um
|
|
)
|
|
|
|
;; NOTE: regexp-opt wraps the whole expression in (), so I can't omit the -.
|
|
(defcustom um-locale-re (regexp-opt '(
|
|
"- home"
|
|
"- away"
|
|
))
|
|
"um locale regexp. Add your custom places within a `regexp-opt' or
|
|
make it general."
|
|
:type '(string)
|
|
:group 'um
|
|
)
|
|
|
|
(defconst um--date-format (concat "%Y" um-date-separator "%m" um-date-separator "%d")
|
|
"Format passed to `format-time-string' when creating
|
|
`um--header'. ISO8601 with a custom `um-date-separator'.")
|
|
|
|
(defconst um--date-re (rx
|
|
line-start
|
|
(literal ": ")
|
|
(group (repeat 4 digit)
|
|
(literal um-date-separator)
|
|
(repeat 1 2 digit)
|
|
(literal um-date-separator)
|
|
(repeat 1 2 digit))
|
|
line-end
|
|
)
|
|
"um date regexp. built from `um-date-separator'."
|
|
)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; find-file-at-point
|
|
|
|
(defun um--root-path ()
|
|
(or (car (seq-filter
|
|
(lambda (path)
|
|
;; this way I don't have to hardcode the full path:
|
|
(string-match um-root-glob path))
|
|
;; stores all project roots in a list of strings:
|
|
(project-known-project-roots)))
|
|
;; fallback if not found
|
|
default-directory
|
|
))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;;; `find-file' integration
|
|
;;
|
|
;; combined with the hook into `file-name-at-point-functions', this means we can
|
|
;; run `find-file' at point, followed by `next-history-element'. By default:
|
|
;; C-x C-f M-n
|
|
|
|
;;;###autoload
|
|
(defun um-find-file-at-point ()
|
|
"Return full file path of thing-at-point, falling back to:
|
|
|
|
1. `default-directory'
|
|
2. `project-root'
|
|
3. `um--root-path'
|
|
"
|
|
(let ((dirs (list
|
|
default-directory
|
|
(when (project-current) (project-root (project-current)))
|
|
(um--root-path)))
|
|
dir
|
|
file)
|
|
(while (and dirs (not file))
|
|
(setq dir (car dirs)
|
|
dirs (cdr dirs))
|
|
(let* ((default-directory dir)
|
|
(fap (thing-at-point 'existing-filename t)))
|
|
(when fap
|
|
(setq file (expand-file-name fap)))))
|
|
file))
|
|
|
|
;; NOTE: we now load this by default if the package is loaded.
|
|
(add-hook 'file-name-at-point-functions 'um-find-file-at-point)
|
|
|
|
(defun um--filelist-dwim ()
|
|
"Return a list of filenames obtained from marks in dired, from the active
|
|
region, or from the file at point, or the current file."
|
|
(let (
|
|
(marks (if (eq major-mode 'dired-mode) (dired-get-marked-files) nil))
|
|
(fap (um-find-file-at-point))
|
|
)
|
|
(cond
|
|
((and (region-active-p) (not (eq major-mode 'dired-mode)))
|
|
(string-split (buffer-substring-no-properties (region-beginning) (region-end))))
|
|
(marks marks)
|
|
(fap (list fap))
|
|
(t (list (buffer-file-name)))
|
|
)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;;; `embark-dwim' integration
|
|
;;
|
|
;; Load this with an advice:
|
|
;; (advice-add 'embark-target-file-at-point :around 'um-target-file-at-point-advice)
|
|
|
|
;; `embark-target-file-at-point' uses `ffap-file-at-point', which uses
|
|
;; `default-directory' to expand the filename.
|
|
|
|
;; NOTE: decided to use an advice rather than add my own function to
|
|
;; `embark-target-finders', since I'd have to remove the file finder and put my
|
|
;; function in the same place in the list anyway.
|
|
|
|
;;;###autoload
|
|
(defun um-target-file-at-point-advice (origfunc)
|
|
"Wrapper for `embark-target-file-at-point' that falls back to:
|
|
|
|
1. `default-directory'
|
|
2. `project-root'
|
|
3. `um--root-path'
|
|
"
|
|
(or
|
|
;; this does both default-directory and project-root:
|
|
(funcall origfunc)
|
|
(let ((default-directory (um--root-path)))
|
|
(funcall origfunc))
|
|
))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; tags
|
|
|
|
(defconst um--tag-regexp "^\\+ \\([[:alpha:]\\_\\-]+\\)$"
|
|
"um tag regexp. Allows hypens and underscores within the tag.")
|
|
|
|
(defconst um--header-end "\n\n" "um header is delimited by the first two newlines.")
|
|
|
|
;; NOTE: this will get saved by savehist-mode
|
|
(defvar um-tags-history nil "History of inserted or searched for tags. Populates
|
|
`completing-read'.")
|
|
|
|
(defun um--header-end-pos ()
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(search-forward um--header-end)))
|
|
|
|
(defun um--tag-first-in-buffer ()
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(when (search-forward um--tag-regexp (um--header-end-pos) t)
|
|
(match-string 1))))
|
|
|
|
;;;###autoload
|
|
(defun um-tag-xref ()
|
|
"Run `project-find-regexp' on a selection made from `um-tags-history' via
|
|
`completing-read'.
|
|
|
|
The initial value provided to `completing-read' is the first tag found in the
|
|
current buffer: it will be first in the list and available via \\`M-n'.
|
|
|
|
NOTE: searches in the current project root by default, but
|
|
\\[universal-argument] will allow choice of the base directory as in
|
|
`project-find-regexp'.
|
|
|
|
Ultimately this relies on `xref-matches-in-files', which calls
|
|
`xref-search-program'.
|
|
"
|
|
(interactive)
|
|
(project-find-regexp (concat "^\\+ "
|
|
(completing-read "um-tag-xref: " um-tags-history nil
|
|
nil nil
|
|
;; NOTE: this means searches
|
|
;; will add to the history:
|
|
'um-tags-history
|
|
(um--tag-first-in-buffer))
|
|
"$")))
|
|
|
|
(defun um--extract-tags (files)
|
|
(let ((tags))
|
|
(dolist (f files)
|
|
(with-current-buffer
|
|
(find-file-noselect f t)
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(let* ((bound (um--header-end-pos))
|
|
(found t))
|
|
(while found
|
|
(setq found (search-forward-regexp um--tag-regexp bound t))
|
|
(when found (add-to-list 'tags (buffer-substring-no-properties
|
|
(match-beginning 1) (match-end 1))))
|
|
)))))
|
|
(reverse tags)))
|
|
|
|
(defun um--tag-insert (tag)
|
|
"Insert TAG as + tag\n in current buffer appending to the journal header.
|
|
|
|
Emits message if TAG is already present, but does not error."
|
|
(if (member tag (um--extract-tags (list buffer-file-name)))
|
|
;; NOTE: user-error would stop any iteration over files:
|
|
(message "\"%s\" tag already exists in %s" tag buffer-file-name)
|
|
(goto-char (um--header-end-pos))
|
|
(forward-line -1)
|
|
(insert (concat "+ " tag "\n"))))
|
|
|
|
(defun um--tag-delete (tag)
|
|
"Delete TAG from the journal header in current buffer."
|
|
(goto-char (point-min))
|
|
(if (search-forward (concat "+ " tag "\n") (um--header-end-pos) t)
|
|
(delete-region (match-beginning 0) (match-end 0))
|
|
;; NOTE: as long as collection was gathered with um--extract-tags, this
|
|
;; shouldn't happen:
|
|
(message "\"%s\" tag not found in %s" tag buffer-file-name)))
|
|
|
|
(defun um--tag-do (tag insert)
|
|
"Insert or delete TAG from the journal header in current buffer.
|
|
|
|
insert when INSERT > 0, delete otherwise."
|
|
(save-excursion
|
|
(if (> insert 0)
|
|
(um--tag-insert tag)
|
|
(um--tag-delete tag))))
|
|
|
|
;;;###autoload
|
|
(defun um-tag-dwim (ARG)
|
|
"Run `um--tag-do' on a list of filenames if region active outside
|
|
dired-mode, or if marks exist in dired-mode, or the filename at point, and
|
|
finally in the current buffer if none of those conditions match.
|
|
|
|
Assumes the files of interest are returned by `um--root-path'.
|
|
|
|
Negative prefix arg is handled by `um--tag-do', which see.
|
|
"
|
|
(interactive "p")
|
|
(let* (
|
|
(insert (> ARG 0))
|
|
(prompt (format "um tag %s: " (if insert "insert" "delete")))
|
|
(files (um--filelist-dwim))
|
|
(collection (if insert um-tags-history (um--extract-tags files)))
|
|
;; no need for dupes:
|
|
(history-delete-duplicates t)
|
|
;; override sorting when deleting, because we sort the tags:
|
|
(completions-sort (if insert completions-sort nil))
|
|
(vertico-sort-function (if (and insert (bound-and-true-p vertico-sort-function))
|
|
vertico-sort-function
|
|
nil))
|
|
(tag (completing-read prompt collection nil nil nil
|
|
'um-tags-history)))
|
|
|
|
(dolist (f files)
|
|
(with-current-buffer
|
|
;; TODO: this should use the fallback logic:
|
|
(find-file-noselect (expand-file-name f (um--root-path)) t)
|
|
(um--tag-do tag ARG)
|
|
(save-buffer)))
|
|
))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; grep
|
|
|
|
(defvar um-grep-history nil)
|
|
|
|
(defun um-grep (ARG)
|
|
"Run `grep' restricted to the files provided by `um--filelist-dwim' and
|
|
relative to `um--root-path'.
|
|
|
|
Prefix arg prompts for the directory instead.
|
|
"
|
|
(interactive "P")
|
|
(grep-compute-defaults)
|
|
(when-let* (
|
|
(default-directory (if ARG (read-string "directory: ") (um--root-path)))
|
|
(query (shell-quote-argument (read-string "um-grep: " nil 'um-grep-history)))
|
|
;; resolve to relative paths to keep the grep buffer sane:
|
|
(files (mapcar (lambda (f) (file-relative-name (expand-file-name f) default-directory))
|
|
(um--filelist-dwim)))
|
|
(file-string (string-join files " "))
|
|
(cmd (concat grep-command query " " file-string))
|
|
)
|
|
(grep cmd)
|
|
))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; ispell
|
|
|
|
(defun um-do-ispell ()
|
|
"Run `ispell-buffer' on a list of files."
|
|
(interactive)
|
|
(dolist (f (um--filelist-dwim))
|
|
(save-window-excursion
|
|
(with-current-buffer
|
|
;; NOTE: not find-file-noselect because ispell needs to display the window:
|
|
(find-file (expand-file-name f (um--root-path)) t)
|
|
(ispell-buffer)
|
|
))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; CLI
|
|
|
|
(defun um--header (filename &optional tags)
|
|
"Create a header composed of:
|
|
|
|
# filename
|
|
: date
|
|
+ tags
|
|
|
|
"
|
|
(insert
|
|
(format "# %s\n: %s%s" filename (format-time-string um--date-format) um--header-end))
|
|
(when tags
|
|
(forward-line -1)
|
|
(dolist (tag (split-string tags ","))
|
|
(insert (format "+ %s\n"
|
|
(if (equal "+" tag)
|
|
(cadr (split-string filename "\\."))
|
|
tag))))
|
|
(forward-line)))
|
|
|
|
;;;###autoload
|
|
(defun um-next (filename &optional tags)
|
|
"Takes the filename generated by the CLI, opens it, and adds the header.
|
|
|
|
Optional TAGS string may contain more than one tag separated by a comma.
|
|
|
|
A tag with a value of \"+\" is rendered as the descriptor portion of the filename.
|
|
"
|
|
(find-file filename)
|
|
(um--header filename tags)
|
|
(message "um next: %s" filename))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; um-minor-mode
|
|
;;
|
|
;; intended to be hooked into markdown-mode
|
|
|
|
(defface um-date-face
|
|
`((((type tty) (class mono)))
|
|
(t (
|
|
:inherit shadow
|
|
)))
|
|
"um date face"
|
|
:group 'um
|
|
)
|
|
|
|
(defface um-locale-face
|
|
`((((type tty) (class mono)))
|
|
(t (
|
|
:inherit shadow
|
|
)))
|
|
"um locale face"
|
|
:group 'um
|
|
)
|
|
|
|
(defface um-tag-face
|
|
`((((type tty) (class mono)))
|
|
(t (
|
|
:inherit shadow
|
|
)))
|
|
"um tag face"
|
|
:group 'um
|
|
)
|
|
|
|
(defvar um--minor-mode-keywords
|
|
`(
|
|
(,um--date-re 1 'um-date-face)
|
|
(,um-locale-re 0 'um-locale-face)
|
|
(,um--tag-regexp 1 'um-tag-face)
|
|
))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;;; section
|
|
|
|
;; Rather than migrate to org-mode or some other more complex specification,
|
|
;; I've opted to extend and augment my own idiom for structuring Markdown as
|
|
;; befits my style of writing: simple sections separated by a horizontal line.
|
|
;; This code is an attempt to make working with those sections easier.
|
|
|
|
(defconst um--section "\n---\n\n")
|
|
|
|
(defun um-section ()
|
|
"Insert a horizontal rule between paragraphs, defining a section of text."
|
|
(interactive)
|
|
;; reduce to either 1 or none: if 1, it'll be pushed to after the string.
|
|
(delete-blank-lines)
|
|
(insert um--section)
|
|
;; move to the beginning of the following text
|
|
(re-search-forward "[^[:space:]]" nil t 1)
|
|
;; the previous leaves us just after the first character.
|
|
(backward-char)
|
|
;; reduces to a single empty line before point.
|
|
(ensure-empty-lines))
|
|
|
|
(defun um--goto-section (direction)
|
|
(let ((origpos (point))
|
|
(header (save-excursion
|
|
(goto-char (point-min))
|
|
(search-forward um--header-end nil t)))
|
|
(pos (save-excursion
|
|
(search-forward um--section nil t direction)))
|
|
(endpos (match-end 0)))
|
|
|
|
(cond ((= origpos (point-min)) (goto-char header))
|
|
((and pos (= 1 direction)) (goto-char pos))
|
|
;; when going backwards, the pos will be at the beginning of the
|
|
;; string, while the endpos will be where we want to end up. This allows
|
|
;; for the backwards movement to take you to that spot if you're within a
|
|
;; section, but move past it if you're already at this point.
|
|
((and pos (= -1 direction))
|
|
(goto-char pos)
|
|
(if (= origpos endpos)
|
|
(um--goto-section -1)
|
|
(goto-char endpos)))
|
|
;; pos will be nil when going back to beginning, so this clause will
|
|
;; match if the header is found:
|
|
((and header (= -1 direction)) (goto-char header))
|
|
)))
|
|
|
|
(defun um-forward-section ()
|
|
"Move to next section marker."
|
|
(interactive) (um--goto-section 1))
|
|
|
|
(defun um-backward-section ()
|
|
"Move to previous section marker."
|
|
(interactive) (um--goto-section -1))
|
|
|
|
(defun um-mark-section ()
|
|
"Mark text between section markers. Repeat to expand."
|
|
(interactive)
|
|
(let* ((pos (save-excursion
|
|
(search-backward um--section nil t)))
|
|
(m (if pos (match-end 0) (point))))
|
|
(when (not (region-active-p)) (set-mark m))
|
|
;; NOTE: don't use um--goto-section because it doesn't go to eobp
|
|
;; 'noerror will take us to the end of buffer if no next
|
|
(search-forward um--section nil 'noerror)))
|
|
|
|
(defun um--backward-section-permissive ()
|
|
;; NOTE: won't work without this hack when point is on line just below the
|
|
;; section, because the um--section includes 2 newlines.
|
|
(search-backward (substring um--section 0 -1) nil t))
|
|
|
|
(defun um--delete-section ()
|
|
(um--backward-section-permissive)
|
|
(delete-region (match-beginning 0) (match-end 0))
|
|
;; to achieve single blank line before:
|
|
(ensure-empty-lines)
|
|
;; and after:
|
|
(delete-blank-lines))
|
|
|
|
(defun um-backward-kill-paragraph-or-section ()
|
|
"Kills either a preceding paragraph or `um-section'"
|
|
(interactive)
|
|
(let ((section-pos) (text-pos))
|
|
(save-excursion
|
|
(setq section-pos (um--backward-section-permissive)))
|
|
(save-excursion
|
|
(setq text-pos (re-search-backward "[[:alpha:]]" nil t 1)))
|
|
(if (and section-pos (> section-pos text-pos))
|
|
(um--delete-section)
|
|
(backward-kill-paragraph 1))))
|
|
|
|
(defun um-transpose-section (&optional arg)
|
|
"Like transpose-paragraphs but for `um-section'.
|
|
|
|
A negative prefix argument moves it backward.
|
|
"
|
|
(interactive "p")
|
|
(let* ((direction (if (< arg 0)
|
|
-1 1
|
|
))
|
|
(origpos (point))
|
|
(last (save-excursion
|
|
(not (search-forward um--section nil t))))
|
|
(secondlast (save-excursion
|
|
(not (search-forward um--section nil t 2)))))
|
|
(cond ((and last (= -1 direction))
|
|
(um--goto-section -1)
|
|
(um-transpose-section 1))
|
|
((and secondlast (= 1 direction))
|
|
(um-mark-section)
|
|
(kill-region nil nil t)
|
|
(goto-char (point-max))
|
|
(um-section)
|
|
(yank)
|
|
(um--delete-section))
|
|
(t
|
|
(um-mark-section)
|
|
(kill-region nil nil t)
|
|
(um--goto-section direction)
|
|
(yank)))
|
|
;; to get us back to the top of the section region
|
|
(set-mark-command 1)
|
|
))
|
|
|
|
(defvar-keymap um-minor-mode-map
|
|
:doc "Keymap for `um-minor-mode'."
|
|
"M-s t" #'um-tag-xref
|
|
;; M-r is not exactly right, but can't think of a better binding:
|
|
"M-r t" #'um-tag-dwim
|
|
;; TODO: find a better default binding?
|
|
"C-c s" #'um-section
|
|
"M-o" #'um-backward-kill-paragraph-or-section
|
|
"C-M-n" #'um-forward-section
|
|
"C-M-p" #'um-backward-section
|
|
"C-M-h" #'um-mark-section
|
|
"C-M-t" #'um-transpose-section
|
|
)
|
|
|
|
(bind-keys
|
|
:repeat-map um-section-repeat-map
|
|
:repeat-docstring "Keymap to repeat um key sequences
|
|
`um-backward-section' and `um-forward-section'."
|
|
("p" . um-backward-section)
|
|
("n" . um-forward-section))
|
|
|
|
;;;###autoload
|
|
(define-minor-mode um-minor-mode
|
|
"Minor mode for `um' commands.
|
|
\\{um-minor-mode-map}"
|
|
:init-value nil
|
|
:lighter " um"
|
|
:keymap um-minor-mode-map
|
|
(if um-minor-mode
|
|
(progn
|
|
(font-lock-add-keywords nil um--minor-mode-keywords)
|
|
(font-lock-flush))
|
|
(font-lock-remove-keywords nil um--minor-mode-keywords)
|
|
(font-lock-flush))
|
|
)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;; um-mode : .um files
|
|
|
|
(defun um-next-line ()
|
|
(interactive) (move-beginning-of-line nil) (next-line))
|
|
|
|
(defun um-previous-line ()
|
|
(interactive) (move-beginning-of-line nil) (previous-line))
|
|
|
|
(defun um--reset-cursor-intangible-property ()
|
|
(let ((modified (buffer-modified-p)))
|
|
;; NOTE: remove all first, because otherwise we might miss a newline:
|
|
(remove-text-properties (point-min) (point-max)
|
|
'(
|
|
cursor-intangible t
|
|
;; NOTE: no faces in yanked text:
|
|
face nil))
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(while (not (eobp))
|
|
;; NOTE: actually the newline is the only position not included, but this
|
|
;; positions the cursor visually at the bol:
|
|
(add-text-properties (pos-bol) (pos-eol) '(cursor-intangible t))
|
|
(forward-line 1)))
|
|
;; we don't want to fuck with the modified state just to change text
|
|
;; properties, but we also don't want to obscure it if it was already modified.
|
|
(set-buffer-modified-p modified)))
|
|
|
|
(defun um--iro (cmd &optional args)
|
|
(let ((inhibit-read-only t))
|
|
(apply cmd args)
|
|
(um--reset-cursor-intangible-property)))
|
|
|
|
(defun um-drag-stuff-up () (interactive) (um--iro 'drag-stuff-up '(1)))
|
|
(defun um-drag-stuff-down () (interactive) (um--iro 'drag-stuff-down '(1)))
|
|
(defun um-kill-line () (interactive) (um--iro 'kill-line))
|
|
(defun um-kill-region () (interactive) (um--iro 'kill-region '(nil nil t)))
|
|
;; NOTE: parallel to dired-copy-filename-as-kill
|
|
(defun um-copy-line-as-kill () (interactive) (kill-ring-save (pos-bol) (pos-eol))
|
|
(message (buffer-substring-no-properties (pos-bol) (pos-eol))))
|
|
(defun um-yank () (interactive) (um--iro 'yank))
|
|
(defun um-tag-dwim-iro (arg) (interactive "p")
|
|
(um--iro 'um-tag-dwim (list arg)))
|
|
|
|
;; NOTE: define-derived-mode defines a sparse keymap with the parent mode. But
|
|
;; we can prefill it:
|
|
(defvar-keymap um-mode-map
|
|
:doc "Keymap for um-mode."
|
|
"n" 'um-next-line
|
|
"p" 'um-previous-line
|
|
"M-p" 'um-drag-stuff-up
|
|
"M-n" 'um-drag-stuff-down
|
|
"C-k" 'um-kill-line
|
|
"k" 'um-kill-line
|
|
"C-w" 'um-kill-region
|
|
"w" 'um-copy-line-as-kill
|
|
"C-y" 'um-yank
|
|
"t" 'um-tag-dwim-iro
|
|
;; g is sidebar-revert-resize to parallel dired:
|
|
"M-s g" #'um-grep
|
|
)
|
|
|
|
;;;###autoload
|
|
(define-derived-mode um-mode special-mode "um-mode"
|
|
"major mode for *.um file lists. Mostly `special-mode' with a few more commands.
|
|
|
|
\\{um-mode-map}
|
|
"
|
|
(let ((inhibit-read-only t))
|
|
(um--reset-cursor-intangible-property))
|
|
(cursor-intangible-mode)
|
|
(read-only-mode))
|
|
|
|
(provide 'um)
|
|
|
|
;; Local Variables:
|
|
;; outline-regexp: ";;;+ [^;]+"
|
|
;; eval: (outline-minor-mode 1)
|
|
;; End:
|