kovisoft / slimv

Official mirror of Slimv versions released on vim.org
454 stars 60 forks source link

SlimvArglist does not handle context like for make-instance #118

Open Jach opened 2 years ago

Jach commented 2 years ago

In Common Lisp, you might define a class:

(defclass person ()
  ((name :initarg :name
         :initform (alexandria:required-argument :name))
   (favorite-color :initarg :favorite-color)))

Here it has two slots, with :name being required. So if you try to do (make-instance 'foo), that is, don't supply a :name, then you'll enter the debugger. (make-instance 'foo :name "Bob") works because :favorite-color is optional.

It would be nice to know in the displayed arglist that when you type "(make-instance 'foo " that :name is a required argument. Instead you just get the default arglist for make-instance, which is the actual (make-instance CLASS &REST INITARGS &KEY &ALLOW-OTHER-KEYS)

Emacs with Sly (and possibly with Slime) however seems to support this and should show something like the expected (make-instance 'person &rest initargs &key favorite-color (name (alexandria:required-argument :name)) &allow-other-keys) i.e. the two possible keyword args are explicitly listed and you even see the default value for the second one indicating it's required.


From here I'll just share some digging I did and some functions I found that may help resolve this, but I am unsure what a good fix would look like here, and I haven't yet even hacked my own local fix yet.

The flow starts at slimv.vim's SlimvArglist function which finds out the function name of the current form after e.g. pressing space, and passes this down to swank.py's swank_op_arglist function. This calls back into Lisp (e.g. for make-instance) something like (swank:operator-arglist "make-instance" *package*) and evaluating that in the REPL normally will give the same echoed string that SlimvArglist then shows.

Digging into swank.lisp's operator-arglist function shows it to be a little wrapper around an implementation-specific introspection call, in SBCL's case it's just sb-introspect:function-lambda-list. e.g.:

(sb-introspect:function-lambda-list 'make-instance)
; -> (CLASS &REST SB-PCL::INITARGS &KEY &ALLOW-OTHER-KEYS)

So this code flow at least cannot ever take into account the context of the class name. How does emacs do it then? I'm still not entirely sure but I did look around and found the file swank-arglists.lisp and suspect emacs makes use of that. In particular the file uses and manipulates an arglist structure and there's an extra-keywords generic function that is defined for things like make-instance, make-condition, change-class, ... basically things that have this same dynamic behavior of expected keywords.

There are a few ways to create the structure given code snippets, here's a REPL output for using swank:arglist-dispatch:

CL-USER> (swank:arglist-dispatch 'make-instance nil)
#S(SWANK/BACKEND:ARGLIST
   :PROVIDED-ARGS NIL
   :REQUIRED-ARGS (CLASS)
   :OPTIONAL-ARGS NIL
   :KEY-P T
   :KEYWORD-ARGS NIL
   :REST SB-PCL::INITARGS
   :BODY-P NIL
   :ALLOW-OTHER-KEYS-P T
   :AUX-ARGS NIL
   :ANY-P NIL
   :ANY-ARGS NIL
   :KNOWN-JUNK NIL
   :UNKNOWN-JUNK NIL)
CL-USER> (swank:arglist-dispatch 'make-instance '('person))
#S(SWANK/BACKEND:ARGLIST
   :PROVIDED-ARGS ('PERSON)
   :REQUIRED-ARGS NIL
   :OPTIONAL-ARGS NIL
   :KEY-P T
   :KEYWORD-ARGS (#S(SWANK::KEYWORD-ARG
                     :KEYWORD :FAVORITE-COLOR
                     :ARG-NAME FAVORITE-COLOR
                     :DEFAULT-ARG NIL)
                  #S(SWANK::KEYWORD-ARG
                     :KEYWORD :NAME
                     :ARG-NAME NAME
                     :DEFAULT-ARG (ALEXANDRIA:REQUIRED-ARGUMENT :NAME)))
   :REST SB-PCL::INITARGS
   :BODY-P NIL
   :ALLOW-OTHER-KEYS-P T
   :AUX-ARGS NIL
   :ANY-P NIL
   :ANY-ARGS NIL
   :KNOWN-JUNK NIL
   :UNKNOWN-JUNK NIL)

More intriguing is the unexported function arglist-from-form that makes this even easier:

(swank::arglist-from-form '(make-instance))
(swank::arglist-from-form '(make-instance 'person))

and these can be printed giving what I'm after:

CL-USER> (swank::decoded-arglist-to-string (swank::arglist-from-form '(make-instance)) :operator "make-instance")
"(make-instance class &rest initargs &key &allow-other-keys)"
CL-USER> (swank::decoded-arglist-to-string (swank::arglist-from-form '(make-instance 'person)) :operator "make-instance")
"(make-instance 'person &rest initargs &key favorite-color
               (name (alexandria:required-argument :name)) &allow-other-keys)"

The optional :operator keyword arg just puts whatever string given at the first of the printed list. This even works if we dynamically extend what args constructing a person can have (e.g. supporting 'British spelling'):

(defmethod initialize-instance :after ((self person) &key favourite-colour)
  (when favourite-colour
    (setf (slot-value self 'favorite-color) favourite-colour)))
; now:
CL-USER> (swank::decoded-arglist-to-string (swank::arglist-from-form '(make-instance 'person)) :operator "make-instance")
"(make-instance 'person &rest initargs &key favourite-colour favorite-color
               (name (alexandria:required-argument :name)) &allow-other-keys)"

It seems like it may be possible (at least for CL) to just swap out slimv's use of swank:operator-arglist on the operator name with the above decoded-arglist-to-string around the arglist-from-form call, and just send the entire form down without having to parse it. I think my next step is to hack this in and I'll share it on this issue if/when I do, but there may be good reasons for using the simpler function and perhaps there are edge cases where this stuff in swank-arglists doesn't work and provide the current string.

Another implementation might be to use the exported function swank:autodoc which constructs the arglist and calls decoded-arglist-to-string, however this expects an emacs-y "raw-form" rather than a lisp form and so maybe is more difficult to use from vim...

kovisoft commented 2 years ago

Thank you for the detailed analysis, I really appreciate it. I checked it in emacs and indeed, it uses swank:autodoc (as I recall, older slime versions used swank:operator-arglist). I'm going to check how can I replicate it in slimv.