Rhombulary is a “Rhombus vocabulary” library for Emacs that implements simple table lookup based content completion. Completion is context insensitive, drawing from a predefined collection of words (typically names), with Company and CAPF (completion-at-point-functions) as supported completion methods. The implementation relies on lookup tables generated ahead of time based on documentation indexes built by Racket’s Scribble documentation tool for a local installation of Racket. Emacs buffer or mode local choice of word tables is supported to allow for customization of word selection and definition ordering for different languages, as Scribble is used for documenting not only Racket language APIs, but also those of Rhombus, Zuo, and others. Rhombulary is likely to be a better fit for languages in which it is customary to use longer, mostly unambiguous and unqualified names, and languages like Scheme in the Lisp-1 tradition ought to be better than most in that respect.
The specific motivations for creating Rhombulary were to:
- automate dictionary (re)generation and (re)loading for Emacs (as compared to Ractionary, requiring makefile invocations from the command line);
- support dictionary configuration with the Emacs
defcustomfacility; and - allow Zuo content assist with Zuo names only (since Zuo runs in a different VM, and cannot use all of the Racket libraries).
Rhombulary can be installed with a package manager that is able to install from a Git repository. The library is split into two different packages (rhombulary and rhombulary-company) to avoid the dependency on Company when it would go unused.
For straight.el package manager the recipes for the two packages are
(straight-use-package
'(rhombulary
:type git :host github :repo "hasu/rhombulary"
:files ("rhombulary*.el" "*.rkt")))
(straight-use-package
'(rhombulary-company
:type git :host github :repo "hasu/rhombulary"
:files ("company/rhombulary-*.el")))and for other managers the recipes should be similar.
The package comes with its own Scribble-based documentation for the Racket API, and in order to build that documentation the library may also be installed as a Racket package. This can be done with raco, by pointing it to the “rhombulary” source directory with the package “info.rkt” file:
raco pkg install --link --name rhombulary <directory>In order to make use of the package(s) to do content completion some configuration is required. Notably, completion support should be configured for Emacs modes as required, either using the Company or CAPF functions.
It may also be useful to define a keybinding for at least the rhombulary-reload command, to be able to conveniently trigger the regeneration of the completion dictionary if it is out of date. It may also be a good idea to execute that command interactively at least once after Rhombulary installation to see if there are any errors in dictionary generation. This may reveal the need to do further configuration, such as setting the rhombulary-racket-program to name the Racket executable to use to run the generator, which then also determines the Racket installation whose scribbled documentation is to be used as input.
While Rhombulary is primarily intended for completion, it does define some commands that are for other kinds of actions. They include:
rhombulary-doc-symbol-at-point- Open documentation for the symbol at point.
rhombulary-doc-chosen-symbol- Open documentation for a symbol selected from a choice list.
rhombulary-doc-search- Open a documentation search with the specified term.
rhombulary-reload- Regenerate and reload the dictionary, which can be appropriate for example after installing or uninstalling software packages.
As the commands can sometimes be useful in almost any Emacs major mode it may make sense to bind them globally, and not just for specific language modes. For example, one might want to look up a Racket name discussed in an Org mode buffer, and having a keybinding for rhombulary-doc-symbol-at-point could then prove convenient. It is worth remembering, however, that dictionary configurations can still be mode sensitive, and therefore symbol lookups may not necessarily give the same results across modes.
(defvar rhombulary-map (make-sparse-keymap) "Keymap for Rhombulary commands.")
(fset 'rhombulary-map rhombulary-map)
(define-key global-map (kbd "<f6>") 'rhombulary-map) ;; choose a free key combination here
(define-key rhombulary-map (kbd "i") #'rhombulary-doc-chosen-symbol)
(define-key rhombulary-map (kbd "l") #'rhombulary-reload)
(define-key rhombulary-map (kbd "p") #'rhombulary-doc-symbol-at-point)
(define-key rhombulary-map (kbd "s") #'rhombulary-doc-search)The rhombulary-company feature supplies different Rhombulary backends for Company. None of them should be enabled globally, since they mostly will only make sense for languages with relevant scribbled documentation. All of the backend functions are autoloadable, and so after installation of the feature they can readily be listed among the company-backends for a given mode.
(setq-local company-backends
'(rhombulary-company
company-capf
company-dabbrev-code))The rhombulary-company provided backend functions are:
rhombulary-company- The “regular” backend, which should be well suited for Racket and Zuo and other S-expression based “Rackety” languages.
rhombulary-company-at-exp- Intended for use in a Scribble mode, for example, but could perhaps be a useful addition for doing completion for
at-exp racketalso, as it specifically supports “@”-prefixed symbols, even if modes that do not treat “@” as a symbol character. rhombulary-company-dotted- A backend that allows “.” characters to appear within strings to be prefix matched against dictionary entries, making this perhaps less bad of a fit for Rhombus than the regular backend, even if a more context sensitive solution would really be preferable.
rhombulary-company-all- A backend that shows all definitions for a name, and not only the “best” one, along with annotations to indicate the source of each definition.
For displaying hover help Company quickhelp is supported, and all the backends respond to the quickhelp-string command.
The rhombulary-capf feature provides rhombulary-capf and rhombulary-capf-at-exp as a more limited selection of functions that may be listed among the completion-at-point-functions for a given mode. The correspond to the first two Company backend functions described above, and again, they should not be enabled globally, as they only tend to make sense for languages with relevant scribbled documentation.
(setq-local completion-at-point-functions '(rhombulary-capf-at-exp rhombulary-capf))Rhombulary supports content completion with multiple different dictionary configurations to allow for dictionary optimization for different Emacs modes, for editing different languages. This configurability is based on rhombulary-configuration-name and related customization variables (see rhombulary-configuration-name documentation for a list of them), which become buffer local when set, and whose values are passed to the dictionary generator when it is invoked. These variables are intended to be set as appropriate in a mode hook.
For those that prefer UI-based configuration the rhombulary-config feature includes pre-defined rhombulary-configure/racket, rhombulary-configure/zuo, and rhombulary-configure/rhombus functions for applying configurations for the respective languages. Each of the functions comes with a corresponding customization group for defining the configuration to apply, and so for example for modifying the Racket configuration one can call
(customize-group 'rhombulary-racket)(defun my-racket-mode-hook ()
(rhombulary-configure/racket)
(setq-local company-backends
'((rhombulary-company company-dabbrev-code)
company-capf
company-files))
(company-mode 1)
(company-quickhelp-local-mode 1))
(add-hook 'racket-mode-hook #'my-racket-mode-hook)As Zuo programs do not run in the Racket execution environment they are not interoperable with Racket APIs in the same way as most #lang languages are, and therefore it may make sense to restrict completions to only Zuo definitions, and rhombulary-configure/zuo does that in its default configuration.
(defun my-zuo-mode-hook ()
(rhombulary-configure/zuo)
(setq-local company-backends
'((rhombulary-company company-dabbrev-code)
company-files))
(company-mode 1)
(company-quickhelp-local-mode 1))
(add-hook 'zuo-mode-hook #'my-zuo-mode-hook)If using a Rhombus mode that does not treat “.” as a symbol character, then it may be useful to use the rhombulary-company-dotted backend to allow the completion prefix to contain “.” characters.
(defun my-rhombus-mode-hook ()
(rhombulary-configure/rhombus)
(setq-local company-backends
'((rhombulary-company-dotted company-dabbrev-code)
company-files))
(company-mode 1)
(company-quickhelp-local-mode 1))
(add-hook 'rhombus-mode-hook #'my-rhombus-mode-hook)For editing Scribble we likely want both S-expression and @-expression completion support, and if we use the Racket API for document authoring we likely want to prefer Racket APIs, and probably ones from the scribble collection over most others:
(defun my-scribble-mode-hook ()
(setq-local company-backends
'(rhombulary-company-at-exp
(company-dabbrev
company-abbrev
:with
rhombulary-company)
company-files))
(setq rhombulary-configuration-name "scribble"
rhombulary-ranked-language-families '("Racket" "*")
rhombulary-ranked-modules '("'#%kernel" "racket/base" "racket" "scribble" "pict")
rhombulary-language-family "Racket")
(company-mode 1)
(company-quickhelp-local-mode 1))
(add-hook 'scribble-mode-hook #'my-scribble-mode-hook)Racket 9.1 and later support the concept of language families, and Rhombulary can also make use of knowledge about language families when it is available, for two different purposes:
- when ordering dictionary entries during generation, by honoring the effective
rhombulary-ranked-language-familiessetting when ranking definitions of the same name - when opening documentation for browsing, by honoring the effective
rhombulary-language-familyandrhombulary-language-family-rootsettings when setting anyfamandfamrootquery parameters
The default family settings do not specify any preference for any family, but the relevant variables can be set as desired, probably buffer locally. For example:
;; Have the generator prefer Racket APIs.
(setq rhombulary-configuration-name "example"
rhombulary-ranked-language-families '("Racket" "*"))
;; Navigate documentation as Racket, but use Rhombus top page.
(setq rhombulary-language-family "Racket"
rhombulary-language-family-root "rhombus")Where support for language families is not available one can still restrict the dictionary entry selection based on module paths. For example, the rhombulary-config default configuration for Zuo assumes the existence of a “Zuo” language family (by listing only it in rhombulary-ranked-language-families), but that will not work as desired if the family is not known to the Racket installation. If we nonetheless only want to include Zuo libraries in the dictionary we can resort to instead listing the module paths (or path patterns) that we wish to include, and set the rhombulary-ranked-as-included-modules flag to indicate that we want to exclude all others:
(defun my-zuo-mode-hook ()
(setq rhombulary-configuration-name "zuo"
rhombulary-ranked-modules '("zuo-doc/fake-kernel" "zuo-doc/fake-zuo" "zuo-doc/fake-zuo-hygienic" "zuo-doc" "zuo" "**/zuo")
rhombulary-ranked-as-included-modules t))Some Rhombulary commands involve presenting the user with choices, and that is done via the minibuffer with either completing-read or Ido, depending on the kind of input. This behavior is configurable via the rhombulary-emacs feature, making it possible to use alternative completing minibuffer readers, either picking one for use throughout (by setting rhombulary-compread-function) or by picking different readers for different use cases (by setting rhombulary-specific-compread-functions).
One might for example elect to use Ivy in some cases and Ido in others:
(setq rhombulary-compread-function 'ivy-read
rhombulary-specific-compread-functions
'((source-module ido-completing-read))
rhombulary-compread-function-adapters
'((ido-completing-read rhombulary-compread-ido-adapt)
(ivy-read rhombulary-compread-ivy-adapt)))Rhombulary includes commands for opening Scribble generated documentation for each dictionary entry originating from such documentation, and the way that the browsing is initiated is controlled by the rhombulary-browse-url-function configuration variable. The default is to use eww-browse-url in some cases and browse-url in others, but we could also for example simply opt to always use browse-url if we do not like EWW:
(setq rhombulary-browse-url-function 'browse-url)The protocol of calling my-rhombulary-racket-describe is actually richer than for browse-url in general, making it possible to implement more specialized actions, of which my-rhombulary-racket-describe (see below) is an example.
Racket Mode has settings and functions that may be useful with Rhombulary also.
For convenience, rhombulary-racket-program is by default set to the same value as Racket Mode’s racket-program, but this only happens if racket-program has already been defined at the time.
In racket-hash-lang-mode the documentation language family of the code being edited may get determined in a content sensitive way, and one might want to configure Rhombulary to get the family from the mode when applicable. This can be done by setting rhombulary-language-family-function to a custom function that tries to get the value by calling racket--hash-lang-doc-family.
(declare-function racket--hash-lang-doc-family "racket-hash-lang")
(defun my-rhombulary-language-family ()
"Get language family for current buffer.
Possibly get it from `racket-hash-lang-mode'."
(or (and (fboundp 'racket--hash-lang-doc-family)
(racket--hash-lang-doc-family))
rhombulary-language-family))
(setq rhombulary-language-family-function 'my-rhombulary-language-family)In the above example we configure the documentation language family to any that might be determined by racket--hash-lang-doc-family, falling back to any value set to the rhombulary-language-family variable.
Racket Mode includes a racket-describe documentation browser, which can be attractive for viewing Rackety documentation within Emacs. In order to have Rhombulary use it one can configure rhombulary-browse-url-function to something that loads documentation into a racket-describe-mode buffer. For example:
(autoload 'racket--describe-path+anchor "racket-describe")
(defun my-rhombulary-racket-describe (url)
"Browse URL in `racket-describe-mode'.
Possibly fall back to `rhombulary-browse-url-maybe-eww'."
(if-let* ((path (plist-get rhombulary-browse-url-params :path)))
(let ((anchor (plist-get rhombulary-browse-url-params :anchor)))
(racket--describe-path+anchor path anchor))
(rhombulary-browse-url-maybe-eww url)))
(setq rhombulary-browse-url-function 'my-rhombulary-racket-describe)The Rhombulary default for opening documentation is to use rhombulary-browse-url-maybe-eww (using either browse-url or eww-browse-url), but in the above example we only that function as a fallback in cases where a direct path to documentation could not be resolved, meaning that we don’t have the arguments required to call racket--describe-path+anchor.
There is likely to be significant duplication in completions if there are completion candidates from both Racket Mode and Rhombulary. One way to address this is to clear the racket--completion-candidates in order to prevent the inclusion of the collection of Racket built-in names that Racket Mode is preconfigured to use:
(eval-after-load 'racket-edit (lambda () (setq racket--completion-candidates nil)))An alternative solution would be to remove racket-complete-at-point from completion-at-point-functions.
Neither of the above workarounds is required when completion is not configured to use those functions, as might be the case for example if using Company without the company-capf backend.
Rhombulary is open source software. Its Emacs Lisp code is made available under GPL version 3 or later, while its Racket code is available under the MIT license, except where otherwise noted in the source files themselves.
- screenshots
- https://tero.hasu.is/rhombulary/screenshots/
- Racket API documentation
- https://tero.hasu.is/rhombulary/api-doc/