%YAML 1.2
---
name: ReScript
file_extensions: [res, resi]
scope: source.res

variables:
  # copied over from reason-highlightjs
  # TODO: \"foo" ident
  RE_IDENT: '[a-z_][0-9a-zA-Z_]*'
  RE_ATTRIBUTE: '[A-Za-z_][A-Za-z0-9_\.]*'
  RE_MODULE_IDENT: '[A-Z_][0-9a-zA-Z_]*'

  # slightly different than reason-highlightjs
  RE_KEYWORDS:
    \b(and|as|assert|begin|class|constraint|done|downto|exception|external|fun|functor|inherit|lazy|let|pub|mutable|new|nonrec|object|of|or|pri|rec|then|to|val|virtual|try|catch|finally|do|else|for|if|switch|while|import|library|export|module|in|raise|private)\b
  RE_LITERAL: \b(true|false)\b

contexts:

  commentLine:
    - match: //
      scope: punctuation.definition.comment
      push:
        - meta_scope: comment.line
        - match: \n
          pop: true

  commentBlock:
    - match: /\*
      scope: punctuation.definition.comment.begin
      push:
        - meta_scope: comment.block
        - match: \*/
          scope: punctuation.definition.comment.end
          pop: true

  punctuations:
    - match: ~
      scope: punctuation.definition.keyword
    - match: ;
      scope: punctuation.terminator
    - match: \.
      scope: punctuation.accessor
    - match: \,
      scope: punctuation.separator
    # covers ternary. Wrong category but whatever
    - match: '\?|:'
      scope: punctuation.separator
    - match: \|(?!\|)
      scope: punctuation.separator
    - match: \{
      scope: punctuation.section.braces.begin
    - match: \}
      scope: punctuation.section.braces.end
    - match: \(
      scope: punctuation.section.parens.begin
    - match: \)
      scope: punctuation.section.parens.end

  storage:
    - match: \btype\b
      scope: storage.type

  keyword:
    - match: '{{RE_KEYWORDS}}'
      scope: keyword

  constant:
    - match: '{{RE_LITERAL}}'
      scope: constant.language

  string:
    - match: '"'
      scope: punctuation.definition.string.begin
      push:
      - meta_scope: string.quoted.double
      - match: \\.
        scope: constant.character.escape
      - match: '"'
        scope: punctuation.definition.string.end
        pop: true
    - match: '({{RE_IDENT}})?(`)'
      captures:
        1: variable.annotation
        2: punctuation.definition.string.begin
      push:
        # different than reason-highlightjs, for now
        - meta_scope: string.quoted.other
        - match: '`'
          scope: punctuation.definition.string.end
          pop: true

  function:
    - match: =>
      # first one is apparently for backward-compat
      scope: storage.type.function keyword.declaration.function

  character:
    - match: '''[\x00-\x7F]'''
      scope: string.quoted.single

  number:
    - match:
        \b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\.[0-9_]+)?([eE][-+]?[0-9_]+)?)?)\b
      scope: constant.numeric

  operator:
    - match:
        '->|\|\||&&|\+\+|\*\*|\+\.|\+|-\.|-|\*\.|\*|/\.|/|\.\.\.|\.\.|\|>|===|==|\^|:=|!|>=|<='
      scope: keyword.operator

  assignment:
    - match: '='
      scope: keyword.operator.assignment

  # doesn't contain ` unlike reason-highlightjs
  constructor:
    - match: '\b[A-Z][0-9a-zA-Z_]*\b'
      # won't mark and highlight this for now. This often shares the same
      # highlighting as entity.name.namespace (our module). We don't want
      # variant and modules confused
      # scope: entity.name.union
      scope: variable.function variable.other
    - match: '(#)[a-zA-Z][0-9a-zA-Z_]*\b'
      captures:
        1: punctuation.definition.keyword

  array:
    - match: '\['
      scope: punctuation.section.brackets.begin
    - match: '\]'
      scope: punctuation.section.brackets.end

  list:
    - match: '\b(list)(\{)'
      captures:
        1: keyword
        2: punctuation.section.braces.begin
    - match: '\}'
      scope: punctuation.section.braces.end

  objectAccess:
    - match: '\b{{RE_IDENT}}(\[)'
      captures:
        # technically this should be punctuation.accessor. But we don't wrap
        # the section, so we'll just conform to list and array brackets
        1: punctuation.section.brackets.begin
    - match: '\]'
      scope: punctuation.section.brackets.end

  attribute:
    - match: '@@?'
      scope: storage.modifier punctuation.definition.annotation
      push: attributeContent
    - match: '%%?'
      scope: storage.modifier punctuation.definition.annotation
      push: attributeContent
  attributeContent:
    - meta_scope: meta.annotation
    # - match: '{{RE_ATTRIBUTE}} *\('
    #   scope: variable.annotation
    #   push:
    #     - match: \)
    #     pop: true
    - match: '{{RE_ATTRIBUTE}}'
      scope: variable.annotation
      pop: true

  jsx:
    - match: '<>|</>|/>'
    - match: '</({{RE_MODULE_IDENT}})'
      captures:
        1: entity.name.namespace
      push: moduleAccessEndsWithModuleThenPop
    - match: '</({{RE_IDENT}})'
    # overloaded: if a <Foo.bar
    # fortunately, still the right category
    - match: '<({{RE_MODULE_IDENT}})'
      captures:
        1: entity.name.namespace
      push: moduleAccessEndsWithModuleThenPop

  openOrIncludeModule:
    - match: '\b(open|include)\s*'
      scope: keyword
      push: moduleAccessEndsWithModuleThenPop

  # Foo.Bar.Baz where Baz is actually a module, not a constructor
  moduleAccessEndsWithModule:
    - match: '{{RE_MODULE_IDENT}}'
      scope: entity.name.namespace
    - match: '(\.)({{RE_MODULE_IDENT}})'
      captures:
        1: punctuation.accessor
        2: entity.name.namespace

  moduleAccessEndsWithModuleThenPop:
    - include: moduleAccessEndsWithModule
    - match: '(?=\S)'
      pop: true

  moduleAccess:
    - match: '\b({{RE_MODULE_IDENT}})(\.)'
      captures:
        1: entity.name.namespace
        2: punctuation.accessor

  functorParameter:
    - match: \(
      push: functorParameter
      scope: punctuation.section.braces.begin
    - match: \)
      scope: punctuation.section.braces.end
      pop: true
    - include: moduleAccessEndsWithModule
    - include: main

  moduleRHS:
    - include: functorParameter
    # this is what makes us bail out of the current stack
    # module Foo = (Bar: Baz) => (Bar: Baz) => List
    # Bar Bar
    # Bar
    # let a = Bar
    #     ^ bail! Now this is bailing way too late, but that's ok for now
    - match: '(?=\S)'
      pop: true

  moduleDeclaration:
    - match: '\b(module)\s+(type\s+)?(of\s+)?({{RE_MODULE_IDENT}})'
      captures:
        1: keyword
        2: keyword
        3: keyword
        4: entity.name.namespace
      push:
        # and then an optional type signature is matched. Hopefully this regex
        # doesn't accidentally match something else
        - match: '\s*:\s*({{RE_MODULE_IDENT}})'
          captures:
            1: entity.name.namespace
        - match: '\s*:\s*(\{)'
          captures:
            1: punctuation.section.braces.begin
          push: moduleInner
        - match: '='
          set: moduleRHS
        - match: '(?=\S)'
          pop: true

  moduleInner:
    - match: \}
      scope: punctuation.section.braces.end
      pop: true
    - include: main

  main:
    - include: storage
    - include: constant
    # these below are basically like reason-highlightjs
    - include: commentLine
    - include: commentBlock
    - include: character
    - include: string
    - include: attribute
    - include: function
    - include: list
    - include: objectAccess
    - include: array
    - include: jsx
    - include: operator
    - include: assignment
    - include: number
    - include: openOrIncludeModule
    - include: moduleDeclaration
    - include: moduleAccess
    - include: constructor
    # this is different than reason-highlightjs too
    - include: keyword
    - include: punctuations