Subject: bug#68246: 30.0.50; Add non-TS mode as extra parent of TS modes

From: João Távora <[email-masked]>
Date: Wed, 17 Jan 2024 10:20:34 +0000

On Wed, Jan 17, 2024 at 2:41 AM Dmitry Gutov <[email-masked]> wrote:

> An "abstract" mode is supposedly one that doesn't do anything. So it
> doesn't have to be callable.

No.  Not "abstract" as in "Java interface", abstract as in "Java
abstract class".

> Anyway, that's +1 feature required for the implementation.

Almost trivial feature.  See patch after sig.  It'll make this work:

(define-derived-mode foo-mode prog-mode "Fooey" :abstract t
   (message "Hey from foo-mode"))
(define-derived-mode bar-mode foo-mode "Barey"
   (message "Hey from bar-mode"))

'foo-mode' can't be called, but 'bar-mode can, of course.  And it
will call its parent.

> > What if we filter by prog-mode?  It would leave the ':ruby-base' and
> > ':python-base' as false positives, I guess.  But then we could reasonably
> > say that anything ending with '-base' is abstract (or use the
> > aforementioned  explicit abstract prop).
>
> We would also filter out :css, for example.

Sure?  I see a super-normal css-base-mode inheriting from
prog-mode.

> TeX modes also do not derive
> from prog-mode. TeX does have an LSP server, however.

At the end of the day we have to come to a conclusion of what
we want to do.  I want to find major modes that correspond
to languages, right?  So:

* these outliers start inheriting from prog-mode
* we inject new lang-mode between prog-mode and fundamental-mode and make
  outliers derive from that.
* we say these outliers aren't languages
* we code exceptions for these outliers

It could even be that

(derived-mode-add-parents 'tex-mode '(prog-mode))

is exactly what's needed here, showcasing how I think this
particular heavy hammer should be used for the exception, not
the rule.

> > Yes.  I don't see the full scan of m-m-remap-alist as problematic
> > from a effiency perspective.  If we decide it's the database, it's
> > the database.   It's unfortunate that the "alist" implementation is
> > hardcoded in the name (which is why I would prefer a (:language "Foo")
> > kwarg to define-derived-mode) but we can abstract the alist aspect
> > away with accessors and do the usual "Do not change this variable
> > directly, use these accessors instead".
>
> I'm not saying in advance that it will be slow. Just that it's a
> different function.

Ah.  Right.  And I think it's a good one.  Eglot needs it, and so does
Yasnippet, probably.

> >> BTW, get-current-mode-for-language could be implemented in terms of
> >> set-buffer-language.
> >
> > What does get-current-mode-for-language do exactly?
>
> The major-mode currently configured to be used for the language (through
> m-m-a-alist, in the current proposal). set-auto-mode will choose it.

Perfect.  But the, "set-buffer-language" comment?  Does a buffer object
have to exist for that job to be done?

> > Right. So maybe
> >
> > (or (with-current-buffer buffer (get-language-for-mode major-mode))
> >      (let (kw)
> >         (and buffer-file-name
> >              (keywordp (setq kw (alist-get buffer-file-name auto-mode-alist)))
> >              kw))
> >      (consult-oracles)
> >      (error "Don't know what language this is, sorry"))
>
> Replied to this one in another email: referring to the results of the
> computation of set-auto-mode is easier. But that's a technical detail.

For sure, reuse as much code as possible.  I was just
illustrating the intended fallback logic.

João

diff --git a/lisp/emacs-lisp/derived.el b/lisp/emacs-lisp/derived.el
index dec5883767d..a9b67965416 100644
--- a/lisp/emacs-lisp/derived.el
+++ b/lisp/emacs-lisp/derived.el
@@ -143,6 +143,9 @@ define-derived-mode
            :interactive BOOLEAN
                    Whether the derived mode should be `interactive' or not.
                    The default is t.
+           :abstract BOOLEAN
+                   You'll never be able to use the CHILD mode directly
+                   in a buffer, just use as a PARENT for other modes.

 BODY:      forms to execute just before running the
            hooks for the new mode.  Do not use `interactive' here.
@@ -192,7 +195,9 @@ define-derived-mode
  (hook (derived-mode-hook-name child))
  (group nil)
         (interactive t)
-        (after-hook nil))
+        (abstract nil)
+        (after-hook nil)
+        (function-name child))

     ;; Process the keyword args.
     (while (keywordp (car body))
@@ -201,9 +206,13 @@ define-derived-mode
  (:abbrev-table (setq abbrev (pop body)) (setq declare-abbrev nil))
  (:syntax-table (setq syntax (pop body)) (setq declare-syntax nil))
         (:after-hook (setq after-hook (pop body)))
+        (:abstract (setq abstract (pop body)))
         (:interactive (setq interactive (pop body)))
  (_ (pop body))))

+    (when abstract
+      (setq function-name (gensym "--internal-"))
+      (put child 'abstract-mode function-name))
     (setq docstring (derived-mode-make-docstring
       parent child docstring syntax abbrev))

@@ -245,13 +254,12 @@ define-derived-mode
          (put ',child 'derived-mode-parent ',parent))
        ,(if group `(put ',child 'custom-mode-group ,group))

-       (defun ,child ()
+       (defun ,function-name ()
  ,docstring
  ,(and interactive '(interactive))
  ; Run the parent.
  (delay-mode-hooks
-
-   (,(or parent 'kill-all-local-variables))
+   (,(or (get parent 'abstract-mode) parent 'kill-all-local-variables))
  ; Identify the child mode.
    (setq major-mode (quote ,child))
    (setq mode-name ,name)