399 lines
13 KiB
EmacsLisp
399 lines
13 KiB
EmacsLisp
;;; semantic/mru-bookmark.el --- Automatic bookmark tracking -*- lexical-binding: t; -*-
|
|
|
|
;; Copyright (C) 2007-2024 Free Software Foundation, Inc.
|
|
|
|
;; Author: Eric M. Ludlam <zappo@gnu.org>
|
|
|
|
;; This file is part of GNU Emacs.
|
|
|
|
;; GNU Emacs 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.
|
|
|
|
;; GNU Emacs 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 GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
;;
|
|
;; Using editing hooks, track the most recently visited or poked tags,
|
|
;; and keep a list of them, with the current point in from, and sorted
|
|
;; by most recently used.
|
|
;;
|
|
;; I envision this would be used in place of switch-buffers once
|
|
;; someone got the hang of it.
|
|
;;
|
|
;; I'd also like to see this used to provide some nice defaults for
|
|
;; other programs where logical destinations or targets are the tags
|
|
;; that have been recently edited.
|
|
;;
|
|
;; Quick Start:
|
|
;;
|
|
;; M-x global-semantic-mru-bookmark-mode RET
|
|
;;
|
|
;; < edit some code >
|
|
;;
|
|
;; C-x B <select a tag name> RET
|
|
;;
|
|
;; In the above, the history is pre-filled with the tags you recently
|
|
;; edited in the order you edited them.
|
|
|
|
;;; Code:
|
|
|
|
(require 'semantic)
|
|
(require 'eieio-base)
|
|
(require 'ring)
|
|
|
|
(declare-function data-debug-new-buffer "data-debug")
|
|
(declare-function data-debug-insert-object-slots "eieio-datadebug")
|
|
(declare-function semantic-momentary-highlight-tag "semantic/decorate")
|
|
(declare-function semantic-tag-similar-p "semantic/tag-ls")
|
|
|
|
;;; TRACKING CORE
|
|
;;
|
|
;; Data structure for tracking MRU tag locations
|
|
|
|
(defclass semantic-bookmark (eieio-named)
|
|
((tag :initarg :tag
|
|
:type semantic-tag
|
|
:documentation "The TAG this bookmark belongs to.")
|
|
(parent :type (or semantic-tag null)
|
|
:documentation "The tag that is the parent of :tag.")
|
|
(offset :type number
|
|
:documentation "The offset from `tag' start that is
|
|
somehow interesting.")
|
|
(filename :type string
|
|
:documentation "String the tag belongs to.
|
|
Set this when the tag gets unlinked from the buffer it belongs to.")
|
|
(frequency :type number
|
|
:initform 0
|
|
:documentation "Track the frequency this tag is visited.")
|
|
(reason :type symbol
|
|
:initform t
|
|
:documentation
|
|
"The reason this tag is interesting.
|
|
Nice values include the following:
|
|
edit - created because the tag text was edited.
|
|
read - created because point lingered in tag text.
|
|
jump - jumped to another tag from this tag.
|
|
mark - created a regular mark in this tag.")
|
|
)
|
|
"A single bookmark.")
|
|
|
|
(cl-defmethod initialize-instance :after ((sbm semantic-bookmark) &rest _fields)
|
|
"Initialize the bookmark SBM with details about :tag."
|
|
(condition-case nil
|
|
(save-excursion
|
|
(oset sbm filename (semantic-tag-file-name (oref sbm tag)))
|
|
(semantic-go-to-tag (oref sbm tag))
|
|
(oset sbm parent (semantic-current-tag-parent)))
|
|
(error (message "Error bookmarking tag.")))
|
|
)
|
|
|
|
(cl-defmethod semantic-mrub-visit ((sbm semantic-bookmark))
|
|
"Visit the semantic tag bookmark SBM.
|
|
Uses `semantic-go-to-tag' and highlighting."
|
|
(require 'semantic/decorate)
|
|
(with-slots (tag filename) sbm
|
|
;; Go to the tag
|
|
(when (not (semantic-tag-in-buffer-p tag))
|
|
(let ((fn (or (semantic-tag-file-name tag)
|
|
filename)))
|
|
(set-buffer (find-file-noselect fn))))
|
|
(semantic-go-to-tag (oref sbm tag) (oref sbm parent))
|
|
;; Go back to the offset.
|
|
(condition-case nil
|
|
(let ((o (oref sbm offset)))
|
|
(forward-char o))
|
|
(error nil))
|
|
;; make it visible
|
|
(pop-to-buffer-same-window (current-buffer))
|
|
(semantic-momentary-highlight-tag tag)
|
|
))
|
|
|
|
(cl-defmethod semantic-mrub-update ((sbm semantic-bookmark) point reason)
|
|
"Update the existing bookmark SBM.
|
|
POINT is some important location.
|
|
REASON is a symbol. See slot `reason' on `semantic-bookmark'."
|
|
(condition-case nil
|
|
(progn
|
|
(with-slots (tag offset frequency) sbm
|
|
(setq offset (- point (semantic-tag-start tag)))
|
|
(setq frequency (1+ frequency))
|
|
)
|
|
(oset sbm reason reason))
|
|
;; This can fail on XEmacs at miscellaneous times.
|
|
(error nil))
|
|
)
|
|
|
|
(cl-defmethod semantic-mrub-preflush ((sbm semantic-bookmark))
|
|
"Method called on a tag before the current buffer list of tags is flushed.
|
|
If there is a buffer match, unlink the tag."
|
|
(let ((tag (oref sbm tag))
|
|
(parent (when (slot-boundp sbm 'parent)
|
|
(oref sbm parent))))
|
|
(let ((b (semantic-tag-in-buffer-p tag)))
|
|
(when (and b (eq b (current-buffer)))
|
|
(semantic--tag-unlink-from-buffer tag)))
|
|
|
|
(when parent
|
|
(let ((b (semantic-tag-in-buffer-p parent)))
|
|
(when (and b (eq b (current-buffer)))
|
|
(semantic--tag-unlink-from-buffer parent))))))
|
|
|
|
(defclass semantic-bookmark-ring ()
|
|
((ring :initarg :ring
|
|
:type ring
|
|
:documentation
|
|
"List of `semantic-bookmark' objects.
|
|
This list is maintained as a list with the first item
|
|
being the current location, and the rest being a list of
|
|
items that were recently visited.")
|
|
(current-index :initform 0
|
|
:type number
|
|
:documentation
|
|
"The current index into RING for some operation.
|
|
User commands use this to move through the ring, or reset.")
|
|
)
|
|
"Track the current MRU stack of bookmarks.
|
|
We can't use the built-in ring data structure because we need
|
|
to delete some items from the ring when we don't have the data.")
|
|
|
|
(defvar semantic-mru-bookmark-ring (semantic-bookmark-ring
|
|
:ring (make-ring 20))
|
|
"The MRU bookmark ring.
|
|
This ring tracks the most recent active tags of interest.")
|
|
|
|
(defun semantic-mrub-find-nearby-tag (point)
|
|
"Find a nearby tag to be pushed for this current location.
|
|
Argument POINT is where to find the tag near."
|
|
;; I thought this was a good idea, but it is not!
|
|
;;(semantic-fetch-tags) ;; Make sure everything is up-to-date.
|
|
(let ((tag (semantic-current-tag)))
|
|
(when (or (not tag) (semantic-tag-of-class-p tag 'type))
|
|
(let ((nearby (or (semantic-find-tag-by-overlay-next point)
|
|
(semantic-find-tag-by-overlay-prev point))))
|
|
(when nearby (setq tag nearby))))
|
|
tag))
|
|
|
|
(cl-defmethod semantic-mrub-push ((sbr semantic-bookmark-ring) point
|
|
&optional reason)
|
|
"Add a bookmark to the ring SBR from POINT.
|
|
REASON is why it is being pushed. See doc for `semantic-bookmark'
|
|
for possible reasons.
|
|
The resulting bookmark is then sorted within the ring."
|
|
(let* ((ring (oref sbr ring))
|
|
(tag (semantic-mrub-find-nearby-tag (point)))
|
|
(idx 0))
|
|
(when tag
|
|
(while (and (not (ring-empty-p ring)) (< idx (ring-size ring)))
|
|
(if (semantic-tag-similar-p (oref (ring-ref ring idx) tag)
|
|
tag)
|
|
(ring-remove ring idx))
|
|
(setq idx (1+ idx)))
|
|
;; Create a new mark
|
|
(let ((sbm (semantic-bookmark (semantic-tag-name tag)
|
|
:tag tag)))
|
|
;; Take the mark, and update it for the current state.
|
|
(ring-insert ring sbm)
|
|
(semantic-mrub-update sbm point reason))
|
|
)))
|
|
|
|
(defun semantic-mrub-cache-flush-fcn ()
|
|
"Function called in the `semantic-before-toplevel-cache-flush-hook'.
|
|
Cause tags in the ring to become unlinked."
|
|
(let* ((ring (oref semantic-mru-bookmark-ring ring))
|
|
(len (ring-length ring))
|
|
(idx 0)
|
|
)
|
|
(while (< idx len)
|
|
(semantic-mrub-preflush (ring-ref ring idx))
|
|
(setq idx (1+ idx)))))
|
|
|
|
(add-hook 'semantic-before-toplevel-cache-flush-hook
|
|
#'semantic-mrub-cache-flush-fcn)
|
|
|
|
;;; EDIT tracker
|
|
;;
|
|
(defvar semantic-mrub-last-overlay nil
|
|
"The last overlay bumped by `semantic-mru-bookmark-change-hook-fcn'.")
|
|
|
|
(defun semantic-mru-bookmark-change-hook-fcn (overlay)
|
|
"Function set into `semantic-edits-new/move-change-hook's.
|
|
Argument OVERLAY is the overlay created to mark the change.
|
|
This function pushes tags onto the tag ring."
|
|
;; Dup?
|
|
(when (not (eq overlay semantic-mrub-last-overlay))
|
|
(setq semantic-mrub-last-overlay overlay)
|
|
(semantic-mrub-push semantic-mru-bookmark-ring
|
|
(point)
|
|
'edit)))
|
|
|
|
;;; MINOR MODE
|
|
;;
|
|
;; Tracking minor mode.
|
|
|
|
(defcustom global-semantic-mru-bookmark-mode nil
|
|
"If non-nil, enable `semantic-mru-bookmark-mode' globally.
|
|
When this mode is enabled, Emacs keeps track of which tags have
|
|
been edited, and you can re-visit them with \\[semantic-mrub-switch-tags]."
|
|
:group 'semantic
|
|
:group 'semantic-modes
|
|
:type 'boolean
|
|
:require 'semantic/util-modes
|
|
:initialize #'custom-initialize-default
|
|
:set (lambda (_sym val)
|
|
(global-semantic-mru-bookmark-mode (if val 1 -1))))
|
|
|
|
;;;###autoload
|
|
(define-minor-mode global-semantic-mru-bookmark-mode
|
|
"Toggle global use of option `semantic-mru-bookmark-mode'."
|
|
:global t :group 'semantic :group 'semantic-modes
|
|
;; Not needed because it's autoloaded instead.
|
|
;; :require 'semantic-util-modes
|
|
(semantic-toggle-minor-mode-globally
|
|
'semantic-mru-bookmark-mode (if global-semantic-mru-bookmark-mode 1 -1)))
|
|
|
|
(defcustom semantic-mru-bookmark-mode-hook nil
|
|
"Hook run at the end of function `semantic-mru-bookmark-mode'."
|
|
:group 'semantic
|
|
:type 'hook)
|
|
|
|
(defvar-keymap semantic-mru-bookmark-mode-map
|
|
:doc "Keymap for mru-bookmark minor mode."
|
|
"C-x B" #'semantic-mrub-switch-tags)
|
|
|
|
(define-minor-mode semantic-mru-bookmark-mode
|
|
"Minor mode for tracking tag-based bookmarks automatically.
|
|
When this mode is enabled, Emacs keeps track of which tags have
|
|
been edited, and you can re-visit them with \\[semantic-mrub-switch-tags].
|
|
|
|
\\{semantic-mru-bookmark-mode-map}
|
|
|
|
The minor mode can be turned on only if semantic feature is
|
|
available and the current buffer was set up for parsing. Return
|
|
non-nil if the minor mode is enabled."
|
|
:keymap semantic-mru-bookmark-mode-map
|
|
(if semantic-mru-bookmark-mode
|
|
(if (not (and (featurep 'semantic) (semantic-active-p)))
|
|
(progn
|
|
;; Disable minor mode if semantic stuff not available
|
|
(setq semantic-mru-bookmark-mode nil)
|
|
(error "Buffer %s was not set up for parsing"
|
|
(buffer-name)))
|
|
(add-hook 'semantic-edits-new-change-functions
|
|
#'semantic-mru-bookmark-change-hook-fcn nil t)
|
|
(add-hook 'semantic-edits-move-change-hooks
|
|
#'semantic-mru-bookmark-change-hook-fcn nil t))
|
|
;; Remove hooks
|
|
(remove-hook 'semantic-edits-new-change-functions
|
|
#'semantic-mru-bookmark-change-hook-fcn t)
|
|
(remove-hook 'semantic-edits-move-change-hooks
|
|
#'semantic-mru-bookmark-change-hook-fcn t)))
|
|
|
|
(semantic-add-minor-mode 'semantic-mru-bookmark-mode
|
|
"k")
|
|
|
|
;;; COMPLETING READ
|
|
;;
|
|
;; Ask the user for a tag in MRU order.
|
|
(defun semantic-mrub-read-history nil
|
|
"History of `semantic-mrub-completing-read'.")
|
|
|
|
(defun semantic-mrub-ring-to-assoc-list (ring)
|
|
"Convert RING into an association list for completion."
|
|
(let ((idx 0)
|
|
(len (ring-length ring))
|
|
(al nil))
|
|
(while (< idx len)
|
|
(let ((r (ring-ref ring idx)))
|
|
(setq al (cons (cons (oref r object-name) r)
|
|
al)))
|
|
(setq idx (1+ idx)))
|
|
(nreverse al)))
|
|
|
|
(defun semantic-mrub-completing-read (prompt)
|
|
"Do a `completing-read' on elements from the mru bookmark ring.
|
|
Argument PROMPT is the prompt to use when reading."
|
|
(if (ring-empty-p (oref semantic-mru-bookmark-ring ring))
|
|
(error "Semantic Bookmark ring is currently empty"))
|
|
(let* ((ring (oref semantic-mru-bookmark-ring ring))
|
|
(ans nil)
|
|
(alist (semantic-mrub-ring-to-assoc-list ring))
|
|
(first (cdr (car alist)))
|
|
(semantic-mrub-read-history nil)
|
|
)
|
|
;; Don't include the current tag.. only those that come after.
|
|
(if (semantic-equivalent-tag-p (oref first tag)
|
|
(semantic-current-tag))
|
|
(setq first (cdr (car (cdr alist)))))
|
|
;; Create a fake history list so we don't have to bind
|
|
;; M-p and M-n to our special cause.
|
|
(let ((elts (reverse alist)))
|
|
(while elts
|
|
(setq semantic-mrub-read-history
|
|
(cons (car (car elts)) semantic-mrub-read-history))
|
|
(setq elts (cdr elts))))
|
|
(setq semantic-mrub-read-history (nreverse semantic-mrub-read-history))
|
|
|
|
;; Do the read/prompt
|
|
(let ((prompt (if first (format "%s (%s): " prompt
|
|
(semantic-format-tag-name
|
|
(oref first tag) t)
|
|
)
|
|
(concat prompt ": ")))
|
|
)
|
|
(setq ans
|
|
(completing-read prompt alist nil nil nil 'semantic-mrub-read-history)))
|
|
;; Calculate the return tag.
|
|
(if (string= ans "")
|
|
(setq ans first)
|
|
;; Return the bookmark object.
|
|
(setq ans (assoc ans alist))
|
|
(if ans
|
|
(cdr ans)
|
|
;; no match. Custom word. Look it up somewhere?
|
|
nil)
|
|
)))
|
|
|
|
(defun semantic-mrub-switch-tags (tagmark)
|
|
"Switch tags to TAGMARK.
|
|
Selects a new tag via prompt through the mru tag ring.
|
|
Jumps to the tag and highlights it briefly."
|
|
(interactive (list (semantic-mrub-completing-read "Switch to tag")))
|
|
(if (not (semantic-bookmark-p tagmark))
|
|
(signal 'wrong-type-argument tagmark))
|
|
|
|
(semantic-mrub-push semantic-mru-bookmark-ring
|
|
(point)
|
|
'jump)
|
|
(semantic-mrub-visit tagmark)
|
|
)
|
|
|
|
;;; Debugging
|
|
;;
|
|
(defun semantic-adebug-mrub ()
|
|
"Display a list of items in the MRU bookmarks list.
|
|
Useful for debugging mrub problems."
|
|
(interactive)
|
|
(require 'eieio-datadebug)
|
|
(let* ((out semantic-mru-bookmark-ring))
|
|
(data-debug-new-buffer "*TAG RING ADEBUG*")
|
|
(data-debug-insert-object-slots out "]")
|
|
))
|
|
|
|
|
|
(provide 'semantic/mru-bookmark)
|
|
|
|
;; Local variables:
|
|
;; generated-autoload-file: "loaddefs.el"
|
|
;; generated-autoload-load-name: "semantic/mru-bookmark"
|
|
;; End:
|
|
|
|
;;; semantic/mru-bookmark.el ends here
|