Serenity Operating System
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}