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