;;; expand-region-core.el --- Increase selected region by semantic units. -*- lexical-binding: t; -*- ;; Copyright (C) 2011-2023 Free Software Foundation, Inc ;; Author: Magnar Sveen ;; Keywords: marking region ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; The core functionality of expand-region. ;; See README.md ;;; Code: (require 'expand-region-custom) (declare-function er/expand-region "expand-region") (defvar er/history '() "A history of start and end points so we can contract after expanding.") ;; history is always local to a single buffer (make-variable-buffer-local 'er/history) (defvar er--space-str " \t\n") (defvar er--blank-list (append er--space-str nil)) (defvar er--show-expansion-message nil) (defvar er/try-expand-list nil "A list of functions that are tried when expanding.") (defvar er/save-mode-excursion nil "A function to save excursion state when expanding.") (defsubst er--first-invocation () "t if this is the first invocation of `er/expand-region' or `er/contract-region'." (not (memq last-command '(er/expand-region er/contract-region)))) (defun er--prepare-expanding () (when (and (er--first-invocation) (not (use-region-p))) (push-mark nil t) ;; one for keeping starting position (push-mark nil t)) ;; one for replace by set-mark in expansions (when (not transient-mark-mode) (setq-local transient-mark-mode (cons 'only transient-mark-mode)))) (defun er--copy-region-to-register () (when (and (stringp expand-region-autocopy-register) (> (length expand-region-autocopy-register) 0)) (set-register (aref expand-region-autocopy-register 0) (filter-buffer-substring (region-beginning) (region-end))))) ;; save-mark-and-excursion in Emacs 25 works like save-excursion did before (eval-when-compile (when (< emacs-major-version 25) (defmacro save-mark-and-excursion (&rest body) `(save-excursion ,@body)))) (defmacro er--save-excursion (&rest body) `(let ((action (lambda () (save-mark-and-excursion ,@body)))) (if er/save-mode-excursion (funcall er/save-mode-excursion action) (funcall action)))) (defun er--expand-region-1 () "Increase selected region by semantic units. Basically it runs all the mark-functions in `er/try-expand-list' and chooses the one that increases the size of the region while moving point or mark as little as possible." (let* ((p1 (point)) (p2 (if (use-region-p) (mark) (point))) (start (min p1 p2)) (end (max p1 p2)) (try-list er/try-expand-list) (best-start (point-min)) (best-end (point-max)) ;; (set-mark-default-inactive nil) ) ;; add hook to clear history on buffer changes (unless er/history (add-hook 'after-change-functions #'er/clear-history t t)) ;; remember the start and end points so we can contract later ;; unless we're already at maximum size (unless (and (= start best-start) (= end best-end)) (push (cons p1 p2) er/history)) (when (and expand-region-skip-whitespace (er--point-is-surrounded-by-white-space) (= start end)) (skip-chars-forward er--space-str) (setq start (point))) (while try-list (er--save-excursion (ignore-errors (funcall (car try-list)) (when (and (region-active-p) (er--this-expansion-is-better start end best-start best-end)) (setq best-start (point)) (setq best-end (mark)) (when (and er--show-expansion-message (not (minibufferp))) (message "%S" (car try-list)))))) (setq try-list (cdr try-list))) (setq deactivate-mark nil) ;; if smart cursor enabled, decide to put it at start or end of region: (if (and expand-region-smart-cursor (not (= start best-start))) (progn (goto-char best-end) (set-mark best-start)) (goto-char best-start) (set-mark best-end)) (er--copy-region-to-register) (when (and (= best-start (point-min)) (= best-end (point-max))) ;; We didn't find anything new, so exit early 'early-exit))) (defun er--this-expansion-is-better (start end best-start best-end) "t if the current region is an improvement on previous expansions. This is provided as a separate function for those that would like to override the heuristic." (and (<= (point) start) (>= (mark) end) (> (- (mark) (point)) (- end start)) (or (> (point) best-start) (and (= (point) best-start) (< (mark) best-end))))) ;;;###autoload (defun er/contract-region (arg) "Contract the selected region to its previous size. With prefix argument contracts that many times. If prefix argument is negative calls `er/expand-region'. If prefix argument is 0 it resets point and mark to their state before calling `er/expand-region' for the first time." (interactive "p") (if (< arg 0) (er/expand-region (- arg)) (when er/history ;; Be sure to reset them all if called with 0 (when (= arg 0) (setq arg (length er/history))) (when (not transient-mark-mode) (setq-local transient-mark-mode (cons 'only transient-mark-mode))) ;; Advance through the list the desired distance (while (and (cdr er/history) (> arg 1)) (setq arg (- arg 1)) (setq er/history (cdr er/history))) ;; Reset point and mark (let* ((last (pop er/history)) (start (car last)) (end (cdr last))) (goto-char start) (set-mark end) (er--copy-region-to-register) (when (eq start end) (deactivate-mark) (er/clear-history)))))) (defun er/prepare-for-more-expansions-internal (repeat-key-str) "Return bindings and a message to inform user about them" (let ((msg (format "Type %s to expand again" repeat-key-str)) (bindings (list (cons repeat-key-str '(er/expand-region 1))))) ;; If contract and expand are on the same binding, ignore contract (unless (string-equal repeat-key-str expand-region-contract-fast-key) (setq msg (concat msg (format ", %s to contract" expand-region-contract-fast-key))) (push (cons expand-region-contract-fast-key '(er/contract-region 1)) bindings)) ;; If reset and either expand or contract are on the same binding, ignore reset (unless (or (string-equal repeat-key-str expand-region-reset-fast-key) (string-equal expand-region-contract-fast-key expand-region-reset-fast-key)) (setq msg (concat msg (format ", %s to reset" expand-region-reset-fast-key))) (push (cons expand-region-reset-fast-key '(er/expand-region 0)) bindings)) (cons msg bindings))) (defun er/prepare-for-more-expansions () "Let one expand more by just pressing the last key." (let* ((repeat-key (event-basic-type last-input-event)) (repeat-key-str (single-key-description repeat-key)) (msg-and-bindings (er/prepare-for-more-expansions-internal repeat-key-str)) (msg (car msg-and-bindings)) (bindings (cdr msg-and-bindings))) (when repeat-key (er/set-temporary-overlay-map (let ((map (make-sparse-keymap))) (dolist (binding bindings map) (define-key map (read-kbd-macro (car binding)) `(lambda () (interactive) (setq this-command `,(cadr ',binding)) (or (not expand-region-show-usage-message) (minibufferp) (message "%s" ,msg)) (eval `,(cdr ',binding)))))) t) (or (not expand-region-show-usage-message) (minibufferp) (message "%s" msg))))) (defalias 'er/set-temporary-overlay-map (if (fboundp 'set-temporary-overlay-map) ;Emacs≄24.3 #'set-temporary-overlay-map ;; Backport this function from newer emacs versions (lambda (map &optional keep-pred) "Set a new keymap that will only exist for a short period of time. The new keymap to use must be given in the MAP variable. When to remove the keymap depends on user input and KEEP-PRED: - if KEEP-PRED is nil (the default), the keymap disappears as soon as any key is pressed, whether or not the key is in MAP; - if KEEP-PRED is t, the keymap disappears as soon as a key *not* in MAP is pressed; - otherwise, KEEP-PRED must be a 0-arguments predicate that will decide if the keymap should be removed (if predicate returns nil) or kept (otherwise). The predicate will be called after each key sequence." (let* ((clearfunsym (make-symbol "clear-temporary-overlay-map")) (overlaysym (make-symbol "t")) (alist (list (cons overlaysym map))) (clearfun `(lambda () (unless ,(cond ((null keep-pred) nil) ((eq t keep-pred) `(eq this-command (lookup-key ',map (this-command-keys-vector)))) (t `(funcall ',keep-pred))) (remove-hook 'pre-command-hook ',clearfunsym) (setq emulation-mode-map-alists (delq ',alist emulation-mode-map-alists)))))) (set overlaysym overlaysym) (fset clearfunsym clearfun) (add-hook 'pre-command-hook clearfunsym) (push alist emulation-mode-map-alists))))) (advice-add 'keyboard-quit :before #'er--collapse-region-before) (advice-add 'cua-cancel :before #'er--collapse-region-before) (defun er--collapse-region-before (&rest _) ;; FIXME: Re-use `er--first-invocation'? (when (memq last-command '(er/expand-region er/contract-region)) (er/contract-region 0))) (advice-add 'minibuffer-keyboard-quit :around #'er--collapse-region-minibuffer-keyboard-quit) (defun er--collapse-region-minibuffer-keyboard-quit (orig-fun &rest args) ;; FIXME: Re-use `er--first-invocation'? (if (memq last-command '(er/expand-region er/contract-region)) (er/contract-region 0) (apply orig-fun args))) (defun er/clear-history (&rest _) "Clear the history." (setq er/history '()) (remove-hook 'after-change-functions #'er/clear-history t)) (defun er--point-is-surrounded-by-white-space () (and (or (memq (char-before) er--blank-list) (eq (point) (point-min))) (memq (char-after) er--blank-list))) (defun er/enable-mode-expansions (mode add-fn) (add-hook (intern (format "%s-hook" mode)) add-fn) (save-window-excursion ;; FIXME: Why? (dolist (buffer (buffer-list)) (with-current-buffer buffer (when (derived-mode-p mode) (funcall add-fn)))))) (defun er/enable-minor-mode-expansions (mode add-fn) (add-hook (intern (format "%s-hook" mode)) add-fn) (save-window-excursion (dolist (buffer (buffer-list)) (with-current-buffer buffer (when (symbol-value mode) (funcall add-fn)))))) ;; Some more performant version of `looking-back' (defun er/looking-back-on-line (regexp) "Version of `looking-back' that only checks current line." (looking-back regexp (line-beginning-position))) (defun er/looking-back-exact (s) "Version of `looking-back' that only looks for exact matches, no regexp." (string= s (buffer-substring (- (point) (length s)) (point)))) (defun er/looking-back-max (regexp count) "Version of `looking-back' that only check COUNT chars back." (looking-back regexp (max 1 (- (point) count)))) (provide 'expand-region-core) ;;; expand-region-core.el ends here