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

From: Dmitry Gutov <[email-masked]>
Date: Mon, 15 Jan 2024 22:51:32 +0200

On 15/01/2024 17:27, João Távora wrote:
> On Mon, Jan 15, 2024 at 2:10 AM Dmitry Gutov <[email-masked]> wrote:
>>
> 
>> It's not like we don't have an existing solution for this: if there are
>> two different modes to configure, change the settings for both modes, or
>> alter two hooks. Less magical and more verbose, but being explicit can
>> be good.
> 
> I don't think there's anything magical about a base mode.  But I like your
> solution too.

As "magical", I meant the original patch for this report. I wouldn't 
mind the "base mode" approach, but I guess its still suffers from not 
being suitable for using with earlier Emacs versions.

And every programming mode will have to come with -base-mode defined, 
otherwise we'll have to revisit this every time a new third-party 
-ts-mode appears.

>> Here's a draft patch of how a "language" could work. It doesn't alter
>> every entry, but it is backward compatible.
> 
> I think something like this can work, yes.
> 
> -      (funcall (alist-get mode major-mode-remap-alist mode))
> +      ;; XXX: When there's no mapping for `:<language>', we could also
> +      ;; look for a function called `<language>-mode'.
> +      (funcall (alist-get mode major-mode-remap-alist (if (keywordp mode)
> +                                                          #'fundamental-mode
> +                                                        mode)))
> +      (when (keywordp mode)             ;Perhaps do that unconditionally.
> +        (run-hooks (intern (format "%s-language-hook" (buffer-language)))))
>         (unless (eq mode major-mode)
> 
> Regarding the "XXX", this is basically the same questions in the
> two headings, I think, which is whether to consider the <foo> in existing
> <foo>-mode as language.  I think we can do it yes.  Eglot and other
> packages [*] have been doing it for quite some time.  It will fail very
> rarely, only for major modes outside Emacs (like "tuareg-mode" for
> Ocaml) and we can probably fix that in-tree.

It's a choice between embedding the implicit logic here, or returning 
nil and allowing the callers to do their own fallbacks. I'm not sure, 
personally, which is the better. One might be convenient, but the other 
more strict, possibly leaning to fewer defects.

This choice is coupled with the corresponding logic in 'buffer-language' 
(whether to keep the replace-regexp-in-string branch).

> The only thing that leaves me some doubts is the 'set-buffer-language'
> entry point.  It's a new thing not strictly required.  Normally the
> databases are edited (via whatever means) and then the buffer is
> reverted for a mode change.  So I don't think we need to introduce
> this user convenience just yet (though, like the other user conveniences
> you have imagined, I'm not necessarily opposed to it).

I was thinking of what would be required to make "language" a 
first-class entity, so that users could interact with them instead of 
major modes. To prove the validity of the feature.

Because it's something that people do (I, at least): invoke the major 
mode to choose a different language/content-type for a buffer (one not 
visiting a file yet, perhaps). And if we have an abstraction over 
mmodes, it would make sense to use it.

The interactive behavior of set-buffer-language seems to justify it, I 
think: when you try to enable a major mode, you get all the session's 
functions as completions. But 'M-x set-buffer-language TAB' gives you a 
neat and tidy list of known languages, it's a tangible improvement.

> Also 'buffer-language' could be 'get-mode-language',  so you don't
> have to have an actual buffer handy to get this association.  The
> implementation would just be a reverse search in major-mode-remap-alist

This makes it dependent on the major mode already being applied. Which 
is a valid strategy, but then it can't be used in the last two features 
from my list ("Further possible additions") - they're about the case 
when there is no major mode defined.

> Other than that, I think the solution is workable.
> 
> The other package [*] that does exactly the same thing as Eglot, and
> invented it independently is markdown-mode:
> 
> (defun markdown-get-lang-mode (lang)
>    "Return major mode that should be used for LANG.
> LANG is a string, and the returned major mode is a symbol."
>    (cl-find-if
>     #'markdown--lang-mode-predicate
>     (nconc (list (cdr (assoc lang markdown-code-lang-modes))
>                  (cdr (assoc (downcase lang) markdown-code-lang-modes)))
>            (and (fboundp 'treesit-language-available-p)
>                 (list (and (treesit-language-available-p (intern lang))
>                            (intern (concat lang "-ts-mode")))
>                       (and (treesit-language-available-p (intern
> (downcase lang)))
>                            (intern (concat (downcase lang) "-ts-mode")))))
>            (list
>             (intern (concat lang "-mode"))
>             (intern (concat (downcase lang) "-mode"))))))
> 
> It uses this to know what major-mode to use to fontify GitHub style
> markdown code blocks (which have a little language cookie after the
> three backticks).  Like Eglot's similar code, I think it could be trivially
> rewritten if something like your patch were in place.

Yup, it could use the entries in major-mode-remap-alist.

> Bug#68217 is also relevant here.  Eglot calls into markdown-mode.el
> to fontify LSP documentation snippets and sometimes the mode picked
> by markdown-mode.el to do the fontification is not the same the user
> is using for the buffer.  It most clearly should be.  So
> get-language-for-mode and get-(preferred)-mode-for-language are two
> evidently needed helpers.

Are there specific uses for get-mode-for-language when there is no 
existing buffer? Hmm, I suppose it could be the better option when 
either the current buffer is not visiting a file (but you want to have 
code completion in it anyway), or the user switched to a different major 
mode explicitly, one that does not correspond to buffer-file-name in the 
current configuration.

We could have both functions: buffer-language and get-language-for-mode 
('get-mode-language'?). Or define one initially and add the other as needed.