[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: ruby-ts-mode.el -- first draft
From: |
Theodor Thornhill |
Subject: |
Re: ruby-ts-mode.el -- first draft |
Date: |
Sun, 11 Dec 2022 09:22:28 +0100 |
Perry Smith <pedz@easesoftware.com> writes:
> 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:
>
[...]
> (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)
I believe we decided against using this indent style technique unless we
had specific styles to show. A user could just:
(add-hook 'ruby-mode-hook
(lambda ()
(setq treesit-simple-indent-rules
my-personal-ruby-indent-rules)))
to override the current default anyway.
>
> (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)
This can be removed.
>
> (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)
Are you sure we need these very specific faces? Can't we reuse any of
the provided ones?
> ;; 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)
> )
> )
Tuck these end-parens up together with the third to last line.
>
> (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))))
Just return the common when the indent style is removed :)
>
> (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)))
>
Are you sure we don't want more granularity than this? Why is
everything in the same regexp?
> (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))))
>
Remove this when indent style is removed.
> (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
Also when this is ready, also add an entry to the NEWS file along with
an update to the build script in 'admin/notes/tree-sitter/build-module/'
so that we can get the ruby language installed easily!
Thanks for your effort!
Theo