diff options
Diffstat (limited to 'siege-mode.el')
| -rw-r--r-- | siege-mode.el | 279 |
1 files changed, 194 insertions, 85 deletions
diff --git a/siege-mode.el b/siege-mode.el index 14119d6..74a33cb 100644 --- a/siege-mode.el +++ b/siege-mode.el @@ -1,10 +1,10 @@ ;;; Package --- Surround region with smart delimiters interactively -;; Time-stamp: <2018-07-05 21:19:22 (tslil)> +;; Time-stamp: <2018-07-07 23:46:55 (tslil)> ;; Copyright (c) 2018 tslil clingman ;; Author: tslil clingman <tslil@posteo.de> -;; Version: 1.0 +;; Version: 2.0 ;; Package-Requires: ((emacs "24.4")) ;; Keywords: region wrap @@ -26,61 +26,76 @@ ;; Lay siege to the region from all sides! ;; ;; When the region is active, all input is redirected to the minibuffer and -;; treated as a delimiter for the region. By default the input is used as the -;; left delimiter from which the right one is derived using -;; `siege-left-to-right-regexs'. This may be reversed by default -;; (`siege-default-left') or during usage via "C-c r" in the minibuffer. If -;; regexes are not desired they may be disabled via "C-c a" in the minibuffer -;; or by default (`siege-default-apply-regexs'). +;; treated as a delimiter for the region. The input is used as the left +;; delimiter from which the right one is derived using `siege-transform-regexs' +;; applied to tokens generated by `siege-boundary-list' and `siege-block-*'. If +;; such derivations are not desired they may be disabled via "C-c a" in the +;; minibuffer or by default (`siege-default-derive'). ;; ;; All changes are dynamically displayed in the buffer (see ;; `siege-preview-face') and may be committed by "SPC" or "Ret" in the ;; minibuffer. +;; +;; By default siege-mode understands the usual delimeter pairs, as well as latex +;; LaTeX begin/end pairs and left/right pairs. Moreover, for ease of use it will +;; pair both _{ and ^{ with } by default. ;;; Code: (require 'subr-x) +(require 'cl-lib) -(defcustom siege-left-to-right-regexs '( ("(" . ")") - ("\\[" . "]") - ("{" . "}") - ("<" . ">") - ("`" . "'") - ("``" . "''") - ("left" . "right") - ("langle" . "rangle") - ("begin" . "end") ) - "List of pairs (REGEX . REPLACE) used to generate the RIGHT delimiter from \ -the LEFT one. The list is traversed in order and every substitution is applied." +(defcustom siege-transform-regexs '(("({" . "})") + ("(" . ")") + ("\\[" . "]") + ("[_^]?{" . "}") + ("<" . ">") + ("`" . "'") + ("``" . "''") + ("\\\\left" . "\\\\right") + ("\\\\langle" . "\\\\rangle") + ("\\\\begin" . "\\\\end")) + "List of pairs (REGEX . REPLACE) used to generate the right delimiter from \ +the left one. The list is traversed in order and every substitution is applied." :group 'siege-mode :type '(repeat (cons string string))) -(defcustom siege-right-to-left-regexs '( (")" . "(") - ("\\]" . "[") - ("}" . "{") - (">" . "<") - ("'" . "`") - ("''" . "``") - ("right" . "left") - ("rangle" . "langle") - ("end" . "begin") ) - "List of pairs (REGEX . REPLACE) used to generate the LEFT delimiter from \ -the RIGHT one. The list is traversed in order and every substitution is applied." +(defcustom siege-boundary-list '("\\\\left\\\\{" + "\\\\left[({<\\[]" + "[_^]?{" + "[([<`*_/ ]" ) + "List of regexes giving tokens at which the input delimiter should be split \ +so that the resulting substrings may be transformed. See \ +`siege--tokenize-non-block' for details." :group 'siege-mode - :type '(repeat (cons string string))) + :type '(list string)) -(defcustom siege-default-left t - "Whether siege mode should default to deriving the right delimiter \ -from the input." +(defcustom siege-block-open ?{ + "Character signifying the start of an atomic block in delimiter input. \ +Such blocks will appear verbatim in the derived delimiter." :group 'siege-mode - :type 'bool) + :type 'char) -(defcustom siege-default-apply-regexs t - "Whether siege mode should default to deriving the matching delimiter \ -from the input using the regexs given in `siege-right-to-left-regexs' \ -and `siege-left-to-right-regexs'." +(defcustom siege-block-close ?} + "Character signifying the end of an atomic block in delimiter input. \ +Such blocks will appear verbatim in the derived delimiter." :group 'siege-mode - :type 'bool) + :type 'char) +(defcustom siege-block-prefix "\\\\begin{\\|{" + "Regex matching the start of an atomic block in the delimiter input, \ +`siege-block-open', or an optional prefix as well as that character. + +Such prefixes will have their relative orientation to the block preserved in \ +the derived delimiter, but will still be subject to the transformations given \ +in `siege-transform-regexs'." + :group 'siege-mode + :type 'string) + +(defcustom siege-default-derive t + "Whether siege-mode should default to deriving the matching delimiter \ +from the input or reproduce it on both sides verbatim." + :group 'siege-mode + :type 'bool) (defface siege-preview-face '((t :inherit (warning))) "Face in which to render delimiter previews for Siege mode." @@ -88,41 +103,137 @@ and `siege-left-to-right-regexs'." (defvar siege--overlay nil) (defvar siege--selected-window nil) -(defvar siege--apply-regexs t) -(defvar siege--is-left t) +(defvar siege--derive t) +(defvar siege--boundary "") (defvar siege--hist '()) -(defun siege--toggle-end () - "Swap the side for which the input is considered a delimiter and \ -from which the matching delimiter is derived, for \ -the currently running instance. See also `siege-default-left'." +(defun siege--toggle-derive () + "Toggle the automatic derivation of a matching delimiter for \ +the currently running instance. See also `siege-default-derive'." (interactive) - (setq siege--is-left (not siege--is-left)) - (minibuffer-message "Currently entering %s delimiter." - (if siege--is-left "LEFT" "RIGHT"))) + (setq siege--derive (not siege--derive)) + (minibuffer-message "%s right delimeter." + (if siege--derive "DERIVING" "NOT deriving"))) -(defun siege--toggle-regex () - "Toggle the use of regexs in deriving a matching delimiter for \ -the currently running instance. See also `siege-default-apply-regexs'." - (interactive) - (setq siege--apply-regexs (not siege--apply-regexs)) - (minibuffer-message "%s regexs on input." - (if siege--apply-regexs "APPLYING" "NOT applying"))) +(defun siege--extract-next-block (string start open-chr close-chr + prefix-match) + "Given a STRING and a START index, find the next block \ +delimeted by balanced pairs of OPEN-CHR CLOSE-CHR, optionally \ +prefixed by anything matching PREFIX-MATCH. + +PREFIX-MATCH is assumed to match against OPEN-CHR. Returns the list\ + (prefix-start block-start block-end)" + (let ((pos start) pre beg end (count 1) curr ) + (save-match-data + (when (setq pre (string-match prefix-match string start)) + (setq beg (1- (match-end 0)) + pos (match-end 0)) + (while (and (not end) (< pos (length string))) + (setq curr (aref string pos)) + (when (char-equal curr open-chr) (incf count)) + (when (char-equal curr close-chr) (decf count)) + (incf pos) + (when (= count 0) (setq end pos))))) + (when end (list pre beg end)))) + +(defun siege--tokenize-blocks (string open-char close-char + prefix-match) + "Given a STRING, produce a list of pairs (type . substring). Type is one of \ +'block or 'text. Blocks are substrings delimeted by balanced pairs of \ +OPEN-CHAR and CLOSE-CHAR, text is everything else. The list is in reverse \ +order of occurence, with the exception of block prefixes (see \ +`siege--extract-next-block' and PREFIX-MATCH) whose relative positioning \ +to the blocks the blocks they abut is preserved." + (let (result (start 0) block) + (while start + (if (setq block (siege--extract-next-block + string start open-char close-char prefix-match)) + (progn + (when (/= start (nth 0 block)) + (push (cons 'text + (substring-no-properties + string start (nth 0 block))) + result)) + (push (cons 'block (substring-no-properties + string (nth 1 block) (nth 2 block))) + result) + (when (/= (nth 0 block) (nth 1 block)) + (push (cons 'text (substring-no-properties + string (nth 0 block) (nth 1 block))) + result)) + (setq start (nth 2 block))) + (if (/= start (length string)) + (push (cons 'text (substring string start)) result)) + (setq start nil))) + result)) + +(defun siege--tokenize-non-block (string boundary-regex) + "Given a STRING, split the string on anything matching \ +BOUNDARY-REGEX and collate the results in reverse order. \ +Splitting tokens appear in output." + (let (result (start 0)) + (save-match-data + (while start + (if (string-match boundary-regex string start) + (progn + (when (/= start (match-beginning 0)) + (push (substring-no-properties + string start (match-beginning 0)) + result)) + (push (substring-no-properties + string (match-beginning 0) (match-end 0)) + result) + (setq start (match-end 0))) + (when (/= start (length string)) + (push (substring-no-properties string start) result)) + (setq start nil)))) + result)) + +(defun siege--derive-delimiter (string + block-open block-close + block-prefix-match + boundary-regex transforms) + "Given a STRING, BLOCK-OPEN and BLOCK-CLOSE characters, and a \ +BLOCK-PREFIX-MATCH regex matching against BLOCK-OPEN, this function first \ +tokenizes the string into blocks and text (see `siege--tokenize-blocks'). The \ +text is then further tokenized on boundaries given by BOUNDARY-REGEX (see \ +`siege--tokenize-non-block') and finally all text tokens (not blocks) are \ +transformed as follows and collated to generate the output. + +TRANSFORMS is a list of pairs of strings (regex . subst) whics is applied \ +sequentially to every text token." + (apply + #'concat + (mapcar + #'(lambda (pair) + (if (eq 'block (car pair)) (cdr pair) + (apply + #'concat + (mapcar + #'(lambda (str) + (let ((res str)) + (dolist (tr transforms res) + (setq + res + (replace-regexp-in-string (car tr) (cdr tr) res))))) + (siege--tokenize-non-block (cdr pair) boundary-regex))))) + (siege--tokenize-blocks string block-open block-close + block-prefix-match)))) + +(defun siege--make-boundary-regex-from-list (list) + "LIST is a list of strings which are concatenated, separated in the output \ +by regex optional match strings (\\|)" + (let ((res "")) (dolist (r list (substring res 0 (- (length res) 2))) + (setq res (concat res r "\\|"))))) (defun siege--matching-pair (input) - "Generate a matching delimiter from INPUT using \ -`siege-left-to-right-regexs' and `siege-right-to-left-regexs' \ -according to \ -`siege-default-left' and `siege-default-apply-regexs'" - (if siege--apply-regexs - (let ((pairs (if siege--is-left siege-left-to-right-regexs - siege-right-to-left-regexs)) - (output input)) - (cl-dolist (expr pairs) - (setq output - (replace-regexp-in-string (car expr) (cdr expr) output))) - (if siege--is-left (cons input output) - (cons output input))) + "Generate a matching delimiter from INPUT according to all siege-* variables." + (if siege--derive + (let ((output (siege--derive-delimiter + input siege-block-open siege-block-close + siege-block-prefix siege--boundary + siege-transform-regexs))) + (cons input output)) (cons input input))) (defun siege--preview-input () @@ -152,8 +263,7 @@ according to \ (defvar siege-minibuffer-map (let ((map minibuffer-local-ns-map)) - (define-key map (kbd "C-c r") 'siege--toggle-end) - (define-key map (kbd "C-c a") 'siege--toggle-regex) + (define-key map (kbd "C-c a") 'siege--toggle-derive) map)) (defun siege--interactive (initial) @@ -163,36 +273,35 @@ INITIAL is the string present in the minibuffer, which is not necessarily\ the default value." (barf-if-buffer-read-only) (setq siege--selected-window (selected-window) - siege--is-left siege-default-left - siege--apply-regexs siege-default-apply-regexs) - (exchange-point-and-mark) + siege--derive siege-default-derive + siege--boundary (siege--make-boundary-regex-from-list + siege-boundary-list)) (let* ((start (region-beginning)) (end (region-end)) - (default (or (car siege--hist) "")) (result (minibuffer-with-setup-hook (lambda () (add-hook 'post-command-hook #'siege--preview-input nil t) (add-hook 'minibuffer-exit-hook #'siege--preview-end nil t) (siege--preview-input)) - (read-from-minibuffer (format "Enter delimiter (default %s): " default) - initial siege-minibuffer-map - nil 'siege--hist default))) + (read-from-minibuffer + "Enter left delimiter: " + initial siege-minibuffer-map + nil 'siege--hist))) (pair (siege--matching-pair result))) (siege--preview-end) (goto-char start) (insert (car pair)) - (goto-char (+ end (length (cdr pair)))) + (goto-char (+ end (string-width (car pair)))) (insert (cdr pair)))) (defun siege--self-insert (arg) "This function replaces `self-insert-command'. -When the region is active, all input is redirected to the minibuffer \ -and treated as a delimiter for the region. By default the input \ -is used as the left delimiter from which the right one is \ -derived using `siege-left-to-right-regexs'. See also \ -`siege-default-left' and `siege-right-to-left-regexs'. ARG is \ -ignored. +When the region is active, all input is redirected to the \ +minibuffer and treated as a delimiter for the region. By default \ +the input is used as the left delimiter from which the right one \ +is derived according to all siege-* variables. See also \ +`siege--derive-delimiter'. ARG is ignored. If the region is not active then ARG is passed on to `self-insert-command'." (interactive "p") |
