1 /*
2 SPDX-FileCopyrightText: 2007 Mirko Stocker <me@misto.ch>
3 SPDX-FileCopyrightText: 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
4 SPDX-FileCopyrightText: 2003, 2004 Anders Lund <anders@alweb.dk>
5 SPDX-FileCopyrightText: 2003 Hamish Rodda <rodda@kde.org>
6 SPDX-FileCopyrightText: 2001, 2002 Joseph Wenninger <jowenn@kde.org>
7 SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org>
8 SPDX-FileCopyrightText: 1999 Jochen Wilhelmy <digisnap@cs.tu-berlin.de>
9
10 SPDX-License-Identifier: LGPL-2.0-or-later
11 */
12
13 // BEGIN INCLUDES
14 #include "katehighlight.h"
15
16 #include "katedocument.h"
17 #include "katesyntaxmanager.h"
18 // END
19
20 // BEGIN KateHighlighting
KateHighlighting(const KSyntaxHighlighting::Definition & def)21 KateHighlighting::KateHighlighting(const KSyntaxHighlighting::Definition &def)
22 {
23 // get name and section, always works
24 iName = def.name();
25 iSection = def.translatedSection();
26
27 // get all included definitions, e.g. PHP for HTML highlighting
28 auto definitions = def.includedDefinitions();
29
30 // handle the "no highlighting" case
31 // it's possible to not have any definitions with malformed file
32 if (!def.isValid() || (definitions.isEmpty() && def.formats().isEmpty())) {
33 // dummy properties + formats
34 m_properties.resize(1);
35 m_propertiesForFormat.push_back(&m_properties[0]);
36 m_formats.resize(1);
37 m_formatsIdToIndex.insert(std::make_pair(m_formats[0].id(), 0));
38
39 // be done, all below is just for the real highlighting variants
40 return;
41 }
42
43 // handle the real highlighting case
44 noHl = false;
45 iHidden = def.isHidden();
46 identifier = def.filePath();
47 iStyle = def.style();
48 m_indentation = def.indenter();
49 folding = def.foldingEnabled();
50 m_foldingIndentationSensitive = def.indentationBasedFoldingEnabled();
51
52 // tell the AbstractHighlighter the definition it shall use
53 setDefinition(def);
54
55 embeddedHighlightingModes.reserve(definitions.size());
56 // first: handle only really included definitions
57 for (const auto &includedDefinition : std::as_const(definitions)) {
58 embeddedHighlightingModes.push_back(includedDefinition.name());
59 }
60
61 // now: handle all, including this definition itself
62 // create the format => attributes mapping
63 // collect embedded highlightings, too
64 //
65 // we start with our definition as we want to have the default format
66 // of the initial definition as attribute with index == 0
67 //
68 // we collect additional properties in the m_properties and
69 // map the formats to the right properties in m_propertiesForFormat
70 definitions.push_front(definition());
71 m_properties.resize(definitions.size());
72 size_t propertiesIndex = 0;
73 for (const auto &includedDefinition : std::as_const(definitions)) {
74 auto &properties = m_properties[propertiesIndex];
75 properties.definition = includedDefinition;
76 properties.emptyLines.reserve(includedDefinition.foldingIgnoreList().size());
77 for (const auto &emptyLine : includedDefinition.foldingIgnoreList()) {
78 properties.emptyLines.push_back(QRegularExpression(emptyLine, QRegularExpression::UseUnicodePropertiesOption));
79 }
80 properties.singleLineCommentMarker = includedDefinition.singleLineCommentMarker();
81 properties.singleLineCommentPosition = includedDefinition.singleLineCommentPosition();
82 const auto multiLineComment = includedDefinition.multiLineCommentMarker();
83 properties.multiLineCommentStart = multiLineComment.first;
84 properties.multiLineCommentEnd = multiLineComment.second;
85
86 // collect character characters
87 for (const auto &enc : includedDefinition.characterEncodings()) {
88 properties.characterEncodingsPrefixStore.addPrefix(enc.second);
89 properties.characterEncodings[enc.second] = enc.first;
90 properties.reverseCharacterEncodings[enc.first] = enc.second;
91 }
92
93 // collect formats
94 for (const auto &format : includedDefinition.formats()) {
95 // register format id => internal attributes, we want no clashs
96 const auto nextId = m_formats.size();
97 m_formatsIdToIndex.insert(std::make_pair(format.id(), nextId));
98 m_formats.push_back(format);
99 m_propertiesForFormat.push_back(&properties);
100 }
101
102 // advance to next properties
103 ++propertiesIndex;
104 }
105 }
106
doHighlight(const Kate::TextLineData * prevLine,Kate::TextLineData * textLine,const Kate::TextLineData * nextLine,bool & ctxChanged,int tabWidth)107 void KateHighlighting::doHighlight(const Kate::TextLineData *prevLine,
108 Kate::TextLineData *textLine,
109 const Kate::TextLineData *nextLine,
110 bool &ctxChanged,
111 int tabWidth)
112 {
113 // default: no context change
114 ctxChanged = false;
115
116 // no text line => nothing to do
117 if (!textLine) {
118 return;
119 }
120
121 // in all cases, remove old hl, or we will grow to infinite ;)
122 textLine->clearAttributesAndFoldings();
123
124 // reset folding start
125 textLine->clearMarkedAsFoldingStart();
126
127 // no hl set, nothing to do more than the above cleaning ;)
128 if (noHl) {
129 return;
130 }
131
132 // ensure we arrive in clean state
133 Q_ASSERT(!m_textLineToHighlight);
134 Q_ASSERT(m_foldingStartToCount.isEmpty());
135
136 // highlight the given line via the abstract highlighter
137 // a bit ugly: we set the line to highlight as member to be able to update its stats in the applyFormat and applyFolding member functions
138 m_textLineToHighlight = textLine;
139 const KSyntaxHighlighting::State initialState(!prevLine ? KSyntaxHighlighting::State() : prevLine->highlightingState());
140 const KSyntaxHighlighting::State endOfLineState = highlightLine(textLine->string(), initialState);
141 m_textLineToHighlight = nullptr;
142
143 // update highlighting state if needed
144 if (textLine->highlightingState() != endOfLineState) {
145 textLine->setHighlightingState(endOfLineState);
146 ctxChanged = true;
147 }
148
149 // handle folding info computed and cleanup hash again, if there
150 // check if folding is not balanced and we have more starts then ends
151 // then this line is a possible folding start!
152 if (!m_foldingStartToCount.isEmpty()) {
153 // possible folding start, if imbalanced, aka hash not empty!
154 textLine->markAsFoldingStartAttribute();
155
156 // clear hash for next doHighlight
157 m_foldingStartToCount.clear();
158 }
159
160 // check for indentation based folding
161 if (m_foldingIndentationSensitive && (tabWidth > 0) && !textLine->markedAsFoldingStartAttribute()) {
162 // compute if we increase indentation in next line
163 if (endOfLineState.indentationBasedFoldingEnabled() && !isEmptyLine(textLine) && !isEmptyLine(nextLine)
164 && (textLine->indentDepth(tabWidth) < nextLine->indentDepth(tabWidth))) {
165 textLine->markAsFoldingStartIndentation();
166 }
167 }
168 }
169
applyFormat(int offset,int length,const KSyntaxHighlighting::Format & format)170 void KateHighlighting::applyFormat(int offset, int length, const KSyntaxHighlighting::Format &format)
171 {
172 // WE ATM assume ascending offset order
173 Q_ASSERT(m_textLineToHighlight);
174 if (!format.isValid()) {
175 return;
176 }
177
178 // get internal attribute, must exist
179 const auto it = m_formatsIdToIndex.find(format.id());
180 Q_ASSERT(it != m_formatsIdToIndex.end());
181
182 // remember highlighting info in our textline
183 m_textLineToHighlight->addAttribute(Kate::TextLineData::Attribute(offset, length, it->second));
184 }
185
applyFolding(int offset,int length,KSyntaxHighlighting::FoldingRegion region)186 void KateHighlighting::applyFolding(int offset, int length, KSyntaxHighlighting::FoldingRegion region)
187 {
188 // WE ATM assume ascending offset order, we add the length to the offset for the folding ends to have ranges spanning the full folding region
189 Q_ASSERT(m_textLineToHighlight);
190 Q_ASSERT(region.isValid());
191 const int foldingValue = (region.type() == KSyntaxHighlighting::FoldingRegion::Begin) ? int(region.id()) : -int(region.id());
192 m_textLineToHighlight->addFolding(offset + ((region.type() == KSyntaxHighlighting::FoldingRegion::Begin) ? 0 : length), length, foldingValue);
193
194 // for each end region, decrement counter for that type, erase if count reaches 0!
195 if (foldingValue < 0) {
196 QHash<int, int>::iterator end = m_foldingStartToCount.find(-foldingValue);
197 if (end != m_foldingStartToCount.end()) {
198 if (end.value() > 1) {
199 --(end.value());
200 } else {
201 m_foldingStartToCount.erase(end);
202 }
203 }
204 }
205
206 // increment counter for each begin region!
207 if (foldingValue > 0) {
208 ++m_foldingStartToCount[foldingValue];
209 }
210 }
211
sanitizeFormatIndex(int attrib) const212 int KateHighlighting::sanitizeFormatIndex(int attrib) const
213 {
214 // sanitize, e.g. one could have old hl info with now invalid attribs
215 if (attrib < 0 || size_t(attrib) >= m_formats.size()) {
216 return 0;
217 }
218 return attrib;
219 }
220
getCharacterEncodings(int attrib) const221 const QHash<QString, QChar> &KateHighlighting::getCharacterEncodings(int attrib) const
222 {
223 return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->characterEncodings;
224 }
225
getCharacterEncodingsPrefixStore(int attrib) const226 const KatePrefixStore &KateHighlighting::getCharacterEncodingsPrefixStore(int attrib) const
227 {
228 return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->characterEncodingsPrefixStore;
229 }
230
getReverseCharacterEncodings(int attrib) const231 const QHash<QChar, QString> &KateHighlighting::getReverseCharacterEncodings(int attrib) const
232 {
233 return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->reverseCharacterEncodings;
234 }
235
attributeRequiresSpellchecking(int attr)236 bool KateHighlighting::attributeRequiresSpellchecking(int attr)
237 {
238 return m_formats[sanitizeFormatIndex(attr)].spellCheck();
239 }
240
defaultStyleForAttribute(int attr) const241 KTextEditor::DefaultStyle KateHighlighting::defaultStyleForAttribute(int attr) const
242 {
243 return textStyleToDefaultStyle(m_formats[sanitizeFormatIndex(attr)].textStyle());
244 }
245
nameForAttrib(int attrib) const246 QString KateHighlighting::nameForAttrib(int attrib) const
247 {
248 const auto &format = m_formats.at(sanitizeFormatIndex(attrib));
249 return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->definition.name() + QLatin1Char(':')
250 + QString(format.isValid() ? format.name() : QStringLiteral("Normal"));
251 }
252
isInWord(QChar c,int attrib) const253 bool KateHighlighting::isInWord(QChar c, int attrib) const
254 {
255 return !m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->definition.isWordDelimiter(c) && !c.isSpace() && c != QLatin1Char('"')
256 && c != QLatin1Char('\'') && c != QLatin1Char('`');
257 }
258
canBreakAt(QChar c,int attrib) const259 bool KateHighlighting::canBreakAt(QChar c, int attrib) const
260 {
261 return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->definition.isWordWrapDelimiter(c) && c != QLatin1Char('"') && c != QLatin1Char('\'');
262 }
263
emptyLines(int attrib) const264 const QVector<QRegularExpression> &KateHighlighting::emptyLines(int attrib) const
265 {
266 return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->emptyLines;
267 }
268
canComment(int startAttrib,int endAttrib) const269 bool KateHighlighting::canComment(int startAttrib, int endAttrib) const
270 {
271 const auto startProperties = m_propertiesForFormat.at(sanitizeFormatIndex(startAttrib));
272 const auto endProperties = m_propertiesForFormat.at(sanitizeFormatIndex(endAttrib));
273 return (startProperties == endProperties
274 && ((!startProperties->multiLineCommentStart.isEmpty() && !startProperties->multiLineCommentEnd.isEmpty())
275 || !startProperties->singleLineCommentMarker.isEmpty()));
276 }
277
getCommentStart(int attrib) const278 QString KateHighlighting::getCommentStart(int attrib) const
279 {
280 return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->multiLineCommentStart;
281 }
282
getCommentEnd(int attrib) const283 QString KateHighlighting::getCommentEnd(int attrib) const
284 {
285 return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->multiLineCommentEnd;
286 }
287
getCommentSingleLineStart(int attrib) const288 QString KateHighlighting::getCommentSingleLineStart(int attrib) const
289 {
290 return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->singleLineCommentMarker;
291 }
292
getCommentSingleLinePosition(int attrib) const293 KSyntaxHighlighting::CommentPosition KateHighlighting::getCommentSingleLinePosition(int attrib) const
294 {
295 return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->singleLineCommentPosition;
296 }
297
characterEncodings(int attrib) const298 const QHash<QString, QChar> &KateHighlighting::characterEncodings(int attrib) const
299 {
300 return m_propertiesForFormat.at(sanitizeFormatIndex(attrib))->characterEncodings;
301 }
302
clearAttributeArrays()303 void KateHighlighting::clearAttributeArrays()
304 {
305 // just clear the hashed attributes, we create them lazy again
306 m_attributeArrays.clear();
307 }
308
attributesForDefinition(const QString & schema) const309 QVector<KTextEditor::Attribute::Ptr> KateHighlighting::attributesForDefinition(const QString &schema) const
310 {
311 // create list of known attributes based on highlighting format & wanted theme
312 QVector<KTextEditor::Attribute::Ptr> array;
313 array.reserve(m_formats.size());
314 const auto currentTheme = KateHlManager::self()->repository().theme(schema);
315 for (const auto &format : m_formats) {
316 // create a KTextEditor attribute matching the given format
317 KTextEditor::Attribute::Ptr newAttribute(new KTextEditor::Attribute(nameForAttrib(array.size()), textStyleToDefaultStyle(format.textStyle())));
318
319 if (const auto color = format.textColor(currentTheme).rgba()) {
320 newAttribute->setForeground(QColor::fromRgba(color));
321 }
322
323 if (const auto color = format.selectedTextColor(currentTheme).rgba()) {
324 newAttribute->setSelectedForeground(QColor::fromRgba(color));
325 }
326
327 if (const auto color = format.backgroundColor(currentTheme).rgba()) {
328 newAttribute->setBackground(QColor::fromRgba(color));
329 } else {
330 newAttribute->clearBackground();
331 }
332
333 if (const auto color = format.selectedBackgroundColor(currentTheme).rgba()) {
334 newAttribute->setSelectedBackground(QColor::fromRgba(color));
335 } else {
336 newAttribute->clearProperty(SelectedBackground);
337 }
338
339 newAttribute->setFontBold(format.isBold(currentTheme));
340 newAttribute->setFontItalic(format.isItalic(currentTheme));
341 newAttribute->setFontUnderline(format.isUnderline(currentTheme));
342 newAttribute->setFontStrikeOut(format.isStrikeThrough(currentTheme));
343 newAttribute->setSkipSpellChecking(format.spellCheck());
344 array.append(newAttribute);
345 }
346 return array;
347 }
348
attributes(const QString & schema)349 QVector<KTextEditor::Attribute::Ptr> KateHighlighting::attributes(const QString &schema)
350 {
351 // query cache first
352 if (m_attributeArrays.contains(schema)) {
353 return m_attributeArrays[schema];
354 }
355
356 // create new attributes array for wanted theme and cache it
357 const auto array = attributesForDefinition(schema);
358 m_attributeArrays.insert(schema, array);
359 return array;
360 }
361
getEmbeddedHighlightingModes() const362 QStringList KateHighlighting::getEmbeddedHighlightingModes() const
363 {
364 return embeddedHighlightingModes;
365 }
366
isEmptyLine(const Kate::TextLineData * textline) const367 bool KateHighlighting::isEmptyLine(const Kate::TextLineData *textline) const
368 {
369 const QString &txt = textline->string();
370 if (txt.isEmpty()) {
371 return true;
372 }
373
374 const auto &l = emptyLines(textline->attribute(0));
375 if (l.isEmpty()) {
376 return false;
377 }
378
379 for (const QRegularExpression &re : l) {
380 const QRegularExpressionMatch match = re.match(txt, 0, QRegularExpression::NormalMatch, QRegularExpression::AnchoredMatchOption);
381 if (match.hasMatch() && match.capturedLength() == txt.length()) {
382 return true;
383 }
384 }
385
386 return false;
387 }
388
attributeForLocation(KTextEditor::DocumentPrivate * doc,const KTextEditor::Cursor cursor)389 int KateHighlighting::attributeForLocation(KTextEditor::DocumentPrivate *doc, const KTextEditor::Cursor cursor)
390 {
391 // Validate parameters to prevent out of range access
392 if (cursor.line() < 0 || cursor.line() >= doc->lines() || cursor.column() < 0) {
393 return 0;
394 }
395
396 // get highlighted line
397 Kate::TextLine tl = doc->kateTextLine(cursor.line());
398
399 // make sure the textline is a valid pointer
400 if (!tl) {
401 return 0;
402 }
403
404 // either get char attribute or attribute of context still active at end of line
405 if (cursor.column() < tl->length()) {
406 return sanitizeFormatIndex(tl->attribute(cursor.column()));
407 } else if (cursor.column() >= tl->length()) {
408 if (!tl->attributesList().isEmpty()) {
409 return sanitizeFormatIndex(tl->attributesList().back().attributeValue);
410 }
411 }
412 return 0;
413 }
414
keywordsForLocation(KTextEditor::DocumentPrivate * doc,const KTextEditor::Cursor cursor)415 QStringList KateHighlighting::keywordsForLocation(KTextEditor::DocumentPrivate *doc, const KTextEditor::Cursor cursor)
416 {
417 // FIXME-SYNTAX: was before more precise, aka context level
418 const auto &def = m_propertiesForFormat.at(attributeForLocation(doc, cursor))->definition;
419 QStringList keywords;
420 keywords.reserve(def.keywordLists().size());
421 for (const auto &keylist : def.keywordLists()) {
422 keywords += def.keywordList(keylist);
423 }
424 return keywords;
425 }
426
spellCheckingRequiredForLocation(KTextEditor::DocumentPrivate * doc,const KTextEditor::Cursor cursor)427 bool KateHighlighting::spellCheckingRequiredForLocation(KTextEditor::DocumentPrivate *doc, const KTextEditor::Cursor cursor)
428 {
429 return m_formats.at(attributeForLocation(doc, cursor)).spellCheck();
430 }
431
higlightingModeForLocation(KTextEditor::DocumentPrivate * doc,const KTextEditor::Cursor cursor)432 QString KateHighlighting::higlightingModeForLocation(KTextEditor::DocumentPrivate *doc, const KTextEditor::Cursor cursor)
433 {
434 return m_propertiesForFormat.at(attributeForLocation(doc, cursor))->definition.name();
435 }
436
437 // END
438