Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 9c35cc1870
Fetching contributors…

Cannot retrieve contributors at this time

9059 lines (7876 sloc) 355.626 kb
;;; slime.el --- Superior Lisp Interaction Mode for Emacs
;;
;;;; License
;; Copyright (C) 2003 Eric Marsden, Luke Gorrie, Helmut Eller
;; Copyright (C) 2004,2005,2006 Luke Gorrie, Helmut Eller
;; Copyright (C) 2007,2008,2009 Helmut Eller, Tobias C. Rittweiler
;; Authors: Eric Marsden, Luke Gorrie, Helmut Eller, Tobias C. Rittweiler
;; URL: http://common-lisp.net/project/slime/
;; Version: 20091016
;; Keywords: languages, lisp, slime
;; Adapted-by: Phil Hagelberg
;;
;; For a detailed list of contributors, see the manual.
;;
;; 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 2 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, write to the Free
;; Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
;; MA 02111-1307, USA.
;;
;;;; Commentary
;;
;; This file contains extensions for programming in Common Lisp. The
;; main features are:
;;
;; A socket-based communication/RPC interface between Emacs and
;; Lisp, enabling introspection and remote development.
;;
;; The `slime-mode' minor-mode complementing `lisp-mode'. This new
;; mode includes many commands for interacting with the Common Lisp
;; process.
;;
;; A Common Lisp debugger written in Emacs Lisp. The debugger pops up
;; an Emacs buffer similar to the Emacs/Elisp debugger.
;;
;; A Common Lisp inspector to interactively look at run-time data.
;;
;; Trapping compiler messages and creating annotations in the source
;; file on the appropriate forms.
;;
;; SLIME should work with Emacs 22 and 23. If it works on XEmacs,
;; consider yourself lucky.
;;
;; In order to run SLIME, a supporting Lisp server called Swank is
;; required. Swank is distributed with slime.el and will automatically
;; be started in a normal installation.
;;;; Dependencies and setup
(eval-and-compile
(when (<= emacs-major-version 20)
(error "Slime requires an Emacs version of 21, or above")))
(eval-and-compile
(require 'cl)
(unless (fboundp 'define-minor-mode)
(require 'easy-mmode)
(defalias 'define-minor-mode 'easy-mmode-define-minor-mode))
(when (locate-library "hyperspec")
(require 'hyperspec)))
(require 'thingatpt)
(require 'comint)
(require 'timer)
(require 'pp)
(require 'hideshow)
(require 'font-lock)
(when (featurep 'xemacs)
(require 'overlay))
(require 'easymenu)
(eval-when (compile)
(require 'arc-mode)
(require 'apropos)
(require 'outline)
(require 'etags))
(eval-and-compile
(defvar slime-path
(let ((path (or (locate-library "slime") load-file-name)))
(and path (file-name-directory path)))
"Directory containing the Slime package.
This is used to load the supporting Common Lisp library, Swank.
The default value is automatically computed from the location of the
Emacs Lisp package."))
;;;###autoload
(progn
(defvar slime-lisp-modes '(lisp-mode))
(defvar slime-setup-contribs nil)
(defun slime-setup (&optional contribs)
"Setup Emacs so that lisp-mode buffers always use SLIME.
CONTRIBS is a list of contrib packages to load."
(when (member 'lisp-mode slime-lisp-modes)
(add-hook 'lisp-mode-hook 'slime-lisp-mode-hook))
(setq slime-setup-contribs contribs)
(slime-setup-contribs)))
(defun slime-setup-contribs ()
"Load and initialize contribs."
(when slime-setup-contribs
(add-to-list 'load-path (expand-file-name "contrib" slime-path))
(dolist (c slime-setup-contribs)
(require c)
(let ((init (intern (format "%s-init" c))))
(when (fboundp init)
(funcall init))))))
;;;###autoload
(defun slime-lisp-mode-hook ()
(slime-mode 1)
(set (make-local-variable 'lisp-indent-function)
'common-lisp-indent-function))
(defun slime-changelog-date ()
"Return the version of the current slime package either from ChangeLog
or from package.el.
Returns nil if both fails."
;; Altered to hard-code package version
"20100404")
(defvar slime-protocol-version 'ignore)
;;;; Customize groups
;;
;;;;; slime
(defgroup slime nil
"Interaction with the Superior Lisp Environment."
:prefix "slime-"
:group 'applications)
;;;;; slime-ui
(defgroup slime-ui nil
"Interaction with the Superior Lisp Environment."
:prefix "slime-"
:group 'slime)
(defcustom slime-truncate-lines t
"Set `truncate-lines' in popup buffers.
This applies to buffers that present lines as rows of data, such as
debugger backtraces and apropos listings."
:type 'boolean
:group 'slime-ui)
(defcustom slime-extended-modeline t
"If non-nil, display various information in the mode line of a
Lisp buffer. The information includes the current connection of
that buffer, the buffer package, and some state indication."
:type 'boolean
:group 'slime-ui)
(defcustom slime-kill-without-query-p nil
"If non-nil, kill SLIME processes without query when quitting Emacs.
This applies to the *inferior-lisp* buffer and the network connections."
:type 'boolean
:group 'slime-ui)
;;;;; slime-lisp
(defgroup slime-lisp nil
"Lisp server configuration."
:prefix "slime-"
:group 'slime)
(defcustom slime-backend "swank-loader.lisp"
"The name of the Lisp file that loads the Swank server.
This name is interpreted relative to the directory containing
slime.el, but could also be set to an absolute filename."
:type 'string
:group 'slime-lisp)
(defcustom slime-connected-hook nil
"List of functions to call when SLIME connects to Lisp."
:type 'hook
:group 'slime-lisp)
(defcustom slime-enable-evaluate-in-emacs nil
"*If non-nil, the inferior Lisp can evaluate arbitrary forms in Emacs.
The default is nil, as this feature can be a security risk."
:type '(boolean)
:group 'slime-lisp)
(defcustom slime-lisp-host "127.0.0.1"
"The default hostname (or IP address) to connect to."
:type 'string
:group 'slime-lisp)
(defcustom slime-port 4005
"Port to use as the default for `slime-connect'."
:type 'integer
:group 'slime-lisp)
(defvar slime-net-valid-coding-systems
'((iso-latin-1-unix nil "iso-latin-1-unix")
(iso-8859-1-unix nil "iso-latin-1-unix")
(binary nil "iso-latin-1-unix")
(utf-8-unix t "utf-8-unix")
(emacs-mule-unix t "emacs-mule-unix")
(euc-jp-unix t "euc-jp-unix"))
"A list of valid coding systems.
Each element is of the form: (NAME MULTIBYTEP CL-NAME)")
(defun slime-find-coding-system (name)
"Return the coding system for the symbol NAME.
The result is either an element in `slime-net-valid-coding-systems'
of nil."
(let ((probe (assq name slime-net-valid-coding-systems)))
(when (and probe (if (fboundp 'check-coding-system)
(ignore-errors (check-coding-system (car probe)))
(eq (car probe) 'binary)))
probe)))
(defcustom slime-net-coding-system
(car (find-if 'slime-find-coding-system
slime-net-valid-coding-systems :key 'car))
"Coding system used for network connections.
See also `slime-net-valid-coding-systems'."
:type (cons 'choice
(mapcar (lambda (x)
(list 'const (car x)))
slime-net-valid-coding-systems))
:group 'slime-lisp)
;;;;; slime-mode
(defgroup slime-mode nil
"Settings for slime-mode Lisp source buffers."
:prefix "slime-"
:group 'slime)
(defcustom slime-find-definitions-function 'slime-find-definitions-rpc
"Function to find definitions for a name.
The function is called with the definition name, a string, as its
argument."
:type 'function
:group 'slime-mode
:options '(slime-find-definitions-rpc
slime-etags-definitions
(lambda (name)
(append (slime-find-definitions-rpc name)
(slime-etags-definitions name)))
(lambda (name)
(or (slime-find-definitions-rpc name)
(and tags-table-list
(slime-etags-definitions name))))))
(defcustom slime-complete-symbol-function 'slime-simple-complete-symbol
"*Function to perform symbol completion."
:group 'slime-mode
:type '(choice (const :tag "Simple" slime-simple-complete-symbol)
(const :tag "Compound" slime-complete-symbol*)
(const :tag "Fuzzy" slime-fuzzy-complete-symbol)))
;;;;; slime-mode-faces
(defgroup slime-mode-faces nil
"Faces in slime-mode source code buffers."
:prefix "slime-"
:group 'slime-mode)
(defun slime-underline-color (color)
"Return a legal value for the :underline face attribute based on COLOR."
;; In XEmacs the :underline attribute can only be a boolean.
;; In GNU it can be the name of a colour.
(if (featurep 'xemacs)
(if color t nil)
color))
(defface slime-error-face
`((((class color) (background light))
(:underline ,(slime-underline-color "red")))
(((class color) (background dark))
(:underline ,(slime-underline-color "red")))
(t (:underline t)))
"Face for errors from the compiler."
:group 'slime-mode-faces)
(defface slime-warning-face
`((((class color) (background light))
(:underline ,(slime-underline-color "orange")))
(((class color) (background dark))
(:underline ,(slime-underline-color "coral")))
(t (:underline t)))
"Face for warnings from the compiler."
:group 'slime-mode-faces)
(defface slime-style-warning-face
`((((class color) (background light))
(:underline ,(slime-underline-color "brown")))
(((class color) (background dark))
(:underline ,(slime-underline-color "gold")))
(t (:underline t)))
"Face for style-warnings from the compiler."
:group 'slime-mode-faces)
(defface slime-note-face
`((((class color) (background light))
(:underline ,(slime-underline-color "brown4")))
(((class color) (background dark))
(:underline ,(slime-underline-color "light goldenrod")))
(t (:underline t)))
"Face for notes from the compiler."
:group 'slime-mode-faces)
(defun slime-face-inheritance-possible-p ()
"Return true if the :inherit face attribute is supported."
(assq :inherit custom-face-attributes))
(defface slime-highlight-face
(if (slime-face-inheritance-possible-p)
'((t (:inherit highlight :underline nil)))
'((((class color) (background light))
(:background "darkseagreen2"))
(((class color) (background dark))
(:background "darkolivegreen"))
(t (:inverse-video t))))
"Face for compiler notes while selected."
:group 'slime-mode-faces)
;;;;; sldb
(defgroup slime-debugger nil
"Backtrace options and fontification."
:prefix "sldb-"
:group 'slime)
(defmacro define-sldb-faces (&rest faces)
"Define the set of SLDB faces.
Each face specifiation is (NAME DESCRIPTION &optional PROPERTIES).
NAME is a symbol; the face will be called sldb-NAME-face.
DESCRIPTION is a one-liner for the customization buffer.
PROPERTIES specifies any default face properties."
`(progn ,@(loop for face in faces
collect `(define-sldb-face ,@face))))
(defmacro define-sldb-face (name description &optional default)
(let ((facename (intern (format "sldb-%s-face" (symbol-name name)))))
`(defface ,facename
(list (list t ,default))
,(format "Face for %s." description)
:group 'slime-debugger)))
(define-sldb-faces
(topline "the top line describing the error")
(condition "the condition class")
(section "the labels of major sections in the debugger buffer")
(frame-label "backtrace frame numbers")
(restart-type "restart names."
(if (slime-face-inheritance-possible-p)
'(:inherit font-lock-keyword-face)))
(restart "restart descriptions")
(restart-number "restart numbers (correspond to keystrokes to invoke)"
'(:bold t))
(frame-line "function names and arguments in the backtrace")
(restartable-frame-line
"frames which are surely restartable"
'(:foreground "lime green"))
(non-restartable-frame-line
"frames which are surely not restartable")
(detailed-frame-line
"function names and arguments in a detailed (expanded) frame")
(local-name "local variable names")
(local-value "local variable values")
(catch-tag "catch tags"))
;;;; Minor modes
;;;;; slime-mode
;;;###autoload
(define-minor-mode slime-mode
"\\<slime-mode-map>\
SLIME: The Superior Lisp Interaction Mode for Emacs (minor-mode).
Commands to compile the current buffer's source file and visually
highlight any resulting compiler notes and warnings:
\\[slime-compile-and-load-file] - Compile and load the current buffer's file.
\\[slime-compile-file] - Compile (but not load) the current buffer's file.
\\[slime-compile-defun] - Compile the top-level form at point.
Commands for visiting compiler notes:
\\[slime-next-note] - Goto the next form with a compiler note.
\\[slime-previous-note] - Goto the previous form with a compiler note.
\\[slime-remove-notes] - Remove compiler-note annotations in buffer.
Finding definitions:
\\[slime-edit-definition] - Edit the definition of the function called at point.
\\[slime-pop-find-definition-stack] - Pop the definition stack to go back from a definition.
Documentation commands:
\\[slime-describe-symbol] - Describe symbol.
\\[slime-apropos] - Apropos search.
\\[slime-disassemble-symbol] - Disassemble a function.
Evaluation commands:
\\[slime-eval-defun] - Evaluate top-level from containing point.
\\[slime-eval-last-expression] - Evaluate sexp before point.
\\[slime-pprint-eval-last-expression] - Evaluate sexp before point, pretty-print result.
Full set of commands:
\\{slime-mode-map}"
nil
nil
;; Fake binding to coax `define-minor-mode' to create the keymap
'((" " 'undefined))
(slime-setup-command-hooks))
(make-variable-buffer-local
(defvar slime-modeline-string nil
"The string that should be displayed in the modeline if
`slime-extended-modeline' is true, and which indicates the
current connection, package and state of a Lisp buffer.
The string is periodically updated by an idle timer."))
;;; These are used to keep track of old values, so we can determine
;;; whether the mode line has changed, and should be updated.
(make-variable-buffer-local
(defvar slime-modeline-package nil))
(make-variable-buffer-local
(defvar slime-modeline-connection-name nil))
(make-variable-buffer-local
(defvar slime-modeline-connection-state nil))
(defun slime-compute-modeline-package ()
(when (memq major-mode slime-lisp-modes)
;; WHEN-LET is defined later.
(let ((package (slime-current-package)))
(when package
(slime-pretty-package-name package)))))
(defun slime-pretty-package-name (name)
"Return a pretty version of a package name NAME."
(cond ((string-match "^#?:\\(.*\\)$" name)
(match-string 1 name))
((string-match "^\"\\(.*\\)\"$" name)
(match-string 1 name))
(t name)))
(defun slime-compute-modeline-connection ()
(let ((conn (slime-current-connection)))
(if (or (null conn) (slime-stale-connection-p conn))
nil
(slime-connection-name conn))))
(defun slime-compute-modeline-connection-state ()
(let* ((conn (slime-current-connection))
(new-state (slime-compute-connection-state conn)))
(if (eq new-state :connected)
(let ((rex-cs (length (slime-rex-continuations)))
(sldb-cs (length (sldb-debugged-continuations conn)))
;; There can be SLDB buffers which have no continuations
;; attached to it, e.g. the one resulting from
;; `slime-interrupt'.
(sldbs (length (sldb-buffers conn))))
(cond ((and (= sldbs 0) (zerop rex-cs)) nil)
((= sldbs 0) (format "%s" rex-cs))
(t (format "%s/%s"
(if (= rex-cs 0) 0 (- rex-cs sldb-cs))
sldbs))))
(slime-connection-state-as-string new-state))))
(defun slime-compute-modeline-string (conn state pkg)
(concat (when (or conn pkg) "[")
(when pkg (format "%s" pkg))
(when (and (or conn state) pkg) ", ")
(when conn (format "%s" conn))
(when state (format "{%s}" state))
(when (or conn pkg) "]")))
(defun slime-update-modeline-string ()
(let ((old-pkg slime-modeline-package)
(old-conn slime-modeline-connection-name)
(old-state slime-modeline-connection-state)
(new-pkg (slime-compute-modeline-package))
(new-conn (slime-compute-modeline-connection))
(new-state (slime-compute-modeline-connection-state)))
(when (or (not (equal old-pkg new-pkg))
(not (equal old-conn new-conn))
(not (equal old-state new-state)))
(setq slime-modeline-package new-pkg)
(setq slime-modeline-connection-name new-conn)
(setq slime-modeline-connection-state new-state)
(setq slime-modeline-string
(slime-compute-modeline-string new-conn new-state new-pkg)))))
(defun slime-shall-we-update-modeline-p ()
(and slime-extended-modeline
(or slime-mode slime-popup-buffer-mode)))
(defun slime-update-all-modelines ()
(dolist (window (window-list))
(with-current-buffer (window-buffer window)
(when (slime-shall-we-update-modeline-p)
(slime-update-modeline-string)
(force-mode-line-update)))))
(defvar slime-modeline-update-timer nil)
(defun slime-restart-or-init-modeline-update-timer ()
(when slime-modeline-update-timer
(cancel-timer slime-modeline-update-timer))
(setq slime-modeline-update-timer
(run-with-idle-timer 0.1 nil 'slime-update-all-modelines)))
(slime-restart-or-init-modeline-update-timer)
(defun slime-recompute-modelines (delay)
(cond (delay
;; Minimize flashing of modeline due to short lived
;; requests such as those of autodoc.
(slime-restart-or-init-modeline-update-timer))
(t
;; Must do this ourselves since emacs may have
;; been idling long enough that
;; SLIME-MODELINE-UPDATE-TIMER is not going to
;; trigger by itself.
(slime-update-all-modelines))))
;; Setup the mode-line to say when we're in slime-mode, which
;; connection is active, and which CL package we think the current
;; buffer belongs to.
(add-to-list 'minor-mode-alist
'(slime-mode
(" Slime" slime-modeline-string)))
;;;;; Key bindings
(defvar slime-parent-map (make-sparse-keymap)
"Parent keymap for shared between all Slime related modes.")
(defvar slime-parent-bindings
'(("\M-." slime-edit-definition)
("\M-," slime-pop-find-definition-stack)
("\M-_" slime-edit-uses) ; for German layout
("\M-?" slime-edit-uses) ; for USian layout
("\C-x4." slime-edit-definition-other-window)
("\C-x5." slime-edit-definition-other-frame)
("\C-x\C-e" slime-eval-last-expression)
("\C-\M-x" slime-eval-defun)
;; Include PREFIX keys...
("\C-c" slime-prefix-map)))
(defvar slime-prefix-map (make-sparse-keymap)
"Keymap for commands prefixed with `slime-prefix-key'.")
(defvar slime-prefix-bindings
'(("\C-r" slime-eval-region)
(":" slime-interactive-eval)
("\C-e" slime-interactive-eval)
("E" slime-edit-value)
("\C-l" slime-load-file)
("\C-b" slime-interrupt)
("\M-d" slime-disassemble-symbol)
("\C-t" slime-toggle-trace-fdefinition)
("I" slime-inspect)
("\C-xt" slime-list-threads)
("\C-xc" slime-list-connections)
("<" slime-list-callers)
(">" slime-list-callees)
;; Include DOC keys...
("\C-d" slime-doc-map)
;; Include XREF WHO-FOO keys...
("\C-w" slime-who-map)
))
;;; These keys are useful for buffers where the user can insert and
;;; edit s-exprs, e.g. for source buffers and the REPL.
(defvar slime-editing-keys
`(;; Arglist display & completion
("\M-\t" slime-complete-symbol)
(" " slime-space)
;; Evaluating
;;("\C-x\M-e" slime-eval-last-expression-display-output :inferior t)
("\C-c\C-p" slime-pprint-eval-last-expression)
;; Macroexpand
("\C-c\C-m" slime-macroexpand-1)
("\C-c\M-m" slime-macroexpand-all)
;; Misc
("\C-c\C-u" slime-undefine-function)
(,(kbd "C-M-.") slime-next-location)
;; Obsolete, redundant bindings
("\C-c\C-i" slime-complete-symbol)
;;("\M-*" pop-tag-mark) ; almost to clever
))
(defvar slime-keys
(append slime-editing-keys
'( ;; Compiler notes
("\M-p" slime-previous-note)
("\M-n" slime-next-note)
("\C-c\M-c" slime-remove-notes)
("\C-c\C-k" slime-compile-and-load-file)
("\C-c\M-k" slime-compile-file)
("\C-c\C-c" slime-compile-defun)
)))
(defun slime-nop ()
"The null command. Used to shadow currently-unused keybindings."
(interactive)
(call-interactively 'undefined))
(defvar slime-doc-map (make-sparse-keymap)
"Keymap for documentation commands. Bound to a prefix key.")
(defvar slime-doc-bindings
'((?a slime-apropos)
(?z slime-apropos-all)
(?p slime-apropos-package)
(?d slime-describe-symbol)
(?f slime-describe-function)
(?h slime-hyperspec-lookup)
(?~ common-lisp-hyperspec-format)
(?# common-lisp-hyperspec-lookup-reader-macro)))
(defvar slime-who-map (make-sparse-keymap)
"Keymap for who-xref commands. Bound to a prefix key.")
(defvar slime-who-bindings
'((?c slime-who-calls)
(?w slime-calls-who)
(?r slime-who-references)
(?b slime-who-binds)
(?s slime-who-sets)
(?m slime-who-macroexpands)
(?a slime-who-specializes)))
(defun slime-init-keymaps ()
"(Re)initialize the keymaps for `slime-mode'."
(interactive)
;; Documentation
(define-prefix-command 'slime-doc-map)
(slime-define-both-key-bindings slime-doc-map slime-doc-bindings)
;; Who-xref
(define-prefix-command 'slime-who-map)
(slime-define-both-key-bindings slime-who-map slime-who-bindings)
;; Prefix map
(define-prefix-command 'slime-prefix-map)
(loop for (key binding) in slime-prefix-bindings
do (define-key slime-prefix-map key binding))
;; Parent map
(setq slime-parent-map (make-sparse-keymap))
(loop for (key binding) in slime-parent-bindings
do (define-key slime-parent-map key binding))
;; Slime mode map
(set-keymap-parent slime-mode-map slime-parent-map)
(loop for (key command) in slime-keys
do (define-key slime-mode-map key command)))
(defun slime-define-both-key-bindings (keymap bindings)
(loop for (char command) in bindings do
;; We bind both unmodified and with control.
(define-key keymap `[,char] command)
(unless (equal char ?h) ; But don't bind C-h
(define-key keymap `[(control ,char)] command))))
(slime-init-keymaps)
;;;; Setup initial `slime-mode' hooks
(make-variable-buffer-local
(defvar slime-pre-command-actions nil
"List of functions to execute before the next Emacs command.
This list of flushed between commands."))
(defun slime-pre-command-hook ()
"Execute all functions in `slime-pre-command-actions', then NIL it."
(dolist (undo-fn slime-pre-command-actions)
(funcall undo-fn))
(setq slime-pre-command-actions nil))
(defun slime-post-command-hook ()
(when (null pre-command-hook) ; sometimes this is lost
(add-hook 'pre-command-hook 'slime-pre-command-hook)))
(defun slime-setup-command-hooks ()
"Setup a buffer-local `pre-command-hook' to call `slime-pre-command-hook'."
(slime-add-local-hook 'pre-command-hook 'slime-pre-command-hook)
(slime-add-local-hook 'post-command-hook 'slime-post-command-hook))
;;;; Framework'ey bits
;;;
;;; This section contains some standard SLIME idioms: basic macros,
;;; ways of showing messages to the user, etc. All the code in this
;;; file should use these functions when applicable.
;;;
;;;;; Syntactic sugar
(defmacro* when-let ((var value) &rest body)
"Evaluate VALUE, and if the result is non-nil bind it to VAR and
evaluate BODY.
\(fn (VAR VALUE) &rest BODY)"
`(let ((,var ,value))
(when ,var ,@body)))
(put 'when-let 'lisp-indent-function 1)
(defmacro destructure-case (value &rest patterns)
"Dispatch VALUE to one of PATTERNS.
A cross between `case' and `destructuring-bind'.
The pattern syntax is:
((HEAD . ARGS) . BODY)
The list of patterns is searched for a HEAD `eq' to the car of
VALUE. If one is found, the BODY is executed with ARGS bound to the
corresponding values in the CDR of VALUE."
(let ((operator (gensym "op-"))
(operands (gensym "rand-"))
(tmp (gensym "tmp-")))
`(let* ((,tmp ,value)
(,operator (car ,tmp))
(,operands (cdr ,tmp)))
(case ,operator
,@(mapcar (lambda (clause)
(if (eq (car clause) t)
`(t ,@(cdr clause))
(destructuring-bind ((op &rest rands) &rest body) clause
`(,op (destructuring-bind ,rands ,operands
. ,body)))))
patterns)
,@(if (eq (caar (last patterns)) t)
'()
`((t (error "Elisp destructure-case failed: %S" ,tmp))))))))
(put 'destructure-case 'lisp-indent-function 1)
(defmacro slime-define-keys (keymap &rest key-command)
"Define keys in KEYMAP. Each KEY-COMMAND is a list of (KEY COMMAND)."
`(progn . ,(mapcar (lambda (k-c) `(define-key ,keymap . ,k-c))
key-command)))
(put 'slime-define-keys 'lisp-indent-function 1)
(defmacro* with-struct ((conc-name &rest slots) struct &body body)
"Like with-slots but works only for structs.
\(fn (CONC-NAME &rest SLOTS) STRUCT &body BODY)"
(flet ((reader (slot) (intern (concat (symbol-name conc-name)
(symbol-name slot)))))
(let ((struct-var (gensym "struct")))
`(let ((,struct-var ,struct))
(symbol-macrolet
,(mapcar (lambda (slot)
(etypecase slot
(symbol `(,slot (,(reader slot) ,struct-var)))
(cons `(,(first slot) (,(reader (second slot))
,struct-var)))))
slots)
. ,body)))))
(put 'with-struct 'lisp-indent-function 2)
;;;;; Very-commonly-used functions
(defvar slime-message-function 'message)
;; Interface
(defun slime-message (format &rest args)
"Like `message' but with special support for multi-line messages.
Single-line messages use the echo area."
(apply slime-message-function format args))
(defun slime-display-warning (message &rest args)
(display-warning '(slime warning) (apply #'format message args)))
(when (or (featurep 'xemacs))
(setq slime-message-function 'slime-format-display-message))
(defun slime-format-display-message (format &rest args)
(slime-display-message (apply #'format format args) "*SLIME Note*"))
(defun slime-display-message (message buffer-name)
"Display MESSAGE in the echo area or in BUFFER-NAME.
Use the echo area if MESSAGE needs only a single line. If the MESSAGE
requires more than one line display it in BUFFER-NAME and add a hook
to `slime-pre-command-actions' to remove the window before the next
command."
(when (get-buffer-window buffer-name) (delete-windows-on buffer-name))
(cond ((or (string-match "\n" message)
(> (length message) (1- (frame-width))))
(lexical-let ((buffer (get-buffer-create buffer-name)))
(with-current-buffer buffer
(erase-buffer)
(insert message)
(goto-char (point-min))
(let ((win (slime-create-message-window)))
(set-window-buffer win (current-buffer))
(shrink-window-if-larger-than-buffer
(display-buffer (current-buffer)))))
(push (lambda () (delete-windows-on buffer) (bury-buffer buffer))
slime-pre-command-actions)))
(t (message "%s" message))))
(defun slime-create-message-window ()
"Create a window at the bottom of the frame, above the minibuffer."
(let ((previous (previous-window (minibuffer-window))))
(when (<= (window-height previous) (* 2 window-min-height))
(save-selected-window
(select-window previous)
(enlarge-window (- (1+ (* 2 window-min-height))
(window-height previous)))))
(split-window previous)))
(defvar slime-background-message-function 'slime-display-oneliner)
;; Interface
(defun slime-background-message (format-string &rest format-args)
"Display a message in passing.
This is like `slime-message', but less distracting because it
will never pop up a buffer or display multi-line messages.
It should be used for \"background\" messages such as argument lists."
(apply slime-background-message-function format-string format-args))
(defun slime-display-oneliner (format-string &rest format-args)
(let* ((msg (apply #'format format-string format-args)))
(unless (minibuffer-window-active-p (minibuffer-window))
(message "%s" (slime-oneliner msg)))))
(defun slime-oneliner (string)
"Return STRING truncated to fit in a single echo-area line."
(substring string 0 (min (length string)
(or (position ?\n string) most-positive-fixnum)
(1- (frame-width)))))
(defun slime-bug (message &rest args)
(slime-display-warning
"%S:%d:%d (pt=%d).
%s
This is a bug in Slime itself. Please report this to the
mailinglist slime-devel@common-lisp.net and include your Emacs
version, the guilty Lisp source file, the header of this
message, and the following backtrace.
Backtrace:
%s
--------------------------------------------------------------
"
(buffer-name)
(line-number-at-pos)
(current-column)
(point)
(apply #'format message args)
(with-output-to-string (backtrace))))
;; Interface
(defun slime-set-truncate-lines ()
"Apply `slime-truncate-lines' to the current buffer."
(when slime-truncate-lines
(set (make-local-variable 'truncate-lines) t)))
;; Interface
(defun slime-read-package-name (prompt &optional initial-value)
"Read a package name from the minibuffer, prompting with PROMPT."
(let ((completion-ignore-case t))
(completing-read prompt (slime-bogus-completion-alist
(slime-eval
`(swank:list-all-package-names t)))
nil t initial-value)))
;; Interface
(defun slime-read-symbol-name (prompt &optional query)
"Either read a symbol name or choose the one at point.
The user is prompted if a prefix argument is in effect, if there is no
symbol at point, or if QUERY is non-nil.
This function avoids mistaking the REPL prompt for a symbol."
(cond ((or current-prefix-arg query (not (slime-symbol-at-point)))
(slime-read-from-minibuffer prompt (slime-symbol-at-point)))
(t (slime-symbol-at-point))))
;; Interface
(defmacro slime-propertize-region (props &rest body)
"Execute BODY and add PROPS to all the text it inserts.
More precisely, PROPS are added to the region between the point's
positions before and after executing BODY."
(let ((start (gensym)))
`(let ((,start (point)))
(prog1 (progn ,@body)
(add-text-properties ,start (point) ,props)))))
(put 'slime-propertize-region 'lisp-indent-function 1)
(defun slime-add-face (face string)
(add-text-properties 0 (length string) (list 'face face) string)
string)
(put 'slime-add-face 'lisp-indent-function 1)
;; Interface
(defsubst slime-insert-propertized (props &rest args)
"Insert all ARGS and then add text-PROPS to the inserted text."
(slime-propertize-region props (apply #'insert args)))
(defmacro slime-with-rigid-indentation (level &rest body)
"Execute BODY and then rigidly indent its text insertions.
Assumes all insertions are made at point."
(let ((start (gensym)) (l (gensym)))
`(let ((,start (point)) (,l ,(or level '(current-column))))
(prog1 (progn ,@body)
(slime-indent-rigidly ,start (point) ,l)))))
(put 'slime-with-rigid-indentation 'lisp-indent-function 1)
(defun slime-indent-rigidly (start end column)
;; Similar to `indent-rigidly' but doesn't inherit text props.
(let ((indent (make-string column ?\ )))
(save-excursion
(goto-char end)
(beginning-of-line)
(while (and (<= start (point))
(progn
(insert-before-markers indent)
(zerop (forward-line -1))))))))
(defun slime-insert-indented (&rest strings)
"Insert all arguments rigidly indented."
(slime-with-rigid-indentation nil
(apply #'insert strings)))
(defun slime-property-bounds (prop)
"Return two the positions of the previous and next changes to PROP.
PROP is the name of a text property."
(assert (get-text-property (point) prop))
(let ((end (next-single-char-property-change (point) prop)))
(list (previous-single-char-property-change end prop) end)))
(defun slime-curry (fun &rest args)
`(lambda (&rest more) (apply ',fun (append ',args more))))
(defun slime-rcurry (fun &rest args)
`(lambda (&rest more) (apply ',fun (append more ',args))))
;;;;; Snapshots of current Emacs state
;;; Window configurations do not save (and hence not restore)
;;; any narrowing that could be applied to a buffer.
;;;
;;; For this purpose, we introduce a superset of a window
;;; configuration that does include the necessary information to
;;; properly restore narrowing.
;;;
;;; We call this superset an Emacs Snapshot.
(defstruct (slime-narrowing-configuration
(:conc-name slime-narrowing-configuration.))
narrowedp beg end)
(defstruct (slime-emacs-snapshot (:conc-name slime-emacs-snapshot.))
;; We explicitly store the value of point even though it's implicitly
;; stored in the window-configuration because Emacs provides no
;; way to access the things stored in a window configuration.
window-configuration narrowing-configuration point-marker)
(defun slime-current-narrowing-configuration (&optional buffer)
(with-current-buffer (or buffer (current-buffer))
(make-slime-narrowing-configuration :narrowedp (slime-buffer-narrowed-p)
:beg (point-min-marker)
:end (point-max-marker))))
(defun slime-set-narrowing-configuration (narrowing-cfg)
(when (slime-narrowing-configuration.narrowedp narrowing-cfg)
(narrow-to-region (slime-narrowing-configuration.beg narrowing-cfg)
(slime-narrowing-configuration.end narrowing-cfg))))
(defun slime-current-emacs-snapshot (&optional frame)
"Returns a snapshot of the current state of FRAME, or the
currently active frame if FRAME is not given respectively."
(with-current-buffer
(if frame
(window-buffer (frame-selected-window (selected-frame)))
(current-buffer))
(make-slime-emacs-snapshot
:window-configuration (current-window-configuration frame)
:narrowing-configuration (slime-current-narrowing-configuration)
:point-marker (point-marker))))
(defun slime-set-emacs-snapshot (snapshot)
"Restores the state of Emacs according to the information saved
in SNAPSHOT."
(let ((window-cfg (slime-emacs-snapshot.window-configuration snapshot))
(narrowing-cfg (slime-emacs-snapshot.narrowing-configuration snapshot))
(marker (slime-emacs-snapshot.point-marker snapshot)))
(set-window-configuration window-cfg) ; restores previously current buffer.
(slime-set-narrowing-configuration narrowing-cfg)
(goto-char (marker-position marker))))
(defun slime-current-emacs-snapshot-fingerprint (&optional frame)
"Return a fingerprint of the current emacs snapshot.
Fingerprints are `equalp' if and only if they represent window
configurations that are very similar (same windows and buffers.)
Unlike real window-configuration objects, fingerprints are not
sensitive to the point moving and they can't be restored."
(mapcar (lambda (window) (list window (window-buffer window)))
(slime-frame-windows frame)))
(defun slime-frame-windows (&optional frame)
"Return the list of windows in FRAME."
(loop with last-window = (previous-window (frame-first-window frame))
for window = (frame-first-window frame) then (next-window window)
collect window
until (eq window last-window)))
;;;;; Temporary popup buffers
(defvar slime-popup-restore-data nil
"Data needed when closing popup windows.
This is used as buffer local variable.
The format is (POPUP-WINDOW SELECTED-WINDOW OLD-BUFFER).
POPUP-WINDOW is the window used to display the temp buffer.
That window may have been reused or freshly created.
SELECTED-WINDOW is the window that was selected before displaying
the popup buffer.
OLD-BUFFER is the buffer that was previously displayed in POPUP-WINDOW.
OLD-BUFFER is nil if POPUP-WINDOW was newly created.
See `view-return-to-alist' for a similar idea.")
;; keep compiler quiet
(defvar slime-buffer-package)
(defvar slime-buffer-connection)
;; Interface
(defmacro* slime-with-popup-buffer ((name &optional package connection select
emacs-snapshot)
&body body)
"Similar to `with-output-to-temp-buffer'.
Bind standard-output and initialize some buffer-local variables.
Restore window configuration when closed.
NAME is the name of the buffer to be created.
PACKAGE is the value `slime-buffer-package'.
CONNECTION is the value for `slime-buffer-connection'.
If nil, no explicit connection is associated with
the buffer. If t, the current connection is taken.
If EMACS-SNAPSHOT is non-NIL, it's used to restore the previous
state of Emacs after closing the temporary buffer. Otherwise, the
current state will be saved and later restored."
`(let* ((vars% (list ,(if (eq package t) '(slime-current-package) package)
,(if (eq connection t) '(slime-connection) connection)
;; Defer the decision for NILness until runtime.
(or ,emacs-snapshot (slime-current-emacs-snapshot))))
(standard-output (slime-make-popup-buffer ,name vars%)))
(with-current-buffer standard-output
(prog1 (progn ,@body)
(assert (eq (current-buffer) standard-output))
(setq buffer-read-only t)
(slime-init-popup-buffer vars%)
(set-window-point (slime-display-popup-buffer ,(or select 'nil))
(point))
(current-buffer)))))
(put 'slime-with-popup-buffer 'lisp-indent-function 1)
(defun slime-make-popup-buffer (name buffer-vars)
"Return a temporary buffer called NAME.
The buffer also uses the minor-mode `slime-popup-buffer-mode'."
(with-current-buffer (or (get-buffer name) (get-buffer-create name))
(kill-all-local-variables)
(setq buffer-read-only nil)
(erase-buffer)
(set-syntax-table lisp-mode-syntax-table)
(slime-init-popup-buffer buffer-vars)
(current-buffer)))
(defun slime-init-popup-buffer (buffer-vars)
(slime-popup-buffer-mode 1)
(multiple-value-setq (slime-buffer-package slime-buffer-connection)
buffer-vars))
(defun slime-display-popup-buffer (select)
"Display the current buffer.
Save the selected-window in a buffer-local variable, so that we
can restore it later."
(let ((selected-window (selected-window))
(old-windows))
(walk-windows (lambda (w) (push (cons w (window-buffer w)) old-windows))
nil t)
(let ((new-window (display-buffer (current-buffer))))
(unless slime-popup-restore-data
(set (make-local-variable 'slime-popup-restore-data)
(list new-window
selected-window
(cdr (find new-window old-windows :key #'car)))))
(when select
(select-window new-window))
new-window)))
(defun slime-close-popup-window ()
(when slime-popup-restore-data
(destructuring-bind (popup-window selected-window old-buffer)
slime-popup-restore-data
(bury-buffer)
(when (eq popup-window (selected-window))
(cond ((and (not old-buffer) (not (one-window-p)))
(delete-window popup-window))
((and old-buffer (buffer-live-p old-buffer))
(set-window-buffer popup-window old-buffer))))
(when (window-live-p selected-window)
(select-window selected-window)))
(kill-local-variable 'slime-popup-restore-data)))
(defmacro slime-save-local-variables (vars &rest body)
(let ((vals (make-symbol "vals")))
`(let ((,vals (mapcar (lambda (var)
(if (slime-local-variable-p var)
(cons var (eval var))))
',vars)))
(prog1 (progn . ,body)
(mapc (lambda (var+val)
(when (consp var+val)
(set (make-local-variable (car var+val)) (cdr var+val))))
,vals)))))
(put 'slime-save-local-variables 'lisp-indent-function 1)
(define-minor-mode slime-popup-buffer-mode
"Mode for displaying read only stuff"
nil
(" Slime-Tmp" slime-modeline-string)
'(("q" . slime-popup-buffer-quit-function)
;;("\C-c\C-z" . slime-switch-to-output-buffer)
("\M-." . slime-edit-definition)))
(set-keymap-parent slime-popup-buffer-mode-map slime-parent-map)
(make-variable-buffer-local
(defvar slime-popup-buffer-quit-function 'slime-popup-buffer-quit
"The function that is used to quit a temporary popup buffer."))
(defun slime-popup-buffer-quit-function (&optional kill-buffer-p)
"Wrapper to invoke the value of `slime-popup-buffer-quit-function'."
(interactive)
(funcall slime-popup-buffer-quit-function kill-buffer-p))
;; Interface
(defun slime-popup-buffer-quit (&optional kill-buffer-p)
"Get rid of the current (temp) buffer without asking.
Restore the window configuration unless it was changed since we
last activated the buffer."
(interactive)
(let ((buffer (current-buffer)))
(slime-close-popup-window)
(when kill-buffer-p
(kill-buffer buffer))))
;;;;; Filename translation
;;;
;;; Filenames passed between Emacs and Lisp should be translated using
;;; these functions. This way users who run Emacs and Lisp on separate
;;; machines have a chance to integrate file operations somehow.
(defvar slime-to-lisp-filename-function #'convert-standard-filename)
(defvar slime-from-lisp-filename-function #'identity)
(defun slime-to-lisp-filename (filename)
"Translate the string FILENAME to a Lisp filename."
(funcall slime-to-lisp-filename-function filename))
(defun slime-from-lisp-filename (filename)
"Translate the Lisp filename FILENAME to an Emacs filename."
(funcall slime-from-lisp-filename-function filename))
;;;; Starting SLIME
;;;
;;; This section covers starting an inferior-lisp, compiling and
;;; starting the server, initiating a network connection.
;;;;; Entry points
;; We no longer load inf-lisp, but we use this variable for backward
;; compatibility.
(defvar inferior-lisp-program "lisp"
"*Program name for invoking an inferior Lisp with for Inferior Lisp mode.")
(defvar slime-lisp-implementations nil
"*A list of known Lisp implementations.
The list should have the form:
((NAME (PROGRAM PROGRAM-ARGS...) &key INIT CODING-SYSTEM ENV) ...)
NAME is a symbol for the implementation.
PROGRAM and PROGRAM-ARGS are strings used to start the Lisp process.
INIT is a function that should return a string to load and start
Swank. The function will be called with the PORT-FILENAME and ENCODING as
arguments. INIT defaults to `slime-init-command'.
CODING-SYSTEM a symbol for the coding system. The default is
slime-net-coding-system
ENV environment variables for the subprocess (see `process-environment').
Here's an example:
((cmucl (\"/opt/cmucl/bin/lisp\" \"-quiet\") :init slime-init-command)
(acl (\"acl7\") :coding-system emacs-mule))")
(defvar slime-default-lisp nil
"*The name of the default Lisp implementation.
See `slime-lisp-implementations'")
;; dummy definitions for the compiler
(defvar slime-net-processes)
(defvar slime-default-connection)
;;;###autoload
(defun slime (&optional command coding-system)
"Start an inferior^_superior Lisp and connect to its Swank server."
(interactive)
(let ((inferior-lisp-program (or command inferior-lisp-program))
(slime-net-coding-system (or coding-system slime-net-coding-system)))
(slime-start* (cond ((and command (symbolp command))
(slime-lisp-options command))
(t (slime-read-interactive-args))))))
(defvar slime-inferior-lisp-program-history '()
"History list of command strings. Used by `slime'.")
(defun slime-read-interactive-args ()
"Return the list of args which should be passed to `slime-start'.
The rules for selecting the arguments are rather complicated:
- In the most common case, i.e. if there's no prefix-arg in
effect and if `slime-lisp-implementations' is nil, use
`inferior-lisp-program' as fallback.
- If the table `slime-lisp-implementations' is non-nil use the
implementation with name `slime-default-lisp' or if that's nil
the first entry in the table.
- If the prefix-arg is `-', prompt for one of the registered
lisps.
- If the prefix-arg is positive, read the command to start the
process."
(let ((table slime-lisp-implementations))
(cond ((not current-prefix-arg) (slime-lisp-options))
((eq current-prefix-arg '-)
(let ((key (completing-read
"Lisp name: " (mapcar (lambda (x)
(list (symbol-name (car x))))
table)
nil t)))
(slime-lookup-lisp-implementation table (intern key))))
(t
(destructuring-bind (program &rest program-args)
(split-string (read-string
"Run lisp: " inferior-lisp-program
'slime-inferior-lisp-program-history))
(let ((coding-system
(if (eq 16 (prefix-numeric-value current-prefix-arg))
(read-coding-system "set slime-coding-system: "
slime-net-coding-system)
slime-net-coding-system)))
(list :program program :program-args program-args
:coding-system coding-system)))))))
(defun slime-lisp-options (&optional name)
(let ((table slime-lisp-implementations))
(assert (or (not name) table))
(cond (table (slime-lookup-lisp-implementation slime-lisp-implementations
(or name slime-default-lisp
(car (car table)))))
(t (destructuring-bind (program &rest args)
(split-string inferior-lisp-program)
(list :program program :program-args args))))))
(defun slime-lookup-lisp-implementation (table name)
(destructuring-bind (name (prog &rest args) &rest keys) (assoc name table)
(list* :name name :program prog :program-args args keys)))
(defun* slime-start (&key (program inferior-lisp-program) program-args
directory
(coding-system slime-net-coding-system)
(init 'slime-init-command)
name
(buffer "*inferior-lisp*")
init-function
env)
(let ((args (list :program program :program-args program-args :buffer buffer
:coding-system coding-system :init init :name name
:init-function init-function :env env)))
(slime-check-coding-system coding-system)
(when (slime-bytecode-stale-p)
(slime-urge-bytecode-recompile))
(let ((proc (slime-maybe-start-lisp program program-args env
directory buffer)))
(slime-inferior-connect proc args)
(pop-to-buffer (process-buffer proc)))))
(defun slime-start* (options)
(apply #'slime-start options))
;;;###autoload
(defun slime-connect (host port &optional coding-system)
"Connect to a running Swank server. Returns the connection."
(interactive (list (read-from-minibuffer "Host: " slime-lisp-host)
(read-from-minibuffer "Port: " (format "%d" slime-port)
nil t)))
(when (and (interactive-p) slime-net-processes
(y-or-n-p "Close old connections first? "))
(slime-disconnect-all))
(message "Connecting to Swank on port %S.." port)
(let ((coding-system (or coding-system slime-net-coding-system)))
(slime-check-coding-system coding-system)
(message "Connecting to Swank on port %S.." port)
(let* ((process (slime-net-connect host port coding-system))
(slime-dispatching-connection process))
(slime-setup-connection process))))
;;(defun slime-start-and-load (filename &optional package)
;; "Start Slime, if needed, load the current file and set the package."
;; (interactive (list (expand-file-name (buffer-file-name))
;; (slime-find-buffer-package)))
;; (cond ((slime-connected-p)
;; (slime-load-file-set-package filename package))
;; (t
;; (slime-start-and-init (slime-lisp-options)
;; (slime-curry #'slime-start-and-load
;; filename package)))))
(defun slime-start-and-init (options fun)
(let* ((rest (plist-get options :init-function))
(init (cond (rest `(lambda () (funcall ',rest) (funcall ',fun)))
(t fun))))
(slime-start* (plist-put (copy-list options) :init-function init))))
;;(defun slime-load-file-set-package (filename package)
;; (let ((filename (slime-to-lisp-filename filename)))
;; (slime-eval-async `(swank:load-file ,filename)
;; (lexical-let ((package package))
;; (lambda (ignored)
;; (slime-repl-set-package package))))))
;;;;; Start inferior lisp
;;;
;;; Here is the protocol for starting SLIME:
;;;
;;; 0. Emacs recompiles/reloads slime.elc if it exists and is stale.
;;; 1. Emacs starts an inferior Lisp process.
;;; 2. Emacs tells Lisp (via stdio) to load and start Swank.
;;; 3. Lisp recompiles the Swank if needed.
;;; 4. Lisp starts the Swank server and writes its TCP port to a temp file.
;;; 5. Emacs reads the temp file to get the port and then connects.
;;; 6. Emacs prints a message of warm encouragement for the hacking ahead.
;;;
;;; Between steps 2-5 Emacs polls for the creation of the temp file so
;;; that it can make the connection. This polling may continue for a
;;; fair while if Swank needs recompilation.
(defvar slime-connect-retry-timer nil
"Timer object while waiting for an inferior-lisp to start.")
;;; Recompiling bytecode:
(defun slime-bytecode-stale-p ()
"Return true if slime.elc is older than slime.el."
(when-let (libfile (locate-library "slime"))
(let* ((basename (file-name-sans-extension libfile))
(sourcefile (concat basename ".el"))
(bytefile (concat basename ".elc")))
(and (file-exists-p bytefile)
(file-newer-than-file-p sourcefile bytefile)))))
(defun slime-recompile-bytecode ()
"Recompile and reload slime.
Warning: don't use this in XEmacs, it seems to crash it!"
(interactive)
(let ((sourcefile (concat (file-name-sans-extension (locate-library "slime"))
".el")))
(byte-compile-file sourcefile t)))
(defun slime-urge-bytecode-recompile ()
"Urge the user to recompile slime.elc.
Return true if we have been given permission to continue."
(cond ((featurep 'xemacs)
;; My XEmacs crashes and burns if I recompile/reload an elisp
;; file from itself. So they have to do it themself.
(or (y-or-n-p "slime.elc is older than source. Continue? ")
(signal 'quit nil)))
((y-or-n-p "slime.elc is older than source. Recompile first? ")
(slime-recompile-bytecode))
(t)))
(defun slime-abort-connection ()
"Abort connection the current connection attempt."
(interactive)
(cond (slime-connect-retry-timer
(slime-cancel-connect-retry-timer)
(message "Cancelled connection attempt."))
(t (error "Not connecting"))))
;;; Starting the inferior Lisp and loading Swank:
(defun slime-maybe-start-lisp (program program-args env directory buffer)
"Return a new or existing inferior lisp process."
(cond ((not (comint-check-proc buffer))
(slime-start-lisp program program-args env directory buffer))
((slime-reinitialize-inferior-lisp-p program program-args env buffer)
(when-let (conn (find (get-buffer-process buffer) slime-net-processes
:key #'slime-inferior-process))
(slime-net-close conn))
(get-buffer-process buffer))
(t (slime-start-lisp program program-args env directory
(generate-new-buffer-name buffer)))))
(defun slime-reinitialize-inferior-lisp-p (program program-args env buffer)
(let ((args (slime-inferior-lisp-args (get-buffer-process buffer))))
(and (equal (plist-get args :program) program)
(equal (plist-get args :program-args) program-args)
(equal (plist-get args :env) env)
(not (y-or-n-p "Create an additional *inferior-lisp*? ")))))
(defvar slime-inferior-process-start-hook nil
"Hook called whenever a new process gets started.")
(defun slime-start-lisp (program program-args env directory buffer)
"Does the same as `inferior-lisp' but less ugly.
Return the created process."
(with-current-buffer (get-buffer-create buffer)
(when directory
(cd (expand-file-name directory)))
(comint-mode)
(let ((process-environment (append env process-environment))
(process-connection-type nil))
(comint-exec (current-buffer) "inferior-lisp" program nil program-args))
(lisp-mode-variables t)
(let ((proc (get-buffer-process (current-buffer))))
(slime-set-query-on-exit-flag proc)
(run-hooks 'slime-inferior-process-start-hook)
proc)))
(defun slime-inferior-connect (process args)
"Start a Swank server in the inferior Lisp and connect."
(slime-delete-swank-port-file 'quiet)
(slime-start-swank-server process args)
(slime-read-port-and-connect process nil))
(defvar slime-inferior-lisp-args nil
"A buffer local variable in the inferior proccess.")
(defun slime-start-swank-server (process args)
"Start a Swank server on the inferior lisp."
(destructuring-bind (&key coding-system init &allow-other-keys) args
(with-current-buffer (process-buffer process)
(make-local-variable 'slime-inferior-lisp-args)
(setq slime-inferior-lisp-args args)
(let ((str (funcall init (slime-swank-port-file) coding-system)))
(goto-char (process-mark process))
(insert-before-markers str)
(process-send-string process str)))))
(defun slime-inferior-lisp-args (process)
(with-current-buffer (process-buffer process)
slime-inferior-lisp-args))
;; XXX load-server & start-server used to be separated. maybe that was better.
(defun slime-init-command (port-filename coding-system)
"Return a string to initialize Lisp."
(let ((loader (if (file-name-absolute-p slime-backend)
slime-backend
(concat slime-path slime-backend)))
(encoding (slime-coding-system-cl-name coding-system)))
;; Return a single form to avoid problems with buffered input.
(format "%S\n\n"
`(progn
(load ,(expand-file-name loader) :verbose t)
(funcall (read-from-string "swank-loader:init"))
(funcall (read-from-string "swank:start-server")
,port-filename
:coding-system ,encoding)))))
(defun slime-swank-port-file ()
"Filename where the SWANK server writes its TCP port number."
(concat (file-name-as-directory (slime-temp-directory))
(format "slime.%S" (emacs-pid))))
(defun slime-temp-directory ()
(cond ((fboundp 'temp-directory) (temp-directory))
((boundp 'temporary-file-directory) temporary-file-directory)
(t "/tmp/")))
(defun slime-delete-swank-port-file (&optional quiet)
(condition-case data
(delete-file (slime-swank-port-file))
(error
(ecase quiet
((nil) (signal (car data) (cdr data)))
(quiet)
(message (message "Unable to delete swank port file %S"
(slime-swank-port-file)))))))
(defun slime-read-port-and-connect (inferior-process retries)
(slime-cancel-connect-retry-timer)
(slime-attempt-connection inferior-process retries 1))
(defun slime-attempt-connection (process retries attempt)
;; A small one-state machine to attempt a connection with
;; timer-based retries.
(let ((file (slime-swank-port-file)))
(unless (active-minibuffer-window)
(message "Polling %S.. (Abort with `M-x slime-abort-connection'.)" file))
(cond ((and (file-exists-p file)
(> (nth 7 (file-attributes file)) 0)) ; file size
(slime-cancel-connect-retry-timer)
(let ((port (slime-read-swank-port))
(args (slime-inferior-lisp-args process)))
(slime-delete-swank-port-file 'message)
(let ((c (slime-connect slime-lisp-host port
(plist-get args :coding-system))))
(slime-set-inferior-process c process))))
((and retries (zerop retries))
(slime-cancel-connect-retry-timer)
(message "Failed to connect to Swank."))
(t
(when (and (file-exists-p file)
(zerop (nth 7 (file-attributes file))))
(message "(Zero length port file)")
;; the file may be in the filesystem but not yet written
(unless retries (setq retries 3)))
(unless slime-connect-retry-timer
(setq slime-connect-retry-timer
(run-with-timer
0.3 0.3
#'slime-timer-call #'slime-attempt-connection
process (and retries (1- retries))
(1+ attempt))))))))
(defun slime-timer-call (fun &rest args)
"Call function FUN with ARGS, reporting all errors.
The default condition handler for timer functions (see
`timer-event-handler') ignores errors."
(condition-case data
(apply fun args)
(error (debug nil (list "Error in timer" fun args data)))))
(defun slime-cancel-connect-retry-timer ()
(when slime-connect-retry-timer
(cancel-timer slime-connect-retry-timer)
(setq slime-connect-retry-timer nil)))
(defun slime-read-swank-port ()
"Read the Swank server port number from the `slime-swank-port-file'."
(save-excursion
(with-temp-buffer
(insert-file-contents (slime-swank-port-file))
(goto-char (point-min))
(let ((port (read (current-buffer))))
(assert (integerp port))
port))))
;;; Words of encouragement
(defun slime-user-first-name ()
(let ((name (if (string= (user-full-name) "")
(user-login-name)
(user-full-name))))
(string-match "^[^ ]*" name)
(capitalize (match-string 0 name))))
(defvar slime-words-of-encouragement
`("Let the hacking commence!"
"Hacks and glory await!"
"Hack and be merry!"
"Your hacking starts... NOW!"
"May the source be with you!"
"Take this REPL, brother, and may it serve you well."
"Lemonodor-fame is but a hack away!"
,(format "%s, this could be the start of a beautiful program."
(slime-user-first-name)))
"Scientifically-proven optimal words of hackerish encouragement.")
(defun slime-random-words-of-encouragement ()
"Return a string of hackerish encouragement."
(eval (nth (random (length slime-words-of-encouragement))
slime-words-of-encouragement)))
;;;; Networking
;;;
;;; This section covers the low-level networking: establishing
;;; connections and encoding/decoding protocol messages.
;;;
;;; Each SLIME protocol message beings with a 3-byte length header
;;; followed by an S-expression as text. The sexp must be readable
;;; both by Emacs and by Common Lisp, so if it contains any embedded
;;; code fragments they should be sent as strings.
;;;
;;; The set of meaningful protocol messages are not specified
;;; here. They are defined elsewhere by the event-dispatching
;;; functions in this file and in swank.lisp.
(defvar slime-net-processes nil
"List of processes (sockets) connected to Lisps.")
(defvar slime-net-process-close-hooks '()
"List of functions called when a slime network connection closes.
The functions are called with the process as their argument.")
(defun slime-secret ()
"Finds the magic secret from the user's home directory.
Returns nil if the file doesn't exist or is empty; otherwise the first
line of the file."
(condition-case err
(with-temp-buffer
(insert-file-contents "~/.slime-secret")
(goto-char (point-min))
(buffer-substring (point-min) (line-end-position)))
(file-error nil)))
;;; Interface
(defun slime-net-connect (host port coding-system)
"Establish a connection with a CL."
(let* ((inhibit-quit nil)
(proc (open-network-stream "SLIME Lisp" nil host port))
(buffer (slime-make-net-buffer " *cl-connection*")))
(push proc slime-net-processes)
(set-process-buffer proc buffer)
(set-process-filter proc 'slime-net-filter)
(set-process-sentinel proc 'slime-net-sentinel)
(slime-set-query-on-exit-flag proc)
(when (fboundp 'set-process-coding-system)
(slime-check-coding-system coding-system)
(set-process-coding-system proc coding-system coding-system))
(when-let (secret (slime-secret))
(slime-net-send secret proc))
proc))
(defun slime-make-net-buffer (name)
"Make a buffer suitable for a network process."
(let ((buffer (generate-new-buffer name)))
(with-current-buffer buffer
(buffer-disable-undo)
(set (make-local-variable 'kill-buffer-query-functions) nil))
buffer))
(defun slime-set-query-on-exit-flag (process)
"Set PROCESS's query-on-exit-flag to `slime-kill-without-query-p'."
(when slime-kill-without-query-p
;; avoid byte-compiler warnings
(let ((fun (if (fboundp 'set-process-query-on-exit-flag)
'set-process-query-on-exit-flag
'process-kill-without-query)))
(funcall fun process nil))))
;;;;; Coding system madness
(defun slime-check-coding-system (coding-system)
"Signal an error if CODING-SYSTEM isn't a valid coding system."
(interactive)
(let ((props (slime-find-coding-system coding-system)))
(unless props
(error "Invalid slime-net-coding-system: %s. %s"
coding-system (mapcar #'car slime-net-valid-coding-systems)))
(when (and (second props) (boundp 'default-enable-multibyte-characters))
(assert default-enable-multibyte-characters))
t))
(defun slime-coding-system-mulibyte-p (coding-system)
(second (slime-find-coding-system coding-system)))
(defun slime-coding-system-cl-name (coding-system)
(third (slime-find-coding-system coding-system)))
;;; Interface
(defun slime-net-send (sexp proc)
"Send a SEXP to Lisp over the socket PROC.
This is the lowest level of communication. The sexp will be READ and
EVAL'd by Lisp."
(let* ((msg (concat (slime-prin1-to-string sexp) "\n"))
(string (concat (slime-net-encode-length (length msg)) msg))
(coding-system (cdr (process-coding-system proc))))
(slime-log-event sexp)
(cond ((slime-safe-encoding-p coding-system string)
(process-send-string proc string))
(t (error "Coding system %s not suitable for %S"
coding-system string)))))
(defun slime-safe-encoding-p (coding-system string)
"Return true iff CODING-SYSTEM can safely encode STRING."
(if (featurep 'xemacs)
;; FIXME: XEmacs encodes non-encodeable chars as ?~ automatically
t
(or (let ((candidates (find-coding-systems-string string))
(base (coding-system-base coding-system)))
(or (equal candidates '(undecided))
(memq base candidates)))
(and (not (multibyte-string-p string))
(not (slime-coding-system-mulibyte-p coding-system))))))
(defun slime-net-close (process &optional debug)
(setq slime-net-processes (remove process slime-net-processes))
(when (eq process slime-default-connection)
(setq slime-default-connection nil))
(cond (debug
(set-process-sentinel process 'ignore)
(set-process-filter process 'ignore)
(delete-process process))
(t
(run-hook-with-args 'slime-net-process-close-hooks process)
;; killing the buffer also closes the socket
(kill-buffer (process-buffer process)))))
(defun slime-net-sentinel (process message)
(message "Lisp connection closed unexpectedly: %s" message)
(slime-net-close process))
;;; Socket input is handled by `slime-net-filter', which decodes any
;;; complete messages and hands them off to the event dispatcher.
(defun slime-net-filter (process string)
"Accept output from the socket and process all complete messages."
(with-current-buffer (process-buffer process)
(goto-char (point-max))
(insert string))
(slime-process-available-input process))
(defun slime-process-available-input (process)
"Process all complete messages that have arrived from Lisp."
(with-current-buffer (process-buffer process)
(while (slime-net-have-input-p)
(let ((event (slime-net-read-or-lose process))
(ok nil))
(slime-log-event event)
(unwind-protect
(save-current-buffer
(slime-dispatch-event event process)
(setq ok t))
(unless ok
(slime-run-when-idle 'slime-process-available-input process)))))))
(defun slime-net-have-input-p ()
"Return true if a complete message is available."
(goto-char (point-min))
(and (>= (buffer-size) 6)
(>= (- (buffer-size) 6) (slime-net-decode-length))))
(defun slime-run-when-idle (function &rest args)
"Call FUNCTION as soon as Emacs is idle."
(apply #'run-at-time
(if (featurep 'xemacs) itimer-short-interval 0)
nil function args))
(defun slime-net-read-or-lose (process)
(condition-case error
(slime-net-read)
(error
(debug)
(slime-net-close process t)
(error "net-read error: %S" error))))
(defun slime-net-read ()
"Read a message from the network buffer."
(goto-char (point-min))
(let* ((length (slime-net-decode-length))
(start (+ 6 (point)))
(end (+ start length)))
(assert (plusp length))
(prog1 (save-restriction
(narrow-to-region start end)
(read (current-buffer)))
(delete-region (point-min) end))))
(defun slime-net-decode-length ()
"Read a 24-bit hex-encoded integer from buffer."
(string-to-number (buffer-substring-no-properties (point) (+ (point) 6)) 16))
(defun slime-net-encode-length (n)
"Encode an integer into a 24-bit hex string."
(format "%06x" n))
(defun slime-prin1-to-string (sexp)
"Like `prin1-to-string' but don't octal-escape non-ascii characters.
This is more compatible with the CL reader."
(with-temp-buffer
(let (print-escape-nonascii
print-escape-newlines
print-length
print-level)
(prin1 sexp (current-buffer))
(buffer-string))))
;;;; Connections
;;;
;;; "Connections" are the high-level Emacs<->Lisp networking concept.
;;;
;;; Emacs has a connection to each Lisp process that it's interacting
;;; with. Typically there would only be one, but a user can choose to
;;; connect to many Lisps simultaneously.
;;;
;;; A connection consists of a control socket, optionally an extra
;;; socket dedicated to receiving Lisp output (an optimization), and a
;;; set of connection-local state variables.
;;;
;;; The state variables are stored as buffer-local variables in the
;;; control socket's process-buffer and are used via accessor
;;; functions. These variables include things like the *FEATURES* list
;;; and Unix Pid of the Lisp process.
;;;
;;; One connection is "current" at any given time. This is:
;;; `slime-dispatching-connection' if dynamically bound, or
;;; `slime-buffer-connection' if this is set buffer-local, or
;;; `slime-default-connection' otherwise.
;;;
;;; When you're invoking commands in your source files you'll be using
;;; `slime-default-connection'. This connection can be interactively
;;; reassigned via the connection-list buffer.
;;;
;;; When a command creates a new buffer it will set
;;; `slime-buffer-connection' so that commands in the new buffer will
;;; use the connection that the buffer originated from. For example,
;;; the apropos command creates the *Apropos* buffer and any command
;;; in that buffer (e.g. `M-.') will go to the same Lisp that did the
;;; apropos search. REPL buffers are similarly tied to their
;;; respective connections.
;;;
;;; When Emacs is dispatching some network message that arrived from a
;;; connection it will dynamically bind `slime-dispatching-connection'
;;; so that the event will be processed in the context of that
;;; connection.
;;;
;;; This is mostly transparent. The user should be aware that he can
;;; set the default connection to pick which Lisp handles commands in
;;; Lisp-mode source buffers, and slime hackers should be aware that
;;; they can tie a buffer to a specific connection. The rest takes
;;; care of itself.
(defvar slime-dispatching-connection nil
"Network process currently executing.
This is dynamically bound while handling messages from Lisp; it
overrides `slime-buffer-connection' and `slime-default-connection'.")
(make-variable-buffer-local
(defvar slime-buffer-connection nil
"Network connection to use in the current buffer.
This overrides `slime-default-connection'."))
(defvar slime-default-connection nil
"Network connection to use by default.
Used for all Lisp communication, except when overridden by
`slime-dispatching-connection' or `slime-buffer-connection'.")
(defun slime-current-connection ()
"Return the connection to use for Lisp interaction.
Return nil if there's no connection."
(or slime-dispatching-connection
slime-buffer-connection
slime-default-connection))
(defun slime-connection ()
"Return the connection to use for Lisp interaction.
Signal an error if there's no connection."
(let ((conn (slime-current-connection)))
(cond ((and (not conn) slime-net-processes)
(or (slime-auto-select-connection)
(error "No default connection selected.")))
((not conn)
(or (slime-auto-connect)
(error "Not connected.")))
((not (eq (process-status conn) 'open))
(error "Connection closed."))
(t conn))))
(defcustom slime-auto-connect 'never
"Controls auto connection when information from lisp process is needed.
This doesn't mean it will connect right after Slime is loaded."
:group 'slime-mode
:type '(choice (const never)
(const always)
(const ask)))
(defun slime-auto-connect ()
(cond ((or (eq slime-auto-connect 'always)
(and (eq slime-auto-connect 'ask)
(y-or-n-p "No connection. Start Slime? ")))
(save-window-excursion
(slime)
(while (not (slime-current-connection))
(sleep-for 1))
(slime-connection)))
(t nil)))
(defcustom slime-auto-select-connection 'ask
"Controls auto selection after the default connection was quited."
:group 'slime-mode
:type '(choice (const never)
(const always)
(const ask)))
(defun slime-auto-select-connection ()
(let* ((c0 (car slime-net-processes))
(c (cond ((eq slime-auto-select-connection 'always) c0)
((and (eq slime-auto-select-connection 'ask)
(y-or-n-p
(format "No default connection selected. %s %s? "
"Switch to" (slime-connection-name c0))))
c0))))
(when c
(slime-select-connection c)
(message "Switching to connection: %s" (slime-connection-name c))
c)))
(defun slime-select-connection (process)
"Make PROCESS the default connection."
(setq slime-default-connection process))
(defun slime-cycle-connections ()
"Change current slime connection, and make it buffer local."
(interactive)
(let* ((tail (or (cdr (member (slime-current-connection)
slime-net-processes))
slime-net-processes))
(p (car tail)))
(slime-select-connection p)
;; (unless (eq major-mode 'slime-repl-mode)
;; (setq slime-buffer-connection p))
(message "Lisp: %s %s" (slime-connection-name p) (process-contact p))))
(defmacro* slime-with-connection-buffer ((&optional process) &rest body)
"Execute BODY in the process-buffer of PROCESS.
If PROCESS is not specified, `slime-connection' is used.
\(fn (&optional PROCESS) &body BODY))"
`(with-current-buffer
(process-buffer (or ,process (slime-connection)
(error "No connection")))
,@body))
(put 'slime-with-connection-buffer 'lisp-indent-function 1)
(defun slime-compute-connection-state (conn)
(cond ((null conn) :disconnected)
((slime-stale-connection-p conn) :stale)
((and (slime-use-sigint-for-interrupt conn)
(slime-busy-p conn)) :busy)
((eq slime-buffer-connection conn) :local)
(t :connected)))
(defun slime-connection-state-as-string (state)
(case state
(:disconnected "not connected")
(:busy "busy..")
(:stale "stale")
(:local "local")))
;;; Connection-local variables:
(defmacro slime-def-connection-var (varname &rest initial-value-and-doc)
"Define a connection-local variable.
The value of the variable can be read by calling the function of the
same name (it must not be accessed directly). The accessor function is
setf-able.
The actual variable bindings are stored buffer-local in the
process-buffers of connections. The accessor function refers to
the binding for `slime-connection'."
(let ((real-var (intern (format "%s:connlocal" varname))))
`(progn
;; Variable
(make-variable-buffer-local
(defvar ,real-var ,@initial-value-and-doc))
;; Accessor
(defun ,varname (&optional process)
(slime-with-connection-buffer (process) ,real-var))
;; Setf
(defsetf ,varname (&optional process) (store)
`(slime-with-connection-buffer (,process)
(setq (\, (quote (\, real-var))) (\, store))
(\, store)))
'(\, varname))))
(put 'slime-def-connection-var 'lisp-indent-function 2)
(put 'slime-indulge-pretty-colors 'slime-def-connection-var t)
(slime-def-connection-var slime-connection-number nil
"Serial number of a connection.
Bound in the connection's process-buffer.")
(slime-def-connection-var slime-lisp-features '()
"The symbol-names of Lisp's *FEATURES*.
This is automatically synchronized from Lisp.")
(slime-def-connection-var slime-lisp-modules '()
"The strings of Lisp's *MODULES*.")
(slime-def-connection-var slime-pid nil
"The process id of the Lisp process.")
(slime-def-connection-var slime-lisp-implementation-type nil
"The implementation type of the Lisp process.")
(slime-def-connection-var slime-lisp-implementation-version nil
"The implementation type of the Lisp process.")
(slime-def-connection-var slime-lisp-implementation-name nil
"The short name for the Lisp implementation.")
(slime-def-connection-var slime-connection-name nil
"The short name for connection.")
(slime-def-connection-var slime-inferior-process nil
"The inferior process for the connection if any.")
(slime-def-connection-var slime-communication-style nil
"The communication style.")
(slime-def-connection-var slime-machine-instance nil
"The name of the (remote) machine running the Lisp process.")
;;;;; Connection setup
(defvar slime-connection-counter 0
"The number of SLIME connections made. For generating serial numbers.")
;;; Interface
(defun slime-setup-connection (process)
"Make a connection out of PROCESS."
(let ((slime-dispatching-connection process))
(slime-init-connection-state process)
(slime-select-connection process)
process))
(defun slime-init-connection-state (proc)
"Initialize connection state in the process-buffer of PROC."
;; To make life simpler for the user: if this is the only open
;; connection then reset the connection counter.
(when (equal slime-net-processes (list proc))
(setq slime-connection-counter 0))
(slime-with-connection-buffer ()
(setq slime-buffer-connection proc))
(setf (slime-connection-number proc) (incf slime-connection-counter))
;; We do the rest of our initialization asynchronously. The current
;; function may be called from a timer, and if we setup the REPL
;; from a timer then it mysteriously uses the wrong keymap for the
;; first command.
(let ((slime-current-thread t))
(slime-eval-async '(swank:connection-info)
(slime-curry #'slime-set-connection-info proc))))
(defun slime-set-connection-info (connection info)
"Initialize CONNECTION with INFO received from Lisp."
(let ((slime-dispatching-connection connection))
(destructuring-bind (&key pid style lisp-implementation machine
features package version modules
&allow-other-keys) info
(slime-check-version version connection)
(setf (slime-pid) pid
(slime-communication-style) style
(slime-lisp-features) features
(slime-lisp-modules) modules)
(destructuring-bind (&key type name version) lisp-implementation
(setf (slime-lisp-implementation-type) type
(slime-lisp-implementation-version) version
(slime-lisp-implementation-name) name
(slime-connection-name) (slime-generate-connection-name name)))
(destructuring-bind (&key instance type version) machine
(setf (slime-machine-instance) instance)))
(let ((args (when-let (p (slime-inferior-process))
(slime-inferior-lisp-args p))))
(when-let (name (plist-get args ':name))
(unless (string= (slime-lisp-implementation-name) name)
(setf (slime-connection-name)
(slime-generate-connection-name (symbol-name name)))))
(slime-load-contribs)
(run-hooks 'slime-connected-hook)
(when-let (fun (plist-get args ':init-function))
(funcall fun)))
(message "Connected. %s" (slime-random-words-of-encouragement))))
(defun slime-check-version (version conn)
(or (equal version slime-protocol-version)
(equal slime-protocol-version 'ignore)
(y-or-n-p
(format "Versions differ: %s (slime) vs. %s (swank). Continue? "
slime-protocol-version version))
(slime-net-close conn)
(top-level)))
(defun slime-generate-connection-name (lisp-name)
(loop for i from 1
for name = lisp-name then (format "%s<%d>" lisp-name i)
while (find name slime-net-processes
:key #'slime-connection-name :test #'equal)
finally (return name)))
(defun slime-connection-close-hook (process)
(when (eq process slime-default-connection)
(when slime-net-processes
(slime-select-connection (car slime-net-processes))
(message "Default connection closed; switched to #%S (%S)"
(slime-connection-number)
(slime-connection-name)))))
(add-hook 'slime-net-process-close-hooks 'slime-connection-close-hook)
;;;;; Commands on connections
(defun slime-disconnect (&optional connection)
"If CONNECTION is non-nil disconnect it, otherwise disconnect
the current slime connection."
(interactive)
(slime-net-close (or connection (slime-connection))))
(defun slime-disconnect-all ()
"Disconnect all connections."
(interactive)
(mapc #'slime-net-close slime-net-processes))
(defun slime-connection-port (connection)
"Return the remote port number of CONNECTION."
(if (featurep 'xemacs)
(car (process-id connection))
(cadr (process-contact connection))))
(defun slime-process (&optional connection)
"Return the Lisp process for CONNECTION (default `slime-connection').
Can return nil if there's no process object for the connection."
(let ((proc (slime-inferior-process connection)))
(if (and proc
(memq (process-status proc) '(run stop)))
proc)))
;; Non-macro version to keep the file byte-compilable.
(defun slime-set-inferior-process (connection process)
(setf (slime-inferior-process connection) process))
(defun slime-use-sigint-for-interrupt (&optional connection)
(let ((c (or connection (slime-connection))))
(ecase (slime-communication-style c)
((:fd-handler nil) t)
((:spawn :sigio) nil))))
(defvar slime-inhibit-pipelining t
"*If true, don't send background requests if Lisp is already busy.")
(defun slime-background-activities-enabled-p ()
(and (let ((con (slime-current-connection)))
(and con
(eq (process-status con) 'open)))
(or (not (slime-busy-p))
(not slime-inhibit-pipelining))))
;;;; Communication protocol
;;;;; Emacs Lisp programming interface
;;;
;;; The programming interface for writing Emacs commands is based on
;;; remote procedure calls (RPCs). The basic operation is to ask Lisp
;;; to apply a named Lisp function to some arguments, then to do
;;; something with the result.
;;;
;;; Requests can be either synchronous (blocking) or asynchronous
;;; (with the result passed to a callback/continuation function). If
;;; an error occurs during the request then the debugger is entered
;;; before the result arrives -- for synchronous evaluations this
;;; requires a recursive edit.
;;;
;;; You should use asynchronous evaluations (`slime-eval-async') for
;;; most things. Reserve synchronous evaluations (`slime-eval') for
;;; the cases where blocking Emacs is really appropriate (like
;;; completion) and that shouldn't trigger errors (e.g. not evaluate
;;; user-entered code).
;;;
;;; We have the concept of the "current Lisp package". RPC requests
;;; always say what package the user is making them from and the Lisp
;;; side binds that package to *BUFFER-PACKAGE* to use as it sees
;;; fit. The current package is defined as the buffer-local value of
;;; `slime-buffer-package' if set, and otherwise the package named by
;;; the nearest IN-PACKAGE as found by text search (first backwards,
;;; then forwards).
;;;
;;; Similarly we have the concept of the current thread, i.e. which
;;; thread in the Lisp process should handle the request. The current
;;; thread is determined solely by the buffer-local value of
;;; `slime-current-thread'. This is usually bound to t meaning "no
;;; particular thread", but can also be used to nominate a specific
;;; thread. The REPL and the debugger both use this feature to deal
;;; with specific threads.
(make-variable-buffer-local
(defvar slime-current-thread t
"The id of the current thread on the Lisp side.
t means the \"current\" thread;
:repl-thread the thread that executes REPL requests;
fixnum a specific thread."))
(make-variable-buffer-local
(defvar slime-buffer-package nil
"The Lisp package associated with the current buffer.
This is set only in buffers bound to specific packages."))
;;; `slime-rex' is the RPC primitive which is used to implement both
;;; `slime-eval' and `slime-eval-async'. You can use it directly if
;;; you need to, but the others are usually more convenient.
(defmacro* slime-rex ((&rest saved-vars)
(sexp &optional
(package '(slime-current-package))
(thread 'slime-current-thread))
&rest continuations)
"(slime-rex (VAR ...) (SEXP &optional PACKAGE THREAD) CLAUSES ...)
Remote EXecute SEXP.
VARs are a list of saved variables visible in the other forms. Each
VAR is either a symbol or a list (VAR INIT-VALUE).
SEXP is evaluated and the princed version is sent to Lisp.
PACKAGE is evaluated and Lisp binds *BUFFER-PACKAGE* to this package.
The default value is (slime-current-package).
CLAUSES is a list of patterns with same syntax as
`destructure-case'. The result of the evaluation of SEXP is
dispatched on CLAUSES. The result is either a sexp of the
form (:ok VALUE) or (:abort). CLAUSES is executed
asynchronously.
Note: don't use backquote syntax for SEXP, because various Emacs
versions cannot deal with that."
(let ((result (gensym)))
`(lexical-let ,(loop for var in saved-vars
collect (etypecase var
(symbol (list var var))
(cons var)))
(slime-dispatch-event
(list :emacs-rex ,sexp ,package ,thread
(lambda (,result)
(destructure-case ,result
,@continuations)))))))
(put 'slime-rex 'lisp-indent-function 2)
;;; Interface
(defun slime-current-package ()
"Return the Common Lisp package in the current context.
If `slime-buffer-package' has a value then return that, otherwise
search for and read an `in-package' form."
(or slime-buffer-package
(save-restriction
(widen)
(slime-find-buffer-package))))
(defvar slime-find-buffer-package-function 'slime-search-buffer-package
"*Function to use for `slime-find-buffer-package'.
The result should be the package-name (a string)
or nil if nothing suitable can be found.")
(defun slime-find-buffer-package ()
"Figure out which Lisp package the current buffer is associated with."
(funcall slime-find-buffer-package-function))
;; When modifing this code consider cases like:
;; (in-package #.*foo*)
;; (in-package #:cl)
;; (in-package :cl)
;; (in-package "CL")
;; (in-package |CL|)
;; (in-package #+ansi-cl :cl #-ansi-cl 'lisp)
(defun slime-search-buffer-package ()
(let ((case-fold-search t)
(regexp (concat "^(\\(cl:\\|common-lisp:\\)?in-package\\>[ \t']*"
"\\([^)]+\\)[ \t]*)")))
(save-excursion
(when (or (re-search-backward regexp nil t)
(re-search-forward regexp nil t))
(match-string-no-properties 2)))))
;;; Synchronous requests are implemented in terms of asynchronous
;;; ones. We make an asynchronous request with a continuation function
;;; that `throw's its result up to a `catch' and then enter a loop of
;;; handling I/O until that happens.
(defvar slime-stack-eval-tags nil
"List of stack-tags of continuations waiting on the stack.")
(defun slime-eval (sexp &optional package)
"Evaluate EXPR on the superior Lisp and return the result."
(when (null package) (setq package (slime-current-package)))
(let* ((tag (gensym (format "slime-result-%d-"
(1+ (slime-continuation-counter)))))
(slime-stack-eval-tags (cons tag slime-stack-eval-tags)))
(apply
#'funcall
(catch tag
(slime-rex (tag sexp)
(sexp package)
((:ok value)
(unless (member tag slime-stack-eval-tags)
(error "Reply to canceled synchronous eval request tag=%S sexp=%S"
tag sexp))
(throw tag (list #'identity value)))
((:abort)
(throw tag (list #'error "Synchronous Lisp Evaluation aborted"))))
(let ((debug-on-quit t)
(inhibit-quit nil)
(conn (slime-connection)))
(while t
(unless (eq (process-status conn) 'open)
(error "Lisp connection closed unexpectedly"))
(slime-accept-process-output nil 0.01)))))))
(defun slime-eval-async (sexp &optional cont package)
"Evaluate EXPR on the superior Lisp and call CONT with the result."
(slime-rex (cont (buffer (current-buffer)))
(sexp (or package (slime-current-package)))
((:ok result)
(when cont
(set-buffer buffer)
(funcall cont result)))
((:abort)
(message "Evaluation aborted.")))
;; Guard against arbitrary return values which once upon a time
;; showed up in the minibuffer spuriously (due to a bug in
;; slime-autodoc.) If this ever happens again, returning the
;; following will make debugging much easier:
:slime-eval-async)
;;; These functions can be handy too:
(defun slime-connected-p ()
"Return true if the Swank connection is open."
(not (null slime-net-processes)))
(defun slime-check-connected ()
"Signal an error if we are not connected to Lisp."
(unless (slime-connected-p)
(error "Not connected. Use `%s' to start a Lisp."
(substitute-command-keys "\\[slime]"))))
(defun slime-stale-connection-p (conn)
(not (memq conn slime-net-processes)))
;; UNUSED
(defun slime-debugged-connection-p (conn)
;; This previously was (AND (SLDB-DEBUGGED-CONTINUATIONS CONN) T),
;; but an SLDB buffer may exist without having continuations
;; attached to it, e.g. the one resulting from `slime-interrupt'.
(loop for b in (sldb-buffers)
thereis (with-current-buffer b
(eq slime-buffer-connection conn))))
(defun slime-busy-p (&optional conn)
"True if Lisp has outstanding requests.
Debugged requests are ignored."
(let ((debugged (sldb-debugged-continuations (or conn (slime-connection)))))
(remove-if (lambda (id)
(memq id debugged))
(slime-rex-continuations)
:key #'car)))
(defun slime-sync ()
"Block until the most recent request has finished."
(when (slime-rex-continuations)
(let ((tag (caar (slime-rex-continuations))))
(while (find tag (slime-rex-continuations) :key #'car)
(slime-accept-process-output nil 0.1)))))
(defun slime-ping ()
"Check that communication works."
(interactive)
(message "%s" (slime-eval "PONG")))
;;;;; Protocol event handler (the guts)
;;;
;;; This is the protocol in all its glory. The input to this function
;;; is a protocol event that either originates within Emacs or arrived
;;; over the network from Lisp.
;;;
;;; Each event is a list beginning with a keyword and followed by
;;; arguments. The keyword identifies the type of event. Events
;;; originating from Emacs have names starting with :emacs- and events
;;; from Lisp don't.
(slime-def-connection-var slime-rex-continuations '()
"List of (ID . FUNCTION) continuations waiting for RPC results.")
(slime-def-connection-var slime-continuation-counter 0
"Continuation serial number counter.")
(defvar slime-event-hooks)
(defun slime-dispatch-event (event &optional process)
(let ((slime-dispatching-connection (or process (slime-connection))))
(or (run-hook-with-args-until-success 'slime-event-hooks event)
(destructure-case event
((:emacs-rex form package thread continuation)
(when (and (slime-use-sigint-for-interrupt) (slime-busy-p))
(slime-display-oneliner "; pipelined request... %S" form))
(let ((id (incf (slime-continuation-counter))))
(slime-send `(:emacs-rex ,form ,package ,thread ,id))
(push (cons id continuation) (slime-rex-continuations))
(slime-recompute-modelines t)))
((:return value id)
(let ((rec (assq id (slime-rex-continuations))))
(cond (rec (setf (slime-rex-continuations)
(remove rec (slime-rex-continuations)))
(slime-recompute-modelines nil)
(funcall (cdr rec) value))
(t
(error "Unexpected reply: %S %S" id value)))))
((:debug-activate thread level &optional select)
(assert thread)
(sldb-activate thread level select))
((:debug thread level condition restarts frames conts)
(assert thread)
(sldb-setup thread level condition restarts frames conts))
((:debug-return thread level stepping)
(assert thread)
(sldb-exit thread level stepping))
((:emacs-interrupt thread)
(slime-send `(:emacs-interrupt ,thread)))
((:channel-send id msg)
(slime-channel-send (or (slime-find-channel id)
(error "Invalid channel id: %S %S" id msg))
msg))
((:emacs-channel-send id msg)
;; FIXME: Guard against errors like in :emacs-rex?
(slime-send `(:emacs-channel-send ,id ,msg)))
((:read-from-minibuffer thread tag prompt initial-value)
(slime-read-from-minibuffer-for-swank thread tag prompt initial-value))
((:y-or-n-p thread tag question)
(slime-y-or-n-p thread tag question))
((:emacs-return-string thread tag string)
(slime-send `(:emacs-return-string ,thread ,tag ,string)))
((:new-features features)
(setf (slime-lisp-features) features))
((:indentation-update info)
(slime-handle-indentation-update info))
((:eval-no-wait fun args)
(apply (intern fun) args))
((:eval thread tag form-string)
(slime-check-eval-in-emacs-enabled)
(slime-eval-for-lisp thread tag form-string))
((:emacs-return thread tag value)
(slime-send `(:emacs-return ,thread ,tag ,value)))
((:ed what)
(slime-ed what))
((:inspect what wait-thread wait-tag)
(let ((hook (when (and wait-thread wait-tag)
(lexical-let ((thread wait-thread)
(tag wait-tag))
(lambda ()
(slime-send `(:emacs-return ,thread ,tag nil)))))))
(slime-open-inspector what nil hook)))
((:background-message message)
(slime-background-message "%s" message))
((:debug-condition thread message)
(assert thread)
(message "%s" message))
((:ping thread tag)
(slime-send `(:emacs-pong ,thread ,tag)))
((:reader-error packet condition)
(slime-with-popup-buffer ("*Slime Error*")
(princ (format "Invalid protocol message:\n%s\n\n%S"
condition packet))
(goto-char (point-min)))
(error "Invalid protocol message"))
((:invalid-rpc id message)
(setf (slime-rex-continuations)
(remove* id (slime-rex-continuations) :key #'car))
(error "Invalid rpc: %s" message))))))
(defun slime-send (sexp)
"Send SEXP directly over the wire on the current connection."
(slime-net-send sexp (slime-connection)))
(defun slime-reset ()
"Clear all pending continuations."
(interactive)
(setf (slime-rex-continuations) '())
(mapc #'kill-buffer (sldb-buffers)))
(defun slime-send-sigint ()
(interactive)
(signal-process (slime-pid) 'SIGINT))
;;;;; Channels
;;; A channel implements a set of operations. Those operations can be
;;; invoked by sending messages to the channel. Channels are used for
;;; protocols which can't be expressed naturally with RPCs, e.g. for
;;; streaming data over the wire.
;;;
;;; A channel can be "remote" or "local". Remote channels are
;;; represented by integers. Local channels are structures. Messages
;;; sent to a closed (remote) channel are ignored.
(slime-def-connection-var slime-channels '()
"Alist of the form (ID . CHANNEL).")
(slime-def-connection-var slime-channels-counter 0
"Channel serial number counter.")
(defstruct (slime-channel (:conc-name slime-channel.)
(:constructor
slime-make-channel% (operations name id plist)))
operations name id plist)
(defun slime-make-channel (operations &optional name)
(let* ((id (incf (slime-channels-counter)))
(ch (slime-make-channel% operations name id nil)))
(push (cons id ch) (slime-channels))
ch))
(defun slime-close-channel (channel)
(setf (slime-channel.operations channel) 'closed-channel)
(let ((probe (assq (slime-channel.id channel) (slime-channels))))
(cond (probe (setf (slime-channels) (delete probe (slime-channels))))
(t (error "Invalid channel: %s" channel)))))
(defun slime-find-channel (id)
(cdr (assq id (slime-channels))))
(defun slime-channel-send (channel message)
(apply (or (gethash (car message) (slime-channel.operations channel))
(error "Unsupported operation: %S %S" message channel))
channel (cdr message)))
(defun slime-channel-put (channel prop value)
(setf (slime-channel.plist channel)
(plist-put (slime-channel.plist channel) prop value)))
(defun slime-channel-get (channel prop)
(plist-get (slime-channel.plist channel) prop))
(eval-and-compile
(defun slime-channel-method-table-name (type)
(intern (format "slime-%s-channel-methods" type))))
(defmacro slime-define-channel-type (name)
(let ((tab (slime-channel-method-table-name name)))
`(progn
(defvar ,tab)
(setq ,tab (make-hash-table :size 10)))))
(put 'slime-indulge-pretty-colors 'slime-define-channel-type t)
(defmacro slime-define-channel-method (type method args &rest body)
`(puthash ',method
(lambda (self . ,args) . ,body)
,(slime-channel-method-table-name type)))
(put 'slime-define-channel-method 'lisp-indent-function 3)
(put 'slime-indulge-pretty-colors 'slime-define-channel-method t)
(defun slime-send-to-remote-channel (channel-id msg)
(slime-dispatch-event `(:emacs-channel-send ,channel-id ,msg)))
;;;;; Event logging to *slime-events*
;;;
;;; The *slime-events* buffer logs all protocol messages for debugging
;;; purposes. Optionally you can enable outline-mode in that buffer,
;;; which is convenient but slows things down significantly.
(defvar slime-log-events t
"*Log protocol events to the *slime-events* buffer.")
(defvar slime-outline-mode-in-events-buffer nil
"*Non-nil means use outline-mode in *slime-events*.")
(defvar slime-event-buffer-name "*slime-events*"
"The name of the slime event buffer.")
(defun slime-log-event (event)
"Record the fact that EVENT occurred."
(when slime-log-events
(with-current-buffer (slime-events-buffer)
;; trim?
(when (> (buffer-size) 100000)
(goto-char (/ (buffer-size) 2))
(re-search-forward "^(" nil t)
(delete-region (point-min) (point)))
(goto-char (point-max))
(save-excursion
(slime-pprint-event event (current-buffer)))
(when (and (boundp 'outline-minor-mode)
outline-minor-mode)
(hide-entry))
(goto-char (point-max)))))
(defun slime-pprint-event (event buffer)
"Pretty print EVENT in BUFFER with limited depth and width."
(let ((print-length 20)
(print-level 6)
(pp-escape-newlines t))
(pp event buffer)))
(defun slime-events-buffer ()
(or (get-buffer slime-event-buffer-name)
(let ((buffer (get-buffer-create slime-event-buffer-name)))
(with-current-buffer buffer
(buffer-disable-undo)
(set (make-local-variable 'outline-regexp) "^(")
(set (make-local-variable 'comment-start) ";")
(set (make-local-variable 'comment-end) "")
(when slime-outline-mode-in-events-buffer
(outline-minor-mode)))
buffer)))
;;;;; Cleanup after a quit
(defun slime-restart-inferior-lisp ()
(interactive)
(assert (slime-inferior-process) () "No inferior lisp process")
(slime-quit-lisp-internal (slime-connection) 'slime-restart-sentinel t))
(defun slime-restart-sentinel (process message)
"Restart the inferior lisp process.
Also rearrange windows."
(assert (process-status process) 'closed)
(let* ((proc (slime-inferior-process process))
(args (slime-inferior-lisp-args proc))
(buffer (buffer-name (process-buffer proc)))
(buffer-window (get-buffer-window buffer))
(new-proc (slime-start-lisp (plist-get args :program)
(plist-get args :program-args)
(plist-get args :env)
nil
buffer))
;;(repl-buffer (slime-repl-buffer nil process))
;;(repl-window (and repl-buffer (get-buffer-window repl-buffer)))
)
(slime-net-close process)
(slime-inferior-connect new-proc args)
(cond ;;((and repl-window (not buffer-window))
;; (set-window-buffer repl-window buffer)
;; (select-window repl-window))
;;(repl-window
;; (select-window repl-window))
(t
(pop-to-buffer buffer)))
(switch-to-buffer buffer)
(goto-char (point-max))))
(defun slime-kill-all-buffers ()
"Kill all the slime related buffers.
This is only used by the repl command sayoonara."
(dolist (buf (buffer-list))
(when (or (string= (buffer-name buf) slime-event-buffer-name)
(string-match "^\\*inferior-lisp*" (buffer-name buf))
(string-match "^\\*slime-repl .*\\*$" (buffer-name buf))
(string-match "^\\*sldb .*\\*$" (buffer-name buf))
(string-match "^\\*SLIME.*\\*$" (buffer-name buf)))
(kill-buffer buf))))
;;;; Compilation and the creation of compiler-note annotations
(defvar slime-highlight-compiler-notes t
"*When non-nil annotate buffers with compilation notes etc.")
(defvar slime-before-compile-functions nil
"A list of function called before compiling a buffer or region.
The function receive two arguments: the beginning and the end of the
region that will be compiled.")
(defcustom slime-compilation-finished-hook 'slime-maybe-show-compilation-log
"Hook called with a list of compiler notes after a compilation."
:group 'slime-mode
:type 'hook
:options '(slime-maybe-show-compilation-log
slime-create-compilation-log
slime-show-compilation-log
slime-maybe-list-compiler-notes
slime-list-compiler-notes
slime-maybe-show-xrefs-for-notes
slime-goto-first-note))
(defvar slime-compilation-policy nil
"When non-nil compile defuns with this debug optimization level.")
(defun slime-compute-policy (arg)
"Return the policy for the prefix argument ARG."
(flet ((between (min n max)
(if (< n min)
min
(if (> n max) max n))))
(let ((n (prefix-numeric-value arg)))
(cond ((not arg) slime-compilation-policy)
((plusp n) `((cl:debug . ,(between 0 n 3))))
((eq arg '-) `((cl:speed . 3)))
(t `((cl:speed . ,(between 0 (abs n) 3))))))))
(defstruct (slime-compilation-result
(:type list)
(:conc-name slime-compilation-result.)
(:constructor nil)
(:copier nil))
tag notes successp duration)
(defvar slime-last-compilation-result nil
"The result of the most recently issued compilation.")
(defun slime-compiler-notes ()
"Return all compiler notes, warnings, and errors."
(slime-compilation-result.notes slime-last-compilation-result))
(defun slime-compile-and-load-file ()
"Compile and load the buffer's file and highlight compiler notes.
Each source location that is the subject of a compiler note is
underlined and annotated with the relevant information. The commands
`slime-next-note' and `slime-previous-note' can be used to navigate
between compiler notes and to display their full details."
(interactive)
(slime-compile-file t))
(defvar slime-compile-file-options '()
"Plist of additional options that C-c C-k should pass to Lisp.
Currently only :fasl-directory is supported.")
(defun slime-compile-file (&optional load)
"Compile current buffer's file and highlight resulting compiler notes.
See `slime-compile-and-load-file' for further details."
(interactive)
;;(unless (memq major-mode slime-lisp-modes)
;; (error "Only valid in lisp-mode"))
(check-parens)
(unless buffer-file-name
(error "Buffer %s is not associated with a file." (buffer-name)))
(when (and (buffer-modified-p)
(y-or-n-p (format "Save file %s? " (buffer-file-name))))
(save-buffer))
(run-hook-with-args 'slime-before-compile-functions (point-min) (point-max))
(let ((file (slime-to-lisp-filename (buffer-file-name))))
(slime-eval-async
`(swank:compile-file-for-emacs ,file ,(if load t nil)
',slime-compile-file-options)
#'slime-compilation-finished)
(message "Compiling %s..." file)))
(defun slime-compile-defun (&optional raw-prefix-arg)
"Compile the current toplevel form.
If invoked with a simple prefix-arg (`C-u'), compile the defun
with maximum debug setting. If invoked with a numeric prefix arg,
compile with a debug setting of that number."
(interactive "P")
(let ((slime-compilation-policy (slime-compute-policy raw-prefix-arg)))
(apply #'slime-compile-region (slime-region-for-defun-at-point))))
(defun slime-compile-region (start end)
"Compile the region."
(interactive "r")
(slime-flash-region start end)
(run-hook-with-args 'slime-before-compile-functions start end)
(slime-compile-string (buffer-substring-no-properties start end) start))
(defun slime-flash-region (start end &optional timeout)
(let ((overlay (make-overlay start end)))
(overlay-put overlay 'face 'secondary-selection)
(run-with-timer (or timeout 0.2) nil 'delete-overlay overlay)))
(defun slime-compile-string (string start-offset)
(slime-eval-async
`(swank:compile-string-for-emacs
,string
,(buffer-name)
,start-offset
,(if (buffer-file-name) (slime-to-lisp-filename (buffer-file-name)))
',slime-compilation-policy)
#'slime-compilation-finished))
(defun slime-compilation-finished (result)
(with-struct (slime-compilation-result. notes duration successp) result
(setf slime-last-compilation-result result)
(slime-show-note-counts notes duration successp)
(when slime-highlight-compiler-notes
(slime-highlight-notes notes))
(run-hook-with-args 'slime-compilation-finished-hook notes)))
(defun slime-show-note-counts (notes secs successp)
(message (concat
(cond (successp "Compilation finished")
(t (slime-add-face 'font-lock-warning-face
"Compilation failed")))
(if (null notes) ". (No warnings)" ": ")
(mapconcat
(lambda (messages)
(destructuring-bind (sev . notes) messages
(let ((len (length notes)))
(format "%d %s%s" len (slime-severity-label sev)
(if (= len 1) "" "s")))))
(sort (slime-alistify notes #'slime-note.severity #'eq)
(lambda (x y) (slime-severity< (car y) (car x))))
" ")
(if secs (format " [%.2f secs]" secs)))))
(defun slime-highlight-notes (notes)
"Highlight compiler notes, warnings, and errors in the buffer."
(interactive (list (slime-compiler-notes)))
(with-temp-message "Highlighting notes..."
(save-excursion
(save-restriction
(widen) ; highlight notes on the whole buffer
(slime-remove-old-overlays)
(mapc #'slime-overlay-note (slime-merge-notes-for-display notes))))))
(defvar slime-note-overlays '()
"List of overlays created by `slime-make-note-overlay'")
(defun slime-remove-old-overlays ()
"Delete the existing note overlays."
(mapc #'delete-overlay slime-note-overlays)
(setq slime-note-overlays '()))
(defun slime-filter-buffers (predicate)
"Return a list of where PREDICATE returns true.
PREDICATE is executed in the buffer to test."
(remove-if-not (lambda (%buffer)
(with-current-buffer %buffer
(funcall predicate)))
(buffer-list)))
;;;;; Recompilation.
(defun slime-recompile-location (location)
(save-excursion
(slime-goto-source-location location)
(slime-compile-defun)))
(defun slime-recompile-locations (locations cont)
(slime-eval-async
`(swank:compile-multiple-strings-for-emacs
',(loop for loc in locations collect
(save-excursion
(slime-goto-source-location loc)
(destructuring-bind (start end)
(slime-region-for-defun-at-point)
(list (buffer-substring-no-properties start end)
(buffer-name)
(slime-current-package)
start
(if (buffer-file-name)
(file-name-directory (buffer-file-name))
nil)))))
',slime-compilation-policy)
cont))
;;;;; Merging together compiler notes in the same location.
(defun slime-merge-notes-for-display (notes)
"Merge together notes that refer to the same location.
This operation is \"lossy\" in the broad sense but not for display purposes."
(mapcar #'slime-merge-notes
(slime-group-similar 'slime-notes-in-same-location-p notes)))
(defun slime-merge-notes (notes)
"Merge NOTES together. Keep the highest severity, concatenate the messages."
(let* ((new-severity (reduce #'slime-most-severe notes
:key #'slime-note.severity))
(new-message (mapconcat #'slime-note.message notes "\n")))
(let ((new-note (copy-list (car notes))))
(setf (getf new-note :message) new-message)
(setf (getf new-note :severity) new-severity)
new-note)))
(defun slime-notes-in-same-location-p (a b)
(equal (slime-note.location a) (slime-note.location b)))
;;;;; Compiler notes list
(defun slime-one-line-ify (string)
"Return a single-line version of STRING.
Each newlines and following indentation is replaced by a single space."
(with-temp-buffer
(insert string)
(goto-char (point-min))
(while (re-search-forward "\n[\n \t]*" nil t)
(replace-match " "))
(buffer-string)))
(defun slime-xrefs-for-notes (notes)
(let ((xrefs))
(dolist (note notes)
(let* ((location (getf note :location))
(fn (cadr (assq :file (cdr location))))
(file (assoc fn xrefs))
(node
(cons (format "%s: %s"
(getf note :severity)
(slime-one-line-ify (getf note :message)))
location)))
(when fn
(if file
(push node (cdr file))
(setf xrefs (acons fn (list node) xrefs))))))
xrefs))
(defun slime-maybe-show-xrefs-for-notes (notes)
"Show the compiler notes NOTES if they come from more than one file."
(let ((xrefs (slime-xrefs-for-notes notes)))
(when (slime-length> xrefs 1) ; >1 file
(slime-show-xrefs
xrefs 'definition "Compiler notes" (slime-current-package)))))
(defun slime-note-has-location-p (note)
(not (eq ':error (car (slime-note.location note)))))
(defun slime-redefinition-note-p (note)
(eq (slime-note.severity note) :redefinition))
(defun slime-create-compilation-log (notes)
"Create a buffer for `next-error' to use."
(with-current-buffer (get-buffer-create "*SLIME Compilation*")
(let ((inhibit-read-only t))
(erase-buffer))
(slime-insert-compilation-log notes)))
(defun slime-maybe-show-compilation-log (notes)
"Display the log on failed compilations or if NOTES is non-nil."
(slime-create-compilation-log notes)
(with-struct (slime-compilation-result. notes duration successp)
slime-last-compilation-result
(unless successp
(with-current-buffer "*SLIME Compilation*"
(let ((inhibit-read-only t))
(goto-char (point-max))
(insert "Compilation " (if successp "succeeded." "failed."))
(goto-char (point-min))
(display-buffer (current-buffer)))))))
(defun slime-show-compilation-log (notes)
(interactive (list (slime-compiler-notes)))
(slime-with-popup-buffer ("*SLIME Compilation*")
(slime-insert-compilation-log notes)))
(defun slime-insert-compilation-log (notes)
"Insert NOTES in format suitable for `compilation-mode'."
(multiple-value-bind (grouped-notes canonicalized-locs-table)
(slime-group-and-sort-notes notes)
(with-temp-message "Preparing compilation log..."
(let ((inhibit-read-only t)
(inhibit-modification-hooks t)) ; inefficient font-lock-hook
(insert (format "cd %s\n%d compiler notes:\n\n"
default-directory (length notes)))
(dolist (notes grouped-notes)
(let ((loc (gethash (first notes) canonicalized-locs-table))
(start (point)))
(insert (slime-canonicalized-location-to-string loc) ":")
(slime-insert-note-group notes)
(insert "\n")
(slime-make-note-overlay (first notes) start (1- (point))))))
(compilation-mode)
(set (make-local-variable 'compilation-skip-threshold) 0)
(setq next-error-last-buffer (current-buffer)))))
(defun slime-insert-note-group (notes)
"Insert a group of compiler messages."
(insert "\n")
(dolist (note notes)
(insert " " (slime-severity-label (slime-note.severity note)) ": ")
(let ((start (point)))
(insert (slime-note.message note))
(let ((ctx (slime-note.source-context note)))
(if ctx (insert "\n" ctx)))
(slime-indent-block start 4))
(insert "\n")))
(defun slime-indent-block (start column)
"If the region back to START isn't a one-liner indent it."
(when (< start (line-beginning-position))
(save-excursion
(goto-char start)
(insert "\n"))
(slime-indent-rigidly start (point) column)))
(defun slime-canonicalized-location (location)
"Takes a `slime-location' and returns a list consisting of
file/buffer name, line, and column number."
(save-excursion
(slime-goto-location-buffer (slime-location.buffer location))
(save-excursion
(slime-goto-source-location location)
(list (or (buffer-file-name) (buffer-name))
(line-number-at-pos)
(1+ (current-column))))))
(defun slime-canonicalized-location-to-string (loc)
(if loc
(destructuring-bind (filename line col) loc
(format "%s:%d:%d"
(cond ((not filename) "")
((let ((rel (file-relative-name filename)))
(if (< (length rel) (length filename))
rel)))
(t filename))
line col))
(format "Unknown location")))
(defun slime-goto-note-in-compilation-log (note)
"Find `note' in the compilation log and display it."
(with-current-buffer (get-buffer "*SLIME Compilation*")
(let ((origin (point))
(foundp nil))
(goto-char (point-min))
(let ((overlay))
(while (and (setq overlay (slime-find-next-note))
(not foundp))
(let ((other-note (overlay-get overlay 'slime-note)))
(when (slime-notes-in-same-location-p note other-note)
(slime-show-buffer-position (overlay-start overlay) 'top)
(setq foundp t)))))
(unless foundp
(goto-char origin)))))
(defun slime-group-and-sort-notes (notes)
"First sort, then group NOTES according to their canonicalized locs."
(let ((locs (make-hash-table :test #'eq)))
(mapc (lambda (note)
(let ((loc (slime-note.location note)))
(when (slime-location-p loc)
(puthash note (slime-canonicalized-location loc) locs))))
notes)
(values (slime-group-similar
(lambda (n1 n2)
(equal (gethash n1 locs nil) (gethash n2 locs t)))
(let* ((bottom most-negative-fixnum)
(+default+ (list "" bottom bottom)))
(sort notes
(lambda (n1 n2)
(destructuring-bind (filename1 line1 col1)
(gethash n1 locs +default+)
(destructuring-bind (filename2 line2 col2)
(gethash n2 locs +default+)
(cond ((string-lessp filename1 filename2) t)
((string-lessp filename2 filename1) nil)
((< line1 line2) t)
((> line1 line2) nil)
(t (< col1 col2)))))))))
locs)))
(defun slime-note.severity (note)
(plist-get note :severity))
(defun slime-note.message (note)
(plist-get note :message))
(defun slime-note.source-context (note)
(plist-get note :source-context))
(defun slime-note.location (note)
(plist-get note :location))
(defun slime-severity-label (severity)
(subseq (symbol-name severity) 1))
;;;;; Adding a single compiler note
(defun slime-overlay-note (note)
"Add a compiler note to the buffer as an overlay.
If an appropriate overlay for a compiler note in the same location
already exists then the new information is merged into it. Otherwise a
new overlay is created."
(multiple-value-bind (start end) (slime-choose-overlay-region note)
(when start
(goto-char start)
(let ((severity (plist-get note :severity))
(message (plist-get note :message))
(overlay (slime-note-at-point)))
(if overlay
(slime-merge-note-into-overlay overlay severity message)
(slime-create-note-overlay note start end severity message))))))
(defun slime-make-note-overlay (note start end)
(let ((overlay (make-overlay start end)))
(overlay-put overlay 'slime-note note)
(push overlay slime-note-overlays)
overlay))
(defun slime-create-note-overlay (note start end severity message)
"Create an overlay representing a compiler note.
The overlay has several properties:
FACE - to underline the relevant text.
SEVERITY - for future reference, :NOTE, :STYLE-WARNING, :WARNING, or :ERROR.
MOUSE-FACE - highlight the note when the mouse passes over.
HELP-ECHO - a string describing the note, both for future reference
and for display as a tooltip (due to the special
property name)."
(let ((overlay (slime-make-note-overlay note start end)))
(flet ((putp (name value) (overlay-put overlay name value)))
(putp 'face (slime-severity-face severity))
(putp 'severity severity)
(putp 'mouse-face 'highlight)
(putp 'help-echo message)
overlay)))
;; XXX Obsolete due to `slime-merge-notes-for-display' doing the
;; work already -- unless we decide to put several sets of notes on a
;; buffer without clearing in between, which only this handles.
(defun slime-merge-note-into-overlay (overlay severity message)
"Merge another compiler note into an existing overlay.
The help text describes both notes, and the highest of the severities
is kept."
(flet ((putp (name value) (overlay-put overlay name value))
(getp (name) (overlay-get overlay name)))
(putp 'severity (slime-most-severe severity (getp 'severity)))
(putp 'face (slime-severity-face (getp 'severity)))
(putp 'help-echo (concat (getp 'help-echo) "\n" message))))
(defun slime-choose-overlay-region (note)
"Choose the start and end points for an overlay over NOTE.
If the location's sexp is a list spanning multiple lines, then the
region around the first element is used.
Return nil if there's no useful source location."
(let ((location (slime-note.location note)))
(when location
(destructure-case location
((:error _) _ nil) ; do nothing
((:location file pos _hints)
(cond ((eq (car file) ':source-form) nil)
((eq (slime-note.severity note) :read-error)
(slime-choose-overlay-for-read-error location))
((equal pos '(:eof))
(list (1- (point-max)) (point-max)))
(t
(slime-choose-overlay-for-sexp location))))))))
(defun slime-choose-overlay-for-read-error (location)
(let ((pos (slime-location-offset location)))
(save-excursion
(goto-char pos)
(cond ((slime-symbol-at-point)
;; package not found, &c.
(values (slime-symbol-start-pos) (slime-symbol-end-pos)))
(t
(values pos (1+ pos)))))))
(defun slime-choose-overlay-for-sexp (location)
(slime-goto-source-location location)
(skip-chars-forward "'#`")
(let ((start (point)))
(ignore-errors (slime-forward-sexp))
(if (slime-same-line-p start (point))
(values start (point))
(values (1+ start)
(progn (goto-char (1+ start))
(ignore-errors (forward-sexp 1))
(point))))))
(defun slime-same-line-p (pos1 pos2)
"Return t if buffer positions POS1 and POS2 are on the same line."
(save-excursion (goto-char (min pos1 pos2))
(<= (max pos1 pos2) (line-end-position))))
(defvar slime-severity-face-plist
'(:error slime-error-face
:read-error slime-error-face
:warning slime-warning-face
:redefinition slime-style-warning-face
:style-warning slime-style-warning-face
:note slime-note-face))
(defun slime-severity-face (severity)
"Return the name of the font-lock face representing SEVERITY."
(or (plist-get slime-severity-face-plist severity)
(error "No face for: %S" severity)))
(defvar slime-severity-order
'(:note :style-warning :redefinition :warning :error :read-error))
(defun slime-severity< (sev1 sev2)
"Return true if SEV1 is less severe than SEV2."
(< (position sev1 slime-severity-order)
(position sev2 slime-severity-order)))
(defun slime-most-severe (sev1 sev2)
"Return the most servere of two conditions."
(if (slime-severity< sev1 sev2) sev2 sev1))
;; XXX: unused function
(defun slime-visit-source-path (source-path)
"Visit a full source path including the top-level form."
(goto-char (point-min))
(slime-forward-source-path source-path))
;;; The following two functions can be handy when inspecting
;;; source-location while debugging `M-.'.
;;;
(defun slime-current-tlf-number ()
"Return the current toplevel number."
(interactive)
(let ((original-pos (car (slime-region-for-defun-at-point)))
(n 0))
(save-excursion
;; We use this and no repeated `beginning-of-defun's to get
;; reader conditionals right.
(goto-char (point-min))
(while (progn (slime-forward-sexp)
(< (point) original-pos))
(incf n)))
n))
;;; This is similiar to `slime-enclosing-form-paths' in the
;;; `slime-parse' contrib except that this does not do any duck-tape
;;; parsing, and gets reader conditionals right.
(defun slime-current-form-path ()
"Returns the path from the beginning of the current toplevel
form to the atom at point, or nil if we're in front of a tlf."
(interactive)
(let ((source-path nil))
(save-excursion
;; Moving forward to get reader conditionals right.
(loop for inner-pos = (point)
for outer-pos = (nth-value 1 (slime-current-parser-state))
while outer-pos do
(goto-char outer-pos)
(unless (eq (char-before) ?#) ; when at #(...) continue.
(forward-char)
(let ((n 0))
(while (progn (slime-forward-sexp)
(< (point) inner-pos))
(incf n))
(push n source-path)
(goto-char outer-pos)))))
source-path))
(defun slime-forward-positioned-source-path (source-path)
"Move forward through a sourcepath from a fixed position.
The point is assumed to already be at the outermost sexp, making the
first element of the source-path redundant."
(ignore-errors
(slime-forward-sexp)
(beginning-of-defun))
(when-let (source-path (cdr source-path))
(down-list 1)
(slime-forward-source-path source-path)))
(defun slime-forward-source-path (source-path)
(let ((origin (point)))
(condition-case nil
(progn
(loop for (count . more) on source-path
do (progn
(slime-forward-sexp count)
(when more (down-list 1))))
;; Align at beginning
(slime-forward-sexp)
(beginning-of-sexp))
(error (goto-char origin)))))
(defun slime-filesystem-toplevel-directory ()
;; Windows doesn't have a true toplevel root directory, and all
;; filenames look like "c:/foo/bar/quux.baz" from an Emacs
;; perspective anyway.
(if (memq system-type '(ms-dos windows-nt))
""
(file-name-as-directory "/")))
(defun slime-file-name-merge-source-root (target-filename buffer-filename)
"Returns a filename where the source root directory of TARGET-FILENAME
is replaced with the source root directory of BUFFER-FILENAME.
If no common source root could be determined, return NIL.
E.g. (slime-file-name-merge-source-root
\"/usr/local/src/joe/upstream/sbcl/code/late-extensions.lisp\"
\"/usr/local/src/joe/hacked/sbcl/compiler/deftype.lisp\")
==> \"/usr/local/src/joe/hacked/sbcl/code/late-extensions.lisp\"
"
(let ((target-dirs (slime-split-string (file-name-directory target-filename) "/" t))
(buffer-dirs (slime-split-string (file-name-directory buffer-filename) "/" t)))
;; Starting from the end, we look if one of the TARGET-DIRS exists
;; in BUFFER-FILENAME---if so, it and everything left from that dirname
;; is considered to be the source root directory of BUFFER-FILENAME.
(loop with target-suffix-dirs = nil
with buffer-dirs* = (reverse buffer-dirs)
with target-dirs* = (reverse target-dirs)
for target-dir in target-dirs*
do (flet ((concat-dirs (dirs)
(apply #'concat (mapcar #'file-name-as-directory dirs))))
(let ((pos (position target-dir buffer-dirs* :test #'equal)))
(if (not pos) ; TARGET-DIR not in BUFFER-FILENAME?
(push target-dir target-suffix-dirs)
(let* ((target-suffix (concat-dirs target-suffix-dirs)) ; PUSH reversed for us!
(buffer-root (concat-dirs (reverse (nthcdr pos buffer-dirs*)))))
(return (concat (slime-filesystem-toplevel-directory)
buffer-root
target-suffix
(file-name-nondirectory target-filename))))))))))
(defun slime-highlight-differences-in-dirname (base-dirname contrast-dirname)
"Returns a copy of BASE-DIRNAME where all differences between
BASE-DIRNAME and CONTRAST-DIRNAME are propertized with a
highlighting face."
(setq base-dirname (file-name-as-directory base-dirname))
(setq contrast-dirname (file-name-as-directory contrast-dirname))
(flet ((insert-dir (dirname)
(insert (file-name-as-directory dirname)))
(insert-dir/propzd (dirname)
(slime-insert-propertized '(face highlight) dirname)
(insert "/"))) ; Not exactly portable (to VMS...)
(let ((base-dirs (slime-split-string base-dirname "/" t))
(contrast-dirs (slime-split-string contrast-dirname "/" t)))
(with-temp-buffer
(loop initially (insert (slime-filesystem-toplevel-directory))
for base-dir in base-dirs do
(let ((pos (position base-dir contrast-dirs :test #'equal)))
(if (not pos)
(insert-dir/propzd base-dir)
(progn (insert-dir base-dir)
(setq contrast-dirs (nthcdr (1+ pos) contrast-dirs))))))
(buffer-substring (point-min) (point-max))))))
(defvar slime-warn-when-possibly-tricked-by-M-. t
"When working on multiple source trees simultaneously, the way
`slime-edit-definition' (M-.) works can sometimes be confusing:
`M-.' visits locations that are present in the current Lisp image,
which works perfectly well as long as the image reflects the source
tree that one is currently looking at.
In the other case, however, one can easily end up visiting a file
in a different source root directory (the one corresponding to
the Lisp image), and is thus easily tricked to modify the wrong
source files---which can lead to quite some stressfull cursing.
If this variable is T, a warning message is issued to raise the
user's attention whenever `M-.' is about opening a file in a
different source root that also exists in the source root
directory of the user's current buffer.
There's no guarantee that all possible cases are covered, but
if you encounter such a warning, it's a strong indication that
you should check twice before modifying.")
(defun slime-maybe-warn-for-different-source-root (target-filename buffer-filename)
(when slime-warn-when-possibly-tricked-by-M-.
(let ((guessed-target (slime-file-name-merge-source-root target-filename
buffer-filename)))
(when (and guessed-target
(not (equal guessed-target target-filename))
(file-exists-p guessed-target))
(slime-message "Attention: This is `%s'."
(concat (slime-highlight-differences-in-dirname
(file-name-directory target-filename)
(file-name-directory guessed-target))
(file-name-nondirectory target-filename)))))))
(defun slime-check-location-filename-sanity (filename)
(flet ((file-truename-safe (filename) (and filename (file-truename filename))))
(let ((target-filename (file-truename-safe filename))
(buffer-filename (file-truename-safe (buffer-file-name))))
(when buffer-filename
(slime-maybe-warn-for-different-source-root target-filename buffer-filename)))))
(defun slime-check-location-buffer-name-sanity (buffer-name)
(flet ((file-truename-safe (filename) (and filename (file-truename filename))))
(let ((old-buffer-filename (file-truename-safe (buffer-file-name)))
(target-buffer-filename (file-truename-safe
(buffer-file-name (get-buffer buffer-name)))))
(when (and target-buffer-filename old-buffer-filename)
(slime-maybe-warn-for-different-source-root target-buffer-filename
old-buffer-filename)))))
(defun slime-goto-location-buffer (buffer)
(destructure-case buffer
((:file filename)
(let ((filename (slime-from-lisp-filename filename)))
(slime-check-location-filename-sanity filename)
(set-buffer (or (get-file-buffer filename)
(let ((find-file-suppress-same-file-warnings t))
(find-file-noselect filename))))))
((:buffer buffer-name)
(slime-check-location-buffer-name-sanity buffer-name)
(set-buffer buffer-name))
((:source-form string)
(set-buffer (get-buffer-create "*SLIME Source Form*"))
(erase-buffer)
(lisp-mode)
(insert string)
(goto-char (point-min)))
((:zip file entry)
(require 'arc-mode)
(set-buffer (find-file-noselect (slime-from-lisp-filename file) t))
(goto-char (point-min))
(re-search-forward (concat " " entry "$"))
(let ((buffer (save-window-excursion
(archive-extract)
(current-buffer))))
(set-buffer buffer)
(goto-char (point-min))))))
(defun slime-goto-location-position (position)
(destructure-case position
((:position pos)
(goto-char 1)
(forward-char (- (1- pos) (slime-eol-conversion-fixup (1- pos)))))
((:offset start offset)
(goto-char start)
(forward-char offset))
((:line start &optional column)
(goto-char (point-min))
(beginning-of-line start)
(cond (column (move-to-column column))
(t (skip-chars-forward " \t"))))
((:function-name name)
(let ((case-fold-search t)
(name (regexp-quote name)))
(when (or
(re-search-forward
(format "\\s *(def\\(\\s_\\|\\sw\\)*\\s +%s\\S_" name) nil t)
(re-search-forward
(format "\\s *(def\\(\\s_\\|\\sw\\)*\\s +(*%s\\S_" name) nil t)
(re-search-forward
(format "[( \t]%s\\>\\(\\s \\|$\\)" name) nil t))
(goto-char (match-beginning 0)))))
((:method name specializers &rest qualifiers)
(slime-search-method-location name specializers qualifiers))
((:source-path source-path start-position)
(cond (start-position
(goto-char start-position)
(slime-forward-positioned-source-path source-path))
(t
(slime-forward-source-path source-path))))
((:eof)
(goto-char (point-max)))))
(defun slime-eol-conversion-fixup (n)
;; Return the number of \r\n eol markers that we need to cross when
;; moving N chars forward. N is the number of chars but \r\n are
;; counted as 2 separate chars.
(case (coding-system-eol-type buffer-file-coding-system)
((1)
(save-excursion
(do ((pos (+ (point) n))
(count 0 (1+ count)))
((>= (point) pos) (1- count))
(forward-line)
(decf pos))))
(t 0)))
(defun slime-search-method-location (name specializers qualifiers)
;; Look for a sequence of words (def<something> method name
;; qualifers specializers don't look for "T" since it isn't requires
;; (arg without t) as class is taken as such.
(let* ((case-fold-search t)
(name (regexp-quote name))
(qualifiers (mapconcat (lambda (el) (concat ".+?\\<" el "\\>"))
qualifiers ""))
(specializers (mapconcat (lambda (el)
(if (eql (aref el 0) ?\()
(let ((spec (read el)))
(if (eq (car spec) 'EQL)
(concat ".*?\\n\\{0,1\\}.*?(EQL.*?'\\{0,1\\}"
(format "%s" (second spec)) ")")
(error "don't understand specializer: %s,%s" el (car spec))))
(concat ".+?\n\\{0,1\\}.+?\\<" el "\\>")))
(remove "T" specializers) ""))
(regexp (format "\\s *(def\\(\\s_\\|\\sw\\)*\\s +%s\\s +%s%s" name
qualifiers specializers)))
(or (and (re-search-forward regexp nil t)
(goto-char (match-beginning 0)))
;; (slime-goto-location-position `(:function-name ,name))
)))
(defun slime-search-call-site (fname)
"Move to the place where FNAME called.
Don't move if there are multiple or no calls in the current defun."
(save-restriction
(narrow-to-defun)
(let ((start (point))
(regexp (concat "(" fname "[\n \t]")))
(cond ((and (re-search-forward regexp nil t)
(not (re-search-forward regexp nil t)))
(goto-char (match-beginning 0)))
(t (goto-char start))))))
(defun slime-goto-source-location (location &optional noerror)
"Move to the source location LOCATION. Several kinds of locations
are supported:
<location> ::= (:location <buffer> <position> <hints>)
| (:error <message>)
<buffer> ::= (:file <filename>)
| (:buffer <buffername>)
| (:source-form <string>)
| (:zip <file> <entry>)
<position> ::= (:position <fixnum>) ; 1 based (for files)
| (:offset <start> <offset>) ; start+offset (for C-c C-c)
| (:line <line> [<column>])
| (:function-name <string>)
| (:source-path <list> <start-position>)
| (:method <name string> <specializer strings> . <qualifiers strings>)"
(destructure-case location
((:location buffer position hints)
(slime-goto-location-buffer buffer)
(let ((pos (slime-location-offset location)))
(cond ((and (<= (point-min) pos) (<= pos (point-max))))
(widen-automatically (widen))
(t (error "Location is outside accessible part of buffer")))
(goto-char pos)))
((:error message)
(if noerror
(slime-message "%s" message)
(error "%s" message)))))
(defun slime-location-offset (location)
"Return the position, as character number, of LOCATION."
(save-restriction
(widen)
(slime-goto-location-position (slime-location.position location))
(let ((hints (slime-location.hints location)))
(when-let (snippet (getf hints :snippet))
(slime-isearch snippet))
(when-let (fname (getf hints :call-site))
(slime-search-call-site fname))
(when (getf hints :align)
(slime-forward-sexp)
(beginning-of-sexp)))
(point)))
;;;;; Incremental search
;;
;; Search for the longest match of a string in either direction.
;;
;; This is for locating text that is expected to be near the point and
;; may have been modified (but hopefully not near the beginning!)
(defun slime-isearch (string)
"Find the longest occurence of STRING either backwards of forwards.
If multiple matches exist the choose the one nearest to point."
(goto-char
(let* ((start (point))
(len1 (slime-isearch-with-function 'search-forward string))
(pos1 (point)))
(goto-char start)
(let* ((len2 (slime-isearch-with-function 'search-backward string))
(pos2 (point)))
(cond ((and len1 len2)
;; Have a match in both directions
(cond ((= len1 len2)
;; Both are full matches -- choose the nearest.
(if (< (abs (- start pos1))
(abs (- start pos2)))
pos1 pos2))
((> len1 len2) pos1)
((> len2 len1) pos2)))
(len1 pos1)
(len2 pos2)
(t start))))))
(defun slime-isearch-with-function (search-fn string)
"Search for the longest substring of STRING using SEARCH-FN.
SEARCH-FN is either the symbol `search-forward' or `search-backward'."
(unless (string= string "")
(loop for i from 1 to (length string)
while (funcall search-fn (substring string 0 i) nil t)
for match-data = (match-data)
do (case search-fn
(search-forward (goto-char (match-beginning 0)))
(search-backward (goto-char (1+ (match-end 0)))))
finally (return (if (null match-data)
nil
;; Finish based on the last successful match
(store-match-data match-data)
(goto-char (match-beginning 0))
(- (match-end 0) (match-beginning 0)))))))
;;;;; Visiting and navigating the overlays of compiler notes
(defun slime-next-note ()
"Go to and describe the next compiler note in the buffer."
(interactive)
(let ((here (point))
(note (slime-find-next-note)))
(if note
(slime-show-note note)
(progn
(goto-char here)
(message "No next note.")))))
(defun slime-previous-note ()
"Go to and describe the previous compiler note in the buffer."
(interactive)
(let ((here (point))
(note (slime-find-previous-note)))
(if note
(slime-show-note note)
(progn
(goto-char here)
(message "No previous note.")))))
(defun slime-goto-first-note (&rest ignore)
"Go to the first note in the buffer."
(let ((point (point)))
(goto-char (point-min))
(cond ((slime-find-next-note)
(slime-show-note (slime-note-at-point)))
(t (goto-char point)))))
(defun slime-remove-notes ()
"Remove compiler-note annotations from the current buffer."
(interactive)
(slime-remove-old-overlays))
(defun slime-show-note (overlay)
"Present the details of a compiler note to the user."
(slime-temporarily-highlight-note overlay)
(if (get-buffer-window "*SLIME Compilation*" t)
(slime-goto-note-in-compilation-log (overlay-get overlay 'slime-note))
(let ((message (get-char-property (point) 'help-echo)))
(slime-message "%s" (if (zerop (length message)) "\"\"" message)))))
(defun slime-temporarily-highlight-note (overlay)
"Temporarily highlight a compiler note's overlay.
The highlighting is designed to both make the relevant source more
visible, and to highlight any further notes that are nested inside the
current one.
The highlighting is automatically undone with a timer."
(run-with-timer 0.2 nil
#'overlay-put overlay 'face (overlay-get overlay 'face))
(overlay-put overlay 'face 'slime-highlight-face))
;;;;; Overlay lookup operations
(defun slime-note-at-point ()
"Return the overlay for a note starting at point, otherwise NIL."
(find (point) (slime-note-overlays-at-point)
:key 'overlay-start))
(defun slime-note-overlay-p (overlay)
"Return true if OVERLAY represents a compiler note."
(overlay-get overlay 'slime-note))
(defun slime-note-overlays-at-point ()
"Return a list of all note overlays that are under the point."
(remove-if-not 'slime-note-overlay-p (overlays-at (point))))
(defun slime-find-next-note ()
"Go to the next position with the `slime-note' text property.
Retuns the note overlay if such a position is found, otherwise nil."
(slime-find-note 'next-single-char-property-change))
(defun slime-find-previous-note ()
"Go to the next position with the `slime' text property.
Retuns the note overlay if such a position is found, otherwise nil."
(slime-find-note 'previous-single-char-property-change))
(defun slime-find-note (next-candidate-fn)
"Seek out the beginning of a note.
NEXT-CANDIDATE-FN is called to find each new position for consideration.
Return the note overlay if such a position is found, otherwise nil."
(let ((origin (point))
(overlay))
(loop do (goto-char (funcall next-candidate-fn (point) 'slime-note))
until (or (setq overlay (slime-note-at-point))
(eobp)
(bobp)))
(unless overlay (goto-char origin))
overlay))
;;;; Arglist Display
(defun slime-space (n)
"Insert a space and print some relevant information (function arglist).
Designed to be bound to the SPC key. Prefix argument can be used to insert
more than one space."
(interactive "p")
(self-insert-command n)
(when (slime-background-activities-enabled-p)
(slime-echo-arglist)))
(put 'slime-space 'delete-selection t) ; for delete-section-mode & CUA
(defvar slime-echo-arglist-function 'slime-show-arglist)
(defun slime-echo-arglist ()
"Display the arglist of the current form in the echo area."
(funcall slime-echo-arglist-function))
(defun slime-show-arglist ()
(let ((op (slime-operator-before-point)))
(when op
(slime-eval-async `(swank:operator-arglist ,op ,(slime-current-package))
(lambda (arglist)
(when arglist
(slime-message "%s" arglist)))))))
(defun slime-operator-before-point ()
(ignore-errors
(save-excursion
(backward-up-list 1)
(down-list 1)
(slime-symbol-at-point))))
;;;; Completion
;; XXX those long names are ugly to read; long names an indicator for
;; bad factoring?
(defvar slime-completions-buffer-name "*Completions*")
(make-variable-buffer-local
(defvar slime-complete-saved-window-configuration nil
"Window configuration before we show the *Completions* buffer.
This is buffer local in the buffer where the completion is
performed."))
(make-variable-buffer-local
(defvar slime-completions-window nil
"The window displaying *Completions* after saving window configuration.
If this window is no longer active or displaying the completions
buffer then we can ignore `slime-complete-saved-window-configuration'."))
(defun slime-complete-maybe-save-window-configuration ()
"Maybe save the current window configuration.
Return true if the configuration was saved."
(unless (or slime-complete-saved-window-configuration
(get-buffer-window slime-completions-buffer-name))
(setq slime-complete-saved-window-configuration
(current-window-configuration))
t))
(defun slime-complete-delay-restoration ()
(slime-add-local-hook 'pre-command-hook
'slime-complete-maybe-restore-window-configuration))
(defun slime-complete-forget-window-configuration ()
(setq slime-complete-saved-window-configuration nil)
(setq slime-completions-window nil))
(defun slime-complete-restore-window-configuration ()
"Restore the window config if available."
(remove-hook 'pre-command-hook
'slime-complete-maybe-restore-window-configuration)
(when (and slime-complete-saved-window-configuration
(slime-completion-window-active-p))
;; XEmacs does not allow us to restore a window configuration from
;; pre-command-hook, so we do it asynchronously.
(slime-run-when-idle
(lambda ()
(save-excursion
(set-window-configuration
slime-complete-saved-window-configuration))
(setq slime-complete-saved-window-configuration nil)
(when (buffer-live-p slime-completions-buffer-name)
(kill-buffer slime-completions-buffer-name))))))
(defun slime-complete-maybe-restore-window-configuration ()
"Restore the window configuration, if the following command
terminates a current completion."
(remove-hook 'pre-command-hook
'slime-complete-maybe-restore-window-configuration)
(condition-case err
(cond ((find last-command-char "()\"'`,# \r\n:")
(slime-complete-restore-window-configuration))
((not (slime-completion-window-active-p))
(slime-complete-forget-window-configuration))
(t
(slime-complete-delay-restoration)))
(error
;; Because this is called on the pre-command-hook, we mustn't let
;; errors propagate.
(message "Error in slime-complete-restore-window-configuration: %S" err))))
(defun slime-completion-window-active-p ()
"Is the completion window currently active?"
(and (window-live-p slime-completions-window)
(equal (buffer-name (window-buffer slime-completions-window))
slime-completions-buffer-name)))
(defun slime-display-completion-list (completions base)
(let ((savedp (slime-complete-maybe-save-window-configuration)))
(with-output-to-temp-buffer slime-completions-buffer-name
(display-completion-list completions)
(let ((offset (- (point) 1 (length base))))
(with-current-buffer standard-output
(setq completion-base-size offset)
(set-syntax-table lisp-mode-syntax-table))))
(when savedp
(setq slime-completions-window
(get-buffer-window slime-completions-buffer-name)))))
(defun slime-display-or-scroll-completions (completions base)
(cond ((and (eq last-command this-command)
(slime-completion-window-active-p))
(slime-scroll-completions))
(t
(slime-display-completion-list completions base)))
(slime-complete-delay-restoration))
(defun slime-scroll-completions ()
(let ((window slime-completions-window))
(with-current-buffer (window-buffer window)
(if (pos-visible-in-window-p (point-max) window)
(set-window-start window (point-min))
(save-selected-window
(select-window window)
(scroll-up))))))
(defun slime-complete-symbol ()
"Complete the symbol at point.
Completion is performed by `slime-complete-symbol-function'."
(interactive)
(funcall slime-complete-symbol-function))
(defun slime-simple-complete-symbol ()
"Complete the symbol at point.
Perform completion more similar to Emacs' complete-symbol."
(or (slime-maybe-complete-as-filename)
(let* ((end (point))
(beg (slime-symbol-start-pos))
(prefix (buffer-substring-no-properties beg end))
(result (slime-simple-completions prefix)))
(destructuring-bind (completions partial) result
(if (null completions)
(progn (slime-minibuffer-respecting-message
"Can't find completion for \"%s\"" prefix)
(ding)
(slime-complete-restore-window-configuration))
(insert-and-inherit (substring partial (length prefix)))
(cond ((slime-length= completions 1)
(slime-minibuffer-respecting-message "Sole completion")
(slime-complete-restore-window-configuration))
;; Incomplete
(t
(slime-minibuffer-respecting-message
"Complete but not unique")
(slime-display-or-scroll-completions completions
partial))))))))
(defun slime-maybe-complete-as-filename ()
"If point is at a string starting with \", complete it as filename.
Return nil if point is not at filename."
(if (save-excursion (re-search-backward "\"[^ \t\n]+\\=" nil t))
(let ((comint-completion-addsuffix '("/" . "\"")))
(comint-replace-by-expanded-filename)
t)
nil))
(defun slime-minibuffer-respecting-message (format &rest format-args)
"Display TEXT as a message, without hiding any minibuffer contents."
(let ((text (format " [%s]" (apply #'format format format-args))))
(if (minibuffer-window-active-p (minibuffer-window))
(if (fboundp 'temp-minibuffer-message) ;; XEmacs
(temp-minibuffer-message text)
(minibuffer-message text))
(message "%s" text))))
(defun slime-indent-and-complete-symbol ()
"Indent the current line and perform symbol completion.
First indent the line. If indenting doesn't move point, complete
the symbol. If there's no symbol at the point, show the arglist
for the most recently enclosed macro or function."
(interactive)
(let ((pos (point)))
(unless (get-text-property (line-beginning-position) 'slime-repl-prompt)
(lisp-indent-line))
(when (= pos (point))
(cond ((save-excursion (re-search-backward "[^() \n\t\r]+\\=" nil t))
(slime-complete-symbol))
((memq (char-before) '(?\t ?\ ))
(slime-echo-arglist))))))
(defvar slime-read-expression-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map minibuffer-local-map)
(define-key map "\t" 'slime-complete-symbol)
(define-key map "\M-\t" 'slime-complete-symbol)
map)
"Minibuffer keymap used for reading CL expressions.")
(defvar slime-read-expression-history '()
"History list of expressions read from the minibuffer.")
(defun slime-read-from-minibuffer (prompt &optional initial-value)
"Read a string from the minibuffer, prompting with PROMPT.
If INITIAL-VALUE is non-nil, it is inserted into the minibuffer before
reading input. The result is a string (\"\" if no input was given)."
(let ((minibuffer-setup-hook
(cons (lexical-let ((package (slime-current-package))
(connection (slime-connection)))
(lambda ()
(setq slime-buffer-package package)
(setq slime-buffer-connection connection)
(set-syntax-table lisp-mode-syntax-table)))
minibuffer-setup-hook)))
(read-from-minibuffer prompt initial-value slime-read-expression-map
nil 'slime-read-expression-history)))
(defun slime-bogus-completion-alist (list)
"Make an alist out of list.
The same elements go in the CAR, and nil in the CDR. To support the
apparently very stupid `try-completions' interface, that wants an
alist but ignores CDRs."
(mapcar (lambda (x) (cons x nil)) list))
(defun slime-simple-completions (prefix)
(let ((slime-current-thread t))
(slime-eval
`(swank:simple-completions ,prefix ',(slime-current-package)))))
;;;; Edit definition
(defun slime-push-definition-stack ()
"Add point to find-tag-marker-ring."
(require 'etags)
(cond ((featurep 'xemacs)
(push-tag-mark))
(t (ring-insert find-tag-marker-ring (point-marker)))))
(defun slime-pop-find-definition-stack ()
"Pop the edit-definition stack and goto the location."
(interactive)
(cond ((featurep 'xemacs) (pop-tag-mark nil))
(t (pop-tag-mark))))
(defstruct (slime-xref (:conc-name slime-xref.) (:type list))
dspec location)
(defstruct (slime-location (:conc-name slime-location.) (:type list)
(:constructor nil)
(:copier nil))
tag buffer position hints)
(defun slime-location-p (o) (and (consp o) (eq (car o) :location)))
(defun slime-xref-has-location-p (xref)
(slime-location-p (slime-xref.location xref)))
(defun make-slime-buffer-location (buffer-name position &optional hints)
`(:location (:buffer ,buffer-name) (:position ,position)
,(when hints `(:hints ,hints))))
(defun make-slime-file-location (file-name position &optional hints)
`(:location (:file ,file-name) (:position ,position)
,(when hints `(:hints ,hints))))
;;; The hooks are tried in order until one succeeds, otherwise the
;;; default implementation involving `slime-find-definitions-function'
;;; is used. The hooks are called with the same arguments as
;;; `slime-edit-definition'.
(defvar slime-edit-definition-hooks)
(defun slime-edit-definition (name &optional where)
"Lookup the definition of the name at point.
If there's no name at point, or a prefix argument is given, then the
function name is prompted."
(interactive (list (slime-read-symbol-name "Edit Definition of: ")))
(or (run-hook-with-args-until-success 'slime-edit-definition-hooks
name where)
(slime-edit-definition-cont (slime-find-definitions name)
name where)))
(defun slime-edit-definition-cont (xrefs name where)
(destructuring-bind (1loc file-alist) (slime-analyze-xrefs xrefs)
(cond ((null xrefs)
(error "No known definition for: %s (in %s)"
name (slime-current-package)))
(1loc
(slime-push-definition-stack)
(slime-pop-to-location (slime-xref.location (car xrefs)) where))
((slime-length= xrefs 1) ; ((:error "..."))
(error "%s" (cadr (slime-xref.location (car xrefs)))))
(t
(slime-push-definition-stack)
(slime-show-xrefs file-alist 'definition name
(slime-current-package))))))
;;; FIXME. TODO: Would be nice to group the symbols (in each
;;; type-group) by their home-package.
(defun slime-edit-uses (symbol)
"Lookup all the uses of SYMBOL."
(interactive (list (slime-read-symbol-name "Edit Uses of: ")))
(slime-xrefs '(:calls :macroexpands
:binds :references :sets
:specializes)
symbol
#'(lambda (xrefs type symbol package snapshot)
(cond
((null xrefs)
(message "No xref information found for %s." symbol))
((and (slime-length= xrefs 1) ; one group
(slime-length= (cdar xrefs) 1)) ; one ref in group
(destructuring-bind (_ (_ loc)) (first xrefs)
(slime-push-definition-stack)
(slime-pop-to-location loc)))
(t
(slime-push-definition-stack)
(slime-show-xref-buffer xrefs type symbol
package snapshot))))))
(defun slime-analyze-xrefs (xrefs)
"Find common filenames in XREFS.
Return a list (SINGLE-LOCATION FILE-ALIST).
SINGLE-LOCATION is true if all xrefs point to the same location.
FILE-ALIST is an alist of the form ((FILENAME . (XREF ...)) ...)."
(list (and xrefs
(let ((loc (slime-xref.location (car xrefs))))
(and (slime-location-p loc)
(every (lambda (x) (equal (slime-xref.location x) loc))
(cdr xrefs)))))
(slime-alistify xrefs #'slime-xref-group #'equal)))
(defun slime-xref-group (xref)
(cond ((slime-xref-has-location-p xref)
(destructure-case (slime-location.buffer (slime-xref.location xref))
((:file filename) filename)
((:buffer bufname)
(let ((buffer (get-buffer bufname)))
(if buffer
(format "%S" buffer) ; "#<buffer foo.lisp>"
(format "%s (previously existing buffer)" bufname))))
((:source-form _) "(S-Exp)")
((:zip zip entry) entry)))
(t
"(No location)")))
(defun slime-pop-to-location (location &optional where)
(slime-goto-source-location location)
(ecase where
((nil) (switch-to-buffer (current-buffer)))
(window (pop-to-buffer (current-buffer) t))
(frame (let ((pop-up-frames t)) (pop-to-buffer (current-buffer) t)))))
(defun slime-find-definitions (name)
"Find definitions for NAME."
(funcall slime-find-definitions-function name))
(defun slime-find-definitions-rpc (name)
(slime-eval `(swank:find-definitions-for-emacs ,name)))
(defun slime-edit-definition-other-window (name)
"Like `slime-edit-definition' but switch to the other window."
(interactive (list (slime-read-symbol-name "Symbol: ")))
(slime-edit-definition name 'window))
(defun slime-edit-definition-other-frame (name)
"Like `slime-edit-definition' but switch to the other window."
(interactive (list (slime-read-symbol-name "Symbol: ")))
(slime-edit-definition name 'frame))
(defun slime-edit-definition-with-etags (name)
(interactive (list (slime-read-symbol-name "Symbol: ")))
(let ((xrefs (slime-etags-definitions name)))
(cond (xrefs
(message "Using tag file...")
(slime-edit-definition-cont xrefs name nil))
(t
(error "No known definition for: %s" name)))))
(defun slime-etags-definitions (name)
"Search definitions matching NAME in the tags file.
The result is a (possibly empty) list of definitions."
(require 'etags)
(let ((defs '()))
(save-excursion
(let ((first-time t))
(while (visit-tags-table-buffer (not first-time))
(setq first-time nil)
(goto-char (point-min))
(while (search-forward name nil t)
(beginning-of-line)
(destructuring-bind (hint line &rest pos) (etags-snarf-tag)
(unless (eq hint t) ; hint==t if we are in a filename line
(let ((file (expand-file-name (file-of-tag))))
(let ((loc `(:location (:file ,file)
(:line ,line)
(:snippet ,hint))))
(push (list hint loc) defs))))))))
(reverse defs))))
;;;;; first-change-hook
(defun slime-first-change-hook ()
"Notify Lisp that a source file's buffer has been modified."
;; Be careful not to disturb anything!
;; In particular if we muck up the match-data then query-replace
;; breaks. -luke (26/Jul/2004)
(save-excursion
(save-match-data
(when (and (buffer-file-name)
(file-exists-p (buffer-file-name))
(slime-background-activities-enabled-p))
(let ((filename (slime-to-lisp-filename (buffer-file-name))))
(slime-eval-async `(swank:buffer-first-change ,filename)))))))
(defun slime-setup-first-change-hook ()
(add-hook (make-local-variable 'first-change-hook)
'slime-first-change-hook))
(add-hook 'slime-mode-hook 'slime-setup-first-change-hook)
;;;; Eval for Lisp
(defun slime-eval-for-lisp (thread tag form-string)
(let ((ok nil)
(value nil)
(c (slime-connection)))
(unwind-protect (progn
(slime-check-eval-in-emacs-enabled)
(setq value (eval (read form-string)))
(setq ok t))
(let ((result (if ok `(:ok ,value) `(:abort))))
(slime-dispatch-event `(:emacs-return ,thread ,tag ,result) c)))))
(defun slime-check-eval-in-emacs-enabled ()
"Raise an error if `slime-enable-evaluate-in-emacs' isn't true."
(unless slime-enable-evaluate-in-emacs
(error (concat "slime-eval-in-emacs disabled for security."
"Set slime-enable-evaluate-in-emacs true to enable it."))))
;;;; `ED'
(defvar slime-ed-frame nil
"The frame used by `slime-ed'.")
(defcustom slime-ed-use-dedicated-frame t
"*When non-nil, `slime-ed' will create and reuse a dedicated frame."
:type 'boolean
:group 'slime-mode)
(defun slime-ed (what)
"Edit WHAT.
WHAT can be:
A filename (string),
A list (:filename FILENAME &key LINE COLUMN POSITION),
A function name (:function-name STRING)
nil.
This is for use in the implementation of COMMON-LISP:ED."
(when slime-ed-use-dedicated-frame
(unless (and slime-ed-frame (frame-live-p slime-ed-frame))
(setq slime-ed-frame (make-frame)))
(select-frame slime-ed-frame))
(when what
(destructure-case what
((:filename file &key line column position)
(find-file (slime-from-lisp-filename file))
(when line (goto-line line))
(when column (move-to-column column))
(when position (goto-char position)))
((:function-name name)
(slime-edit-definition name)))))
(defun slime-y-or-n-p (thread tag question)
(slime-dispatch-event `(:emacs-return ,thread ,tag ,(y-or-n-p question))))
(defun slime-read-from-minibuffer-for-swank (thread tag prompt initial-value)
(let ((answer (condition-case nil
(slime-read-from-minibuffer prompt initial-value)
(quit nil))))
(slime-dispatch-event `(:emacs-return ,thread ,tag ,answer))))
;;;; Interactive evaluation.
(defun slime-interactive-eval (string)
"Read and evaluate STRING and print value in minibuffer.
Note: If a prefix argument is in effect then the result will be
inserted in the current buffer."
(interactive (list (slime-read-from-minibuffer "Slime Eval: ")))
(cond ((not current-prefix-arg)
(slime-eval-with-transcript `(swank:interactive-eval ,string)))
(t
(slime-eval-print string))))
(defun slime-display-eval-result (value)
(slime-message "%s" value))
(defun slime-eval-print (string)
"Eval STRING in Lisp; insert any output and the result at point."
(slime-eval-async `(swank:eval-and-grab-output ,string)
(lambda (result)
(destructuring-bind (output value) result
(insert output value)))))
(defvar slime-transcript-start-hook nil
"Hook run before start an evalution.")
(defvar slime-transcript-stop-hook nil
"Hook run after finishing a evalution.")
(defun slime-eval-with-transcript (form)
"Eval FROM in Lisp. Display output, if any."
(run-hooks 'slime-transcript-start-hook)
(slime-rex () (form)
((:ok value)
(run-hooks 'slime-transcript-stop-hook)
(slime-display-eval-result value))
((:abort)
(run-hooks 'slime-transcript-stop-hook)
(message "Evaluation aborted."))))
(defun slime-eval-describe (form)
"Evaluate FORM in Lisp and display the result in a new buffer."
(slime-eval-async form (slime-rcurry #'slime-show-description
(slime-current-package))))
(defvar slime-description-autofocus nil
"If non-nil select description windows on display.")
(defun slime-show-description (string package)
;; So we can have one description buffer open per connection. Useful
;; for comparing the output of DISASSEMBLE across implementations.
(let ((bufname (format "*SLIME Description <%s>*" (slime-connection-name))))
(slime-with-popup-buffer (bufname package t slime-description-autofocus)
(princ string)
(goto-char (point-min)))))
(defun slime-last-expression ()
(buffer-substring-no-properties
(save-excursion (backward-sexp) (point))
(point)))
(defun slime-eval-last-expression ()
"Evaluate the expression preceding point."
(interactive)
(slime-interactive-eval (slime-last-expression)))
;;(defun slime-eval-last-expression-display-output ()
;; "Display output buffer and evaluate the expression preceding point."
;; (interactive)
;; (slime-display-output-buffer)
;; (slime-interactive-eval (slime-last-expression)))
(defun slime-eval-defun ()
"Evaluate the current toplevel form.
Use `slime-re-evaluate-defvar' if the from starts with '(defvar'"
(interactive)
(let ((form (slime-defun-at-point)))
(cond ((string-match "^(defvar " form)
(slime-re-evaluate-defvar form))
(t
(slime-interactive-eval form)))))
(defun slime-eval-region (start end)
"Evaluate region."
(interactive "r")
(slime-eval-with-transcript
`(swank:interactive-eval-region
,(buffer-substring-no-properties start end))))
(defun slime-eval-buffer ()