unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
From: Randy Taylor <dev@rjt.dev>
To: Yuan Fu <casouri@gmail.com>
Cc: Eli Zaretskii <eliz@gnu.org>, theo@thornhill.no, 60025@debbugs.gnu.org
Subject: bug#60025: [PATCH] Add go-ts-mode and go-mod-ts-mode
Date: Wed, 14 Dec 2022 20:54:39 +0000	[thread overview]
Message-ID: <onHMso1wpk-LXbNoCfpK0FjkgGQdElX1YhqSVfo2f6hizMsHYqjFIYFJN6orpxBhzcAom7QPicA0_18qzUzvUNWiRj6UGgBo8ZoOXPzLOmE=@rjt.dev> (raw)
In-Reply-To: <B377A51D-368E-4253-895A-9FAA31B85416@gmail.com>

[-- Attachment #1: Type: text/plain, Size: 1903 bytes --]

On Wednesday, December 14th, 2022 at 14:55, Yuan Fu <casouri@gmail.com> wrote:
> 
> > I am having a tiny bit of trouble with a go.mod indentation rule. Using the patch, create a go.mod file anywhere, activate go-mod-ts-mode and add the following:
> > 
> > require ()
> > 
> > Place point inside the parens, and then hit enter. The expectation is that point will end up indented inside that block. If you add the text "test v1.0.0" and hit TAB, it will indent properly (and if you hit enter after that text it will indent properly for the next entry). If you go to the end of the line for the top paren and hit enter, it will not indent (and we want it to). It seems to give us no-node in that circumstance. Is there a simple indent rule that can match exactly that that I'm missing?
> 
> 
> I think you can just test for the parent? In C, if point is at an empty line after a statement in a block, like this:
> 
> int main() {
> return 0;
> |
> }
> 
> The matched rule is (parent-is “compond_statement”), where compound_statement is the block. In your case, I guess you can test if parent is the argument list.
> 
> Yuan

I do match for the parent, but it doesn't seem to help.
Here's what tree-sitter explorer shows for the following:
require (

)

(require_directive require ( \n ) \n)

If I put point on the line right below r and do C-S-a, tree-sitter explorer shows:
(require_directive require (*\n ) \n)
The * indicates that part is highlighted.

The second I make it "proper" like so:
require (
test v1
)

I can hit TAB and it will indent properly (but not anything above it, only that line and anything below).

I would think that checking parent-is for require_directive would be enough but it's not somehow???
It's instead matching my no-node rule...

(Also I attached a new patch which contains a few more indent rules for go.mod I forgot about).

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-go-ts-mode-and-go-mod-ts-mode-Bug-60025.patch --]
[-- Type: text/x-patch; name=0001-Add-go-ts-mode-and-go-mod-ts-mode-Bug-60025.patch, Size: 15259 bytes --]

From 7555a4b0f7830210b569bff2ddcd87c42cacda3e Mon Sep 17 00:00:00 2001
From: Randy Taylor <dev@rjt.dev>
Date: Sun, 11 Dec 2022 18:41:16 -0500
Subject: [PATCH] Add go-ts-mode and go-mod-ts-mode (Bug#60025)

* admin/notes/tree-sitter/build-module/batch.sh:
* admin/notes/tree-sitter/build-module/build.sh: Add go-mod support.
* etc/NEWS: Mention them.
* lisp/progmodes/eglot.el (eglot-server-programs): Add them.
* lisp/progmodes/go-ts-mode.el: New major modes with
tree-sitter support.
---
 admin/notes/tree-sitter/build-module/batch.sh |   1 +
 admin/notes/tree-sitter/build-module/build.sh |   5 +
 etc/NEWS                                      |  11 +
 lisp/progmodes/eglot.el                       |   3 +-
 lisp/progmodes/go-ts-mode.el                  | 335 ++++++++++++++++++
 5 files changed, 354 insertions(+), 1 deletion(-)
 create mode 100644 lisp/progmodes/go-ts-mode.el

diff --git a/admin/notes/tree-sitter/build-module/batch.sh b/admin/notes/tree-sitter/build-module/batch.sh
index c395449977..c322d405f2 100755
--- a/admin/notes/tree-sitter/build-module/batch.sh
+++ b/admin/notes/tree-sitter/build-module/batch.sh
@@ -8,6 +8,7 @@ languages=
     'c-sharp'
     'dockerfile'
     'go'
+    'go-mod'
     'html'
     'javascript'
     'json'
diff --git a/admin/notes/tree-sitter/build-module/build.sh b/admin/notes/tree-sitter/build-module/build.sh
index 4195ea58c3..6d22127bd1 100755
--- a/admin/notes/tree-sitter/build-module/build.sh
+++ b/admin/notes/tree-sitter/build-module/build.sh
@@ -26,6 +26,11 @@ grammardir=
     "cmake")
         org="uyha"
         ;;
