Subject: bug#54702: 29.0.50; ruby-mode indentation: endless methods

From: Dmitry Gutov <[email-masked]>
Date: Fri, 16 Dec 2022 14:31:17 +0200

On 16/12/2022 07:07, Aaron Jensen wrote:

>> Not a problem for me, but could you test the attached patch anyway?
>>
>> It seems to handle a bunch of different/complex cases fine without
>> regressions, but it's always better with a second pair of eyes.
> 
> Sure. I tried a few things and the only problem I can find is that it
> does not handle endless module methods:
> 
> def self.some_method =
>    "some-value"
> 
> (with or without the line break, it handles them as the unpatched
> version handles instance endless methods.

Right, thanks. See the attached updated patch.

>>> I'd probably use ruby-mode if
>>> it supported indenting long parameter/argument lists the way the
>>> non-smie version does, like this:
>>>
>>> def some_method(
>>>     some_param,
>>>     some_other_param
>>> )
>>
>> Now that the SMIE stuff is again in my short-term memory, it shouldn't
>> be too hard. Just please file a separate bug report (slash feature
>> request) with a precise example. Bonus points for linking to a relevant
>> Rubocop rule, so that we can pick a better name for the new user option.
> 
> Sure thing, just sent one in: bug#60110
> 
>> I don't see the non-SMIE version indenting it like this -- it looks more
>> like this instead (and only if I set ruby-deep-indent-paren to nil):
>>
>> def test2 (
>>       asd,
>>       asd
>>       asd
>>       )
>>
>> So let's start with a couple of good examples.
> 
> Yeah, this is what I see too. Not sure what I saw before. In any case,
> I sent a current/desired.

Thanks! The implementation (on top of the patch here) looks trivial, the 
question is whether to add an option, or just change the behavior and 
treat it like a "fix". Let's continue there after closing this one.

> Unrelated, but I'm excited about the prospect of a treesit mode for Ruby.

Yeah, it looks promising, but let's see how it goes.

And there is a certain barrier to compiling the tree-sitter stuff, so 
we'll probably need to maintain the current/pure version in parallel for 
a while.

diff --git a/lisp/progmodes/ruby-mode.el b/lisp/progmodes/ruby-mode.el
index 4ac289d529..d20e5e9b51 100644
--- a/lisp/progmodes/ruby-mode.el
+++ b/lisp/progmodes/ruby-mode.el
@@ -134,6 +134,12 @@ ruby-symbol-chars
 (defconst ruby-symbol-re (concat "[" ruby-symbol-chars "]")
   "Regexp to match symbols.")
 
+(defconst ruby-endless-method-head-re
+  (format " *\\(self\\.\\)?%s+[?!]? *\\(([^()]*)\\)? *=" ruby-symbol-re)
+  "Regexp to match the beginning of an endless method definition.
+
+It should match the part after \"def\" and until \"=\".")
+
 (defvar ruby-use-smie t)
 (make-obsolete-variable 'ruby-use-smie nil "28.1")
 
@@ -351,7 +357,8 @@ ruby-smie-grammar
        (exp  (exp1) (exp "," exp) (exp "=" exp)
              (id " @ " exp))
        (exp1 (exp2) (exp2 "?" exp1 ":" exp1))
-       (exp2 (exp3) (exp3 "." exp3))
+       (exp2 (exp3) (exp3 "." exp3)
+             (exp3 "def=" exp3))
        (exp3 ("def" insts "end")
              ("begin" insts-rescue-insts "end")
              ("do" insts "end")
@@ -528,6 +535,9 @@ ruby-smie--forward-token
               (ruby-smie--forward-token)) ;Fully redundant.
              (t ";")))
            ((equal tok "&.") ".")
+           ((and (equal tok "def")
+                 (looking-at ruby-endless-method-head-re))
+            "def=")
            (t tok)))))))))
 
 (defun ruby-smie--backward-token ()
@@ -575,6 +585,9 @@ ruby-smie--backward-token
             (ruby-smie--backward-token)) ;Fully redundant.
            (t ";")))
          ((equal tok "&.") ".")
+         ((and (equal tok "def")
+               (looking-at (concat "def" ruby-endless-method-head-re)))
+          "def=")
          (t tok)))))))
 
 (defun ruby-smie--indent-to-stmt ()
@@ -641,9 +654,11 @@ ruby-smie-rules
        (unless (or (eolp) (forward-comment 1))
          (cons 'column (current-column)))))
     ('(:before . " @ ")
-     (save-excursion
-       (skip-chars-forward " \t")
-       (cons 'column (current-column))))
+     (if (smie-rule-parent-p "def=")
+         (smie-rule-parent)
+       (save-excursion
+         (skip-chars-forward " \t")
+         (cons 'column (current-column)))))
     ('(:before . "do") (ruby-smie--indent-to-stmt))
     ('(:before . ".")
      (if (smie-rule-sibling-p)
@@ -672,6 +687,9 @@ ruby-smie-rules
      (and (smie-rule-parent-p ";" nil)
           (smie-indent--hanging-p)
           ruby-indent-level))
+    (`(:before . "=")
+     (and (smie-rule-parent-p " @ ")
+          (smie-rule-parent ruby-indent-level)))
     (`(:after . ,(or "?" ":")) ruby-indent-level)
     (`(:before . ,(guard (memq (intern-soft token) ruby-alignable-keywords)))
      (when (not (ruby--at-indentation-p))
@@ -1631,7 +1649,7 @@ ruby-add-log-current-method
                   (while (and (re-search-backward definition-re nil t)
                               (if (if (string-equal "def" (match-string 1))
                                       ;; We're inside a method.
-                                      (if (ruby-block-contains-point start)
+                                      (if (ruby-block-contains-point (1- start))
                                           t
                                         ;; Try to match a method only once.
                                         (setq definition-re module-re)
diff --git a/test/lisp/progmodes/ruby-mode-resources/ruby.rb b/test/lisp/progmodes/ruby-mode-resources/ruby.rb
index f39489071e..2e087393a8 100644
--- a/test/lisp/progmodes/ruby-mode-resources/ruby.rb
+++ b/test/lisp/progmodes/ruby-mode-resources/ruby.rb
@@ -500,3 +500,25 @@ def resolve(**args)
 
   member.call(**args)
 end
+
+# Endless methods.
+class Bar
+  def foo(abc) =
+    bar +
+    bar
+      .baz
+
+  def self.bar =
+    123 +
+    4
+
+  def request_params = {
+    headers: request_headers,
+    body: request_body
+  }
+end
+
+class Foo
+  def foo(...) = z
+  def bar = y
+end
diff --git a/test/lisp/progmodes/ruby-mode-tests.el b/test/lisp/progmodes/ruby-mode-tests.el
index e90a9e4075..9be01dc78f 100644
--- a/test/lisp/progmodes/ruby-mode-tests.el
+++ b/test/lisp/progmodes/ruby-mode-tests.el
@@ -605,6 +605,18 @@ ruby-add-log-current-method-after-inner-class-outside-methods-with-text
     (search-backward "FOO")
     (should (string= (ruby-add-log-current-method) "M::C"))))
 
+(ert-deftest ruby-add-log-current-method-after-endless-method ()
+  (ruby-with-temp-buffer (ruby-test-string
+                          "module M
+                          |  class C
+                          |    def foo =
+                          |      4_
+                          |  end
+                          |end")
+    (search-backward "_")
+    (delete-char 1)
+    (should (string= (ruby-add-log-current-method) "M::C#foo"))))
+
 (defvar ruby-block-test-example
   (ruby-test-string
    "class C