From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.io!.POSTED.blaine.gmane.org!not-for-mail From: Perry Smith Newsgroups: gmane.emacs.devel Subject: ruby-ts-mode.el -- first draft Date: Sat, 10 Dec 2022 21:51:10 -0600 Message-ID: <065A1DE9-B9BA-4AA3-9D59-D0F5547B8824@easesoftware.com> Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3731.200.110.1.12\)) Content-Type: multipart/signed; boundary="Apple-Mail=_C9A0E6B3-F085-44C4-B616-DBD0B8DD587B"; protocol="application/pgp-signature"; micalg=pgp-sha256 Injection-Info: ciao.gmane.io; posting-host="blaine.gmane.org:116.202.254.214"; logging-data="33290"; mail-complaints-to="usenet@ciao.gmane.io" To: emacs-devel Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Sun Dec 11 04:52:17 2022 Return-path: Envelope-to: ged-emacs-devel@m.gmane-mx.org Original-Received: from lists.gnu.org ([209.51.188.17]) by ciao.gmane.io with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1p4DNx-0008RH-7G for ged-emacs-devel@m.gmane-mx.org; Sun, 11 Dec 2022 04:52:17 +0100 Original-Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1p4DND-0002s5-U6; Sat, 10 Dec 2022 22:51:31 -0500 Original-Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1p4DNB-0002rZ-TZ for emacs-devel@gnu.org; Sat, 10 Dec 2022 22:51:29 -0500 Original-Received: from cyan.elm.relay.mailchannels.net ([23.83.212.47]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1p4DN7-0001bB-LK for emacs-devel@gnu.org; Sat, 10 Dec 2022 22:51:29 -0500 X-Sender-Id: a2hosting|x-authuser|pedz+easesoftware.com@mi3-ss4.a2hosting.com Original-Received: from relay.mailchannels.net (localhost [127.0.0.1]) by relay.mailchannels.net (Postfix) with ESMTP id 5F61C3C0B21 for ; Sun, 11 Dec 2022 03:51:23 +0000 (UTC) Original-Received: from mi3-ss4.a2hosting.com (unknown [127.0.0.6]) (Authenticated sender: a2hosting) by relay.mailchannels.net (Postfix) with ESMTPA id A81C03C0A52 for ; Sun, 11 Dec 2022 03:51:22 +0000 (UTC) ARC-Seal: i=1; s=arc-2022; d=mailchannels.net; t=1670730683; a=rsa-sha256; cv=none; b=nhLcl575T27leb4sPH3RurpdtPwJqqXdrN80Dx8UPRDd1KlnTI0ddUfXRWVUVGz49DICc9 vvzb8zxTB0NqYFDmsn2UL5SsPQ3UQo+morhZVczAb7tewVLHtU9vSdzzWxg6U0Qw9KFsT6 4SQR8jv7x5JJpSorSsI4lIFAxxwjod98nTwBZ8WA5b9eAz3AAnSg3BRk8TDGh2ao4rpDNg g3KUavkmH5ZToUZOlhfNdZK0tBZRZ8A5f31TPFulBc7nepebUyU1R4fxKFO7gga2huH8vp 7CioFva0p7ZX9eD867gdCetADXhk45niMmMwiKvTnjDrf1jh7pAWCAXrAbUmVQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=mailchannels.net; s=arc-2022; t=1670730682; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: dkim-signature; bh=Id7OxYPD4j1DluGMertGOcnANa0rephAMeKdDvCWf+8=; b=qeeHwe8IEuRNyYBwGt5lnL56LC61LFLNjymIOVi6OGV1DgtVnqChClnWcf5cNmMUxyQSQS 00kVmn/EBgoh2v4gWkzqpL3KQcTjTYF8hDlIX+4iMDPiJCiEm1FXpbfxkZBfafLG+uUQi+ N8DIz/HPiFPbB02Kd0dHWUE6o8R6g5b9owlvKys7KUvXyTM6+aymUQNC8GroVlT1LSYcfR QGUCG6G15zh5qheyaAzWit/WchZdXQg/BUaYVoz3fkhoTGrWIE+yuAlYEJohXlsNfZ5nUu SCqdol4EzOqlmaKF6gggK3dXk2uRVUIdA9eqOfug5JhLaDcvAgVcALWGkEol7Q== ARC-Authentication-Results: i=1; rspamd-85f95c7974-xk78k; auth=pass smtp.auth=a2hosting smtp.mailfrom=pedz@easesoftware.com X-Sender-Id: a2hosting|x-authuser|pedz+easesoftware.com@mi3-ss4.a2hosting.com X-MC-Relay: Neutral X-MailChannels-SenderId: a2hosting|x-authuser|pedz+easesoftware.com@mi3-ss4.a2hosting.com X-MailChannels-Auth-Id: a2hosting X-Supply-Whispering: 53d0a17a20e77773_1670730683202_3140240186 X-MC-Loop-Signature: 1670730683202:3956681445 X-MC-Ingress-Time: 1670730683202 Original-Received: from mi3-ss4.a2hosting.com (mi3-ss4.a2hosting.com [68.66.200.199]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384) by 100.97.74.32 (trex/6.7.1); Sun, 11 Dec 2022 03:51:23 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=easesoftware.com; s=default; h=To:Date:Message-Id:Subject:Mime-Version: Content-Type:From:Sender:Reply-To:Cc:Content-Transfer-Encoding:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:In-Reply-To:References:List-Id:List-Help:List-Unsubscribe: List-Subscribe:List-Post:List-Owner:List-Archive; bh=Id7OxYPD4j1DluGMertGOcnANa0rephAMeKdDvCWf+8=; b=ij+0zGaJUjhT1MnzK/APRIUGsh EPPWvAcjxosNYH4CY6dwkFXryjw88dyqcPoxcwZRS6XHy7jEPr3pejhPqyfCiT0cryagM4L48VIgk irlZtrB4KvkP4uT0jJ6cDQPiKD928FPs8s1jq0nlGtWr+snZQL6rxDTTUdlBhdhut/hHcmi9Wyuy0 gV40wrMhjk+T9yEKCdvrd4VBkhgeYpeZhHQj+OC85fS/W2dCuzOeu4rFWJ+or1Qh/2N3sw90wnkm7 keGGyG1sLIQQILbxqb1ralKYjDNXlfAEK44toJob4ZZpHim0j2ArgWAIHbR0cyeJWPWl8SYdd0osj y4v2ra9Q==; Original-Received: from cpe-70-94-128-193.satx.res.rr.com ([70.94.128.193]:56379 helo=smtpclient.apple) by mi3-ss4.a2hosting.com with esmtpsa (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.95) (envelope-from ) id 1p4DN3-00AMSa-H7 for emacs-devel@gnu.org; Sat, 10 Dec 2022 22:51:21 -0500 X-Mailer: Apple Mail (2.3731.200.110.1.12) X-AuthUser: pedz+easesoftware.com@mi3-ss4.a2hosting.com Received-SPF: pass client-ip=23.83.212.47; envelope-from=pedz@easesoftware.com; helo=cyan.elm.relay.mailchannels.net X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, HTML_MESSAGE=0.001, RCVD_IN_DNSWL_NONE=-0.0001, RCVD_IN_MSPIKE_H2=-0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Original-Sender: emacs-devel-bounces+ged-emacs-devel=m.gmane-mx.org@gnu.org Xref: news.gmane.io gmane.emacs.devel:301137 Archived-At: --Apple-Mail=_C9A0E6B3-F085-44C4-B616-DBD0B8DD587B Content-Type: multipart/alternative; boundary="Apple-Mail=_9526CEDF-31D0-4B8C-8827-4622AA8BE155" --Apple-Mail=_9526CEDF-31D0-4B8C-8827-4622AA8BE155 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 Ruby is a versatile language and I fear that I may have missed wide = swaths of its features. So, here is the first pass. I hope folks can = play with it and find the bugs. Tree sitter is so versatile that for fortification, it is practically = endless the features you could add. I have a git repository here: https://github.com/pedz/ruby-ts-mode And here inline is the file: ;;; This is currently a work in progress. My intent is to release it ;;; with whatever copyright notice Free Software Foundation, ;;; Inc. wants. ;; Author : Perry Smith ;; Created : December 2022 ;; Keywords : ruby languages tree-sitter ;; This program 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. ;; This program 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 this program. If not, see . ;;; Code: (require 'treesit) (declare-function treesit-parser-create "treesit.c") (defcustom ruby-ts-mode-indent-offset 2 "Number of spaces for each indentation step in `ruby-ts-mode'." :version "29.1" :type 'integer :safe 'integerp :group 'ruby) (defcustom ruby-ts-indent-tabs-mode nil "Indentation can insert tabs in Ruby TS mode if this is non-nil." :version "29.1" :type 'boolean :safe 'booleanp :group 'ruby) (defcustom ruby-ts-mode-indent-style 'base "Style used for indentation. The selected style could be one of Ruby. If one of the supplied styles doesn't suffice a function could be set instead. This function is expected return a list that follows the form of `treesit-simple-indent-rules'." :version "29.1" :type '(choice (symbol :tag "Base" 'base) (function :tag "A function for user customized style" = ignore)) :group 'ruby) (defcustom ruby-ts-mode-indent-style 'gnu "Style used for indentation. Currently can only be set to BASE. If one of the supplied styles doesn't suffice a function could be set instead. This function is expected return a list that follows the form of `treesit-simple-indent-rules'." :version "29.1" :type '(choice (symbol :tag "Base" 'base) (function :tag "A function for user customized style" = ignore)) :group 'ruby) (defface ruby-ts-mode--constant-assignment-face '((((class grayscale) (background light)) :foreground "DimGray" :slant = italic) (((class grayscale) (background dark)) :foreground "LightGray" = :slant italic) (((class color) (min-colors 88) (background light)) :foreground = "VioletRed4") (((class color) (min-colors 88) (background dark)) :foreground = "plum2") (((class color) (min-colors 16) (background light)) :foreground = "RosyBrown") (((class color) (min-colors 16) (background dark)) :foreground = "LightSalmon") (((class color) (min-colors 8)) :foreground "green") (t :slant italic)) "Font Lock mode face used in ruby-ts-mode to highlight assignments to = constants." :group 'font-lock-faces) (defface ruby-ts-mode--assignment-face '((((class grayscale) (background light)) :foreground "DimGray" :slant = italic) (((class grayscale) (background dark)) :foreground "LightGray" = :slant italic) (((class color) (min-colors 88) (background light)) :foreground = "VioletRed4") (((class color) (min-colors 88) (background dark)) :foreground = "coral1") (((class color) (min-colors 16) (background light)) :foreground = "RosyBrown") (((class color) (min-colors 16) (background dark)) :foreground = "LightSalmon") (((class color) (min-colors 8)) :foreground "green") (t :slant italic)) "Font Lock mode face used in ruby-ts-mode to hightlight assignments." :group 'font-lock-faces) (defvar ruby-ts-mode--syntax-table (let ((table (make-syntax-table))) ;; Mostly stolen from ruby-mode but enh-ruby-mode also added ?? (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 ?=3D "." 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 ?\` "\"" table) (modify-syntax-entry ?\n ">" table) (modify-syntax-entry ?\{ "(}" table) (modify-syntax-entry ?\} "){" table) (modify-syntax-entry ?_ "_" table) (modify-syntax-entry ?| "." table) table) "Syntax table used by =E2=80=98ruby-ts-mode=E2=80=99 buffers.") (defvar ruby-ts-mode--operators-arithmetic '("+" "-" "*" "/" "%" "**") "Ruby arithmetic operators for tree-sitter font-locking.") ;; treesit-query-validate doesn't like these: ;; "eql?" "equal?" (defvar ruby-ts-mode--operators-comparison '("=3D=3D" "!=3D" ">" "<" ">=3D" "<=3D" "<=3D>" "=3D=3D=3D") "Ruby comparison operators for tree-sitter font-locking.") (defvar ruby-ts-mode--operators-assignment '("=3D" "+=3D" "-=3D" "*=3D" "/=3D" "%=3D" "**=3D") "Ruby assignment operators for tree-sitter font-locking.") (defvar ruby-ts-mode--operators-bitwise '("&" "|" "^" "~" "<<" ">>") "Ruby bitwise operators for tree-sitter font-locking.") (defvar ruby-ts-mode--operators-logical '("!" "&&" "and" "not" "or" "||") "Ruby logical operators for tree-sitter font-locking.") (defvar ruby-ts-mode--operators-ternary '("?" ":") "Ruby ternary operators for tree-sitter font-locking.") (defvar ruby-ts-mode--operators-range '(".." "...") "Ruby range operators for tree-sitter font-locking.") (defvar ruby-ts-mode--operators-defined '("defined?") "Ruby defined? operators for tree-sitter font-locking.") (defvar ruby-ts-mode--operators-dot-colon '("." "::") "Ruby dot and double colon operators for tree-sitter font-locking.") (defvar ruby-ts-mode--operators (append ruby-ts-mode--operators-arithmetic ruby-ts-mode--operators-comparison ruby-ts-mode--operators-assignment ruby-ts-mode--operators-bitwise ruby-ts-mode--operators-logical ruby-ts-mode--operators-ternary ruby-ts-mode--operators-range ruby-ts-mode--operators-defined ruby-ts-mode--operators-dot-colon) "Ruby operators for tree-sitter font-locking.") ;; doc/keywords.rdoc in the Ruby git repository considers these to be ;; reserved keywords. If these keywords are added to the list, it ;; causes the font-lock to stop working. ;; ;; "__ENCODING__" "__FILE__" "__LINE__" "false" "self" "super" "true" ;; ;; "nil" (which does not exhibit this issue) is also considered a ;; keyword but I removed it and added it as a constant. ;; (defun ruby-ts-mode--keywords (language) "Ruby keywords for tree-sitter font-locking. Currently LANGUAGE is ignored but shoule be set to `ruby'." (let ((common-keywords '("BEGIN" "END" "alias" "and" "begin" "break" "case" "class" "def" "defined?" "do" "else" "elsif" "end" "ensure" "for" "if" "in" "module" "next" "not" "or" "redo" "rescue" "retry" "return" "then" "undef" "unless" "until" "when" "while" "yield"))) common-keywords)) ;; Ideas of what could be added: ;; 1. The regular expressions start, end, and content could be font ;; locked. Ditto for the command strings `foo`. The symbols ;; inside a %s, %i, and %I could be given the "symbol" font. ;; etc. (defun ruby-ts-mode--font-lock-settings (language) "Tree-sitter font-lock settings. Currently LANGUAGE is ignored but should be set to `ruby'." (treesit-font-lock-rules :language language :feature 'comment `((comment) @font-lock-comment-face (comment) @contextual) :language language :feature 'keyword `([,@(ruby-ts-mode--keywords language)] @font-lock-keyword-face) :language language :feature 'constant `((true) @font-lock-constant-face (false) @font-lock-constant-face (nil) @font-lock-constant-face (self) @font-lock-constant-face (super) @font-lock-constant-face) ;; Before 'operator so (unary) works. (I didn't want to try ;; :override) :language language :feature 'literal `((unary ["+" "-"] [(integer) (rational) (float) (complex)]) = @font-lock-number-face (simple_symbol) @font-lock-number-face (delimited_symbol) @font-lock-number-face (integer) @font-lock-number-face (float) @font-lock-number-face (complex) @font-lock-number-face (rational) @font-lock-number-face) :language language :feature 'operator `("!" @font-lock-negation-char-face [,@ruby-ts-mode--operators] @font-lock-operator-face) :language language :feature 'string `((string) @font-lock-string-face (string_content) @font-lock-string-face) :language language :feature 'type `((constant) @font-lock-type-face) :language language :feature 'assignment '((assignment left: (identifier) @ruby-ts-mode--assignment-face) (assignment left: (left_assignment_list (identifier) = @ruby-ts-mode--assignment-face)) (operator_assignment left: (identifier) @ruby-ts-mode--assignment-face)) ;; Constant and scoped constant assignment (declaration) ;; Must be enabled explicitly :language language :feature 'constant-assignment :override t `((assignment left: (constant) @ruby-ts-mode--constant-assignment-face) (assignment left: (scope_resolution name: (constant) = @ruby-ts-mode--constant-assignment-face))) :language language :feature 'function '((call method: (identifier) @font-lock-function-name-face) (method name: (identifier) @font-lock-function-name-face)) :language language :feature 'variable '((identifier) @font-lock-variable-name-face) :language language :feature 'error '((ERROR) @font-lock-warning-face) :feature 'escape-sequence :language language :override t '((escape_sequence) @font-lock-escape-face) :language language :feature 'bracket '((["(" ")" "[" "]" "{" "}"]) @font-lock-bracket-face) ) ) (defun ruby-ts-mode--indent-styles (language) "Indent rules supported by `ruby-ts-mode'. Currently LANGUAGE is ignored but should be set to `ruby'" (let ((common `( ;; Slam all top level nodes to the left margin ((parent-is "program") parent 0) ((node-is ")") parent 0) ((node-is "end") grand-parent 0) ;; method parameters with and without '(' ((query "(method_parameters \"(\" _ @indent)") first-sibling = 1) ((parent-is "method_parameters") first-sibling 0) ((node-is "body_statement") parent = ruby-ts-mode-indent-offset) ((parent-is "body_statement") first-sibling 0) ((parent-is "binary") first-sibling 0) ;; "when" list spread across multiple lines ((n-p-gp "pattern" "when" "case") (nth-sibling 1) 0) ((n-p-gp nil "then" "when") grand-parent = ruby-ts-mode-indent-offset) ;; if / unless unless expressions ((node-is "else") parent-bol 0) ((node-is "elsif") parent-bol 0) ((node-is "when") parent-bol 0) ((parent-is "then") parent-bol ruby-ts-mode-indent-offset) ((parent-is "else") parent-bol ruby-ts-mode-indent-offset) ((parent-is "elsif") parent-bol ruby-ts-mode-indent-offset) ;; for, while, until loops ((parent-is "do") grand-parent ruby-ts-mode-indent-offset) ;; Assignment of hash and array ((n-p-gp "}" "hash" "assignment") grand-parent 0) ((n-p-gp "pair" "hash" "assignment") grand-parent = ruby-ts-mode-indent-offset) ((n-p-gp "]" "array" "assignment") grand-parent 0) ((n-p-gp ".*" "array" "assignment") grand-parent = ruby-ts-mode-indent-offset) ;; hash and array other than assignments ((node-is "}") first-sibling 0) ((parent-is "hash") first-sibling 1) ((node-is "]") first-sibling 0) ((parent-is "array") first-sibling 1) ;; method call arguments with and without '(' ((query "(argument_list \"(\" _ @indent)") first-sibling 1) ((parent-is "argument_list") first-sibling 0) ))) `((base ,@common)))) (defun ruby-ts-mode--class-or-module-p (node) "Predicate returns turthy if NODE is a class or module" (string-match-p "class\\|module" (treesit-node-type node))) (defun ruby-ts-mode--get-name (node) "Returns the text of the `name' field of NODE" (treesit-node-text (treesit-node-child-by-field-name node "name"))) (defun ruby-ts-mode--full-name (node) "Returns the fully qualified name of NODE" (let* ((name (get-name node)) (delimiter "#")) (while (setq node (treesit-parent-until node = #'ruby-ts-mode--class-or-module-p)) (setq name (concat (get-name node) delimiter name)) (setq delimiter "::")) name)) (defun ruby-ts-mode--imenu-helper (node) "Helper for `ruby-ts-mode--imenu' converting a treesit sparse tree into a list of imenu ( name . pos ) nodes" (let* ((ts-node (car node)) (subtrees (mapcan #'ruby-ts-mode--imenu-helper (cdr node))) (name (when ts-node (ruby-ts-mode--full-name ts-node))) (marker (when ts-node (set-marker (make-marker) (treesit-node-start ts-node))))) (cond ((or (null ts-node) (null name)) subtrees) ;; Don't include the anonymous "class" and "module" nodes ((string-match-p "(\"\\(class\\|module\\)\")" (treesit-node-string ts-node)) nil) (subtrees `((,name ,(cons name marker) ,@subtrees))) (t `((,name . ,marker)))))) ;; For now, this is going to work like ruby-mode and return a list of ;; class, modules, def (methods), and alias. It is likely that this ;; can be rigged to be easily extended. (defun ruby-ts-mode--imenu () "Return Imenu alist for the current buffer." (let* ((root (treesit-buffer-root-node)) (nodes (treesit-induce-sparse-tree root = "^\\(method\\|alias\\|class\\|module\\)$"))) (ruby-ts-mode--imenu-helper nodes))) (defun ruby-ts-mode--set-indent-style (language) "Helper function to set the indentation style. Currently LANGUAGE is ignored but should be set to `ruby'." (let ((style (if (functionp ruby-ts-mode-indent-style) (funcall ruby-ts-mode-indent-style) (pcase ruby-ts-mode-indent-style ('base (alist-get 'base (ruby-ts-mode--indent-styles = language))))))) `((,language ,@style)))) (define-derived-mode ruby-ts-base-mode prog-mode "Ruby" "Major mode for editing Ruby, powered by tree-sitter." :syntax-table ruby-ts-mode--syntax-table ;; Navigation. (setq-local treesit-defun-type-regexp (regexp-opt '("method" "singleton_method"))) ;; AFAIK, Ruby can not nest methods (setq-local treesit-defun-prefer-top-level nil) ;; Imenu. (setq-local imenu-create-index-function #'ruby-ts-mode--imenu) ;; seems like this could be defined when I know more how tree sitter ;; works. (setq-local which-func-functions nil) (setq-local treesit-font-lock-feature-list '(( comment definition) ( keyword preprocessor string type) ( assignment constant escape-sequence label literal = property ) ( bracket delimiter error function operator variable))) ) (define-derived-mode ruby-ts-mode ruby-ts-base-mode "Ruby" "Major mode for editing Ruby, powered by tree-sitter." :group 'ruby (unless (treesit-ready-p 'ruby) (error "Tree-sitter for Ruby isn't available")) (treesit-parser-create 'ruby) ;; Comments. (setq-local comment-start "# ") (setq-local comment-end "") (setq-local comment-start-skip "#+ *") (setq indent-tabs-mode ruby-ts-indent-tabs-mode) (setq-local treesit-simple-indent-rules (ruby-ts-mode--set-indent-style 'ruby)) ;; Font-lock. (setq-local treesit-font-lock-settings = (ruby-ts-mode--font-lock-settings 'ruby)) (treesit-major-mode-setup)) ;; end of ruby-ts-mode.el --Apple-Mail=_9526CEDF-31D0-4B8C-8827-4622AA8BE155 Content-Transfer-Encoding: quoted-printable Content-Type: text/html; charset=utf-8
Ruby is a = versatile language and I fear that I may have missed wide swaths of its = features.  So, here is the first pass.  I hope folks can play = with it and find the bugs.

Tree sitter is so = versatile that for fortification, it is practically endless the features = you could add.

I have a git repository here: https://github.com/pedz/ruby= -ts-mode

And here inline is the = file:

;;; This is currently a work in = progress.  My intent is to release it
;;; with whatever = copyright notice Free Software Foundation,
;;; Inc. = wants.

;; Author     : Perry Smith = <pedz@easesoftware.com>
;; Created    : = December 2022
;; Keywords   : ruby languages = tree-sitter

;; This program 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.

;; This = program 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 this program. =  If not, see = <http://www.gnu.org/licenses/>.

;;; = Code:

(require = 'treesit)

(declare-function = treesit-parser-create "treesit.c")

(defcustom = ruby-ts-mode-indent-offset 2
  "Number of spaces for each = indentation step in `ruby-ts-mode'."
  :version = "29.1"
  :type 'integer
  :safe = 'integerp
  :group = 'ruby)

(defcustom ruby-ts-indent-tabs-mode = nil
  "Indentation can insert tabs in Ruby TS mode if = this is non-nil."
  :version "29.1"
  = :type 'boolean
  :safe 'booleanp
  :group = 'ruby)

(defcustom ruby-ts-mode-indent-style = 'base
  "Style used for = indentation.

The selected style could be one of = Ruby.  If one of the supplied
styles doesn't suffice a = function could be set instead.  This
function is expected = return a list that follows the form = of
`treesit-simple-indent-rules'."
  :version = "29.1"
  :type '(choice (symbol :tag "Base" = 'base)
                =  (function :tag "A function for user customized style" = ignore))
  :group = 'ruby)

(defcustom ruby-ts-mode-indent-style = 'gnu
  "Style used for = indentation.

Currently can only be set to BASE. =  If one of the supplied styles
doesn't suffice a function = could be set instead.  This function
is expected return a = list that follows the form = of
`treesit-simple-indent-rules'."
  :version = "29.1"
  :type '(choice (symbol :tag "Base" = 'base)
                =  (function :tag "A function for user customized style" = ignore))
  :group = 'ruby)

(defface = ruby-ts-mode--constant-assignment-face
  '((((class = grayscale) (background light)) :foreground "DimGray" :slant = italic)
    (((class grayscale) (background dark)) =  :foreground "LightGray" :slant italic)
    = (((class color) (min-colors 88) (background light)) :foreground = "VioletRed4")
    (((class color) (min-colors 88) = (background dark))  :foreground "plum2")
    = (((class color) (min-colors 16) (background light)) :foreground = "RosyBrown")
    (((class color) (min-colors 16) = (background dark))  :foreground "LightSalmon")
  =   (((class color) (min-colors 8)) :foreground = "green")
    (t :slant italic))
  = "Font Lock mode face used in ruby-ts-mode to highlight assignments to = constants."
  :group = 'font-lock-faces)

(defface = ruby-ts-mode--assignment-face
  '((((class grayscale) = (background light)) :foreground "DimGray" :slant = italic)
    (((class grayscale) (background dark)) =  :foreground "LightGray" :slant italic)
    = (((class color) (min-colors 88) (background light)) :foreground = "VioletRed4")
    (((class color) (min-colors 88) = (background dark))  :foreground "coral1")
    = (((class color) (min-colors 16) (background light)) :foreground = "RosyBrown")
    (((class color) (min-colors 16) = (background dark))  :foreground "LightSalmon")
  =   (((class color) (min-colors 8)) :foreground = "green")
    (t :slant italic))
  = "Font Lock mode face used in ruby-ts-mode to hightlight = assignments."
  :group = 'font-lock-faces)

(defvar = ruby-ts-mode--syntax-table
  (let ((table = (make-syntax-table)))
    ;; Mostly stolen from = ruby-mode but enh-ruby-mode also added ??
    = (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 ?=3D =  "."  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 ?\` "\"" table)
    = (modify-syntax-entry ?\n ">"  table)
    = (modify-syntax-entry ?\{ "(}" table)
    = (modify-syntax-entry ?\} "){" table)
    = (modify-syntax-entry ?_  "_"  table)
    = (modify-syntax-entry ?|  "."  table)
    = table)
  "Syntax table used by =E2=80=98ruby-ts-mode=E2=80=99= buffers.")

(defvar = ruby-ts-mode--operators-arithmetic
  '("+" "-" "*" "/" = "%" "**")
  "Ruby arithmetic operators for tree-sitter = font-locking.")

;; treesit-query-validate = doesn't like these:
;; "eql?" "equal?"
(defvar = ruby-ts-mode--operators-comparison
  '("=3D=3D" "!=3D" = ">" "<" ">=3D" "<=3D" "<=3D>" "=3D=3D=3D")
&nb= sp; "Ruby comparison operators for tree-sitter = font-locking.")

(defvar = ruby-ts-mode--operators-assignment
  '("=3D" "+=3D" "-=3D" = "*=3D" "/=3D" "%=3D" "**=3D")
  "Ruby assignment = operators for tree-sitter = font-locking.")

(defvar = ruby-ts-mode--operators-bitwise
  '("&" "|" "^" "~" = "<<" ">>")
  "Ruby bitwise operators for = tree-sitter font-locking.")

(defvar = ruby-ts-mode--operators-logical
  '("!" "&&" = "and" "not" "or" "||")
  "Ruby logical operators for = tree-sitter font-locking.")

(defvar = ruby-ts-mode--operators-ternary
  '("?" = ":")
  "Ruby ternary operators for tree-sitter = font-locking.")

(defvar = ruby-ts-mode--operators-range
  '(".." = "...")
  "Ruby range operators for tree-sitter = font-locking.")

(defvar = ruby-ts-mode--operators-defined
  = '("defined?")
  "Ruby defined? operators for tree-sitter = font-locking.")

(defvar = ruby-ts-mode--operators-dot-colon
  '("." = "::")
  "Ruby dot and double colon operators for = tree-sitter font-locking.")

(defvar = ruby-ts-mode--operators
  (append = ruby-ts-mode--operators-arithmetic
        =   ruby-ts-mode--operators-comparison
      =     ruby-ts-mode--operators-assignment
    =       ruby-ts-mode--operators-bitwise
  =         = ruby-ts-mode--operators-logical
        =   ruby-ts-mode--operators-ternary
      =     ruby-ts-mode--operators-range
    =       ruby-ts-mode--operators-defined
  =         = ruby-ts-mode--operators-dot-colon)
  "Ruby operators for = tree-sitter font-locking.")

;; = doc/keywords.rdoc in the Ruby git repository considers these to = be
;; reserved keywords.  If these keywords are added to = the list, it
;; causes the font-lock to stop = working.
;;
;; "__ENCODING__" "__FILE__" "__LINE__" = "false" "self" "super" "true"
;;
;; "nil" (which = does not exhibit this issue) is also considered a
;; keyword = but I removed it and added it as a = constant.
;;
(defun ruby-ts-mode--keywords = (language)
  "Ruby keywords for tree-sitter = font-locking.
Currently LANGUAGE is ignored but shoule be set = to `ruby'."
  (let ((common-keywords
  =        '("BEGIN" "END" "alias" "and" "begin" "break" = "case" "class"
           "def" = "defined?" "do" "else" "elsif" "end" "ensure" "for"
  =          "if" "in" "module" "next" "not" "or" = "redo" "rescue"
          =  "retry" "return" "then" "undef" "unless" "until" = "when"
           "while" = "yield")))
    = common-keywords))

;; Ideas of what could be = added:
;;   1. The regular expressions start, end, and = content could be font
;;      locked. =  Ditto for the command strings `foo`.  The = symbols
;;      inside a %s, %i, and %I could = be given the "symbol" font.
;;     =  etc.
(defun ruby-ts-mode--font-lock-settings = (language)
  "Tree-sitter font-lock = settings.
Currently LANGUAGE is ignored but should be set to = `ruby'."
  (treesit-font-lock-rules
  =  :language language
   :feature = 'comment
   `((comment) = @font-lock-comment-face
     (comment) = @contextual)

   :language = language
   :feature 'keyword
  =  `([,@(ruby-ts-mode--keywords language)] = @font-lock-keyword-face)

   :language = language
   :feature 'constant
  =  `((true) @font-lock-constant-face
    =  (false) @font-lock-constant-face
    =  (nil) @font-lock-constant-face
    =  (self) @font-lock-constant-face
    =  (super) =  @font-lock-constant-face)

   ;; = Before 'operator so (unary) works.  (I didn't want to = try
   ;; :override)
  =  :language language
   :feature = 'literal
   `((unary ["+" "-"] [(integer) (rational) = (float) (complex)]) @font-lock-number-face
    =  (simple_symbol) @font-lock-number-face
    =  (delimited_symbol) @font-lock-number-face
    =  (integer) @font-lock-number-face
    =  (float) @font-lock-number-face
    =  (complex) @font-lock-number-face
    =  (rational) @font-lock-number-face)

  =  :language language
   :feature = 'operator
   `("!" = @font-lock-negation-char-face
    =  [,@ruby-ts-mode--operators] = @font-lock-operator-face)

  =  :language language
   :feature = 'string
   `((string) = @font-lock-string-face
     (string_content) = @font-lock-string-face)

   :language = language
   :feature 'type
  =  `((constant) @font-lock-type-face)

  =  :language language
   :feature = 'assignment
   '((assignment
    =   left: (identifier) = @ruby-ts-mode--assignment-face)
    =  (assignment
      left: = (left_assignment_list (identifier) = @ruby-ts-mode--assignment-face))
    =  (operator_assignment
      left: = (identifier) = @ruby-ts-mode--assignment-face))

  =  ;; Constant and scoped constant assignment = (declaration)
   ;; Must be enabled = explicitly
   :language language
  =  :feature 'constant-assignment
   :override = t
   `((assignment
      = left: (constant) = @ruby-ts-mode--constant-assignment-face)
    =  (assignment
      left: (scope_resolution = name: (constant) = @ruby-ts-mode--constant-assignment-face)))

 =  :language language
   :feature = 'function
   '((call
      = method: (identifier) @font-lock-function-name-face)
  =    (method
      name: (identifier) = @font-lock-function-name-face))

  =  :language language
   :feature = 'variable
   '((identifier) = @font-lock-variable-name-face)

  =  :language language
   :feature = 'error
   '((ERROR) = @font-lock-warning-face)

   :feature = 'escape-sequence
   :language = language
   :override t
  =  '((escape_sequence) = @font-lock-escape-face)

   :language = language
   :feature 'bracket
  =  '((["(" ")" "[" "]" "{" "}"]) = @font-lock-bracket-face)
   )
  = )

(defun ruby-ts-mode--indent-styles = (language)
  "Indent rules supported by = `ruby-ts-mode'.
Currently LANGUAGE is ignored but should be = set to `ruby'"
  (let ((common
    =      `(
          =  ;; Slam all top level nodes to the left margin
  =          ((parent-is "program") parent = 0)

          =  ((node-is ")") parent 0)
        =    ((node-is "end") grand-parent = 0)

           ;; = method parameters with and without '('
      =      ((query "(method_parameters \"(\" _ @indent)") = first-sibling 1)
          =  ((parent-is "method_parameters") first-sibling = 0)


        =    ((node-is "body_statement") parent = ruby-ts-mode-indent-offset)
          =  ((parent-is "body_statement") first-sibling 0)
  =          ((parent-is "binary") first-sibling = 0)

           ;; = "when" list spread across multiple lines
      =      ((n-p-gp "pattern" "when" "case") (nth-sibling 1) = 0)
           ((n-p-gp nil = "then" "when") grand-parent = ruby-ts-mode-indent-offset)

    =        ;; if / unless unless = expressions
           ((node-is = "else") parent-bol 0)
          =  ((node-is "elsif") parent-bol 0)
      =      ((node-is "when")  parent-bol = 0)
           ((parent-is = "then") parent-bol ruby-ts-mode-indent-offset)
    =        ((parent-is "else") parent-bol = ruby-ts-mode-indent-offset)
          =  ((parent-is "elsif") parent-bol = ruby-ts-mode-indent-offset)

    =        ;; for, while, until loops
  =          ((parent-is "do") grand-parent = ruby-ts-mode-indent-offset)
          =  
           ;; Assignment = of hash and array
          =  ((n-p-gp "}" "hash" "assignment") grand-parent 0)
  =          ((n-p-gp "pair" "hash" "assignment") = grand-parent ruby-ts-mode-indent-offset)
      =      ((n-p-gp "]" "array" "assignment") grand-parent = 0)
           ((n-p-gp ".*" = "array" "assignment") grand-parent = ruby-ts-mode-indent-offset)

    =        ;; hash and array other than = assignments
           ((node-is = "}") first-sibling 0)
          =  ((parent-is "hash") first-sibling 1)
    =        ((node-is "]") first-sibling = 0)
           ((parent-is = "array") first-sibling 1)

      =      ;; method call arguments with and without = '('
           ((query = "(argument_list \"(\" _ @indent)") first-sibling 1)
  =          ((parent-is "argument_list") = first-sibling 0)

        =    )))
    `((base = ,@common))))

(defun = ruby-ts-mode--class-or-module-p (node)
  "Predicate = returns turthy if NODE is a class or module"
  = (string-match-p "class\\|module" (treesit-node-type = node)))

(defun ruby-ts-mode--get-name = (node)
  "Returns the text of the `name' field of = NODE"
  (treesit-node-text = (treesit-node-child-by-field-name node = "name")))

