Lem
GitHubDiscord Matrix Toggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

Configuration

Lem loads ~/.lem/init.lisp when starting up.

You probably want to start it with

(in-package :lem-user)

Otherwise, be sure to refer to Lem functions with the lem: prefix.

Show the completion list instantly

The completion pop-up, like the one we get with M-x, wants us to press TAB before we get the completion candidates. If you want to open it right away, add this to your init file:

(add-hook *prompt-after-activate-hook*
          (lambda ()
            (call-command 'lem/prompt-window::prompt-completion nil)))

(add-hook *prompt-deactivate-hook*
          (lambda ()
            (lem/completion-mode:completion-end)))

You can start typing and the list will narrow down.

Choosing the completion pop-up position: center, top, bottom

Lem’s completion pop-up is by default centered on the middle of the screen.

Since May of 2024, it is possible to choose its position: à la Emacs at the bottom? À la VSCode at the top?

If you want the completion pop-up at the bottom like this:

Lem's completion pop-up at the window bottom.

Add this to your init file:

(setf lem-core::*default-prompt-gravity* :bottom-display)
(setf lem/prompt-window::*prompt-completion-window-gravity* :horizontally-above-window)
(setf lem/prompt-window::*fill-width* t)

If you want it at the top like this:

Lem's completion pop-up at the window top.

Add this to your init file:

(setf lem-core::*default-prompt-gravity* :top-display)
(setf lem/prompt-window::*fill-width* t)

There is more. You can allow the order of completions to be reversed: this is helpful when the prompt is at the bottom of the screen, so that the firts completion candidate is just above the prompt, so you don’t have to move your eyes. Use this so that completions are reversed in regular prompts, but normal in code completion::

(add-hook *prompt-after-activate-hook*
          (lambda ()
            (setf lem/completion-mode::*completion-reverse* t)
            (call-command 'lem/prompt-window::prompt-completion nil)))

(add-hook *prompt-deactivate-hook*
          (lambda ()
            (setf lem/completion-mode::*completion-reverse* nil)
            (lem/completion-mode:completion-end)))

Binding keys

We will learn how to bind your own keys to commands. Afterwards we’ll learn how to write your own interactive commands.

define-key

Use the define-key function to associate a keybinding to a command:

(lem:define-key lem:*global-keymap*
    "C-x F" 'lem-core/commands/file:find-file-recursively)

Its first argument is a keymap. Essentially, there is a keymap for global keybindings, for each programming language mode (such as lem-python-mode::*python-mode-keymap*, you can find it in Lem with the autocompletion), and for every other tool: there is a keymap for grep mode, for the directory mode, for the different vi mode states, etc.

define-keys

You can also use lem:define-keys to define more than one key at once:

(define-keys *global-keymap*
  ("C-a b" 'describe-bindings)
  ("C-a k" 'describe-key)
  ("C-a a" 'lem-lisp-mode:lisp-apropos)
  ("C-a c" 'apropos-command)
  ("C-a p" 'lem-lisp-mode:lisp-apropos-package)
  ("C-a f" 'lem-lisp-mode:lisp-describe-symbol))

But wait, there is more.

Defining a keymap

Did you notice that all the above commands have the C-a prefix? We can refactor this.

First, we define a keymap object. Let’s take another example, the frame-multiplexer (aka tabs):

(defvar *keymap*
  (make-keymap :name '*frame-multiplexer-keymap*)
  "Keymap for commands related to the frame-multiplexer.")

Now we can use (define-key *keymap* key command) (singular), like this:

(define-key *keymap* "c" 'frame-multiplexer-create-with-new-buffer-list)
(define-key *keymap* "d" 'frame-multiplexer-delete)
(define-key *keymap* "p" 'frame-multiplexer-prev)
(define-key *keymap* "n" 'frame-multiplexer-next)
(define-key *keymap* "r" 'frame-mulitplexer-rename)

And now we can define the prefix key for our keymap and all its sub-keys commands:

(define-key *global-keymap* "C-z" *keymap*)

As a result, we defined C-z c for 'frame-mulitplexer-create-with-new-buffer-list.

Writing one’s own commands

Use define-command:

(in-package :lem)
(define-command open-init-file () ()
    ;; @sasanidas
    (lem:find-file
        (merge-pathnames "init.lisp" (lem-home))))

or copy-paste this with M-:

(lem:define-command open-init-file () ()
    (lem:find-file
        (merge-pathnames "init.lisp" (lem:lem-home))))

You can now call it with M-x open-init-file.

Commands arguments

How can we give an argument to a command, how can we run it on a region?

The define-command examples above have two empty () (). This is the place for parameters names and special argument descriptors.

We can tell it to accept a prefix argument (a number), we can make the command ask for user input (a string, an integer, a buffer, a file) and work on region delimiters.

Universal prefix argument

We tell the following command that it accepts a prefix argument. We use :universal and n is the argument name to use in the command body. n defaults to 1.

(define-command hellos (n) (:universal)
  (dotimes (i n)
    (insert-string (current-point) "hello ")))

Call this command M-x hellos and it writes 1 “hello”.

Call it with C-u 3 M-x hellos and it writes “hello” 3 times.

Optional arguments and default values

We can also use (&optional n) (:universal-nil) or (&optional (n 3)) (:universal-nil) to change the default value.

:universal-nil is similar to :universal but doesn’t default to 1.

Ask for a number

Use :number. This prompts the user for a number.

Ask for a string

Use :string for the arg-descriptor, and (:string "My prompt: ") to give a custom prompt.

This is how a directory-mode is done:

(define-command simple-message (s) ((:string "Enter a string: "))
  (message s))

Run it with M-x simple-message.

Ask for a buffer

Use :buffer. It defaults to the current buffer’s name.

There is also :other-buffer that defaults to the other buffer’s name.

Ask for a file

Use :file to prompt for a file. It defaults to the current buffer’s directory.

There is also :new-file that defaults to the buffer’s directory and must not be existing.

Run on region

Next, how can we run a command on a region? A region is defined with a start and end point. We have to tell the command it operates on a region, that way it gives us the two points.

We do so by declaring two start and end arguments, and a special argument descriptor: :region. Here’s how it is done for the python-eval-region command:

(define-command python-eval-region (start end) (:region)
  (unless (alive-process-p)
    (editor-error "Python process doesn't exist."))
  (lem-process:process-send-input *process* (points-to-string start end)))

Multiple arguments

We can have multiple arguments, for example:

(define-command multiple-args (regex &optional arg) ((:string "Enter a regex:") :universal-nil)
  ...)

Formatting code

Lem uses external code formatters that can be run automatically when you save a file.

Currently formatters available by default are:

  • gofmt for go-mode
  • clang-format for c-mode
  • prettier for js-mode and json-mode
  • and it works for lisp code of course.

Additionally, rust-mode uses rustfmt.

To use formatting, you can either manually invoke M-x format-current-buffer, or enable auto-formatting like this:

(setf lem:*auto-format* t)

Formatters can be defined for major modes, they are implemented as a function that accepts a buffer as an argument. Some are built-in, but you can make your own in two ways:

  1. register-formatter

You can register a formatter for an existing major mode like this:

(lem:register-formatter my-mode #'my-formatter)
  1. define-major-mode

If you are creating your own major mode, you can add a formatter in the mode definition:

(define-major-mode my-mode ()
    (:name "My mode"
     :keymap *my-mode-keymap*
     :formatter #'my-formatter)
  )

Hooks

Hooks allow you to run abritrary code before (or after) a command is called or a given mode is entered (or exited).

Some examples:

  • after-save-hook
  • kill-buffer-hook
  • *find-file-hook*
  • *exit-editor-hook*
  • *after-init-hook*
  • self-insert-hook: run code before or after a character is inserted in the buffer.
  • and more: checkout each mode (programming modes, vi mode…) inside Lem with the completion.

The hooks mechanism is defined in src/common/hooks.lisp. It is made of the available functions and macros:

  • add-hook place callback: add a hook

    • example: (add-hook *after-init-hook* #'display-welcome)
  • remove-hook place callback: remove a hook

  • run-hooks list: run all hooks.

Each mode defines a hook variable name. For example:

(define-major-mode c-mode language-mode
    (:name "C"
     :keymap *c-mode-keymap*
     :syntax-table *c-syntax-table*
     :mode-hook *c-mode-hook*       ;; <---------- defines our hook name
     :formatter #'lem-c-mode/format:clang-format)
  (setf (variable-value 'enable-syntax-highlight) t)
  ;; more settings here…
  )

(add-hook *c-mode-hook* 'guess-offset)  ;; <------ adds a hook

Vi mode

Start Lem in vi-mode:

;; Start in vi-mode
(lem-vi-mode:vi-mode)

We can use a hook to be in vi insert mode when we start the Lisp REPL:

;; Start the Lisp REPL in vi insert mode.
(add-hook lem-lisp-mode:*lisp-repl-mode-hook* 'lem-vi-mode/commands:vi-insert)

Site init

The site init configuration allows the user to arrange different libraries/files, the main two important directories to organize the files are:

  • *inits-directory-name* ("~/.lem/lisp"):

    This directory is intended to have libraries (asdf packages) that are going to be loaded before our configuration files.

  • "~/.lem/inits":

    Here is the place we can put our configuration files (which are .lisp files not asdf systems).

It’s usually recommended to make sure that the files are specify in the .asd file that is created at the root of the (lem-home) directory, usually "~/.lem", the file is lem-site-init.asd, an example maybe:

;; don't edit !!! (you can ignore this warning)
(asdf/parse-defsystem:defsystem "lem-site-init"
  :depends-on
  (:system-on-lisp-directory)
  :components
  ((:file "inits/my-file-one") (:file "inits/my-file-two")))

Then, in the main init file "init.lisp", you can load the systems + files with the function:

(lem-core:load-site-init)

Example users’ init files

For inspiration, see those configuration files: