From 67d4d11a5e06832c3bc4df957e4420b0de906157 Mon Sep 17 00:00:00 2001 From: "Alex Pinkus (Bot)" Date: Sat, 17 Feb 2024 19:59:37 -0800 Subject: [PATCH] Highlight try? and try! as compound keyword+operator See #351 --- grammar.js | 24 ++++++++++--- queries/highlights.scm | 7 ++-- src/scanner.c | 42 ++++++++++++++++++++++ test/corpus/expressions.txt | 13 +++++++ test/corpus/statements.txt | 2 ++ test/highlight/AppDelegate.swift | 3 +- test/highlight/HeroStringConvertible.swift | 2 +- 7 files changed, 84 insertions(+), 9 deletions(-) diff --git a/grammar.js b/grammar.js index ca75d7e..b645abc 100644 --- a/grammar.js +++ b/grammar.js @@ -226,7 +226,7 @@ module.exports = grammar({ $._eq_eq_custom, $._plus_then_ws, $._minus_then_ws, - $.bang, + $._bang_custom, $._throws_keyword, $._rethrows_keyword, $.default_keyword, @@ -238,6 +238,10 @@ module.exports = grammar({ $._as_bang_custom, $._async_keyword_custom, $._custom_operator, + + // Fake operator that will never get triggered, but follows the sequence of characters for `try!`. Tracked by the + // custom scanner so that it can avoid triggering `$.bang` for that case. + $._fake_try_bang, ], inline: ($) => [$._locally_permitted_modifiers], rules: { @@ -762,7 +766,7 @@ module.exports = grammar({ prec.right( PRECS["try"], seq( - $._try_operator, + $.try_operator, field( "expr", choice( @@ -1064,7 +1068,11 @@ module.exports = grammar({ "self", seq("[", optional(sep1($.value_argument, ",")), "]") ), - _try_operator: ($) => choice("try", "try!", "try?"), + try_operator: ($) => + prec.right( + seq("try", choice(optional($._try_operator_type), $._fake_try_bang)) + ), + _try_operator_type: ($) => token.immediate(choice("!", "?")), _assignment_and_operator: ($) => choice("+=", "-=", "*=", "/=", "%=", "="), _equality_operator: ($) => choice("!=", "!==", $._eq_eq, "==="), _comparison_operator: ($) => choice("<", ">", "<=", ">="), @@ -1147,7 +1155,7 @@ module.exports = grammar({ PRECS.loop, seq( "for", - optional($._try_operator), + optional($.try_operator), optional($._await_operator), field("item", alias($._binding_pattern_no_expr, $.pattern)), optional($.type_annotation), @@ -1521,6 +1529,7 @@ module.exports = grammar({ _as: ($) => alias($._as_custom, "as"), _as_quest: ($) => alias($._as_quest_custom, "as?"), _as_bang: ($) => alias($._as_bang_custom, "as!"), + bang: ($) => choice($._bang_custom, "!"), _async_keyword: ($) => alias($._async_keyword_custom, "async"), _async_modifier: ($) => token("async"), throws: ($) => choice($._throws_keyword, $._rethrows_keyword), @@ -1864,6 +1873,13 @@ module.exports = grammar({ ) ) ), + // Dumping ground for any nodes that used to exist in the grammar, but have since been removed for whatever + // reason. + // Neovim applies updates non-atomically to the parser and the queries. Meanwhile, `tree-sitter` rejects any query + // that contains any unrecognized nodes. Putting those two facts together, we see that we must never remove nodes + // that once existed. + unused_for_backward_compatibility: ($) => + choice(alias("unused1", "try?"), alias("unused2", "try!")), }, }); function sep1(rule, separator) { diff --git a/queries/highlights.scm b/queries/highlights.scm index f5a8210..9fe6922 100644 --- a/queries/highlights.scm +++ b/queries/highlights.scm @@ -70,6 +70,9 @@ (simple_identifier) @type) ; SomeType.method(): highlight SomeType as a type (#match? @type "^[A-Z]")) +(try_operator) @operator +(try_operator ["try" @keyword]) + (directive) @function.macro (diagnostic) @function.macro @@ -133,10 +136,8 @@ ; Operators (custom_operator) @operator [ - "try" - "try?" - "try!" "!" + "?" "+" "-" "*" diff --git a/src/scanner.c b/src/scanner.c index e7878fa..c6b4e6b 100644 --- a/src/scanner.c +++ b/src/scanner.c @@ -2,6 +2,8 @@ #include #include +#define TOKEN_COUNT 28 + enum TokenType { BLOCK_COMMENT, RAW_STR_PART, @@ -30,6 +32,7 @@ enum TokenType { AS_BANG, ASYNC_KEYWORD, CUSTOM_OPERATOR, + FAKE_TRY_BANG }; #define OPERATOR_COUNT 20 @@ -110,6 +113,29 @@ const enum TokenType OP_SYMBOLS[OPERATOR_COUNT] = { ASYNC_KEYWORD }; +const uint64_t OP_SYMBOL_SUPPRESSOR[OPERATOR_COUNT] = { + 0, // ARROW_OPERATOR, + 0, // DOT_OPERATOR, + 0, // CONJUNCTION_OPERATOR, + 0, // DISJUNCTION_OPERATOR, + 0, // NIL_COALESCING_OPERATOR, + 0, // EQUAL_SIGN, + 0, // EQ_EQ, + 0, // PLUS_THEN_WS, + 0, // MINUS_THEN_WS, + 1 << FAKE_TRY_BANG, // BANG, + 0, // THROWS_KEYWORD, + 0, // RETHROWS_KEYWORD, + 0, // DEFAULT_KEYWORD, + 0, // WHERE_KEYWORD, + 0, // ELSE_KEYWORD, + 0, // CATCH_KEYWORD, + 0, // AS_KEYWORD, + 0, // AS_QUEST, + 0, // AS_BANG, + 0, // ASYNC_KEYWORD +}; + #define RESERVED_OP_COUNT 31 const char* RESERVED_OPS[RESERVED_OP_COUNT] = { @@ -474,6 +500,22 @@ static bool eat_operators( } if (full_match != -1) { + // We have a match -- first see if that match has a symbol that suppresses it. For example, in `try!`, we do not + // want to emit the `!` as a symbol in our scanner, because we want the parser to have the chance to parse it as + // an immediate token. + uint64_t suppressing_symbols = OP_SYMBOL_SUPPRESSOR[full_match]; + if (suppressing_symbols) { + for (uint64_t suppressor = 0; suppressor < TOKEN_COUNT; suppressor++) { + if (!(suppressing_symbols & 1 << suppressor)) { + continue; + } + + // The suppressing symbol is valid in this position, so skip it. + if (valid_symbols[suppressor]) { + return false; + } + } + } *symbol_result = OP_SYMBOLS[full_match]; return true; } diff --git a/test/corpus/expressions.txt b/test/corpus/expressions.txt index b23b5b8..e6372ec 100755 --- a/test/corpus/expressions.txt +++ b/test/corpus/expressions.txt @@ -1044,6 +1044,7 @@ try foo() (source_file (try_expression + (try_operator) (call_expression (simple_identifier) (call_suffix @@ -1059,6 +1060,7 @@ try foo() ? 1 : 0 (source_file (try_expression + (try_operator) (ternary_expression (call_expression (simple_identifier) @@ -1093,6 +1095,7 @@ await try foo() (source_file (try_expression + (try_operator) (await_expression (call_expression (simple_identifier) @@ -1100,6 +1103,7 @@ await try foo() (value_arguments))))) (await_expression (try_expression + (try_operator) (call_expression (simple_identifier) (call_suffix @@ -1135,6 +1139,7 @@ for try await value in values { (source_file (for_statement + (try_operator) (pattern (simple_identifier)) (simple_identifier) @@ -1515,3 +1520,11 @@ let opaqueSelf = (any WithModifiersSyntax).self (type_identifier))) (navigation_suffix (simple_identifier))))) + +=== +Try with prefix operation after it +=== + +let result = try !(inner1() || inner2()) + +--- diff --git a/test/corpus/statements.txt b/test/corpus/statements.txt index 6e1fee8..1a28810 100755 --- a/test/corpus/statements.txt +++ b/test/corpus/statements.txt @@ -627,6 +627,7 @@ func doSomething() { (value_binding_pattern) (simple_identifier) (try_expression + (try_operator) (call_expression (navigation_expression (simple_identifier) @@ -770,6 +771,7 @@ If try (source_file (if_statement (try_expression + (try_operator) (equality_expression (simple_identifier) (conjunction_expression diff --git a/test/highlight/AppDelegate.swift b/test/highlight/AppDelegate.swift index da61822..23f9828 100644 --- a/test/highlight/AppDelegate.swift +++ b/test/highlight/AppDelegate.swift @@ -18,7 +18,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { // ^ parameter _ = try! DatabaseQueue() // ^ operator -// ^ operator +// ^ keyword +// ^ operator // ^ function.call _ = FTS5() _ = sqlite3_preupdate_new(nil, 0, nil) diff --git a/test/highlight/HeroStringConvertible.swift b/test/highlight/HeroStringConvertible.swift index 972a0bc..360388a 100644 --- a/test/highlight/HeroStringConvertible.swift +++ b/test/highlight/HeroStringConvertible.swift @@ -38,7 +38,7 @@ extension String { do { // ^ keyword let nodes = try parser.parse() -// ^ operator +// ^ keyword var results = [T]() // ^ punctuation.bracket for node in nodes {