Skip to content

Instantly share code, notes, and snippets.

@dkochmanski
Last active July 11, 2019 08:53
Show Gist options
  • Save dkochmanski/276b4a28c8ce176c0a2bd8af4dace96c to your computer and use it in GitHub Desktop.
Save dkochmanski/276b4a28c8ce176c0a2bd8af4dace96c to your computer and use it in GitHub Desktop.

Command name completer in the Interactor

Links

Glossary

completion-gestures
A list of gesture to complete the user input “as fully as possible”. In McCLIM :complete gesture bound to TAB.
help-gestures
A list of gestures to show help or to show possibilities. In McCLIM :help gesture bound to C-?.
possibilities-gestures
A list of gestures to show possible completions. In McCLIM :possibilities gesture bound to C-/.
(complete-input stream function &key partial-completers ,@other)
Function works on the input-editing-stream (i.e Drei). It accepts a function which is responsible for producing completions.
(complete-from-generator str gen delim &key action predcate)
Returns set of possibilities starting with str, delimited with delim and generated by a function gen.

Analysis

Presentation method accept specialized on command-name is defined in Core/clim-core/commands.lisp:1213. It calls complete-input with a trampoline to complete-from-generator called with a local function going over all command names which are not disabled. There is some nasty drei-induced hack in there but is is irrelevant to the issue, we may assume that function generator “just works” and ignore it.

(multiple-value-bind (object success string)
    (complete-input stream
                    #'(lambda (so-far mode)
                        (complete-from-generator so-far
                                                 #'generator
                                                 '(#\space)
                                                 :action mode))
                    :partial-completers '(#\space))
  (if success
      (values object type)
      (simple-parse-error "No command named ~S" string)))

So parsing is done as: first command name is parsed then arguments. When we decide that something is a command name there is no way to go back based on what has been put as the first argument. I.e if we have commands Print and Print Foo if we succesfully parse Print as a command typing later Foo as an argument has no means to affect the command name. In other words we can’t make a decision based on input typed after the command.

Function passed to complete-input must behave as follows:

Accepts prefix (so-far string) and action being one of:

:complete
complete either an exact match or “as much as possible”
:complete-maximal
complete “as much as possible”
:complete-limited
complete to the next partial delimiter
:possibilities
return alist of possible completions as last value

Returns five values:

  • completed input string,
  • boolean value indicating success or failure,
  • object matching the completion,
  • the number of matches,
  • the list of possible completions

Function complete-input behaves as follows

Function reads input from the user on the input editing stream until it reads one of the possibilities. partial-completers argument allows completing on some strategic points (i.e spaces or dashes). It returns three values: object, success and string.

Commands and command parsing

Whem command is parsed, then its name is delimited from its argument by one of characters in *command-name-delimiters*. It is specified, that space must be one of them. In principle we could add another delimiter like a double colon, so in ambigous case foo bar qux: could be parsed without fail.

Proposing to “prohibit” space in command name would directly violate how define-command is specified. Most notably command-name-from-symbol defines, that hyphens are replaced by spaces in the command name.

Current complete-input implementation

By “current” I mean that this is how it behaves, not that it is specified to do so.

  1. Return object if it is already typed in the stream.
  2. Read gesture and decide its action.
  • when gesture is possibilities return them
  • when gesture is one of partial-completers return :complete-limited
  • when gesture is one of *completion-gestures* return :complete-maximal
  • when gesture is either delimiter or activation gesture return :complete
  • otherwise return nil
  1. If there is no action append the character to the stream. Goto 1.
  2. Call the completer function with current buffer and the action.
  3. Do spaghetti logic which boils down to:
  • when mode is :complete-limited and there are not matches fallback to :complete if the gesture is a delimiter or activation gesture.
  • then if match is a success and mode is :complete unread the gesture
  • then if there are matches and mode is :possibilities read match from a box
  • then if we are succesful or mode is not :complete put completion in stream
  • then

– if we are succesful and mode is complete return result – else if gesture is activation-gesture either return or signal failure – else Goto 1

Interesting paths and issues:

Undesired parsing interrupt by a value accepting dialog

  1. in point 1 in practice we may return :complete only by activation

gesture, because delimiter is the same as partial-delimiter (space).

Since we unread the gesture on succesful complete activation gesture is read later (return or newline) so not only it completes the input but also opens accepting-values dialog (undesired)

Get stuck at the completion fork

  1. completion-* action yields 2 results and none is exact match
  2. mode is not :complete so we insert the common prefix in buffer
  3. we loop over and completion yields once again 2 results
  4. mode is not :complete so we insert “” in buffer

That means that we can’t complete further with neither completer and we need to type the missing character to reach the conclusion. If the missing character is space, which is partial completer, we are out of luck - it is impossible to enter the command.

Possibilities issue

When we press C-/ multiple times possibilities are shown but output should not be scrolled. It looks awkward and is hardly justifiable. In some cases it also finishes the parsing.

Test cases

Commands in the table: “T”, “To”, “ToLisp”, “To Lisp”, “ToL isp” and “Toggle Foo”. They expose various problems with completion. Also, to not occlude the :possibilities issue , there is a command “Print”. There are also commands “A”, “AB” and “ABC” to illustrate :complete-maximal. Each command accepts one argument.

