diff --git a/.gitmodules b/.gitmodules index 327d470315..0c2ecc6681 100644 --- a/.gitmodules +++ b/.gitmodules @@ -123,3 +123,6 @@ [submodule "assets/syntaxes/varlink"] path = assets/syntaxes/varlink url = https://github.com/varlink/syntax-highlight-varlink.git +[submodule "assets/syntaxes/sublime-fish"] + path = assets/syntaxes/sublime-fish + url = https://github.com/Phidica/sublime-fish.git diff --git a/assets/create.sh b/assets/create.sh index 3965618339..602069f696 100755 --- a/assets/create.sh +++ b/assets/create.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -euo pipefail ASSET_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" diff --git a/assets/syntaxes/fish.sublime-syntax b/assets/syntaxes/fish.sublime-syntax new file mode 100644 index 0000000000..94aefcedfe --- /dev/null +++ b/assets/syntaxes/fish.sublime-syntax @@ -0,0 +1,1009 @@ +%YAML 1.2 +--- +# http://www.sublimetext.com/docs/3/syntax.html +name: friendly interactive shell (fish) +file_extensions: + - fish +first_line_match: '^#!.*\b(fish)|^#\s*-\*-[^*]*mode:\s*shell-script[^*]*-\*-' +scope: source.shell.fish +contexts: + main: + - include: comment-external + - include: line-continuation + - match: \)|end + comment: In an ideal world, command-call-standard would be performing this match because fish highlights the strings which follow as arguments. But we can't do that in a tmLanguage + push: + - meta_scope: meta.function-call.fish invalid.illegal.function-call.fish + - match: '(?=[\s;&)|<>])' + pop: true + - match: (?=\S) + comment: Anonymous scope - Base scope pipeline goes up until one of the definitive ends (newline and ';') or the sequences that could be an end if we're actually inside a $self scope right now (')' and "end") + push: + - match: \n|(;)|(?=\))|(?=end) + captures: + 1: meta.function-call.operator.fish keyword.operator.control.fish + pop: true + - match: '(?:[&|]|(?:[0-9]+)?(?:<|>>?|\^\^?))' + comment: Match operators ('&', pipe, and redirect) which cannot start a pipeline because they must be consumed within or after a pipeline + scope: invalid.illegal.operator.fish + - include: comment-internal-end + - match: (?=\S) + comment: The reason we match '&' here is because we explicitly require it come after a command (unlike ';' which can be alone on a line) + push: + - match: '(?=[\n;)])|(&)' + captures: + 1: meta.function-call.operator.fish keyword.operator.control.fish + pop: true + - include: pipeline + argument: + - match: '(?![\s;&)|<>^])' + comment: End arg if it precedes whitespace or operators (excluding stderr redirect '^' due to a fish quirk) + push: + - match: '(?=[\s;&)|<>])' + pop: true + - match: \% + comment: Process expansion only occurs if the '%' is at the front of the argument, and continues for the entire argument + captures: + 0: meta.string.unquoted.fish punctuation.definition.process.fish + push: + - meta_scope: meta.parameter.argument.process-expansion.fish + - match: '(?=[\s;&)|<>])' + pop: true + - match: '(?:self|last)(?=$|[\s;&)|<>])' + comment: Match special process names. By a convention that I'm making up, scope them as a type of variable + scope: meta.string.unquoted.fish variable.language.fish + - include: parameter-patterns + - match: '(?:[+-]?)(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?=$|[\s;&)|<>])' + comment: Treat a sequence of integers (with possible sign and decimal separator) as a standalone constant. Do this separate to the + scope: meta.parameter.argument.numeric.fish meta.string.unquoted.fish constant.numeric.fish + - match: '(?![\s($])' + comment: This scope can be used by plugins to locate arguments which don't *start* with command substitution or variable expansion and may directly resolve to file paths. Of course, they could have command substitution or variable expansion further on in them, but looking ahead for that too is nontrivial + push: + - meta_scope: meta.parameter.argument.path.fish + - match: '(?=[\s;&)|<>])' + pop: true + - match: \~ + comment: Home directory expansion only occurs if the '~' is at the front of the argument, so check it first + scope: meta.string.unquoted.fish keyword.operator.tilde.fish + - include: parameter-patterns + - match: (?!\s) + comment: Use standard parameter patterns for whatever doesn't match the above + push: + - meta_scope: meta.parameter.argument.fish + - match: '(?=[\s;&)|<>])' + pop: true + - include: parameter-patterns + command-call-meta: + - match: '(builtin|command|exec)\b(?!\s+[-&|])' + comment: These meta commands force the parameter to behave as a standard command. They stop at piping + captures: + 1: support.function.fish + push: + - match: |- + (?x) + (?# Look ahead for control operations after whitespace) + (?=\s* + (?: + (?# Find simple control operations) + [\n;&)] + | + (?# Find piping) + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + ) + ) + pop: true + - include: line-continuation + - include: command-call-standard + - match: '(not)\b(?!\s+[-&|])' + comment: This meta command acts as a unary operator on the command to the right, which can also be a meta command. It only applies to one command and stops at piping + captures: + 1: keyword.operator.word.fish + push: + - match: |- + (?x) + (?# Look ahead for control operations after whitespace) + (?=\s* + (?: + (?# Find simple control operations) + [\n;&)] + | + (?# Find piping) + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + ) + ) + pop: true + - include: line-continuation + - include: command-call-meta + - include: command-call-standard + command-call-standard: + - match: '\#' + comment: A command call can't be a comment, but this match will only be satisfied if the command is first after a pipe because comments are otherwise consumed earlier + push: + - meta_scope: invalid.illegal.function-call.fish + - match: '(?=[\n)])' + pop: true + - match: "(?:[&|<>^])" + comment: Match an operator which cannot start a command call but does not stop the next characters from being interpreted as a command + scope: invalid.illegal.operator.fish + - match: (?=\S) + comment: Anonymous scope - A complete command comprising a name element and optional parameter, redirection, and comment elements + push: + - match: |- + (?x) + (?# Look ahead for operators) + (?= + (?: + (?# Find a control operator) + [\n;&)] + | + (?# Find a pipe operator) + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + ) + ) + pop: true + - match: '(?![\s<>^%])' + comment: Anonymous scope - A name or block element. If a block is found, everything up to the `end` command is captured here. Note that redirection and process expansion can't start the element + push: + - match: '(?=[\s;&)|<>])' + pop: true + - include: command-call-standard-block + - match: '\[(?=[\s<>]|\\\n)' + comment: "Look for the alternate form of test, which uses a matching pair of '[' ']'" + captures: + 0: support.function.test.begin.fish + push: + - match: '(\])|(\n|[;&)|].*)' + captures: + 1: support.function.test.end.fish + 2: invalid.illegal.function-call.fish + pop: true + - include: line-continuation + - include: parameter + - include: redirection + - match: '(?:break|continue|return)(?=[\s;&)|<>])' + comment: Look for loop/function control commands. We perform no checking on the validity of their scope (because only allowing them in the correct scope won't work if they are used within if-blocks) or parameters (because fish does that during execution not parsing) + captures: + 0: keyword.control.conditional.fish + - match: (?!\s) + comment: Anonymous scope - A generic name element + push: + - match: '(?=[\s;&)|<>])' + pop: true + - match: (?=\() + comment: fish would match the whole command name invalid if there was a command substitution anywhere in it, but we can't look ahead that effectively + push: + - meta_scope: invalid.illegal.function-call.fish + - match: '(?=[\s;&)|<>])' + pop: true + - match: \( + push: + - match: '\)|(?=[\n;&)|<>])' + pop: true + - match: (?!\s) + comment: Otherwise, treat the element as a fraction of a name made of arbitrary strings (which breaks at an escaped newline) + push: + - meta_scope: variable.function.fish + - match: '(?=[\s;&()|<>])' + pop: true + - match: \$ + comment: The string scope explicitly forbids '$' so that the argument rule can pick it up as a variable expansion, but '$' is treated as a literal in command names, so we have to match it separately + scope: meta.string.unquoted.fish + - include: string + - match: \% + comment: A command name can't begin with a process expansion operator (however the variable expansion operator '$' is allowed) + push: + - meta_scope: invalid.illegal.function-call.fish + - match: '(?=[\s;&)|<>])' + pop: true + - include: string + - include: redirection + - match: '(?:[^\n\S]+)' + comment: Match any whitespace characters that aren't the newline + push: + - match: |- + (?x) + (?# Look ahead for operators) + (?= + (?: + (?# Find a control operator) + [\n;&)] + | + (?# Find a pipe operator) + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + ) + ) + pop: true + - match: '(?!--[\s;&)|<>])' + comment: A list of elements that does not start with an end-of-options parameter + push: + - match: |- + (?x) + (?# Look ahead for operators or the end of options) + (?= + (?: + (?# Find a control operator) + [\n;&)] + | + (?# Find a pipe operator) + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + | + (?# Find a double hyphen) + --[\s;&)|<>] + ) + ) + pop: true + - include: line-continuation + - include: comment-internal-end + - include: redirection + - include: parameter + - match: '(?=--[\s;&)|<>])' + comment: A list of elements that starts with an end-of-options parameter + push: + - match: |- + (?x) + (?# Look ahead for operators) + (?= + (?: + (?# Find a control operator) + [\n;&)] + | + (?# Find a pipe operator) + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + ) + ) + pop: true + - match: '(?=--[\s;&)|<>])' + comment: Contain just the end-of-options parameter and give it the normal scope + push: + - match: '(?=[\s;&)|<>])' + pop: true + - include: parameter + - match: (?=\s) + comment: A list of elements (now forcibly using arguments) + push: + - match: |- + (?x) + (?# Look ahead for operators) + (?= + (?: + (?# Find a control operator) + [\n;&)] + | + (?# Find a pipe operator) + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + ) + ) + pop: true + - include: line-continuation + - include: comment-internal-end + - include: redirection + - include: argument + command-call-standard-block: + - match: '(begin|while|if|for|switch|function)\s*([&|<>])' + comment: Block commands cannot be backgrounded, piped, or redirected + captures: + 1: variable.function.fish + 2: invalid.illegal.operator.fish + - match: (begin)\s*(\)) + comment: The begin command uniquely cannot be the last command in a command substitution + captures: + 1: variable.function.fish + 2: invalid.illegal.operator.fish + - match: 'begin(?=\s*$|\s*[\n;]|\s+[^\s-])' + comment: The begin command can be alone on a line or followed by any command that doesn't start with a '-'. If a '-' is seen it shouldn't be treated as a block + captures: + 0: keyword.control.conditional.fish + push: + - meta_scope: meta.block.begin.fish + - match: 'end(?=$|[\s;&)|<>])' + captures: + 0: keyword.control.conditional.fish + pop: true + - include: main + - match: '(?=while\s+[^\s;)-])' + comment: If the command name is followed by a valid string (not anything that closes the scope or a string starting with a '-') then open the scope + push: + - meta_scope: meta.block.while.fish + - match: 'end(?=$|[\s;&)|<>])' + captures: + 0: keyword.control.conditional.fish + pop: true + - match: while + comment: Anonymous scope - Capture the command name we know is there, include a single instance of a pipeline, and end when an operator is seen + captures: + 0: keyword.control.conditional.fish + push: + - match: '\s*(?=[\n;&)])' + pop: true + - include: line-continuation + - include: pipeline + - match: '\n|(;)|([&)])' + comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen + captures: + 1: keyword.operator.control.fish + 2: invalid.illegal.operator.fish + push: + - match: '(?=end(?:$|[\s;&)|<>]))' + pop: true + - include: main + - match: '(?=if\s+[^\s;)-])' + comment: If the command name is followed by a valid string (not anything that closes the scope or a string starting with a '-') then open the scope + push: + - meta_scope: meta.block.if.fish + - match: 'end(?=$|[\s;&)|<>])' + captures: + 0: keyword.control.conditional.fish + pop: true + - include: command-call-standard-block-if-internal + - match: '(?=for\s+[^\s;)-])' + comment: If the command name is followed by a valid string (not anything that closes the scope or a string starting with a '-') then open the scope + push: + - meta_scope: meta.block.for-in.fish + - match: 'end(?=$|[\s;&)|<>])' + captures: + 0: keyword.control.conditional.fish + pop: true + - match: (for)(?:\s+) + comment: Anonymous scope - Capture the command name we know is there, include a single instance of a parameter (the varname), and end when the whitespace after the varname is captured + captures: + 1: keyword.control.conditional.fish + push: + - match: \s+ + pop: true + - include: line-continuation + - include: parameter + - match: \S+ + comment: Capture anything that a parameter explicitly rejects, which is mostly operators + scope: invalid.illegal.operator.fish + - include: line-continuation + - match: in(?=\s) + comment: Anonymous scope - Capture the command name which might be there, include an arbitrary number of arguments, and end when the control operator is seen + captures: + 0: keyword.control.conditional.fish + push: + - match: '\s*(?=[\n;&)])' + pop: true + - include: line-continuation + - include: comment-internal-end + - include: argument + - match: '\n|(;)|([&)])' + comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen + captures: + 1: keyword.operator.control.fish + 2: invalid.illegal.operator.fish + push: + - match: '(?=end(?:$|[\s;&)|<>]))' + pop: true + - include: main + - match: '\S+?(?=[\s;&)])' + comment: Anything beside line continuation, "in", or a control operator is invalid + scope: invalid.illegal.function-call.fish + - match: '(?=switch\s+[^\s;)-])' + comment: If the command name is followed by a valid string (not anything that closes the scope or a string starting with a '-') then open the scope + push: + - meta_scope: meta.block.switch.fish + - match: 'end(?=$|[\s;&)|<>])' + captures: + 0: keyword.control.conditional.fish + pop: true + - match: (?=switch) + comment: Anonymous scope - Match the valid part of the switch statement, then look for an invalid part + push: + - match: '\s*(?=[\n;&)])' + pop: true + - match: (switch)(?:\s+) + comment: Anonymous scope - Capture the command name we know is there, include a single instance of a parameter (the value), and end when whitespace or a control operator is seen + captures: + 1: keyword.control.conditional.fish + push: + - match: '(?=[\s;&)])' + pop: true + - include: line-continuation + - include: parameter + - match: \S+ + comment: Capture anything that a parameter explicitly rejects, which is mostly operators + scope: invalid.illegal.operator.fish + - match: \s+ + comment: Anonymous scope - Capture whitespace which might be there, match any non-control-operator strings as invalid, and end when a control operator is seen + push: + - match: '(?=[\n;&)])' + pop: true + - match: '\S+?(?=[\s;&)])' + scope: invalid.illegal.string.fish + - match: '\n|(;)|([&)])' + comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen + captures: + 1: keyword.operator.control.fish + 2: invalid.illegal.operator.fish + push: + - match: '(?=end(?:$|[\s;&)|<>]))' + pop: true + - match: 'case(?=[\s;&)])' + comment: Anonymous scope - Capture the command name which might be there, include an arbitrary number of arguments, and end when the control operator is captured + captures: + 0: keyword.control.conditional.fish + push: + - match: '\n|(;)|([&)])' + captures: + 1: keyword.operator.control.fish + 2: invalid.illegal.operator.fish + pop: true + - include: line-continuation + - include: comment-internal-end + - include: argument + - match: '\S+?(?=[\s;&)])' + comment: Anything else (eg, redirection) is illegal + scope: invalid.illegal.operator.fish + - include: main + - match: '(?=function\s+[^\s;)-])' + comment: If the command name is followed by a valid string (not anything that closes the scope or a string starting with a '-') then open the scope + push: + - meta_scope: meta.block.function.fish + - match: 'end(?=$|[\s;&)|<>])' + captures: + 0: keyword.control.conditional.fish + pop: true + - match: (?=function) + comment: Anonymous scope - Match the defined name of the function statement, then look for further parameters + push: + - match: '\s*(?=[\n;&)])' + pop: true + - match: (function)(?:\s+) + comment: Anonymous scope - Capture the command name we know is there, include a single instance of a parameter (the value), and end when whitespace or a control operator is seen + captures: + 1: keyword.control.conditional.fish + push: + - match: '(?=[\s;&)])' + pop: true + - include: line-continuation + - match: "(?:[()|<>])" + push: + - meta_scope: invalid.illegal.string.fish + - match: '(?=[\s;&)])' + pop: true + - match: (?!\\\n) + comment: Anonymous scope - Start when an escaped newline isn't present, and end when whitespace or an operator is seen + push: + - match: '(?=[\s;&()|<>])' + pop: true + - match: (?!\s) + push: + - meta_scope: entity.name.function.fish + - match: '(?=[\s;&()|<>])' + pop: true + - include: parameter + - match: \s+ + comment: Anonymous scope - Capture whitespace which might be there, then match anything normal for a command call + push: + - match: '(?=[\n;&)])' + pop: true + - include: line-continuation + - include: comment-internal-end + - include: redirection + - include: parameter + - match: '\n|(;)|([&)])' + comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen + captures: + 1: keyword.operator.control.fish + 2: invalid.illegal.operator.fish + push: + - match: '(?=end(?:$|[\s;&)|<>]))' + pop: true + - include: main + command-call-standard-block-if-internal: + - match: '(?=if(?:\s*\n|\s+[^\s;]))' + comment: Anonymous scope - Capture an `if` and the command up to the control operator, then capture from the control operator indefinitely + push: + - match: '(?=end(?:$|[\s;&)|<>]))' + pop: true + - match: if + comment: Anonymous scope - Match the command name we know is there, include a single instance of a pipeline, and end when a control operator is seen + captures: + 0: keyword.control.conditional.fish + push: + - match: '\s*(?=[\n;&])' + pop: true + - include: line-continuation + - include: pipeline + - match: \n|(;)|(&) + comment: Anonymous scope - Match the operator we know is there, then include the base scope or an `else` structure + captures: + 1: keyword.operator.control.fish + 2: invalid.illegal.operator.fish + push: + - match: '(?=end(?:$|[\s;&)|<>]))' + pop: true + - match: '(?=else\s*[\s;])' + comment: Anonymous scope - Capture an `else` up to the control operator or the start of an `if` structure, then match from the control operator indefinitely or match an `if` structure + push: + - match: '(?=end(?:$|[\s;&)|<>]))' + pop: true + - match: 'else(?=\s*[\s;])' + comment: Anonymous scope - Match the `else` we know is there and any comment, and mark anything besides an `if` as illegal + captures: + 0: keyword.control.conditional.fish + push: + - match: '\s*(?=[\n;&]|if(?:\s*\n|\s+[^\s;]))' + pop: true + - include: line-continuation + - include: comment-internal-end + - match: '\S+?(?=[\s;&])' + comment: Anything else is illegal + scope: invalid.illegal.string.fish + - match: \n|(;)|(&) + comment: Anonymous scope - Match the operator which will be there if no `if` was seen, then include the base scope which marks further `else` commands as invalid + captures: + 1: keyword.operator.control.fish + 2: invalid.illegal.operator.fish + push: + - match: '(?=end(?:$|[\s;&)|<>]))' + pop: true + - include: main + - include: command-call-standard-block-if-internal + - include: main + command-substitution: + - match: (?=\() + comment: 'Capture "(...)" or "(...)[...]"' + push: + - match: '(?![\(\[])' + pop: true + - match: \( + captures: + 0: punctuation.section.parens.begin.fish + push: + - meta_scope: meta.parens.command-substitution.fish + - match: \) + captures: + 0: punctuation.section.parens.end.fish + pop: true + - include: main + - include: index-expansion + comment-external: + - match: '\#' + comment: A full or inline comment outside of any command call + captures: + 0: punctuation.definition.comment.fish + push: + - meta_scope: comment.line.external.fish + - match: \n + pop: true + comment-internal-end: + - match: '\#' + comment: An inline comment at the end of a command call. Does not consume the newline, thus allowing the command call to capture it and end + captures: + 0: punctuation.definition.comment.fish + push: + - meta_scope: comment.line.internal.end.fish + - match: (?=\n) + pop: true + index-expansion: + - match: '\[' + comment: In other words, the anonymous scope which contains the variable and the index expansion parameter list should only be allowed to contain a single copy of each of those two things. We cannot enforce that without a scope stack. Our workaround is to allow an infinite number of these and hope the user can keep track of when there are too many + captures: + 0: punctuation.section.brackets.begin.fish + push: + - meta_scope: meta.brackets.index-expansion.fish + - match: '\]' + captures: + 0: punctuation.section.brackets.end.fish + pop: true + - match: \.\. + scope: keyword.operator.range.fish + - include: command-substitution + - include: variable-expansion + - include: string-quoted + - match: '(?:[+-]?[0-9]+)(?=[\s;&)|<>]|\]|\.\.)' + scope: constant.numeric.fish + - match: '(?![\s''"]|\.\.)' + comment: 'Begin/end string as before with the addition of breaking at a '']'' or ".."' + push: + - match: '(?=[\s;&)|<>''"]|\]|\.\.)' + pop: true + - include: string-unquoted-patterns + line-continuation: + - match: (?=\\\n) + comment: End when an unescaped newline is seen, the first character of a line isn't whitespace or a comment character or the escaped newline itself, or if the next character after some consumed whitespace isn't more whitespace or a comment character + push: + - match: '(?=\n)|^(?![\s\#\\])|\s(?![\s\#])' + pop: true + - match: \\\n + scope: constant.character.escape + - match: '\#' + captures: + 0: punctuation.definition.comment.fish + push: + - meta_scope: comment.line.continuation.fish + - match: \n + pop: true + parameter: + - match: '(?![\s;&)|<>^])' + comment: See the argument rule for more general information on parameters + push: + - match: '(?=[\s;&)|<>])' + pop: true + - match: '(?:--)(?=[\s;&)|<>])' + comment: End of options (parameter of just two hyphens) + scope: meta.parameter.option.end.fish variable.parameter.fish punctuation.definition.option.end.fish meta.string.unquoted.fish + - match: (?=--) + comment: Long option (parameter starting with two hyphens) + push: + - meta_scope: meta.parameter.option.long.fish + - match: '(?=[\s;&)|<>])' + pop: true + - match: (?:--) + captures: + 0: punctuation.definition.option.long.begin.fish meta.string.unquoted.fish + push: + - meta_scope: variable.parameter.fish + - match: '(?=[\s;&)|<>]|=)' + pop: true + - include: command-substitution + - match: (?=\$) + push: + - meta_scope: meta.string.unquoted.fish + - match: (?!\$) + pop: true + - include: variable-expansion + - include: string-quoted + - match: '(?![''"])' + push: + - meta_scope: meta.string.unquoted.fish + - match: '(?=[\s;&()|<>''"$]|\=)' + pop: true + - include: string-unquoted-patterns + - match: (?:=) + comment: Consume the '=' and then use standard parameter patterns as well as numerics + captures: + 0: variable.parameter.fish punctuation.definition.option.long.separator.fish meta.string.unquoted.fish + push: + - match: '(?=[\s;&)|<>])' + pop: true + - match: '(?:[+-]?)(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?=$|[\s;&)|<>])' + scope: meta.string.unquoted.fish constant.numeric.fish + - include: parameter-patterns + - match: '(?:-)(?=[^\s;&)|<>])' + comment: Short option (parameter starting with one hyphen) + captures: + 0: punctuation.definition.option.short.fish meta.string.unquoted.fish + push: + - meta_scope: meta.parameter.option.short.fish variable.parameter.fish + - match: '(?=[\s;&)|<>])' + pop: true + - include: parameter-patterns + - include: argument + parameter-patterns: + - include: command-substitution + - match: (?=\$) + comment: Give variable expansion the unquoted string scope + push: + - meta_scope: meta.string.unquoted.fish + - match: (?!\$) + pop: true + - include: variable-expansion + - include: string + pipeline: + - match: '(?:[&|]|(?:[0-9]+)?(?:<|>>?|\^\^?))' + comment: Todo - Restructure pipeline so that this match isn't duplicated from the base scope, which it must also be for any other scopes which implement their own control operator consumption. Might require the unary operator commands to become an explicit recursive match (though we tried this once and it was more complicated than anything should be) + scope: invalid.illegal.operator.fish + - match: (and|or)\b(?!\s+-) + comment: Todo - These commands cannot be followed by backgrounding, piping, or redirection alone. Add logic to catch these cases. It will be extensive... + scope: meta.function-call.fish keyword.operator.word.fish + - include: line-continuation + - match: (not)\b(?!\s+-) + comment: This is a hack for now, which allows nesting of 'not' and 'and'/'or' commands. A better solution will be explicit recursivity in these commands + scope: meta.function-call.fish keyword.operator.word.fish + - match: '(?:case|else|end)(?=[\s;&)|<>])' + comment: Match a command which is illegal in the base scope + scope: invalid.illegal.function-call.fish + - match: '(?=[^\s#])' + comment: Anonymous scope - Pipeline. Define a pipeline as either one command call, or multiple command calls linked by pipe operators ('|', '2>|', etc). The pipeline terminates at the first encounter of any control operator + push: + - match: '(\s*)(?=[\n;&)])' + captures: + 1: meta.function-call.fish + pop: true + - match: |- + (?x) + (?# Negative lookahead for piping) + (?! + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + ) + comment: Match the first command of a potential pipeline + push: + - meta_scope: meta.function-call.fish + - match: |- + (?x) + (?# Look ahead for operators after whitespace) + (?=\s* + (?: + (?# Find a control operator) + [\n;&)] + | + (?# Find a pipe operator) + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + ) + ) + pop: true + - include: command-call-meta + - include: command-call-standard + - match: |- + (?x) + (?# Look ahead for piping) + (?= + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + ) + comment: Match a second or later command of a pipeline, starting with the connective piping + push: + - meta_scope: meta.function-call.fish + - match: '(?=\s*[\n;&)])' + pop: true + - match: |- + (?x) + (?# Look ahead for piping followed by either control operators or piping) + (?= + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + \s* + (?: + $ + | + [\n;&)] + | + (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\| + ) + ) + comment: Match a pipe not followed by a command, hence a malformed segment of the pipeline + push: + - match: '(?=\s*$|\s*[\n;&)])' + pop: true + - match: '(?:(?:[0-9]+)?(?:<|>|>>))?\|(?=\s*$|\s*[\n)])' + comment: If the pipeline would end implicitly (ie, with a newline or close parenthesis), then mark the pipe itself invalid + scope: invalid.illegal.operator.fish + - match: |- + (?x) + (?# Consume valid piping; captures 1 2 3) + (?:([0-9]+)?(<|>>?|\^\^?))?(\|) + (?# Consume whitespace) + \s* + (?# Consume remainder; capture 4) + (.*) + comment: If the pipeline would end with an explicit operator or encounter a second set of piping, then mark the first set of piping as valid and beyond as invalid + captures: + 1: meta.pipe.fish constant.numeric.file-descriptor.fish + 2: meta.pipe.fish keyword.operator.redirect.fish + 3: meta.pipe.fish keyword.operator.pipe.fish + 4: invalid.illegal.function-call.fish + - match: '(?:([0-9]+)?(<|>>?|\^\^?))?(\|)' + comment: Pick up a legitimate pipe + scope: meta.pipe.fish + captures: + 1: constant.numeric.file-descriptor.fish + 2: keyword.operator.pipe.redirect.fish + 3: keyword.operator.pipe.fish + - include: line-continuation + - include: command-call-meta + - include: command-call-standard + redirection: + - match: '(?=(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])\&)' + comment: End at anything that would end a parameter, including redirections *if* they are *not* this same type of redirection (ie, have an '&'), in which case this scope stays open and we match the next one. The negative lookahead for <>^ at the end is to keep ST2 happy (not hanging) + push: + - match: '(?=[\s;&)|]|(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])(?![&<>^]))' + pop: true + - match: '(?:([0-9]+)(<|>|>>)|(>>|\^\^|[<>^]))(\&)\s*' + comment: We have to try and catch an '&' here because if it is seen by the outer end match then it will be considered a valid operator and the redirection scope will immediately terminate + captures: + 1: constant.numeric.file-descriptor.fish + 2: keyword.operator.redirect.fish + 3: keyword.operator.redirect.fish + 4: keyword.operator.redirect.dereference.fish + push: + - meta_scope: meta.redirection.fish + - match: '(\&.*$)|(?![&\\])' + captures: + 1: invalid.illegal.file-descriptor.fish + pop: true + - include: line-continuation + - match: (?=\\\n) + push: + - meta_scope: meta.redirection.fish + - match: (?!\\\n) + pop: true + - include: line-continuation + - match: (?=\() + comment: Evaluates to a string which may be an integer + push: + - meta_scope: meta.redirection.fish + - match: (?!\() + pop: true + - include: command-substitution + - match: (?=\$) + comment: Evaluates to a string which may be an integer + push: + - meta_scope: meta.redirection.fish + - match: (?!\$) + pop: true + - include: variable-expansion + - match: '(?=[''"])' + comment: May be a quoted integer, which is allowed + push: + - meta_scope: meta.redirection.fish + - match: '(?![''"])' + pop: true + - include: string-quoted + - match: '(?:[0-9]+)(?=$|[\s;&)|<>])' + scope: meta.redirection.file-descriptor.fish constant.numeric.file-descriptor.fish + - match: '(?:-)(?=$|[\s;&)|<>])' + scope: meta.redirection.file-descriptor.fish keyword.operator.redirect.close.fish + - match: (?:\S+.*)$ + comment: Anything else is illegal + scope: meta.redirection.fish invalid.illegal.file-descriptor.fish + - match: '(?=(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])\??)' + comment: End at anything that would end a parameter, including redirections *if* they are *not* this same type of redirection (ie, redirection into file descriptor, or into pipe), in which case this scope stays open and we match the next one + push: + - match: '(?=[\s;&)|]|(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])[&|])' + pop: true + - match: '(?:([0-9]+)(<|>|>>)|(>>|\^\^|[<>^]))(\?)?\s*' + comment: We have to try and catch bad operators here because if they are seen by the outer end match then they will be considered valid and the redirection scope will immediately terminate + captures: + 1: constant.numeric.file-descriptor.fish + 2: keyword.operator.redirect.fish + 3: keyword.operator.redirect.fish + 4: keyword.operator.redirect.clobber-test.fish + push: + - meta_scope: meta.redirection.fish + - match: "((?:[&?]|[0-9]*[<>^]).*$)|(?![&?<>^])" + captures: + 1: invalid.illegal.path.fish + pop: true + - include: line-continuation + - match: (?=\\\n) + push: + - meta_scope: meta.redirection.fish + - match: (?!\\\n) + pop: true + - include: line-continuation + - match: (?=\() + comment: Evaluates to a string, so path cannot begin with '(' + push: + - meta_scope: meta.redirection.fish + - match: (?!\() + pop: true + - include: command-substitution + - match: (?=\$) + comment: Evaluates to a string, so path cannot begin with '$' + push: + - meta_scope: meta.redirection.fish + - match: (?!\$) + pop: true + - include: variable-expansion + - match: "(?:[&?]|[0-9]*[<>^]).*$" + comment: Check for characters which are associated with redirection, so path cannot begin with them. Don't put them in the meta.redirection.path scope, so that only valid paths are in there + scope: meta.redirection.fish invalid.illegal.path.fish + - match: \~ + scope: meta.redirection.path.fish keyword.operator.tilde.fish + - match: '(?:\''(?:\\[\''\\]|[^\''\\])*\''|\"(?:\\[\"$\n\\]|[^\"$\n\\])*\"|(?:\\[abefnrtv $\\*?#(){}\[\]<>^&;|"'']|\\[~%]|\\[xX][0-9A-Fa-f]{1,2}|\\[0-7]{1,3}|\\u[0-9A-Fa-f]{1,4}|\\U[0-9A-Fa-f]{1,8}|\\c[?-~]|[^\s$\\*?~%#()<>&|;"'']|\\(?=[^abefnrtv\s$\\*?#(){}\[\]<>^&;|"''xXuUc])|\\\n|[~%#])+)+' + comment: Use the function call match to build a file path, as the syntax is fairly similar (possibly identical, after exceptions caught above? I haven't checked) + scope: meta.redirection.path.fish + string: + - include: string-quoted + - include: string-unquoted + string-quoted: + - match: \' + captures: + 0: punctuation.definition.string.begin.fish + push: + - meta_scope: string.quoted.single.fish + - match: \' + captures: + 0: punctuation.definition.string.end.fish + pop: true + - match: '\\[\''\\]' + comment: Only accepted escapes are \' and \\ + scope: constant.character.escape.fish + - match: \" + captures: + 0: punctuation.definition.string.begin.fish + push: + - meta_scope: string.quoted.double.fish + - match: \" + captures: + 0: punctuation.definition.string.end.fish + pop: true + - match: '\\[\n\"\\$]' + comment: Only accepted escapes are \, \", \\, and \$ + scope: constant.character.escape.fish + - include: variable-expansion + string-unquoted: + - match: '(?![\s;&()|<>''"$])' + comment: End unquoted string at anything that can't be in one + push: + - meta_scope: meta.string.unquoted.fish + - match: '(?=[\s;&()|<>''"$])' + pop: true + - include: string-unquoted-patterns + string-unquoted-patterns: + - match: |- + (?x) + \\[abefnrtv $\\*?#(){}\[\]<>^&|;"'] + | + \\[~%] + | + \\[xX][0-9A-Fa-f]{1,2} + | + \\[0-7]{1,3} + | + \\u[0-9A-Fa-f]{1,4} + | + \\U[0-9A-Fa-f]{1,8} + | + \\c[?-~] + comment: This list follows the order given in official fish documentation. Technically '~' and '%' only need escaping if they appear at the front of a parameter. If they are escaped within a parameter, then fish does not *highlight* the escape, however it does silently *parse* the escape and the backslash is removed before the parameter is passed to the command. So, we highlight these escapes as well since they are actually treated as valid escapes by fish + scope: constant.character.escape.fish + - match: \\\n + comment: Just for convenience we separate the newline escape + scope: constant.character.escape.fish + - match: '\{' + captures: + 0: punctuation.section.braces.begin.fish + push: + - meta_scope: meta.braces.brace-expansion.fish + - match: '(\})|(\n|[;&)|].*)' + captures: + 1: punctuation.section.braces.end.fish + 2: invalid.illegal.punctuation.section.fish + pop: true + - match: \, + scope: punctuation.section.braces.separator.fish + - include: command-substitution + - include: variable-expansion + - match: '(?:[^\S\n]+)' + comment: Unescaped spaces aren't allowed, as technically that separates the braces into two separate arguments. Don't consume a newline though, so the scope end capture can get it + scope: invalid.illegal.whitespace.fish + - include: string-quoted + - match: '(?:[+-]?)(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?=$|[\s;&)|<>]|\}|\,)' + scope: constant.numeric.fish + - match: '(?![\s;&)|<>''"])' + comment: "Begin/end string as before with the addition of breaking at a '}' or ','" + push: + - match: '(?=[\s;&)|<>''"]|\}|\,)' + pop: true + - match: \\\, + scope: constant.character.escape.fish + - include: string-unquoted-patterns + - match: (\*\*)|(\*)|(\?) + scope: meta.wildcard-expansion.fish + captures: + 1: keyword.operator.double-star.fish + 2: keyword.operator.single-star.fish + 3: keyword.operator.question-mark.fish + variable-expansion: + - include: variable-expansion-illegal + - match: (?=\$) + comment: 'Capture "$foo" or "$foo[]" or "$$foo[][]" etc' + push: + - meta_scope: meta.variable-expansion.fish + - match: '(?=[^\$\w\[])' + pop: true + - match: \$ + captures: + 0: punctuation.definition.variable.fish + push: + - meta_scope: variable.other.fish + - match: '(?=[^\$\w])' + pop: true + - include: variable-expansion-illegal + - include: variable-expansion-simple + - include: index-expansion + variable-expansion-illegal: + - match: '\$(?:(?=[,''"\]}\s;&)|])|[^\w\$][^$,''"\]}\s;&)|]*)' + comment: A lone '$' in a scope, or an attempt to expand a variable starting with a nonword character, is an error. These boundaries are the same as for meta.string.unquoted + scope: invalid.illegal.variable-expansion.fish + variable-expansion-simple: + - match: \$ + captures: + 0: punctuation.definition.variable.fish + push: + - meta_scope: variable.other.fish + - match: '(?=[^\$\w])' + pop: true + - include: variable-expansion-illegal + - include: variable-expansion-simple diff --git a/assets/syntaxes/sublime-fish b/assets/syntaxes/sublime-fish new file mode 160000 index 0000000000..079576415a --- /dev/null +++ b/assets/syntaxes/sublime-fish @@ -0,0 +1 @@ +Subproject commit 079576415af7e25116f1cec0a508a3a43344416b