Serenity Operating System
at master 218 lines 8.6 kB view raw
1/* 2 * Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include "SyntaxHighlighter.h" 8#include <LibCMake/Lexer.h> 9#include <LibCMake/Token.h> 10 11namespace CMake { 12 13static Syntax::TextStyle style_for_token_type(Gfx::Palette const& palette, Token::Type type) 14{ 15 switch (type) { 16 case Token::Type::BracketComment: 17 case Token::Type::LineComment: 18 return { palette.syntax_comment() }; 19 case Token::Type::Identifier: 20 return { palette.syntax_function() }; 21 case Token::Type::ControlKeyword: 22 return { palette.syntax_control_keyword() }; 23 case Token::Type::OpenParen: 24 case Token::Type::CloseParen: 25 return { palette.syntax_punctuation() }; 26 case Token::Type::BracketArgument: 27 return { palette.syntax_parameter() }; 28 case Token::Type::QuotedArgument: 29 return { palette.syntax_string() }; 30 case Token::Type::UnquotedArgument: 31 return { palette.syntax_parameter() }; 32 case Token::Type::Garbage: 33 return { palette.red() }; 34 case Token::Type::VariableReference: 35 // This is a bit arbitrary, since we don't have a color specifically for this. 36 return { palette.syntax_preprocessor_value() }; 37 default: 38 return { palette.base_text() }; 39 } 40} 41 42bool SyntaxHighlighter::is_identifier(u64 token_type) const 43{ 44 auto cmake_token = static_cast<Token::Type>(token_type); 45 return cmake_token == Token::Type::Identifier; 46} 47 48void SyntaxHighlighter::rehighlight(Gfx::Palette const& palette) 49{ 50 auto text = m_client->get_text(); 51 auto tokens = Lexer::lex(text).release_value_but_fixme_should_propagate_errors(); 52 auto& document = m_client->get_document(); 53 54 struct OpenBlock { 55 Token token; 56 int open_paren_count { 0 }; 57 Optional<Token> ending_paren {}; 58 }; 59 Vector<OpenBlock> open_blocks; 60 Vector<GUI::TextDocumentFoldingRegion> folding_regions; 61 Vector<GUI::TextDocumentSpan> spans; 62 auto highlight_span = [&](Token::Type type, Position const& start, Position const& end) { 63 GUI::TextDocumentSpan span; 64 span.range.set_start({ start.line, start.column }); 65 span.range.set_end({ end.line, end.column }); 66 if (!span.range.is_valid()) 67 return; 68 69 auto style = style_for_token_type(palette, type); 70 span.attributes.color = style.color; 71 span.attributes.bold = style.bold; 72 if (type == Token::Type::Garbage) { 73 span.attributes.underline = true; 74 span.attributes.underline_color = palette.red(); 75 span.attributes.underline_style = Gfx::TextAttributes::UnderlineStyle::Wavy; 76 } 77 span.is_skippable = false; 78 span.data = static_cast<u64>(type); 79 spans.append(move(span)); 80 }; 81 82 auto create_region_from_block_type = [&](auto control_keywords, Token const& end_token) { 83 if (open_blocks.is_empty()) 84 return; 85 86 // Find the most recent open block with a matching keyword. 87 Optional<size_t> found_index; 88 OpenBlock open_block; 89 for (int i = open_blocks.size() - 1; i >= 0; i--) { 90 for (auto value : control_keywords) { 91 if (open_blocks[i].token.control_keyword == value) { 92 found_index = i; 93 open_block = open_blocks[i]; 94 break; 95 } 96 } 97 if (found_index.has_value()) 98 break; 99 } 100 101 if (found_index.has_value()) { 102 // Remove the found token and all after it. 103 open_blocks.shrink(found_index.value()); 104 105 // Create a region. 106 GUI::TextDocumentFoldingRegion region; 107 if (open_block.ending_paren.has_value()) { 108 region.range.set_start({ open_block.ending_paren->end.line, open_block.ending_paren->end.column }); 109 } else { 110 // The opening command is invalid, it does not have a closing paren. 111 // So, we just start the region at the end of the line where the command identifier was. (eg, `if`) 112 region.range.set_start({ open_block.token.end.line, document.line(open_block.token.end.line).last_non_whitespace_column().value() }); 113 } 114 region.range.set_end({ end_token.start.line, end_token.start.column }); 115 folding_regions.append(move(region)); 116 } 117 }; 118 119 for (auto const& token : tokens) { 120 if (token.type == Token::Type::QuotedArgument || token.type == Token::Type::UnquotedArgument) { 121 // Alternately highlight the regular/variable-reference parts. 122 // 0-length ranges are caught in highlight_span() so we don't have to worry about them. 123 Position previous_position = token.start; 124 for (auto const& reference : token.variable_references) { 125 highlight_span(token.type, previous_position, reference.start); 126 highlight_span(Token::Type::VariableReference, reference.start, reference.end); 127 previous_position = reference.end; 128 } 129 highlight_span(token.type, previous_position, token.end); 130 continue; 131 } 132 133 highlight_span(token.type, token.start, token.end); 134 135 if (!open_blocks.is_empty() && !open_blocks.last().ending_paren.has_value()) { 136 auto& open_block = open_blocks.last(); 137 if (token.type == Token::Type::OpenParen) { 138 open_block.open_paren_count++; 139 } else if (token.type == Token::Type::CloseParen) { 140 open_block.open_paren_count--; 141 if (open_block.open_paren_count == 0) 142 open_block.ending_paren = token; 143 } 144 } 145 146 // Create folding regions from control-keyword blocks. 147 if (token.type == Token::Type::ControlKeyword) { 148 switch (token.control_keyword.value()) { 149 case ControlKeywordType::If: 150 open_blocks.empend(token); 151 break; 152 case ControlKeywordType::ElseIf: 153 case ControlKeywordType::Else: 154 create_region_from_block_type(Array { ControlKeywordType::If, ControlKeywordType::ElseIf }, token); 155 open_blocks.empend(token); 156 break; 157 case ControlKeywordType::EndIf: 158 create_region_from_block_type(Array { ControlKeywordType::If, ControlKeywordType::ElseIf, ControlKeywordType::Else }, token); 159 break; 160 case ControlKeywordType::ForEach: 161 open_blocks.empend(token); 162 break; 163 case ControlKeywordType::EndForEach: 164 create_region_from_block_type(Array { ControlKeywordType::ForEach }, token); 165 break; 166 case ControlKeywordType::While: 167 open_blocks.empend(token); 168 break; 169 case ControlKeywordType::EndWhile: 170 create_region_from_block_type(Array { ControlKeywordType::While }, token); 171 break; 172 case ControlKeywordType::Macro: 173 open_blocks.empend(token); 174 break; 175 case ControlKeywordType::EndMacro: 176 create_region_from_block_type(Array { ControlKeywordType::Macro }, token); 177 break; 178 case ControlKeywordType::Function: 179 open_blocks.empend(token); 180 break; 181 case ControlKeywordType::EndFunction: 182 create_region_from_block_type(Array { ControlKeywordType::Function }, token); 183 break; 184 case ControlKeywordType::Block: 185 open_blocks.empend(token); 186 break; 187 case ControlKeywordType::EndBlock: 188 create_region_from_block_type(Array { ControlKeywordType::Block }, token); 189 break; 190 default: 191 break; 192 } 193 } 194 } 195 m_client->do_set_spans(move(spans)); 196 m_client->do_set_folding_regions(move(folding_regions)); 197 198 m_has_brace_buddies = false; 199 highlight_matching_token_pair(); 200 201 m_client->do_update(); 202} 203 204Vector<SyntaxHighlighter::MatchingTokenPair> SyntaxHighlighter::matching_token_pairs_impl() const 205{ 206 static Vector<MatchingTokenPair> pairs; 207 if (pairs.is_empty()) { 208 pairs.append({ static_cast<u64>(Token::Type::OpenParen), static_cast<u64>(Token::Type::CloseParen) }); 209 } 210 return pairs; 211} 212 213bool SyntaxHighlighter::token_types_equal(u64 token1, u64 token2) const 214{ 215 return static_cast<Token::Type>(token1) == static_cast<Token::Type>(token2); 216} 217 218}