Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion book/src/generated/lang-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
| lua-format-string | ✓ | | | | | |
| luap | ✓ | | | | | |
| luau | ✓ | ✓ | ✓ | | | `luau-lsp` |
| mail | ✓ | ✓ | | | ✓ | |
| mail | ✓ | ✓ | | | ✓ | |
| make | ✓ | | ✓ | | | |
| markdoc | ✓ | | | | | `markdoc-ls` |
| markdown | ✓ | | | ✓ | | `marksman`, `markdown-oxide`, `rumdl` |
Expand Down
6 changes: 3 additions & 3 deletions languages.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2094,13 +2094,13 @@ source = { git = "https://github.com/Flakebi/tree-sitter-tablegen", rev = "3e9c4

[[language]]
name = "mail"
scope = "text.mail"
file-types = ["eml"]
scope = "source.mail"
file-types = ["eml", "email"]
injection-regex = "mail|eml|email"

[[grammar]]
name = "mail"
source = { git = "https://codeberg.org/ficd/tree-sitter-mail", rev = "5eddbfdbec4c893182c79047499901c196332e78" }
source = { git = "https://codeberg.org/philocalyst/MailGrammar", rev = "631ce96da3fcf6a817f599d8667ef985438f5d36" }

[[language]]
name = "markdown"
Expand Down
18 changes: 18 additions & 0 deletions runtime/queries/mail/folds.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
; Fold the entire header block.
(header) @fold

; Fold long recipient lists and individual Received: trace headers.
(address_list) @fold
(received_field) @fold

; Fold MIME sections independently.
(mime_part) @fold
(part_headers) @fold
(part_body) @fold
(multipart_body) @fold

; Fold body paragraphs, quote groups, and signatures.
(body_block) @fold
(quote_group) @fold
(quoted_block) @fold
(signature) @fold
175 changes: 149 additions & 26 deletions runtime/queries/mail/highlights.scm
Original file line number Diff line number Diff line change
@@ -1,28 +1,151 @@
; header fields
[
(header_field_email)
(header_field_subject)
(header_field)
] @keyword

; delimited punctuation
(header_separator) @punctuation.delimiter
(email_delimiter) @punctuation.delimiter

; email subject contents
(header_subject
(subject) @markup.bold)
; extra metadata headers
(header_other
(header_unstructured) @comment)

; Addressee Name (Firstname, Lastname, etc.)
(atom) @variable

; Email Address
(email) @string

; Quoted Reply
(quote_marker) @punctuation.special
(ERROR) @error
(malformed_line) @error

(address_field name: (field_name) @constant)
(date_field name: (field_name) @constant)
(message_id_field name: (field_name) @constant)
(in_reply_to_field name: (field_name) @constant)
(references_field name: (field_name) @constant)
(received_field name: (field_name) @constant)
(return_path_field name: (field_name) @constant)
(subject_field name: (field_name) @constant)
(comments_field name: (field_name) @constant)
(content_type_field name: (field_name) @constant)
(content_transfer_encoding_field name: (field_name) @constant)
(content_disposition_field name: (field_name) @constant)
(mime_version_field name: (field_name) @constant)
(content_description_field name: (field_name) @constant)

; Covers non-ASCII field names as well, which the grammar degrades to
; optional_field
(optional_field name: (field_name) @property)

(subject_field value: (unstructured) @markup.heading)
(subject_field prefix: (reply_forward_marker) @comment)

(comments_field value: (unstructured) @string)
(content_description_field value: (unstructured) @string)
(optional_field value: (unstructured) @string)
(content_type_field value: (unstructured) @string)
(mime_version_field value: (unstructured) @string)

; Obsolete forms (2-digit year, named/military timezones like GMT) share
; the same `year`/`zone` node types as their modern equivalents.
(date_time) @string.special
(day_of_week) @string.special
(day) @string.special
(month) @string.special
(year) @string.special
(hour) @string.special
(minute) @string.special
(second) @string.special
(zone) @string.special

(msg_id) @string.special
(msg_id "<" @punctuation.bracket)
(msg_id ">" @punctuation.bracket)
(msg_id "@" @punctuation.delimiter)

; addr_spec (local@domain) captured without surrounding <> brackets.
(addr_spec) @markup.link.url
(addr_spec "@" @punctuation.delimiter)

(domain_literal) @string.special

(angle_addr "<" @punctuation.bracket)
(angle_addr ">" @punctuation.bracket)

(mailbox display_name: (display_name) @string)
(group display_name: (display_name) @string)

(group ":" @punctuation.delimiter)
(group ";" @punctuation.delimiter)

(encoded_word) @constant

(mailbox_sep "," @punctuation.delimiter)

(route) @string.special

(return_path_value) @string.special

(mime_type) @type
(mime_subtype) @type
(content_type "/" @punctuation.delimiter)

(parameter_name) @property
(parameter_value) @string

; The quoted form (e.g. filename="report.pdf") needs its own capture.
(mime_parameter value: (quoted_string) @string)
(mime_parameter "=" @punctuation.delimiter)
(mime_parameter ";" @punctuation.special)
(mime_boundary_parameter "=" @punctuation.delimiter)
(mime_boundary_parameter ";" @punctuation.special)

(encoding_mechanism) @string.special
(disposition_type) @string.special
(mime_version) @string.special

; Highlight attachment filenames distinctly.
(content_disposition
type: (disposition_type) @_disposition_type
(mime_parameter
name: (parameter_name) @_param_name
value: (_) @label)
(#match? @_disposition_type "^[Aa][Tt][Tt][Aa][Cc][Hh][Mm][Ee][Nn][Tt]$")
(#match? @_param_name "^[Ff][Ii][Ll][Ee][Nn][Aa][Mm][Ee]"))

(dash_boundary) @punctuation.special
(close_delimiter) @punctuation.special

; Base case: every quote marker/line gets markup.quote regardless of depth.
(quote_marker) @markup.quote
(quote_contents) @markup.quote

; Apply progressively deeper quote captures through six nesting levels.
(quote_group
(quoted_block
[(quote_marker) (quote_contents)] @markup.quote.1))

(quote_group
(quoted_block
(quoted_block
[(quote_marker) (quote_contents)] @markup.quote.2)))

(quote_group
(quoted_block
(quoted_block
(quoted_block
[(quote_marker) (quote_contents)] @markup.quote.3))))

(quote_group
(quoted_block
(quoted_block
(quoted_block
(quoted_block
[(quote_marker) (quote_contents)] @markup.quote.4)))))

(quote_group
(quoted_block
(quoted_block
(quoted_block
(quoted_block
(quoted_block
[(quote_marker) (quote_contents)] @markup.quote.5))))))

(quote_group
(quoted_block
(quoted_block
(quoted_block
(quoted_block
(quoted_block
(quoted_block
[(quote_marker) (quote_contents)] @markup.quote.6)))))))

(signature_separator) @punctuation.special
(signature) @comment

(comment) @comment

(received_tokens) @string
1 change: 1 addition & 0 deletions runtime/queries/mail/indents.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
; Email is not an indented language.
64 changes: 64 additions & 0 deletions runtime/queries/mail/injections.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
; Inject MIME parts based on their Content-Type subtype. A subtype that does
; not correspond to a configured Helix language is left uninterpreted.
(mime_part
(part_headers
(content_type_field
value: (content_type
subtype: (mime_subtype) @injection.language)))
(part_body) @injection.content
(#not-any-of? @injection.language "plain" "md" "x-markdown" "rtf" "x-rtf"))

; MIME aliases whose subtype does not match Helix's language name.
(mime_part
(part_headers
(content_type_field
value: (content_type
subtype: (mime_subtype) @_subtype)))
(part_body) @injection.content
(#any-of? @_subtype "md" "x-markdown")
(#set! injection.language "markdown"))

(mime_part
(part_headers
(content_type_field
value: (content_type
subtype: (mime_subtype) @_subtype)))
(part_body) @injection.content
(#any-of? @_subtype "rtf" "x-rtf")
(#set! injection.language "rtf"))

; Diff-bearing plain-text bodies receive diff highlighting; other text bodies
; are rendered as Markdown.
([
(body_block)
(epilogue)
] @injection.content
(#match? @injection.content "diff --git ")
(#set! injection.language "diff"))

([
(body_block)
(epilogue)
] @injection.content
(#not-match? @injection.content "diff --git ")
(#set! injection.language "markdown"))

(mime_part
(part_headers
(content_type_field
value: (content_type
subtype: (mime_subtype) @_subtype)))
(part_body) @injection.content
(#match? @_subtype "^[Pp][Ll][Aa][Ii][Nn]$")
(#match? @injection.content "diff --git ")
(#set! injection.language "diff"))

(mime_part
(part_headers
(content_type_field
value: (content_type
subtype: (mime_subtype) @_subtype)))
(part_body) @injection.content
(#match? @_subtype "^[Pp][Ll][Aa][Ii][Nn]$")
(#not-match? @injection.content "diff --git ")
(#set! injection.language "markdown"))
1 change: 1 addition & 0 deletions runtime/queries/mail/locals.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
; Mail does not have variable scoping.
9 changes: 9 additions & 0 deletions runtime/queries/mail/tags.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
; Threading headers provide useful definitions and references.
(message_id_field
value: (msg_id) @name) @definition.message

(in_reply_to_field
(msg_id) @name) @reference.reply

(references_field
(msg_id) @name) @reference.thread
64 changes: 55 additions & 9 deletions runtime/queries/mail/textobjects.scm
Original file line number Diff line number Diff line change
@@ -1,13 +1,59 @@
(atom_block
(atom) @entry.inside) @entry.around
(address_field
value: (address_list) @field.inner @entry.inside) @field.outer @entry.around

(email_address) @entry.around
(header_other
(header_unstructured) @entry.around)
(subject_field
value: (unstructured) @field.inner @entry.inside) @field.outer @entry.around

(quoted_block)+ @comment.around
(comments_field
value: (unstructured) @field.inner @entry.inside) @field.outer @entry.around

(body_block)+ @function.around
(date_field
value: (_) @field.inner @entry.inside) @field.outer @entry.around

(header_subject
(subject) @function.around)
(message_id_field
value: (msg_id) @field.inner @entry.inside) @field.outer @entry.around

(in_reply_to_field) @field.outer @entry.around
(references_field) @field.outer @entry.around

(received_field) @field.outer @entry.around

(return_path_field
value: (return_path_value) @field.inner @entry.inside) @field.outer @entry.around

(content_type_field
value: (_) @field.inner @entry.inside) @field.outer @entry.around

(content_transfer_encoding_field
value: (encoding_mechanism) @field.inner @entry.inside) @field.outer @entry.around

(content_disposition_field
value: (content_disposition) @field.inner @entry.inside) @field.outer @entry.around

(mime_version_field
value: (_) @field.inner @entry.inside) @field.outer @entry.around

(content_description_field
value: (unstructured) @field.inner @entry.inside) @field.outer @entry.around

(optional_field
value: (unstructured) @field.inner @entry.inside) @field.outer @entry.around

(comment) @comment.outer @comment.around
(comment) @comment.inner @comment.inside

(quote_group) @quote.outer @comment.outer @comment.around

(quoted_block
(quote_contents) @quote.inner @comment.inner @comment.inside)

(body
(signature_separator)
(signature) @signature.inner) @signature.outer

(mime_part
(part_body) @mime_part.inner) @mime_part.outer

(body) @body.outer
(body_block) @body.inner
(epilogue) @body.inner