;;; sn.el -- Source Navigator interface for Emacs ;;; Copyright (C) 1997 Cygnus Solutions ;;; Known problems and things to do: ;;; * SN tags workalike should work by making phony tags file and ;;; using tags-table-format-hooks. Then we'd get all the tags ;;; functionality for free. ;;; * Should stick a function on find-file-hook that asks each SN ;;; session if the new file is part of the session. ;;; Constants. ;; non-nil if using XEmacs. (defconst sn-is-xemacs (string-match "XEmacs" emacs-version)) ;;; Variable definitions. ;; History list for tags finding. (defvar sn-history-list nil) ;; This holds the connection to SN. It is local to each buffer; this ;; lets us have multiple SN projects share an Emacs. (defvar sn-process nil) (make-variable-buffer-local 'sn-process) ;; Name of the current process. This is only set when running a ;; function from a process filter. It is only defvar'd because I ;; don't like to use variables that aren't declared. (defvar sn-current-process nil) (defvar sn-minor-mode nil "t if source navigator mode is active") (make-variable-buffer-local 'sn-minor-mode) (or (assoc 'sn-minor-mode minor-mode-alist) (setq minor-mode-alist (cons '(sn-minor-mode " SN") minor-mode-alist))) (setplist 'sn-minor-mode (plist-put (symbol-plist 'sn-minor-mode) 'permanent-local t)) (defun sn-minor-mode (arg) "Minor mode for working with Source Navigator. Adds some commands for looking up stuff in SN: \\{sn-keymap} This mode is automatically activated when files are opened by SN and cannot be activated for other buffers. You can toggle it for SN-related buffers though. This lets you access the command bindings that this mode overrides." (interactive "P") (unless sn-process (error "This buffer has no Source Navigator connection")) (setq sn-minor-mode (if (null arg) (not sn-minor-mode) (> (prefix-numeric-value arg) 0)))) ;; When we tell SN about a file, we must always send it exactly the ;; same name as it sent us. So we stash the original filename here. (defvar sn-file-name nil) (make-variable-buffer-local 'sn-file-name) (defvar sn-keymap nil "Keymap for Source Navigator minor mode.") (unless sn-keymap (setq sn-keymap (make-sparse-keymap)) (define-key sn-keymap "\M-." 'sn-find-tag) (define-key sn-keymap "\C-x4." 'sn-tag-unimplemented) (define-key sn-keymap "\C-x5." 'sn-tag-unimplemented) (define-key sn-keymap "\M-," 'sn-tag-unimplemented) (define-key sn-keymap "\M-\t" 'sn-tag-unimplemented) (define-key sn-keymap "\C-c.c" 'sn-classbrowser) (define-key sn-keymap "\C-c.h" 'sn-classtree) (define-key sn-keymap "\C-c.r" 'sn-retrieve) (define-key sn-keymap "\C-c.x" 'sn-xref) (cond (sn-is-xemacs (define-key sn-keymap '(meta control ?.) 'sn-tag-unimplemented)) ;; GNU Emacs. (t (define-key sn-keymap [\M-\C-.] 'sn-tag-unimplemented)))) (or (assoc 'sn-minor-mode minor-mode-map-alist) (setq minor-mode-map-alist (cons (cons 'sn-minor-mode sn-keymap) minor-mode-map-alist))) ;;; ;;; Commands that the user can run to interact with SN. ;;; ;; Hide the current project. (defun sn-hide-project () "Hide the Source Navigator project associated with this buffer." (interactive) (sn-send "tkbHideShow withdraw")) ;; Like find-tag, but use SN to look up the tag. (defun sn-find-tag (tagname) "Like find-tag, but use Source Navigator to look up name." (interactive (progn (require 'etags) (list (read-string "Find tag: " (find-tag-default) 'sn-history-list)))) (sn-send (concat "sn_emacs_display_object " (sn-tcl-quote tagname))) ;; We know a response is coming. This makes things look a little ;; more synchronous. (accept-process-output)) (defun sn-classbrowser (class) "Browse the contents of a class in the Source Navigator." (interactive (progn (require 'etags) (list (read-string "Browse class: " (find-tag-default) 'sn-history-list)))) (sn-send (concat "sn_classbrowser " (sn-tcl-quote class)))) (defun sn-classtree (class) "Browse a class in the Source Navigator hierarchy browser." (interactive (progn (require 'etags) (list (read-string "Browse class: " (find-tag-default) 'sn-history-list)))) (sn-send (concat "sn_classtree " (sn-tcl-quote class)))) (defun sn-retrieve (pattern) "Tell Source Navigator to retrieve all symbols matching pattern. If there is only one match SN will take Emacs there. If there are several they are listed in a pop-up where you can select one to edit." (interactive (progn (require 'etags) (list (read-string "Retrieve pattern: " (find-tag-default) 'sn-history-list)))) (sn-send (concat "sn_retrieve_symbol " (sn-tcl-quote pattern) " all"))) (defun sn-xref (symbol) "Look up a symbol in the Source Navigator cross-referencer." (interactive (progn (require 'etags) (list (read-string "Xref symbol: " (find-tag-default) 'sn-history-list)))) (sn-send (concat "sn_xref both " (sn-tcl-quote symbol)))) (defun sn-tag-unimplemented () "Bound to tags-finding keys that Source Navigator can't (yet) handle." (interactive) (error "this keybinding is unimplemented in Source Navigator")) ;; find-tag-other-frame and find-tag-other-window versions are harder ;; to do; there is a synchronization problem here. ;; (defun sn-find-tag-other-frame) ;;(defun sn-find-tag-other-window) ;; (defun sn-find-tag-regexp) ; FIXME do it? ;; FIXME what about tags-query-replace, tags-loop-continue, ;; tags-search, tags-table-files, find-tag-hook, find-tag-noselect? ;; Turn off menus for now. Why bother when there is only one item? ; (progn ; (define-key sn-keymap [menu-bar SN] (cons "SN" (make-sparse-keymap))) ; (define-key sn-keymap [menu-bar SN hide] '("Hide project" ; . sn-hide-project))) ; ) ;;; ;;; Internal functions that can talk to SN. ;;; ;; Connect to Source Navigator. Arguments are: ;; * TMPFILENAME - a temp file containing some lisp code; remove it ;; here. This can be nil, meaning no file exists. ;; * HOSTNAME - name of host to connect to ;; * DIRECTORY - directory where temp file might be (if not absolute) ;; * PORT - port to connect to (defun sn-startup (tmpfilename hostname directory port) (save-excursion (let ((buffer (generate-new-buffer " sn"))) (set-buffer buffer) (setq sn-process (open-network-stream "sn" buffer hostname port)) (process-kill-without-query sn-process nil) (set-process-filter sn-process 'sn-filter) (set-process-sentinel sn-process 'sn-sentinel) (and tmpfilename (delete-file (expand-file-name tmpfilename directory)))))) ;; This quoting is sufficient to protect eg a filename from any sort ;; of expansion or splitting. Tcl quoting sure sucks. (defun sn-tcl-quote (string) (mapconcat (function (lambda (char) (if (memq char '(?[ ?] ?{ ?} ?\\ ?\" ?$ ? ?\;)) (concat "\\" (char-to-string char)) (char-to-string char)))) string "")) ;; Send a command to SN. (defun sn-send (string) (process-send-string sn-process (concat string "\n"))) ;; This is run on a hook after a file is saved. If we have to, we ;; notify the appropriate SN. (defun sn-after-save () (if sn-minor-mode (sn-send (concat "sn_parse_uptodate " (sn-tcl-quote sn-file-name) " 0")))) ; Disable annoying popup. ;; This is the process filter for reading from SN. It just tries to ;; read the process buffer as a lisp object; when the read succeeds, ;; the result is evalled. (defun sn-filter (proc string) ;; Only do the work if the process buffer is alive. (if (buffer-name (process-buffer proc)) (let ((inhibit-quit t) (sn-current-process proc) form form-list) (save-match-data (save-excursion (set-buffer (process-buffer proc)) ;; If process marker not already set, we must set it. ;; This seems to contradict the docs; go figure. (or (marker-position (process-mark proc)) (set-marker (process-mark proc) (point-min))) (goto-char (process-mark proc)) (insert string) (set-marker (process-mark proc) (point)) (goto-char (point-min)) ;; Note that we only catch end-of-file. invalid-read-syntax ;; we let through; that indicates an SN bug that we really ;; want to see. (while (progn (setq form (condition-case nil (read (current-buffer)) (end-of-file nil))) form) ;; Remove the stuff we've read. (delete-region (point-min) (point)) (setq form-list (cons form form-list))))) ;; Now go through each form on our list and eval it. We do ;; this outside the save-excursion because we want the ;; expression to be able to move point around. We also turn ;; C-g back on. (nreverse form-list) (setq inhibit-quit nil) (while form-list (eval (car form-list)) (setq form-list (cdr form-list)))))) ;; This is run when the SN connection dies. We go through each buffer ;; and do some cleaning up. We also remove our own process buffer. (defun sn-sentinel (process event) (save-excursion (let ((b-list (buffer-list))) (while b-list (set-buffer (car b-list)) (if (eq sn-process process) (progn ;; This buffer belongs to the current invocation. Close ;; down. (setq sn-process nil) (setq sn-minor-mode nil))) (setq b-list (cdr b-list))))) (kill-buffer (process-buffer process))) ;;; ;;; Functions that are run by SN. These functions can assume that ;;; sn-current-process is set, if they like. ;;; ;; Sent by SN when we should visit a file. ;; Arguments are: ;; * DIRECTORY - base directory of project ;; * PARTIAL-FILE - possibly-relative filename ;; * LINE, COLUMN - where cursor should end up ;; * STATE - either "normal" or "disabled"; the latter means read-only (defun sn-visit (directory partial-file line column state) (let* ((file (expand-file-name partial-file directory)) (obuf (get-file-buffer file))) (cond (obuf (switch-to-buffer obuf) (push-mark)) (t (set-buffer (if (string= state "disabled") (find-file-read-only file) (find-file file)))))) (setq sn-process sn-current-process) (goto-line line) (forward-char column) (setq sn-minor-mode t) (setq sn-file-name partial-file) (add-hook 'after-save-hook 'sn-after-save nil t)) ;; This command is sent by SN when a buffer we have should be put into ;; SN mode. It actually sends a list of (possibly relative) filenames ;; and the project's root directory. (defun sn-mark-for-project (directory file-list) (save-excursion (let (buffer file) (while file-list (setq file (expand-file-name (car file-list) directory)) (setq buffer (get-file-buffer file)) (if buffer (progn (set-buffer buffer) (if (not sn-minor-mode) (progn (setq sn-minor-mode t) (setq sn-process sn-current-process))))) (setq file-list (cdr file-list)))))) (provide 'sn) ;;; sn.el ends here