;;; helm-ring.el --- kill-ring, mark-ring, and register browsers for helm. -*- lexical-binding: t -*- ;; Copyright (C) 2012 ~ 2023 Thierry Volpiatto ;; 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 . ;;; Code: (require 'cl-lib) (require 'helm) (require 'helm-utils) (require 'helm-help) (require 'helm-elisp) (declare-function undo-tree-restore-state-from-register "ext:undo-tree.el" (register)) (defgroup helm-ring nil "Ring related Applications and libraries for Helm." :group 'helm) (defcustom helm-kill-ring-threshold 3 "Minimum length of a candidate to be listed by `helm-source-kill-ring'." :type 'integer :group 'helm-ring) (defcustom helm-kill-ring-max-offset 400 "Max number of chars displayed per candidate in kill-ring browser. When `t', don't truncate candidate, show all. By default it is approximatively the number of bits contained in five lines of 80 chars each, i.e. 80*5. Note that if you set this to nil multiline will be disabled, i.e. you will not have separators between candidates any more." :type '(choice (const :tag "Disabled" t) (integer :tag "Max candidate offset")) :group 'helm-ring) (defcustom helm-kill-ring-actions '(("Yank marked" . helm-kill-ring-action-yank) ("Delete marked" . helm-kill-ring-action-delete) ("Search from candidate" . helm-kill-ring-search-from-string)) "List of actions for kill ring source." :group 'helm-ring :type '(alist :key-type string :value-type function)) (defcustom helm-kill-ring-separator "\n" "The separator used to separate marked candidates when yanking." :group 'helm-ring :type 'string) (defcustom helm-register-max-offset 160 "Max size of string register entries before truncating." :group 'helm-ring :type 'integer) ;;; Kill ring ;; ;; (defvar helm-kill-ring-map (let ((map (make-sparse-keymap))) (set-keymap-parent map helm-map) (define-key map (kbd "M-y") 'helm-next-line) (define-key map (kbd "M-u") 'helm-previous-line) (define-key map (kbd "M-D") 'helm-kill-ring-delete) (define-key map (kbd "C-s") 'helm-kill-ring-run-search-from-string) (define-key map (kbd "C-]") 'helm-kill-ring-toggle-truncated) (define-key map (kbd "C-c C-k") 'helm-kill-ring-kill-selection) (define-key map (kbd "C-c d") 'helm-kill-ring-run-persistent-delete) map) "Keymap for `helm-show-kill-ring'.") (defvar helm-source-kill-ring (helm-build-sync-source "Kill Ring" :init (lambda () (helm-set-attr 'last-command last-command) (helm-set-attr 'multiline helm-kill-ring-max-offset)) :candidates #'helm-kill-ring-candidates :filtered-candidate-transformer #'helm-kill-ring-transformer :action 'helm-kill-ring-actions :persistent-action 'ignore :help-message 'helm-kill-ring-help-message :persistent-help "DoNothing" :keymap helm-kill-ring-map :migemo t :multiline 'helm-kill-ring-max-offset :group 'helm-ring) "Source for browse and insert contents of kill-ring.") (defun helm-kill-ring-candidates () (cl-loop with cands = (helm-fast-remove-dups kill-ring :test 'equal) for kill in (if (eq (helm-get-attr 'last-command) 'yank) (cdr cands) cands) unless (or (< (length kill) helm-kill-ring-threshold) (string-match "\\`[\n[:blank:]]+\\'" kill)) collect kill)) (defun helm-kill-ring-transformer (candidates _source) "Ensure CANDIDATES are not read-only." (cl-loop for i in candidates when (get-text-property 0 'read-only i) do (set-text-properties 0 (length i) '(read-only nil) i) collect i)) (defvar helm-kill-ring--truncated-flag nil) (defun helm-kill-ring-toggle-truncated () "Toggle truncated view of candidates in helm kill-ring browser." (interactive) (with-helm-alive-p (setq helm-kill-ring--truncated-flag (not helm-kill-ring--truncated-flag)) (let* ((cur-cand (helm-get-selection)) (presel-fn (lambda () (helm-kill-ring--preselect-fn cur-cand)))) (helm-set-attr 'multiline (if helm-kill-ring--truncated-flag 15000000 helm-kill-ring-max-offset)) (helm-update presel-fn)))) (put 'helm-kill-ring-toggle-truncated 'helm-only t) (defun helm-kill-ring-kill-selection () "Store the real value of candidate in kill-ring. Same as `helm-kill-selection-and-quit' called with a prefix arg." (interactive) (helm-kill-selection-and-quit t)) (put 'helm-kill-ring-kill-selection 'helm-only t) (defun helm-kill-ring--preselect-fn (candidate) "Internal, used to preselect CANDIDATE when toggling truncated view." ;; Preselection by regexp may not work if candidate is huge, so walk ;; the helm buffer until selection is on CANDIDATE. (helm-awhile (condition-case-unless-debug nil (and (not (helm-pos-header-line-p)) (helm-get-selection)) (error nil)) (if (string= it candidate) (cl-return) (helm-next-line)))) (defun helm-kill-ring-action-yank (_str) "Insert concatenated marked candidates in current-buffer. When two prefix args are given prompt to choose separator, otherwise use `helm-kill-ring-separator' as default." (let ((marked (helm-marked-candidates)) (sep (if (equal helm-current-prefix-arg '(16)) (read-string "Separator: ") helm-kill-ring-separator))) (helm-kill-ring-action-yank-1 (cl-loop for c in (butlast marked) concat (concat c sep) into str finally return (concat str (car (last marked))))))) (defun helm-kill-ring-action-yank-1 (str) "Insert STR in `kill-ring' and set STR to the head. When called with a prefix arg, point and mark are exchanged without activating region. If this action is executed just after `yank', replace with STR as yanked string." (let ((yank-fn (lambda (&optional before yank-pop) (insert-for-yank str) ;; Set the window start back where it was in ;; the yank command, if possible. (when yank-pop (set-window-start (selected-window) yank-window-start t)) (when (or (equal helm-current-prefix-arg '(4)) before) ;; Same as exchange-point-and-mark but without ;; activating region. (goto-char (prog1 (mark t) (set-marker (mark-marker) (point) helm-current-buffer))))))) ;; Prevent inserting and saving highlighted items. (set-text-properties 0 (length str) nil str) (with-helm-current-buffer (unwind-protect (progn (setq kill-ring (delete str kill-ring)) ;; Adding a `delete-selection' property ;; to `helm-kill-ring-action' is not working ;; because `this-command' will be `helm-maybe-exit-minibuffer', ;; so use this workaround (Bug#1520). (when (and (region-active-p) delete-selection-mode) (delete-region (region-beginning) (region-end))) (if (not (eq (helm-get-attr 'last-command helm-source-kill-ring) 'yank)) (progn ;; Ensure mark is at beginning of inserted text. (push-mark) ;; When yanking in a helm minibuffer we need a small ;; delay to detect the mark in previous minibuffer. [1] (run-with-timer 0.01 nil yank-fn)) ;; from `yank-pop' (let ((inhibit-read-only t) (before (< (point) (mark t)))) (if before (funcall (or yank-undo-function 'delete-region) (point) (mark t)) (funcall (or yank-undo-function 'delete-region) (mark t) (point))) (setq yank-undo-function nil) (set-marker (mark-marker) (point) helm-current-buffer) ;; Same as [1] but use the same mark and point as in ;; the initial yank according to BEFORE even if no ;; prefix arg is given. (run-with-timer 0.01 nil yank-fn before 'pop)))) (kill-new str))))) (define-obsolete-function-alias 'helm-kill-ring-action 'helm-kill-ring-action-yank "2.4.0") (defun helm-kill-ring-search-from-string (candidate) (let ((str (car (split-string candidate "\n")))) (helm-multi-occur-1 (list (current-buffer)) (regexp-quote (substring-no-properties str))))) (helm-make-command-from-action helm-kill-ring-run-search-from-string "Run helm-occur from kill ring." 'helm-kill-ring-search-from-string) (defun helm-kill-ring-action-delete (_candidate) "Delete marked candidates from `kill-ring'." (cl-loop for c in (helm-marked-candidates) do (setq kill-ring (delete c kill-ring)))) (defun helm-kill-ring-persistent-delete (_candidate) (unwind-protect (cl-loop for c in (helm-marked-candidates) do (progn (helm-preselect (format "^%s" (regexp-quote c))) (setq kill-ring (delete c kill-ring)) (helm-delete-current-selection) (helm--remove-marked-and-update-mode-line c))) (with-helm-buffer (setq helm-marked-candidates nil helm-visible-mark-overlays nil)) (helm-force-update (helm-aif (helm-get-selection nil t) (regexp-quote it))))) (helm-make-persistent-command-from-action helm-kill-ring-run-persistent-delete "Delete current candidate without quitting." 'quick-delete 'helm-kill-ring-persistent-delete) (helm-make-command-from-action helm-kill-ring-delete "Delete marked candidates from `kill-ring'." 'helm-kill-ring-action-delete) ;;;; ;; DO NOT use these sources with other sources use ;; the commands `helm-mark-ring', `helm-global-mark-ring' or ;; `helm-all-mark-rings' instead. (defun helm-mark-ring-line-string-at-pos (pos) "Return line string at position POS." (save-excursion (goto-char pos) (forward-line 0) (let ((line (car (split-string (thing-at-point 'line) "[\n\r]")))) (remove-text-properties 0 (length line) '(read-only) line) (if (string= "" line) "" line)))) (defun helm-mark-ring-get-candidates () (with-helm-current-buffer (cl-loop with marks = (if (mark t) (cons (mark-marker) mark-ring) mark-ring) for marker in marks with max-line-number = (line-number-at-pos (point-max)) with width = (length (number-to-string max-line-number)) for m = (format (concat "%" (number-to-string width) "d: %s") (line-number-at-pos marker) (helm-mark-ring-line-string-at-pos marker)) unless (and recip (assoc m recip)) collect (cons m marker) into recip finally return recip))) (defun helm-mark-ring-default-action (candidate) (let ((target (copy-marker candidate))) (helm-aif (marker-buffer candidate) (progn (switch-to-buffer it) (helm-log-run-hook "helm-mark-ring-default-action" 'helm-goto-line-before-hook) (helm-match-line-cleanup) (with-helm-current-buffer (unless helm-yank-point (setq helm-yank-point (point)))) (helm-goto-char target) (helm-highlight-current-line)) ;; marker points to no buffer, no need to dereference it, just ;; delete it. (setq mark-ring (delete target mark-ring)) (error "Marker points to no buffer")))) (defvar helm-source-mark-ring (helm-build-sync-source "mark-ring" :candidates #'helm-mark-ring-get-candidates :action '(("Goto line" . helm-mark-ring-default-action)) :persistent-help "Show this line" :group 'helm-ring)) ;;; Global-mark-ring (defvar helm-source-global-mark-ring (helm-build-sync-source "global-mark-ring" :candidates #'helm-global-mark-ring-get-candidates :action '(("Goto line" . helm-mark-ring-default-action)) :persistent-help "Show this line" :group 'helm-ring)) (defun helm-global-mark-ring-format-buffer (marker) (with-current-buffer (marker-buffer marker) (goto-char marker) (forward-line 0) (let ((line (pcase (thing-at-point 'line) ((and line (pred stringp) (guard (not (string-match-p "\\`\n?\\'" line)))) (car (split-string line "[\n\r]"))) (_ "")))) (remove-text-properties 0 (length line) '(read-only) line) (format "%7d:%s: %s" (line-number-at-pos) (marker-buffer marker) line)))) (defun helm-global-mark-ring-get-candidates () (let ((marks global-mark-ring)) (when marks (cl-loop for marker in marks for mb = (marker-buffer marker) for gm = (unless (or (string-match "^ " (format "%s" mb)) (null mb)) (helm-global-mark-ring-format-buffer marker)) when (and gm (not (assoc gm recip))) collect (cons gm marker) into recip finally return recip)))) ;;;; ;;; Insert from register (defvar helm-source-register (helm-build-sync-source "Registers" :candidates #'helm-register-candidates :action-transformer #'helm-register-action-transformer :persistent-help "" :multiline t :action '(("Delete Register(s)" . (lambda (_candidate) (cl-loop for candidate in (helm-marked-candidates) for register = (car candidate) do (setq register-alist (delq (assoc register register-alist) register-alist)))))) :group 'helm-ring) "See (info \"(emacs)Registers\")") (defun helm-register-candidates () "Collecting register contents and appropriate commands." (cl-loop for (char . rval) in register-alist for key = (single-key-description char) for e27 = (registerv-p rval) for val = (if e27 ; emacs-27 (registerv-data rval) rval) for string-actions = (cond ((numberp val) (list (int-to-string val) 'insert-register 'increment-register)) ((markerp val) (let ((buf (marker-buffer val))) (if (null buf) (list "a marker in no buffer") (list (concat "a buffer position:" (buffer-name buf) ", position " (int-to-string (marker-position val))) 'jump-to-register 'insert-register)))) ((and (consp val) (window-configuration-p (car val))) (list "window configuration." 'jump-to-register)) ((and (vectorp val) (fboundp 'undo-tree-register-data-p) (undo-tree-register-data-p (if e27 val (elt val 1)))) (list "Undo-tree entry." 'undo-tree-restore-state-from-register)) ((or (and (vectorp val) (eq 'registerv (aref val 0))) (and (consp val) (frame-configuration-p (car val)))) (list "frame configuration." 'jump-to-register)) ((and (consp val) (eq (car val) 'file)) (list (concat "file:" (prin1-to-string (cdr val)) ".") 'jump-to-register)) ((and (consp val) (eq (car val) 'file-query)) (list (concat "file:a file-query reference: file " (car (cdr val)) ", position " (int-to-string (car (cdr (cdr val)))) ".") 'jump-to-register)) ((consp val) (let ((lines (format "%4d" (length val)))) (list (format "%s: %s\n" lines (truncate-string-to-width (mapconcat 'identity (list (car val)) "^J") (- (window-width) 15))) 'insert-register))) ((stringp val) (list (concat (substring-no-properties val 0 (min (length val) helm-register-max-offset)) (if (> (length val) helm-register-max-offset) "[...]" "")) 'insert-register 'kill-new 'append-to-register 'prepend-to-register))) unless (null string-actions) ; Fix Bug#1107. collect (cons (format "Register %3s:\n %s" key (car string-actions)) (cons char (cdr string-actions))))) (defun helm-register-action-transformer (actions register-and-functions) "Decide actions by the contents of register." (cl-loop with func-actions = '((insert-register "Insert Register" . (lambda (c) (insert-register (car c)))) (kill-new "Kill Register" . (lambda (c) (with-temp-buffer (insert-register (car c)) (kill-new (buffer-string))))) (jump-to-register "Jump to Register" . (lambda (c) (jump-to-register (car c)))) (append-to-register "Append Region to Register" . (lambda (c) (append-to-register (car c) (region-beginning) (region-end)))) (prepend-to-register "Prepend Region to Register" . (lambda (c) (prepend-to-register (car c) (region-beginning) (region-end)))) (increment-register "Increment Prefix Arg to Register" . (lambda (c) (increment-register helm-current-prefix-arg (car c)))) (undo-tree-restore-state-from-register "Restore Undo-tree register" . (lambda (c) (and (fboundp 'undo-tree-restore-state-from-register) (undo-tree-restore-state-from-register (car c)))))) for func in (cdr register-and-functions) when (assq func func-actions) collect (cdr it) into transformer-actions finally return (append transformer-actions actions))) ;;;###autoload (defun helm-mark-ring () "Preconfigured `helm' for `helm-source-mark-ring'." (interactive) (helm :sources 'helm-source-mark-ring :resume 'noresume :buffer "*helm mark*")) ;;;###autoload (defun helm-global-mark-ring () "Preconfigured `helm' for `helm-source-global-mark-ring'." (interactive) (helm :sources 'helm-source-global-mark-ring :resume 'noresume :buffer "*helm global mark*")) ;;;###autoload (defun helm-all-mark-rings () "Preconfigured `helm' for mark rings. Source used are `helm-source-global-mark-ring' and `helm-source-mark-ring'." (interactive) (helm :sources '(helm-source-mark-ring helm-source-global-mark-ring) :resume 'noresume :buffer "*helm mark ring*")) ;;;###autoload (defun helm-register () "Preconfigured `helm' for Emacs registers." (interactive) (helm :sources 'helm-source-register :resume 'noresume :buffer "*helm register*")) ;;;###autoload (defun helm-show-kill-ring () "Preconfigured `helm' for `kill-ring'. It is drop-in replacement of `yank-pop'. First call open the kill-ring browser, next calls move to next line." (interactive) (setq helm-kill-ring--truncated-flag nil) (let ((enable-recursive-minibuffers t)) (helm :sources helm-source-kill-ring :buffer "*helm kill ring*" :resume 'noresume :allow-nest t))) ;;;###autoload (defun helm-execute-kmacro () "Preconfigured helm for keyboard macros. Define your macros with `f3' and `f4'. See (info \"(emacs) Keyboard Macros\") for detailed infos. This command is useful when used with persistent action." (interactive) (let ((helm-quit-if-no-candidate (lambda () (message "No kbd macro has been defined")))) (helm :sources (helm-build-sync-source "Kmacro" :candidates (lambda () (helm-fast-remove-dups (cons (kmacro-ring-head) kmacro-ring) :test 'equal)) :multiline t :candidate-transformer (lambda (candidates) (cl-loop for c in candidates collect (propertize (help-key-description (car c) nil) 'helm-realvalue c))) :persistent-help "Execute kmacro" :help-message 'helm-kmacro-help-message :action (helm-make-actions "Execute kmacro (`C-u ' to execute times)" 'helm-kbd-macro-execute "Concat marked macros" 'helm-kbd-macro-concat-macros "Delete marked macros" 'helm-kbd-macro-delete-macro "Edit marked macro" 'helm-kbd-macro-edit-macro) :group 'helm-ring) :buffer "*helm kmacro*"))) (defun helm-kbd-macro-execute (candidate) ;; Move candidate on top of list for next use. (setq kmacro-ring (delete candidate kmacro-ring)) (kmacro-push-ring) (kmacro-split-ring-element candidate) (kmacro-exec-ring-item candidate helm-current-prefix-arg)) (defun helm-kbd-macro-concat-macros (_candidate) (let ((mkd (helm-marked-candidates))) (when (cdr mkd) (kmacro-push-ring) (setq last-kbd-macro (mapconcat 'identity (cl-loop for km in mkd if (vectorp km) append (cl-loop for k across km collect (key-description (vector k))) into result else collect (car km) into result finally return result) ""))))) (defun helm-kbd-macro-delete-macro (_candidate) (let ((mkd (helm-marked-candidates))) (kmacro-push-ring) (cl-loop for km in mkd do (setq kmacro-ring (delete km kmacro-ring))) (kmacro-pop-ring1))) (defun helm-kbd-macro-edit-macro (candidate) (kmacro-push-ring) (setq kmacro-ring (delete candidate kmacro-ring)) (kmacro-split-ring-element candidate) (kmacro-edit-macro)) (provide 'helm-ring) ;;; helm-ring.el ends here