brownts / ada-ts-mode

Ada major mode using tree-sitter for Emacs
GNU General Public License v3.0
4 stars 1 forks source link

Using lsp-mode and ALS for indentation #1

Open flexibeast opened 2 months ago

flexibeast commented 2 months ago

Firstly, thanks for ada-ts-mode!

In this message to the ada-mode-users list, you wrote:

I believe the only thing missing from ada-ts-mode (at least for me) is indentation support (which I have a significant portion of this implemented, just not publicly available yet as I wanted to have a complete implementation before releasing it). If desired, you can use the Ada Language Server indentation support. I was hesitant about adding indentation support for ada-ts-mode because I figured the LSP indentation might be sufficient. However, I finally decided I was going to implement it, just so that it is available without needing a Language Server.

ada-ts-mode providing indentation support would be great! But in the meantime, i'm trying to work out how i could use ALS for indentation within ada-ts-mode.

i'm really not familiar with using LSP servers in Emacs, either with lsp-mode or with eglot, but i do already have lsp-mode installed, and have installed ALS via that. However, i can't work out how to configure my system such that i'm using ada-ts-mode overall, but ALS for indentation. Could you point me in the right direction?

brownts commented 2 months ago

Hi @flexibeast, thanks for your interest in ada-ts-mode, I'm glad you find it useful.

I know I had experimented with the ALS indentation a while back (maybe in Eglot ... I don't remember now), but haven't been actively using ALS for indentation, so it gave me an opportunity to look into it again. While looking into it for lsp-mode, I discovered that lsp-mode was not happy when trying to trigger the indentation due to the ada-ts-mode-indent-offset variable not being defined. When I added ada-ts-mode support to lsp-mode, I had added that indentation variable in there with the expectation that I would soon add in the indentation support to ada-ts-mode, but that hasn't happened yet...although I'm still working on it when I get a chance. I'll update the package this weekend to add that variable in, however in the meantime, you can just define it yourself to configure the indentation offset.

(setq ada-ts-mode-indent-offset 3)

Once that is set, you should be able to use the lsp-mode/ALS indentation. lsp-mode enables indentation by default (i.e., lsp-enable-indentation is t) and ALS enables it by default as well. lsp-mode provides interfaces to indent the buffer (lsp-format-buffer) and region (lsp-format-region). Both of these commands are available in the lsp-command-map (if you have that configured), otherwise you can access them with M-x. That would probably be a good first step to see if that is working for you. If not, something else is probably not properly configured in your setup. lsp-mode has lots of knobs to configure things, so I'm still working through exactly all of the settings I prefer.

If you want indentation to trigger on each line, you'll need to hook into indent-line-function to have it interface with lsp-mode/ALS to trigger the indentation. I experimented with putting together a function that can be used to perform this (see init.el/lsp-format-line below). In doing so, I noticed that ALS will not perform indentation with invalid syntax and also won't indent on empty lines. In cases where the LS won't indent I have it fallback to indenting to the same level as the previous line. This means the indentation won't be great while you have syntax errors, but as soon as the syntax errors are gone, it should update the indentation. This is easily seen on compound statements, like case statements, if statements, etc. If you can insert a snippet or template that provides a skeleton for those (so it remains in a valid syntax state), the indentation will work better.

The following should be a good start at having line indentation. I've included most of my lsp-mode configuration here in case it helps (although this doesn't include my lsp-ui config).