buffer stategestureactionbuffer afterresult
“T”[space]:complete-limited“To”
“To”[space]:complete-limited“To”:complete fall no-op
“To”[return]:complete“To”command name read, dialog
“To”[tab]:complete-maximal“To”conflicts (no fallback)
“To”“g”nil“Tog”
“To”[tab]:complete-maximal“Toggle Foo”no conflicts procesing OK
“Toggle Foo”[space]:complete-limitedsuccess:complete fall, return
“ToL”[space]:complete-limited“ToL ”We skip direct match ToL
“ToL ”[space]:complete-limited“ToL isp”Next space will rad line
“A”[tab]:complete-maximal“AB”
“AB”[tab]:complete-maximal“ABC”
“ABC”[tab]:complete-maximal“ABC”no op after maximal match
“Print”C-/:possibilities“Print”possibilities are displayed
“Print”[space]:complete-limited“Print”match, command returned
“Pr”C-/:possibilities“Pr”possibilities are displayed
“Pr”[click][continuation]“Print”buffer has the command
“Print”[space]:complete-limited“Print”match, command returned
“Pr”C-/:possibilities“Pr”possibilities are displayed
“Pr”C-/:possibilities””command parse aborted (??)

Proposed chagnes

  • possibilities gesture invoked invoked during possibilities are shown

is a no-op (doesn’t scroll, doesn’t abort command - nothing)

I’d like to test how it behaves more though (especially finding and redefining ramifications when and where it is shown, when (if at all) it disappears, when we can select commands from the list, how it scrolls the buffer etc. I’ve found some other bugs but I didn’t care to narrow them down yet.

  • when the :complete-limited action is triggered and we have a

conflict (nmatches>=2) and input buffer doesn’t change after the operation “just” append gesture to the buffer

  • when the :complete-maximal action is triggered and we have a

conflict (nmatches>=2) and input buffer doesn’t change after the operation invoke the possibilities menu (both as a visual hint that there is a conflict and to show what the options are)

  • when the :complete action is triggered and we have a direct match

do not unread the gesture.

Here is a pseudocode draft of how it should (imo) behave. I also think that structure should not be a progn with multiple IFs but rather ecase like in this example, code in there is very hard to follow. TAGBODY + ECASE would be justified here. Mind it is not something I’ve run or tested, I’ve just tried to put above suggestion in code.

(loop
   (multiple-value-bind (gesture mode)
       (read-completion-gesture)
     (tagbody
      :again
        (multiple-value-bind (input success object nmatches possibilities)
            (and mode (funcall func (subseq so-far 0) mode))
          (ecase mode
            ((nil)
             (vector-push-extend gesture so-far))
            (:possibilities
             (multiple-value-bind (input object) (read-possibility)
               (when input
                 (return-from complete-input
                   (values object success input)))))
            (:complete
             (when success
               (return-from complete-input
                 (values object success input))))
            (:complete-limited
             (cond ((zerop nmatches)
                    ;; we have no further completion, try what we have
                    (setf mode :complete)
                    (go :again))
                   ((emptyp input)
                    ;; we have no progress despite matches
                    (setf mode nil)
                    (go :again))
                   (t
                    ;; if we have a common prefix, complete-limited!
                    (insert-input input))))
            (:complete-maximal
             (cond ((zerop nmatches)
                    (beep))
                   ((emtyp input)
                    ;; we have no progress despite matches
                    (setf mode :possibilities)
                    (go :again))
                   (t
                    ;; we have completion which adds input, complete-maximal!
                    (insert-input input))))))
        (when (activation-gesture-p gesture)
          (if allow-any-input
              (return-from complete-input
                (values nil t (subseq so-far 0)))
              (error 'simple-completion-error
                     :format-control "Input ~S does not match"
                     :format-arguments (list so-far)
                     :input-so-far so-far))))))

Source code for testing

(in-package :clim-user)

(clim:define-application-frame foo-frame ()
  ()
  (:pane :interactor)
  (:geometry :width 600 :height 600))

(defun open-foo-frame ()
  (let ((frame (clim:make-application-frame 'foo-frame)))
    (clim:run-frame-top-level frame)))

(define-foo-frame-command (cmd-0 :name "Print")      ((arg string)) (format t arg))

(define-foo-frame-command (cmd-1 :name "T")          ((arg string)) (format t arg))
(define-foo-frame-command (cmd-2 :name "To")         ((arg string)) (format t arg))
(define-foo-frame-command (cmd-3 :name "ToLisp")     ((arg string)) (format t arg))
(define-foo-frame-command (cmd-4 :name "To Lisp")    ((arg string)) (format t arg))
(define-foo-frame-command (cmd-5 :name "ToL isp")    ((arg string)) (format t arg))
(define-foo-frame-command (cmd-6 :name "Toggle Foo") ((arg string)) (format t arg))

(define-foo-frame-command (cmd-7 :name "A")          ((arg string)) (format t arg))
(define-foo-frame-command (cmd-8 :name "AB")         ((arg string)) (format t arg))
(define-foo-frame-command (cmd-9 :name "ABC")        ((arg string)) (format t arg))

(open-foo-frame)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment