emacsmajor-mode

Emacs add consistent indent rule to generic-x mode


I wrote a very simple Emacs mode for Standard ML:

;; sml syntax
(require 'generic-x)

(define-generic-mode
    'sml-mode                          ;; name of the mode
  '(("(*" . "*)"))                           ;; comments delimiter
  '("fun" "fn" "let" "val" "datatype" "type" "case" "of" "end" "structure" "struct" "signature" "sig")
  '(("=" . 'font-lock-builtin-face)
    ("|" . 'font-lock-builtin-face)
    (">" . 'font-lock-builtin-face)
    ("<" . 'font-lock-builtin-face)
    ("-" . 'font-lock-builtin-face)
    ("+" . 'font-lock-builtin-face)
    (";" . 'font-lock-builtin-face)
    ("," . 'font-lock-builtin-face)
    ("{" . 'font-lock-builtin-face)
    ("}" . 'font-lock-builtin-face)
    ("(" . 'font-lock-builtin-face)
    (")" . 'font-lock-builtin-face)
    (":" . 'font-lock-builtin-face)
    ("[" . 'font-lock-builtin-face)
    ("]" . 'font-lock-builtin-face))     ;; a built-in
  '("\\.sml$")                    ;; files that trigger this mode
  nil                              ;; any other functions to call
  "SML highlighting mode"     ;; doc string
  )

However, it will not indent consistently. I can't describe exactly how it indents, but it switches inconsistently between tabs and spaces and length of the spaces. The simplest rule I can think of is to always start a new line on the same column and tabbing always takes you to the next column that is a multiple of 4. Tabs should be spaces. How can I do this using the generic mode?

As a note on the mode definition, I am using the builtin-face incorrectly because the operator-face had no coloring. It does look ugly now though.


Solution

  • First things first: I strongly recommend you start with define-derived-mode rather than with define-generic-mode because the former will seamlessly grow to accomodate a fully-featured major mode whereas define-generic-mode will quickly impose limits that are inconvenient to work around.

    E.g. you could rewrite your code as:

    (defvar sml-mode-syntax-table
      (let ((st (make-syntax-table)))
        ;; Make (*...*) a comment.
        (modify-syntax-entry ?\( "()1" st)
        (modify-syntax-entry ?\) ")(4" st)
        (modify-syntax-entry ?\* ". 23n" st)
        st))
    
    (defvar sml-font-lock-keywords
      `((,(concat "\\_<" 
                  (regexp-opt '("fun" "fn" "let" "val" "datatype" "type" "case" "of" "end" "structure" "struct" "signature" "sig"))
                  "\\_>")
         (0 font-lock-keyword-face))
        ("[][=|><-+;,{}():]" (0 font-lock-builtin-face))))
    
    ;;;###autoload
    (define-derived-mode sml-mode prog-mode "SML"
      "SML major mode."
      (set (make-local-variable 'comment-start) "(* ")
      (set (make-local-variable 'comment-end) " *)")
      (set (make-local-variable 'font-lock-defaults)
           '(sml-font-lock-keywords)))
    
    ;;;###autoload
    (add-to-list 'auto-mode-alist '("\\.sml\\'" . sml-mode))
    

    W.r.t TABs and SPCs, "switching between them" is the default Emacs behavior (the attitude being that TAB is just an optimisation which we use when it's applicable). If you don't like it, then put (setq-default indent-tabs-mode nil) in your ~/.emacs and not in your major mode's definition since that is a personal choice unrelated to SML (which does not distinguish TABs and SPCs, contrary to, say, Haskell).

    As for the indentation you suggest, you could start by adding (set (make-local-variable 'indent-line-function) #'indent-relative) which should make sure that by default indentation is just the same as the previous line; and for the "TAB should advance by 4 columns" maybe something like (set (make-local-variable 'tab-stop-list '(4 8 12 16 20 24 28 32 36 40 44 48 52 56 60 64)) would do the trick (in more recent Emacsen, '(4 8) is sufficient because Emacs finally learned to "auto-extend the list".

    But I'm curious: why not just use the existing sml-mode that's in GNU ELPA?