11

I'm considering writing a major mode for editing Magic: the Gathering decks.

Most of it seems pretty straightforward but I have one question. There are about 15 000 unique Magic cards available (cards with unique names that is). I would like to be able to complete against them by writing a completion-at-point-function. I've been looking for some simple, basic example of a capf function that just completes against a set of words to base my mode on but have failed to find anything so far. Do you know of any good example for this to get started? And do you believe it would be easy to get good performance or would I have to write my own data structure (I'm thinking like a Trie maybe).

Obviously I'd need to find a way to sync with new cards etc and in the future maybe even be able to search for cards by other characteristics than just the card name but that can wait.

Mattias Bengtsson
  • 1,271
  • 1
  • 11
  • 18

2 Answers2

21

Documentation

The API completion at point function can be found in the documentation of completion-at-point-functions

Each function on this hook is called in turns without any argument and should return either nil to mean that it is not applicable at point, or a function of no argument to perform completion (discouraged), or a list of the form (START END COLLECTION . PROPS) where START and END delimit the entity to complete and should include point, COLLECTION is the completion table to use to complete it, and PROPS is a property list for additional information.

start, end and props are obvious, but I think the format of collection is not defined properly. For that you can see the documentation of try-completion or all-completions

If COLLECTION is an alist, the keys (cars of elements) are the possible completions. If an element is not a cons cell, then the element itself is the possible completion. If COLLECTION is a hash-table, all the keys that are strings or symbols are the possible completions. If COLLECTION is an obarray, the names of all symbols in the obarray are the possible completions.

COLLECTION can also be a function to do the completion itself. It receives three arguments: the values STRING, PREDICATE and nil. Whatever it returns becomes the value of `try-completion'.

Example

Below is a simple example of completion at point function which uses the words defined in /etc/dictionaries-common/words to complete the words in the buffer

(defvar words (split-string (with-temp-buffer
                              (insert-file-contents-literally "/etc/dictionaries-common/words")
                              (buffer-string))
                            "\n"))

(defun words-completion-at-point ()
  (let ((bounds (bounds-of-thing-at-point 'word)))
    (when bounds
      (list (car bounds)
            (cdr bounds)
            words
            :exclusive 'no
            :company-docsig #'identity
            :company-doc-buffer (lambda (cand)
                                  (company-doc-buffer (format "'%s' is defined in '/etc/dictionaries-common/words'" cand)))
            :company-location (lambda (cand)
                                (with-current-buffer (find-file-noselect "/etc/dictionaries-common/words")
                                  (goto-char (point-min))
                                  (cons (current-buffer) (search-forward cand nil t))))))))

The completion function looks for word at point (the library thingatpt is used to find the bounds of word) and completes it against the words in the /etc/dictionaries-common/words file, the property :exclusive is set to no so that emacs can use other capf functions if our fails. Finally some additional properties are set to enhance the company-mode integration.

Performance

The words file on my system had 99171 entries and emacs was able to complete them without any issues, so I guess 15000 entries should not be a problem.

Integration with company-mode

Company mode integrates very well with completion-at-point-functions using the company-capf backend, so it should work out of the box for you, but you can enhance the completions offered by company by returning additional props in the result of capf function. The props currently supported are

:company-doc-buffer - Used by company to display metadata for current candidate

:company-docsig - Used by company to echo metadata about the candidate in the minibuffer

:company-location - Used by company to jump to the location of current candidate

Iqbal Ansari
  • 7,468
  • 1
  • 28
  • 31
0

@Iqbal Ansari gave a great answer. Here's a supplemental answer, hope it'll help.

Here's a implementation using emacs classic completion mechanism, 2009.

;; this is your lang's keywords
(setq xyz-kwdList
      '("touch"
       "touch_start"
       "touch_end"
       "for"
       "foreach"
       "forall"
       ))

The following is the code that does the completion.

(defun xyz-complete-symbol ()
  "Perform keyword completion on word before cursor."
  (interactive)
  (let ((posEnd (point))
        (meat (thing-at-point 'symbol))
        maxMatchResult)

    ;; when nil, set it to empty string, so user can see all lang's keywords.
    ;; if not done, try-completion on nil result lisp error.
    (when (not meat) (setq meat ""))
    (setq maxMatchResult (try-completion meat xyz-kwdList))

    (cond ((eq maxMatchResult t))
          ((null maxMatchResult)
           (message "Can't find completion for “%s”" meat)
           (ding))
          ((not (string= meat maxMatchResult))
           (delete-region (- posEnd (length meat)) posEnd)
           (insert maxMatchResult))
          (t (message "Making completion list…")
             (with-output-to-temp-buffer "*Completions*"
               (display-completion-list 
                (all-completions meat xyz-kwdList)
                meat))
             (message "Making completion list…%s" "done")))))

Following's a implementation using ido-mode's interface. Much simpler.

(defun abc-complete-symbol ()
  "Perform keyword completion on current symbol.
This uses `ido-mode' user interface for completion."
  (interactive)
  (let* (
         (bds (bounds-of-thing-at-point 'symbol))
         (p1 (car bds))
         (p2 (cdr bds))
         (current-sym
          (if  (or (null p1) (null p2) (equal p1 p2))
              ""
            (buffer-substring-no-properties p1 p2)))
         result-sym)
    (when (not current-sym) (setq current-sym ""))
    (setq result-sym
          (ido-completing-read "" xyz-kwdList nil nil current-sym ))
    (delete-region p1 p2)
    (insert result-sym)))

You'll need to define xyz-kwdList as a list of your words.

Xah Lee
  • 1,756
  • 12
  • 11
  • 3
    -1 for reinventing the completion interface in a worse way, going for `null` over `not` and using camelcased identifiers and greek symbols that only make sense in your own modes. – wasamasa Sep 03 '15 at 08:54
  • @wasamasa, null is the proper function for testing if something is nil. So, am not sure you have a valid point there other than personal style difference. For the greek symbol in var name, i agree it's non-standard, but i have my own reasons. Basically, it's a kinda of "hungarian" notation, which is also used in perl, php, ruby, even lisp. The advantage is that, it gives a syntactic distinction of local variables. Article here http://xahlee.info/comp/programing_variable_naming.html – Xah Lee Sep 03 '15 at 10:02
  • @wasamasa what you mean reinventing completion interface? what did i do wrong? – Xah Lee Sep 03 '15 at 10:07
  • 3
    -1 for not answering the question which was about `completion-at-point-functions` (I disagree with @wasamasa about the `null` vs `not` thing). – npostavs Sep 03 '15 at 14:06
  • @npostavs `completion-at-point-function` is a variable wrapper that points to a function for completion... – Xah Lee Sep 03 '15 at 15:03
  • 3
    @XahLee The functions in `completion-at-point-functions` are supposed to return completion data, not perform the completion themselves. So the functions in your answer are not usable as entries in `completion-at-point-functions`. – npostavs Sep 03 '15 at 15:20
  • 1
    @npostavs ah i see. you are right. Thanks! – Xah Lee Sep 03 '15 at 16:58
  • 5
    @npostavs This kind of function would still work, but indeed writing a completion function this way is against the documented interface, and it's strongly discouraged. – Dmitry Sep 03 '15 at 18:29