Skip to content

Commit

Permalink
Highlight try? and try! as compound keyword+operator
Browse files Browse the repository at this point in the history
See #351
  • Loading branch information
alex-pinkus committed Feb 19, 2024
1 parent 269ca66 commit 67d4d11
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 9 deletions.
24 changes: 20 additions & 4 deletions grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: {
Expand Down Expand Up @@ -762,7 +766,7 @@ module.exports = grammar({
prec.right(
PRECS["try"],
seq(
$._try_operator,
$.try_operator,
field(
"expr",
choice(
Expand Down Expand Up @@ -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("<", ">", "<=", ">="),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 4 additions & 3 deletions queries/highlights.scm
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -133,10 +136,8 @@
; Operators
(custom_operator) @operator
[
"try"
"try?"
"try!"
"!"
"?"
"+"
"-"
"*"
Expand Down
42 changes: 42 additions & 0 deletions src/scanner.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#include <string.h>
#include <wctype.h>

#define TOKEN_COUNT 28

enum TokenType {
BLOCK_COMMENT,
RAW_STR_PART,
Expand Down Expand Up @@ -30,6 +32,7 @@ enum TokenType {
AS_BANG,
ASYNC_KEYWORD,
CUSTOM_OPERATOR,
FAKE_TRY_BANG
};

#define OPERATOR_COUNT 20
Expand Down Expand Up @@ -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] = {
Expand Down Expand Up @@ -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;
}
Expand Down
13 changes: 13 additions & 0 deletions test/corpus/expressions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,7 @@ try foo()

(source_file
(try_expression
(try_operator)
(call_expression
(simple_identifier)
(call_suffix
Expand All @@ -1059,6 +1060,7 @@ try foo() ? 1 : 0

(source_file
(try_expression
(try_operator)
(ternary_expression
(call_expression
(simple_identifier)
Expand Down Expand Up @@ -1093,13 +1095,15 @@ await try foo()

(source_file
(try_expression
(try_operator)
(await_expression
(call_expression
(simple_identifier)
(call_suffix
(value_arguments)))))
(await_expression
(try_expression
(try_operator)
(call_expression
(simple_identifier)
(call_suffix
Expand Down Expand Up @@ -1135,6 +1139,7 @@ for try await value in values {

(source_file
(for_statement
(try_operator)
(pattern
(simple_identifier))
(simple_identifier)
Expand Down Expand Up @@ -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())

---
2 changes: 2 additions & 0 deletions test/corpus/statements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ func doSomething() {
(value_binding_pattern)
(simple_identifier)
(try_expression
(try_operator)
(call_expression
(navigation_expression
(simple_identifier)
Expand Down Expand Up @@ -770,6 +771,7 @@ If try
(source_file
(if_statement
(try_expression
(try_operator)
(equality_expression
(simple_identifier)
(conjunction_expression
Expand Down
3 changes: 2 additions & 1 deletion test/highlight/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// ^ parameter
_ = try! DatabaseQueue()
// ^ operator
// ^ operator
// ^ keyword
// ^ operator
// ^ function.call
_ = FTS5()
_ = sqlite3_preupdate_new(nil, 0, nil)
Expand Down
2 changes: 1 addition & 1 deletion test/highlight/HeroStringConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ extension String {
do {
// ^ keyword
let nodes = try parser.parse()
// ^ operator
// ^ keyword
var results = [T]()
// ^ punctuation.bracket
for node in nodes {
Expand Down

0 comments on commit 67d4d11

Please sign in to comment.