+    "go-mod")
+        # The parser is called "gomod".
+        lang="gomod"
+        namespace="camdencheek"
+        ;;
     "typescript")
         sourcedir="tree-sitter-typescript/typescript/src"
         grammardir="tree-sitter-typescript/typescript"
diff --git a/etc/NEWS b/etc/NEWS
index 6cb531cd0f..02c8d19778 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -3078,6 +3078,17 @@ A major mode based on the tree-sitter library for editing CMake files.
 It includes support for font-locking, indentation, Imenu, and
 which-func.
 
+** New major mode 'go-ts-mode'.
+A major mode based on the tree-sitter library for editing programs in
+the Go language.  It includes support for font-locking, indentation,
+Imenu, and which-func.  This mode will be auto-enabled for files with
+the ".go" extension.
+
+** New major mode 'go-mod-ts-mode'.
+A major mode based on the tree-sitter library for editing "go.mod"
+files.  It includes support for font-locking and indentation.  This
+mode will be auto-enabled for "go.mod" files.
+
 \f
 * Incompatible Lisp Changes in Emacs 29.1
 
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 9c5a361df7..02bb6bb665 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -211,7 +211,8 @@ eglot-server-programs
                                 (elm-mode . ("elm-language-server"))
                                 (mint-mode . ("mint" "ls"))
                                 (kotlin-mode . ("kotlin-language-server"))
-                                ((go-mode go-dot-mod-mode go-dot-work-mode) . ("gopls"))
+                                ((go-mode go-dot-mod-mode go-dot-work-mode go-ts-mode go-mod-ts-mode)
+                                 . ("gopls"))
                                 ((R-mode ess-r-mode) . ("R" "--slave" "-e"
                                                         "languageserver::run()"))
                                 ((java-mode java-ts-mode) . ("jdtls"))
