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