1 /*
2 # PostgreSQL Database Modeler (pgModeler)
3 #
4 # Copyright 2006-2020 - Raphael Araújo e Silva <raphael@pgmodeler.io>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation version 3.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # The complete text of GPLv3 is at LICENSE file on source code root directory.
16 # Also, you can get the complete GNU General Public License at <http://www.gnu.org/licenses/>
17 */
18
19 #include "codecompletionwidget.h"
20 #include "generalconfigwidget.h"
21 #include "pgmodeleruins.h"
22 #include "snippetsconfigwidget.h"
23
CodeCompletionWidget(QPlainTextEdit * code_field_txt,bool enable_snippets)24 CodeCompletionWidget::CodeCompletionWidget(QPlainTextEdit *code_field_txt, bool enable_snippets) : QWidget(dynamic_cast<QWidget *>(code_field_txt))
25 {
26 if(!code_field_txt)
27 throw Exception(ErrorCode::AsgNotAllocattedObject,__PRETTY_FUNCTION__,__FILE__,__LINE__);
28
29 this->enable_snippets = enable_snippets;
30 popup_timer.setInterval(300);
31
32 completion_wgt=new QWidget(this);
33 completion_wgt->setWindowFlags(Qt::Popup);
34
35 name_list=new QListWidget(completion_wgt);
36 name_list->setSpacing(2);
37 name_list->setIconSize(QSize(16,16));
38 name_list->setSortingEnabled(false);
39
40 persistent_chk=new QCheckBox(completion_wgt);
41 persistent_chk->setText(tr("Make &persistent"));
42 persistent_chk->setToolTip(tr("Makes the widget closable only by ESC key or mouse click on other controls."));
43 persistent_chk->setFocusPolicy(Qt::NoFocus);
44
45 QVBoxLayout *vbox=new QVBoxLayout(completion_wgt);
46 vbox->addWidget(name_list);
47 vbox->addWidget(persistent_chk);
48 vbox->setContentsMargins(4,4,4,4);
49 vbox->setSpacing(6);
50 completion_wgt->setLayout(vbox);
51
52 PgModelerUiNs::configureWidgetFont(name_list, PgModelerUiNs::MediumFontFactor);
53
54 this->code_field_txt=code_field_txt;
55 auto_triggered=false;
56
57 db_model=nullptr;
58 setQualifyingLevel(nullptr);
59
60 connect(name_list, SIGNAL(itemDoubleClicked(QListWidgetItem*)), this, SLOT(selectItem()));
61 connect(name_list, SIGNAL(currentRowChanged(int)), this, SLOT(showItemTooltip()));
62
63 connect(&popup_timer, &QTimer::timeout, [&](){
64 if(qualifying_level < 2)
65 {
66 auto_triggered=true;
67 this->show();
68 }
69 });
70
71 this->setVisible(false);
72
73 if(enable_snippets)
74 connect(this, SIGNAL(s_wordSelected(QString)), this, SLOT(handleSelectedWord(QString)));
75 }
76
handleSelectedWord(QString word)77 void CodeCompletionWidget::handleSelectedWord(QString word)
78 {
79 if(SnippetsConfigWidget::isSnippetExists(word))
80 {
81 QTextCursor tc=code_field_txt->textCursor();
82 tc.movePosition(QTextCursor::PreviousWord, QTextCursor::KeepAnchor);
83 tc.removeSelectedText();
84 tc.insertText(SnippetsConfigWidget::getParsedSnippet(word));
85 }
86 }
87
eventFilter(QObject * object,QEvent * event)88 bool CodeCompletionWidget::eventFilter(QObject *object, QEvent *event)
89 {
90 QKeyEvent *k_event=dynamic_cast<QKeyEvent *>(event);
91
92 if(k_event && k_event->type()==QEvent::KeyPress)
93 {
94 if(object==code_field_txt)
95 {
96 //Filters the trigger char and shows up the code completion only if there is a valid database model in use
97 if(QChar(k_event->key())==completion_trigger && db_model)
98 {
99 /* If the completion widget is not visible start the timer to give the user
100 a small delay in order to type another character. If no char is typed the completion is triggered */
101 if(!completion_wgt->isVisible() && !popup_timer.isActive())
102 popup_timer.start();
103
104 if(name_list->isVisible())
105 {
106 this->selectItem();
107 this->show();
108 }
109 }
110 else
111 {
112 popup_timer.stop();
113
114 //Filters the Crtl+Space to trigger the code completion
115 if(k_event->key()==Qt::Key_Space && (k_event->modifiers()==Qt::ControlModifier || k_event->modifiers()==Qt::MetaModifier))
116 {
117 setQualifyingLevel(nullptr);
118 this->show();
119 return true;
120 }
121 else if(k_event->key()==Qt::Key_Space || k_event->key()==Qt::Key_Backspace || k_event->key()==Qt::Key_Delete)
122 {
123 QTextCursor tc=code_field_txt->textCursor();
124 tc.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
125
126 /* Avoiding deleting text using backspace or delete if the current char is the completion trigger (.).
127 This will block the cursor and cause the list to stay in the current qualifying level */
128 if(completion_wgt->isVisible() &&
129 (k_event->key()==Qt::Key_Backspace || k_event->key()==Qt::Key_Delete) &&
130 tc.selectedText().contains(completion_trigger))
131 {
132 event->ignore();
133 return true;
134 }
135 else if(k_event->key()==Qt::Key_Space)
136 {
137 setQualifyingLevel(nullptr);
138
139 if(!persistent_chk->isChecked())
140 this->close();
141 }
142
143 if(persistent_chk->isChecked())
144 this->show();
145 }
146 }
147 }
148 else if(object==name_list)
149 {
150 if(k_event->key()==Qt::Key_Escape)
151 {
152 this->close();
153 return true;
154 }
155 //Filters the ENTER/RETURN press to close the code completion widget select the name
156 else if(k_event->key()==Qt::Key_Enter || k_event->key()==Qt::Key_Return)
157 {
158 if(!persistent_chk->isChecked())
159 this->selectItem();
160 else
161 {
162 //Forcing the line break on the code field when holding Control key and hit return/enter
163 if(k_event->modifiers()==Qt::ControlModifier)
164 {
165 QTextCursor cursor=code_field_txt->textCursor();
166 code_field_txt->insertPlainText(QChar(QChar::LineFeed));
167 cursor.movePosition(QTextCursor::Down);
168 code_field_txt->setTextCursor(cursor);
169 }
170 else
171 this->selectItem();
172
173 this->show();
174 }
175
176 return true;
177 }
178 //Filters other key press and redirects to the code input field
179 else if(k_event->key()!=Qt::Key_Up && k_event->key()!=Qt::Key_Down &&
180 k_event->key()!=Qt::Key_PageUp && k_event->key()!=Qt::Key_PageDown &&
181 k_event->key()!=Qt::Key_Home && k_event->key()!=Qt::Key_End &&
182 k_event->modifiers()!=Qt::AltModifier)
183 {
184
185 QCoreApplication::sendEvent(code_field_txt, k_event);
186 this->updateList();
187 return true;
188 }
189 }
190 }
191
192 return QWidget::eventFilter(object, event);
193 }
194
configureCompletion(DatabaseModel * db_model,SyntaxHighlighter * syntax_hl,const QString & keywords_grp)195 void CodeCompletionWidget::configureCompletion(DatabaseModel *db_model, SyntaxHighlighter *syntax_hl, const QString &keywords_grp)
196 {
197 map<QString, attribs_map> confs=GeneralConfigWidget::getConfigurationParams();
198
199 name_list->clear();
200 word.clear();
201 setQualifyingLevel(nullptr);
202 auto_triggered=false;
203 this->db_model=db_model;
204
205 if(confs[Attributes::Configuration][Attributes::CodeCompletion]==Attributes::True)
206 {
207 code_field_txt->installEventFilter(this);
208 name_list->installEventFilter(this);
209
210 if(syntax_hl && keywords.isEmpty())
211 {
212 //Get the keywords from the highlighter
213 vector<QRegExp> exprs=syntax_hl->getExpressions(keywords_grp);
214
215 while(!exprs.empty())
216 {
217 keywords.push_front(exprs.back().pattern());
218 exprs.pop_back();
219 }
220
221 completion_trigger=syntax_hl->getCompletionTrigger();
222 }
223 else
224 completion_trigger=QChar('.');
225
226 if(enable_snippets)
227 {
228 clearCustomItems();
229 insertCustomItems(SnippetsConfigWidget::getAllSnippetsAttribute(Attributes::Id),
230 SnippetsConfigWidget::getAllSnippetsAttribute(Attributes::Label),
231 QPixmap(PgModelerUiNs::getIconPath("codesnippet")));
232 }
233 }
234 else
235 {
236 code_field_txt->removeEventFilter(this);
237 name_list->removeEventFilter(this);
238 }
239 }
240
insertCustomItem(const QString & name,const QString & tooltip,const QPixmap & icon)241 void CodeCompletionWidget::insertCustomItem(const QString &name, const QString &tooltip, const QPixmap &icon)
242 {
243 if(!name.isEmpty())
244 {
245 QString item_name=name.simplified();
246 custom_items[item_name]=icon;
247 custom_items_tips[item_name]=tooltip;
248 }
249 }
250
insertCustomItems(const QStringList & names,const QStringList & tooltips,const QPixmap & icon)251 void CodeCompletionWidget::insertCustomItems(const QStringList &names, const QStringList &tooltips, const QPixmap &icon)
252 {
253 for(int i=0; i < names.size(); i++)
254 {
255 insertCustomItem(names[i], (i < tooltips.size() ? tooltips[i] : ""), icon);
256 }
257 }
258
insertCustomItems(const QStringList & names,const QString & tooltip,ObjectType obj_type)259 void CodeCompletionWidget::insertCustomItems(const QStringList &names, const QString &tooltip, ObjectType obj_type)
260 {
261 for(auto &name : names)
262 insertCustomItem(name, tooltip, QPixmap(PgModelerUiNs::getIconPath(obj_type)));
263 }
264
clearCustomItems()265 void CodeCompletionWidget::clearCustomItems()
266 {
267 custom_items.clear();
268 }
269
populateNameList(vector<BaseObject * > & objects,QString filter)270 void CodeCompletionWidget::populateNameList(vector<BaseObject *> &objects, QString filter)
271 {
272 QListWidgetItem *item=nullptr;
273 QString obj_name;
274 ObjectType obj_type;
275 QRegExp regexp(filter.remove('"') + QString("*"), Qt::CaseInsensitive, QRegExp::Wildcard);
276
277 name_list->clear();
278
279 for(unsigned i=0; i < objects.size(); i++)
280 {
281 obj_type=objects[i]->getObjectType();
282 obj_name.clear();
283
284 //Formatting the object name according to the object type
285 if(obj_type==ObjectType::Function)
286 {
287 dynamic_cast<Function *>(objects[i])->createSignature(false);
288 obj_name=dynamic_cast<Function *>(objects[i])->getSignature();
289 }
290 else if(obj_type==ObjectType::Operator)
291 obj_name=dynamic_cast<Operator *>(objects[i])->getSignature(false);
292 else
293 obj_name+=objects[i]->getName(false, false);
294
295 //The object will be inserted if its name matches the filter or there is no filter set
296 if(filter.isEmpty() || regexp.exactMatch(obj_name))
297 {
298 item=new QListWidgetItem(QPixmap(PgModelerUiNs::getIconPath(objects[i]->getSchemaName())), obj_name);
299 item->setToolTip(QString("%1 (%2)").arg(objects[i]->getName(true)).arg(objects[i]->getTypeName()));
300 item->setData(Qt::UserRole, QVariant::fromValue<void *>(objects[i]));
301 item->setToolTip(BaseObject::getTypeName(obj_type));
302 name_list->addItem(item);
303 }
304 }
305
306 name_list->sortItems();
307 }
308
show()309 void CodeCompletionWidget::show()
310 {
311 prev_txt_cur=code_field_txt->textCursor();
312 this->updateList();
313 completion_wgt->show();
314 this->showItemTooltip();
315 popup_timer.stop();
316 }
317
setQualifyingLevel(BaseObject * obj)318 void CodeCompletionWidget::setQualifyingLevel(BaseObject *obj)
319 {
320 if(!obj)
321 qualifying_level=-1;
322 else if(obj->getObjectType()==ObjectType::Schema)
323 qualifying_level=0;
324 else if(BaseTable::isBaseTable(obj->getObjectType()))
325 qualifying_level=1;
326 else
327 qualifying_level=2;
328
329 if(qualifying_level < 0)
330 {
331 sel_objects={ nullptr, nullptr, nullptr };
332 }
333 else
334 {
335 sel_objects[qualifying_level]=obj;
336 lvl_cur=code_field_txt->textCursor();
337 }
338 }
339
updateList()340 void CodeCompletionWidget::updateList()
341 {
342 QListWidgetItem *item=nullptr;
343 QString pattern;
344 QStringList list;
345 vector<BaseObject *> objects;
346 vector<ObjectType> types=BaseObject::getObjectTypes(false, { ObjectType::Textbox, ObjectType::Relationship, ObjectType::BaseRelationship });
347 QTextCursor tc;
348
349 name_list->clear();
350 word.clear();
351 new_txt_cur=tc=code_field_txt->textCursor();
352
353 /* Try to move the cursor to the previous char in order to check if the user is
354 calling the completion without an attached word */
355 tc.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
356
357 if(!tc.selectedText().trimmed().isEmpty() && new_txt_cur.movePosition(QTextCursor::WordLeft, QTextCursor::KeepAnchor))
358 {
359 //Move the cursor right before the trigger char in order to get the complete word
360 code_field_txt->setTextCursor(new_txt_cur);
361 word=code_field_txt->textCursor().selectedText();
362 word.remove('"');
363
364 //Case the completion was triggered using the trigger char
365 if(db_model && (auto_triggered || completion_trigger==word))
366 {
367 /* The completion will try to find a schema, table or view that matches the word,
368 if the serach returns one item the completion will start/continue an qualifying level */
369 new_txt_cur.movePosition(QTextCursor::WordLeft, QTextCursor::KeepAnchor);
370 code_field_txt->setTextCursor(new_txt_cur);
371 word=code_field_txt->textCursor().selectedText();
372 word.remove(completion_trigger);
373 word.remove('"');
374
375 objects=db_model->findObjects(word, { ObjectType::Schema, ObjectType::Table, ObjectType::ForeignTable, ObjectType::View }, false, false, true);
376
377 if(objects.size()==1)
378 setQualifyingLevel(objects[0]);
379 }
380
381 code_field_txt->setTextCursor(prev_txt_cur);
382 }
383
384 if(!word.isEmpty() && !auto_triggered)
385 pattern=QString("(^") + word.simplified() + QString(")");
386 else if(auto_triggered)
387 pattern=word;
388
389 if(db_model)
390 {
391 //Negative qualifying level means that user called the completion before a space (empty word)
392 if(qualifying_level < 0)
393 //The default behavior for this is to search all the objects on the model
394 objects=db_model->findObjects(pattern, types, false, !auto_triggered, auto_triggered);
395 else
396 {
397 QString left_word;
398
399 //Searching objects according to qualifying level.
400 tc=code_field_txt->textCursor();
401 tc.movePosition(QTextCursor::WordLeft, QTextCursor::KeepAnchor);
402
403 /* Retrieving the word at the left in order to compare it to the object's name at the current qualifying level,
404 if the word does not matches the object then children objects will not be retrieved */
405 if(tc.selectedText().contains('\"'))
406 {
407 tc.movePosition(QTextCursor::WordLeft, QTextCursor::KeepAnchor);
408 left_word=tc.selectedText();
409 left_word.remove('"');
410 }
411 else
412 left_word=tc.selectedText();
413
414 //Level 0 indicates that user selected a schema, so all objects of the schema are retrieved
415 if(qualifying_level==0 /*&& left_word==sel_objects[qualifying_level]->getName()*/)
416 objects=db_model->getObjects(sel_objects[qualifying_level]);
417
418 /* Level 1 indicates that user selected a table or view, so all child objects are retrieved.
419 If the current level is 1 and the table/view name isn't present then the children will not be listed */
420 else if(qualifying_level==1 /*&& left_word==sel_objects[qualifying_level]->getName()*/)
421 objects=dynamic_cast<BaseTable *>(sel_objects[qualifying_level])->getObjects();
422
423 /* If the current qualifying level and current word does retrieve any object as a fallback
424 we try to find any object in the model and reset the qualifying level */
425 else
426 {
427 objects=db_model->findObjects(pattern, types, false, !auto_triggered, auto_triggered);
428 setQualifyingLevel(nullptr);
429 }
430
431 /* If the typed word is equal to the current level object's name clear the order in order
432 to avoid listing the same object */
433 if(qualifying_level >=0 && word==sel_objects[qualifying_level]->getName())
434 word.clear();
435 }
436
437 populateNameList(objects, word);
438 }
439
440 /* List the keywords if the qualifying level is negative or the
441 completion wasn't triggered using the special char */
442 if(qualifying_level < 0 && !auto_triggered)
443 {
444 QRegExp regexp(pattern, Qt::CaseInsensitive);
445
446 list=keywords.filter(regexp);
447 for(int i=0; i < list.size(); i++)
448 {
449 item=new QListWidgetItem(QPixmap(PgModelerUiNs::getIconPath("keyword")), list[i]);
450 item->setToolTip(tr("SQL Keyword"));
451 name_list->addItem(item);
452 }
453
454 name_list->sortItems();
455
456 //If there are custom items, they wiill be placed at the very beggining of the list
457 if(!custom_items.empty())
458 {
459 QStringList list;
460 int row=0;
461 QListWidgetItem *item=nullptr;
462
463 for(auto &itr : custom_items)
464 {
465 if(itr.first.contains(regexp))
466 list.push_back(itr.first);
467 }
468
469 list.sort();
470 for(auto &item_name : list)
471 {
472 item=new QListWidgetItem(custom_items[item_name], item_name);
473 item->setToolTip(custom_items_tips[item_name]);
474 name_list->insertItem(row++, item);
475 }
476 }
477 }
478
479 if(name_list->count()==0)
480 {
481 name_list->addItem(tr("(no items found.)"));
482 name_list->item(0)->setFlags(Qt::NoItemFlags);
483 QToolTip::hideText();
484 }
485 else
486 name_list->item(0)->setSelected(true);
487
488 //Sets the list position right below of text cursor
489 completion_wgt->move(code_field_txt->mapToGlobal(code_field_txt->cursorRect().topLeft() + QPoint(0,20)));
490 name_list->setFocus();
491 }
492
selectItem()493 void CodeCompletionWidget::selectItem()
494 {
495 if(!name_list->selectedItems().isEmpty())
496 {
497 QListWidgetItem *item=name_list->selectedItems().at(0);
498 BaseObject *object=nullptr;
499 QTextCursor tc;
500
501 if(qualifying_level < 0)
502 code_field_txt->setTextCursor(new_txt_cur);
503
504 //If the selected item is a object (data not null)
505 if(!item->data(Qt::UserRole).isNull())
506 {
507 //Retrieve the object
508 object=reinterpret_cast<BaseObject *>(item->data(Qt::UserRole).value<void *>());
509
510 /* Move the cursor to the start of the word because all the chars will be replaced
511 with the object name */
512 prev_txt_cur.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
513
514 tc=prev_txt_cur;
515 tc.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
516
517 /* An small workaround to correctly write the object name in the current
518 qualifying level without remove the parent's name. This happens only when
519 the completion is marked as persistent */
520 if(persistent_chk->isChecked())
521 {
522 if(tc.selectedText().startsWith('.'))
523 {
524 prev_txt_cur.movePosition(QTextCursor::EndOfWord, QTextCursor::MoveAnchor);
525
526 if(!tc.selectedText().endsWith('.'))
527 prev_txt_cur.insertText(completion_trigger);
528 }
529 else if(qualifying_level >= 0 && !tc.selectedText().endsWith('.'))
530 {
531 prev_txt_cur.movePosition(QTextCursor::EndOfWord, QTextCursor::MoveAnchor);
532 prev_txt_cur.insertText(completion_trigger);
533 }
534 }
535 else if(tc.selectedText().contains('"'))
536 prev_txt_cur=tc;
537
538 code_field_txt->setTextCursor(prev_txt_cur);
539
540 insertObjectName(object);
541 setQualifyingLevel(object);
542 }
543 else
544 {
545 code_field_txt->insertPlainText(item->text() + QString(" "));
546 setQualifyingLevel(nullptr);
547 }
548
549 emit s_wordSelected(item->text());
550 }
551 else
552 setQualifyingLevel(nullptr);
553
554 name_list->clearSelection();
555 auto_triggered=false;
556
557 if(!persistent_chk->isChecked())
558 this->close();
559 }
560
showItemTooltip()561 void CodeCompletionWidget::showItemTooltip()
562 {
563 QListWidgetItem *item=name_list->currentItem();
564
565 if(item)
566 {
567 QPoint pos=name_list->mapToGlobal(QPoint(name_list->width(), name_list->geometry().top()));
568 QToolTip::showText(pos, item->toolTip());
569 }
570 }
571
close()572 void CodeCompletionWidget::close()
573 {
574 name_list->clearSelection();
575 completion_wgt->close();
576 auto_triggered=false;
577 }
578
insertObjectName(BaseObject * obj)579 void CodeCompletionWidget::insertObjectName(BaseObject *obj)
580 {
581 bool sch_qualified=!sel_objects[0],
582 modify_name=QApplication::keyboardModifiers()==Qt::AltModifier;
583 QString name=obj->getName(true, sch_qualified);
584 ObjectType obj_type=obj->getObjectType();
585 int move_cnt=0;
586
587
588 if(modify_name &&
589 (PhysicalTable::isPhysicalTable(obj_type) || TableObject::isTableObject(obj_type)))
590 {
591 if(PhysicalTable::isPhysicalTable(obj_type))
592 {
593 PhysicalTable *tab=dynamic_cast<PhysicalTable *>(obj);
594
595 name+=QString("(");
596 for(unsigned i=0; i < tab->getColumnCount(); i++)
597 name+=tab->getColumn(i)->getName(true) + QString(",");
598
599 name.remove(name.size()-1, 1);
600 name+=QString(")");
601 }
602 else
603 {
604 if(sel_objects[0])
605 move_cnt=2;
606 else
607 move_cnt=3;
608
609 lvl_cur.movePosition(QTextCursor::WordLeft, QTextCursor::KeepAnchor, move_cnt);
610 code_field_txt->setTextCursor(lvl_cur);
611 }
612 }
613 else if(obj_type==ObjectType::Function)
614 {
615 Function *func=dynamic_cast<Function *>(obj);
616 func->createSignature(true, sch_qualified);
617 name=func->getSignature();
618 }
619 else if(obj_type==ObjectType::Cast)
620 {
621 name.replace(',', QLatin1String(" AS "));
622 }
623 else if(obj_type==ObjectType::Aggregate)
624 {
625 Aggregate *agg;
626 agg=dynamic_cast<Aggregate *>(obj);
627 name+=QString("(");
628
629 if(agg->getDataTypeCount()==0)
630 name+='*';
631 else
632 {
633 for(unsigned i=0; i < agg->getDataTypeCount(); i++)
634 name+=~agg->getDataType(i) + ',';
635 name.remove(name.size()-1, 1);
636 }
637
638 name+=')';
639 }
640
641 code_field_txt->insertPlainText(name);
642 }
643