diff --git a/lisp/progmodes/go-ts-mode.el b/lisp/progmodes/go-ts-mode.el
new file mode 100644
index 0000000000..02f5d53810
--- /dev/null
+++ b/lisp/progmodes/go-ts-mode.el
@@ -0,0 +1,335 @@
+;;; go-ts-mode.el --- tree-sitter support for Go  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; Author     : Randy Taylor <dev@rjt.dev>
+;; Maintainer : Randy Taylor <dev@rjt.dev>
+;; Created    : December 2022
+;; Keywords   : go languages tree-sitter
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(require 'treesit)
+(eval-when-compile (require 'rx))
+
+(declare-function treesit-parser-create "treesit.c")
+(declare-function treesit-induce-sparse-tree "treesit.c")
+(declare-function treesit-node-child "treesit.c")
+(declare-function treesit-node-child-by-field-name "treesit.c")
+(declare-function treesit-node-start "treesit.c")
+(declare-function treesit-node-type "treesit.c")
+
+(defcustom go-ts-mode-indent-offset 4
+  "Number of spaces for each indentation step in `go-ts-mode'."
+  :version "29.1"
+  :type 'integer
+  :safe 'integerp
+  :group 'go)
+
+(defvar go-ts-mode--syntax-table
+  (let ((table (make-syntax-table)))
+    (modify-syntax-entry ?+   "."      table)
+    (modify-syntax-entry ?-   "."      table)
+    (modify-syntax-entry ?=   "."      table)
+    (modify-syntax-entry ?%   "."      table)
+    (modify-syntax-entry ?&   "."      table)
+    (modify-syntax-entry ?|   "."      table)
+    (modify-syntax-entry ?^   "."      table)
+    (modify-syntax-entry ?!   "."      table)
+    (modify-syntax-entry ?<   "."      table)
+    (modify-syntax-entry ?>   "."      table)
+    (modify-syntax-entry ?\\  "\\"     table)
+    (modify-syntax-entry ?/   ". 124b" table)
+    (modify-syntax-entry ?*   ". 23"   table)
+    (modify-syntax-entry ?\n  "> b"    table)
+    table)
+  "Syntax table for `go-ts-mode'.")
+
+(defvar go-ts-mode--indent-rules
+  `((go
+     ((node-is ")") parent-bol 0)
+     ((node-is "]") parent-bol 0)
+     ((node-is "}") parent-bol 0)
+     ((node-is "labeled_statement") no-indent)
+     ((parent-is "argument_list") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "block") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "const_declaration") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "default_case") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "expression_case") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "expression_switch_statement") parent-bol 0)
+     ((parent-is "field_declaration_list") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "import_spec_list") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "labeled_statement") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "literal_value") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "type_spec") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "var_declaration") parent-bol go-ts-mode-indent-offset)
+     (no-node parent-bol 0)))
+  "Tree-sitter indent rules for `go-ts-mode'.")
+
+(defvar go-ts-mode--keywords
+  '("break" "case" "chan" "const" "continue" "default" "defer" "else"
+    "fallthrough" "for" "func" "go" "goto" "if" "import" "interface" "map"
+    "package" "range" "return" "select" "struct" "switch" "type" "var")
+  "Go keywords for tree-sitter font-locking.")
+
+(defvar go-ts-mode--operators
+  '("+" "&" "+=" "&=" "&&" "==" "!=" "-" "|" "-=" "|=" "||" "<" "<="
+    "*" "^" "*=" "^=" "<-" ">" ">=" "/" "<<" "/=" "<<=" "++" "=" ":=" "%"
+    ">>" "%=" ">>=" "--" "!"  "..."  "&^" "&^=" "~")
+  "Go operators for tree-sitter font-locking.")
+
+(defvar go-ts-mode--font-lock-settings
+  (treesit-font-lock-rules
+   :language 'go
+   :feature 'bracket
+   '((["(" ")" "[" "]" "{" "}"]) @font-lock-bracket-face)
+
+   :language 'go
+   :feature 'comment
+   '((comment) @font-lock-comment-face)
+
+   :language 'go
+   :feature 'constant
+   '([(false) (iota) (nil) (true)] @font-lock-constant-face
+     (const_declaration
+      (const_spec name: (identifier) @font-lock-constant-face)))
+
+   :language 'go
+   :feature 'delimiter
+   '((["," "." ";" ":"]) @font-lock-delimiter-face)
+
+   :language 'go
+   :feature 'function
+   '((call_expression
+      function: (identifier) @font-lock-function-name-face)
+     (call_expression
+      function: (selector_expression
+                 field: (field_identifier) @font-lock-function-name-face))
+     (function_declaration
+      name: (identifier) @font-lock-function-name-face)
+     (method_declaration
+      name: (field_identifier) @font-lock-function-name-face))
+
+   :language 'go
+   :feature 'keyword
+   `([,@go-ts-mode--keywords] @font-lock-keyword-face)
+
+   :language 'go
+   :feature 'label
+   '((label_name) @font-lock-constant-face)
+
+   :language 'go
+   :feature 'number
+   '([(float_literal)
+      (imaginary_literal)
+      (int_literal)] @font-lock-number-face)
+
+   :language 'go
+   :feature 'string
+   '([(interpreted_string_literal)
+      (raw_string_literal)
+      (rune_literal)] @font-lock-string-face)
+
+   :language 'go
+   :feature 'type
+   '([(package_identifier) (type_identifier)] @font-lock-type-face)
+
+   :language 'go
+   :feature 'variable
+   '((identifier) @font-lock-variable-name-face)
+
+   :language 'go
+   :feature 'escape-sequence
+   :override t
+   '((escape_sequence) @font-lock-escape-face)
+
+   :language 'go
+   :feature 'property
+   :override t
+   '((field_identifier) @font-lock-property-face
+     (keyed_element (_ (identifier) @font-lock-property-face)))
+
+   :language 'go
+   :feature 'error
+   :override t
+   '((ERROR) @font-lock-warning-face))
+  "Tree-sitter font-lock settings for `go-ts-mode'.")
+
+(defun go-ts-mode--imenu ()
+  "Return Imenu alist for the current buffer."
+  (let* ((node (treesit-buffer-root-node))
+         (func-tree (treesit-induce-sparse-tree
+                     node "function_declaration" nil 1000))
+         (type-tree (treesit-induce-sparse-tree
+                     node "type_spec" nil 1000))
+         (func-index (go-ts-mode--imenu-1 func-tree))
+         (type-index (go-ts-mode--imenu-1 type-tree)))
+    (append
+     (when func-index `(("Function" . ,func-index)))
+     (when type-index `(("Type" . ,type-index))))))
+
+(defun go-ts-mode--imenu-1 (node)
+  "Helper for `go-ts-mode--imenu'.
+Find string representation for NODE and set marker, then recurse
+the subtrees."
+  (let* ((ts-node (car node))
+         (children (cdr node))
+         (subtrees (mapcan #'go-ts-mode--imenu-1
+                           children))
+         (name (when ts-node
+                 (treesit-node-text
+                  (pcase (treesit-node-type ts-node)
+                    ("function_declaration"
+                     (treesit-node-child-by-field-name ts-node "name"))
+                    ("type_spec"
+                     (treesit-node-child-by-field-name ts-node "name"))))))
+         (marker (when ts-node
+                   (set-marker (make-marker)
+                               (treesit-node-start ts-node)))))
+    (cond
+     ((or (null ts-node) (null name)) subtrees)
+     (subtrees
+      `((,name ,(cons name marker) ,@subtrees)))
+     (t
+      `((,name . ,marker))))))
+
+;;;###autoload
+(add-to-list 'auto-mode-alist '("\\.go\\'" . go-ts-mode))
+
+;;;###autoload
+(define-derived-mode go-ts-mode prog-mode "Go"
+  "Major mode for editing Go, powered by tree-sitter."
+  :group 'go
+  :syntax-table go-ts-mode--syntax-table
+
+  (when (treesit-ready-p 'go)
+    (treesit-parser-create 'go)
+
+    ;; Comments.
+    (setq-local comment-start "// ")
+    (setq-local comment-end "")
+    (setq-local comment-start-skip (rx "//" (* (syntax whitespace))))
+
+    ;; Imenu.
+    (setq-local imenu-create-index-function #'go-ts-mode--imenu)
+    (setq-local which-func-functions nil)
+
+    ;; Indent.
+    (setq-local indent-tabs-mode t
+                treesit-simple-indent-rules go-ts-mode--indent-rules)
+
+    ;; Font-lock.
+    (setq-local treesit-font-lock-settings go-ts-mode--font-lock-settings)
+    (setq-local treesit-font-lock-feature-list
+                '(( comment)
+                  ( keyword string type)
+                  ( constant escape-sequence function label number
+                    property variable)
+                  ( bracket delimiter error operator)))
+
+    (treesit-major-mode-setup)))
+
+;; go.mod support.
+
+(defvar go-mod-ts-mode--syntax-table
+  (let ((table (make-syntax-table)))
+    (modify-syntax-entry ?/   ". 124b" table)
+    (modify-syntax-entry ?\n  "> b"    table)
+    table)
+  "Syntax table for `go-mod-ts-mode'.")
+
+(defvar go-mod-ts-mode--indent-rules
+  `((gomod
+     ((node-is ")") parent-bol 0)
+     ((parent-is "exclude_directive") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "module_directive") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "replace_directive") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "require_directive") parent-bol go-ts-mode-indent-offset)
+     ((parent-is "retract_directive") parent-bol go-ts-mode-indent-offset)
+     (no-node parent-bol 0)))
+  "Tree-sitter indent rules for `go-mod-ts-mode'.")
+
+(defvar go-mod-ts-mode--keywords
+  '("exclude" "go" "module" "replace" "require" "retract")
+  "go.mod keywords for tree-sitter font-locking.")
+
+(defvar go-mod-ts-mode--font-lock-settings
+  (treesit-font-lock-rules
+   :language 'gomod
+   :feature 'bracket
+   '((["(" ")"]) @font-lock-bracket-face)
+
+   :language 'gomod
+   :feature 'comment
+   '((comment) @font-lock-comment-face)
+
+   :language 'gomod
+   :feature 'keyword
+   `([,@go-mod-ts-mode--keywords] @font-lock-keyword-face)
+
+   :language 'gomod
+   :feature 'number
+   '([(go_version) (version)] @font-lock-number-face)
+
+   :language 'gomod
+   :feature 'operator
+   '((["=>"]) @font-lock-operator-face)
+
+   :language 'gomod
+   :feature 'error
+   :override t
+   '((ERROR) @font-lock-warning-face))
+  "Tree-sitter font-lock settings for `go-mod-ts-mode'.")
+
+;;;###autoload
+(add-to-list 'auto-mode-alist '("/go\\.mod\\'" . go-mod-ts-mode))
+
+;;;###autoload
+(define-derived-mode go-mod-ts-mode prog-mode "Go Mod"
+  "Major mode for editing go.mod files, powered by tree-sitter."
+  :group 'go
+  :syntax-table go-mod-ts-mode--syntax-table
+
+  (when (treesit-ready-p 'gomod)
+    (treesit-parser-create 'gomod)
+
+    ;; Comments.
+    (setq-local comment-start "// ")
+    (setq-local comment-end "")
+    (setq-local comment-start-skip (rx "//" (* (syntax whitespace))))
+
+    ;; Indent.
+    (setq-local indent-tabs-mode t
+                treesit-simple-indent-rules go-mod-ts-mode--indent-rules)
+
+    ;; Font-lock.
+    (setq-local treesit-font-lock-settings go-mod-ts-mode--font-lock-settings)
+    (setq-local treesit-font-lock-feature-list
+                '((comment)
+                  (keyword)
+                  (number)
+                  (bracket error operator)))
+
+    (treesit-major-mode-setup)))
+
+(provide 'go-ts-mode)
+
+;;; go-ts-mode.el ends here
-- 
2.39.0


  reply	other threads:[~2022-12-14 20:54 UTC|newest]

Thread overview: 20+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-12-13  2:13 bug#60025: [PATCH] Add go-ts-mode and go-mod-ts-mode Randy Taylor
2022-12-13 12:25 ` Eli Zaretskii
2022-12-13 19:39   ` Randy Taylor
2022-12-14 12:02     ` Eli Zaretskii
2022-12-14 16:21       ` Randy Taylor
2022-12-14 19:55         ` Yuan Fu
2022-12-14 20:54           ` Randy Taylor [this message]
2022-12-14 21:27             ` Theodor Thornhill via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-12-14 21:56               ` Randy Taylor
2022-12-15  2:15               ` Randy Taylor
2022-12-15  7:20                 ` Theodor Thornhill via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-12-15 16:40                   ` Randy Taylor
2022-12-15 18:06                     ` Theodor Thornhill via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-12-15 19:45                       ` Randy Taylor
2022-12-15 19:59                         ` Theodor Thornhill via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-12-15 20:09                           ` Randy Taylor
2022-12-15 22:22                             ` Randy Taylor
2022-12-14  2:22 ` Yuan Fu
2022-12-16  1:22 ` Yuan Fu
2022-12-16  2:05   ` Randy Taylor

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.gnu.org/software/emacs/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to='onHMso1wpk-LXbNoCfpK0FjkgGQdElX1YhqSVfo2f6hizMsHYqjFIYFJN6orpxBhzcAom7QPicA0_18qzUzvUNWiRj6UGgBo8ZoOXPzLOmE=@rjt.dev' \
    --to=dev@rjt.dev \
    --cc=60025@debbugs.gnu.org \
    --cc=casouri@gmail.com \
    --cc=eliz@gnu.org \
    --cc=theo@thornhill.no \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).