(use-package lsp-mode
  :ensure t
  :defer t
  :init
  (progn
    (setq gc-cons-threshold 100000000)            ; 100MB
    (setq read-process-output-max (* 1024 1024))) ; 1MB
  :preface
  (defun init.el/lsp-format-line ()
    ;; Ada Language Server won't indent an empty line so we use an
    ;; indent relative to the previous non-blank line.  Also the Ada
    ;; Language Server will return an error if attemping to indent when
    ;; syntax errors exist: "Syntactically incorrect code can't be
    ;; formatted".  Fallback to indenting relative to the previous line
    ;; when this happens too.
    (if (string-match (rx bos (zero-or-more space) eos)
                      (buffer-substring (line-beginning-position)
                                        (line-end-position)))
        (progn
          ;; Remove leading spaces and reindent
          (delete-horizontal-space)
          (indent-relative))
      (condition-case err
          (lsp-format-region (line-beginning-position)
                             (line-end-position))
        (error
         (save-excursion
           ;; Remove leading spaces and reindent
           (beginning-of-line 1)
           (delete-horizontal-space)
           (indent-relative))))))

  (defun init.el/hack-local-variables-lsp ()
    "Delayed LSP initialization for certain major modes."
    (when (and (derived-mode-p 'ada-mode 'ada-ts-mode)
               ;; Don't enable LSP in test files
               (not (string-suffix-p ".erts" (buffer-file-name) 'ignore-case)))
      (lsp)
      ;; Set desired indentation offset
      (setq-local ada-ts-mode-indent-offset 3)
      ;; Use line indentation via LSP
      (setq-local indent-line-function #'init.el/lsp-format-line)
      ;; Convenient characters to trigger indentation, besides RET
      (setq-local electric-indent-chars (append ";>," electric-indent-chars))))
  :custom ((lsp-auto-guess-root t)
           (lsp-keymap-prefix "C-c l")
           ;; (lsp-log-io t) ;; Toggle as needed: C-c l T L
           (lsp-headerline-breadcrumb-enable nil) ;; Toggle as needed: C-c l T b
           (lsp-headerline-breadcrumb-enable-diagnostics nil)
           (lsp-semantic-tokens-enable t)
           (lsp-eldoc-enable-hover nil) ; using `lsp-ui-doc-include-signature'
           (lsp-enable-imenu nil)) ; prefer mode's own imenu instead of lsp
  :custom-face (lsp-face-semhl-number
                ,(if (facep 'font-lock-number-face)
                     '((t (:inherit font-lock-number-face)))
                   '((t (:inherit font-lock-constant-face)))))
  :hook ((lsp-mode . lsp-enable-which-key-integration)
         (hack-local-variables . init.el/hack-local-variables-lsp)
         (c-ts-mode . lsp-deferred)
         (gpr-ts-mode . lsp-deferred))
  :commands lsp)

Once you have this part working, you'll likely want to configure exactly how ALS formats the source code. Under the hood, ALS is using the gnat pretty printer engine. The configuration for that is read by the language server from the GPR file. If you have a "Pretty_Printer" package in your GPR project, you can configure it how you like. The following is a very simple configuration I used while testing this out:

package Pretty_Printer is
   for Default_Switches ("Ada") use ("--source-line-breaks");
end Pretty_Printer;

Let me know if you run into any trouble and I should be able to help you through it.

flexibeast commented 2 months ago

Thanks for your reply!

i'm an old bat, and have been using Emacs since around 1997, so my ~4000 line Org-based Emacs config doesn't make use of use-package (and changing this seems like it would be a lot of work for relatively little gain). For those who might be in a similar boat, here's how i adapted the above ELisp:

(defun brownts/lsp-format-line ()
  ;; Ada Language Server won't indent an empty line so we use an
  ;; indent relative to the previous non-blank line.  Also the Ada
  ;; Language Server will return an error if attemping to indent when
  ;; syntax errors exist: "Syntactically incorrect code can't be
  ;; formatted".  Fallback to indenting relative to the previous line
  ;; when this happens too.
  (if (string-match (rx bos (zero-or-more space) eos)
                    (buffer-substring (line-beginning-position)
                                      (line-end-position)))
      (progn
        ;; Remove leading spaces and reindent
        (delete-horizontal-space)
        (indent-relative))
    (condition-case err
        (lsp-format-region (line-beginning-position)
                           (line-end-position))
      (error
       (save-excursion
         ;; Remove leading spaces and reindent
         (beginning-of-line 1)
         (delete-horizontal-space)
         (indent-relative))))))

(defun brownts/hack-local-variables-lsp ()
  "Delayed LSP initialization for certain major modes."
  (when (and (derived-mode-p 'ada-mode 'ada-ts-mode)
             ;; Don't enable LSP in test files
             (not (string-suffix-p ".erts" (buffer-file-name) 'ignore-case)))
    (lsp)
    ;; Set desired indentation offset
    (setq-local ada-ts-mode-indent-offset 3)
    ;; Use line indentation via LSP
    (setq-local indent-line-function #'brownts/lsp-format-line)
    ;; Convenient characters to trigger indentation, besides RET
    (setq-local electric-indent-chars (append ";>," electric-indent-chars))))

(defun set-up-ada-ts-mode ()
  (progn
    (brownts/hack-local-variables-lsp)
    (setq-local lsp-auto-guess-root nil)
    (setq-local lsp-semantic-tokens-enable t)
    (setq-local lsp-eldoc-enable-hover nil) ; using `lsp-ui-doc-include-signature'
    (setq-local lsp-enable-imenu nil))) ; prefer mode's own imenu instead of lsp
(add-hook 'ada-ts-mode-hook #'set-up-ada-ts-mode)

(Thanks for providing ada-ts-mode-hook; i never got around to supporting an analogous hook in my plisp-mode.)

This seems to be working so far. 🙂

brownts commented 2 months ago

I'm probably considered an old bat too, lol, but I've avoided Emacs for most of my career, however I did use vim for a couple years in the early 2000's. Anyway, glad it's working for you.

Just FYI, Eglot has a similar eglot-format command which can be used to perform this same indentation. I was working on making this more configurable where the user could just configure an "indent region" function, pointing either to the Eglot or lsp-mode "indent region" function, depending on which LSP client they are using. As part of working on that, I looked more into why ALS didn't seem to be indenting properly after a newline, and believe I've tracked it down to how edits are applied in both lsp-mode and Eglot. They prevent the point from being moved while they perform the edit, but the ALS seems to expect this to be allowed. I've filed a bug report against Eglot, we'll see if this gets any traction. If that gets accepted/fixed, then I'll work on getting it fixed in lsp-mode too.

Technically, the approach described in the previous messages use the textDocument/rangeFormatting LSP command, but I believe the intention for the ALS was to use the textDocument/onTypeFormatting command for indentation. I think either will ultimately work, but it would be good to have ALS determine the indentation on empty lines if possible rather than falling back on indenting to the previous line, as that's not always correct.

Once this gets sorted out, I'll make a change to ada-ts-mode to make it easier for people to configure this.