diff --git a/tree-sitter-mlf/queries/highlights.scm b/tree-sitter-mlf/queries/highlights.scm index 3fe2781..436c682 100644 --- a/tree-sitter-mlf/queries/highlights.scm +++ b/tree-sitter-mlf/queries/highlights.scm @@ -111,6 +111,13 @@ "*" ] @operator +; The `!` marker. Context gives it its meaning: +; - On a field/parameter: required-field marker +; - Inside a union: closed-union marker +(field "!" @keyword.operator) +(parameter "!" @keyword.operator) +(union_type "!" @keyword.operator) + ; Delimiters [ "(" @@ -126,3 +133,12 @@ ";" "." ] @punctuation.delimiter + +; Annotation object literal keys (the `"name"` in `{ "name": value }`) +(annotation_object_entry + key: (string) @property) + +; Backtick-escaped identifiers — flag visually so authors can see when +; a reserved word is being used as an identifier. +((identifier) @string.special + (#match? @string.special "^`.*`$")) diff --git a/website/syntaxes/mlf.sublime-syntax b/website/syntaxes/mlf.sublime-syntax index e5851d5..b716e84 100644 --- a/website/syntaxes/mlf.sublime-syntax +++ b/website/syntaxes/mlf.sublime-syntax @@ -10,11 +10,18 @@ contexts: - include: comments - include: annotations - include: keywords + - include: backtick_identifiers - include: types - include: strings - include: numbers - include: operators + # Reserved words can be used as identifiers by wrapping in backticks. + # Highlight them distinctly so the escape is visually obvious. + backtick_identifiers: + - match: '`[^`]+`' + scope: string.other.identifier.mlf + comments: - match: '///' scope: punctuation.definition.comment.mlf @@ -36,53 +43,93 @@ contexts: captures: 1: entity.name.namespace.mlf 2: entity.name.function.annotation.mlf - push: - - match: '\(' - scope: punctuation.section.arguments.begin.mlf - set: - - meta_scope: meta.annotation.arguments.mlf - - match: '\)' - scope: punctuation.section.arguments.end.mlf - pop: true - - match: '([a-zA-Z_][a-zA-Z0-9_]*)\s*(:)' - captures: - 1: variable.parameter.mlf - 2: punctuation.separator.mlf - - include: strings - - include: numbers - - match: '\b(true|false)\b' - scope: constant.language.mlf - - match: ',' - scope: punctuation.separator.mlf - - match: '(?=\S)' - pop: true + push: maybe_annotation_args # Bare annotation with args: @deprecated(true) - match: '@([a-zA-Z_][a-zA-Z0-9_]*)' scope: meta.annotation.mlf captures: 1: entity.name.function.annotation.mlf - push: - - match: '\(' - scope: punctuation.section.arguments.begin.mlf - set: - - meta_scope: meta.annotation.arguments.mlf - - match: '\)' - scope: punctuation.section.arguments.end.mlf - pop: true - - match: '([a-zA-Z_][a-zA-Z0-9_]*)\s*(:)' - captures: - 1: variable.parameter.mlf - 2: punctuation.separator.mlf - - include: strings - - include: numbers - - match: '\b(true|false)\b' - scope: constant.language.mlf - - match: ',' - scope: punctuation.separator.mlf - - match: '(?=\S)' + push: maybe_annotation_args + + # After an annotation head, an opening `(` starts the argument list. + # Any other non-space token means the annotation was bare — bail out. + maybe_annotation_args: + - match: '\(' + scope: punctuation.section.arguments.begin.mlf + set: + - meta_scope: meta.annotation.arguments.mlf + - match: '\)' + scope: punctuation.section.arguments.end.mlf pop: true + # Named argument: name: + - match: '([a-zA-Z_][a-zA-Z0-9_]*)\s*(:)' + captures: + 1: variable.parameter.mlf + 2: punctuation.separator.mlf + - include: annotation_value + - match: ',' + scope: punctuation.separator.mlf + - match: '(?=\S)' + pop: true + + # Annotation values: string, number, bool, null, type path, array, object. + # Used inside annotation args, array items, and object entry values. + annotation_value: + - include: strings + - include: numbers + - match: '\b(true|false|null)\b' + scope: constant.language.mlf + - match: '\[' + scope: punctuation.section.brackets.begin.mlf + push: annotation_array_body + - match: '\{' + scope: punctuation.section.braces.begin.mlf + push: annotation_object_body + # Type path: dotted identifier (e.g. `com.example.foo.Bar`). Match + # after strings/numbers so plain keywords and bracket tokens win first. + - match: '\b[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)+\b' + scope: support.type.mlf + + annotation_array_body: + - match: '\]' + scope: punctuation.section.brackets.end.mlf + pop: true + - include: annotation_value + - match: ',' + scope: punctuation.separator.mlf + + annotation_object_body: + - match: '\}' + scope: punctuation.section.braces.end.mlf + pop: true + # Object key — any string literal followed by `:` + - match: '(")((?:\\.|[^"\\])*)(")\s*(:)' + captures: + 1: punctuation.definition.string.begin.mlf + 2: string.quoted.double.mlf entity.name.tag.mlf + 3: punctuation.definition.string.end.mlf + 4: punctuation.separator.mlf + - include: annotation_value + - match: ',' + scope: punctuation.separator.mlf keywords: + # `error` keyword — when followed by `{`, begin an error-block context + # so error names inside render as constants. + - match: '\b(error)\b\s*(\{)' + captures: + 1: keyword.control.mlf + 2: punctuation.section.block.begin.mlf + push: + - meta_scope: meta.error-block.mlf + - match: '\}' + scope: punctuation.section.block.end.mlf + pop: true + - include: comments + - match: '\b[a-zA-Z_][a-zA-Z0-9_]*\b' + scope: constant.other.mlf + - match: ',' + scope: punctuation.separator.mlf - match: '\b(namespace|use|as|record|inline|def|type|token|query|procedure|subscription|throws|constrained|error|self)\b' scope: keyword.control.mlf - match: '\b(main|defs)\b'