1 #include "latexcompleter.h"
2 #include "latexcompleter_p.h"
3 #include "latexcompleter_config.h"
4 #include "help.h"
5 #include "usermacro.h"
6
7 #include "smallUsefulFunctions.h"
8
9 #include "qdocumentline.h"
10 #include "qdocument_p.h"
11 #include "qeditorinputbinding.h"
12 #include "qformatfactory.h"
13 #include "qdocumentline_p.h"
14
15 #include "latexdocument.h"
16 #include "latexeditorview.h"
17 #include "qdocument.h"
18
19 #include <algorithm>
20 #include <QtConcurrentFilter>
21 #include <QtConcurrentMap>
22
23
24 //------------------------------Default Input Binding--------------------------------
25 class CompleterInputBinding: public QEditorInputBinding
26 {
27 public:
CompleterInputBinding()28 CompleterInputBinding(): active(false), showAlways(false), showMostUsed(0), completer(nullptr), editor(nullptr), oldBinding(nullptr), curStart(0), maxWritten(0), curLineNumber(0) {}
id() const29 virtual QString id() const
30 {
31 return "TXS::CompleterInputBinding";
32 }
name() const33 virtual QString name() const
34 {
35 return "TXS::CompleterInputBinding";
36 }
37
mousePressEvent(QMouseEvent * mouse,QEditor * editor)38 virtual bool mousePressEvent(QMouseEvent *mouse, QEditor *editor)
39 {
40 // remove unused argument warnings
41 Q_UNUSED(mouse)
42 Q_UNUSED(editor)
43 simpleRestoreAutoOverride();
44 resetBinding();
45 return false;
46 }
47
getCurWord()48 QString getCurWord()
49 {
50 if (!editor) return QString();
51 //QMessageBox::information(0,curLine.text().mid(curStart,editor->cursor().columnNumber()-curStart),"",0);
52 return editor->text(curLineNumber).mid(curStart, editor->cursor().columnNumber() - curStart);
53 }
54
55 /// check if current cursor/placeholder is mirrored
isMirrored()56 bool isMirrored()
57 {
58 if (!editor) return false;
59 if ( editor->currentPlaceHolder() >= 0 && editor->currentPlaceHolder() < editor->placeHolderCount() ) {
60 PlaceHolder ph = editor->getPlaceHolder(editor->currentPlaceHolder());
61 return ph.mirrors.count() > 0;
62 }
63 return false;
64 }
65
insertText(const QString & text)66 void insertText(const QString &text)
67 {
68 if (!editor) return;
69 maxWritten += text.length();
70 if ( editor->currentPlaceHolder() >= 0 && editor->currentPlaceHolder() < editor->placeHolderCount() )
71 editor->document()->beginMacro();
72 editor->write(text);
73 //cursor mirrors
74 if ( editor->currentPlaceHolder() >= 0 && editor->currentPlaceHolder() < editor->placeHolderCount() ) {
75 PlaceHolder ph = editor->getPlaceHolder(editor->currentPlaceHolder());
76
77 QString baseText = ph.cursor.selectedText();
78
79 for ( int phm = 0; phm < ph.mirrors.count(); ++phm ) {
80 //QString s = ph.affector ? ph.affector->affect(text, baseText, m_curPlaceHolder, phm) : baseText;
81
82 ph.mirrors[phm].replaceSelectedText(baseText);
83 }
84 editor->document()->endMacro();
85 }
86 //end cursor mirrors
87 if (editor->cursor().columnNumber() > curStart + 1 && !completer->isVisible()) {
88 QString wrd = getCurWord();
89 completer->filterList(wrd, showMostUsed);
90 completer->widget->show();
91 if (showMostUsed == 1 && completer->countWords() == 0) { // if prefered list is empty, take next more extensive one
92 completer->setTab(0); // typical
93 }
94 if (showMostUsed == 0 && completer->countWords() == 0) {
95 completer->setTab(3); // all
96 }
97 completer->adjustWidget();
98 }
99 }
100
insertCompletedWord()101 bool insertCompletedWord()
102 {
103 if (!editor) return false;
104 if (completer->list->isVisible() && maxWritten >= curStart && completer->list->currentIndex().isValid()) {
105 QDocumentCursor cursor = editor->cursor();
106 editor->document()->beginMacro();
107 QVariant v = completer->list->model()->data(completer->list->currentIndex(), Qt::DisplayRole);
108 if (!v.isValid() || !v.canConvert<CompletionWord>()) return false;
109 CompletionWord cw = v.value<CompletionWord>();
110 completer->listModel->incUsage(completer->list->currentIndex());
111 //int alreadyWrittenLen=editor->cursor().columnNumber()-curStart;
112 //remove current text for correct case
113 if (completer->forcedRef) {
114 QString line = cursor.line().text();
115 int col = cursor.columnNumber();
116 bool missingCloseBracket = (findClosingBracket(line, col) < 0);
117 QString eow = "}],";
118 while (!cursor.atLineEnd() && !eow.contains(cursor.nextChar())
119 && !(cursor.nextChar().isSpace() && missingCloseBracket)) { // spaces are allowed in labels and should be deleted, however we stop deleting at spaces if the closing bracket is missing. otherwise it deletes the complete line.
120 cursor.deleteChar();
121 }
122 //if(cursor.nextChar()=='}')
123 // cursor.deleteChar();
124 }
125 if (completer->forcedCite) {
126 QString line = cursor.line().text();
127 int col = cursor.columnNumber();
128 bool missingCloseBracket = (findClosingBracket(line, col) < 0);
129 QString eow = "},";
130 while (!cursor.atLineEnd() && !eow.contains(cursor.nextChar())
131 && !(cursor.nextChar().isSpace() && missingCloseBracket))
132 cursor.deleteChar();
133 }
134 if (isMirrored() && (cw.lines.first().startsWith("\\begin") || cw.lines.first().startsWith("\\end"))) {
135 QString text = cw.lines.first();
136 int i = cursor.columnNumber() - curStart;
137 text.remove(0, i);
138 i = text.indexOf('}');
139 if (i >= 0)
140 text.remove(i, text.length());
141 while (!cursor.atLineEnd() && cursor.nextChar() != '}') {
142 cursor.deleteChar();
143 }
144 insertText(text);
145 editor->document()->endMacro();
146 return true;
147 }
148 // check whether cursor is inside math in case of automatic delimter insertion
149 // this is done here as the charcter format is used for detection and here we are sure that at least 1 character was used.
150 QString cwCmd=cw.word;
151 QRegExp rx("\\\\[a-zA-Z]+");
152 int pos=rx.indexIn(cwCmd);
153 if(pos>-1){
154 cwCmd=rx.cap(0);
155 }
156 bool inMath=false;
157 if(cw.lines.size()==1 && completer->latexParser.possibleCommands["math"].contains(cwCmd)){
158 LatexEditorView *view = editor->property("latexEditor").value<LatexEditorView *>();
159 Q_ASSERT(view);
160 inMath=view->isInMathHighlighting(cursor);
161 }
162
163 //for (int i = maxWritten - cursor.columnNumber(); i > 0; i--) cursor.deleteChar();
164 if(maxWritten>cursor.columnNumber()){
165 cursor.movePosition(maxWritten-cursor.columnNumber(),QDocumentCursor::NextCharacter,QDocumentCursor::KeepAnchor);
166 cursor.removeSelectedText();
167 }
168 //for (int i = cursor.columnNumber() - curStart; i > 0; i--) cursor.deletePreviousChar();
169 if(curStart<cursor.columnNumber()){
170 cursor.movePosition(cursor.columnNumber()-curStart,QDocumentCursor::PreviousCharacter,QDocumentCursor::KeepAnchor);
171 cursor.removeSelectedText();
172 }
173 if (!autoOverridenText.isEmpty()) {
174 cursor.insertText(autoOverridenText);
175 cursor.movePosition(autoOverridenText.length(), QDocumentCursor::PreviousCharacter);
176 editor->resizeAutoOverridenPlaceholder(cursor, autoOverridenText.size());
177 autoOverridenText = "";
178 }
179 //cursor.endEditBlock(); //doesn't work and lead to crash when auto indentation is enabled => TODO:figure out why
180 // cursor.setColumnNumber(curStart);
181 CodeSnippet::PlaceholderMode phMode = (LatexCompleter::config && LatexCompleter::config->usePlaceholders) ? CodeSnippet::PlacehodersActive : CodeSnippet::PlaceholdersRemoved;
182
183 if(cw.lines.size()==1 && completer->latexParser.possibleCommands["math"].contains(cwCmd)){
184 if(!inMath && LatexCompleter::config && LatexCompleter::config->autoInsertMathDelimiters){
185 // add $$ to mathcommand outsiode math env
186 cw.lines.first().prepend(LatexCompleter::config->startMathDelimiter);
187 cw.lines.first().append(LatexCompleter::config->stopMathDelimiter);
188 // move cursors
189 if(cw.cursorOffset>-1) cw.cursorOffset++;
190 if(cw.anchorOffset>-1) cw.anchorOffset++;
191 for(int i=0;i<cw.placeHolders.size();i++){
192 for(int j=0;j<cw.placeHolders[i].size();j++){
193 cw.placeHolders[i][j].offset++;
194 }
195 }
196 }
197 }
198 cw.insertAt(editor, &cursor, phMode, !completer->startedFromTriggerKey, completer->forcedKeyval);
199 editor->document()->endMacro();
200
201 return true;
202 }
203 return false;
204 }
205
removeRightWordPart()206 void removeRightWordPart()
207 {
208 if (!editor) return;
209 QDocumentCursor cursor = editor->cursor();
210 for (int i = maxWritten - cursor.columnNumber(); i > 0; i--) cursor.deleteChar();
211 maxWritten = cursor.columnNumber();
212 editor->setCursor(cursor);//necessary to keep the cursor at the same place (but why???) TODO: remove this line (it cause \ to disable placeholders which other keys don't disable)
213 }
214
215 ///selects an index in the completion suggestion list
select(const QModelIndex & ind)216 void select(const QModelIndex &ind)
217 {
218 if (!completer || !completer->list) return;
219 completer->list->setCurrentIndex(ind);
220 completer->selectionChanged(ind);
221 }
222
223 ///moves the selection index to the next/previous delta-th entry in the suggestion list
selectDelta(const int delta)224 bool selectDelta(const int delta)
225 {
226 if (!completer || !completer->list || !completer->list->isVisible()) {
227 resetBinding();
228 return false;
229 }
230 QModelIndex ind = completer->list->model()->index(completer->list->currentIndex().row() + delta, 0, QModelIndex());
231 if (!ind.isValid()) {
232 if (delta < 0) ind = completer->list->model()->index(0, 0, QModelIndex());
233 else if (delta > 0) ind = completer->list->model()->index(completer->list->model()->rowCount() - 1, 0, QModelIndex());
234 }
235 if (ind.isValid()) select(ind);
236 return true;
237 }
238
239 /*!
240 * Complete the common part of all remaining possible completions.
241 * \return true if there was a common part, that could be completed.
242 */
completeCommonPrefix(bool reducedRange=false)243 bool completeCommonPrefix(bool reducedRange = false)
244 {
245 QString my_curWord = getCurWord();
246 if (my_curWord.isEmpty()) return false;
247 if (!completer) return false;
248 /*if (!completer->list->isVisible()) {
249 resetBinding();
250 return false;
251 }*/
252 // get list of most recent choices
253 const QList<CompletionWord> &words = completer->listModel->getWords();
254 // filter list for longest common characters
255 if (words.count() > 1) {
256 QString myResult = words.first().word;
257 int curWordLength = my_curWord.length();
258 my_curWord = completer->listModel->getLastWord().word;
259
260 if (reducedRange && words.count() > 10) {
261 my_curWord = words.at(10).word;
262 }
263
264
265 if(showMostUsed==2){
266 // as the list is unsorted, all words must be checked !!!
267 int j=0;
268 bool allIdentical=false;
269 for (j = 0; (j < my_curWord.length() && j < myResult.length()); j++) {
270 for(const auto &cw : words){
271 if(cw.word.length()<=j){
272 allIdentical=false;
273 break;
274 }
275 if(cw.word[j]!=myResult[j]){
276 allIdentical=false;
277 break;
278 }
279 allIdentical=true;
280 }
281 if(!allIdentical){
282 break;
283 }
284 }
285 if(!allIdentical){
286 j=j-1;
287 }
288 if(j>0){
289 myResult = myResult.left(j);
290 }else{
291 return true;
292 }
293 }else{
294 for (int j = curWordLength; (j < my_curWord.length() && j < myResult.length()); j++) {
295 if (myResult[j] != my_curWord[j]) {
296 myResult = myResult.left(j);
297 }
298 }
299 }
300
301 if (myResult.length() == curWordLength) {
302 return false; // no common segment to complete
303 }
304
305 removeRightWordPart();
306 if(showMostUsed==2){
307 // fuzzy mode
308 // remove left hand side as well
309 QDocumentCursor cursor = editor->cursor();
310 for (int i = curWordLength; i > 0; i--) cursor.deletePreviousChar();
311 maxWritten = cursor.columnNumber();
312 editor->setCursor(cursor);
313 curWordLength=0;
314 }
315 insertText(myResult.right(myResult.length() - curWordLength));
316 completer->filterList(getCurWord(), getMostUsed());
317 if (!completer->list->currentIndex().isValid())
318 select(completer->list->model()->index(0, 0, QModelIndex()));
319 return true;
320 } else {
321 if (showMostUsed == 1) return false;
322 insertCompletedWord();
323 resetBinding();
324 return true;
325 }
326 }
327
simpleRestoreAutoOverride(const QString & written="????")328 void simpleRestoreAutoOverride(const QString &written = "????") //simple means without protecting the change from undo/redo
329 {
330 if (!editor) return;
331 if (!autoOverridenText.isEmpty() && !editor->isAutoOverrideText(written)) {
332 int curpos = editor->cursor().columnNumber();
333 if (curpos < maxWritten) {
334 QDocumentCursor c = editor->cursor();
335 c.movePosition(maxWritten - curpos, QDocumentCursor::NextCharacter);
336 editor->setCursor(c);
337 }
338 editor->insertText(autoOverridenText);
339 QDocumentCursor c = editor->cursor();
340 c.movePosition(autoOverridenText.length() + (curpos < maxWritten ? maxWritten - curpos : 0), QDocumentCursor::PreviousCharacter);
341 editor->setCursor(c);
342 editor->resizeAutoOverridenPlaceholder(c, autoOverridenText.size());
343 }
344 }
345
keyPressEvent(QKeyEvent * event,QEditor * editor)346 virtual bool keyPressEvent(QKeyEvent *event, QEditor *editor)
347 {
348 Q_ASSERT (completer && completer->listModel);
349 if (!completer || !completer->listModel) return false;
350 if (event->key() == Qt::Key_Shift || event->key() == Qt::Key_Alt || event->key() == Qt::Key_AltGr || event->key() == Qt::Key_Control || event->key() == Qt::Key_CapsLock)
351 return false;
352 if (!active) return false; //we should never have been called
353 bool handled = false;
354 if (event->key() == Qt::Key_Backspace) {
355 maxWritten--;
356 QDocumentCursorHandle *dch=editor->cursorHandle();
357 Q_ASSERT(dch);
358 dch->deletePreviousChar();
359 if (dch->columnNumber() <= curStart) {
360 resetBinding();
361 return true;
362 } else if (dch->columnNumber() + 1 <= curStart && !showAlways) {
363 completer->widget->hide();
364 return true;
365 }
366 handled = true;
367 } else if (event->key() == Qt::Key_Escape) {
368 simpleRestoreAutoOverride("");
369 resetBinding();
370 return true;
371 } else if (event->key() == Qt::Key_Delete) {
372 if (editor->cursor().columnNumber() < maxWritten) maxWritten--;
373 if (completer->forcedRef) {
374 if (editor->cursor().nextChar() == '}')
375 completer->forcedRef = false;
376 }
377 editor->cursor().deleteChar();
378 handled = true;
379 } else if (event->key() == Qt::Key_Left) {
380 if (event->modifiers()) {
381 resetBinding();
382 return false;
383 }
384 editor->setCursorPosition(curLineNumber, editor->cursor().columnNumber() - 1);
385 if (editor->cursor().columnNumber() <= curStart) resetBinding();
386 handled = true;
387 } else if (event->key() == Qt::Key_Right) {
388 if (event->modifiers()) {
389 resetBinding();
390 return false;
391 }
392 editor->setCursorPosition(curLineNumber, editor->cursor().columnNumber() + 1);
393 if (editor->cursor().columnNumber() > maxWritten) resetBinding();
394 handled = true;
395 } else if (event->key() == Qt::Key_Up) return selectDelta(-1);
396 else if (event->key() == Qt::Key_Down) return selectDelta(+1);
397 else if (event->key() == Qt::Key_PageUp) return selectDelta(-5);
398 else if (event->key() == Qt::Key_PageDown) return selectDelta(+5);
399 else if (event->key() == Qt::Key_Home) {
400 if (!completer->list->isVisible()) {
401 resetBinding();
402 return false;
403 }
404 QModelIndex ind = completer->list->model()->index(0, 0, QModelIndex());
405 if (ind.isValid()) select(ind);
406 return true;
407 } else if (event->key() == Qt::Key_End) {
408 if (!completer->list->isVisible()) {
409 resetBinding();
410 return false;
411 }
412 QModelIndex ind = completer->list->model()->index(completer->list->model()->rowCount() - 1, 0, QModelIndex());
413 if (ind.isValid()) select(ind);
414 return true;
415 } else if (event->key() == Qt::Key_Tab) {
416 if (completeCommonPrefix(true)) {
417 return true;
418 } else if (insertCompletedWord()) {
419 resetBinding();
420 return true;
421 }
422 return false;
423 } else if (event->key() == Qt::Key_Space && event->modifiers() == Qt::ShiftModifier) {
424 //showMostUsed=!showMostUsed;
425 //handled=true;
426 showMostUsed++;
427 if (showMostUsed > 3)
428 showMostUsed = 0;
429 completer->tbAbove->setCurrentIndex(showMostUsed);
430 completer->tbBelow->setCurrentIndex(showMostUsed);
431 return true;
432 } else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
433 insertCompletedWord();
434 resetBinding();
435 return true;
436 } else {
437 if (event->text().length() != 1 || event->text() == " ") {
438 resetBinding();
439 return false;
440 }
441
442 QChar written = event->text().at(0);
443 if (written == '\\') {
444 if (getCurWord() == "\\") {
445 resetBinding();
446 return false;
447 } else if (getCurWord() == "") {
448 maxWritten = curStart + 1;
449 } else {
450 if (LatexCompleter::config && LatexCompleter::config->eowCompletes) {
451 insertCompletedWord();
452 }
453 QDocumentCursor edc = editor->cursor();
454 if (edc.hasSelection()) {
455 edc.removeSelectedText();
456 editor->setCursor(edc);
457 }
458 curStart = edc.columnNumber();
459 maxWritten = curStart + 1;
460 }
461 bool autoOverride = editor->isAutoOverrideText("\\");
462 if (!autoOverride) editor->cursor().insertText(written);
463 else {
464 QDocumentCursor c = editor->cursor();
465 editor->document()->beginMacro();
466 c.deleteChar();
467 c.insertText("\\");
468 editor->document()->endMacro();
469 autoOverridenText = "\\";
470 }
471 //editor->insertText(written); <- can't use that since it may break the completer by auto closing brackets
472 handled = true;
473 } else if (completer->acceptChar(written, editor->cursor().columnNumber() - curStart)) {
474 insertText(written);
475 handled = true;
476 } else if (event->text().length() == 1 && getCommonEOW().contains(event->text().at(0)) ) {
477 QString curWord = getCurWord();
478
479 if (curWord == "\\" || !LatexCompleter::config || !LatexCompleter::config->eowCompletes) {
480 resetBinding();
481 simpleRestoreAutoOverride(written);
482 return false;
483 }
484 const QList<CompletionWord> &words = completer->listModel->getWords();
485 QString newWord;
486 int eowchars = 10000;
487 foreach (const CodeSnippet &w, words) {
488 if (!w.word.startsWith(curWord)) continue;
489 if (w.word.length() == curWord.length()) {
490 newWord = w.word;
491 break;
492 }
493 int newoffset = w.lines.first().indexOf(written, curWord.length());
494 if (newoffset < 0) continue;
495 int neweowchars = 0;
496 for (int i = curWord.length(); i < newoffset; i++)
497 if (getCommonEOW().contains(w.lines.first()[i]))
498 neweowchars++;
499 if (neweowchars < eowchars) {
500 newWord = w.word;
501 eowchars = neweowchars;
502 if (eowchars == 1) break;
503 }
504 }
505
506 if (!newWord.isEmpty() && newWord.length() != curWord.length()) {
507 QString insertion = newWord.mid(curWord.length(), newWord.indexOf(written, curWord.length()) - curWord.length() + 1); //choose text until written eow character
508 insertText(insertion);
509 //insertText(written);
510 handled = true;
511 }
512
513 if (!handled) {
514 insertCompletedWord();
515 if (newWord.isEmpty())
516 simpleRestoreAutoOverride(written);
517 //insertText(written);
518 resetBinding();
519 return false;//oldBinding->keyPressEvent(event,editor); //call old input binding for long words (=> key replacements after completions, but the user can still write \")
520 }
521 } else {
522 int curLength = getCurWord().length();
523 resetBinding();
524 return curLength >= 2 && oldBinding->keyPressEvent(event, editor); //call old input binding for long words (=> key replacements after completions, but the user can still write \")
525 }
526 }
527 completer->filterList(getCurWord(), showMostUsed);
528 if (!completer->list->currentIndex().isValid())
529 select(completer->list->model()->index(0, 0, QModelIndex()));
530 return handled;
531 }
532
inputMethodEvent(QInputMethodEvent * event,QEditor * editor)533 virtual bool inputMethodEvent(QInputMethodEvent* event, QEditor *editor)
534 {
535 Q_ASSERT (completer && completer->listModel);
536 if (!completer || !completer->listModel) return false;
537 if(event->commitString().isEmpty()){
538 return false;
539 }
540
541 bool handled=false;
542 QString text=event->commitString();
543 if (text.length() != 1 || text == " ") {
544 resetBinding();
545 return false;
546 }
547
548 QChar written = text.at(0);
549 if (written == '\\') {
550 if (getCurWord() == "\\") {
551 resetBinding();
552 return false;
553 } else if (getCurWord() == "") {
554 maxWritten = curStart + 1;
555 } else {
556 if (LatexCompleter::config && LatexCompleter::config->eowCompletes) {
557 insertCompletedWord();
558 }
559 QDocumentCursor edc = editor->cursor();
560 if (edc.hasSelection()) {
561 edc.removeSelectedText();
562 editor->setCursor(edc);
563 }
564 curStart = edc.columnNumber();
565 maxWritten = curStart + 1;
566 }
567 bool autoOverride = editor->isAutoOverrideText("\\");
568 if (!autoOverride) editor->cursor().insertText(written);
569 else {
570 QDocumentCursor c = editor->cursor();
571 editor->document()->beginMacro();
572 c.deleteChar();
573 c.insertText("\\");
574 editor->document()->endMacro();
575 autoOverridenText = "\\";
576 }
577 //editor->insertText(written); <- can't use that since it may break the completer by auto closing brackets
578 handled = true;
579 } else if (completer->acceptChar(written, editor->cursor().columnNumber() - curStart)) {
580 insertText(written);
581 handled = true;
582 } else if (getCommonEOW().contains(written) ) {
583 QString curWord = getCurWord();
584
585 if (curWord == "\\" || !LatexCompleter::config || !LatexCompleter::config->eowCompletes) {
586 resetBinding();
587 simpleRestoreAutoOverride(written);
588 return false;
589 }
590 const QList<CompletionWord> &words = completer->listModel->getWords();
591 QString newWord;
592 int eowchars = 10000;
593 foreach (const CodeSnippet &w, words) {
594 if (!w.word.startsWith(curWord)) continue;
595 if (w.word.length() == curWord.length()) {
596 newWord = w.word;
597 break;
598 }
599 int newoffset = w.lines.first().indexOf(written, curWord.length());
600 if (newoffset < 0) continue;
601 int neweowchars = 0;
602 for (int i = curWord.length(); i < newoffset; i++)
603 if (getCommonEOW().contains(w.lines.first()[i]))
604 neweowchars++;
605 if (neweowchars < eowchars) {
606 newWord = w.word;
607 eowchars = neweowchars;
608 if (eowchars == 1) break;
609 }
610 }
611
612 if (!newWord.isEmpty() && newWord.length() != curWord.length()) {
613 QString insertion = newWord.mid(curWord.length(), newWord.indexOf(written, curWord.length()) - curWord.length() + 1); //choose text until written eow character
614 insertText(insertion);
615 //insertText(written);
616 handled = true;
617 }
618
619 if (!handled) {
620 insertCompletedWord();
621 if (newWord.isEmpty())
622 simpleRestoreAutoOverride(written);
623 //insertText(written);
624 resetBinding();
625 return false;//oldBinding->keyPressEvent(event,editor); //call old input binding for long words (=> key replacements after completions, but the user can still write \")
626 }
627 }
628
629 completer->filterList(getCurWord(), showMostUsed);
630 if (!completer->list->currentIndex().isValid())
631 select(completer->list->model()->index(0, 0, QModelIndex()));
632 return handled;
633 }
634
cursorPositionChanged(QEditor * edit)635 void cursorPositionChanged(QEditor *edit)
636 {
637 if (edit != editor) return; //should never happen
638 QDocumentCursor c = editor->cursor();
639 if (c.lineNumber() != curLineNumber || c.columnNumber() < curStart) resetBinding();
640 }
641
setMostUsed(int mu,bool quiet=false)642 void setMostUsed(int mu, bool quiet = false)
643 {
644 showMostUsed = mu;
645 if (quiet)
646 return;
647 completer->filterList(getCurWord(), showMostUsed);
648 if (!completer->list->currentIndex().isValid())
649 select(completer->list->model()->index(0, 0, QModelIndex()));
650 }
651
getMostUsed()652 int getMostUsed()
653 {
654 return showMostUsed;
655 }
656
resetBinding()657 void resetBinding()
658 {
659 if (completer)
660 completer->listModel->setEnvironMode(false);
661 showMostUsed = false;
662 QString curWord = getCurWord();
663 if (!active) return;
664 QToolTip::hideText();
665 //reenable auto close chars
666 if(completer)
667 editor->setFlag(QEditor::AutoCloseChars, completer->editorAutoCloseChars);
668 editor->setInputBinding(oldBinding);
669 if (completer && completer->widget && completer->widget->isVisible()){
670 completer->widget->hide();
671 editor->setFocus();
672 }
673 if (completer) {
674 completer->disconnect(editor, SIGNAL(cursorPositionChanged()), completer, SLOT(cursorPositionChanged()));
675 }
676 active = false;
677 //editor=0; this leads to a crash, as the editor is still in use after reseting the cursor
678 if (completer && completer->completingKey() && curWord.endsWith("=")) {
679 LatexEditorView *view = editor->property("latexEditor").value<LatexEditorView *>();
680 Q_ASSERT(view);
681 view->mayNeedToOpenCompleter();
682 }
683 if (completer && completer->completingGraphic() && curWord.endsWith("/")) {
684 completer->complete(editor, LatexCompleter::CompletionFlags(LatexCompleter::CF_FORCE_VISIBLE_LIST | LatexCompleter::CF_FORCE_GRAPHIC));
685 }
686 }
687
bindTo(QEditor * edit,LatexCompleter * caller,bool forced,int start)688 void bindTo(QEditor *edit, LatexCompleter *caller, bool forced, int start)
689 {
690 if (active) resetBinding();
691 active = true;
692 completer = caller;
693 editor = edit;
694 oldBinding = (editor->inputBindings().count() > 0 ? editor->inputBindings()[0] : nullptr);
695 editor->setInputBinding(this);
696 curStart = start > 0 ? start : 0;
697 maxWritten = editor->cursor().columnNumber();
698 if (maxWritten < start) maxWritten = start;
699 curLineNumber = editor->cursor().lineNumber();
700 showAlways = forced; //curWord!="\\";
701 completer->filterList(getCurWord());
702 if (showAlways) {
703 completer->widget->show();
704 select(completer->list->model()->index(0, 0, QModelIndex()));
705 }
706 }
707
isActive()708 bool isActive()
709 {
710 return active;
711 }
712
713 QString autoOverridenText;
714 private:
715 bool active;
716 bool showAlways;
717 int showMostUsed;
718 LatexCompleter *completer;
719 QEditor *editor;
720 QEditorInputBindingInterface *oldBinding;
721 QString completionWord;
722 int curStart, maxWritten;
723 int curLineNumber;
724 };
725
726 //Q_DECLARE_METATYPE(LatexEditorView *)
727
728 CompleterInputBinding *completerInputBinding = new CompleterInputBinding();
729 //------------------------------Item Delegate--------------------------------
730 class CompletionItemDelegate: public QItemDelegate
731 {
732 public:
CompletionItemDelegate(QObject * parent=nullptr)733 explicit CompletionItemDelegate(QObject *parent = nullptr): QItemDelegate(parent)
734 {
735 }
736
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const737 void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
738 {
739 QVariant v = index.model()->data(index);
740 if (!v.isValid() || !v.canConvert<CompletionWord>()) return;
741 CompletionWord cw = v.value<CompletionWord>();
742 if (cw.lines.empty() || cw.placeHolders.empty()) return;
743 QFont fNormal = option.font;
744 QFont fPlHolder = option.font;
745 //fPlHolder.setItalic(true);
746 QColor normalColor, plHolderColor;
747 if ((QStyle::State_HasFocus | QStyle::State_Selected) & option.state) {
748 painter->fillRect(option.rect, option.palette.brush(QPalette::Highlight));
749 normalColor = option.palette.color(QPalette::HighlightedText);
750 } else {
751 painter->fillRect(option.rect, option.palette.brush(QPalette::Base)); //doesn't seem to be necessary
752 normalColor = option.palette.color(QPalette::Text);
753 }
754 painter->setPen(normalColor);
755 plHolderColor = normalColor;
756 plHolderColor.setAlpha(128);
757
758 QRectF r = option.rect;
759 r.setLeft(r.left() + 2);
760 bool drawPlaceholder = !cw.placeHolders.empty();
761 QString firstLine = cw.lines[0];
762 if (!cw.getName().isEmpty()) {
763 drawPlaceholder = false;
764 firstLine = cw.getName();
765 }
766 if (!drawPlaceholder)
767 painter->drawText(r, Qt::AlignLeft | Qt::AlignTop | Qt::TextSingleLine, firstLine);
768 else {
769 QFontMetricsF fmn(fNormal);
770 QFontMetricsF fmi(fPlHolder);
771 int p = 0;
772 for (int i = 0; i < cw.placeHolders[0].size(); i++) {
773 QString temp = firstLine.mid(p, cw.placeHolders[0][i].offset - p);
774 painter->drawText(r, Qt::AlignLeft | Qt::AlignTop | Qt::TextSingleLine, temp);
775 r.setLeft(r.left() + UtilsUi::getFmWidth(fmn, temp));
776 temp = firstLine.mid(cw.placeHolders[0][i].offset, cw.placeHolders[0][i].length);
777 painter->setFont(fPlHolder);
778 painter->setPen(plHolderColor);
779 painter->drawText(r, Qt::AlignLeft | Qt::AlignTop | Qt::TextSingleLine, temp);
780 r.setLeft(r.left() + UtilsUi::getFmWidth(fmi, temp) + 1);
781 p = cw.placeHolders[0][i].offset + cw.placeHolders[0][i].length;
782 painter->setFont(fNormal);
783 painter->setPen(normalColor);
784 if (p > firstLine.length()) break;
785 }
786 painter->drawText(r, Qt::AlignLeft | Qt::AlignTop | Qt::TextSingleLine, firstLine.mid(p));
787 }
788 }
789 };
790
791
792
793 //----------------------------list model------------------------------------
794 LatexCompleterConfig *CompletionListModel::config = nullptr;
rowCount(const QModelIndex & parent) const795 int CompletionListModel::rowCount(const QModelIndex &parent) const
796 {
797 Q_UNUSED(parent)
798 return words.count();
799 }
800
data(const QModelIndex & index,int role) const801 QVariant CompletionListModel::data(const QModelIndex &index, int role)const
802 {
803 if (!index.isValid())
804 return QVariant();
805
806 if (index.row() >= words.count())
807 return QVariant();
808
809 if (role == Qt::DisplayRole) {
810 QVariant temp;
811 temp.setValue(words.at(index.row()));
812 return temp;
813 } else
814 return QVariant();
815 }
816
headerData(int section,Qt::Orientation orientation,int role) const817 QVariant CompletionListModel::headerData(int section, Qt::Orientation orientation,
818 int role) const
819 {
820 Q_UNUSED(role)
821 Q_UNUSED(orientation)
822 Q_UNUSED(section)
823
824 return QVariant();
825 }
826
isNextCharPossible(const QChar & c)827 bool CompletionListModel::isNextCharPossible(const QChar &c)
828 {
829 //if (words.count()>100) //probable that every char is there (especially since acceptedChars is already checked)
830 // return true; -> leads to faulty behaviour (\b{ is supposed to be within list ...)
831 Qt::CaseSensitivity cs = Qt::CaseInsensitive;
832 if (LatexCompleter::config &&
833 LatexCompleter::config->caseSensitive == LatexCompleterConfig::CCS_CASE_SENSITIVE)
834 cs = Qt::CaseSensitive;
835 QString extension = curWord + c;
836 foreach (const CompletionWord &cw, words)
837 if (cw.word.startsWith(extension, cs)) return true;
838 return false;
839 }
840
canFetchMore(const QModelIndex &) const841 bool CompletionListModel::canFetchMore(const QModelIndex &) const
842 {
843 return mCanFetchMore;
844 }
845
fetchMore(const QModelIndex &)846 void CompletionListModel::fetchMore(const QModelIndex &)
847 {
848 beginInsertRows(QModelIndex(), words.count(), qMin(words.count() + 100, mWordCount));
849 filterList(mLastWord, mLastMU, true, mLastType);
850 endInsertRows();
851 }
852
getLastWord()853 CompletionWord CompletionListModel::getLastWord()
854 {
855 return mLastWordInList;
856 }
857
setKeyValWords(const QString & name,const QSet<QString> & newwords)858 void CompletionListModel::setKeyValWords(const QString &name, const QSet<QString> &newwords)
859 {
860 QList<CompletionWord> newWordList;
861 newWordList.clear();
862 for (QSet<QString>::const_iterator i = newwords.constBegin(); i != newwords.constEnd(); ++i) {
863 QString str = *i;
864 QString validValues;
865 if (str.contains("#") && !str.startsWith("#")) {
866 int j = str.indexOf("#");
867 validValues = str.mid(j + 1);
868 str = str.left(j);
869 QStringList lst = validValues.split(",");
870 QString key = str;
871 if (key.endsWith("="))
872 key.chop(1);
873 setKeyValWords(name + "/" + key, convertStringListtoSet(lst));
874 }
875 CompletionWord cw(str, false);
876 cw.index = 0;
877 cw.usageCount = -2;
878 cw.snippetLength = 0;
879 newWordList.append(cw);
880 }
881 std::sort(newWordList.begin(), newWordList.end());
882
883 keyValLists.insert(name, newWordList);
884 }
885
setContextWords(const QSet<QString> & newwords,const QString & context)886 void CompletionListModel::setContextWords(const QSet<QString> &newwords, const QString &context)
887 {
888 QList<CompletionWord> newWordList;
889 newWordList.clear();
890 for (QSet<QString>::const_iterator i = newwords.constBegin(); i != newwords.constEnd(); ++i) {
891 QString str = *i;
892 QString validValues;
893 if (str.contains("#")) {
894 int j = str.indexOf("#");
895 validValues = str.mid(j + 1);
896 str = str.left(j);
897 QStringList lst = validValues.split(",");
898 QString key = str;
899 if (key.endsWith("="))
900 key.chop(1);
901 setKeyValWords(context + "/" + key, convertStringListtoSet(lst));
902 }
903 CompletionWord cw(str);
904 cw.index = 0;
905 cw.usageCount = 2;
906
907 cw.snippetLength = 0;
908 newWordList.append(cw);
909 }
910 std::sort(newWordList.begin(), newWordList.end());
911
912 contextLists.insert(context, newWordList);
913 }
914
makeSortWord(const QString & normal)915 QString makeSortWord(const QString &normal)
916 {
917 QString res = normal.toLower();
918 res.replace("{", "!"); //js: still using dirty hack, however order should be ' '{[* abcde...
919 res.replace("}", "!"); // needs to be replaced as well for sorting \bgein{abc*} after \bgein{abc}
920 res.replace("[", "\\"); //(first is a space->) !"# follow directly in the ascii table
921 res.replace("*", "#");
922 return res;
923 }
924
setEnvironMode(bool mode)925 void CompletionListModel::setEnvironMode(bool mode)
926 {
927 mEnvMode = mode;
928 }
929
cwLessThan(const CompletionWord & s1,const CompletionWord & s2)930 bool cwLessThan(const CompletionWord &s1, const CompletionWord &s2)
931 {
932 return s1.score > s2.score;
933 }
934
935
filterList(const QString & word,int mostUsed,bool fetchMore,CodeSnippet::Type type)936 void CompletionListModel::filterList(const QString &word, int mostUsed, bool fetchMore, CodeSnippet::Type type)
937 {
938 if (mostUsed < 0)
939 mostUsed = LatexCompleter::config->preferedCompletionTab;
940 if (!word.isEmpty() && word == curWord && mostUsed == mostUsedUpdated && !fetchMore) return; //don't return if mostUsed differnt from last call
941 mLastWord = word;
942 mLastMU = mostUsed;
943 mLastType = type;
944 mCanFetchMore = false;
945 mostUsedUpdated = mostUsed;
946 if (!fetchMore)
947 words.clear();
948 Qt::CaseSensitivity cs = Qt::CaseInsensitive;
949 bool checkFirstChar = false;
950 if (LatexCompleter::config) {
951 if (LatexCompleter::config->caseSensitive == LatexCompleterConfig::CCS_CASE_SENSITIVE)
952 cs = Qt::CaseSensitive;
953 checkFirstChar = LatexCompleter::config->caseSensitive == LatexCompleterConfig::CCS_FIRST_CHARACTER_CASE_SENSITIVE && word.length() > 1;
954 }
955 int cnt = 0;
956 QString sortWord = makeSortWord(word);
957 if(mostUsed==2){
958 //fuzzy search
959 // proof of concept
960 // generate regexp
961 //QTime tm;
962 //tm.start();
963 #if (QT_VERSION>=QT_VERSION_CHECK(5,14,0))
964 QStringList chars=word.split("",Qt::SkipEmptyParts);
965 #else
966 QStringList chars=word.split("",QString::SkipEmptyParts);
967 #endif
968 if(chars.value(0)==QChar('\\')){
969 chars.takeFirst();
970 }
971 QString regExpression=chars.join(".*");
972
973 QRegularExpression rx(regExpression);
974
975 words=QtConcurrent::blockingFiltered(baselist,[rx](const CompletionWord &item){
976 return item.sortWord.contains(rx);
977 });
978
979 QtConcurrent::blockingMap(words,[word](CompletionWord &item){
980 int score=0;
981 int l=0;
982 int lastMatch=-2;
983 for(int i=0;i<item.sortWord.length();i++){
984 if(l>=word.length())
985 break;
986 if(item.sortWord.at(i)==word.at(l)){
987 if(lastMatch+1==i)
988 score+=20;
989 score-=i; // later position are degraded
990 lastMatch=i;
991 l++;
992 }
993 }
994 if (item.word.contains('@')) {
995 if(item.word.contains("@@")){ // special treatment for command-names containing @
996 QString ln = item.lines[0];
997 ln.replace("@@", "@");
998 item=CompletionWord(ln);
999 }else{
1000 QString ln = item.lines[0];
1001 ln.replace("{@}", "%<bibid%>");
1002 ln.replace("{@l}", "%<label%>");
1003 item=CompletionWord(ln);
1004 }
1005 }
1006 // reduce score for atypical or unused
1007 if(item.index){
1008 // don't upvote reference commands
1009 score+=item.usageCount<=0 ? 10*item.usageCount : 10;
1010 }
1011 item.score=score;
1012 });
1013
1014 std::stable_sort(words.begin(),words.end(),cwLessThan);
1015 //qDebug()<<tm.elapsed();
1016 }else{
1017 // normal sorting
1018 if (!fetchMore) {
1019 it = std::lower_bound(baselist.begin(), baselist.end(), CompletionWord(word));
1020 }
1021 // special treatment for citation commands as they generated on the fly
1022 //TODO: needs to be adapted to later code
1023 if (it == baselist.end() || !it->word.startsWith(word, cs)) {
1024 int i = word.lastIndexOf("{");
1025 QString test = word.left(i) + "{";
1026 QList<CompletionWord>::iterator lIt = std::lower_bound(baselist.begin(), baselist.end(), CompletionWord(test));
1027 if(lIt != baselist.end()){
1028 // part of command without argument is in baselist
1029 // is it a on-the-fly filled command ?
1030 if(lIt->word.contains('@')){
1031 QString citeStart = word.mid(i + 1);
1032 QString ln = lIt->lines[0];
1033 struct Rpl {
1034 QString id;
1035 QList<CompletionWord>& lst;
1036 };
1037
1038 QMap<QString,std::shared_ptr<Rpl>>replacement;
1039 std::shared_ptr<Rpl> r0(new Rpl{"{%<bibid%>}",wordsCitations});
1040 replacement.insert("@",r0);
1041 std::shared_ptr<Rpl> r1(new Rpl{"{%<label%>}",wordsLabels});
1042 replacement.insert("@l",r1);
1043 for(QMap<QString,std::shared_ptr<Rpl>>::const_iterator localIt=replacement.cbegin();localIt!=replacement.cend();++localIt){
1044 QString searchWord="{"+localIt.key()+"}";
1045 QString key=localIt.key();
1046 std::shared_ptr<Rpl> repl=*localIt;
1047 if(ln.contains(searchWord)){
1048 if(repl->id.startsWith("{%<"+citeStart)){
1049 // keep general id if it matches input
1050 ln.replace(searchWord, repl->id);
1051 words.append(CompletionWord(ln));
1052 }
1053 cnt++;
1054 foreach (const CompletionWord id, repl->lst) {
1055 if(!id.word.startsWith(citeStart))
1056 continue;
1057 CompletionWord cw = *lIt;
1058 int index = cw.lines[0].indexOf(key);
1059 cw.word.replace(key, id.word);
1060 cw.sortWord.replace(key, id.word);
1061 cw.lines[0].replace(key, id.word);
1062 for (int i = 0; i < cw.placeHolders.count(); i++) {
1063 if (cw.placeHolders[i].isEmpty())
1064 continue;
1065 for (int j = 0; j < cw.placeHolders[i].count(); j++) {
1066 CodeSnippetPlaceHolder &ph = cw.placeHolders[i][j];
1067 if (ph.offset > index)
1068 ph.offset += id.word.length() - 1;
1069 }
1070 }
1071 words.append(cw);
1072 }
1073 cnt += repl->id.length();
1074 }
1075 }
1076 }
1077 }
1078 }
1079 //
1080 while (it != baselist.end()) {
1081 if (it->word.startsWith(word, cs) &&
1082 (!checkFirstChar || it->word[1] == word[1]) ) {
1083
1084 if (mostUsed == 3 || it->usageCount >= mostUsed || it->usageCount == -2) {
1085 if (mostUsed < 2 && type != CodeSnippet::none && it->type != type) {
1086 ++it;
1087 continue; // leave out words which don't have the proper type (except for all-mode)
1088 }
1089 if (mEnvMode) {
1090 CompletionWord cw = *it;
1091 if (cw.word.startsWith("\\begin") || cw.word.startsWith("\\end")) {
1092 QString text = cw.word;
1093 int i = text.indexOf('}');
1094 text.truncate(i + 1);
1095 cw.word = text;
1096 cw.sortWord = text;
1097 cw.lines = QStringList(text);
1098 cw.placeHolders.clear();
1099 cw.placeHolders.append(QList<CodeSnippetPlaceHolder>());
1100 }
1101
1102 if (!words.contains(cw))
1103 words.append(cw);
1104 } else {
1105 // cite command
1106 if (it->word.contains('@')) {
1107 if(it->word.contains("@@")){ // special treatment for command-names containing @
1108 QString ln = it->lines[0];
1109 ln.replace("@@", "@");
1110 words.append(CompletionWord(ln));
1111 }else{
1112 QString ln = it->lines[0];
1113 struct Rpl {
1114 QString id;
1115 QList<CompletionWord>& lst;
1116 };
1117
1118 QMap<QString,std::shared_ptr<Rpl>>replacement;
1119 std::shared_ptr<Rpl> r0(new Rpl{"{%<bibid%>}",wordsCitations});
1120 replacement.insert("@",r0);
1121 std::shared_ptr<Rpl> r1(new Rpl{"{%<label%>}",wordsLabels});
1122 replacement.insert("@l",r1);
1123 for(QMap<QString,std::shared_ptr<Rpl>>::const_iterator localIt=replacement.cbegin();localIt!=replacement.cend();++localIt){
1124 QString searchWord="{"+localIt.key()+"}";
1125 QString key=localIt.key();
1126 std::shared_ptr<Rpl> repl=*localIt;
1127 if(ln.contains(searchWord)){
1128 ln.replace(searchWord, repl->id);
1129 words.append(CompletionWord(ln));
1130 cnt++;
1131 foreach (const CompletionWord id, repl->lst) {
1132 CompletionWord cw = *it;
1133 int index = cw.lines[0].indexOf(key);
1134 cw.word.replace(key, id.word);
1135 cw.sortWord.replace(key, id.word);
1136 cw.lines[0].replace(key, id.word);
1137 for (int i = 0; i < cw.placeHolders.count(); i++) {
1138 if (cw.placeHolders[i].isEmpty())
1139 continue;
1140 for (int j = 0; j < cw.placeHolders[i].count(); j++) {
1141 CodeSnippetPlaceHolder &ph = cw.placeHolders[i][j];
1142 if (ph.offset > index)
1143 ph.offset += id.word.length() - 1;
1144 }
1145 }
1146 words.append(cw);
1147 }
1148 cnt += repl->id.length();
1149 }
1150 }
1151 }
1152 } else {
1153 words.append(*it);
1154 }
1155 }
1156 cnt++;
1157 }
1158 } else {
1159 if (!it->sortWord.startsWith(sortWord))
1160 break; // sorted list
1161 }
1162 ++it;
1163 if (cnt > 100) {
1164 mCanFetchMore = true;
1165 break;
1166 }
1167 }
1168 }
1169 curWord = word;
1170 if (!fetchMore) {
1171 mWordCount = words.count();
1172 if (!words.isEmpty())
1173 mLastWordInList = words.last();
1174 }
1175 if (mCanFetchMore && !fetchMore) {
1176 // calculate real number of rows
1177 QString wordp = word;
1178 if (wordp.isEmpty()) {
1179 mWordCount = baselist.count();
1180 mLastWordInList = baselist.last();
1181 } else {
1182 QChar lst = wordp[wordp.length() - 1];
1183 ushort nr = lst.unicode();
1184 wordp[wordp.length() - 1] = QChar(nr + 1);
1185 QList<CompletionWord>::const_iterator it2 = std::lower_bound(baselist.constBegin(),baselist.constEnd(), CompletionWord(wordp));
1186 mWordCount = it2 - it;
1187 if(it2==baselist.constBegin()){
1188 mLastWordInList = baselist.last();
1189 }else{
1190 mLastWordInList = *(--it2);
1191 }
1192 }
1193 }
1194
1195 if (!fetchMore) {
1196 beginResetModel();
1197 endResetModel();
1198 }
1199 }
1200
incUsage(const QModelIndex & index)1201 void CompletionListModel::incUsage(const QModelIndex &index)
1202 {
1203 if (!index.isValid())
1204 return ;
1205
1206 if (index.row() >= words.size())
1207 return ;
1208
1209 int j = index.row();
1210 CompletionWord curWord = words.at(j);
1211 if (curWord.usageCount < -1)
1212 return; // don't count text words
1213
1214 CodeSnippetList::iterator it = std::lower_bound(wordsCommands.begin(), wordsCommands.end(), curWord);
1215 if (it == wordsCommands.end()) // not found, e.g. citations
1216 return;
1217 if (it->word == curWord.word) {
1218 it->usageCount++;
1219 if (curWord.snippetLength > 0) {
1220 bool replaced = false;
1221 QList<QPair<int, int> >res = config->usage.values(curWord.index);
1222 for (int j = 0; j < res.size(); ++j) {
1223 if (res.at(j).first == curWord.snippetLength) {
1224 config->usage.remove(curWord.index, res.at(j));
1225 config->usage.insert(curWord.index, qMakePair(curWord.snippetLength, it->usageCount));
1226 replaced = true;
1227 break;
1228 }
1229 }
1230 if (!replaced)
1231 config->usage.insert(curWord.index, qMakePair(curWord.snippetLength, it->usageCount)); // new word
1232 }
1233 }
1234 }
1235
1236 typedef QPair<int, int> PairIntInt;
setBaseWords(const QSet<QString> & newwords,CompletionType completionType)1237 void CompletionListModel::setBaseWords(const QSet<QString> &newwords, CompletionType completionType)
1238 {
1239 QList<CompletionWord> newWordList;
1240 newWordList.clear();
1241 for (QSet<QString>::const_iterator i = newwords.constBegin(); i != newwords.constEnd(); ++i) {
1242 QString str = *i;
1243 CompletionWord cw(str);
1244 if (completionType == CT_COMMANDS) {
1245 cw.index = qHash(str);
1246 cw.snippetLength = str.length();
1247 cw.usageCount = 0;
1248 QList<QPair<int, int> >res = config->usage.values(cw.index);
1249 foreach (const PairIntInt &elem, res) {
1250 if (elem.first == cw.snippetLength) {
1251 cw.usageCount = elem.second;
1252 break;
1253 }
1254 }
1255 } else {
1256 cw.index = 0;
1257 cw.usageCount = -2;
1258 cw.snippetLength = 0;
1259 }
1260 newWordList.append(cw);
1261 }
1262 std::sort(newWordList.begin(), newWordList.end());
1263
1264 switch (completionType) {
1265 case CT_NORMALTEXT:
1266 wordsText = newWordList;
1267 break;
1268 case CT_CITATIONS:
1269 wordsCitations = newWordList;
1270 break;
1271 default:
1272 wordsCommands = newWordList;
1273 }
1274
1275 //if (completionType==CT_NORMALTEXT) wordsText=newWordList;
1276 //else wordsCommands=newWordList;
1277 baselist = wordsCommands;
1278 }
1279
setBaseWords(const std::set<QString> & newwords,CompletionType completionType)1280 void CompletionListModel::setBaseWords(const std::set<QString> &newwords, CompletionType completionType)
1281 {
1282 QList<CompletionWord> newWordList;
1283 newWordList.clear();
1284 for (std::set<QString>::const_iterator i = newwords.cbegin(); i != newwords.cend(); ++i) {
1285 QString str = *i;
1286 CompletionWord cw(str);
1287 if (completionType == CT_COMMANDS) {
1288 cw.index = qHash(str);
1289 cw.snippetLength = str.length();
1290 cw.usageCount = 0;
1291 QList<QPair<int, int> >res = config->usage.values(cw.index);
1292 foreach (const PairIntInt &elem, res) {
1293 if (elem.first == cw.snippetLength) {
1294 cw.usageCount = elem.second;
1295 break;
1296 }
1297 }
1298 } else {
1299 cw.index = 0;
1300 cw.usageCount = -2;
1301 cw.snippetLength = 0;
1302 }
1303 newWordList.append(cw);
1304 }
1305
1306 switch (completionType) {
1307 case CT_NORMALTEXT:
1308 wordsText = newWordList;
1309 break;
1310 case CT_CITATIONS:
1311 wordsCitations = newWordList;
1312 break;
1313 default:
1314 wordsCommands = newWordList;
1315 }
1316
1317 //if (completionType==CT_NORMALTEXT) wordsText=newWordList;
1318 //else wordsCommands=newWordList;
1319 baselist = wordsCommands;
1320 }
1321
setBaseWords(const QList<CompletionWord> & newwords,CompletionType completionType)1322 void CompletionListModel::setBaseWords(const QList<CompletionWord> &newwords, CompletionType completionType)
1323 {
1324 QList<CompletionWord> newWordList;
1325 newWordList.clear();
1326 foreach (const CompletionWord &cw, newwords) {
1327 newWordList.append(cw);
1328 }
1329 std::sort(newWordList.begin(), newWordList.end());
1330
1331 //if (completionType==CT_NORMALTEXT) wordsText=newWordList;
1332 //else wordsCommands=newWordList;
1333 switch (completionType) {
1334 case CT_NORMALTEXT:
1335 wordsText = newWordList;
1336 break;
1337 case CT_CITATIONS:
1338 wordsCitations = newWordList;
1339 break;
1340 default:
1341 wordsCommands = newWordList;
1342 }
1343
1344 baselist = wordsCommands;
1345 }
1346
setBaseWords(const CodeSnippetList & baseCommands,const CodeSnippetList & newwords,CompletionType completionType)1347 void CompletionListModel::setBaseWords(const CodeSnippetList &baseCommands, const CodeSnippetList &newwords, CompletionType completionType)
1348 {
1349 CodeSnippetList newWordList;
1350
1351 newWordList << baseCommands;
1352 newWordList.unite(newwords);
1353
1354 CodeSnippetList::iterator last = std::unique(newWordList.begin(), newWordList.end());
1355 newWordList.erase(last, newWordList.end());
1356
1357 switch (completionType) {
1358 case CT_NORMALTEXT:
1359 wordsText = newWordList;
1360 break;
1361 case CT_CITATIONS:
1362 wordsCitations = newWordList;
1363 break;
1364 case CT_LABELS:
1365 wordsLabels = newWordList;
1366 break;
1367 default:
1368 wordsCommands = newWordList;
1369 }
1370
1371 baselist = newWordList;
1372 }
1373
setAbbrevWords(const QList<CompletionWord> & newwords)1374 void CompletionListModel::setAbbrevWords(const QList<CompletionWord> &newwords)
1375 {
1376 wordsAbbrev = newwords;
1377 std::sort(wordsAbbrev.begin(), wordsAbbrev.end());
1378 }
1379
setConfig(LatexCompleterConfig * newConfig)1380 void CompletionListModel::setConfig(LatexCompleterConfig *newConfig)
1381 {
1382 config = newConfig;
1383 }
1384
1385
1386 //------------------------------completer-----------------------------------
1387 LatexReference *LatexCompleter::latexReference = nullptr;
1388 LatexCompleterConfig *LatexCompleter::config = nullptr;
1389
LatexCompleter(const LatexParser & latexParser,QObject * p)1390 LatexCompleter::LatexCompleter(const LatexParser &latexParser, QObject *p): QObject(p), latexParser(latexParser), maxWordLen(0), editorAutoCloseChars(false), forcedRef(false),
1391 forcedGraphic(false), forcedCite(false), forcedPackage(false), forcedKeyval(false), forcedSpecialOption(false), forcedLength(false), startedFromTriggerKey(false)
1392 {
1393 // addTrigger("\\");
1394 if (!qobject_cast<QWidget *>(parent()))
1395 QMessageBox::critical(nullptr, "Serious PROBLEM", QString("The completer has been created without a parent widget. This is impossible!\n") +
1396 QString("Please report it ASAP to the bug tracker on texstudio.sf.net and check if your computer is going to explode!\n") +
1397 QString("(please report the bug *before* going to a safe place, you could rescue others)"), QMessageBox::Ok);
1398 list = new QListView(qobject_cast<QWidget *>(parent()));
1399 listModel = new CompletionListModel(list);
1400 connect(list, SIGNAL(clicked(const QModelIndex&)) , this, SLOT(selectionChanged(const QModelIndex&)));
1401 list->setModel(listModel);
1402 list->setFocusPolicy(Qt::NoFocus);
1403 list->setItemDelegate(new CompletionItemDelegate(list));
1404 list->setAutoFillBackground(true);
1405 editor = nullptr;
1406 workingDir = "/";
1407 dirReader = nullptr;
1408 bibReader = nullptr;
1409 packageList = nullptr;
1410 widget = new QWidget(qobject_cast<QWidget *>(parent()));
1411 //widget->setAutoFillBackground(true);
1412 QVBoxLayout *layout = new QVBoxLayout;
1413 layout->setSpacing(0);
1414 tbAbove = new QTabBar();
1415 tbAbove->setShape(QTabBar::RoundedNorth);
1416 tbAbove->addTab(tr("typical"));
1417 tbAbove->addTab(tr("most used"));
1418 tbAbove->addTab(tr("fuzzy"));
1419 tbAbove->addTab(tr("all"));
1420 tbAbove->setToolTip(tr("press shift+space to change view"));
1421 layout->addWidget(tbAbove);
1422 tbAbove->hide();
1423 layout->addWidget(list);
1424 tbBelow = new QTabBar();
1425 tbBelow->setShape(QTabBar::RoundedSouth);
1426 tbBelow->addTab(tr("typical"));
1427 tbBelow->addTab(tr("most used"));
1428 tbBelow->addTab(tr("fuzzy"));
1429 tbBelow->addTab(tr("all"));
1430 tbBelow->setToolTip(tr("press shift+space to change view"));
1431 layout->addWidget(tbBelow);
1432 widget->setLayout(layout);
1433 widget->setContextMenuPolicy(Qt::PreventContextMenu);
1434 connect(list, SIGNAL(activated(QModelIndex)), this, SLOT(listClicked(QModelIndex)));
1435 // todo: change tab when shift+space is pressed ...
1436 //connect(tbBelow,SIGNAL(currentChanged(int)),this,SLOT(changeView(int)));
1437 //connect(tbAbove,SIGNAL(currentChanged(int)),this,SLOT(changeView(int)));
1438 }
1439
~LatexCompleter()1440 LatexCompleter::~LatexCompleter()
1441 {
1442 if (dirReader) {
1443 dirReader->quit();
1444 dirReader->wait();
1445 delete dirReader;
1446 }
1447 if (bibReader) {
1448 bibReader->quit();
1449 bibReader->wait();
1450 delete bibReader;
1451 }
1452 }
1453
changeView(int pos)1454 void LatexCompleter::changeView(int pos)
1455 {
1456 completerInputBinding->setMostUsed(pos);
1457 }
1458
listClicked(QModelIndex index)1459 void LatexCompleter::listClicked(QModelIndex index)
1460 {
1461
1462 Q_UNUSED(index)
1463 if (!completerInputBinding->insertCompletedWord()) {
1464 editor->insertText("\n");
1465 }
1466 completerInputBinding->resetBinding();
1467 }
1468
insertText(QString txt)1469 void LatexCompleter::insertText(QString txt)
1470 {
1471 if (!isVisible())
1472 return;
1473 completerInputBinding->insertText(txt);
1474 QString cur = completerInputBinding->getCurWord();
1475 filterList(cur, completerInputBinding->getMostUsed());
1476 }
1477
setAdditionalWords(const QSet<QString> & newwords,CompletionType completionType)1478 void LatexCompleter::setAdditionalWords(const QSet<QString> &newwords, CompletionType completionType)
1479 {
1480 // convert to codesnippets
1481 CodeSnippetList newWordList;
1482 for (QSet<QString>::const_iterator i = newwords.constBegin(); i != newwords.constEnd(); ++i) {
1483 QString str = *i;
1484 bool isReference = str.startsWith('@');
1485 if (isReference)
1486 str = str.mid(1);
1487 CompletionWord cw(str);
1488 if (completionType == CT_COMMANDS) {
1489 cw.index = qHash(str);
1490 cw.snippetLength = str.length();
1491 cw.usageCount = isReference ? 2 : 0; // make reference always visible (most used) in completer
1492 QList<QPair<int, int> >res = config->usage.values(cw.index);
1493 foreach (const PairIntInt &elem, res) {
1494 if (elem.first == cw.snippetLength) {
1495 cw.usageCount = elem.second;
1496 break;
1497 }
1498 }
1499 } else {
1500 cw.index = 0;
1501 cw.usageCount = -2;
1502 cw.snippetLength = 0;
1503 }
1504 newWordList.append(cw);
1505 }
1506 std::sort(newWordList.begin(), newWordList.end());
1507 //
1508 CodeSnippetList concated;
1509 if (config && completionType == CT_COMMANDS) concated.unite(config->words);
1510 //concated.unite(newwords);
1511 listModel->setBaseWords(concated, newWordList, completionType);
1512 widget->resize(200, 200);
1513 }
1514 /*!
1515 * \brief setAdditionalWords from a std::set<QString>
1516 * The advantage is that a std::set is already sorted.
1517 * \param newwords
1518 * \param completionType
1519 */
setAdditionalWords(const std::set<QString> & newwords,CompletionType completionType)1520 void LatexCompleter::setAdditionalWords(const std::set<QString> &newwords, CompletionType completionType)
1521 {
1522 // convert to codesnippets
1523 CodeSnippetList newWordList;
1524 for (std::set<QString>::const_iterator i = newwords.cbegin(); i != newwords.cend(); ++i) {
1525 QString str = *i;
1526 bool isReference = str.startsWith('@');
1527 if (isReference)
1528 str = str.mid(1);
1529 CompletionWord cw(str);
1530 if (completionType == CT_COMMANDS) {
1531 cw.index = qHash(str);
1532 cw.snippetLength = str.length();
1533 cw.usageCount = isReference ? 2 : 0; // make reference always visible (most used) in completer
1534 QList<QPair<int, int> >res = config->usage.values(cw.index);
1535 foreach (const PairIntInt &elem, res) {
1536 if (elem.first == cw.snippetLength) {
1537 cw.usageCount = elem.second;
1538 break;
1539 }
1540 }
1541 } else {
1542 cw.index = 0;
1543 cw.usageCount = -2;
1544 cw.snippetLength = 0;
1545 }
1546 newWordList.append(cw);
1547 }
1548
1549 CodeSnippetList concated;
1550 if (config && completionType == CT_COMMANDS) concated.unite(config->words);
1551 //concated.unite(newwords);
1552 listModel->setBaseWords(concated, newWordList, completionType);
1553 widget->resize(200, 200);
1554 }
1555
setAdditionalWords(const CodeSnippetList & newwords,CompletionType completionType)1556 void LatexCompleter::setAdditionalWords(const CodeSnippetList &newwords, CompletionType completionType)
1557 {
1558 CodeSnippetList concated;
1559 if (config && completionType == CT_COMMANDS) concated.unite(config->words);
1560 //concated.unite(newwords);
1561 listModel->setBaseWords(concated, newwords, completionType);
1562 widget->resize(200, 200);
1563 }
1564
setKeyValWords(const QString & name,const QSet<QString> & newwords)1565 void LatexCompleter::setKeyValWords(const QString &name, const QSet<QString> &newwords)
1566 {
1567 listModel->setKeyValWords(name, newwords);
1568 }
1569
setContextWords(const QSet<QString> & newwords,const QString & context)1570 void LatexCompleter::setContextWords(const QSet<QString> &newwords, const QString &context)
1571 {
1572 listModel->setContextWords(newwords, context);
1573 }
1574
1575 /*!
1576 * \brief adjust width and position of the widget.
1577 * \return BelowCursor if the calculated position is below the cursor, otherwise AboveCursor
1578 */
adjustWidget()1579 void LatexCompleter::adjustWidget()
1580 {
1581 // adjust width
1582 int newWordMax = 0;
1583 QFont f = QApplication::font();
1584 f.setItalic(true);
1585 QFontMetrics fm(f);
1586 const QList<CompletionWord> &words = listModel->getWords();
1587 for (int i = 0; i < words.size(); i++) {
1588 if (words[i].lines.empty() || words[i].placeHolders.empty()) continue;
1589 int temp = UtilsUi::getFmWidth(fm, words[i].lines[0]) + words[i].placeHolders[0].size() + 10;
1590 if (temp > newWordMax) newWordMax = temp;
1591 }
1592 maxWordLen = newWordMax;
1593 int width = qMax(400, maxWordLen);
1594 QScrollBar *bar = list->verticalScrollBar();
1595 if (bar && bar->isVisible()) {
1596 width += bar->width() * 4;
1597 }
1598 widget->resize(width, 200);
1599
1600 // adjust position
1601 QPointF offset;
1602 bool isAboveCursor = false;
1603 if (editor->getPositionBelowCursor(offset, widget->width(), widget->height(), isAboveCursor))
1604 widget->move(editor->mapTo(qobject_cast<QWidget *>(parent()), offset.toPoint()));
1605
1606 // adjust visible tab bar depending on location relative to cursor
1607 QTabBar *tbOn = (isAboveCursor) ? tbAbove : tbBelow;
1608 QTabBar *tbOff = (isAboveCursor) ? tbBelow : tbAbove;
1609 disconnect(tbOn, SIGNAL(currentChanged(int)), this, SLOT(changeView(int)));
1610 disconnect(tbOff, SIGNAL(currentChanged(int)), this, SLOT(changeView(int)));
1611 tbOn->show();
1612 tbOff->hide();
1613 tbOn->setCurrentIndex(config->preferedCompletionTab);
1614 connect(tbOn, SIGNAL(currentChanged(int)), this, SLOT(changeView(int)));
1615 }
1616
updateAbbreviations()1617 void LatexCompleter::updateAbbreviations()
1618 {
1619 REQUIRE(config);
1620 QList<CompletionWord> wordsAbbrev;
1621 foreach (const Macro ¯o, config->userMacros) {
1622 if (macro.abbrev.isEmpty() || macro.snippet().isEmpty())
1623 continue;
1624 //CompletionWord cw(abbr);
1625 // for compatibility to texmaker ...
1626 CompletionWord cw(macro.snippet());
1627 // <!compatibility>
1628 cw.word = macro.abbrev;
1629 cw.sortWord = makeSortWord(cw.word);
1630 cw.setName(macro.abbrev + tr(" (Usertag)"));
1631 wordsAbbrev << cw;
1632 }
1633 listModel->setAbbrevWords(wordsAbbrev);
1634 }
1635
complete(QEditor * newEditor,const CompletionFlags & flags)1636 void LatexCompleter::complete(QEditor *newEditor, const CompletionFlags &flags)
1637 {
1638 Q_ASSERT(list);
1639 Q_ASSERT(listModel);
1640 Q_ASSERT(completerInputBinding);
1641
1642 bool alreadyActive=completerInputBinding->isActive(); // called with already open completer
1643
1644 forcedRef = flags & CF_FORCE_REF;
1645 forcedGraphic = flags & CF_FORCE_GRAPHIC;
1646 forcedCite = flags & CF_FORCE_CITE;
1647 forcedPackage = flags & CF_FORCE_PACKAGE;
1648 forcedKeyval = flags & CF_FORCE_KEYVAL;
1649 forcedSpecialOption = flags & CF_FORCE_SPECIALOPTION;
1650 forcedLength = flags & CF_FORCE_LENGTH;
1651 startedFromTriggerKey = !(flags & CF_FORCE_VISIBLE_LIST);
1652 if (editor != newEditor) {
1653 if (editor) disconnect(editor, SIGNAL(destroyed()), this, SLOT(editorDestroyed()));
1654 if (newEditor) connect(newEditor, SIGNAL(destroyed()), this, SLOT(editorDestroyed()));
1655 editor = newEditor;
1656 }
1657 if (!editor) return;
1658 QDocumentCursor c = editor->cursor();
1659 if (!c.isValid()) return;
1660 /*int phId=editor->currentPlaceHolder();
1661 if(phId>-1 && phId<editor->placeHolderCount()){
1662 PlaceHolder ph=editor->getPlaceHolder(phId);
1663 if(ph.cursor.isWithinSelection(c) && !ph.mirrors.isEmpty()){
1664 editor->removePlaceHolder(phId);
1665 }
1666 }*/
1667 if (editor->cursorMirrorCount() > 0) return; // completer does not handle cursor mirrors. It would just remove them. -> don't use completer
1668
1669 if (c.hasSelection()) {
1670 c.setColumnNumber(qMax(c.columnNumber(), c.anchorColumnNumber()));
1671 editor->setCursor(c);
1672 }
1673
1674 //disable auto close char while completer is open
1675 if(!alreadyActive)
1676 editorAutoCloseChars = editor->flag(QEditor::AutoCloseChars); // don't change again from open completer (and therin changed flag, see #1347)
1677
1678 editor->setFlag(QEditor::AutoCloseChars, false);
1679
1680 completerInputBinding->setMostUsed(config->preferedCompletionTab, true);
1681 bool handled = false;
1682 if (forcedGraphic) {
1683 if (!dirReader) {
1684 dirReader = new directoryReader(this);
1685 connect(dirReader, &directoryReader::directoryLoaded, this, &LatexCompleter::directoryLoaded);
1686 connect(this, SIGNAL(setDirectoryForCompletion(QString)), dirReader, SLOT(readDirectory(QString)));
1687 dirReader->start();
1688 }
1689 QSet<QString> files;
1690 listModel->setBaseWords(files, CT_NORMALTEXT);
1691 listModel->baselist = listModel->wordsText;
1692 handled = true;
1693 }
1694 if (forcedCite) {
1695 listModel->baselist = listModel->wordsCitations;
1696 handled = true;
1697 }
1698 if (forcedRef) {
1699 listModel->baselist = listModel->wordsLabels;
1700 handled = true;
1701 }
1702 if (forcedPackage) {
1703 listModel->setBaseWords(*packageList, CT_NORMALTEXT);
1704 listModel->baselist = listModel->wordsText;
1705 handled = true;
1706 }
1707 if (forcedKeyval) {
1708 listModel->baselist.clear();
1709 handled = true;
1710 foreach (const CompletionWord &cw, listModel->keyValLists.value(workingDir)) {
1711 if (cw.word.startsWith("#")) {
1712 if (cw.word == "#L") {
1713 // complete with length
1714 forcedLength = true;
1715 forcedKeyval = false;
1716 handled = false;
1717 break;
1718 }
1719 handled=true;
1720 break;
1721 }
1722 if (cw.word.startsWith('%')) {
1723 QString specialList = cw.word;
1724 if (listModel->contextLists.contains(specialList)) {
1725 listModel->baselist << listModel->contextLists.value(specialList);
1726 QList<CompletionWord>::iterator middle = listModel->baselist.end() - listModel->contextLists.value(specialList).length();
1727 std::inplace_merge(listModel->baselist.begin(), middle, listModel->baselist.end());
1728 }
1729 } else {
1730 // nothing special, simply add
1731 QList<CompletionWord>::iterator it;
1732 it = std::lower_bound(listModel->baselist.begin(), listModel->baselist.end(), cw);
1733 listModel->baselist.insert(it, cw); // keep sorting
1734 }
1735 }
1736
1737
1738 }
1739 if (forcedSpecialOption) {
1740 listModel->baselist = listModel->contextLists.value(workingDir);
1741
1742 handled = true;
1743 }
1744 if (!handled) {
1745 if (flags & CF_NORMAL_TEXT) listModel->baselist = listModel->wordsText;
1746 else listModel->baselist = listModel->wordsCommands;
1747 listModel->baselist << listModel->wordsAbbrev;
1748 QList<CompletionWord>::iterator middle = listModel->baselist.end() - listModel->wordsAbbrev.length();
1749 std::inplace_merge(listModel->baselist.begin(), middle, listModel->baselist.end());
1750 }
1751 if ( editor->currentPlaceHolder() >= 0 && editor->currentPlaceHolder() < editor->placeHolderCount() ) {
1752 PlaceHolder ph = editor->getPlaceHolder(editor->currentPlaceHolder());
1753 if (ph.mirrors.count() > 0)
1754 listModel->setEnvironMode(true);
1755 }
1756 //qSort(listModel->baselist.begin(),listModel->baselist.end());
1757 if (c.previousChar() != '\\' || (flags & CF_FORCE_VISIBLE_LIST)) {
1758 int start = c.columnNumber() - 1;
1759 if (flags & CF_NORMAL_TEXT) start = 0;
1760 if (flags & CF_FORCE_GRAPHIC) start = 0;
1761 QString eow = "~!@#$%^&*()_+{}|:\"<>?,./;[]-= \n\r`+�\t";
1762 if (flags & CF_FORCE_CITE) {
1763 // the prohibited chars in bibtex keys are not well documented and differ among bibtex tools
1764 // this is what JabRef uses (assuming they have a good understanding due to the maturity of the project):
1765 eow = "\n\r\t #{}\\\"~,^'";
1766 }
1767 if (flags & CF_FORCE_GRAPHIC) {
1768 eow.remove("/");
1769 eow.remove("\\");
1770 eow.remove(".");
1771 eow.remove(":");
1772 eow.remove("_");
1773 }
1774 if (flags & CF_FORCE_PACKAGE) {
1775 eow.remove("_");
1776 eow.remove("-");
1777 }
1778 if (flags == CF_FORCE_VISIBLE_LIST)
1779 eow.remove("{");
1780 if (flags & CF_FORCE_REF) eow = "[]{}\\";
1781 if (flags & CF_FORCE_REFLIST) eow = "[]{}\\,";
1782 QString lineText = c.line().text();
1783 int i = c.columnNumber() - 1;
1784 if(i>lineText.length()){
1785 i=lineText.length()-1; // avoid crash
1786 }
1787 for (; i >= 0; i--) {
1788 if ((lineText.at(i) == QChar('\\')) && !(flags & CF_FORCE_GRAPHIC)) {
1789 start = i;
1790 break;
1791 } else if (eow.contains(lineText.at(i))) {
1792 if (flags & CF_NORMAL_TEXT) start = i + 1;
1793 if (flags & CF_FORCE_GRAPHIC) start = i + 1;
1794 if (flags & CF_FORCE_CITE) start = i + 1;
1795 if (flags & CF_FORCE_REF) start = i + 1;
1796 if (flags & CF_FORCE_PACKAGE) start = i + 1;
1797 if (flags & CF_FORCE_KEYVAL) start = i + 1;
1798 if (flags & CF_FORCE_SPECIALOPTION) start = i + 1;
1799 break;
1800 }
1801 }
1802 QString path;
1803 if (flags & CF_FORCE_GRAPHIC) {
1804 QString fn = lineText.mid(start, c.columnNumber() - start);
1805 int lastIndex = fn.lastIndexOf("/");
1806 if (lastIndex >= 0)
1807 start = start + lastIndex + 1;
1808 if (fn.isEmpty())
1809 fn = workingDir + "/";
1810
1811 QFileInfo fi(QDir(workingDir),fn);
1812 path = fi.absolutePath();
1813 }
1814 completerInputBinding->bindTo(editor, this, true, start);
1815 adjustWidget();
1816
1817 if ((flags & CF_FORCE_GRAPHIC) && !path.isEmpty()) {
1818 emit setDirectoryForCompletion(path);
1819 }
1820 } else completerInputBinding->bindTo(editor, this, false, c.columnNumber() - 1);
1821
1822 if (completerInputBinding->getMostUsed() == 1 && countWords() == 0) { // if prefered list is empty, take next more extensive one
1823 setTab(0); // typical
1824 adjustWidget();
1825 }
1826 if (completerInputBinding->getMostUsed() && countWords() == 0) {
1827 setTab(3); // all
1828 adjustWidget();
1829 }
1830
1831 if (completerInputBinding->getMostUsed() == config->preferedCompletionTab)
1832 completerInputBinding->setMostUsed(config->preferedCompletionTab, false);
1833
1834 //line.document()->cursor(0,0).insertText(QString::number(offset.x())+":"+QString::number(offset.y()));
1835 connect(editor, SIGNAL(cursorPositionChanged()), this, SLOT(cursorPositionChanged()));
1836
1837 completerInputBinding->autoOverridenText = (flags & CF_OVERRIDEN_BACKSLASH) ? "\\" : "";
1838
1839 if (config && config->completeCommonPrefix && alreadyActive) completerInputBinding->completeCommonPrefix(); // only complete common prefix if the completer was visible when called
1840 }
1841
directoryLoaded(QString,QSet<QString> content)1842 void LatexCompleter::directoryLoaded(QString , QSet<QString> content)
1843 {
1844 listModel->setBaseWords(content, CT_NORMALTEXT);
1845 listModel->baselist = listModel->wordsText;
1846 //setTab(2);
1847 completerInputBinding->setMostUsed(3); // all
1848 adjustWidget();
1849 }
1850
acceptTriggerString(const QString & trigger)1851 bool LatexCompleter::acceptTriggerString(const QString &trigger)
1852 {
1853 return trigger == "\\" && (!config || config->enabled);
1854 }
1855
setConfig(LatexCompleterConfig * config)1856 void LatexCompleter::setConfig(LatexCompleterConfig *config)
1857 {
1858 REQUIRE(config);
1859 this->config = config;
1860 listModel->setConfig(config);
1861
1862 QFont f(QApplication::font());
1863 f.setPointSize( f.pointSize() * config->tabRelFontSizePercent / 100);
1864 tbAbove->setFont(f);
1865 tbBelow->setFont(f);
1866 }
1867
getConfig() const1868 LatexCompleterConfig *LatexCompleter::getConfig() const
1869 {
1870 return config;
1871 }
1872
setPackageList(std::set<QString> * lst)1873 void LatexCompleter::setPackageList(std::set<QString> *lst)
1874 {
1875 packageList = lst;
1876 }
1877
countWords()1878 int LatexCompleter::countWords()
1879 {
1880 return listModel->getWords().count();
1881 }
1882
setTab(int index)1883 void LatexCompleter::setTab(int index)
1884 {
1885 Q_ASSERT(index >= 0 && index < 4);
1886 if (tbBelow->isVisible())
1887 tbBelow->setCurrentIndex(index);
1888 if (tbAbove->isVisible())
1889 tbAbove->setCurrentIndex(index);
1890 }
1891
filterList(QString word,int showMostUsed)1892 void LatexCompleter::filterList(QString word, int showMostUsed)
1893 {
1894 QString cur = ""; //needed to preserve selection
1895 if (list->isVisible() && list->currentIndex().isValid())
1896 cur = list->model()->data(list->currentIndex(), Qt::DisplayRole).toString();
1897 CodeSnippet::Type type = CodeSnippet::none;
1898 if (forcedLength)
1899 type = CodeSnippet::length;
1900 listModel->filterList(word, showMostUsed, false, type);
1901 if (cur != "") {
1902 int p = listModel->getWords().indexOf(cur);
1903 if (p >= 0) list->setCurrentIndex(list->model()->index(p, 0, QModelIndex()));
1904 }
1905 }
1906
acceptChar(QChar c,int pos)1907 bool LatexCompleter::acceptChar(QChar c, int pos)
1908 {
1909 //always accept alpha numerical characters
1910 if (((c >= QChar('a')) && (c <= QChar('z'))) ||
1911 ((c >= QChar('A')) && (c <= QChar('Z'))) ||
1912 ((c >= QChar('0')) && (c <= QChar('9')))) return true;
1913 if (pos <= 1) return false;
1914 if (listModel->isNextCharPossible(c))
1915 return true; //only accept non standard character, if one of the current words contains it
1916 return false;
1917 }
1918
cursorPositionChanged()1919 void LatexCompleter::cursorPositionChanged()
1920 {
1921 if (!completerInputBinding || !completerInputBinding->isActive()) {
1922 disconnect(editor, SIGNAL(cursorPositionChanged()), this, SLOT(cursorPositionChanged()));
1923 return;
1924 }
1925 completerInputBinding->cursorPositionChanged(editor);
1926 }
1927
1928 //called when item is clicked, more important: normal (not signal/slot) called when completerbinding change selection
selectionChanged(const QModelIndex & index)1929 void LatexCompleter::selectionChanged(const QModelIndex &index)
1930 {
1931 // Note on tooltip usage: It is not possible to hide and shortly afterwards reshow a tooltip
1932 // with the same text (there's some interference between the internal hideTimer of the tooltip and
1933 // the internal tooltip-reuse when QToolTip::showText() for the same text. As a result, the tooltip
1934 // does not persist. This is a Qt-Bug.
1935 // Workaround: we cannot QToolTip::hideText generally and QToolTip::showText as needed. Therefore we have to
1936 // hide the tooltip in every single exit branch of the function that does not show the tooltip.
1937 if (!index.isValid() || !isVisible()) {
1938 QToolTip::hideText();
1939 return;
1940 }
1941 if (config->tooltipPreview && forcedGraphic) { // picture preview even if help is disabled (maybe the same for cite/ref ?)
1942 QString fn = workingDir + QDir::separator() + listModel->words[index.row()].word;
1943 QToolTip::hideText();
1944 emit showImagePreview(fn);
1945 return;
1946 }
1947
1948 if (index.row() < 0 || index.row() >= listModel->words.size()) {
1949 QToolTip::hideText();
1950 return;
1951 }
1952 if (config->tooltipPreview && forcedSpecialOption && workingDir == "%color") {
1953 QToolTip::hideText();
1954 QString text;
1955 text = QString("{\\color{%1} \\rule{1cm}{1cm}}").arg(listModel->words[index.row()].word);
1956 emit showPreview(text);
1957 return;
1958 }
1959 QRegExp wordrx("^\\\\([^ {[*]+|begin\\{[^ {}]+)");
1960 if (!forcedCite && wordrx.indexIn(listModel->words[index.row()].word) == -1) {
1961 QToolTip::hideText();
1962 return;
1963 }
1964 QString cmd = wordrx.cap(0);
1965 QString topic;
1966 if (config->tooltipPreview && latexParser.possibleCommands["%ref"].contains(cmd)) {
1967 QString value = listModel->words[index.row()].word;
1968 int i = value.indexOf("{");
1969 value.remove(0, i + 1);
1970 i = value.indexOf("}");
1971 value = value.left(i);
1972 LatexDocument *document = qobject_cast<LatexDocument *>(editor->document());
1973 int cnt = document->countLabels(value);
1974 topic = "";
1975 if (cnt == 0) {
1976 topic = tr("label missing!");
1977 } else if (cnt > 1) {
1978 topic = tr("label defined multiple times!");
1979 } else {
1980 QMultiHash<QDocumentLineHandle *, int> result = document->getLabels(value);
1981 QDocumentLineHandle *mLine = result.keys().constFirst();
1982 int l = mLine->document()->indexOf(mLine);
1983 if (mLine->document() != editor->document()) {
1984 //LatexDocument *doc=document->parent->findDocument(mLine->document());
1985 LatexDocument *doc = qobject_cast<LatexDocument *>(mLine->document());
1986 Q_ASSERT_X(doc, "missing latexdoc", "qdoc is not latex document !");
1987 if (doc) topic = tr("<p style='white-space:pre'><b>Filename: %1</b>\n").arg(doc->getFileName());
1988 }
1989 for (int i = qMax(0, l - 2); i < qMin(mLine->document()->lines(), l + 3); i++) {
1990 topic += mLine->document()->line(i).text().left(80);
1991 if (mLine->document()->line(i).text().length() > 80) topic += "...";
1992 if (i < l + 2) topic += "\n";
1993 }
1994 }
1995 } else if (config->tooltipPreview && (forcedCite || latexParser.possibleCommands["%cite"].contains(cmd))) {
1996 QToolTip::hideText();
1997 QString value = listModel->words[index.row()].word;
1998 const QRegularExpression re{"{([^}]+?)}"};
1999 const QRegularExpressionMatch match = re.match(value);
2000 if(match.hasMatch()){
2001 value = match.captured(1);
2002 }
2003 LatexDocument *document = qobject_cast<LatexDocument *>(editor->document());
2004 if (!bibReader) {
2005 bibReader = new bibtexReader(this);
2006 connect(bibReader, SIGNAL(sectionFound(QString)), this, SLOT(bibtexSectionFound(QString)));
2007 connect(this, SIGNAL(searchBibtexSection(QString,QString)), bibReader, SLOT(searchSection(QString,QString)));
2008 bibReader->start();
2009 }
2010 QString file = document->findFileFromBibId(value);
2011 if (!file.isEmpty())
2012 emit searchBibtexSection(file, value);
2013 return;
2014 } else {
2015 if (latexReference && config->tooltipHelp)
2016 topic = latexReference->getTextForTooltip(cmd);
2017 if (topic.isEmpty()) {
2018 QToolTip::hideText();
2019 return;
2020 }
2021 }
2022 showTooltip(topic);
2023 }
2024
showTooltip(QString text)2025 void LatexCompleter::showTooltip(QString text)
2026 {
2027 if (!isVisible()) {
2028 QToolTip::hideText();
2029 return;
2030 }
2031 QModelIndex index = list->currentIndex();
2032 QRect r = list->visualRect(index);
2033 QDocumentCursor c = editor->cursor();
2034 qreal lineHeight = c.line().document()->getLineSpacing();
2035 QPoint pos = list->mapToGlobal(QPoint(list->width(), r.top() - lineHeight));
2036 showTooltipLimited(pos, text, list->width());
2037 }
2038
editorDestroyed()2039 void LatexCompleter::editorDestroyed()
2040 {
2041 editor = nullptr;
2042 }
2043
bibtexSectionFound(QString content)2044 void LatexCompleter::bibtexSectionFound(QString content)
2045 {
2046 showTooltip(content);
2047 }
2048
2049 //ends completion (closes the list) and returns true if there was any
close()2050 bool LatexCompleter::close()
2051 {
2052 if (completerInputBinding->isActive()) {
2053 completerInputBinding->simpleRestoreAutoOverride();
2054 completerInputBinding->resetBinding();
2055 widget->setVisible(false);
2056 listModel->curWord = "";
2057 return true;
2058 } else return false;
2059 }
2060
setFiles(const QStringList & newFiles)2061 void LatexCompleterConfig::setFiles(const QStringList &newFiles)
2062 {
2063 files = newFiles;
2064 }
2065
getLoadedFiles() const2066 const QStringList &LatexCompleterConfig::getLoadedFiles() const
2067 {
2068 return files;
2069 }
2070
existValues()2071 bool LatexCompleter::existValues()
2072 {
2073 return listModel->keyValLists.value(workingDir).size() > 0;
2074 }
2075