;;; slime-capf.el --- slime completion capf -*-lexical-binding:t-*- ;; ;; Copyright (C) 2009-2021 Ole Arndt ;; Copyright (C) 2023 Alexander Rosenberg ;; ;; Author: Ole Arndt , Alexander Rosenberg ;; 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 . ;; ;;; 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) ;;; ---------------------------------------------------------------------------- ;;; * 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