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.

up vote 13 down vote accepted

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

  • Oh my! Thanks for the thorough answer! I'll try this out a bit and will accept after that. Extra thanks for the Company hints (which I'm actually using). – Mattias Bengtsson Sep 3 '15 at 4:15
  • Thanks, that is really helpful now I can configure custom completions easily :) – compunaut Sep 3 '15 at 17:16
  • Glad I could help :) – Iqbal Ansari Sep 3 '15 at 17:58

@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.

  • 1
    -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 3 '15 at 8: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 xahlee.info/comp/programing_variable_naming.html – Xah Lee Sep 3 '15 at 10:02
  • 2
    -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 3 '15 at 14:06
  • 2
    @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 3 '15 at 15:20
  • 3
    @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 3 '15 at 18:29

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.