emacs-config/elisp/slime-capf.el

377 lines
14 KiB
EmacsLisp
Raw Normal View History

2024-07-13 04:02:31 -07:00
;;; slime-capf.el --- slime completion capf -*-lexical-binding:t-*-
;;
;; Copyright (C) 2009-2021 Ole Arndt
;; Copyright (C) 2023 Alexander Rosenberg
;;
;; Author: Ole Arndt <anwyn@sugarshark.com>, Alexander Rosenberg <zanderpkg@pm.me>
;; Keywords: convenience, lisp, abbrev
;; Version: 1.0
;; Package-Requires: ((emacs "24.4") (slime "2.13") (cape "1.5"))
;;
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; 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, see <http://www.gnu.org/licenses/>.
;;
;;; Commentary:
;; This file is adapted from this repository:
;; https://github.com/anwyn/slime-company
;;
;; It was modified so that it will work with as a capf instead of a company
;; backend.
;;; Code:
(require 'slime)
(require 'cl-lib)
(require 'eldoc)
(require 'subr-x)
(require 'thingatpt)
(require 'cape)
(define-slime-contrib slime-capf
"Interaction between slime and Emacs capfs."
(:license "GPL")
(:authors "Ole Arndt <anwyn@sugarshark.com>" "Alexander Rosenberg <zanderpkg@pm.me>")
(:swank-dependencies swank-arglists)
(:on-load
(advice-add 'slime--completion-at-point :override
'slime-capf-complete-at-point))
(:on-unload
(advice-remove 'slime--completion-at-point 'slime-capf-complete-at-point)))
;;; ----------------------------------------------------------------------------
;;; * Customization
(defgroup slime-capf nil
"Interaction between slime and the company completion mode."
:group 'slime)
(defcustom slime-capf-after-completion nil
"What to do after a successful completion.
In addition to displaying the arglist slime-capf will also do one of:
- `nil': nothing,
- insert a space. Useful if space does not select the completion candidate.
Works best if you also call `delete-horizontal-space' before closing
parentheses to remove excess whitespace.
- call an arbitrary function with the completion string as the first parameter.
Do not call company-complete inside this function, company doesn't like to
be invoked recursively.
"
:group 'slime-capf
:type '(choice
(const :tag "Do nothing" nil)
(const :tag "Insert space" slime-capf-just-one-space)
(function :tag "Custom function" nil)))
(defcustom slime-capf-transform-arglist 'downcase
"Before echoing the arglist it is passed to this function for transformation."
:group 'slime-capf
:type '(choice
(const :tag "Downcase" downcase)
(const :tag "Do nothing" identity)
(function :tag "Custom function" nil)))
(defcustom slime-capf-display-arglist nil
"Whether to display the arglist of a function in the company popup."
:group 'slime-capf
:type '(choice
(const :tag "Hide arglist" nil)
(const :tag "Show arglist" t)))
(defcustom slime-capf-display-flags t
"Whether to display the symbol's flags in the company popup.
Symbol flags are only returned with the `fuzzy' completion type."
:group 'slime-capf
:type '(choice
(const :tag "Hide flags" nil)
(const :tag "Show flags" t)))
(defcustom slime-capf-completion 'simple
"Which Slime completion method to use: `simple' or `fuzzy'.
`simple' just displays the completion candidate,
`fuzzy' also displays the classification flags as an annotation,
alignment of annotations via `company-tooltip-align-annotations'
is recommended. This method also can complete package names.
"
:group 'slime-capf
:type '(choice
(const simple)
(const fuzzy)))
(defcustom slime-capf-complete-in-comments-and-strings nil
"Should slime-capf also complete in comments and strings."
:group 'slime-capf
:type 'boolean)
(defcustom slime-capf-major-modes
'(lisp-mode clojure-mode slime-repl-mode scheme-mode)
"List of major modes in which slime-capf should be active.
slime-capf actually calls `derived-mode-p' on this list, so it will
be active in derived modes as well."
:group 'slime-capf
:type '(repeat symbol))
(defun slime-capf-just-one-space (completion-string)
(unless (string-suffix-p ":" completion-string)
(just-one-space)))
(defsubst slime-capf-active-p ()
"Test if the slime-capf backend should be active in the current buffer."
(apply #'derived-mode-p slime-capf-major-modes))
(define-derived-mode slime-capf-doc-mode help-mode "Doc"
"Documentation mode for slime-capf."
(setq font-lock-defaults
'((("^\\([^ ]\\{4,\\}\\)\\b" . (1 font-lock-function-name-face t))
("^[ ]*\\b\\([A-Z][A-Za-z0-9_ %\\*\\-]+:\\)\\([ ]\\|$\\)"
. (1 font-lock-doc-face))
("^\\([A-Z][A-Za-z ]+:\\)\\([ ]\\|$\\)"
. (1 font-lock-doc-face t))
("(\\(FUNCTION\\|VALUES\\|OR\\|EQL\\|LAMBDA\\)\\b"
. (1 font-lock-keyword-face))
("[ (]+\\(&[A-Z0-9\\-]+\\)\\b" . (1 font-lock-type-face))
("[ (]+\\(:[A-Z0-9\\-]+\\)\\b" . (1 font-lock-builtin-face))
("\\b\\(T\\|t\\|NIL\\|nil\\|NULL\\|null\\)\\b" . (1 font-lock-constant-face))
("\\b[+-]?[0-9/\\.]+[sdeSDE]?\\+?[0-9]*\\b" . font-lock-constant-face)
("#[xX][+-]?[0-9A-F/]+\\b" . font-lock-constant-face)
("#[oO][+-]?[0-7/]+\\b" . font-lock-constant-face)
("#[bB][+-]?[01/]+\\b" . font-lock-constant-face)
("#[0-9]+[rR][+-]?[0-9A-Z/]+\\b" . font-lock-constant-face)
("\\b\\([A-Z0-9:+%<>#*\\.\\-]\\{2,\\}\\)\\b"
. (1 font-lock-variable-name-face))))))
;;; ----------------------------------------------------------------------------
;;; * Internals
(defun slime-capf--fetch-candidates-async (prefix)
(when (slime-connected-p)
(cl-ecase slime-capf-completion
(simple (slime-capf--fetch-candidates-simple prefix))
(fuzzy (slime-capf--fetch-candidates-fuzzy prefix)))))
(defun slime-capf--fetch-candidates-simple (prefix)
(let ((slime-current-thread :repl-thread)
(package (slime-current-package)))
(cons :async
(lambda (callback)
(slime-eval-async
`(swank:simple-completions ,prefix ',package)
(lambda (result)
(funcall callback (car result)))
package)))))
(defun slime-capf--fetch-candidates-fuzzy (prefix)
(let ((slime-current-thread :repl-thread)
(package (slime-current-package)))
(cons :async
(lambda (callback)
(slime-eval-async
`(swank:fuzzy-completions ,prefix ',package)
(lambda (result)
(funcall callback
(mapcar
(lambda (completion)
(cl-destructuring-bind (sym score _ flags)
completion
(propertize sym 'score score 'flags flags)))
(car result))))
package)))))
(defun slime-capf--fontify-lisp-buffer ()
"Return a buffer in lisp-mode usable for fontifying lisp expressions."
(let ((buffer-name " *slime-capf-fontify*"))
(or (get-buffer buffer-name)
(with-current-buffer (get-buffer-create buffer-name)
(unless (derived-mode-p 'lisp-mode)
;; Advice from slime: Just calling (lisp-mode) will turn slime-mode
;; on in that buffer, which may interfere with the calling function
(setq major-mode 'lisp-mode)
(lisp-mode-variables t))
(current-buffer)))))
(defun slime-capf--fontify-lisp (string)
"Fontify STRING as `font-lock-mode' does in Lisp mode."
;; copied functionality from slime, trimmed somewhat
(with-current-buffer (slime-capf--fontify-lisp-buffer)
(erase-buffer)
(insert (funcall slime-capf-transform-arglist string))
(let ((font-lock-verbose nil))
(font-lock-fontify-region (point-min) (point-max)))
(goto-char (point-min))
(buffer-substring (point-min) (point-max))))
(defun slime-capf--format (doc)
(let ((doc (slime-capf--fontify-lisp doc)))
(cond ((eq eldoc-echo-area-use-multiline-p t) doc)
(t (slime-oneliner (replace-regexp-in-string "[ \n\t]+" " " doc))))))
(defun slime-capf--arglist (arg)
(let ((arglist (slime-eval
`(swank:operator-arglist ,arg ,(slime-current-package)))))
(when arglist
(slime-capf--format arglist))))
(defun slime-capf--arglist-only (arg)
(let ((arglist (slime-eval
`(swank:operator-arglist ,arg ,(slime-current-package)))))
(when arglist
(replace-regexp-in-string
(concat "(" (funcall slime-capf-transform-arglist arg) " ")
" (" (funcall slime-capf-transform-arglist arglist) t t))))
(defun slime-capf--echo-arglist (arg)
(slime-eval-async `(swank:operator-arglist ,arg ,(slime-current-package))
(lambda (arglist)
(when arglist
(slime-message "%s" (slime-capf--format arglist))))))
(defun slime-capf--package-name (pkg)
"Convert a string into into a uninterned symbol name, if it looks
like a package name, i.e. if it has a trailing colon.
Returns NIL if the string does not look like a package name."
(when (string-suffix-p ":" pkg)
(format "#:%s" (string-remove-suffix ":" (string-remove-suffix ":" pkg)))))
(defun slime-capf--build-describe-request (candidate &optional verbose)
(let ((pkg-name (slime-capf--package-name candidate)))
(cond (pkg-name
`(swank::describe-to-string
(cl:find-package
(cl:symbol-name (cl:read-from-string ,pkg-name)))))
(verbose
`(swank:describe-symbol ,candidate))
(t
`(swank:documentation-symbol ,candidate)))))
(defun slime-capf--fontify-doc-buffer (&optional doc)
"Return a buffer in `slime-capf-doc-mode' usable for fontifying documentation."
(with-current-buffer (get-buffer-create "*slime-capf-doc-buffer*")
(slime-capf-doc-mode)
(setq buffer-read-only nil)
(when doc
(insert doc))
(goto-char (point-min))
(current-buffer)))
(defun slime-capf--doc-buffer (candidate)
"Show the Lisp symbol documentation for CANDIDATE in a buffer.
Shows more type info than `slime-capf--quickhelp-string'."
(let* ((slime-current-thread :repl-thread))
(slime-capf--fontify-doc-buffer
(slime-eval (slime-capf--build-describe-request candidate t)
(slime-current-package)))))
(defun slime-capf--quickhelp-string (candidate)
"Retrieve the Lisp symbol documentation for CANDIDATE.
This function does not fontify and displays the result of SWANK's
`documentation-symbol' function, instead of the more verbose `describe-symbol'."
(let ((slime-current-thread :repl-thread))
(slime-eval (slime-capf--build-describe-request candidate)
(slime-current-package))))
(defun slime-capf--location (candidate)
(let ((source-buffer (current-buffer)))
(save-window-excursion
(slime-edit-definition candidate)
(let ((buffer (if (eq source-buffer (current-buffer))
slime-xref-last-buffer
(current-buffer))))
(when (buffer-live-p buffer)
(cons buffer (with-current-buffer buffer
(point))))))))
(defun slime-capf--post-completion (candidate)
(slime-capf--echo-arglist candidate)
(when (functionp slime-capf-after-completion)
(funcall slime-capf-after-completion candidate)))
(defun slime-capf--in-string-or-comment ()
"Return non-nil if point is within a string or comment.
In the REPL we disregard anything not in the current input area."
(save-restriction
(when (derived-mode-p 'slime-repl-mode)
(narrow-to-region slime-repl-input-start-mark (point)))
(let* ((sp (syntax-ppss))
(beg (nth 8 sp)))
(when (or (eq (char-after beg) ?\")
(nth 4 sp))
beg))))
(defun slime-capf--kind (candidate)
"Return the kind of completion that CANDIDATE is."
(when-let ((cs (get-text-property 0 'flags candidate))
((stringp cs)))
(cl-loop for c across cs
when (eq c ?m)
return 'macro
when (or (eq c ?f) (eq c ?g))
return 'function
when (eq c ?b)
return 'variable
when (eq c ?c)
return 'class
when (eq c ?p)
return 'module
when (eq c ?s)
return 'keyword)))
;;; ----------------------------------------------------------------------------
;;; * Company backend function
(defvar *slime-capf--meta-request* nil
"Workaround lock for company-quickhelp, which invokes 'quickhelp-string' or
doc-buffer' while a 'meta' request is running, causing SLIME to cancel requests.")
(defun slime-capf-company-slime (command &optional arg &rest ignored)
"Company mode backend for slime."
(let ((candidate (and arg (substring-no-properties arg))))
(cl-case command
(init
(slime-capf-active-p))
(prefix
(when (and (slime-capf-active-p)
(slime-connected-p)
(or slime-capf-complete-in-comments-and-strings
(null (slime-capf--in-string-or-comment))))
(thing-at-point 'symbol t)))
(candidates
(slime-capf--fetch-candidates-async candidate))
(meta
(let ((*slime-capf--meta-request* t))
(slime-capf--arglist candidate)))
(annotation
(concat (when slime-capf-display-arglist
(slime-capf--arglist-only candidate))
(when slime-capf-display-flags
(concat " " (get-text-property 0 'flags arg)))))
(kind
(slime-capf--kind arg))
(doc-buffer
(unless *slime-capf--meta-request*
(slime-capf--doc-buffer candidate)))
(quickhelp-string
(unless *slime-capf--meta-request*
(slime-capf--quickhelp-string candidate)))
(location
(slime-capf--location candidate))
(post-completion
(slime-capf--post-completion candidate))
(sorted
(eq slime-capf-completion 'fuzzy)))))
(defalias 'slime-capf-complete-at-point
(cape-company-to-capf 'slime-capf-company-slime))
(provide 'slime-capf)
;;; slime-capf.el ends here