Author: Jonas Devlieghere Date: 2026-02-18T13:54:14-08:00 New Revision: b453adff7ac47421751cf9ad598af420733b5fe2
URL: https://github.com/llvm/llvm-project/commit/b453adff7ac47421751cf9ad598af420733b5fe2 DIFF: https://github.com/llvm/llvm-project/commit/b453adff7ac47421751cf9ad598af420733b5fe2.diff LOG: [lldb] Add tree-sitter based Rust syntax highlighting (#181282) This adds tree-sitter based Rust syntax highlighting to LLDB. It consists of the RustTreeSitterHighlighter plugin and the vendored Rust grammar [1], which is licensed under MIT. [1] https://github.com/tree-sitter/tree-sitter-rust Added: lldb/source/Plugins/Highlighter/TreeSitter/Rust/CMakeLists.txt lldb/source/Plugins/Highlighter/TreeSitter/Rust/HighlightQuery.h.in lldb/source/Plugins/Highlighter/TreeSitter/Rust/RustTreeSitterHighlighter.cpp lldb/source/Plugins/Highlighter/TreeSitter/Rust/RustTreeSitterHighlighter.h lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/LICENSE lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/grammar.js lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/highlights.scm lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/scanner.c lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/tree-sitter.json Modified: lldb/source/Plugins/Highlighter/TreeSitter/CMakeLists.txt lldb/source/Plugins/Highlighter/TreeSitter/README.md lldb/unittests/Highlighter/CMakeLists.txt lldb/unittests/Highlighter/HighlighterTest.cpp Removed: ################################################################################ diff --git a/lldb/source/Plugins/Highlighter/TreeSitter/CMakeLists.txt b/lldb/source/Plugins/Highlighter/TreeSitter/CMakeLists.txt index 9236725b82b13..d36e888d58546 100644 --- a/lldb/source/Plugins/Highlighter/TreeSitter/CMakeLists.txt +++ b/lldb/source/Plugins/Highlighter/TreeSitter/CMakeLists.txt @@ -41,4 +41,5 @@ function(add_tree_sitter_grammar name source_dir binary_dir) ) endfunction() +add_subdirectory(Rust) add_subdirectory(Swift) diff --git a/lldb/source/Plugins/Highlighter/TreeSitter/README.md b/lldb/source/Plugins/Highlighter/TreeSitter/README.md index 3a631b6879b36..cc8405be5d196 100644 --- a/lldb/source/Plugins/Highlighter/TreeSitter/README.md +++ b/lldb/source/Plugins/Highlighter/TreeSitter/README.md @@ -14,3 +14,4 @@ Each plugin contains a vendored copy of the corresponding grammar in the ## Supported Languages - Swift based on [swift-tree-sitter](https://github.com/tree-sitter/swift-tree-sitter) 0.9.0 +- Rust based on [tree-sitter-rust](https://github.com/tree-sitter/tree-sitter-rust) 0.24.0 diff --git a/lldb/source/Plugins/Highlighter/TreeSitter/Rust/CMakeLists.txt b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/CMakeLists.txt new file mode 100644 index 0000000000000..25499022937cb --- /dev/null +++ b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/CMakeLists.txt @@ -0,0 +1,16 @@ +add_lldb_library(lldbPluginHighlighterTreeSitterRust PLUGIN + RustTreeSitterHighlighter.cpp + + LINK_COMPONENTS + Support + LINK_LIBS + lldbCore + lldbUtility + lldbTreeSitter + tree-sitter-rust +) + +add_tree_sitter_grammar(tree-sitter-rust + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} +) diff --git a/lldb/source/Plugins/Highlighter/TreeSitter/Rust/HighlightQuery.h.in b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/HighlightQuery.h.in new file mode 100644 index 0000000000000..4b5803efb97ec --- /dev/null +++ b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/HighlightQuery.h.in @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SOURCE_PLUGINS_HIGHLIGHTER_TREESITTER_RUST_HIGHLIGHTQUERY_H +#define LLDB_SOURCE_PLUGINS_HIGHLIGHTER_TREESITTER_RUST_HIGHLIGHTQUERY_H + +#include "llvm/ADT/StringRef.h" + +static constexpr llvm::StringLiteral highlight_query = + R"__(@HIGHLIGHT_QUERY@)__"; + +#endif // LLDB_SOURCE_PLUGINS_HIGHLIGHTER_TREESITTER_RUST_HIGHLIGHTQUERY_H diff --git a/lldb/source/Plugins/Highlighter/TreeSitter/Rust/RustTreeSitterHighlighter.cpp b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/RustTreeSitterHighlighter.cpp new file mode 100644 index 0000000000000..02e7601a926d2 --- /dev/null +++ b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/RustTreeSitterHighlighter.cpp @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "RustTreeSitterHighlighter.h" +#include "HighlightQuery.h" +#include "lldb/Target/Language.h" + +LLDB_PLUGIN_DEFINE_ADV(RustTreeSitterHighlighter, HighlighterTreeSitterRust) + +extern "C" { +const TSLanguage *tree_sitter_rust(); +} + +using namespace lldb_private; + +const TSLanguage *RustTreeSitterHighlighter::GetLanguage() const { + return tree_sitter_rust(); +} + +llvm::StringRef RustTreeSitterHighlighter::GetHighlightQuery() const { + return highlight_query; +} + +Highlighter * +RustTreeSitterHighlighter::CreateInstance(lldb::LanguageType language) { + if (language == lldb::eLanguageTypeRust) + return new RustTreeSitterHighlighter(); + return nullptr; +} + +void RustTreeSitterHighlighter::Initialize() { + PluginManager::RegisterPlugin(GetPluginNameStatic(), GetPluginNameStatic(), + CreateInstance); +} + +void RustTreeSitterHighlighter::Terminate() { + PluginManager::UnregisterPlugin(CreateInstance); +} diff --git a/lldb/source/Plugins/Highlighter/TreeSitter/Rust/RustTreeSitterHighlighter.h b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/RustTreeSitterHighlighter.h new file mode 100644 index 0000000000000..ea0e7321d460e --- /dev/null +++ b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/RustTreeSitterHighlighter.h @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SOURCE_PLUGINS_LANGUAGE_TREESITTERCOMMON_RUSTTREESITTERHIGHLIGHTER_H +#define LLDB_SOURCE_PLUGINS_LANGUAGE_TREESITTERCOMMON_RUSTTREESITTERHIGHLIGHTER_H + +#include "../TreeSitterHighlighter.h" +#include "llvm/ADT/StringRef.h" + +namespace lldb_private { + +class RustTreeSitterHighlighter : public TreeSitterHighlighter { +public: + RustTreeSitterHighlighter() = default; + ~RustTreeSitterHighlighter() override = default; + + llvm::StringRef GetName() const override { return "tree-sitter-rust"; } + + static Highlighter *CreateInstance(lldb::LanguageType language); + + static void Terminate(); + static void Initialize(); + + static llvm::StringRef GetPluginNameStatic() { + return "Tree-sitter Rust Highlighter"; + } + llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); } + +protected: + const TSLanguage *GetLanguage() const override; + llvm::StringRef GetHighlightQuery() const override; +}; + +} // namespace lldb_private + +#endif // LLDB_SOURCE_PLUGINS_LANGUAGE_TREESITTERCOMMON_RUSTTREESITTERHIGHLIGHTER_H diff --git a/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/LICENSE b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/LICENSE new file mode 100644 index 0000000000000..ceaf3c9adca54 --- /dev/null +++ b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Maxim Sokolov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/grammar.js b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/grammar.js new file mode 100644 index 0000000000000..1907a3a3f56ca --- /dev/null +++ b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/grammar.js @@ -0,0 +1,1690 @@ +/** + * @file Rust grammar for tree-sitter + * @author Maxim Sokolov <[email protected]> + * @author Max Brunsfeld <[email protected]> + * @author Amaan Qureshi <[email protected]> + * @license MIT + */ + +/// <reference types="tree-sitter-cli/dsl" /> +// @ts-check + +// https://doc.rust-lang.org/reference/expressions.html#expression-precedence +const PREC = { + call: 15, + field: 14, + try: 13, + unary: 12, + cast: 11, + multiplicative: 10, + additive: 9, + shift: 8, + bitand: 7, + bitxor: 6, + bitor: 5, + comparative: 4, + and: 3, + or: 2, + range: 1, + assign: 0, + closure: -1, +}; + +const numericTypes = [ + 'u8', + 'i8', + 'u16', + 'i16', + 'u32', + 'i32', + 'u64', + 'i64', + 'u128', + 'i128', + 'isize', + 'usize', + 'f32', + 'f64', +]; + +// https://doc.rust-lang.org/reference/tokens.html#punctuation +const TOKEN_TREE_NON_SPECIAL_PUNCTUATION = [ + '+', '-', '*', '/', '%', '^', '!', '&', '|', '&&', '||', '<<', + '>>', '+=', '-=', '*=', '/=', '%=', '^=', '&=', '|=', '<<=', + '>>=', '=', '==', '!=', '>', '<', '>=', '<=', '@', '_', '.', + '..', '...', '..=', ',', ';', ':', '::', '->', '=>', '#', '?', +]; + +const primitiveTypes = numericTypes.concat(['bool', 'str', 'char']); + +module.exports = grammar({ + name: 'rust', + + extras: $ => [ + /\s/, + $.line_comment, + $.block_comment, + ], + + externals: $ => [ + $.string_content, + $._raw_string_literal_start, + $.raw_string_literal_content, + $._raw_string_literal_end, + $.float_literal, + $._outer_block_doc_comment_marker, + $._inner_block_doc_comment_marker, + $._block_comment_content, + $._line_doc_content, + $._error_sentinel, + ], + + supertypes: $ => [ + $._expression, + $._type, + $._literal, + $._literal_pattern, + $._declaration_statement, + $._pattern, + ], + + inline: $ => [ + $._path, + $._type_identifier, + $._tokens, + $._field_identifier, + $._non_special_token, + $._declaration_statement, + $._reserved_identifier, + $._expression_ending_with_block, + ], + + conflicts: $ => [ + // Local ambiguity due to anonymous types: + // See https://internals.rust-lang.org/t/pre-rfc-deprecating-anonymous-parameters/3710 + [$._type, $._pattern], + [$.unit_type, $.tuple_pattern], + [$.scoped_identifier, $.scoped_type_identifier], + [$.parameters, $._pattern], + [$.parameters, $.tuple_struct_pattern], + [$.array_expression], + [$.visibility_modifier], + [$.visibility_modifier, $.scoped_identifier, $.scoped_type_identifier], + ], + + word: $ => $.identifier, + + rules: { + source_file: $ => seq( + optional($.shebang), + repeat($._statement), + ), + + _statement: $ => choice( + $.expression_statement, + $._declaration_statement, + ), + + empty_statement: _ => ';', + + expression_statement: $ => choice( + seq($._expression, ';'), + prec(1, $._expression_ending_with_block), + ), + + _declaration_statement: $ => choice( + $.const_item, + $.macro_invocation, + $.macro_definition, + $.empty_statement, + $.attribute_item, + $.inner_attribute_item, + $.mod_item, + $.foreign_mod_item, + $.struct_item, + $.union_item, + $.enum_item, + $.type_item, + $.function_item, + $.function_signature_item, + $.impl_item, + $.trait_item, + $.associated_type, + $.let_declaration, + $.use_declaration, + $.extern_crate_declaration, + $.static_item, + ), + + // Section - Macro definitions + + macro_definition: $ => { + const rules = seq( + repeat(seq($.macro_rule, ';')), + optional($.macro_rule), + ); + + return seq( + 'macro_rules!', + field('name', choice( + $.identifier, + $._reserved_identifier, + )), + choice( + seq('(', rules, ')', ';'), + seq('[', rules, ']', ';'), + seq('{', rules, '}'), + ), + ); + }, + + macro_rule: $ => seq( + field('left', $.token_tree_pattern), + '=>', + field('right', $.token_tree), + ), + + _token_pattern: $ => choice( + $.token_tree_pattern, + $.token_repetition_pattern, + $.token_binding_pattern, + $.metavariable, + $._non_special_token, + ), + + token_tree_pattern: $ => choice( + seq('(', repeat($._token_pattern), ')'), + seq('[', repeat($._token_pattern), ']'), + seq('{', repeat($._token_pattern), '}'), + ), + + token_binding_pattern: $ => prec(1, seq( + field('name', $.metavariable), + ':', + field('type', $.fragment_specifier), + )), + + token_repetition_pattern: $ => seq( + '$', '(', repeat($._token_pattern), ')', optional(/[^+*?]+/), choice('+', '*', '?'), + ), + + fragment_specifier: _ => choice( + 'block', 'expr', 'expr_2021', 'ident', 'item', 'lifetime', 'literal', 'meta', 'pat', + 'pat_param', 'path', 'stmt', 'tt', 'ty', 'vis', + ), + + _tokens: $ => choice( + $.token_tree, + $.token_repetition, + $.metavariable, + $._non_special_token, + ), + + token_tree: $ => choice( + seq('(', repeat($._tokens), ')'), + seq('[', repeat($._tokens), ']'), + seq('{', repeat($._tokens), '}'), + ), + + token_repetition: $ => seq( + '$', '(', repeat($._tokens), ')', optional(/[^+*?]+/), choice('+', '*', '?'), + ), + + // Matches non-delimiter tokens common to both macro invocations and + // definitions. This is everything except $ and metavariables (which begin + // with $). + _non_special_token: $ => choice( + $._literal, $.identifier, $.mutable_specifier, $.self, $.super, $.crate, + alias(choice(...primitiveTypes), $.primitive_type), + prec.right(repeat1(choice(...TOKEN_TREE_NON_SPECIAL_PUNCTUATION))), + '\'', + 'as', 'async', 'await', 'break', 'const', 'continue', 'default', 'enum', 'fn', 'for', 'gen', + 'if', 'impl', 'let', 'loop', 'match', 'mod', 'pub', 'return', 'static', 'struct', 'trait', + 'type', 'union', 'unsafe', 'use', 'where', 'while', + ), + + // Section - Declarations + + attribute_item: $ => seq( + '#', + '[', + $.attribute, + ']', + ), + + inner_attribute_item: $ => seq( + '#', + '!', + '[', + $.attribute, + ']', + ), + + attribute: $ => seq( + $._path, + optional(choice( + seq('=', field('value', $._expression)), + field('arguments', alias($.delim_token_tree, $.token_tree)), + )), + ), + + mod_item: $ => seq( + optional($.visibility_modifier), + 'mod', + field('name', $.identifier), + choice( + ';', + field('body', $.declaration_list), + ), + ), + + foreign_mod_item: $ => seq( + optional($.visibility_modifier), + $.extern_modifier, + choice( + ';', + field('body', $.declaration_list), + ), + ), + + declaration_list: $ => seq( + '{', + repeat($._declaration_statement), + '}', + ), + + struct_item: $ => seq( + optional($.visibility_modifier), + 'struct', + field('name', $._type_identifier), + field('type_parameters', optional($.type_parameters)), + choice( + seq( + optional($.where_clause), + field('body', $.field_declaration_list), + ), + seq( + field('body', $.ordered_field_declaration_list), + optional($.where_clause), + ';', + ), + ';', + ), + ), + + union_item: $ => seq( + optional($.visibility_modifier), + 'union', + field('name', $._type_identifier), + field('type_parameters', optional($.type_parameters)), + optional($.where_clause), + field('body', $.field_declaration_list), + ), + + enum_item: $ => seq( + optional($.visibility_modifier), + 'enum', + field('name', $._type_identifier), + field('type_parameters', optional($.type_parameters)), + optional($.where_clause), + field('body', $.enum_variant_list), + ), + + enum_variant_list: $ => seq( + '{', + sepBy(',', seq(repeat($.attribute_item), $.enum_variant)), + optional(','), + '}', + ), + + enum_variant: $ => seq( + optional($.visibility_modifier), + field('name', $.identifier), + field('body', optional(choice( + $.field_declaration_list, + $.ordered_field_declaration_list, + ))), + optional(seq( + '=', + field('value', $._expression), + )), + ), + + field_declaration_list: $ => seq( + '{', + sepBy(',', seq(repeat($.attribute_item), $.field_declaration)), + optional(','), + '}', + ), + + field_declaration: $ => seq( + optional($.visibility_modifier), + field('name', $._field_identifier), + ':', + field('type', $._type), + ), + + ordered_field_declaration_list: $ => seq( + '(', + sepBy(',', seq( + repeat($.attribute_item), + optional($.visibility_modifier), + field('type', $._type), + )), + optional(','), + ')', + ), + + extern_crate_declaration: $ => seq( + optional($.visibility_modifier), + 'extern', + $.crate, + field('name', $.identifier), + optional(seq( + 'as', + field('alias', $.identifier), + )), + ';', + ), + + const_item: $ => seq( + optional($.visibility_modifier), + 'const', + field('name', $.identifier), + ':', + field('type', $._type), + optional( + seq( + '=', + field('value', $._expression), + ), + ), + ';', + ), + + static_item: $ => seq( + optional($.visibility_modifier), + 'static', + + // Not actual rust syntax, but made popular by the lazy_static crate. + optional('ref'), + + optional($.mutable_specifier), + field('name', $.identifier), + ':', + field('type', $._type), + optional(seq( + '=', + field('value', $._expression), + )), + ';', + ), + + type_item: $ => seq( + optional($.visibility_modifier), + 'type', + field('name', $._type_identifier), + field('type_parameters', optional($.type_parameters)), + optional($.where_clause), + '=', + field('type', $._type), + optional($.where_clause), + ';', + ), + + function_item: $ => seq( + optional($.visibility_modifier), + optional($.function_modifiers), + 'fn', + field('name', choice($.identifier, $.metavariable)), + field('type_parameters', optional($.type_parameters)), + field('parameters', $.parameters), + optional(seq('->', field('return_type', $._type))), + optional($.where_clause), + field('body', $.block), + ), + + function_signature_item: $ => seq( + optional($.visibility_modifier), + optional($.function_modifiers), + 'fn', + field('name', choice($.identifier, $.metavariable)), + field('type_parameters', optional($.type_parameters)), + field('parameters', $.parameters), + optional(seq('->', field('return_type', $._type))), + optional($.where_clause), + ';', + ), + + function_modifiers: $ => repeat1(choice( + 'async', + 'default', + 'const', + 'unsafe', + $.extern_modifier, + )), + + where_clause: $ => prec.right(seq( + 'where', + optional(seq( + sepBy1(',', $.where_predicate), + optional(','), + )), + )), + + where_predicate: $ => seq( + field('left', choice( + $.lifetime, + $._type_identifier, + $.scoped_type_identifier, + $.generic_type, + $.reference_type, + $.pointer_type, + $.tuple_type, + $.array_type, + $.higher_ranked_trait_bound, + alias(choice(...primitiveTypes), $.primitive_type), + )), + field('bounds', $.trait_bounds), + ), + + impl_item: $ => seq( + optional('unsafe'), + 'impl', + field('type_parameters', optional($.type_parameters)), + optional(seq( + optional('!'), + field('trait', choice( + $._type_identifier, + $.scoped_type_identifier, + $.generic_type, + )), + 'for', + )), + field('type', $._type), + optional($.where_clause), + choice(field('body', $.declaration_list), ';'), + ), + + trait_item: $ => seq( + optional($.visibility_modifier), + optional('unsafe'), + 'trait', + field('name', $._type_identifier), + field('type_parameters', optional($.type_parameters)), + field('bounds', optional($.trait_bounds)), + optional($.where_clause), + field('body', $.declaration_list), + ), + + associated_type: $ => seq( + 'type', + field('name', $._type_identifier), + field('type_parameters', optional($.type_parameters)), + field('bounds', optional($.trait_bounds)), + optional($.where_clause), + ';', + ), + + trait_bounds: $ => seq( + ':', + sepBy1('+', choice( + $._type, + $.lifetime, + $.higher_ranked_trait_bound, + )), + ), + + higher_ranked_trait_bound: $ => seq( + 'for', + field('type_parameters', $.type_parameters), + field('type', $._type), + ), + + removed_trait_bound: $ => seq( + '?', + $._type, + ), + + type_parameters: $ => prec(1, seq( + '<', + sepBy1(',', seq( + repeat($.attribute_item), + choice( + $.metavariable, + $.type_parameter, + $.lifetime_parameter, + $.const_parameter, + ), + )), + optional(','), + '>', + )), + + const_parameter: $ => seq( + 'const', + field('name', $.identifier), + ':', + field('type', $._type), + optional( + seq( + '=', + field('value', + choice( + $.block, + $.identifier, + $._literal, + $.negative_literal, + ), + ), + ), + ), + ), + + type_parameter: $ => prec(1, seq( + field('name', $._type_identifier), + optional(field('bounds', $.trait_bounds)), + optional( + seq( + '=', + field('default_type', $._type), + ), + ), + )), + + lifetime_parameter: $ => prec(1, seq( + field('name', $.lifetime), + optional(field('bounds', $.trait_bounds)), + )), + + let_declaration: $ => seq( + 'let', + optional($.mutable_specifier), + field('pattern', $._pattern), + optional(seq( + ':', + field('type', $._type), + )), + optional(seq( + '=', + field('value', $._expression), + )), + optional(seq( + 'else', + field('alternative', $.block), + )), + ';', + ), + + use_declaration: $ => seq( + optional($.visibility_modifier), + 'use', + field('argument', $._use_clause), + ';', + ), + + _use_clause: $ => choice( + $._path, + $.use_as_clause, + $.use_list, + $.scoped_use_list, + $.use_wildcard, + ), + + scoped_use_list: $ => seq( + field('path', optional($._path)), + '::', + field('list', $.use_list), + ), + + use_list: $ => seq( + '{', + sepBy(',', choice( + $._use_clause, + )), + optional(','), + '}', + ), + + use_as_clause: $ => seq( + field('path', $._path), + 'as', + field('alias', $.identifier), + ), + + use_wildcard: $ => seq( + optional(seq(optional($._path), '::')), + '*', + ), + + parameters: $ => seq( + '(', + sepBy(',', seq( + optional($.attribute_item), + choice( + $.parameter, + $.self_parameter, + $.variadic_parameter, + '_', + $._type, + ))), + optional(','), + ')', + ), + + self_parameter: $ => seq( + optional('&'), + optional($.lifetime), + optional($.mutable_specifier), + $.self, + ), + + variadic_parameter: $ => seq( + optional($.mutable_specifier), + optional(seq( + field('pattern', $._pattern), + ':', + )), + '...', + ), + + parameter: $ => seq( + optional($.mutable_specifier), + field('pattern', choice( + $._pattern, + $.self, + )), + ':', + field('type', $._type), + ), + + extern_modifier: $ => seq( + 'extern', + optional($.string_literal), + ), + + visibility_modifier: $ => choice( + $.crate, + seq( + 'pub', + optional(seq( + '(', + choice( + $.self, + $.super, + $.crate, + seq('in', $._path), + ), + ')', + )), + ), + ), + + // Section - Types + + _type: $ => choice( + $.abstract_type, + $.reference_type, + $.metavariable, + $.pointer_type, + $.generic_type, + $.scoped_type_identifier, + $.tuple_type, + $.unit_type, + $.array_type, + $.function_type, + $._type_identifier, + $.macro_invocation, + $.never_type, + $.dynamic_type, + $.bounded_type, + $.removed_trait_bound, + alias(choice(...primitiveTypes), $.primitive_type), + ), + + bracketed_type: $ => seq( + '<', + choice( + $._type, + $.qualified_type, + ), + '>', + ), + + qualified_type: $ => seq( + field('type', $._type), + 'as', + field('alias', $._type), + ), + + lifetime: $ => prec(1, seq('\'', $.identifier)), + + array_type: $ => seq( + '[', + field('element', $._type), + optional(seq( + ';', + field('length', $._expression), + )), + ']', + ), + + for_lifetimes: $ => seq( + 'for', + '<', + sepBy1(',', $.lifetime), + optional(','), + '>', + ), + + function_type: $ => seq( + optional($.for_lifetimes), + prec(PREC.call, seq( + choice( + field('trait', choice( + $._type_identifier, + $.scoped_type_identifier, + )), + seq( + optional($.function_modifiers), + 'fn', + ), + ), + field('parameters', $.parameters), + )), + optional(seq('->', field('return_type', $._type))), + ), + + tuple_type: $ => seq( + '(', + sepBy1(',', $._type), + optional(','), + ')', + ), + + unit_type: _ => seq('(', ')'), + + generic_function: $ => prec(1, seq( + field('function', choice( + $.identifier, + $.scoped_identifier, + $.field_expression, + )), + '::', + field('type_arguments', $.type_arguments), + )), + + generic_type: $ => prec(1, seq( + field('type', choice( + $._type_identifier, + $._reserved_identifier, + $.scoped_type_identifier, + )), + field('type_arguments', $.type_arguments), + )), + + generic_type_with_turbofish: $ => seq( + field('type', choice( + $._type_identifier, + $.scoped_identifier, + )), + '::', + field('type_arguments', $.type_arguments), + ), + + bounded_type: $ => prec.left(-1, seq( + choice($.lifetime, $._type, $.use_bounds), + '+', + choice($.lifetime, $._type, $.use_bounds), + )), + + use_bounds: $ => seq( + 'use', + token(prec(1, '<')), + sepBy( + ',', + choice( + $.lifetime, + $._type_identifier, + ), + ), + optional(','), + '>', + ), + + type_arguments: $ => seq( + token(prec(1, '<')), + sepBy1(',', seq( + choice( + $._type, + $.type_binding, + $.lifetime, + $._literal, + $.block, + ), + optional($.trait_bounds), + )), + optional(','), + '>', + ), + + type_binding: $ => seq( + field('name', $._type_identifier), + field('type_arguments', optional($.type_arguments)), + '=', + field('type', $._type), + ), + + reference_type: $ => seq( + '&', + optional($.lifetime), + optional($.mutable_specifier), + field('type', $._type), + ), + + pointer_type: $ => seq( + '*', + choice('const', $.mutable_specifier), + field('type', $._type), + ), + + never_type: _ => '!', + + abstract_type: $ => seq( + 'impl', + optional(seq('for', $.type_parameters)), + field('trait', prec(1, choice( + $._type_identifier, + $.scoped_type_identifier, + $.removed_trait_bound, + $.generic_type, + $.function_type, + $.tuple_type, + $.bounded_type, + ))), + ), + + dynamic_type: $ => seq( + 'dyn', + field('trait', choice( + $.higher_ranked_trait_bound, + $._type_identifier, + $.scoped_type_identifier, + $.generic_type, + $.function_type, + $.tuple_type, + )), + ), + + mutable_specifier: _ => 'mut', + + // Section - Expressions + + _expression_except_range: $ => choice( + $.unary_expression, + $.reference_expression, + $.try_expression, + $.binary_expression, + $.assignment_expression, + $.compound_assignment_expr, + $.type_cast_expression, + $.call_expression, + $.return_expression, + $.yield_expression, + $._literal, + prec.left($.identifier), + alias(choice(...primitiveTypes), $.identifier), + prec.left($._reserved_identifier), + $.self, + $.scoped_identifier, + $.generic_function, + $.await_expression, + $.field_expression, + $.array_expression, + $.tuple_expression, + prec(1, $.macro_invocation), + $.unit_expression, + $.break_expression, + $.continue_expression, + $.index_expression, + $.metavariable, + $.closure_expression, + $.parenthesized_expression, + $.struct_expression, + $._expression_ending_with_block, + ), + + _expression: $ => choice( + $._expression_except_range, + $.range_expression, + ), + + _expression_ending_with_block: $ => choice( + $.unsafe_block, + $.async_block, + $.gen_block, + $.try_block, + $.block, + $.if_expression, + $.match_expression, + $.while_expression, + $.loop_expression, + $.for_expression, + $.const_block, + ), + + macro_invocation: $ => seq( + field('macro', choice( + $.scoped_identifier, + $.identifier, + $._reserved_identifier, + )), + '!', + alias($.delim_token_tree, $.token_tree), + ), + + delim_token_tree: $ => choice( + seq('(', repeat($._delim_tokens), ')'), + seq('[', repeat($._delim_tokens), ']'), + seq('{', repeat($._delim_tokens), '}'), + ), + + _delim_tokens: $ => choice( + $._non_delim_token, + alias($.delim_token_tree, $.token_tree), + ), + + // Should match any token other than a delimiter. + _non_delim_token: $ => choice( + $._non_special_token, + '$', + ), + + scoped_identifier: $ => seq( + field('path', optional(choice( + $._path, + $.bracketed_type, + alias($.generic_type_with_turbofish, $.generic_type), + ))), + '::', + field('name', choice($.identifier, $.super)), + ), + + scoped_type_identifier_in_expression_position: $ => prec(-2, seq( + field('path', optional(choice( + $._path, + alias($.generic_type_with_turbofish, $.generic_type), + ))), + '::', + field('name', $._type_identifier), + )), + + scoped_type_identifier: $ => seq( + field('path', optional(choice( + $._path, + alias($.generic_type_with_turbofish, $.generic_type), + $.bracketed_type, + $.generic_type, + ))), + '::', + field('name', $._type_identifier), + ), + + range_expression: $ => prec.left(PREC.range, choice( + seq($._expression, choice('..', '...', '..='), $._expression), + seq($._expression, '..'), + seq('..', $._expression), + '..', + )), + + unary_expression: $ => prec(PREC.unary, seq( + choice('-', '*', '!'), + $._expression, + )), + + try_expression: $ => prec(PREC.try, seq( + $._expression, + '?', + )), + + reference_expression: $ => prec(PREC.unary, seq( + '&', + choice( + seq('raw', choice('const', $.mutable_specifier)), + optional($.mutable_specifier), + ), + field('value', $._expression), + )), + + binary_expression: $ => { + const table = [ + [PREC.and, '&&'], + [PREC.or, '||'], + [PREC.bitand, '&'], + [PREC.bitor, '|'], + [PREC.bitxor, '^'], + [PREC.comparative, choice('==', '!=', '<', '<=', '>', '>=')], + [PREC.shift, choice('<<', '>>')], + [PREC.additive, choice('+', '-')], + [PREC.multiplicative, choice('*', '/', '%')], + ]; + + // @ts-ignore + return choice(...table.map(([precedence, operator]) => prec.left(precedence, seq( + field('left', $._expression), + // @ts-ignore + field('operator', operator), + field('right', $._expression), + )))); + }, + + assignment_expression: $ => prec.left(PREC.assign, seq( + field('left', $._expression), + '=', + field('right', $._expression), + )), + + compound_assignment_expr: $ => prec.left(PREC.assign, seq( + field('left', $._expression), + field('operator', choice('+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=')), + field('right', $._expression), + )), + + type_cast_expression: $ => prec.left(PREC.cast, seq( + field('value', $._expression), + 'as', + field('type', $._type), + )), + + return_expression: $ => choice( + prec.left(seq('return', $._expression)), + prec(-1, 'return'), + ), + + yield_expression: $ => choice( + prec.left(seq('yield', $._expression)), + prec(-1, 'yield'), + ), + + call_expression: $ => prec(PREC.call, seq( + field('function', $._expression_except_range), + field('arguments', $.arguments), + )), + + arguments: $ => seq( + '(', + sepBy(',', seq(repeat($.attribute_item), $._expression)), + optional(','), + ')', + ), + + array_expression: $ => seq( + '[', + repeat($.attribute_item), + choice( + seq( + $._expression, + ';', + field('length', $._expression), + ), + seq( + sepBy(',', seq(repeat($.attribute_item), $._expression)), + optional(','), + ), + ), + ']', + ), + + parenthesized_expression: $ => seq( + '(', + $._expression, + ')', + ), + + tuple_expression: $ => seq( + '(', + repeat($.attribute_item), + seq($._expression, ','), + repeat(seq($._expression, ',')), + optional($._expression), + ')', + ), + + unit_expression: _ => seq('(', ')'), + + struct_expression: $ => seq( + field('name', choice( + $._type_identifier, + alias($.scoped_type_identifier_in_expression_position, $.scoped_type_identifier), + $.generic_type_with_turbofish, + )), + field('body', $.field_initializer_list), + ), + + field_initializer_list: $ => seq( + '{', + sepBy(',', choice( + $.shorthand_field_initializer, + $.field_initializer, + $.base_field_initializer, + )), + optional(','), + '}', + ), + + shorthand_field_initializer: $ => seq( + repeat($.attribute_item), + $.identifier, + ), + + field_initializer: $ => seq( + repeat($.attribute_item), + field('field', choice($._field_identifier, $.integer_literal)), + ':', + field('value', $._expression), + ), + + base_field_initializer: $ => seq( + '..', + $._expression, + ), + + if_expression: $ => prec.right(seq( + 'if', + field('condition', $._condition), + field('consequence', $.block), + optional(field('alternative', $.else_clause)), + )), + + let_condition: $ => seq( + 'let', + field('pattern', $._pattern), + '=', + field('value', prec.left(PREC.and, $._expression)), + ), + + _let_chain: $ => prec.left(PREC.and, choice( + seq($._let_chain, '&&', $.let_condition), + seq($._let_chain, '&&', $._expression), + seq($.let_condition, '&&', $._expression), + seq($.let_condition, '&&', $.let_condition), + seq($._expression, '&&', $.let_condition), + )), + + _condition: $ => choice( + $._expression, + $.let_condition, + alias($._let_chain, $.let_chain), + ), + + else_clause: $ => seq( + 'else', + choice( + $.block, + $.if_expression, + ), + ), + + match_expression: $ => seq( + 'match', + field('value', $._expression), + field('body', $.match_block), + ), + + match_block: $ => seq( + '{', + optional(seq( + repeat($.match_arm), + alias($.last_match_arm, $.match_arm), + )), + '}', + ), + + match_arm: $ => prec.right(seq( + repeat(choice($.attribute_item, $.inner_attribute_item)), + field('pattern', $.match_pattern), + '=>', + choice( + seq(field('value', $._expression), ','), + field('value', prec(1, $._expression_ending_with_block)), + ), + )), + + last_match_arm: $ => seq( + repeat(choice($.attribute_item, $.inner_attribute_item)), + field('pattern', $.match_pattern), + '=>', + field('value', $._expression), + optional(','), + ), + + match_pattern: $ => seq( + $._pattern, + optional(seq('if', field('condition', $._condition))), + ), + + while_expression: $ => seq( + optional(seq($.label, ':')), + 'while', + field('condition', $._condition), + field('body', $.block), + ), + + loop_expression: $ => seq( + optional(seq($.label, ':')), + 'loop', + field('body', $.block), + ), + + for_expression: $ => seq( + optional(seq($.label, ':')), + 'for', + field('pattern', $._pattern), + 'in', + field('value', $._expression), + field('body', $.block), + ), + + const_block: $ => seq( + 'const', + field('body', $.block), + ), + + closure_expression: $ => prec(PREC.closure, seq( + optional('static'), + optional('async'), + optional('move'), + field('parameters', $.closure_parameters), + choice( + seq( + optional(seq('->', field('return_type', $._type))), + field('body', $.block), + ), + field('body', choice($._expression, '_')), + ), + )), + + closure_parameters: $ => seq( + '|', + sepBy(',', choice( + $._pattern, + $.parameter, + )), + '|', + ), + + label: $ => seq('\'', $.identifier), + + break_expression: $ => prec.left(seq('break', optional($.label), optional($._expression))), + + continue_expression: $ => prec.left(seq('continue', optional($.label))), + + index_expression: $ => prec(PREC.call, seq($._expression, '[', $._expression, ']')), + + await_expression: $ => prec(PREC.field, seq( + $._expression, + '.', + 'await', + )), + + field_expression: $ => prec(PREC.field, seq( + field('value', $._expression), + '.', + field('field', choice( + $._field_identifier, + $.integer_literal, + )), + )), + + unsafe_block: $ => seq( + 'unsafe', + $.block, + ), + + async_block: $ => seq( + 'async', + optional('move'), + $.block, + ), + + gen_block: $ => seq( + 'gen', + optional('move'), + $.block, + ), + + try_block: $ => seq( + 'try', + $.block, + ), + + block: $ => seq( + optional(seq($.label, ':')), + '{', + repeat($._statement), + optional($._expression), + '}', + ), + + // Section - Patterns + + _pattern: $ => choice( + $._literal_pattern, + alias(choice(...primitiveTypes), $.identifier), + $.identifier, + $.scoped_identifier, + $.generic_pattern, + $.tuple_pattern, + $.tuple_struct_pattern, + $.struct_pattern, + $._reserved_identifier, + $.ref_pattern, + $.slice_pattern, + $.captured_pattern, + $.reference_pattern, + $.remaining_field_pattern, + $.mut_pattern, + $.range_pattern, + $.or_pattern, + $.const_block, + $.macro_invocation, + '_', + ), + + generic_pattern: $ => seq( + choice( + $.identifier, + $.scoped_identifier, + ), + '::', + field('type_arguments', $.type_arguments), + ), + + tuple_pattern: $ => seq( + '(', + sepBy(',', choice($._pattern, $.closure_expression)), + optional(','), + ')', + ), + + slice_pattern: $ => seq( + '[', + sepBy(',', $._pattern), + optional(','), + ']', + ), + + tuple_struct_pattern: $ => seq( + field('type', choice( + $.identifier, + $.scoped_identifier, + alias($.generic_type_with_turbofish, $.generic_type), + )), + '(', + sepBy(',', $._pattern), + optional(','), + ')', + ), + + struct_pattern: $ => seq( + field('type', choice( + $._type_identifier, + $.scoped_type_identifier, + )), + '{', + sepBy(',', choice($.field_pattern, $.remaining_field_pattern)), + optional(','), + '}', + ), + + field_pattern: $ => seq( + optional('ref'), + optional($.mutable_specifier), + choice( + field('name', alias($.identifier, $.shorthand_field_identifier)), + seq( + field('name', $._field_identifier), + ':', + field('pattern', $._pattern), + ), + ), + ), + + remaining_field_pattern: _ => '..', + + mut_pattern: $ => prec(-1, seq( + $.mutable_specifier, + $._pattern, + )), + + range_pattern: $ => choice( + seq( + field('left', choice( + $._literal_pattern, + $._path, + )), + choice( + seq( + choice('...', '..=', '..'), + field('right', choice( + $._literal_pattern, + $._path, + )), + ), + '..', + ), + ), + seq( + choice('..=', '..'), + field('right', choice( + $._literal_pattern, + $._path, + )), + ), + ), + + ref_pattern: $ => seq( + 'ref', + $._pattern, + ), + + captured_pattern: $ => seq( + $.identifier, + '@', + $._pattern, + ), + + reference_pattern: $ => seq( + '&', + optional($.mutable_specifier), + $._pattern, + ), + + or_pattern: $ => prec.left(-2, choice( + seq($._pattern, '|', $._pattern), + seq('|', $._pattern), + )), + + // Section - Literals + + _literal: $ => choice( + $.string_literal, + $.raw_string_literal, + $.char_literal, + $.boolean_literal, + $.integer_literal, + $.float_literal, + ), + + _literal_pattern: $ => choice( + $.string_literal, + $.raw_string_literal, + $.char_literal, + $.boolean_literal, + $.integer_literal, + $.float_literal, + $.negative_literal, + ), + + negative_literal: $ => seq('-', choice($.integer_literal, $.float_literal)), + + integer_literal: _ => token(seq( + choice( + /[0-9][0-9_]*/, + /0x[0-9a-fA-F_]+/, + /0b[01_]+/, + /0o[0-7_]+/, + ), + optional(choice(...numericTypes)), + )), + + string_literal: $ => seq( + alias(/[bc]?"/, '"'), + repeat(choice( + $.escape_sequence, + $.string_content, + )), + token.immediate('"'), + ), + + raw_string_literal: $ => seq( + $._raw_string_literal_start, + alias($.raw_string_literal_content, $.string_content), + $._raw_string_literal_end, + ), + + char_literal: _ => token(seq( + optional('b'), + '\'', + optional(choice( + seq('\\', choice( + /[^xu]/, + /u[0-9a-fA-F]{4}/, + /u\{[0-9a-fA-F]+\}/, + /x[0-9a-fA-F]{2}/, + )), + /[^\\']/, + )), + '\'', + )), + + escape_sequence: _ => token.immediate( + seq('\\', + choice( + /[^xu]/, + /u[0-9a-fA-F]{4}/, + /u\{[0-9a-fA-F]+\}/, + /x[0-9a-fA-F]{2}/, + ), + )), + + boolean_literal: _ => choice('true', 'false'), + + comment: $ => choice( + $.line_comment, + $.block_comment, + ), + + line_comment: $ => seq( + // All line comments start with two // + '//', + // Then are followed by: + // - 2 or more slashes making it a regular comment + // - 1 slash or 1 or more bang operators making it a doc comment + // - or just content for the comment + choice( + // A tricky edge case where what looks like a doc comment is not + seq(token.immediate(prec(2, /\/\//)), /.*/), + // A regular doc comment + seq($._line_doc_comment_marker, field('doc', alias($._line_doc_content, $.doc_comment))), + token.immediate(prec(1, /.*/)), + ), + ), + + _line_doc_comment_marker: $ => choice( + // An outer line doc comment applies to the element that it is outside of + field('outer', alias($._outer_line_doc_comment_marker, $.outer_doc_comment_marker)), + // An inner line doc comment applies to the element it is inside of + field('inner', alias($._inner_line_doc_comment_marker, $.inner_doc_comment_marker)), + ), + + _inner_line_doc_comment_marker: _ => token.immediate(prec(2, '!')), + _outer_line_doc_comment_marker: _ => token.immediate(prec(2, '/')), + + block_comment: $ => seq( + '/*', + optional( + choice( + // Documentation block comments: /** docs */ or /*! docs */ + seq( + $._block_doc_comment_marker, + optional(field('doc', alias($._block_comment_content, $.doc_comment))), + ), + // Non-doc block comments + $._block_comment_content, + ), + ), + '*/', + ), + + _block_doc_comment_marker: $ => choice( + field('outer', alias($._outer_block_doc_comment_marker, $.outer_doc_comment_marker)), + field('inner', alias($._inner_block_doc_comment_marker, $.inner_doc_comment_marker)), + ), + + _path: $ => choice( + $.self, + alias(choice(...primitiveTypes), $.identifier), + $.metavariable, + $.super, + $.crate, + $.identifier, + $.scoped_identifier, + $._reserved_identifier, + ), + + identifier: _ => /(r#)?[_\p{XID_Start}][_\p{XID_Continue}]*/, + + shebang: _ => /#![\r\f\t\v ]*([^\[\n].*)?\n/, + + _reserved_identifier: $ => alias(choice( + 'default', + 'union', + 'gen', + ), $.identifier), + + _type_identifier: $ => alias($.identifier, $.type_identifier), + _field_identifier: $ => alias($.identifier, $.field_identifier), + + self: _ => 'self', + super: _ => 'super', + crate: _ => 'crate', + + metavariable: _ => /\$[a-zA-Z_]\w*/, + }, +}); + +/** + * Creates a rule to match one or more of the rules separated by the separator. + * + * @param {RuleOrLiteral} sep - The separator to use. + * @param {RuleOrLiteral} rule + * + * @returns {SeqRule} + */ +function sepBy1(sep, rule) { + return seq(rule, repeat(seq(sep, rule))); +} + + +/** + * Creates a rule to optionally match one or more of the rules separated by the separator. + * + * @param {RuleOrLiteral} sep - The separator to use. + * @param {RuleOrLiteral} rule + * + * @returns {ChoiceRule} + */ +function sepBy(sep, rule) { + return optional(sepBy1(sep, rule)); +} diff --git a/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/highlights.scm b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/highlights.scm new file mode 100644 index 0000000000000..48c7284ec2b1c --- /dev/null +++ b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/highlights.scm @@ -0,0 +1,161 @@ +; Identifiers + +(type_identifier) @type +(primitive_type) @type.builtin +(field_identifier) @property + +; Identifier conventions + +; Assume all-caps names are constants +((identifier) @constant + (#match? @constant "^[A-Z][A-Z\\d_]+$'")) + +; Assume uppercase names are enum constructors +((identifier) @constructor + (#match? @constructor "^[A-Z]")) + +; Assume that uppercase names in paths are types +((scoped_identifier + path: (identifier) @type) + (#match? @type "^[A-Z]")) +((scoped_identifier + path: (scoped_identifier + name: (identifier) @type)) + (#match? @type "^[A-Z]")) +((scoped_type_identifier + path: (identifier) @type) + (#match? @type "^[A-Z]")) +((scoped_type_identifier + path: (scoped_identifier + name: (identifier) @type)) + (#match? @type "^[A-Z]")) + +; Assume all qualified names in struct patterns are enum constructors. (They're +; either that, or struct names; highlighting both as constructors seems to be +; the less glaring choice of error, visually.) +(struct_pattern + type: (scoped_type_identifier + name: (type_identifier) @constructor)) + +; Function calls + +(call_expression + function: (identifier) @function) +(call_expression + function: (field_expression + field: (field_identifier) @function.method)) +(call_expression + function: (scoped_identifier + "::" + name: (identifier) @function)) + +(generic_function + function: (identifier) @function) +(generic_function + function: (scoped_identifier + name: (identifier) @function)) +(generic_function + function: (field_expression + field: (field_identifier) @function.method)) + +(macro_invocation + macro: (identifier) @function.macro + "!" @function.macro) + +; Function definitions + +(function_item (identifier) @function) +(function_signature_item (identifier) @function) + +(line_comment) @comment +(block_comment) @comment + +(line_comment (doc_comment)) @comment.documentation +(block_comment (doc_comment)) @comment.documentation + +"(" @punctuation.bracket +")" @punctuation.bracket +"[" @punctuation.bracket +"]" @punctuation.bracket +"{" @punctuation.bracket +"}" @punctuation.bracket + +(type_arguments + "<" @punctuation.bracket + ">" @punctuation.bracket) +(type_parameters + "<" @punctuation.bracket + ">" @punctuation.bracket) + +"::" @punctuation.delimiter +":" @punctuation.delimiter +"." @punctuation.delimiter +"," @punctuation.delimiter +";" @punctuation.delimiter + +(parameter (identifier) @variable.parameter) + +(lifetime (identifier) @label) + +"as" @keyword +"async" @keyword +"await" @keyword +"break" @keyword +"const" @keyword +"continue" @keyword +"default" @keyword +"dyn" @keyword +"else" @keyword +"enum" @keyword +"extern" @keyword +"fn" @keyword +"for" @keyword +"gen" @keyword +"if" @keyword +"impl" @keyword +"in" @keyword +"let" @keyword +"loop" @keyword +"macro_rules!" @keyword +"match" @keyword +"mod" @keyword +"move" @keyword +"pub" @keyword +"raw" @keyword +"ref" @keyword +"return" @keyword +"static" @keyword +"struct" @keyword +"trait" @keyword +"type" @keyword +"union" @keyword +"unsafe" @keyword +"use" @keyword +"where" @keyword +"while" @keyword +"yield" @keyword +(crate) @keyword +(mutable_specifier) @keyword +(use_list (self) @keyword) +(scoped_use_list (self) @keyword) +(scoped_identifier (self) @keyword) +(super) @keyword + +(self) @variable.builtin + +(char_literal) @string +(string_literal) @string +(raw_string_literal) @string + +(boolean_literal) @constant.builtin +(integer_literal) @constant.builtin +(float_literal) @constant.builtin + +(escape_sequence) @escape + +(attribute_item) @attribute +(inner_attribute_item) @attribute + +"*" @operator +"&" @operator +"'" @operator diff --git a/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/scanner.c b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/scanner.c new file mode 100644 index 0000000000000..269f6b2af61c4 --- /dev/null +++ b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/scanner.c @@ -0,0 +1,393 @@ +#include "tree_sitter/alloc.h" +#include "tree_sitter/parser.h" + +#include <wctype.h> + +enum TokenType { + STRING_CONTENT, + RAW_STRING_LITERAL_START, + RAW_STRING_LITERAL_CONTENT, + RAW_STRING_LITERAL_END, + FLOAT_LITERAL, + BLOCK_OUTER_DOC_MARKER, + BLOCK_INNER_DOC_MARKER, + BLOCK_COMMENT_CONTENT, + LINE_DOC_CONTENT, + ERROR_SENTINEL +}; + +typedef struct { + uint8_t opening_hash_count; +} Scanner; + +void *tree_sitter_rust_external_scanner_create() { return ts_calloc(1, sizeof(Scanner)); } + +void tree_sitter_rust_external_scanner_destroy(void *payload) { ts_free((Scanner *)payload); } + +unsigned tree_sitter_rust_external_scanner_serialize(void *payload, char *buffer) { + Scanner *scanner = (Scanner *)payload; + buffer[0] = (char)scanner->opening_hash_count; + return 1; +} + +void tree_sitter_rust_external_scanner_deserialize(void *payload, const char *buffer, unsigned length) { + Scanner *scanner = (Scanner *)payload; + scanner->opening_hash_count = 0; + if (length == 1) { + Scanner *scanner = (Scanner *)payload; + scanner->opening_hash_count = buffer[0]; + } +} + +static inline bool is_num_char(int32_t c) { return c == '_' || iswdigit(c); } + +static inline void advance(TSLexer *lexer) { lexer->advance(lexer, false); } + +static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } + +static inline bool process_string(TSLexer *lexer) { + bool has_content = false; + for (;;) { + if (lexer->lookahead == '\"' || lexer->lookahead == '\\') { + break; + } + if (lexer->eof(lexer)) { + return false; + } + has_content = true; + advance(lexer); + } + lexer->result_symbol = STRING_CONTENT; + lexer->mark_end(lexer); + return has_content; +} + +static inline bool scan_raw_string_start(Scanner *scanner, TSLexer *lexer) { + if (lexer->lookahead == 'b' || lexer->lookahead == 'c') { + advance(lexer); + } + if (lexer->lookahead != 'r') { + return false; + } + advance(lexer); + + uint8_t opening_hash_count = 0; + while (lexer->lookahead == '#') { + advance(lexer); + opening_hash_count++; + } + + if (lexer->lookahead != '"') { + return false; + } + advance(lexer); + scanner->opening_hash_count = opening_hash_count; + + lexer->result_symbol = RAW_STRING_LITERAL_START; + return true; +} + +static inline bool scan_raw_string_content(Scanner *scanner, TSLexer *lexer) { + for (;;) { + if (lexer->eof(lexer)) { + return false; + } + if (lexer->lookahead == '"') { + lexer->mark_end(lexer); + advance(lexer); + unsigned hash_count = 0; + while (lexer->lookahead == '#' && hash_count < scanner->opening_hash_count) { + advance(lexer); + hash_count++; + } + if (hash_count == scanner->opening_hash_count) { + lexer->result_symbol = RAW_STRING_LITERAL_CONTENT; + return true; + } + } else { + advance(lexer); + } + } +} + +static inline bool scan_raw_string_end(Scanner *scanner, TSLexer *lexer) { + advance(lexer); + for (unsigned i = 0; i < scanner->opening_hash_count; i++) { + advance(lexer); + } + lexer->result_symbol = RAW_STRING_LITERAL_END; + return true; +} + +static inline bool process_float_literal(TSLexer *lexer) { + lexer->result_symbol = FLOAT_LITERAL; + + advance(lexer); + while (is_num_char(lexer->lookahead)) { + advance(lexer); + } + + bool has_fraction = false, has_exponent = false; + + if (lexer->lookahead == '.') { + has_fraction = true; + advance(lexer); + if (iswalpha(lexer->lookahead)) { + // The dot is followed by a letter: 1.max(2) => not a float but an integer + return false; + } + + if (lexer->lookahead == '.') { + return false; + } + while (is_num_char(lexer->lookahead)) { + advance(lexer); + } + } + + lexer->mark_end(lexer); + + if (lexer->lookahead == 'e' || lexer->lookahead == 'E') { + has_exponent = true; + advance(lexer); + if (lexer->lookahead == '+' || lexer->lookahead == '-') { + advance(lexer); + } + if (!is_num_char(lexer->lookahead)) { + return true; + } + advance(lexer); + while (is_num_char(lexer->lookahead)) { + advance(lexer); + } + + lexer->mark_end(lexer); + } + + if (!has_exponent && !has_fraction) { + return false; + } + + if (lexer->lookahead != 'u' && lexer->lookahead != 'i' && lexer->lookahead != 'f') { + return true; + } + advance(lexer); + if (!iswdigit(lexer->lookahead)) { + return true; + } + + while (iswdigit(lexer->lookahead)) { + advance(lexer); + } + + lexer->mark_end(lexer); + return true; +} + +static inline bool process_line_doc_content(TSLexer *lexer) { + lexer->result_symbol = LINE_DOC_CONTENT; + for (;;) { + if (lexer->eof(lexer)) { + return true; + } + if (lexer->lookahead == '\n') { + // Include the newline in the doc content node. + // Line endings are useful for markdown injection. + advance(lexer); + return true; + } + advance(lexer); + } +} + +typedef enum { + LeftForwardSlash, + LeftAsterisk, + Continuing, +} BlockCommentState; + +typedef struct { + BlockCommentState state; + unsigned nestingDepth; +} BlockCommentProcessing; + +static inline void process_left_forward_slash(BlockCommentProcessing *processing, char current) { + if (current == '*') { + processing->nestingDepth += 1; + } + processing->state = Continuing; +}; + +static inline void process_left_asterisk(BlockCommentProcessing *processing, char current, TSLexer *lexer) { + if (current == '*') { + lexer->mark_end(lexer); + processing->state = LeftAsterisk; + return; + } + + if (current == '/') { + processing->nestingDepth -= 1; + } + + processing->state = Continuing; +} + +static inline void process_continuing(BlockCommentProcessing *processing, char current) { + switch (current) { + case '/': + processing->state = LeftForwardSlash; + break; + case '*': + processing->state = LeftAsterisk; + break; + } +} + +static inline bool process_block_comment(TSLexer *lexer, const bool *valid_symbols) { + char first = (char)lexer->lookahead; + // The first character is stored so we can safely advance inside + // these if blocks. However, because we only store one, we can only + // safely advance 1 time. Since there's a chance that an advance could + // happen in one state, we must advance in all states to ensure that + // the program ends up in a sane state prior to processing the block + // comment if need be. + if (valid_symbols[BLOCK_INNER_DOC_MARKER] && first == '!') { + lexer->result_symbol = BLOCK_INNER_DOC_MARKER; + advance(lexer); + return true; + } + if (valid_symbols[BLOCK_OUTER_DOC_MARKER] && first == '*') { + advance(lexer); + lexer->mark_end(lexer); + // If the next token is a / that means that it's an empty block comment. + if (lexer->lookahead == '/') { + return false; + } + // If the next token is a * that means that this isn't a BLOCK_OUTER_DOC_MARKER + // as BLOCK_OUTER_DOC_MARKER's only have 2 * not 3 or more. + if (lexer->lookahead != '*') { + lexer->result_symbol = BLOCK_OUTER_DOC_MARKER; + return true; + } + } else { + advance(lexer); + } + + if (valid_symbols[BLOCK_COMMENT_CONTENT]) { + BlockCommentProcessing processing = {Continuing, 1}; + // Manually set the current state based on the first character + switch (first) { + case '*': + processing.state = LeftAsterisk; + if (lexer->lookahead == '/') { + // This case can happen in an empty doc block comment + // like /*!*/. The comment has no contents, so bail. + return false; + } + break; + case '/': + processing.state = LeftForwardSlash; + break; + default: + processing.state = Continuing; + break; + } + + // For the purposes of actually parsing rust code, this + // is incorrect as it considers an unterminated block comment + // to be an error. However, for the purposes of syntax highlighting + // this should be considered successful as otherwise you are not able + // to syntax highlight a block of code prior to closing the + // block comment + while (!lexer->eof(lexer) && processing.nestingDepth != 0) { + // Set first to the current lookahead as that is the second character + // as we force an advance in the above code when we are checking if we + // need to handle a block comment inner or outer doc comment signifier + // node + first = (char)lexer->lookahead; + switch (processing.state) { + case LeftForwardSlash: + process_left_forward_slash(&processing, first); + break; + case LeftAsterisk: + process_left_asterisk(&processing, first, lexer); + break; + case Continuing: + lexer->mark_end(lexer); + process_continuing(&processing, first); + break; + default: + break; + } + advance(lexer); + if (first == '/' && processing.nestingDepth != 0) { + lexer->mark_end(lexer); + } + } + lexer->result_symbol = BLOCK_COMMENT_CONTENT; + return true; + } + + return false; +} + +bool tree_sitter_rust_external_scanner_scan(void *payload, TSLexer *lexer, const bool *valid_symbols) { + // The documentation states that if the lexical analysis fails for some reason + // they will mark every state as valid and pass it to the external scanner + // However, we can't do anything to help them recover in that case so we + // should just fail. + /* + link: https://tree-sitter.github.io/tree-sitter/creating-parsers#external-scanners + If a syntax error is encountered during regular parsing, Tree-sitter’s + first action during error recovery will be to call the external scanner’s + scan function with all tokens marked valid. The scanner should detect this + case and handle it appropriately. One simple method of detection is to add + an unused token to the end of the externals array, for example + + externals: $ => [$.token1, $.token2, $.error_sentinel], + + then check whether that token is marked valid to determine whether + Tree-sitter is in error correction mode. + */ + if (valid_symbols[ERROR_SENTINEL]) { + return false; + } + + Scanner *scanner = (Scanner *)payload; + + if (valid_symbols[BLOCK_COMMENT_CONTENT] || valid_symbols[BLOCK_INNER_DOC_MARKER] || + valid_symbols[BLOCK_OUTER_DOC_MARKER]) { + return process_block_comment(lexer, valid_symbols); + } + + if (valid_symbols[STRING_CONTENT] && !valid_symbols[FLOAT_LITERAL]) { + return process_string(lexer); + } + + if (valid_symbols[LINE_DOC_CONTENT]) { + return process_line_doc_content(lexer); + } + + while (iswspace(lexer->lookahead)) { + skip(lexer); + } + + if (valid_symbols[RAW_STRING_LITERAL_START] && + (lexer->lookahead == 'r' || lexer->lookahead == 'b' || lexer->lookahead == 'c')) { + return scan_raw_string_start(scanner, lexer); + } + + if (valid_symbols[RAW_STRING_LITERAL_CONTENT]) { + return scan_raw_string_content(scanner, lexer); + } + + if (valid_symbols[RAW_STRING_LITERAL_END] && lexer->lookahead == '"') { + return scan_raw_string_end(scanner, lexer); + } + + if (valid_symbols[FLOAT_LITERAL] && iswdigit(lexer->lookahead)) { + return process_float_literal(lexer); + } + + return false; +} diff --git a/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/tree-sitter.json b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/tree-sitter.json new file mode 100644 index 0000000000000..76a39633212ab --- /dev/null +++ b/lldb/source/Plugins/Highlighter/TreeSitter/Rust/tree-sitter-rust/tree-sitter.json @@ -0,0 +1,53 @@ +{ + "grammars": [ + { + "name": "rust", + "camelcase": "Rust", + "scope": "source.rust", + "path": ".", + "file-types": [ + "rs" + ], + "highlights": [ + "queries/highlights.scm" + ], + "injections": [ + "queries/injections.scm" + ], + "tags": [ + "queries/tags.scm" + ], + "injection-regex": "rust" + } + ], + "metadata": { + "version": "0.24.0", + "license": "MIT", + "description": "Rust grammar for tree-sitter", + "authors": [ + { + "name": "Maxim Sokolov", + "email": "[email protected]" + }, + { + "name": "Max Brunsfeld", + "email": "[email protected]" + }, + { + "name": "Amaan Qureshi", + "email": "[email protected]" + } + ], + "links": { + "repository": "https://github.com/tree-sitter/tree-sitter-rust" + } + }, + "bindings": { + "c": true, + "go": true, + "node": true, + "python": true, + "rust": true, + "swift": true + } +} diff --git a/lldb/unittests/Highlighter/CMakeLists.txt b/lldb/unittests/Highlighter/CMakeLists.txt index a4fc679cc931f..827020f207620 100644 --- a/lldb/unittests/Highlighter/CMakeLists.txt +++ b/lldb/unittests/Highlighter/CMakeLists.txt @@ -1,5 +1,8 @@ if (LLDB_ENABLE_TREESITTER) - set(SWIFT_HIGHLIGHTER_PLUGIN lldbPluginHighlighterTreeSitterSwift) + set(TREESITTER_HIGHLIGHTER_PLUGINS + lldbPluginHighlighterTreeSitterRust + lldbPluginHighlighterTreeSitterSwift + ) endif() add_lldb_unittest(HighlighterTests @@ -11,5 +14,5 @@ add_lldb_unittest(HighlighterTests lldbPluginCPlusPlusLanguage lldbPluginObjCLanguage lldbPluginObjCPlusPlusLanguage - ${SWIFT_HIGHLIGHTER_PLUGIN} + ${TREESITTER_HIGHLIGHTER_PLUGINS} ) diff --git a/lldb/unittests/Highlighter/HighlighterTest.cpp b/lldb/unittests/Highlighter/HighlighterTest.cpp index 048a04360473e..95d77f4f1303e 100644 --- a/lldb/unittests/Highlighter/HighlighterTest.cpp +++ b/lldb/unittests/Highlighter/HighlighterTest.cpp @@ -14,9 +14,11 @@ #include "Plugins/Language/ObjC/ObjCLanguage.h" #include "Plugins/Language/ObjCPlusPlus/ObjCPlusPlusLanguage.h" #include "lldb/Core/Highlighter.h" +#include "lldb/Host/Config.h" #include "lldb/Host/FileSystem.h" #if LLDB_ENABLE_TREESITTER +#include "Plugins/Highlighter/TreeSitter/Rust/RustTreeSitterHighlighter.h" #include "Plugins/Highlighter/TreeSitter/Swift/SwiftTreeSitterHighlighter.h" #endif @@ -31,7 +33,7 @@ class HighlighterTest : public testing::Test { // filename. SubsystemRAII<FileSystem, ClangHighlighter, #if LLDB_ENABLE_TREESITTER - SwiftTreeSitterHighlighter, + SwiftTreeSitterHighlighter, RustTreeSitterHighlighter, #endif DefaultHighlighter, CPlusPlusLanguage, ObjCLanguage, ObjCPlusPlusLanguage> @@ -59,6 +61,7 @@ TEST_F(HighlighterTest, HighlighterSelectionType) { #if LLDB_ENABLE_TREESITTER EXPECT_EQ(getName(lldb::eLanguageTypeSwift), "tree-sitter-swift"); + EXPECT_EQ(getName(lldb::eLanguageTypeRust), "tree-sitter-rust"); #endif EXPECT_EQ(getName(lldb::eLanguageTypeUnknown), "none"); @@ -440,4 +443,126 @@ TEST_F(HighlighterTest, SwiftClosures) { EXPECT_EQ(" <k>let</k> closure = { (x: <k>Int</k>) in return x * 2 }", highlightSwift(" let closure = { (x: Int) in return x * 2 }", s)); } + +static std::string +highlightRust(llvm::StringRef code, HighlightStyle style, + std::optional<size_t> cursor = std::optional<size_t>()) { + HighlighterManager mgr; + const Highlighter &h = + mgr.getHighlighterFor(lldb::eLanguageTypeRust, "main.rs"); + return h.Highlight(style, code, cursor); +} + +TEST_F(HighlighterTest, RustComments) { + HighlightStyle s; + s.comment.Set("<cc>", "</cc>"); + + EXPECT_EQ(" <cc>// I'm feeling lucky today</cc>", + highlightRust(" // I'm feeling lucky today", s)); + EXPECT_EQ(" <cc>/* This is a\nmultiline comment */</cc>", + highlightRust(" /* This is a\nmultiline comment */", s)); + EXPECT_EQ(" <cc>/* nested /* comment */ works */</cc>", + highlightRust(" /* nested /* comment */ works */", s)); + EXPECT_EQ(" <cc>/// Documentation comment</cc>", + highlightRust(" /// Documentation comment", s)); + EXPECT_EQ(" <cc>//! Inner doc comment</cc>", + highlightRust(" //! Inner doc comment", s)); +} + +TEST_F(HighlighterTest, RustKeywords) { + HighlightStyle s; + s.keyword.Set("<k>", "</k>"); + + EXPECT_EQ(" <k>let</k> x = 5;", highlightRust(" let x = 5;", s)); + EXPECT_EQ(" <k>let</k> <k>mut</k> y = 10;", + highlightRust(" let mut y = 10;", s)); + EXPECT_EQ(" <k>fn</k> foo() { <k>return</k> 42; }", + highlightRust(" fn foo() { return 42; }", s)); + EXPECT_EQ(" <k>struct</k> <k>Point</k> {}", + highlightRust(" struct Point {}", s)); + EXPECT_EQ(" <k>enum</k> <k>Color</k> {}", highlightRust(" enum Color {}", s)); + EXPECT_EQ(" <k>impl</k> <k>MyStruct</k> {}", + highlightRust(" impl MyStruct {}", s)); + EXPECT_EQ(" <k>trait</k> <k>MyTrait</k> {}", + highlightRust(" trait MyTrait {}", s)); + EXPECT_EQ(" <k>if</k> x { }", highlightRust(" if x { }", s)); + EXPECT_EQ(" <k>for</k> i <k>in</k> 0..10 { }", + highlightRust(" for i in 0..10 { }", s)); + EXPECT_EQ(" <k>while</k> x { }", highlightRust(" while x { }", s)); + EXPECT_EQ(" <k>match</k> x { _ => {} }", + highlightRust(" match x { _ => {} }", s)); + EXPECT_EQ(" <k>pub</k> <k>fn</k> foo() {}", + highlightRust(" pub fn foo() {}", s)); + EXPECT_EQ(" <k>const</k> MAX: u32 = 100;", + highlightRust(" const MAX: u32 = 100;", s)); + EXPECT_EQ(" <k>static</k> GLOBAL: i32 = 0;", + highlightRust(" static GLOBAL: i32 = 0;", s)); + EXPECT_EQ(" <k>if</k> <k>let</k> Some(foo) = foo_maybe {", + highlightRust(" if let Some(foo) = foo_maybe {", s, 0)); +} + +TEST_F(HighlighterTest, RustStringLiterals) { + HighlightStyle s; + s.string_literal.Set("<str>", "</str>"); + + EXPECT_EQ(" let s = <str>\"Hello, World!\"</str>;", + highlightRust(" let s = \"Hello, World!\";", s)); + EXPECT_EQ(" let raw = <str>r\"C:\\\\path\"</str>;", + highlightRust(" let raw = r\"C:\\\\path\";", s)); + EXPECT_EQ(" let raw2 = <str>r#\"He said \"hi\"\"#</str>;", + highlightRust(" let raw2 = r#\"He said \"hi\"\"#;", s)); + EXPECT_EQ(" let byte_str = <str>b\"bytes\"</str>;", + highlightRust(" let byte_str = b\"bytes\";", s)); +} + +TEST_F(HighlighterTest, RustScalarLiterals) { + HighlightStyle s; + s.scalar_literal.Set("<scalar>", "</scalar>"); + + EXPECT_EQ(" let i = 42;", highlightRust(" let i = 42;", s)); + EXPECT_EQ(" let hex = 0xFF;", highlightRust(" let hex = 0xFF;", s)); + EXPECT_EQ(" let bin = 0b1010;", highlightRust(" let bin = 0b1010;", s)); + EXPECT_EQ(" let oct = 0o77;", highlightRust(" let oct = 0o77;", s)); + EXPECT_EQ(" let f = 3.14;", highlightRust(" let f = 3.14;", s)); + EXPECT_EQ(" let typed = 42u32;", highlightRust(" let typed = 42u32;", s)); + EXPECT_EQ(" let c = 'x';", highlightRust(" let c = 'x';", s)); +} + +TEST_F(HighlighterTest, RustIdentifiers) { + HighlightStyle s; + s.identifier.Set("<id>", "</id>"); + + EXPECT_EQ(" let foo = <id>bar</id>();", + highlightRust(" let foo = bar();", s)); + EXPECT_EQ(" my_variable = 10;", highlightRust(" my_variable = 10;", s)); + EXPECT_EQ(" let x: i32 = 5", highlightRust(" let x: i32 = 5", s)); + EXPECT_EQ(" fn <id>foo</id>() -> String { }", + highlightRust(" fn foo() -> String { }", s)); + EXPECT_EQ(" fn <id>foo</id><'a>(x: &'a str) {}", + highlightRust(" fn foo<'a>(x: &'a str) {}", s)); + EXPECT_EQ(" struct Foo<'a> { x: &'a i32 }", + highlightRust(" struct Foo<'a> { x: &'a i32 }", s)); +} + +TEST_F(HighlighterTest, RustOperators) { + HighlightStyle s; + s.operators.Set("[", "]"); + + EXPECT_EQ(" 1+2-3[*]4/5", highlightRust(" 1+2-3*4/5", s)); + EXPECT_EQ(" x && y || z", highlightRust(" x && y || z", s)); + EXPECT_EQ(" a == b != c", highlightRust(" a == b != c", s)); + EXPECT_EQ(" x [&]y", highlightRust(" x &y", s)); + EXPECT_EQ(" [*]ptr", highlightRust(" *ptr", s)); +} + +TEST_F(HighlighterTest, RustCursorPosition) { + HighlightStyle s; + s.selected.Set("<c>", "</c>"); + + EXPECT_EQ("<c> </c>let x = 5;", highlightRust(" let x = 5;", s, 0)); + EXPECT_EQ(" <c>l</c>et x = 5;", highlightRust(" let x = 5;", s, 1)); + EXPECT_EQ(" l<c>e</c>t x = 5;", highlightRust(" let x = 5;", s, 2)); + EXPECT_EQ(" le<c>t</c> x = 5;", highlightRust(" let x = 5;", s, 3)); + EXPECT_EQ(" let<c> </c>x = 5;", highlightRust(" let x = 5;", s, 4)); +} #endif _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
