1;;; semantic/symref.el --- Symbol Reference API -*- lexical-binding: t; -*- 2 3;; Copyright (C) 2008-2021 Free Software Foundation, Inc. 4 5;; Author: Eric M. Ludlam <zappo@gnu.org> 6 7;; This file is part of GNU Emacs. 8 9;; GNU Emacs is free software: you can redistribute it and/or modify 10;; it under the terms of the GNU General Public License as published by 11;; the Free Software Foundation, either version 3 of the License, or 12;; (at your option) any later version. 13 14;; GNU Emacs is distributed in the hope that it will be useful, 15;; but WITHOUT ANY WARRANTY; without even the implied warranty of 16;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17;; GNU General Public License for more details. 18 19;; You should have received a copy of the GNU General Public License 20;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. 21 22;;; Commentary: 23;; 24;; Semantic Symbol Reference API. 25;; 26;; Semantic's native parsing tools do not handle symbol references. 27;; Tracking such information is a task that requires a huge amount of 28;; space and processing not appropriate for an Emacs Lisp program. 29;; 30;; Many desired tools used in refactoring, however, need to have 31;; such references available to them. This API aims to provide a 32;; range of functions that can be used to identify references. The 33;; API is backed by an OO system that is used to allow multiple 34;; external tools to provide the information. 35;; 36;; The default implementation uses a find/grep combination to do a 37;; search. This works ok in small projects. For larger projects, it 38;; is important to find an alternate tool to use as a back-end to 39;; symref. 40;; 41;; See the command: `semantic-symref' for an example app using this api. 42;; 43;; TO USE THIS TOOL 44;; 45;; The following functions can be used to find different kinds of 46;; references. 47;; 48;; `semantic-symref-find-references-by-name' 49;; `semantic-symref-find-file-references-by-name' 50;; `semantic-symref-find-text' 51;; 52;; All the search routines return a class of type 53;; `semantic-symref-result'. You can reference the various slots, but 54;; you will need the following methods to get extended information. 55;; 56;; `semantic-symref-result-get-files' 57;; `semantic-symref-result-get-tags' 58;; 59;; ADD A NEW EXTERNAL TOOL 60;; 61;; To support a new external tool, subclass `semantic-symref-tool-baseclass' 62;; and implement the methods. The baseclass provides support for 63;; managing external processes that produce parsable output. 64;; 65;; Your tool should then create an instance of `semantic-symref-result'. 66 67(require 'semantic) 68(eval-when-compile (require 'semantic/find)) ;For semantic-find-tags-* 69(eval-when-compile (require 'ede/proj)) ;For `metasubproject' warning. 70 71(defvar ede-minor-mode) 72(declare-function data-debug-new-buffer "data-debug") 73(declare-function data-debug-insert-object-slots "eieio-datadebug") 74(declare-function ede-toplevel "ede/base") 75(declare-function ede-project-root-directory "ede/files") 76(declare-function ede-up-directory "ede/files") 77 78;;; Code: 79(defcustom semantic-symref-tool 'detect 80 "The active symbol reference tool name. 81The tool symbol can be `detect', or a symbol that is the name of 82a tool that can be used for symbol referencing." 83 :type 'symbol 84 :group 'semantic) 85(make-variable-buffer-local 'semantic-symref-tool) 86 87;;; TOOL SETUP 88;; 89(defvar semantic-symref-tool-alist 90 '( ( (lambda (rootdir) (file-exists-p (expand-file-name "GPATH" rootdir))) . 91 global) 92 ( (lambda (rootdir) (file-exists-p (expand-file-name "ID" rootdir))) . 93 idutils) 94 ( (lambda (rootdir) (file-exists-p (expand-file-name "cscope.out" rootdir))) . 95 cscope ) 96 ) 97 "Alist of tools usable by `semantic-symref'. 98Each entry is of the form: 99 ( PREDICATE . KEY ) 100Where PREDICATE is a function that takes a directory name for the 101root of a project, and returns non-nil if the tool represented by KEY 102is supported. 103 104If no tools are supported, then 'grep is assumed.") 105 106(defun semantic-symref-calculate-rootdir () 107 "Calculate the root directory for a symref search. 108Start with an EDE project, or use the default directory." 109 (let* ((rootproj (when (and (featurep 'ede) ede-minor-mode) 110 (ede-toplevel))) 111 (rootdirbase (if rootproj 112 (ede-project-root-directory rootproj) 113 default-directory))) 114 (if (and rootproj (condition-case nil 115 ;; Hack for subprojects. 116 (oref rootproj metasubproject) 117 (error nil))) 118 (ede-up-directory rootdirbase) 119 rootdirbase))) 120 121(defun semantic-symref-detect-symref-tool () 122 "Detect the symref tool to use for the current buffer." 123 (if (not (eq semantic-symref-tool 'detect)) 124 semantic-symref-tool 125 ;; We are to perform a detection for the right tool to use. 126 (let* ((rootdir (semantic-symref-calculate-rootdir)) 127 (tools semantic-symref-tool-alist)) 128 (while (and tools (eq semantic-symref-tool 'detect)) 129 (when (funcall (car (car tools)) rootdir) 130 (setq semantic-symref-tool (cdr (car tools)))) 131 (setq tools (cdr tools))) 132 133 (when (eq semantic-symref-tool 'detect) 134 (setq semantic-symref-tool 'grep)) 135 136 semantic-symref-tool))) 137 138(defun semantic-symref-instantiate (&rest args) 139 "Instantiate a new symref search object. 140ARGS are the initialization arguments to pass to the created class." 141 (let* ((srt (symbol-name (semantic-symref-detect-symref-tool))) 142 (class (intern-soft (concat "semantic-symref-tool-" srt))) 143 (inst nil) 144 ) 145 (when (not (class-p class)) 146 (error "Unknown symref tool %s" semantic-symref-tool)) 147 (setq inst (apply #'make-instance class args)) 148 inst)) 149 150(defvar semantic-symref-last-result nil 151 "The last calculated symref result.") 152 153(defun semantic-symref-data-debug-last-result () 154 "Run the last symref data result in Data Debug." 155 (interactive) 156 (require 'eieio-datadebug) 157 (if semantic-symref-last-result 158 (progn 159 (data-debug-new-buffer "*Symbol Reference ADEBUG*") 160 (data-debug-insert-object-slots semantic-symref-last-result "]")) 161 (message "Empty results."))) 162 163;;; EXTERNAL API 164;; 165 166;;;###autoload 167(defun semantic-symref-find-references-by-name (name &optional scope tool-return) 168 "Find a list of references to NAME in the current project. 169Optional SCOPE specifies which file set to search. Defaults to `project'. 170Refers to `semantic-symref-tool', to determine the reference tool to use 171for the current buffer. 172Returns an object of class `semantic-symref-result'. 173TOOL-RETURN is an optional symbol, which will be assigned the tool used 174to perform the search. This was added for use by a test harness." 175 (interactive "sName: ") 176 (let* ((inst (semantic-symref-instantiate 177 :searchfor name 178 :searchtype 'symbol 179 :searchscope (or scope 'project) 180 :resulttype 'line)) 181 (result (semantic-symref-get-result inst))) 182 (when tool-return 183 (set tool-return inst)) 184 (prog1 185 (setq semantic-symref-last-result result) 186 (when (called-interactively-p 'interactive) 187 (semantic-symref-data-debug-last-result)))) 188 ) 189 190;;;###autoload 191(defun semantic-symref-find-tags-by-name (name &optional scope) 192 "Find a list of tags by NAME in the current project. 193Optional SCOPE specifies which file set to search. Defaults to `project'. 194Refers to `semantic-symref-tool', to determine the reference tool to use 195for the current buffer. 196Returns an object of class `semantic-symref-result'." 197 (interactive "sName: ") 198 (let* ((inst (semantic-symref-instantiate 199 :searchfor name 200 :searchtype 'tagname 201 :searchscope (or scope 'project) 202 :resulttype 'line)) 203 (result (semantic-symref-get-result inst))) 204 (prog1 205 (setq semantic-symref-last-result result) 206 (when (called-interactively-p 'interactive) 207 (semantic-symref-data-debug-last-result)))) 208 ) 209 210;;;###autoload 211(defun semantic-symref-find-tags-by-regexp (name &optional scope) 212 "Find a list of references to NAME in the current project. 213Optional SCOPE specifies which file set to search. Defaults to `project'. 214Refers to `semantic-symref-tool', to determine the reference tool to use 215for the current buffer. 216Returns an object of class `semantic-symref-result'." 217 (interactive "sName: ") 218 (let* ((inst (semantic-symref-instantiate 219 :searchfor name 220 :searchtype 'tagregexp 221 :searchscope (or scope 'project) 222 :resulttype 'line)) 223 (result (semantic-symref-get-result inst))) 224 (prog1 225 (setq semantic-symref-last-result result) 226 (when (called-interactively-p 'interactive) 227 (semantic-symref-data-debug-last-result)))) 228 ) 229 230;;;###autoload 231(defun semantic-symref-find-tags-by-completion (name &optional scope) 232 "Find a list of references to NAME in the current project. 233Optional SCOPE specifies which file set to search. Defaults to `project'. 234Refers to `semantic-symref-tool', to determine the reference tool to use 235for the current buffer. 236Returns an object of class `semantic-symref-result'." 237 (interactive "sName: ") 238 (let* ((inst (semantic-symref-instantiate 239 :searchfor name 240 :searchtype 'tagcompletions 241 :searchscope (or scope 'project) 242 :resulttype 'line)) 243 (result (semantic-symref-get-result inst))) 244 (prog1 245 (setq semantic-symref-last-result result) 246 (when (called-interactively-p 'interactive) 247 (semantic-symref-data-debug-last-result)))) 248 ) 249 250;;;###autoload 251(defun semantic-symref-find-file-references-by-name (name &optional scope) 252 "Find a list of references to NAME in the current project. 253Optional SCOPE specifies which file set to search. Defaults to `project'. 254Refers to `semantic-symref-tool', to determine the reference tool to use 255for the current buffer. 256Returns an object of class `semantic-symref-result'." 257 (interactive "sName: ") 258 (let* ((inst (semantic-symref-instantiate 259 :searchfor name 260 :searchtype 'regexp 261 :searchscope (or scope 'project) 262 :resulttype 'file)) 263 (result (semantic-symref-get-result inst))) 264 (prog1 265 (setq semantic-symref-last-result result) 266 (when (called-interactively-p 'interactive) 267 (semantic-symref-data-debug-last-result)))) 268 ) 269 270;;;###autoload 271(defun semantic-symref-find-text (text &optional scope) 272 "Find a list of occurrences of TEXT in the current project. 273TEXT is a regexp formatted for use with grep -E. 274Optional SCOPE specifies which file set to search. Defaults to `project'. 275Refers to `semantic-symref-tool', to determine the reference tool to use 276for the current buffer. 277Returns an object of class `semantic-symref-result'." 278 (interactive "sGrep -E style Regexp: ") 279 (let* ((inst (semantic-symref-instantiate 280 :searchfor text 281 :searchtype 'regexp 282 :searchscope (or scope 'project) 283 :resulttype 'line)) 284 (result (semantic-symref-get-result inst))) 285 (prog1 286 (setq semantic-symref-last-result result) 287 (when (called-interactively-p 'interactive) 288 (semantic-symref-data-debug-last-result)))) 289 ) 290 291;;; SYMREF TOOLS 292;; 293;; The base symref tool provides something to hang new tools off of 294;; for finding symbol references. 295(defclass semantic-symref-tool-baseclass () 296 ((searchfor :initarg :searchfor 297 :type string 298 :documentation "The thing to search for.") 299 (searchtype :initarg :searchtype 300 :type symbol 301 :documentation "The type of search to do. 302Values could be 'symbol, 'regexp, 'tagname, or 'completion.") 303 (searchscope :initarg :searchscope 304 :type symbol 305 :documentation 306 "The scope to search for. 307Can be 'project, 'target, or 'file.") 308 (resulttype :initarg :resulttype 309 :type symbol 310 :documentation 311 "The kind of search results desired. 312Can be `line', `file', or `tag'. 313The type of result can be converted from `line' to `file', or `line' to `tag', 314but not from `file' to `line' or `tag'.") 315 ) 316 "Baseclass for all symbol references tools. 317A symbol reference tool supplies functionality to identify the locations of 318where different symbols are used. 319 320Subclasses should be named `semantic-symref-tool-NAME', where 321NAME is the name of the tool used in the configuration variable 322`semantic-symref-tool'." 323 :abstract t) 324 325(cl-defmethod semantic-symref-get-result ((tool semantic-symref-tool-baseclass)) 326 "Calculate the results of a search based on TOOL. 327The symref TOOL should already contain the search criteria." 328 (let ((answer (semantic-symref-perform-search tool)) 329 ) 330 (when answer 331 (let ((answersym (if (eq (oref tool resulttype) 'file) 332 :hit-files 333 (if (stringp (car answer)) 334 :hit-text 335 :hit-lines)))) 336 (semantic-symref-result (oref tool searchfor) 337 answersym 338 answer 339 :created-by tool)) 340 ) 341 )) 342 343(cl-defmethod semantic-symref-perform-search ((_tool semantic-symref-tool-baseclass)) 344 "Base search for symref tools should throw an error." 345 (error "Symref tool objects must implement `semantic-symref-perform-search'")) 346 347(cl-defmethod semantic-symref-parse-tool-output ((tool semantic-symref-tool-baseclass) 348 outputbuffer) 349 "Parse the entire OUTPUTBUFFER of a symref tool. 350Calls the method `semantic-symref-parse-tool-output-one-line' over and 351over until it returns nil." 352 (with-current-buffer outputbuffer 353 (goto-char (point-min)) 354 (let ((result nil) 355 (hit nil)) 356 (while (setq hit (semantic-symref-parse-tool-output-one-line tool)) 357 (setq result (cons hit result))) 358 (nreverse result))) 359 ) 360 361(cl-defmethod semantic-symref-parse-tool-output-one-line ((_tool semantic-symref-tool-baseclass)) 362 "Base tool output parser is not implemented." 363 (error "Symref tool objects must implement `semantic-symref-parse-tool-output-one-line'")) 364 365;;; RESULTS 366;; 367;; The results class and methods provide features for accessing hits. 368(defclass semantic-symref-result () 369 ((created-by :initarg :created-by 370 :type semantic-symref-tool-baseclass 371 :documentation 372 "Back-pointer to the symref tool creating these results.") 373 (hit-files :initarg :hit-files 374 :type list 375 :documentation 376 "The list of files hit.") 377 (hit-text :initarg :hit-text 378 :type list 379 :documentation 380 "If the result doesn't provide full lines, then fill in hit-text. 381GNU Global does completion search this way.") 382 (hit-lines :initarg :hit-lines 383 :type list 384 :documentation 385 "The list of line hits. 386Each element is a cons cell of the form (LINE . FILENAME).") 387 (hit-tags :initarg :hit-tags 388 :type list 389 :documentation 390 "The list of tags with hits in them. 391Use the `semantic-symref-hit-tags' method to get this list.") 392 ) 393 "The results from a symbol reference search.") 394 395(cl-defmethod semantic-symref-result-get-files ((result semantic-symref-result)) 396 "Get the list of files from the symref result RESULT." 397 (if (slot-boundp result 'hit-files) 398 (oref result hit-files) 399 (let* ((lines (oref result hit-lines)) 400 (files (mapcar (lambda (a) (cdr a)) lines)) 401 (ans nil)) 402 (setq ans (list (car files)) 403 files (cdr files)) 404 (dolist (F files) 405 ;; This algorithm for uniquifying the file list depends on the 406 ;; tool in question providing all the hits in the same file 407 ;; grouped together. 408 (when (not (string= F (car ans))) 409 (setq ans (cons F ans)))) 410 (oset result hit-files (nreverse ans)) 411 ) 412 )) 413 414(defvar semantic-symref-recently-opened-buffers nil 415 "List of buffers opened by `semantic-symref-result-get-tags'.") 416 417(defun semantic-symref-cleanup-recent-buffers-fcn () 418 "Hook function to be used in `post-command-hook' to cleanup buffers. 419Buffers collected during symref can result in some files being 420opened multiple times for one operation. This will keep buffers open 421until the next command is executed." 422 ;;(message "To Clean Up: %S" semantic-symref-recently-opened-buffers) 423 (mapc (lambda (buff) 424 ;; Don't delete any buffers which are being used 425 ;; upon completion of some command. 426 (when (not (get-buffer-window buff)) 427 (kill-buffer buff))) 428 semantic-symref-recently-opened-buffers) 429 (setq semantic-symref-recently-opened-buffers nil) 430 (remove-hook 'post-command-hook #'semantic-symref-cleanup-recent-buffers-fcn) 431 ) 432 433(cl-defmethod semantic-symref-result-get-tags ((result semantic-symref-result) 434 &optional open-buffers) 435 "Get the list of tags from the symref result RESULT. 436Optional OPEN-BUFFERS indicates that the buffers that the hits are 437in should remain open after scanning. 438Note: This can be quite slow if most of the hits are not in buffers 439already." 440 (if (and (slot-boundp result 'hit-tags) (oref result hit-tags)) 441 (oref result hit-tags) 442 ;; Calculate the tags. 443 (let ((lines (oref result hit-lines)) 444 (txt (oref (oref result created-by) searchfor)) 445 (searchtype (oref (oref result created-by) searchtype)) 446 (ans nil) 447 (out nil)) 448 (save-excursion 449 (setq ans (mapcar 450 (lambda (hit) 451 (semantic-symref-hit-to-tag-via-buffer 452 hit txt searchtype open-buffers)) 453 lines))) 454 ;; Kill off dead buffers, unless we were requested to leave them open. 455 (if (not open-buffers) 456 (add-hook 'post-command-hook #'semantic-symref-cleanup-recent-buffers-fcn) 457 ;; Else, just clear the saved buffers so they aren't deleted later. 458 (setq semantic-symref-recently-opened-buffers nil) 459 ) 460 ;; Strip out duplicates. 461 (dolist (T ans) 462 (if (and T (not (semantic-equivalent-tag-p (car out) T))) 463 (setq out (cons T out)) 464 (when T 465 ;; Else, add this line into the existing list of lines. 466 (let ((lines (append (semantic--tag-get-property (car out) :hit) 467 (semantic--tag-get-property T :hit)))) 468 (semantic--tag-put-property (car out) :hit lines))) 469 )) 470 ;; Out is reversed... twice 471 (oset result hit-tags (nreverse out))))) 472 473(defun semantic-symref-hit-to-tag-via-db (hit searchtxt searchtype) 474 "Convert the symref HIT into a TAG by looking up the tag via a database. 475Return the Semantic tag associated with HIT. 476SEARCHTXT is the text that is being searched for. 477Used to narrow the in-buffer search. 478SEARCHTYPE is the type of search (such as 'symbol or 'tagname). 479If there is no database, or if the searchtype is wrong, return nil." 480 ;; Allowed search types for this mechanism: 481 ;; tagname, tagregexp, tagcompletions 482 (if (not (memq searchtype '(tagname tagregexp tagcompletions))) 483 nil 484 (let* ((file (cdr hit)) 485 ;; FAIL here vv - don't load is not obeyed if no table found. 486 (db (semanticdb-file-table-object file t)) 487 (found 488 (cond ((eq searchtype 'tagname) 489 (semantic-find-tags-by-name searchtxt db)) 490 ((eq searchtype 'tagregexp) 491 (semantic-find-tags-by-name-regexp searchtxt db)) 492 ((eq searchtype 'tagcompletions) 493 (semantic-find-tags-for-completion searchtxt db)))) 494 (hit nil) 495 ) 496 ;; Loop over FOUND to see if we can line up a match with a line number. 497 (when (= (length found) 1) 498 (setq hit (car found))) 499 500 ;; FAIL here ^^ - symref finds line numbers, but our DB uses character locations. 501 ;; as such, this is a cheat and we will need to give up. 502 hit))) 503 504(defun semantic-symref-hit-to-tag-via-buffer (hit searchtxt searchtype &optional open-buffers) 505 "Convert the symref HIT into a TAG by looking up the tag via a buffer. 506Return the Semantic tag associated with HIT. 507SEARCHTXT is the text that is being searched for. 508Used to narrow the in-buffer search. 509SEARCHTYPE is the type of search (such as 'symbol or 'tagname). 510Optional OPEN-BUFFERS, when nil will use a faster version of 511`find-file' when a file needs to be opened. If non-nil, then 512normal buffer initialization will be used. 513This function will leave buffers loaded from a file open, but 514will add buffers that must be opened to 515`semantic-symref-recently-opened-buffers'. 516Any caller MUST deal with that variable, either clearing it, or 517deleting the buffers that were opened." 518 (let* ((line (car hit)) 519 (file (cdr hit)) 520 (buff (find-buffer-visiting file)) 521 (tag nil) 522 ) 523 (cond 524 ;; We have a buffer already. Check it out. 525 (buff 526 (set-buffer buff)) 527 528 ;; We have a table, but it needs a refresh. 529 ;; This means we should load in that buffer. 530 (t 531 (let ((kbuff 532 (if open-buffers 533 ;; Even if we keep the buffers open, don't 534 ;; let EDE ask lots of questions. 535 (let ((ede-auto-add-method 'never)) 536 (find-file-noselect file t)) 537 ;; When not keeping the buffers open, then 538 ;; don't setup all the fancy froo-froo features 539 ;; either. 540 (semantic-find-file-noselect file t)))) 541 (set-buffer kbuff) 542 (push kbuff semantic-symref-recently-opened-buffers) 543 (semantic-fetch-tags) 544 )) 545 ) 546 547 ;; Too much baggage in goto-line 548 ;; (goto-line line) 549 (goto-char (point-min)) 550 (forward-line (1- line)) 551 552 ;; Search forward for the matching text. 553 ;; FIXME: This still fails if the regexp uses something specific 554 ;; to the extended syntax, like grouping. 555 (when (re-search-forward (if (memq searchtype '(regexp tagregexp)) 556 searchtxt 557 (regexp-quote searchtxt)) 558 (point-at-eol) 559 t) 560 (goto-char (match-beginning 0)) 561 ) 562 563 (setq tag (semantic-current-tag)) 564 565 ;; If we are searching for a tag, but bound the tag we are looking 566 ;; for, see if it resides in some other parent tag. 567 ;; 568 ;; If there is no parent tag, then we still need to hang the originator 569 ;; in our list. 570 (when (and (eq searchtype 'symbol) 571 (string= (semantic-tag-name tag) searchtxt)) 572 (setq tag (or (semantic-current-tag-parent) tag))) 573 574 ;; Copy the tag, which adds a :filename property. 575 (when tag 576 (setq tag (semantic-tag-copy tag nil t)) 577 ;; Ad this hit to the tag. 578 (semantic--tag-put-property tag :hit (list line))) 579 tag)) 580 581(provide 'semantic/symref) 582 583;; Local variables: 584;; generated-autoload-file: "loaddefs.el" 585;; generated-autoload-load-name: "semantic/symref" 586;; End: 587 588;;; semantic/symref.el ends here 589