KSyntaxHighlighting

abstracthighlighter.cpp
1 /*
2  SPDX-FileCopyrightText: 2016 Volker Krause <[email protected]>
3 
4  SPDX-License-Identifier: MIT
5 */
6 
7 #include "abstracthighlighter.h"
8 #include "abstracthighlighter_p.h"
9 #include "context_p.h"
10 #include "definition_p.h"
11 #include "foldingregion.h"
12 #include "format.h"
13 #include "ksyntaxhighlighting_logging.h"
14 #include "repository.h"
15 #include "rule_p.h"
16 #include "state.h"
17 #include "state_p.h"
18 #include "theme.h"
19 
20 using namespace KSyntaxHighlighting;
21 
22 AbstractHighlighterPrivate::AbstractHighlighterPrivate()
23 {
24 }
25 
26 AbstractHighlighterPrivate::~AbstractHighlighterPrivate()
27 {
28 }
29 
30 void AbstractHighlighterPrivate::ensureDefinitionLoaded()
31 {
32  auto defData = DefinitionData::get(m_definition);
33  if (Q_UNLIKELY(!m_definition.isValid() && defData->repo && !m_definition.name().isEmpty())) {
34  qCDebug(Log) << "Definition became invalid, trying re-lookup.";
35  m_definition = defData->repo->definitionForName(m_definition.name());
36  defData = DefinitionData::get(m_definition);
37  }
38 
39  if (Q_UNLIKELY(!defData->repo && !defData->fileName.isEmpty())) {
40  qCCritical(Log) << "Repository got deleted while a highlighter is still active!";
41  }
42 
43  if (m_definition.isValid()) {
44  defData->load();
45  }
46 }
47 
48 AbstractHighlighter::AbstractHighlighter()
49  : d_ptr(new AbstractHighlighterPrivate)
50 {
51 }
52 
53 AbstractHighlighter::AbstractHighlighter(AbstractHighlighterPrivate *dd)
54  : d_ptr(dd)
55 {
56 }
57 
58 AbstractHighlighter::~AbstractHighlighter()
59 {
60  delete d_ptr;
61 }
62 
64 {
65  return d_ptr->m_definition;
66 }
67 
69 {
71  d->m_definition = def;
72 }
73 
75 {
76  Q_D(const AbstractHighlighter);
77  return d->m_theme;
78 }
79 
81 {
83  d->m_theme = theme;
84 }
85 
86 /**
87  * Returns the index of the first non-space character. If the line is empty,
88  * or only contains white spaces, text.size() is returned.
89  */
90 static inline int firstNonSpaceChar(QStringView text)
91 {
92  for (int i = 0; i < text.length(); ++i) {
93  if (!text[i].isSpace()) {
94  return i;
95  }
96  }
97  return text.size();
98 }
99 
100 #if KSYNTAXHIGHLIGHTING_BUILD_DEPRECATED_SINCE(5, 87)
101 State AbstractHighlighter::highlightLine(const QString &text, const State &state)
102 {
103  return highlightLine(QStringView(text), state);
104 }
105 #endif
106 
108 {
110 
111  // verify definition, deal with no highlighting being enabled
112  d->ensureDefinitionLoaded();
113  const auto defData = DefinitionData::get(d->m_definition);
114  if (!d->m_definition.isValid() || !defData->isLoaded()) {
115  applyFormat(0, text.size(), Format());
116  return State();
117  }
118 
119  // verify/initialize state
120  auto newState = state;
121  auto stateData = StateData::get(newState);
122  const auto definitionId = DefinitionData::get(d->m_definition)->id;
123  if (!stateData->isEmpty() && stateData->m_defId != definitionId) {
124  qCDebug(Log) << "Got invalid state, resetting.";
125  stateData->clear();
126  }
127  if (stateData->isEmpty()) {
128  stateData->push(defData->initialContext(), QStringList());
129  stateData->m_defId = definitionId;
130  }
131 
132  // process empty lines
133  if (text.isEmpty()) {
134  /**
135  * handle line empty context switches
136  * guard against endless loops
137  * see https://phabricator.kde.org/D18509
138  */
139  int endlessLoopingCounter = 0;
140  while (!stateData->topContext()->lineEmptyContext().isStay() || !stateData->topContext()->lineEndContext().isStay()) {
141  /**
142  * line empty context switches
143  */
144  if (!stateData->topContext()->lineEmptyContext().isStay()) {
145  if (!d->switchContext(stateData, stateData->topContext()->lineEmptyContext(), QStringList())) {
146  /**
147  * end when trying to #pop the main context
148  */
149  break;
150  }
151  /**
152  * line end context switches only when lineEmptyContext is #stay. This avoids
153  * skipping empty lines after a line continuation character (see bug 405903)
154  */
155  } else if (!d->switchContext(stateData, stateData->topContext()->lineEndContext(), QStringList())) {
156  break;
157  }
158 
159  // guard against endless loops
160  ++endlessLoopingCounter;
161  if (endlessLoopingCounter > 1024) {
162  qCDebug(Log) << "Endless switch context transitions for line empty context, aborting highlighting of line.";
163  break;
164  }
165  }
166  auto context = stateData->topContext();
167  applyFormat(0, 0, context->attributeFormat());
168  return newState;
169  }
170 
171  int offset = 0;
172  int beginOffset = 0;
173  bool lineContinuation = false;
174 
175  /**
176  * for expensive rules like regexes we do:
177  * - match them for the complete line, as this is faster than re-trying them at all positions
178  * - store the result of the first position that matches (or -1 for no match in the full line) in the skipOffsets hash for re-use
179  * - have capturesForLastDynamicSkipOffset as guard for dynamic regexes to invalidate the cache if they might have changed
180  */
181  QVarLengthArray<QPair<Rule *, int>, 8> skipOffsets;
182  QStringList capturesForLastDynamicSkipOffset;
183 
184  auto getSkipOffsetValue = [&skipOffsets](Rule *r) -> int {
185  auto i = std::find_if(skipOffsets.begin(), skipOffsets.end(), [r](const auto &v) {
186  return v.first == r;
187  });
188  if (i == skipOffsets.end())
189  return 0;
190  return i->second;
191  };
192 
193  auto insertSkipOffset = [&skipOffsets](Rule *r, int i) {
194  auto it = std::find_if(skipOffsets.begin(), skipOffsets.end(), [r](const auto &v) {
195  return v.first == r;
196  });
197  if (it == skipOffsets.end()) {
198  skipOffsets.push_back({r, i});
199  } else {
200  it->second = i;
201  }
202  };
203 
204  /**
205  * current active format
206  * stored as pointer to avoid deconstruction/constructions inside the internal loop
207  * the pointers are stable, the formats are either in the contexts or rules
208  */
209  auto currentFormat = &stateData->topContext()->attributeFormat();
210 
211  /**
212  * cached first non-space character, needs to be computed if < 0
213  */
214  int firstNonSpace = -1;
215  int lastOffset = offset;
216  int endlessLoopingCounter = 0;
217  do {
218  /**
219  * avoid that we loop endless for some broken hl definitions
220  */
221  if (lastOffset == offset) {
222  ++endlessLoopingCounter;
223  if (endlessLoopingCounter > 1024) {
224  qCDebug(Log) << "Endless state transitions, aborting highlighting of line.";
225  break;
226  }
227  } else {
228  // ensure we made progress, clear the endlessLoopingCounter
229  Q_ASSERT(offset > lastOffset);
230  lastOffset = offset;
231  endlessLoopingCounter = 0;
232  }
233 
234  /**
235  * try to match all rules in the context in order of declaration in XML
236  */
237  bool isLookAhead = false;
238  int newOffset = 0;
239  const Format *newFormat = nullptr;
240  for (const auto &rule : stateData->topContext()->rules()) {
241  /**
242  * filter out rules that require a specific column
243  */
244  if ((rule->requiredColumn() >= 0) && (rule->requiredColumn() != offset)) {
245  continue;
246  }
247 
248  /**
249  * filter out rules that only match for leading whitespace
250  */
251  if (rule->firstNonSpace()) {
252  /**
253  * compute the first non-space lazy
254  * avoids computing it for contexts without any such rules
255  */
256  if (firstNonSpace < 0) {
257  firstNonSpace = firstNonSpaceChar(text);
258  }
259 
260  /**
261  * can we skip?
262  */
263  if (offset > firstNonSpace) {
264  continue;
265  }
266  }
267 
268  /**
269  * shall we skip application of this rule? two cases:
270  * - rule can't match at all => currentSkipOffset < 0
271  * - rule will only match for some higher offset => currentSkipOffset > offset
272  *
273  * we need to invalidate this if we are dynamic and have different captures then last time
274  */
275  if (rule->isDynamic() && (capturesForLastDynamicSkipOffset != stateData->topCaptures())) {
276  skipOffsets.clear();
277  }
278  const auto currentSkipOffset = getSkipOffsetValue(rule.get());
279  if (currentSkipOffset < 0 || currentSkipOffset > offset) {
280  continue;
281  }
282 
283  const auto newResult = rule->doMatch(text, offset, stateData->topCaptures());
284  newOffset = newResult.offset();
285 
286  /**
287  * update skip offset if new one rules out any later match or is larger than current one
288  */
289  if (newResult.skipOffset() < 0 || newResult.skipOffset() > currentSkipOffset) {
290  insertSkipOffset(rule.get(), newResult.skipOffset());
291 
292  // remember new captures, if dynamic to enforce proper reset above on change!
293  if (rule->isDynamic()) {
294  capturesForLastDynamicSkipOffset = stateData->topCaptures();
295  }
296  }
297 
298  if (newOffset <= offset) {
299  continue;
300  }
301 
302  /**
303  * apply folding.
304  * special cases:
305  * - rule with endRegion + beginRegion: in endRegion, the length is 0
306  * - rule with lookAhead: length is 0
307  */
308  if (rule->endRegion().isValid() && rule->beginRegion().isValid()) {
309  applyFolding(offset, 0, rule->endRegion());
310  } else if (rule->endRegion().isValid()) {
311  applyFolding(offset, rule->isLookAhead() ? 0 : newOffset - offset, rule->endRegion());
312  }
313  if (rule->beginRegion().isValid()) {
314  applyFolding(offset, rule->isLookAhead() ? 0 : newOffset - offset, rule->beginRegion());
315  }
316 
317  if (rule->isLookAhead()) {
318  Q_ASSERT(!rule->context().isStay());
319  d->switchContext(stateData, rule->context(), newResult.captures());
320  isLookAhead = true;
321  break;
322  }
323 
324  d->switchContext(stateData, rule->context(), newResult.captures());
325  newFormat = rule->attributeFormat().isValid() ? &rule->attributeFormat() : &stateData->topContext()->attributeFormat();
326  if (newOffset == text.size() && rule->isLineContinue()) {
327  lineContinuation = true;
328  }
329  break;
330  }
331  if (isLookAhead) {
332  continue;
333  }
334 
335  if (newOffset <= offset) { // no matching rule
336  if (stateData->topContext()->fallthrough()) {
337  d->switchContext(stateData, stateData->topContext()->fallthroughContext(), QStringList());
338  continue;
339  }
340 
341  newOffset = offset + 1;
342  newFormat = &stateData->topContext()->attributeFormat();
343  }
344 
345  /**
346  * if we arrive here, some new format has to be set!
347  */
348  Q_ASSERT(newFormat);
349 
350  /**
351  * on format change, apply the last one and switch to new one
352  */
353  if (newFormat != currentFormat && newFormat->id() != currentFormat->id()) {
354  if (offset > 0) {
355  applyFormat(beginOffset, offset - beginOffset, *currentFormat);
356  }
357  beginOffset = offset;
358  currentFormat = newFormat;
359  }
360 
361  /**
362  * we must have made progress if we arrive here!
363  */
364  Q_ASSERT(newOffset > offset);
365  offset = newOffset;
366 
367  } while (offset < text.size());
368 
369  /**
370  * apply format for remaining text, if any
371  */
372  if (beginOffset < offset) {
373  applyFormat(beginOffset, text.size() - beginOffset, *currentFormat);
374  }
375 
376  /**
377  * handle line end context switches
378  * guard against endless loops
379  * see https://phabricator.kde.org/D18509
380  */
381  {
382  int endlessLoopingCounter = 0;
383  while (!stateData->topContext()->lineEndContext().isStay() && !lineContinuation) {
384  if (!d->switchContext(stateData, stateData->topContext()->lineEndContext(), QStringList())) {
385  break;
386  }
387 
388  // guard against endless loops
389  ++endlessLoopingCounter;
390  if (endlessLoopingCounter > 1024) {
391  qCDebug(Log) << "Endless switch context transitions for line end context, aborting highlighting of line.";
392  break;
393  }
394  }
395  }
396 
397  return newState;
398 }
399 
400 bool AbstractHighlighterPrivate::switchContext(StateData *data, const ContextSwitch &contextSwitch, const QStringList &captures)
401 {
402  // kill as many items as requested from the stack, will always keep the initial context alive!
403  const bool initialContextSurvived = data->pop(contextSwitch.popCount());
404 
405  // if we have a new context to add, push it
406  // then we always "succeed"
407  if (contextSwitch.context()) {
408  data->push(contextSwitch.context(), captures);
409  return true;
410  }
411 
412  // else we abort, if we did try to pop the initial context
413  return initialContextSurvived;
414 }
415 
416 void AbstractHighlighter::applyFolding(int offset, int length, FoldingRegion region)
417 {
418  Q_UNUSED(offset);
419  Q_UNUSED(length);
420  Q_UNUSED(region);
421 }
Opaque handle to the state of the highlighting engine.
Definition: state.h:25
Represents a begin or end of a folding region.
Definition: foldingregion.h:18
quint16 id() const
Returns a unique identifier of this format.
Definition: format.cpp:101
Describes the format to be used for a specific text fragment.
Definition: format.h:33
Abstract base class for highlighters.
qsizetype size() const const
void push_back(const T &t)
virtual void applyFormat(int offset, int length, const Format &format)=0
Reimplement this to apply formats to your output.
virtual void applyFolding(int offset, int length, FoldingRegion region)
Reimplement this to apply folding to your output.
void clear()
bool isEmpty() const const
State highlightLine(QStringView text, const State &state)
Highlight the given line.
QVarLengthArray::iterator begin()
virtual void setDefinition(const Definition &def)
Sets the syntax definition used for highlighting.
int length() const const
QVarLengthArray::iterator end()
Definition definition() const
Returns the syntax definition used for highlighting.
Theme theme() const
Returns the currently selected theme for highlighting.
Represents a syntax definition.
Definition: definition.h:86
Q_D(Todo)
virtual QVariant get(ScriptableExtension *callerPrincipal, quint64 objId, const QString &propName)
Color theme definition used for highlighting.
Definition: theme.h:64
virtual void setTheme(const Theme &theme)
Sets the theme used for highlighting.
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Wed Sep 27 2023 03:58:31 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.