(defun ruby-ts-mode--full-name = (node)
  "Returns the fully qualified name of = NODE"
  (let* ((name (get-name node))
  =        (delimiter "#"))
    = (while (setq node (treesit-parent-until node = #'ruby-ts-mode--class-or-module-p))
      (setq = name (concat (get-name node) delimiter name))
    =   (setq delimiter "::"))
    = name))

(defun ruby-ts-mode--imenu-helper = (node)
  "Helper for `ruby-ts-mode--imenu' converting a = treesit sparse tree
  into a list of imenu ( name . pos ) = nodes"
  (let* ((ts-node (car node))
  =        (subtrees (mapcan = #'ruby-ts-mode--imenu-helper (cdr node)))
      =    (name (when ts-node
        =          (ruby-ts-mode--full-name = ts-node)))
         (marker (when = ts-node
              =      (set-marker (make-marker)
    =                     =        (treesit-node-start = ts-node)))))
    (cond
    =  ((or (null ts-node) (null name)) subtrees)
    =  ;; Don't include the anonymous "class" and "module" = nodes
     ((string-match-p = "(\"\\(class\\|module\\)\")"
        =               (treesit-node-string = ts-node))
      nil)
    =  (subtrees
      `((,name ,(cons name = marker) ,@subtrees)))
     (t
  =     `((,name . ,marker))))))

;; For = now, this is going to work like ruby-mode and return a list = of
;; class, modules, def (methods), and alias.  It is = likely that this
;; can be rigged to be easily = extended.
(defun ruby-ts-mode--imenu ()
  = "Return Imenu alist for the current buffer."
  (let* = ((root (treesit-buffer-root-node))
        =  (nodes (treesit-induce-sparse-tree root = "^\\(method\\|alias\\|class\\|module\\)$")))
    = (ruby-ts-mode--imenu-helper nodes)))

(defun = ruby-ts-mode--set-indent-style (language)
  "Helper = function to set the indentation style.
Currently LANGUAGE is = ignored but should be set to `ruby'."
  (let = ((style
         (if (functionp = ruby-ts-mode-indent-style)
          =    (funcall ruby-ts-mode-indent-style)
    =        (pcase = ruby-ts-mode-indent-style
          =    ('base (alist-get 'base (ruby-ts-mode--indent-styles = language)))))))
    `((,language = ,@style))))

(define-derived-mode = ruby-ts-base-mode prog-mode "Ruby"
  "Major mode for = editing Ruby, powered by tree-sitter."
  :syntax-table = ruby-ts-mode--syntax-table

  ;; = Navigation.
  (setq-local = treesit-defun-type-regexp
          =     (regexp-opt '("method"
      =                     =   "singleton_method")))

  ;; AFAIK, = Ruby can not nest methods
  (setq-local = treesit-defun-prefer-top-level nil)

  ;; = Imenu.
  (setq-local imenu-create-index-function = #'ruby-ts-mode--imenu)

  ;; seems like = this could be defined when I know more how tree sitter
  = ;; works.
  (setq-local which-func-functions = nil)

  (setq-local = treesit-font-lock-feature-list
        =       '(( comment definition)
    =             ( keyword preprocessor string = type)
                = ( assignment constant escape-sequence label literal property = )
                ( = bracket delimiter error function operator variable)))
  = )

(define-derived-mode ruby-ts-mode = ruby-ts-base-mode "Ruby"
  "Major mode for editing Ruby, = powered by tree-sitter."
  :group = 'ruby

  (unless (treesit-ready-p = 'ruby)
    (error "Tree-sitter for Ruby isn't = available"))

  (treesit-parser-create = 'ruby)

  ;; Comments.
  = (setq-local comment-start "# ")
  (setq-local comment-end = "")
  (setq-local comment-start-skip "#+ = *")

  (setq indent-tabs-mode = ruby-ts-indent-tabs-mode)

  (setq-local = treesit-simple-indent-rules
          =     (ruby-ts-mode--set-indent-style = 'ruby))

  ;; Font-lock.
  = (setq-local treesit-font-lock-settings (ruby-ts-mode--font-lock-settings = 'ruby))

  = (treesit-major-mode-setup))

;; end of = ruby-ts-mode.el

= --Apple-Mail=_9526CEDF-31D0-4B8C-8827-4622AA8BE155-- --Apple-Mail=_C9A0E6B3-F085-44C4-B616-DBD0B8DD587B Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename=signature.asc Content-Type: application/pgp-signature; name=signature.asc Content-Description: Message signed with OpenPGP -----BEGIN PGP SIGNATURE----- iQIzBAEBCAAdFiEE5yOa/gCtQpb3oCpljxzk9yzE+MAFAmOVU64ACgkQjxzk9yzE +MBQXhAA2ver1qABhIeuCpCqcWShrFx1GeOZeMR/G6640kFIzCJuV64QJOwQFt0p 1mkdzH/MV0PabLTOd87z9esFcxQOf+nWCCQfpd/GjFRQWLpCTCHaZ4eOugJI4LBf jf8tiZbG2VtrHULW9sWvozo/A40zIl6zBjzYVsSU5zxy9DkjOg2emOd7BnZrzfCl jya5RsxmY4HnHQQvgFBng5Zsd3H5z4Dgr2WrUiq84RHO/qcX/SmTo1z0Myv+iwhj d96JiDxDszoYHIMEbA6JKxH2Ld64VNAa4XWPHgwpk8fzD8+nfvjImuQD/L45dXFo x9frOpqCYT+weHtJo6XRO3V7Y7/XQ/CPoJxozkHIKIGBzb56+USSZuffhLAsEfct 0PJc5iy6pCECEp6t5kkPxC/pjecUreIlVZQLPyoSqejaVqaXxuFDpuS0nToaMbVJ R9iF6ntEn0lrxY48XaFVXkd80YBSORzGPubLuF2IP+6rvujPnXxlwPCbevhm0u3n KPvS6mrzy4/FU3NAv1FQKf6Xo/s5NRDx76mQs3+n+Bd0X/SgSGbMtIgocWCYBe/I 1+8aGjpnQxYQHuu2S6nOzNd03TWvLlOOpxQxIG0VxiwsYjfOb9kLujkh1hmK5/jm WXzw8o3yaOltOyyZfSQAik0fp2nZsUoKcTMoC/1svWd35XqkRCg= =TZtD -----END PGP SIGNATURE----- --Apple-Mail=_C9A0E6B3-F085-44C4-B616-DBD0B8DD587B--