"Fossies" - the Fresh Open Source Software Archive 
Member "texstudio-3.1.1/src/syntaxcheck.cpp" (21 Feb 2021, 43147 Bytes) of package /linux/misc/texstudio-3.1.1.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file.
For more information about "syntaxcheck.cpp" see the
Fossies "Dox" file reference documentation and the latest
Fossies "Diffs" side-by-side code changes report:
3.1.0_vs_3.1.1.
1 #include "syntaxcheck.h"
2 #include "latexdocument.h"
3 #include "latexeditorview_config.h"
4 #include "spellerutility.h"
5 #include "tablemanipulation.h"
6 #include "latexparser/latexparsing.h"
7
8 /*! \class SyntaxCheck
9 *
10 * asynchrnous thread which checks latex syntax of the text lines
11 * It gets the linehandle via a queue, together with a ticket number.
12 * The ticket number is increased with every change of the text of a line, thus it can be determined of the processed handle is still unchanged and can be discarded otherwise.
13 * Syntaxinformation are stated via markers on the text.
14 * Furthermore environment information, especially tabular information are stored in "cookies" as they are needed in subsequent lines.
15 *
16 */
17
18 /*!
19 * \brief contructor
20 * \param parent
21 */
22 SyntaxCheck::SyntaxCheck(QObject *parent) :
23 SafeThread(parent), mSyntaxChecking(true), syntaxErrorFormat(-1), ltxCommands(nullptr), newLtxCommandsAvailable(false), speller(nullptr), newSpeller(nullptr)
24 {
25 mLinesLock.lock();
26 stopped = false;
27 mLines.clear();
28 mLinesEnqueuedCounter.fetchAndStoreOrdered(0);
29 mLinesLock.unlock();
30 }
31
32 /*!
33 * \brief set the errorformat for syntax errors
34 * \param errFormat
35 */
36 void SyntaxCheck::setErrFormat(int errFormat)
37 {
38 syntaxErrorFormat = errFormat;
39 }
40
41 /*!
42 * \brief add line to queue
43 * \param dlh linehandle
44 * \param previous linehandle of previous line
45 * \param stack tokenstack at line start (for handling open arguments of previous commands)
46 * \param clearOverlay clear syntaxcheck overlay
47 */
48 void SyntaxCheck::putLine(QDocumentLineHandle *dlh, StackEnvironment previous, TokenStack stack, bool clearOverlay, int hint)
49 {
50 REQUIRE(dlh);
51 SyntaxLine newLine;
52 dlh->ref(); // impede deletion of handle while in syntax check queue
53 dlh->lockForRead();
54 newLine.ticket = dlh->getCurrentTicket();
55 dlh->unlock();
56 newLine.stack = stack;
57 newLine.dlh = dlh;
58 newLine.prevEnv = previous;
59 newLine.clearOverlay = clearOverlay;
60 newLine.hint=hint;
61 mLinesLock.lock();
62 mLines.enqueue(newLine);
63 mLinesEnqueuedCounter.ref();
64 mLinesLock.unlock();
65 //avoid reading of any results before this execution is stopped
66 //mResultLock.lock(); not possible under windows
67 mLinesAvailable.release();
68 }
69
70 /*!
71 * \brief stop processing syntax checks
72 */
73 void SyntaxCheck::stop()
74 {
75 stopped = true;
76 mLinesAvailable.release();
77 }
78
79 /*!
80 * \brief actual thread loop
81 */
82 void SyntaxCheck::run()
83 {
84 ltxCommands = new LatexParser();
85
86 forever {
87 //wait for enqueued lines
88 mLinesAvailable.acquire();
89 if (stopped) break;
90
91 if (newLtxCommandsAvailable) {
92 mLtxCommandLock.lock();
93 if (newLtxCommandsAvailable) {
94 newLtxCommandsAvailable = false;
95 *ltxCommands = newLtxCommands;
96 speller=newSpeller;
97 mReplacementList=newReplacementList;
98 mFormatList=newFormatList;
99 }
100 mLtxCommandLock.unlock();
101 }
102
103 // get Linedata
104 mLinesLock.lock();
105 SyntaxLine newLine = mLines.dequeue();
106 mLinesLock.unlock();
107 // do syntax check
108 newLine.dlh->lockForRead();
109 QString line = newLine.dlh->text();
110 if (newLine.dlh->hasCookie(QDocumentLine::UNCLOSED_ENVIRONMENT_COOKIE)) {
111 newLine.dlh->unlock();
112 newLine.dlh->lockForWrite();
113 newLine.dlh->removeCookie(QDocumentLine::UNCLOSED_ENVIRONMENT_COOKIE); //remove possible errors from unclosed envs
114 }
115 TokenList tl = newLine.dlh->getCookie(QDocumentLine::LEXER_COOKIE).value<TokenList>();
116 QPair<int,int> commentStart = newLine.dlh->getCookie(QDocumentLine::LEXER_COMMENTSTART_COOKIE).value<QPair<int,int> >();
117 newLine.dlh->unlock();
118
119 StackEnvironment activeEnv = newLine.prevEnv;
120 Ranges newRanges;
121
122 checkLine(line, newRanges, activeEnv, newLine.dlh, tl, newLine.stack, newLine.ticket,commentStart.first);
123 // place results
124 if (newLine.clearOverlay){
125 QList<int> fmtList={syntaxErrorFormat,SpellerUtility::spellcheckErrorFormat};
126 fmtList.append(mFormatList.values());
127 newLine.dlh->clearOverlays(fmtList);
128 }
129 //if(newRanges.isEmpty()) continue;
130 newLine.dlh->lockForWrite();
131 if (newLine.ticket == newLine.dlh->getCurrentTicket()) { // discard results if text has been changed meanwhile
132 newLine.dlh->setCookie(QDocumentLine::LEXER_COOKIE,QVariant::fromValue<TokenList>(tl));
133 foreach (const Error &elem, newRanges){
134 if(!mSyntaxChecking && (elem.type!=ERR_spelling) && (elem.type!=ERR_highlight) ){
135 // skip all syntax errors
136 continue;
137 }
138 int fmt= elem.type == ERR_spelling ? SpellerUtility::spellcheckErrorFormat : syntaxErrorFormat;
139 fmt= elem.type == ERR_highlight ? elem.format : fmt;
140 newLine.dlh->addOverlayNoLock(QFormatRange(elem.range.first, elem.range.second, fmt));
141 }
142 // active envs
143 QVariant oldEnvVar = newLine.dlh->getCookie(QDocumentLine::STACK_ENVIRONMENT_COOKIE);
144 StackEnvironment oldEnv;
145 if (oldEnvVar.isValid())
146 oldEnv = oldEnvVar.value<StackEnvironment>();
147 bool cookieChanged = !equalEnvStack(oldEnv, activeEnv);
148 //if excessCols has changed the subsequent lines need to be rechecked.
149 // don't on initial check
150 if (cookieChanged) {
151 QVariant env;
152 env.setValue(activeEnv);
153 newLine.dlh->setCookie(QDocumentLine::STACK_ENVIRONMENT_COOKIE, env);
154 newLine.dlh->ref(); // avoid being deleted while in queue
155 //qDebug() << newLine.dlh->text() << ":" << activeEnv.size();
156 emit checkNextLine(newLine.dlh, true, newLine.ticket, newLine.hint);
157 }
158 }
159 newLine.dlh->unlock();
160
161 newLine.dlh->deref(); //if deleted, delete now
162 }
163
164 delete ltxCommands;
165 ltxCommands = nullptr;
166 }
167
168 /*!
169 * \brief get error description for syntax error in line 'dlh' at column 'pos'
170 * \param dlh linehandle
171 * \param pos column
172 * \param previous environment stack at start of line
173 * \param stack tokenstack at start of line
174 * \return error description
175 */
176 QString SyntaxCheck::getErrorAt(QDocumentLineHandle *dlh, int pos, StackEnvironment previous, TokenStack stack)
177 {
178 // do syntax check
179 QString line = dlh->text();
180 QStack<Environment> activeEnv = previous;
181 TokenList tl = dlh->getCookieLocked(QDocumentLine::LEXER_COOKIE).value<TokenList>();
182 QPair<int,int> commentStart = dlh->getCookieLocked(QDocumentLine::LEXER_COMMENTSTART_COOKIE).value<QPair<int,int> >();
183 Ranges newRanges;
184 checkLine(line, newRanges, activeEnv, dlh, tl, stack, dlh->getCurrentTicket(),commentStart.first);
185 // add Error for unclosed env
186 QVariant var = dlh->getCookieLocked(QDocumentLine::UNCLOSED_ENVIRONMENT_COOKIE);
187 if (var.isValid()) {
188 activeEnv = var.value<StackEnvironment>();
189 Q_ASSERT_X(activeEnv.size() == 1, "SyntaxCheck", "Cookie error");
190 Environment env = activeEnv.top();
191 QString cmd = "\\begin{" + env.name + "}";
192 int index = line.lastIndexOf(cmd);
193 if (index >= 0) {
194 Error elem;
195 elem.range = QPair<int, int>(index, cmd.length());
196 elem.type = ERR_EnvNotClosed;
197 newRanges.append(elem);
198 }
199 }
200 // find Error at Position
201 ErrorType result = ERR_none;
202 foreach (const Error &elem, newRanges) {
203 if (elem.range.second + elem.range.first < pos) continue;
204 if (elem.range.first > pos) break;
205 result = elem.type;
206 }
207 // now generate Error message
208
209 QStringList messages; // indices have to match ErrorType
210 messages << tr("no error")
211 << tr("unrecognized environment")
212 << tr("unrecognized command")
213 << tr("unrecognized math command")
214 << tr("unrecognized tabular command")
215 << tr("tabular command outside tabular env")
216 << tr("math command outside math env")
217 << tr("tabbing command outside tabbing env")
218 << tr("more cols in tabular than specified")
219 << tr("cols in tabular missing")
220 << tr("\\\\ missing")
221 << tr("closing environment which has not been opened")
222 << tr("environment not closed")
223 << tr("unrecognized key in key option")
224 << tr("unrecognized value in key option")
225 << tr("command outside suitable env")
226 << tr("spelling")
227 << "highlight"; // mock message for arbitrary highlight. Will not be shown.
228 Q_ASSERT(messages.length() == ERR_MAX);
229 return messages.value(int(result), tr("unknown"));
230 }
231
232 /*!
233 * \brief set latex commands which are referenced for syntax checking
234 * \param cmds
235 */
236 void SyntaxCheck::setLtxCommands(const LatexParser &cmds)
237 {
238 if (stopped) return;
239 mLtxCommandLock.lock();
240 newLtxCommandsAvailable = true;
241 newLtxCommands = cmds;
242 mLtxCommandLock.unlock();
243 }
244
245 /*!
246 * \brief set new spellchecker engine (language)
247 * \param su new spell checker
248 */
249 void SyntaxCheck::setSpeller(SpellerUtility *su)
250 {
251 if (stopped) return;
252 mLtxCommandLock.lock();
253 newLtxCommandsAvailable = true;
254 newSpeller=su;
255 mLtxCommandLock.unlock();
256 }
257 /*!
258 * \brief enable showing of Syntax errors
259 * Since the syntax checker is also used for asynchronous syntax highligting/spell checking, it will not be disabled any more. Only syntax error will not be shown any more.
260 * \param enable
261 */
262 void SyntaxCheck::enableSyntaxCheck(const bool enable){
263 if (stopped) return;
264 mSyntaxChecking=enable;
265 }
266 /*!
267 * \brief set character/text replacementList for spell checking
268 * \param replacementList Map for characater/text replacement prior to spellchecking words. E.g. "u -> ΓΌ when german is activated
269 */
270 void SyntaxCheck::setReplacementList(QMap<QString, QString> replacementList)
271 {
272 if (stopped) return;
273 mLtxCommandLock.lock();
274 newLtxCommandsAvailable = true;
275 newReplacementList=replacementList;
276 mLtxCommandLock.unlock();
277 }
278
279 void SyntaxCheck::setFormats(QMap<QString, int> formatList)
280 {
281 if (stopped) return;
282 mLtxCommandLock.lock();
283 newLtxCommandsAvailable = true;
284 newFormatList=formatList;
285 mLtxCommandLock.unlock();
286 }
287
288 #ifndef NO_TESTS
289
290 /*!
291 * \brief Wait for syntax checker to finish processing.
292 * \details Wait for syntax checker to finish processing. This method should be used only in self-tests because
293 * in some rare cases it could return too early before the syntax checker queue is fully processsed.
294 */
295 void SyntaxCheck::waitForQueueProcess(void)
296 {
297 int linesBefore, linesAfter;
298
299 /*
300 * The logic in the following loop is not perfect because it could terminate the loop too early if it takes more
301 * than 10ms between the call to mLinesAvailable.acquire() and the following call to mLinesAvailable.release().
302 * Implementing the check properly requires bi-directional communication with the worker thread with commands to
303 * pause/unpause the worker thread which complicates the code too much just to handle testing.
304 */
305 linesBefore = mLinesEnqueuedCounter.fetchAndAddOrdered(0);
306 forever {
307 for (int i = 0; i < 2; ++i) {
308 QCoreApplication::processEvents(QEventLoop::AllEvents, 1000); // Process queued checkNextLine events
309 QCoreApplication::sendPostedEvents(Q_NULLPTR, QEvent::DeferredDelete); // Deferred delete must be processed explicitly. Using 0 for event_type does not work.
310 wait(5); // Give the checkNextLine signal handler time to queue the next line
311 }
312 linesAfter = mLinesEnqueuedCounter.fetchAndAddOrdered(0);
313 if ((linesBefore == linesAfter) && !mLinesAvailable.available()) {
314 break;
315 }
316 linesBefore = linesAfter;
317 }
318 }
319
320 #endif
321
322 /*!
323 * \brief check if top-most environment in 'envs' is `name`
324 * \param name environment name which is checked
325 * \param envs stack of environments
326 * \param id check for `id` of the environment, <0 means check is disabled
327 * \return environment id or 0
328 */
329 int SyntaxCheck::topEnv(const QString &name, const StackEnvironment &envs, const int id)
330 {
331 if (envs.isEmpty())
332 return 0;
333
334 Environment env = envs.top();
335 if (env.name == name) {
336 if (id < 0 || env.id == id)
337 return env.id;
338 }
339 if (id < 0 && ltxCommands->environmentAliases.contains(env.name)) {
340 QStringList altEnvs = ltxCommands->environmentAliases.values(env.name);
341 foreach (const QString &altEnv, altEnvs) {
342 if (altEnv == name)
343 return env.id;
344 }
345 }
346 return 0;
347 }
348
349 /*!
350 * \brief check if the environment stack contains a environment with name `name`
351 * \param parser reference to LatexParser. It is used to access environment aliases, e.g. equation is also a math environment
352 * \param name name of the checked environment
353 * \param envs stack of environements
354 * \param id if >=0 check if the env has the given id.
355 * \return environment id of found env otherwise 0
356 */
357 int SyntaxCheck::containsEnv(const LatexParser &parser, const QString &name, const StackEnvironment &envs, const int id)
358 {
359 for (int i = envs.size() - 1; i > -1; --i) {
360 Environment env = envs.at(i);
361 if (env.name == name) {
362 if (id < 0 || env.id == id)
363 return env.id;
364 }
365 if (id < 0 && parser.environmentAliases.contains(env.name)) {
366 QStringList altEnvs = parser.environmentAliases.values(env.name);
367 foreach (const QString &altEnv, altEnvs) {
368 if (altEnv == name)
369 return env.id;
370 }
371 }
372 }
373 return 0;
374 }
375
376 /*!
377 * \brief check if the command is valid in the environment stack
378 * \param cmd name of command
379 * \param envs environment stack
380 * \return is valid
381 */
382 bool SyntaxCheck::checkCommand(const QString &cmd, const StackEnvironment &envs)
383 {
384 for (int i = 0; i < envs.size(); ++i) {
385 Environment env = envs.at(i);
386 if (ltxCommands->possibleCommands.contains(env.name) && ltxCommands->possibleCommands.value(env.name).contains(cmd))
387 return true;
388 if (ltxCommands->environmentAliases.contains(env.name)) {
389 QStringList altEnvs = ltxCommands->environmentAliases.values(env.name);
390 foreach (const QString &altEnv, altEnvs) {
391 if (ltxCommands->possibleCommands.contains(altEnv) && ltxCommands->possibleCommands.value(altEnv).contains(cmd))
392 return true;
393 }
394 }
395 }
396 return false;
397 }
398
399 /*!
400 * \brief compare two environment stacks
401 * \param env1
402 * \param env2
403 * \return are equal
404 */
405 bool SyntaxCheck::equalEnvStack(StackEnvironment env1, StackEnvironment env2)
406 {
407 if (env1.isEmpty() || env2.isEmpty())
408 return env1.isEmpty() && env2.isEmpty();
409 if (env1.size() != env2.size())
410 return false;
411 for (int i = 0; i < env1.size(); i++) {
412 if (env1.value(i) != env2.value(i))
413 return false;
414 }
415 return true;
416 }
417
418 /*!
419 * \brief mark environment start
420 *
421 * This function is used to mark unclosed environment,i.e. environments which are unclosed at the end of the text
422 * \param env used environment
423 */
424 void SyntaxCheck::markUnclosedEnv(Environment env)
425 {
426 QDocumentLineHandle *dlh = env.dlh;
427 if (!dlh)
428 return;
429 dlh->lockForWrite();
430 if (dlh->getCurrentTicket() == env.ticket) {
431 QString line = dlh->text();
432 line = ltxCommands->cutComment(line);
433 QString cmd = "\\begin{" + env.name + "}";
434 int index = line.lastIndexOf(cmd);
435 if (index >= 0) {
436 Error elem;
437 elem.range = QPair<int, int>(index, cmd.length());
438 elem.type = ERR_EnvNotClosed;
439 int fmt= elem.type == ERR_spelling ? SpellerUtility::spellcheckErrorFormat : syntaxErrorFormat;
440 fmt= elem.type == ERR_highlight ? elem.format : fmt;
441 dlh->addOverlayNoLock(QFormatRange(elem.range.first, elem.range.second, fmt));
442 QVariant var_env;
443 StackEnvironment activeEnv;
444 activeEnv.append(env);
445 var_env.setValue(activeEnv);
446 dlh->setCookie(QDocumentLine::UNCLOSED_ENVIRONMENT_COOKIE, var_env); //ERR_EnvNotClosed;
447 }
448 }
449 dlh->unlock();
450 }
451
452 /*!
453 * \brief check if the tokenstack contains a definition-token
454 * \param stack tokenstack
455 * \return contains a definition
456 */
457 bool SyntaxCheck::stackContainsDefinition(const TokenStack &stack) const
458 {
459 for (int i = 0; i < stack.size(); i++) {
460 if (stack[i].subtype == Token::definition)
461 return true;
462 }
463 return false;
464 }
465
466 /*!
467 * \brief check one line
468 *
469 * Checks one line. Context information needs to be given by newRanges,activeEnv,dlh and ticket.
470 * This method is obsolete as the new system relies on tokens.
471 * \param line text of line as string
472 * \param newRanges will return the result as ranges
473 * \param activeEnv environment context
474 * \param dlh linehandle
475 * \param tl tokenlist of line
476 * \param stack token stack at start of line
477 * \param ticket ticket number for current processed line
478 */
479 void SyntaxCheck::checkLine(const QString &line, Ranges &newRanges, StackEnvironment &activeEnv, QDocumentLineHandle *dlh, TokenList &tl, TokenStack stack, int ticket,int commentStart)
480 {
481 // do syntax check on that line
482 int cols = containsEnv(*ltxCommands, "tabular", activeEnv);
483
484 // special treatment for empty lines with $/$$ math environmens
485 // latex treats them as error, so do we
486 if(tl.length()==0 && line.simplified().isEmpty() && !activeEnv.isEmpty() && activeEnv.top().name=="math"){
487 if(activeEnv.top().origName=="$" || activeEnv.top().origName=="$$"){
488 Environment env=activeEnv.pop();
489 /* how to present an error without character present ?
490 Error elem;
491 elem.type = ERR_highlight;
492 elem.format=mFormatList["math"];
493 elem.range = QPair<int, int>(0, 0);
494 newRanges.prepend(elem);
495 */
496 }
497 }
498
499 // check command-words
500 for (int i = 0; i < tl.length(); i++) {
501 Token &tk = tl[i];
502 // ignore commands in definition arguments e.g. \newcommand{cmd}{definition}
503 if (stackContainsDefinition(stack)) {
504 Token top = stack.top();
505 if (top.dlh != tk.dlh) {
506 if (tk.type == Token::closeBrace) {
507 stack.pop();
508 } else
509 continue;
510 } else {
511 if (tk.start < top.start + top.length)
512 continue;
513 else
514 stack.pop();
515 }
516 }
517 if (tk.subtype == Token::definition ) { // don't check command definitions
518 if(tk.type == Token::braces || tk.type == Token::openBrace){
519 stack.push(tk);
520 }
521 continue;
522 }
523 if (tk.type == Token::verbatim ) { // don't check command definitions
524 // highlight
525 Error elem;
526 elem.range = QPair<int, int>(tk.start, tk.length);
527 elem.type = ERR_highlight;
528 elem.format=mFormatList["verbatim"];
529 newRanges.append(elem);
530 continue;
531 }
532 if (tk.type == Token::punctuation || tk.type == Token::symbol) {
533 QString word = line.mid(tk.start, tk.length);
534 QStringList forbiddenSymbols;
535 forbiddenSymbols<<"^"<<"_";
536 if(forbiddenSymbols.contains(word) && !containsEnv(*ltxCommands, "math", activeEnv) && tk.subtype!=Token::formula){
537 Error elem;
538 elem.range = QPair<int, int>(tk.start, tk.length);
539 elem.type = ERR_MathCommandOutsideMath;
540 newRanges.append(elem);
541 }
542 }
543 // math highlighting of formula
544 if(tk.subtype==Token::formula){
545 // highlight
546 Error elem;
547 elem.range = QPair<int, int>(tk.start, tk.length);
548 elem.type = ERR_highlight;
549 if(tk.type==Token::command){
550 elem.format=mFormatList["#math"];
551 }else{
552 elem.format=mFormatList["math"];
553 }
554 newRanges.append(elem);
555 }
556 // spell checking
557 if (speller->inlineSpellChecking && tk.type == Token::word && (tk.subtype == Token::text || tk.subtype == Token::title || tk.subtype == Token::shorttitle || tk.subtype == Token::todo || tk.subtype == Token::none) && tk.length >= 3 && speller) {
558 int tkLength=tk.length;
559 QString word = tk.getText();
560 if(i+1 < tl.length()){
561 //check if next token is . or -
562 Token tk1 = tl.at(i+1);
563 if(tk1.type==Token::punctuation && tk1.start==(tk.start+tk.length) && !word.endsWith("\"")){
564 QString add=tk1.getText();
565 if(add=="."||add=="-"){
566 word+=add;
567 i++;
568 tkLength+=tk1.length;
569 }
570 if(add=="'"){
571 if(i+2 < tl.length()){
572 Token tk2 = tl.at(i+2);
573 if(tk2.type==Token::word && tk2.start==(tk1.start+tk1.length)){
574 add+=tk2.getText();
575 word+=add;
576 i+=2;
577 tkLength+=tk1.length+tk2.length;
578 }
579 }
580 }
581 }
582 }
583 word = latexToPlainWordwithReplacementList(word, mReplacementList); //remove special chars
584 if (speller->hideNonTextSpellingErrors && (containsEnv(*ltxCommands, "math", activeEnv)||containsEnv(*ltxCommands, "picture", activeEnv))){
585 word.clear();
586 tk.ignoreSpelling=true;
587 }else{
588 tk.ignoreSpelling=false;
589 }
590 if (!word.isEmpty() && !speller->check(word) ) {
591 if (word.endsWith('-') && speller->check(word.left(word.length() - 1)))
592 continue; // word ended with '-', without that letter, word is correct (e.g. set-up / german hypehantion)
593 if(word.endsWith('.')){
594 tkLength--; // don't take point into misspelled word
595 }
596 Error elem;
597 elem.range = QPair<int, int>(tk.start, tk.length);
598 elem.type = ERR_spelling;
599 newRanges.append(elem);
600 }
601 }
602 if (tk.type == Token::commandUnknown) {
603 QString word = line.mid(tk.start, tk.length);
604 if (word.contains('@')) {
605 continue; //ignore commands containg @
606 }
607 if (ltxCommands->mathStartCommands.contains(word) && (activeEnv.isEmpty() || activeEnv.top().name != "math")) {
608 Environment env;
609 env.name = "math";
610 env.origName=word;
611 env.id = 1; // to be changed
612 env.dlh = dlh;
613 env.ticket = ticket;
614 env.level = tk.level;
615 env.startingColumn=tk.start+tk.length;
616 activeEnv.push(env);
617 // highlight delimiter
618 Error elem;
619 elem.type = ERR_highlight;
620 elem.format=mFormatList["&math"];
621 elem.range = QPair<int, int>(tk.start, tk.length);
622 newRanges.append(elem);
623 continue;
624 }
625 if (ltxCommands->mathStopCommands.contains(word) && !activeEnv.isEmpty() && activeEnv.top().name == "math") {
626 int i=ltxCommands->mathStopCommands.indexOf(word);
627 QString txt=ltxCommands->mathStartCommands.value(i);
628 if(activeEnv.top().origName==txt){
629 Environment env=activeEnv.pop();
630 Error elem;
631 elem.type = ERR_highlight;
632 elem.format=mFormatList["math"];
633 if(dlh == env.dlh){
634 //inside line
635 elem.range = QPair<int, int>(env.startingColumn, tk.start-env.startingColumn);
636 }else{
637 elem.range = QPair<int, int>(0, tk.start);
638 }
639 newRanges.prepend(elem);
640 // highlight delimiter
641 elem.type = ERR_highlight;
642 elem.format=mFormatList["&math"];
643 elem.range = QPair<int, int>(tk.start, tk.length);
644 newRanges.append(elem);
645 }// ignore mismatching mathstop commands
646 continue;
647 }
648 if (word == "\\\\" && topEnv("tabular", activeEnv) != 0 && tk.level == activeEnv.top().level) {
649 if (activeEnv.top().excessCol < (activeEnv.top().id - 1)) {
650 Error elem;
651 elem.range = QPair<int, int>(tk.start, tk.length);
652 elem.type = ERR_tooLittleCols;
653 newRanges.append(elem);
654 }
655 if (activeEnv.top().excessCol >= (activeEnv.top().id)) {
656 Error elem;
657 elem.range = QPair<int, int>(tk.start, tk.length);
658 elem.type = ERR_tooManyCols;
659 newRanges.append(elem);
660 }
661 activeEnv.top().excessCol = 0;
662 continue;
663 }
664 // command highlighing
665 // this looks slow
666 // TODO: optimize !
667 for(const Environment &env:activeEnv){
668 if(!env.dlh)
669 continue; //ignore "normal" env
670 if(env.name=="document")
671 continue; //ignore "document" env
672 for(const QString &key: mFormatList.keys()){
673 if(key.at(0)=='#'){
674 QStringList altEnvs = ltxCommands->environmentAliases.values(env.name);
675 altEnvs<<env.name;
676 if(altEnvs.contains(key.mid(1))){
677 Error elem;
678 elem.range = QPair<int, int>(tk.start, tk.length);
679 elem.type = ERR_highlight;
680 elem.format=mFormatList.value(key);
681 newRanges.append(elem);
682 }
683 }
684 }
685 }
686 if (ltxCommands->possibleCommands["user"].contains(word) || ltxCommands->customCommands.contains(word))
687 continue;
688 if (!checkCommand(word, activeEnv)) {
689 Error elem;
690 elem.range = QPair<int, int>(tk.start, tk.length);
691 elem.type = ERR_unrecognizedCommand;
692 newRanges.append(elem);
693 continue;
694 }
695 }
696 if (tk.type == Token::env) {
697 QString env = line.mid(tk.start, tk.length);
698 // corresponds \end{env}
699 if (!activeEnv.isEmpty()) {
700 Environment tp = activeEnv.top();
701 if (tp.name == env) {
702 Environment closingEnv=activeEnv.pop();
703 if (tp.name == "tabular" || ltxCommands->environmentAliases.values(tp.name).contains("tabular")) {
704 // correct length of col error if it exists
705 if (!newRanges.isEmpty()) {
706 Error &elem = newRanges.last();
707 if (elem.type == ERR_tooManyCols && elem.range.first + elem.range.second > tk.start) {
708 elem.range.second = tk.start - elem.range.first;
709 }
710 }
711 // get new cols
712 cols = containsEnv(*ltxCommands, "tabular", activeEnv);
713 }
714 // handle higlighting
715 QStringList altEnvs = ltxCommands->environmentAliases.values(env);
716 altEnvs<<env;
717 for(const QString &key: mFormatList.keys()){
718 if(altEnvs.contains(key)){
719 Error elem;
720 int start= closingEnv.dlh==dlh ? closingEnv.startingColumn : 0;
721 int end=tk.start-1;
722 if(i>1){
723 Token tk=tl.at(i-2);
724 if(tk.type==Token::command && line.mid(tk.start, tk.length)=="\\end"){
725 end=tk.start;
726 }
727 }
728 // trick to avoid coloring of end
729 if(!newRanges.isEmpty() && newRanges.last().type==ERR_highlight){
730 if(i>1){
731 Token tk=tl.at(i-2); // skip over brace
732 if(tk.type==Token::command && line.mid(tk.start,tk.length)=="\\end"){
733 //previous token is end
734 // see whether it was colored with *-keyword i.e. #math or #picture
735 if(newRanges.last().range==QPair<int,int>(tk.start,tk.length)){
736 // yes, remove !
737 newRanges.removeLast();
738 }
739 }
740 }
741 }
742 elem.range = QPair<int, int>(start, end);
743 elem.type = ERR_highlight;
744 elem.format=mFormatList.value(key);
745 newRanges.append(elem);
746 }
747 }
748 } else {
749 Error elem;
750 elem.range = QPair<int, int>(tk.start, tk.length);
751 elem.type = ERR_closingUnopendEnv;
752 newRanges.append(elem);
753 }
754 } else {
755 Error elem;
756 elem.range = QPair<int, int>(tk.start, tk.length);
757 elem.type = ERR_closingUnopendEnv;
758 newRanges.append(elem);
759 }
760 }
761
762 if (tk.type == Token::beginEnv) {
763 QString env = line.mid(tk.start, tk.length);
764 // corresponds \begin{env}
765 Environment tp;
766 tp.name = env;
767 tp.id = 1; //needs correction
768 tp.excessCol = 0;
769 tp.dlh = dlh;
770 tp.startingColumn=tk.start+tk.length+1; // after closing brace
771 tp.ticket = ticket;
772 tp.level = tk.level-1; // tk is the argument, not the command, hence -1
773 if (env == "tabular" || ltxCommands->environmentAliases.values(env).contains("tabular")) {
774 // tabular env opened
775 // get cols !!!!
776 QString option;
777 if ((env == "tabu") || (env == "longtabu")) { // special treatment as the env is rather not latex standard
778 for (int k = i + 1; k < tl.length(); k++) {
779 Token elem = tl.at(k);
780 if (elem.level < tk.level-1)
781 break;
782 if (elem.level > tk.level)
783 continue;
784 if (elem.type == Token::braces) { // take the first mandatory argument at the correct level -> TODO: put colDef also for tabu correctly in lexer
785 option = line.mid(elem.start + 1, elem.length - 2); // strip {}
786 break; // first argument only !
787 }
788 }
789 } else {
790 if(env=="tikztimingtable"){
791 option="ll"; // is always 2 columns
792 }else{
793 for (int k = i + 1; k < tl.length(); k++) {
794 Token elem = tl.at(k);
795 if (elem.level < tk.level)
796 break;
797 if (elem.level > tk.level)
798 continue;
799 if (elem.subtype == Token::colDef) {
800 option = line.mid(elem.start + 1, elem.length - 2); // strip {}
801 break;
802 }
803 }
804 }
805 }
806 QSet<QString> translationMap=ltxCommands->possibleCommands.value("%columntypes");
807 QStringList res = LatexTables::splitColDef(option);
808 QStringList res2;
809 for(const auto &elem: res){
810 bool add=true;
811 for(const auto &i:translationMap){
812 if(i.left(1)==elem && add){
813 res2 << LatexTables::splitColDef(i.mid(1));
814 add=false;
815 }
816 }
817 if(add){
818 res2<<elem;
819 }
820 }
821 cols = res2.count();
822 tp.id = cols;
823 }
824 activeEnv.push(tp);
825 }
826
827
828 if (tk.type == Token::command) {
829 QString word = line.mid(tk.start, tk.length);
830 if(!tk.optionalCommandName.isEmpty()){
831 word=tk.optionalCommandName;
832 }
833 Token tkEnvName;
834
835 if (word == "\\begin" || word == "\\end") {
836 // check complete expression e.g. \begin{something}
837 if (tl.length() > i + 1 && tl.at(i + 1).type == Token::braces) {
838 tkEnvName = tl.at(i + 1);
839 word = word + line.mid(tkEnvName.start, tkEnvName.length);
840 }
841 }
842 // special treatment for & in math
843 if(word=="&" && containsEnv(*ltxCommands, "math", activeEnv)){
844 Error elem;
845 elem.range = QPair<int, int>(tk.start, tk.length);
846 elem.type = ERR_highlight;
847 elem.format=mFormatList.value("align-ampersand");
848 newRanges.append(elem);
849 continue;
850 }
851
852 if (ltxCommands->mathStartCommands.contains(word) && (activeEnv.isEmpty() || activeEnv.top().name != "math")) {
853 Environment env;
854 env.name = "math";
855 env.origName=word;
856 env.id = 1; // to be changed
857 env.dlh = dlh;
858 env.ticket = ticket;
859 env.level = tk.level;
860 env.startingColumn=tk.start+tk.length;
861 activeEnv.push(env);
862 // highlight delimiter
863 Error elem;
864 elem.type = ERR_highlight;
865 elem.format=mFormatList["&math"];
866 elem.range = QPair<int, int>(tk.start, tk.length);
867 newRanges.append(elem);
868 continue;
869 }
870 if (ltxCommands->mathStopCommands.contains(word) && !activeEnv.isEmpty() && activeEnv.top().name == "math") {
871 int i=ltxCommands->mathStopCommands.indexOf(word);
872 QString txt=ltxCommands->mathStartCommands.value(i);
873 if(activeEnv.top().origName==txt){
874 Environment env=activeEnv.pop();
875 Error elem;
876 elem.type = ERR_highlight;
877 elem.format=mFormatList["math"];
878 if(dlh == env.dlh){
879 //inside line
880 elem.range = QPair<int, int>(env.startingColumn, tk.start-env.startingColumn);
881 }else{
882 elem.range = QPair<int, int>(0, tk.start);
883 }
884 newRanges.prepend(elem);
885 // highlight delimiter
886 elem.type = ERR_highlight;
887 elem.format=mFormatList["&math"];
888 elem.range = QPair<int, int>(tk.start, tk.length);
889 newRanges.append(elem);
890 }// ignore mismatching mathstop commands
891 continue;
892 }
893
894 //tabular checking
895 if (topEnv("tabular", activeEnv) != 0) {
896 if (word == "&") {
897 activeEnv.top().excessCol++;
898 if (activeEnv.top().excessCol >= activeEnv.top().id) {
899 Error elem;
900 elem.range = QPair<int, int>(tk.start, tk.length);
901 elem.type = ERR_tooManyCols;
902 newRanges.append(elem);
903 }
904 continue;
905 }
906
907 if ((word == "\\\\") || (word == "\\tabularnewline")) {
908 if (activeEnv.top().excessCol < (activeEnv.top().id - 1)) {
909 Error elem;
910 elem.range = QPair<int, int>(tk.start, tk.length);
911 elem.type = ERR_tooLittleCols;
912 newRanges.append(elem);
913 }
914 if (activeEnv.top().excessCol >= (activeEnv.top().id)) {
915 Error elem;
916 elem.range = QPair<int, int>(tk.start, tk.length);
917 elem.type = ERR_tooManyCols;
918 newRanges.append(elem);
919 }
920 activeEnv.top().excessCol = 0;
921 continue;
922 }
923 if (word == "\\multicolumn") {
924 QRegExp rxMultiColumn("\\\\multicolumn\\{(\\d+)\\}\\{.+\\}\\{.+\\}");
925 rxMultiColumn.setMinimal(true);
926 int res = rxMultiColumn.indexIn(line, tk.start);
927 if (res > -1) {
928 // multicoulmn before &
929 bool ok;
930 int c = rxMultiColumn.cap(1).toInt(&ok);
931 if (ok) {
932 activeEnv.top().excessCol += c - 1;
933 }
934 }
935 if (activeEnv.top().excessCol >= activeEnv.top().id) {
936 Error elem;
937 elem.range = QPair<int, int>(tk.start, tk.length);
938 elem.type = ERR_tooManyCols;
939 newRanges.append(elem);
940 }
941 continue;
942 }
943
944 }
945
946 // command highlighing
947 // this looks slow
948 // TODO: optimize !
949 for(const Environment &env:activeEnv){
950 if(!env.dlh)
951 continue; //ignore "normal" env
952 if(env.name=="document")
953 continue; //ignore "document" env
954 for(const QString &key: mFormatList.keys()){
955 if(key.at(0)=='#'){
956 QStringList altEnvs = ltxCommands->environmentAliases.values(env.name);
957 altEnvs<<env.name;
958 if(altEnvs.contains(key.mid(1))){
959 Error elem;
960 elem.range = QPair<int, int>(tk.start, tk.length);
961 elem.type = ERR_highlight;
962 elem.format=mFormatList.value(key);
963 newRanges.append(elem);
964 }
965 }
966 }
967 }
968
969 if (ltxCommands->possibleCommands["user"].contains(word) || ltxCommands->customCommands.contains(word))
970 continue;
971
972 if (!checkCommand(word, activeEnv)) {
973 Error elem;
974 if (tkEnvName.type == Token::braces) {
975 Token tkEnvName = tl.at(i+1);
976 elem.range = QPair<int, int>(tkEnvName.innerStart(), tkEnvName.innerLength());
977 elem.type = ERR_unrecognizedEnvironment;
978 } else {
979 elem.range = QPair<int, int>(tk.start, tk.length);
980 elem.type = ERR_unrecognizedCommand;
981 }
982
983
984 if (ltxCommands->possibleCommands["math"].contains(word))
985 elem.type = ERR_MathCommandOutsideMath;
986 if (ltxCommands->possibleCommands["tabular"].contains(word))
987 elem.type = ERR_TabularCommandOutsideTab;
988 if (ltxCommands->possibleCommands["tabbing"].contains(word))
989 elem.type = ERR_TabbingCommandOutside;
990 if(elem.type== ERR_unrecognizedEnvironment){
991 // try to find command in unspecified envs
992 QStringList keys=ltxCommands->possibleCommands.keys();
993 keys.removeAll("math");
994 keys.removeAll("tabular");
995 keys.removeAll("tabbing");
996 keys.removeAll("normal");
997 foreach (QString key, keys) {
998 if(key.contains("%"))
999 continue;
1000 if(ltxCommands->possibleCommands[key].contains(word)){
1001 elem.type = ERR_commandOutsideEnv;
1002 break;
1003 }
1004 }
1005 }
1006 if(elem.type != ERR_MathCommandOutsideMath || tk.subtype!=Token::formula){
1007 newRanges.append(elem);
1008 }
1009 }
1010 }
1011 if (tk.type == Token::specialArg) {
1012 QString value = line.mid(tk.start, tk.length);
1013 QString special = ltxCommands->mapSpecialArgs.value(int(tk.type - Token::specialArg));
1014 if (!ltxCommands->possibleCommands[special].contains(value)) {
1015 Error elem;
1016 elem.range = QPair<int, int>(tk.start, tk.length);
1017 elem.type = ERR_unrecognizedKey;
1018 newRanges.append(elem);
1019 }
1020 }
1021 if (tk.type == Token::keyVal_key) {
1022 // special treatment for key val checking
1023 QString command = tk.optionalCommandName;
1024 QString value = line.mid(tk.start, tk.length);
1025
1026 // search stored keyvals
1027 QString elem;
1028 foreach (elem, ltxCommands->possibleCommands.keys()) {
1029 if (elem.startsWith("key%") && elem.mid(4) == command)
1030 break;
1031 if (elem.startsWith("key%") && elem.mid(4, command.length()) == command && elem.mid(4 + command.length(), 1) == "/" && !elem.endsWith("#c")) {
1032 // special treatment for distinguishing \command[keyvals]{test} where argument needs to equal test (used in yathesis.cwl)
1033 // now find mandatory argument
1034 QString subcommand;
1035 for (int k = i + 1; k < tl.length(); k++) {
1036 Token tk_elem = tl.at(k);
1037 if (tk_elem.level > tk.level)
1038 continue;
1039 if (tk_elem.level < tk.level)
1040 break;
1041 if (tk_elem.type == Token::braces) {
1042 subcommand = line.mid(tk_elem.start + 1, tk_elem.length - 2);
1043 if (elem == "key%" + command + "/" + subcommand) {
1044 break;
1045 } else {
1046 subcommand.clear();
1047 }
1048 }
1049 }
1050 if (!subcommand.isEmpty())
1051 elem = "key%" + command + "/" + subcommand;
1052 else
1053 elem.clear();
1054 break;
1055 }
1056 elem.clear();
1057 }
1058 if (!elem.isEmpty()) {
1059 QStringList lst = ltxCommands->possibleCommands[elem].values();
1060 QStringList::iterator iterator;
1061 QStringList toAppend;
1062 for (iterator = lst.begin(); iterator != lst.end(); ++iterator) {
1063 int i = iterator->indexOf("#");
1064 if (i > -1)
1065 *iterator = iterator->left(i);
1066
1067 i = iterator->indexOf("=");
1068 if (i > -1) {
1069 *iterator = iterator->left(i);
1070 }
1071 if (iterator->startsWith("%")) {
1072 toAppend << ltxCommands->possibleCommands[*iterator].values();
1073 }
1074 }
1075 lst << toAppend;
1076 if (!lst.contains(value)) {
1077 Error elem;
1078 elem.range = QPair<int, int>(tk.start, tk.length);
1079 elem.type = ERR_unrecognizedKey;
1080 newRanges.append(elem);
1081 }
1082 }
1083 }
1084 if (tk.subtype == Token::keyVal_val) {
1085 //figure out keyval
1086 QString word = line.mid(tk.start, tk.length);
1087 // first get command
1088 Token cmd = Parsing::getCommandTokenFromToken(tl, tk);
1089 QString command = line.mid(cmd.start, cmd.length);
1090 // figure out key
1091 QString key;
1092 for (int k = i - 1; k >= 0; k--) {
1093 Token elem = tl.at(k);
1094 if (elem.level == tk.level - 1) {
1095 if (elem.type == Token::keyVal_key) {
1096 key = line.mid(elem.start, elem.length);
1097 }
1098 break;
1099 }
1100 }
1101 // find if values are defined
1102 QString elem;
1103 foreach (elem, ltxCommands->possibleCommands.keys()) {
1104 if (elem.startsWith("key%") && elem.mid(4) == command)
1105 break;
1106 if (elem.startsWith("key%") && elem.mid(4, command.length()) == command && elem.mid(4 + command.length(), 1) == "/" && !elem.endsWith("#c")) {
1107 // special treatment for distinguishing \command[keyvals]{test} where argument needs to equal test (used in yathesis.cwl)
1108 // now find mandatory argument
1109 QString subcommand;
1110 for (int k = i + 1; k < tl.length(); k++) {
1111 Token tk_elem = tl.at(k);
1112 if (tk_elem.level > tk.level - 2)
1113 continue;
1114 if (tk_elem.level < tk.level - 2)
1115 break;
1116 if (tk_elem.type == Token::braces) {
1117 subcommand = line.mid(tk_elem.start + 1, tk_elem.length - 2);
1118 if (elem == "key%" + command + "/" + subcommand) {
1119 break;
1120 } else {
1121 subcommand.clear();
1122 }
1123 }
1124 }
1125 if (!subcommand.isEmpty())
1126 elem = "key%" + command + "/" + subcommand;
1127 break;
1128 }
1129 elem.clear();
1130 }
1131 if (!elem.isEmpty()) {
1132 // check whether keys is valid
1133 QStringList lst = ltxCommands->possibleCommands[elem].values();
1134 QStringList::iterator iterator;
1135 QString options;
1136 for (iterator = lst.begin(); iterator != lst.end(); ++iterator) {
1137 int i = iterator->indexOf("#");
1138 options.clear();
1139 if (i > -1) {
1140 options = iterator->mid(i + 1);
1141 *iterator = iterator->left(i);
1142 }
1143
1144 if (iterator->endsWith("=")) {
1145 iterator->chop(1);
1146 }
1147 if (*iterator == key)
1148 break;
1149 }
1150 if (iterator != lst.end() && !options.isEmpty()) {
1151 if(options.startsWith("#")){
1152 continue; // ignore type keys, like width#L
1153 }
1154 if(options.startsWith("%")){
1155 if (!ltxCommands->possibleCommands[options].contains(word)) {
1156 Error elem;
1157 elem.range = QPair<int, int>(tk.start, tk.length);
1158 elem.type = ERR_unrecognizedKeyValues;
1159 newRanges.append(elem);
1160 }
1161 }else{
1162 QStringList l = options.split(",");
1163 if (!l.contains(word)) {
1164 Error elem;
1165 elem.range = QPair<int, int>(tk.start, tk.length);
1166 elem.type = ERR_unrecognizedKeyValues;
1167 newRanges.append(elem);
1168 }
1169 }
1170 }
1171 }
1172 }
1173 }
1174 if(!activeEnv.isEmpty()){
1175 //check active env for env highlighting (math,verbatim)
1176 for(const Environment &env: activeEnv){
1177 QStringList altEnvs = ltxCommands->environmentAliases.values(env.name);
1178 altEnvs<<env.name;
1179 for(const QString &key: mFormatList.keys()){
1180 if(altEnvs.contains(key)){
1181 Error elem;
1182 int start= env.dlh==dlh ? env.startingColumn : 0;
1183 elem.range = QPair<int, int>(start, commentStart>=0 ? commentStart-start : line.length()-start);
1184 elem.type = ERR_highlight;
1185 elem.format=mFormatList.value(key);
1186 newRanges.prepend(elem); // draw this first and then other on top (e.g. keyword highlighting) !
1187 }
1188 }
1189 }
1190 }
1191 }