%YAML 1.2
---
#
# G-Code (RepRap, not ISO-6983)
# Author: @thinkyhead
#
# RapRap G-Code is very simple.
#
# TODO: Lines that start with N get a different context, accepting a checksum.
#
name: G-Code (RepRap)
file_extensions:
  - [ g, gco, gcode ]
scope: source.gcode
variables:
  decimal: '[+-]?\d+(\.\d*)?'

contexts:
  prototype:
    - match: \s+

    - include: mixin_comment

    - match: $
      pop: true

  main:
    - meta_scope: line.gcode

    - match: '([Nn]\s*(\d+))'
      captures:
        1: entity.nword.gcode
        2: constant.numeric.line-number.gcode

    - match: ()
      set: gcode_command

  # G, M, or T command
  gcode_command:
    - meta_content_scope: ctx.command.gcode

    # M20 S2 P/path/to/file/name.gco
    - match: ([Mm](20))\s*((S)(2)\s*(P))
      captures:
        1: entity.command.gcode markup.bold.gcode
        2: constant.numeric.command.gcode
        3: ctx.params.gcode
        4: keyword.param.gcode
        5: constant.numeric.param.gcode
        6: keyword.param.gcode
      set: gcode_string_arg

    # M117 or M118 - Commands taking a string
    - match: ([Mm]\s*(11[78]))\b
      captures:
        1: entity.command.gcode markup.bold.gcode
        2: constant.numeric.command.gcode
      set: gcode_string_arg

    # Other commands, followed by data
    - match: ([GMTgmt]\s*(\d+)((\.)(\d+))?)
      captures:
        1: entity.command.gcode markup.bold.gcode
        2: constant.numeric.command.gcode
        4: entity.separator.subcode
        5: constant.numeric.subcode
      set: gcode_params

    - match: ()
      set: syntax_error

  # Parameters of a command
  gcode_params:
    - meta_content_scope: ctx.params.gcode

    # M32 [S<pos>] [P<bool>] !/path/file.gco#
    - match: \!
      scope: punctuation.string.path.open.gcode
      push: gcode_path_arg

    # asterisk starts a checksum
    - match: \*
      scope: punctuation.marker.checksum.gcode
      set: gcode_checksum

    # parameter and single-quoted value
    - match: ([A-Za-z])\s*(')
      captures:
        1: keyword.param.gcode
        2: punctuation.quote.single.open.gcode
      push: gcode_string_arg_quoted_single

    # parameter and double-quoted value
    - match: ([A-Za-z])\s*(")
      captures:
        1: keyword.param.gcode
        2: punctuation.quote.double.open.gcode
      push: gcode_string_arg_quoted_double

    # parameter and list of values
    - match: ([Ee])\s*(({{decimal}}\s*:\s*)+{{decimal}})
      captures:
        1: keyword.param.gcode
        2: constant.numeric.param.gcode

    # parameter and numeric value
    - match: ([A-Za-z])\s*({{decimal}})
      captures:
        1: keyword.param.gcode
        2: constant.numeric.param.gcode

    # parameter with no value
    - match: '[A-Za-z]'
      scope: keyword.param.gcode
      set: gcode_params

    - match: ()
      set: syntax_error

  gcode_string_arg_quoted_single:
    - meta_content_scope: string.quoted.single.gcode

    - match: ([^'\\]+)

    - match: (\\)
      scope: punctuation.string.escape.gcode
      push: escape_char

    - match: (')
      scope: punctuation.quote.single.close.gcode
      pop: true

    - match: ()
      set: syntax_error

  gcode_string_arg_quoted_double:
    - meta_content_scope: string.quoted.double.gcode

    - match: ([^"\\]+)

    - match: (\\)
      scope: punctuation.string.escape.gcode
      push: escape_char

    - match: (")
      scope: punctuation.quote.double.close.gcode
      pop: true

    - match: ()
      set: syntax_error

  gcode_string_arg:
    - meta_content_scope: ctx.string.gcode

    - match: ([^;]+)
      scope: string.unquoted.gcode

  escape_char:
    - meta_scope: string.escape.gcode punctuation.string.escape.gcode

    - match: '.'
      pop: true

  gcode_path_arg:
    - meta_content_scope: string.unquoted.path.gcode

    - match: (#)
      scope: punctuation.string.path.close.gcode
      pop: true

  gcode_checksum:
    - meta_content_scope: constant.numeric.checksum.gcode
    - meta_include_prototype: false

    - match: \d+

    - match: ( *)$
      pop: true

    - include: mixin_comment

    - match: ()
      set: syntax_error

  # Done interpreting to the end of the line
  gcode_line_done:
    - match: \s*$
      pop: true

  # Comments begin with a ';' and finish at the end of the line.
  mixin_comment:
    - match: ^\s*;
      scope: punctuation.comment.line.start
      set: gcode_comment
    - match: \s*;
      scope: punctuation.comment.eol.start
      set: gcode_comment
    - match: \s*\(
      scope: punctuation.paren.comment.open
      push: gcode_comment_paren

  # Comment to end of line.
  gcode_comment:
    - meta_content_scope: comment.gcode
    - match: \s*$
      pop: true

  gcode_comment_paren:
    - meta_content_scope: paren.comment.gcode

    - match: '[^)]+'

    - match: '[)]'
      scope: punctuation.paren.comment.close
      pop: true

    - match: \s*$
      pop: true

  # Everything after this point is broken by a syntax error
  syntax_error:
    - meta_scope: invalid.error.syntax.gcode

    - match: .*$
      pop: true