Friday 5 September 2014

Index project with ctags and cscope with a single button press in emacs.


I should probably comment on the madness happening below, but I'm too tired at the moment, and, more importantly, I don't think anyone needs it anyway.

The short and messy description is that you press f9 and both ctags and cscope databases are built for your project. Pretty convenient.

To make it work with the project of your choice, you need to describe your project in the known-projects list (it's already filled with some examples).

The first part is a list of directories that designate the root of the project (so that you can run the command from any internal dir).

The second part is a blacklist that contains names and paths that should not be indexed, e.g. you would want to blacklist the huge test collection in GCC which will inflate your database tremendously and add an incredible amount of duplicated names, or the equally humongous Linux driver collection (unless you're a driver developer, duh).

1-2 auxiliary subroutines may have been copied from various places on the Internet. The code may hang your editor if you have cyclic links. Also, I can't program in elisp, although that must be strikingly obvious without me saying that.

;; Define the core dir trappings and blacklisted directories here
(setq known-projects '((("gcc" "config" "libgcc" "libiberty") ("/testsuite/" "/test/" "/build/"))
               (("mono" "mcs" "eglib" "docs" "tools") ())
               (("DROD" "DRODLib" "DRODUtil") ())
               (("arch" "drivers" "include" "kernel") ("/drivers/")) ; Linux
 

               (("README" "INSTALL") ("/.pc/")) ; Generic software project #1 (e.g. emacs)
               (("README") ("/.pc/")) ; Generic software project #2.
               ((".") ())          ; Oh well, let's at least index the current dir
               ))              ; if everything else fails.

(defun index-project () (interactive)
  (defun dump-vars-to-file (varlist filename)
    "simplistic dumping of variables in VARLIST to a file FILENAME"
    (save-excursion
      (let ((buf (find-file-noselect filename)))
    (set-buffer buf)
    (erase-buffer)
    (loop for var in varlist do
          (princ (concat var "\n") buf))
    (save-buffer)
    (kill-buffer))))
 
  (defun lookup-project (projects)
    (let ((lookup-failed t) (blacklist nil) (remaining-trappings nil) (lookup-path nil) (project nil))
      (while (and (consp projects) lookup-failed)
    (setq lookup-path buffer-file-name)
    (setq project (pop projects))
    ; (message "Trying project %s" project)
    (while (and (> (string-width lookup-path) 1) lookup-failed)
      ;; For some reason (file-exists-p "main.c/.") = t!
      (when lookup-failed
        (setq lookup-path (replace-regexp-in-string "/[^/]*$" "" lookup-path)))
      ; (message "Trying path %s" lookup-path)
      (setq lookup-failed nil)
      (setq remaining-trappings (nth 0 project))
      (setq blacklist (nth 1 project))
      (while (and (consp remaining-trappings) (not lookup-failed))
        (if (not (file-exists-p (concat lookup-path "/" (pop remaining-trappings))))
        (setq lookup-failed t)))))
      (when lookup-failed (setq lookup-path nil) (setq blacklist nil))
      (list lookup-path blacklist)))

  (let ((project-info (lookup-project known-projects))
    (project-root nil) (project-blacklist nil))

    (setq project-root (nth 0 project-info))
    (setq project-blacklist (nth 1 project-info))

    (message "Project found at %s. Scanning files... (excliding %s)" project-root project-blacklist)

    (when project-root
      (defun is-path-blacklisted (path blacklist)
    (while (and blacklist (not (string-match (car blacklist) path))) (pop blacklist))
    (if blacklist t nil))

      (defun build-cscope-database (root blacklist)
    (setq dirs (list root)) ;; "/usr/include/")) ; I found it to add too much clutter, better add some individual files instead.
    (setq files '())
    (while dirs
      (setq dir (pop dirs))
      (setq ls (directory-files dir))
      (while ls
        (setq file (pop ls))
        (setq file-path (concat dir "/" file))
        (unless (is-path-blacklisted file-path blacklist)
          (if (file-accessible-directory-p file-path)
          (unless (or (string= file ".") (string= file "..")) (add-to-list 'dirs file-path))
        (if (and (file-readable-p file-path) (string-match "\\.\\(c\\|cpp\\|h\\|hpp\\|vala\\|def\\)$" file-path))
            (add-to-list 'files file-path))))))
    ;; Append some commonly used header files. Uncomment the "/usr/include" line above
    ;; and remove/comment this one if you want to see them all their multitude instead
    ;; of this selectivity.
    (dolist (header '("stdio.h" "stdlib.h" "stdarg.h" "ctype.h" "string.h" "math.h" "dlfcn.h"
              "sys/types.h" "sys/time.h" "unistd.h" "assert.h" "limits.h" "poll.h"
              "libgen.h" "signal.h" "fcntl.h"
              "bits/stdio2.h" "bits/string2.h"
              "X11/Xos.h" "X11/Xlib.h" "X11/Xfuncproto.h" "X11/Xatom.h" "X11/Xproto.h"
              "X11/extensions/Xrandr.h" "X11/extensions/shape.h" "X11/cursorfont.h"
              "X11/extensions/XInput2.h" "X11/X.h"
              "boost/bind.hpp" "boost/foreach.hpp"
              "sys/wait.h" "sys/types.h" "sys/stat.h"
              "glib-2.0/glib/gspawn.h" "glib-2.0/glib/gslice.h" "glib-2.0/glib/gmacros.h"
              "glib-2.0/gobject/gobject.h" "glib-2.0/gobject/gtype.h"
              ))
      (add-to-list 'files (concat "/usr/include/" header)))
    files)

      (dump-vars-to-file (build-cscope-database project-root project-blacklist)
             (concat project-root "/" cscope-index-file))

      (message "Processing...")
      (cscope-set-initial-directory project-root)
      (with-temp-buffer (shell-command (concat "cd " project-root "; cscope -b; ctags -eL " cscope-index-file) t)) ; with-temp-buffer is needed to hide output
      (setq tags-file-name (concat project-root "/TAGS"))
      (message "Done processing")))
)

(global-set-key (kbd "<f9>") 'index-project)